chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type {
|
||||
CommandEffectDescriptor,
|
||||
HttpRequestProjection,
|
||||
InstalledContractContribution,
|
||||
InstalledHttpContract,
|
||||
RuntimeValidator,
|
||||
} from "../../../contracts/external-contract-runtime.ts";
|
||||
import { REFERENCE_FEATURE_ID } from "./reference-feature-contract.ts";
|
||||
import {
|
||||
referenceResourceListQuerySchema,
|
||||
referenceResourceParamsSchema,
|
||||
} from "./reference-schemas.ts";
|
||||
|
||||
/**
|
||||
* §4.8. The single `TEMPLATE_FIXTURE` contribution. It keeps the reference
|
||||
* HTTP vertical executable as deterministic template data and is excluded from
|
||||
* `contractSet`. A product feature MUST instead pin an external package and
|
||||
* import it from its own `contracts/<service>-contract-contribution.ts`.
|
||||
*/
|
||||
|
||||
function zodValidator<T>(
|
||||
schemaId: string,
|
||||
schema: z.ZodType<T>,
|
||||
): RuntimeValidator<T> {
|
||||
return Object.freeze({
|
||||
schemaId,
|
||||
safeParse(value: unknown) {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return Object.freeze({
|
||||
success: true as const,
|
||||
data: structuredClone(result.data),
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
success: false as const,
|
||||
issues: Object.freeze(
|
||||
result.error.issues.map((issue) =>
|
||||
Object.freeze({
|
||||
path: Object.freeze(
|
||||
issue.path.map((segment): string | number =>
|
||||
typeof segment === "number" ? segment : String(segment),
|
||||
),
|
||||
),
|
||||
code: String(issue.code),
|
||||
}),
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const referenceResourceDto = z
|
||||
.object({
|
||||
id: z.string().min(1).max(120),
|
||||
name: z.string().min(1).max(240),
|
||||
createdAt: z.string().min(1).optional(),
|
||||
})
|
||||
.strip();
|
||||
|
||||
const problemSchema = z
|
||||
.object({
|
||||
type: z.string().min(1).max(512),
|
||||
title: z.string().min(1).max(240),
|
||||
status: z.int().min(100).max(599),
|
||||
code: z.string().min(1).max(120).optional(),
|
||||
})
|
||||
.strip();
|
||||
|
||||
export type ReferenceProblem = z.output<typeof problemSchema>;
|
||||
|
||||
const PROBLEM_VALIDATOR = zodValidator("ReferenceProblem", problemSchema);
|
||||
|
||||
const createCommandSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
note: z.string().trim().max(500).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Fixture-side classifier standing in for the package-provided pure bounded
|
||||
* classifier. A status the fixture does not describe stays `MAYBE_APPLIED`.
|
||||
*/
|
||||
const CREATE_EFFECT: CommandEffectDescriptor<ReferenceProblem> = Object.freeze({
|
||||
successEffect: "APPLIED_CONFIRMED" as const,
|
||||
classifyProblem({ status }: Readonly<{ status: number; problem: ReferenceProblem }>) {
|
||||
if (status === 400 || status === 409 || status === 422) return "NOT_APPLIED";
|
||||
return "MAYBE_APPLIED";
|
||||
},
|
||||
});
|
||||
|
||||
const LIST_REFERENCE_RESOURCES: InstalledHttpContract<
|
||||
z.output<typeof referenceResourceListQuerySchema>,
|
||||
readonly z.output<typeof referenceResourceDto>[],
|
||||
ReferenceProblem
|
||||
> = Object.freeze({
|
||||
contract: Object.freeze({
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
method: "GET" as const,
|
||||
pathTemplate: "/api/reference-resources",
|
||||
inputValidator: zodValidator(
|
||||
"ReferenceResourceListQuery",
|
||||
referenceResourceListQuerySchema,
|
||||
),
|
||||
outputValidator: zodValidator(
|
||||
"ReferenceResourceListPayload",
|
||||
z.array(referenceResourceDto).max(100),
|
||||
),
|
||||
problemValidator: PROBLEM_VALIDATOR,
|
||||
acceptedStatuses: Object.freeze([200]),
|
||||
emptyBodyStatuses: Object.freeze([]),
|
||||
retrySemantics: "SAFE" as const,
|
||||
requestBody: "NONE" as const,
|
||||
responseBody: "REQUIRED_JSON" as const,
|
||||
commandRecovery: null,
|
||||
commandEffect: null,
|
||||
projectRequest(input: z.output<typeof referenceResourceListQuerySchema>) {
|
||||
const entries: (readonly [string, string])[] = [];
|
||||
if (input.cursor !== undefined) entries.push(["cursor", input.cursor]);
|
||||
entries.push(["limit", String(input.limit)]);
|
||||
for (const tag of input.tags ?? []) entries.push(["tags", tag]);
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({}),
|
||||
queryEntries: Object.freeze(entries),
|
||||
body: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
frontend: Object.freeze({
|
||||
policyId: "REFERENCE_LIST_V1",
|
||||
requestByteLimit: 0,
|
||||
responseByteLimit: 262_144,
|
||||
totalDeadlineMs: 10_000,
|
||||
retryBudget: 2 as const,
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
diagnosticsOperation: "reference.list",
|
||||
}),
|
||||
});
|
||||
|
||||
const GET_REFERENCE_RESOURCE: InstalledHttpContract<
|
||||
z.output<typeof referenceResourceParamsSchema>,
|
||||
z.output<typeof referenceResourceDto>,
|
||||
ReferenceProblem
|
||||
> = Object.freeze({
|
||||
contract: Object.freeze({
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
method: "GET" as const,
|
||||
pathTemplate: "/api/reference-resources/{resourceId}",
|
||||
inputValidator: zodValidator(
|
||||
"ReferenceResourceParams",
|
||||
referenceResourceParamsSchema,
|
||||
),
|
||||
outputValidator: zodValidator(
|
||||
"ReferenceResourcePayload",
|
||||
referenceResourceDto,
|
||||
),
|
||||
problemValidator: PROBLEM_VALIDATOR,
|
||||
acceptedStatuses: Object.freeze([200]),
|
||||
emptyBodyStatuses: Object.freeze([]),
|
||||
retrySemantics: "SAFE" as const,
|
||||
requestBody: "NONE" as const,
|
||||
responseBody: "REQUIRED_JSON" as const,
|
||||
commandRecovery: null,
|
||||
commandEffect: null,
|
||||
projectRequest(input: z.output<typeof referenceResourceParamsSchema>) {
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({ resourceId: input.resourceId }),
|
||||
queryEntries: Object.freeze([]),
|
||||
body: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
frontend: Object.freeze({
|
||||
policyId: "REFERENCE_DETAIL_V1",
|
||||
requestByteLimit: 0,
|
||||
responseByteLimit: 32_768,
|
||||
totalDeadlineMs: 10_000,
|
||||
retryBudget: 2 as const,
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
diagnosticsOperation: "reference.detail",
|
||||
}),
|
||||
});
|
||||
|
||||
const CREATE_REFERENCE_RESOURCE: InstalledHttpContract<
|
||||
z.output<typeof createCommandSchema>,
|
||||
z.output<typeof referenceResourceDto>,
|
||||
ReferenceProblem
|
||||
> = Object.freeze({
|
||||
contract: Object.freeze({
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
method: "POST" as const,
|
||||
pathTemplate: "/api/reference-resources",
|
||||
inputValidator: zodValidator(
|
||||
"CreateReferenceResourceCommand",
|
||||
createCommandSchema,
|
||||
),
|
||||
outputValidator: zodValidator(
|
||||
"ReferenceResourcePayload",
|
||||
referenceResourceDto,
|
||||
),
|
||||
problemValidator: PROBLEM_VALIDATOR,
|
||||
acceptedStatuses: Object.freeze([200, 201]),
|
||||
emptyBodyStatuses: Object.freeze([]),
|
||||
retrySemantics: "KEYED" as const,
|
||||
requestBody: "JSON" as const,
|
||||
responseBody: "REQUIRED_JSON" as const,
|
||||
commandRecovery: Object.freeze({
|
||||
mode: "IDEMPOTENCY_REPLAY" as const,
|
||||
operationIdentityField: "idempotencyKey",
|
||||
}),
|
||||
commandEffect: CREATE_EFFECT,
|
||||
projectRequest(input: z.output<typeof createCommandSchema>) {
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({}),
|
||||
queryEntries: Object.freeze([]),
|
||||
body: Object.freeze({ ...input }),
|
||||
});
|
||||
},
|
||||
}),
|
||||
frontend: Object.freeze({
|
||||
policyId: "REFERENCE_CREATE_V1",
|
||||
requestByteLimit: 32_768,
|
||||
responseByteLimit: 32_768,
|
||||
totalDeadlineMs: 10_000,
|
||||
/**
|
||||
* §8.3. A KEYED command has no automatic retry after a dispatched attempt
|
||||
* lost its response; the fixture therefore declares a zero budget rather
|
||||
* than relying on a runtime special case.
|
||||
*/
|
||||
retryBudget: 0 as const,
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
diagnosticsOperation: "reference.create",
|
||||
}),
|
||||
});
|
||||
|
||||
export const REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION: InstalledContractContribution =
|
||||
Object.freeze({
|
||||
contributionId: "reference-feature-http-v1",
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
source: Object.freeze({
|
||||
kind: "TEMPLATE_FIXTURE" as const,
|
||||
fixtureId: "REFERENCE_FEATURE_V1" as const,
|
||||
revision: 1 as const,
|
||||
}),
|
||||
http: Object.freeze([
|
||||
LIST_REFERENCE_RESOURCES,
|
||||
GET_REFERENCE_RESOURCE,
|
||||
CREATE_REFERENCE_RESOURCE,
|
||||
]) as readonly InstalledHttpContract<unknown, unknown, unknown>[],
|
||||
events: Object.freeze([]),
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import { defineQueryNamespaceIdentity } from "../../../contracts/query-keys.ts";
|
||||
import { defineQueryInvalidationTopic } from "../../../contracts/query-invalidation.ts";
|
||||
import { defineRestOperation } from "../../../contracts/api-operations.ts";
|
||||
import { REFERENCE_RUNTIME_SCHEMA_CODECS } from "./reference-schemas.ts";
|
||||
import { REFERENCE_BOUNDARY_MAPPERS } from "./reference-mapper.ts";
|
||||
|
||||
export const REFERENCE_FEATURE_ID = "reference-feature";
|
||||
export const REFERENCE_RESOURCE_QUERY_NAMESPACE = defineQueryNamespaceIdentity(
|
||||
"reference-resource",
|
||||
1,
|
||||
);
|
||||
export const REFERENCE_RESOURCE_INVALIDATION_TOPIC =
|
||||
defineQueryInvalidationTopic("qinv.01k10f7m3w9p6r2c8v5n4x");
|
||||
|
||||
export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
runtimeSchemas: REFERENCE_RUNTIME_SCHEMA_CODECS,
|
||||
mappers: REFERENCE_BOUNDARY_MAPPERS,
|
||||
schemas: Object.freeze({
|
||||
ReferenceResourceParams: Object.freeze({
|
||||
schemaId: "ReferenceResourceParams",
|
||||
boundary: "route-params",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "REQUEST",
|
||||
unknownFieldPolicy: "REJECT_UNKNOWN",
|
||||
}),
|
||||
ReferenceResourceListQuery: Object.freeze({
|
||||
schemaId: "ReferenceResourceListQuery",
|
||||
boundary: "route-search-api-request",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "REQUEST",
|
||||
unknownFieldPolicy: "REJECT_UNKNOWN",
|
||||
}),
|
||||
CreateReferenceResourceCommand: Object.freeze({
|
||||
schemaId: "CreateReferenceResourceCommand",
|
||||
boundary: "api-request",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "REQUEST",
|
||||
unknownFieldPolicy: "REJECT_UNKNOWN",
|
||||
}),
|
||||
NoRequest: Object.freeze({
|
||||
schemaId: "NoRequest",
|
||||
boundary: "api-request",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "REQUEST",
|
||||
unknownFieldPolicy: "REJECT_UNKNOWN",
|
||||
}),
|
||||
ReferenceResourceListPayload: Object.freeze({
|
||||
schemaId: "ReferenceResourceListPayload",
|
||||
boundary: "api-response",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "RESPONSE",
|
||||
unknownFieldPolicy: "STRIP_UNKNOWN",
|
||||
}),
|
||||
ReferenceResourcePayload: Object.freeze({
|
||||
schemaId: "ReferenceResourcePayload",
|
||||
boundary: "api-response",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
runtime: "zod",
|
||||
schemaVersion: 1,
|
||||
direction: "RESPONSE",
|
||||
unknownFieldPolicy: "STRIP_UNKNOWN",
|
||||
}),
|
||||
}),
|
||||
routes: Object.freeze({
|
||||
REFERENCE_RESOURCE_LIST: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
path: "/examples/reference-resources",
|
||||
paramsSchema: null,
|
||||
searchSchema: "ReferenceResourceListQuery",
|
||||
access: "session-required",
|
||||
loadingSurface: "reference-resource-list",
|
||||
errorSurface: "feature-boundary",
|
||||
chunkId: "route-reference-resources",
|
||||
title: "Reference feature",
|
||||
navigationLabel: "Reference feature",
|
||||
navigationOrder: 50,
|
||||
}),
|
||||
REFERENCE_RESOURCE_DETAIL: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
path: "/examples/reference-resources/:resourceId",
|
||||
paramsSchema: "ReferenceResourceParams",
|
||||
searchSchema: null,
|
||||
access: "session-required",
|
||||
loadingSurface: "reference-resource-detail",
|
||||
errorSurface: "feature-boundary",
|
||||
chunkId: "route-reference-resource-detail",
|
||||
title: "Reference detail",
|
||||
navigationLabel: null,
|
||||
navigationOrder: null,
|
||||
}),
|
||||
REFERENCE_RESOURCE_FORM: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_FORM",
|
||||
path: "/examples/reference-resources/new",
|
||||
paramsSchema: null,
|
||||
searchSchema: null,
|
||||
access: "session-required",
|
||||
loadingSurface: "reference-resource-form",
|
||||
errorSurface: "feature-boundary",
|
||||
chunkId: "route-reference-resource-form",
|
||||
title: "Reference form",
|
||||
navigationLabel: null,
|
||||
navigationOrder: null,
|
||||
}),
|
||||
REFERENCE_RESOURCE_STATUS: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_STATUS",
|
||||
path: "/examples/reference-resources/status",
|
||||
paramsSchema: null,
|
||||
searchSchema: null,
|
||||
access: "session-required",
|
||||
loadingSurface: "reference-resource-status",
|
||||
errorSurface: "feature-boundary",
|
||||
chunkId: "route-reference-resource-status",
|
||||
title: "Reference status",
|
||||
navigationLabel: null,
|
||||
navigationOrder: null,
|
||||
}),
|
||||
}),
|
||||
routeRuntimeContracts: Object.freeze({
|
||||
REFERENCE_RESOURCE_LIST: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
moduleId: "reference-resource-page",
|
||||
paramsCodec: "none",
|
||||
searchCodec: "ReferenceResourceListQuery",
|
||||
}),
|
||||
REFERENCE_RESOURCE_DETAIL: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
moduleId: "reference-resource-detail-page",
|
||||
paramsCodec: "ReferenceResourceParams",
|
||||
searchCodec: "none",
|
||||
}),
|
||||
REFERENCE_RESOURCE_FORM: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_FORM",
|
||||
moduleId: "reference-resource-form-page",
|
||||
paramsCodec: "none",
|
||||
searchCodec: "none",
|
||||
}),
|
||||
REFERENCE_RESOURCE_STATUS: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_STATUS",
|
||||
moduleId: "reference-resource-status-page",
|
||||
paramsCodec: "none",
|
||||
searchCodec: "none",
|
||||
}),
|
||||
}),
|
||||
apiOperations: Object.freeze({
|
||||
LIST_REFERENCE_RESOURCES: defineRestOperation({
|
||||
method: "GET",
|
||||
path: "/api/reference-resources",
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
auth: "external-session",
|
||||
timeoutMs: null,
|
||||
idempotency: "safe",
|
||||
retry: "runtime",
|
||||
requestSource: "search",
|
||||
requestSchema: "ReferenceResourceListQuery",
|
||||
responseSchema: "ReferenceResourceListPayload",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
contractVersion: 2,
|
||||
protocol: "REST",
|
||||
semantics: "QUERY",
|
||||
replayPolicy: "SAFE",
|
||||
idempotencyKeyPolicy: "NONE",
|
||||
mapperId: "ReferenceResourceListMapper",
|
||||
successStatuses: [200],
|
||||
responseMediaTypes: ["application/json"],
|
||||
maxResponseBytes: 262_144,
|
||||
providerId: "PRIMARY_API",
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
csrfProfileId: "NO_CSRF_BEARER",
|
||||
pathSchema: "NoRequest",
|
||||
pathParameterNames: [],
|
||||
maxEncodedSearchBytes: 4_096,
|
||||
}),
|
||||
CREATE_REFERENCE_RESOURCE: defineRestOperation({
|
||||
method: "POST",
|
||||
path: "/api/reference-resources",
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
auth: "external-session",
|
||||
timeoutMs: null,
|
||||
idempotency: "keyed",
|
||||
retry: "runtime",
|
||||
requestSource: "body",
|
||||
requestSchema: "CreateReferenceResourceCommand",
|
||||
responseSchema: "ReferenceResourcePayload",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
contractVersion: 2,
|
||||
protocol: "REST",
|
||||
semantics: "COMMAND",
|
||||
replayPolicy: "KEYED_COMMAND",
|
||||
idempotencyKeyPolicy: "REQUIRED",
|
||||
mapperId: "ReferenceResourceMapper",
|
||||
successStatuses: [200, 201],
|
||||
responseMediaTypes: ["application/json"],
|
||||
maxResponseBytes: 32_768,
|
||||
providerId: "PRIMARY_API",
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
csrfProfileId: "NO_CSRF_BEARER",
|
||||
pathSchema: "NoRequest",
|
||||
pathParameterNames: [],
|
||||
maxEncodedSearchBytes: 0,
|
||||
}),
|
||||
GET_REFERENCE_RESOURCE: defineRestOperation({
|
||||
method: "GET",
|
||||
path: "/api/reference-resources/{resourceId}",
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
auth: "external-session",
|
||||
timeoutMs: null,
|
||||
idempotency: "safe",
|
||||
retry: "runtime",
|
||||
requestSource: "none",
|
||||
requestSchema: "NoRequest",
|
||||
responseSchema: "ReferenceResourcePayload",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
contractVersion: 2,
|
||||
protocol: "REST",
|
||||
semantics: "QUERY",
|
||||
replayPolicy: "SAFE",
|
||||
idempotencyKeyPolicy: "NONE",
|
||||
mapperId: "ReferenceResourceMapper",
|
||||
successStatuses: [200],
|
||||
responseMediaTypes: ["application/json"],
|
||||
maxResponseBytes: 32_768,
|
||||
providerId: "PRIMARY_API",
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
csrfProfileId: "NO_CSRF_BEARER",
|
||||
pathSchema: "ReferenceResourceParams",
|
||||
pathParameterNames: ["resourceId"],
|
||||
maxEncodedSearchBytes: 0,
|
||||
}),
|
||||
}),
|
||||
invalidation: Object.freeze({
|
||||
topics: Object.freeze([REFERENCE_RESOURCE_INVALIDATION_TOPIC]),
|
||||
namespaces: Object.freeze([REFERENCE_RESOURCE_QUERY_NAMESPACE]),
|
||||
edges: Object.freeze([
|
||||
Object.freeze({
|
||||
topicId: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
|
||||
namespace: REFERENCE_RESOURCE_QUERY_NAMESPACE,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
topicVersions: Object.freeze([
|
||||
Object.freeze({
|
||||
topicId: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
|
||||
topicVersion: 1,
|
||||
}),
|
||||
]),
|
||||
} as const);
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
createReferenceResource,
|
||||
type ReferenceResource,
|
||||
} from "../domain/reference-resource.ts";
|
||||
import {
|
||||
mappingFailure,
|
||||
mappingSuccess,
|
||||
type MappingResult,
|
||||
type InstalledBoundaryMapper,
|
||||
} from "../../../contracts/boundary-mapper.ts";
|
||||
|
||||
export type ReferenceResourceView = Readonly<{
|
||||
resourceId: string;
|
||||
title: string;
|
||||
createdAt: string | null;
|
||||
optimistic?: boolean;
|
||||
}>;
|
||||
|
||||
function mapReferenceDto(value: unknown): MappingResult<ReferenceResource> {
|
||||
if (!value || typeof value !== "object") {
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
const dto = value as Record<string, unknown>;
|
||||
if (typeof dto.id !== "string" || typeof dto.name !== "string") {
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
try {
|
||||
return mappingSuccess(
|
||||
createReferenceResource({
|
||||
id: dto.id,
|
||||
displayName: dto.name,
|
||||
createdAt: typeof dto.createdAt === "string" ? dto.createdAt : null,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
}
|
||||
|
||||
export function mapReferenceOperation(
|
||||
operationId: string,
|
||||
payload: unknown,
|
||||
): MappingResult<ReferenceResource | readonly ReferenceResource[]> {
|
||||
if (operationId === "LIST_REFERENCE_RESOURCES") {
|
||||
if (!Array.isArray(payload)) {
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
if (payload.length > 100) return mappingFailure("OUTPUT_LIMIT_EXCEEDED");
|
||||
const output: ReferenceResource[] = [];
|
||||
for (const item of payload) {
|
||||
const mapped = mapReferenceDto(item);
|
||||
if (!mapped.ok) return mapped;
|
||||
output.push(mapped.value);
|
||||
}
|
||||
return mappingSuccess(Object.freeze(output));
|
||||
}
|
||||
if (
|
||||
operationId === "CREATE_REFERENCE_RESOURCE" ||
|
||||
operationId === "GET_REFERENCE_RESOURCE"
|
||||
) {
|
||||
return mapReferenceDto(payload);
|
||||
}
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
|
||||
export const REFERENCE_BOUNDARY_MAPPERS = Object.freeze({
|
||||
ReferenceResourceListMapper: Object.freeze({
|
||||
mapperId: "ReferenceResourceListMapper",
|
||||
mapperVersion: 1,
|
||||
inputSchemaId: "ReferenceResourceListPayload",
|
||||
outputContractId: "ReferenceResourceList",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
maxOutputItems: 100,
|
||||
map: (input: unknown) =>
|
||||
mapReferenceOperation("LIST_REFERENCE_RESOURCES", input),
|
||||
}),
|
||||
ReferenceResourceMapper: Object.freeze({
|
||||
mapperId: "ReferenceResourceMapper",
|
||||
mapperVersion: 1,
|
||||
inputSchemaId: "ReferenceResourcePayload",
|
||||
outputContractId: "ReferenceResource",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
maxOutputItems: 1,
|
||||
map: (input: unknown) =>
|
||||
mapReferenceOperation("GET_REFERENCE_RESOURCE", input),
|
||||
}),
|
||||
} satisfies Readonly<Record<string, InstalledBoundaryMapper>>);
|
||||
|
||||
export function toReferenceView(
|
||||
resource: ReferenceResource,
|
||||
): ReferenceResourceView {
|
||||
return Object.freeze({
|
||||
resourceId: resource.id,
|
||||
title: resource.displayName,
|
||||
createdAt: resource.createdAt,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const REFERENCE_MESSAGE_CATALOGS = Object.freeze({
|
||||
"ko-KR": Object.freeze({
|
||||
"route.REFERENCE_RESOURCE_LIST.navigation": "Reference feature",
|
||||
"route.REFERENCE_RESOURCE_LIST.title": "Reference resources",
|
||||
"route.REFERENCE_RESOURCE_DETAIL.navigation": "Reference detail",
|
||||
"route.REFERENCE_RESOURCE_DETAIL.title": "Reference resource detail",
|
||||
"route.REFERENCE_RESOURCE_FORM.navigation": "Reference form",
|
||||
"route.REFERENCE_RESOURCE_FORM.title": "Create reference resource",
|
||||
"route.REFERENCE_RESOURCE_STATUS.navigation": "Reference status",
|
||||
"route.REFERENCE_RESOURCE_STATUS.title": "Reference resource status",
|
||||
}),
|
||||
"en-US": Object.freeze({
|
||||
"route.REFERENCE_RESOURCE_LIST.navigation": "Reference feature",
|
||||
"route.REFERENCE_RESOURCE_LIST.title": "Reference resources",
|
||||
"route.REFERENCE_RESOURCE_DETAIL.navigation": "Reference detail",
|
||||
"route.REFERENCE_RESOURCE_DETAIL.title": "Reference resource detail",
|
||||
"route.REFERENCE_RESOURCE_FORM.navigation": "Reference form",
|
||||
"route.REFERENCE_RESOURCE_FORM.title": "Create reference resource",
|
||||
"route.REFERENCE_RESOURCE_STATUS.navigation": "Reference status",
|
||||
"route.REFERENCE_RESOURCE_STATUS.title": "Reference resource status",
|
||||
}),
|
||||
} as const);
|
||||
@@ -0,0 +1,111 @@
|
||||
import { z } from "zod";
|
||||
import type { RuntimeSchemaCodec } from "../../../contracts/schema-registry.ts";
|
||||
|
||||
export const referenceResourceListQuerySchema = z
|
||||
.object({
|
||||
cursor: z.string().min(1).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
tags: z
|
||||
.preprocess(
|
||||
(value) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: Array.isArray(value)
|
||||
? value
|
||||
: [value],
|
||||
z.array(z.string().trim().min(1)),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const referenceResourceParamsSchema = z
|
||||
.object({
|
||||
resourceId: z.string().trim().min(1).max(120),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const referenceResourceDtoSchema = z
|
||||
.object({
|
||||
id: z.string().min(1).max(120),
|
||||
name: z.string().min(1).max(240),
|
||||
createdAt: z.string().datetime().optional(),
|
||||
})
|
||||
.strip();
|
||||
|
||||
const payloadSchemas = {
|
||||
ReferenceResourceListPayload: z.array(referenceResourceDtoSchema).max(100),
|
||||
ReferenceResourcePayload: referenceResourceDtoSchema,
|
||||
} satisfies Record<string, z.ZodType>;
|
||||
|
||||
const requestSchemas = {
|
||||
ReferenceResourceParams: referenceResourceParamsSchema,
|
||||
ReferenceResourceListQuery: referenceResourceListQuerySchema,
|
||||
NoRequest: z.object({}).strict(),
|
||||
CreateReferenceResourceCommand: z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
note: z.string().trim().max(500).optional(),
|
||||
})
|
||||
.strict(),
|
||||
} satisfies Record<string, z.ZodType>;
|
||||
|
||||
function project(result: z.ZodSafeParseResult<unknown>) {
|
||||
return result.success
|
||||
? ({ success: true as const, data: structuredClone(result.data) })
|
||||
: ({
|
||||
success: false as const,
|
||||
issues: result.error.issues.map((issue) => ({
|
||||
path: issue.path.join("."),
|
||||
code: issue.code,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function codec(
|
||||
schemaId: string,
|
||||
schema: z.ZodType,
|
||||
): RuntimeSchemaCodec {
|
||||
return Object.freeze({
|
||||
schemaId,
|
||||
parse: (value: unknown) => project(schema.safeParse(value)),
|
||||
});
|
||||
}
|
||||
|
||||
export const REFERENCE_RUNTIME_SCHEMA_CODECS = Object.freeze({
|
||||
ReferenceResourceParams: codec(
|
||||
"ReferenceResourceParams",
|
||||
referenceResourceParamsSchema,
|
||||
),
|
||||
ReferenceResourceListQuery: codec(
|
||||
"ReferenceResourceListQuery",
|
||||
referenceResourceListQuerySchema,
|
||||
),
|
||||
NoRequest: codec("NoRequest", requestSchemas.NoRequest),
|
||||
CreateReferenceResourceCommand: codec(
|
||||
"CreateReferenceResourceCommand",
|
||||
requestSchemas.CreateReferenceResourceCommand,
|
||||
),
|
||||
ReferenceResourceListPayload: codec(
|
||||
"ReferenceResourceListPayload",
|
||||
payloadSchemas.ReferenceResourceListPayload,
|
||||
),
|
||||
ReferenceResourcePayload: codec(
|
||||
"ReferenceResourcePayload",
|
||||
payloadSchemas.ReferenceResourcePayload,
|
||||
),
|
||||
});
|
||||
|
||||
export function validateReferencePayload(schemaId: string, value: unknown) {
|
||||
const schema = payloadSchemas[schemaId as keyof typeof payloadSchemas];
|
||||
return schema
|
||||
? project(schema.safeParse(value))
|
||||
: { success: false as const, issues: [{ path: "", code: "SCHEMA_NOT_REGISTERED" }] };
|
||||
}
|
||||
|
||||
export function validateReferenceRequest(schemaId: string, value: unknown) {
|
||||
const schema = requestSchemas[schemaId as keyof typeof requestSchemas];
|
||||
return schema
|
||||
? project(schema.safeParse(value))
|
||||
: { success: false as const, issues: [{ path: "", code: "SCHEMA_NOT_REGISTERED" }] };
|
||||
}
|
||||
Reference in New Issue
Block a user