feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
@@ -1,55 +1,81 @@
import { canonicalize } from "../../../contracts/query-keys.js";
import { canonicalize } 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";
const REFERENCE_NAMESPACE = Object.freeze(["reference-resource", 1]);
const REFERENCE_NAMESPACE = Object.freeze(["reference-resource", 1] as const);
export const REFERENCE_RESOURCE_INVALIDATION_TOPIC =
defineQueryInvalidationTopic("qinv.01k10f7m3w9p6r2c8v5n4x");
export const referenceQueryKeys = Object.freeze({
all: () => REFERENCE_NAMESPACE,
list: (filters = {}) =>
list: (filters: Readonly<object> = {}) =>
Object.freeze([...REFERENCE_NAMESPACE, "list", canonicalize(filters)]),
/** @param {string} resourceId */
detail: (resourceId) =>
detail: (resourceId: string) =>
Object.freeze([...REFERENCE_NAMESPACE, "detail", String(resourceId)]),
});
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({
@@ -133,7 +159,7 @@ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
}),
}),
apiOperations: Object.freeze({
LIST_REFERENCE_RESOURCES: Object.freeze({
LIST_REFERENCE_RESOURCES: defineRestOperation({
method: "GET",
path: "/api/reference-resources",
operationId: "LIST_REFERENCE_RESOURCES",
@@ -145,8 +171,23 @@ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
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: Object.freeze({
CREATE_REFERENCE_RESOURCE: defineRestOperation({
method: "POST",
path: "/api/reference-resources",
operationId: "CREATE_REFERENCE_RESOURCE",
@@ -158,8 +199,23 @@ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
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: Object.freeze({
GET_REFERENCE_RESOURCE: defineRestOperation({
method: "GET",
path: "/api/reference-resources/{resourceId}",
operationId: "GET_REFERENCE_RESOURCE",
@@ -170,7 +226,22 @@ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
requestSource: "none",
requestSchema: "NoRequest",
responseSchema: "ReferenceResourcePayload",
owner: "feature-frontend-form-page-platform",
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,
}),
}),
queryRegistry: Object.freeze({
@@ -179,8 +250,10 @@ export const REFERENCE_FEATURE_CONTRACT = Object.freeze({
serialization: "canonical-object-order",
identity: "no-pii-token-or-raw-url",
invalidation: "reference resource namespace after successful mutation",
invalidationTopic: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
crossContext: "invalidate-only",
version: 1,
persistence: "disabled",
}),
}),
});
} as const);
@@ -1,7 +1,13 @@
import {
createReferenceResource,
type ReferenceResource,
} from "../domain/reference-resource.js";
} from "../domain/reference-resource.ts";
import {
mappingFailure,
mappingSuccess,
type MappingResult,
type InstalledBoundaryMapper,
} from "../../../contracts/boundary-mapper.ts";
export type ReferenceResourceView = Readonly<{
resourceId: string;
@@ -10,28 +16,43 @@ export type ReferenceResourceView = Readonly<{
optimistic?: boolean;
}>;
function mapReferenceDto(value: unknown): ReferenceResource {
function mapReferenceDto(value: unknown): MappingResult<ReferenceResource> {
if (!value || typeof value !== "object") {
throw new TypeError("Validated reference DTO is required");
return mappingFailure("MAPPING_INVARIANT_REJECTED");
}
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 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");
}
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[] {
): MappingResult<ReferenceResource | readonly ReferenceResource[]> {
if (operationId === "LIST_REFERENCE_RESOURCES") {
if (!Array.isArray(payload)) throw new TypeError("Expected a reference list");
return payload.map(mapReferenceDto);
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" ||
@@ -39,9 +60,32 @@ export function mapReferenceOperation(
) {
return mapReferenceDto(payload);
}
throw new TypeError(`No reference mapper registered for ${operationId}`);
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 {
@@ -19,4 +19,4 @@ export const REFERENCE_MESSAGE_CATALOGS = Object.freeze({
"route.REFERENCE_RESOURCE_STATUS.navigation": "Reference status",
"route.REFERENCE_RESOURCE_STATUS.title": "Reference resource status",
}),
});
} as const);
@@ -1,4 +1,5 @@
import { z } from "zod";
import type { RuntimeSchemaCodec } from "../../../contracts/schema-registry.ts";
export const referenceResourceListQuerySchema = z
.object({
@@ -26,18 +27,19 @@ export const referenceResourceParamsSchema = z
const referenceResourceDtoSchema = z
.object({
id: z.string().min(1),
name: z.string().min(1),
id: z.string().min(1).max(120),
name: z.string().min(1).max(240),
createdAt: z.string().datetime().optional(),
})
.strict();
.strip();
const payloadSchemas = {
ReferenceResourceListPayload: z.array(referenceResourceDtoSchema),
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
@@ -60,6 +62,40 @@ function project(result: z.ZodSafeParseResult<unknown>) {
});
}
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