feat: add removable reference feature vertical slice
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { canonicalize } from "../../../contracts/query-keys.js";
|
||||
|
||||
export const REFERENCE_FEATURE_ID = "reference-feature";
|
||||
const REFERENCE_NAMESPACE = Object.freeze(["reference-resource", 1]);
|
||||
|
||||
export const referenceQueryKeys = Object.freeze({
|
||||
all: () => REFERENCE_NAMESPACE,
|
||||
list: (filters = {}) =>
|
||||
Object.freeze([...REFERENCE_NAMESPACE, "list", canonicalize(filters)]),
|
||||
});
|
||||
|
||||
export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
routes: Object.freeze({
|
||||
REFERENCE_RESOURCE_LIST: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
path: "/examples/reference-resources",
|
||||
paramsSchema: null,
|
||||
searchSchema: "ReferenceResourceListQuery",
|
||||
access: "integration-defined",
|
||||
loadingSurface: "reference-resource-list",
|
||||
errorSurface: "feature-boundary",
|
||||
chunkId: "route-reference-resources",
|
||||
title: "Reference feature",
|
||||
navigationLabel: "Reference feature",
|
||||
navigationOrder: 50,
|
||||
}),
|
||||
}),
|
||||
routeRuntimeContracts: Object.freeze({
|
||||
REFERENCE_RESOURCE_LIST: Object.freeze({
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
moduleId: "reference-resource-page",
|
||||
paramsCodec: "none",
|
||||
searchCodec: "ReferenceResourceListQuery",
|
||||
}),
|
||||
}),
|
||||
apiOperations: Object.freeze({
|
||||
LIST_REFERENCE_RESOURCES: Object.freeze({
|
||||
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",
|
||||
}),
|
||||
CREATE_REFERENCE_RESOURCE: Object.freeze({
|
||||
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",
|
||||
}),
|
||||
}),
|
||||
queryRegistry: Object.freeze({
|
||||
REFERENCE_RESOURCE: Object.freeze({
|
||||
namespace: REFERENCE_NAMESPACE,
|
||||
serialization: "canonical-object-order",
|
||||
identity: "no-pii-token-or-raw-url",
|
||||
invalidation: "reference resource namespace after successful mutation",
|
||||
version: 1,
|
||||
persistence: "disabled",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
createReferenceResource,
|
||||
type ReferenceResource,
|
||||
} from "../domain/reference-resource.js";
|
||||
|
||||
export type ReferenceResourceView = Readonly<{
|
||||
resourceId: string;
|
||||
title: string;
|
||||
createdAtLabel: string | null;
|
||||
optimistic?: boolean;
|
||||
}>;
|
||||
|
||||
function mapReferenceDto(value: unknown): ReferenceResource {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new TypeError("Validated reference DTO is required");
|
||||
}
|
||||
const dto = value as Record<string, unknown>;
|
||||
if (typeof dto.id !== "string" || typeof dto.name !== "string") {
|
||||
throw new TypeError("Validated reference DTO invariants were breached");
|
||||
}
|
||||
return createReferenceResource({
|
||||
id: dto.id,
|
||||
displayName: dto.name,
|
||||
createdAt: typeof dto.createdAt === "string" ? dto.createdAt : null,
|
||||
});
|
||||
}
|
||||
|
||||
export function mapReferenceOperation(
|
||||
operationId: string,
|
||||
payload: unknown,
|
||||
): ReferenceResource | readonly ReferenceResource[] {
|
||||
if (operationId === "LIST_REFERENCE_RESOURCES") {
|
||||
if (!Array.isArray(payload)) throw new TypeError("Expected a reference list");
|
||||
return payload.map(mapReferenceDto);
|
||||
}
|
||||
if (operationId === "CREATE_REFERENCE_RESOURCE") {
|
||||
return mapReferenceDto(payload);
|
||||
}
|
||||
throw new TypeError(`No reference mapper registered for ${operationId}`);
|
||||
}
|
||||
|
||||
export function toReferenceView(
|
||||
resource: ReferenceResource,
|
||||
formatDate: (value: Date) => string = (value) =>
|
||||
new Intl.DateTimeFormat("ko-KR").format(value),
|
||||
): ReferenceResourceView {
|
||||
return Object.freeze({
|
||||
resourceId: resource.id,
|
||||
title: resource.displayName,
|
||||
createdAtLabel: resource.createdAt
|
||||
? formatDate(new Date(resource.createdAt))
|
||||
: null,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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();
|
||||
|
||||
const referenceResourceDtoSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
createdAt: z.string().datetime().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const payloadSchemas = {
|
||||
ReferenceResourceListPayload: z.array(referenceResourceDtoSchema),
|
||||
ReferenceResourcePayload: referenceResourceDtoSchema,
|
||||
} satisfies Record<string, z.ZodType>;
|
||||
|
||||
const requestSchemas = {
|
||||
ReferenceResourceListQuery: referenceResourceListQuerySchema,
|
||||
CreateReferenceResourceCommand: z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
})
|
||||
.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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
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