refactor: 프론트 템플릿 리펙토링
This commit is contained in:
@@ -24,11 +24,12 @@ augmentation으로 `"reference-feature": ReferenceFeatureInput`을 기여하므
|
||||
`ERROR_REGISTRY` key에서 파생된 닫힌 vocabulary이며, transport 호환 이름인
|
||||
`ApiFailure`는 같은 type의 alias다.
|
||||
|
||||
HTTP adapter의 operation map은 operation ID마다 허용된 route ID, request shape와
|
||||
성공 value type을 함께 묶는다. raw HTTP executor의 성공값은 Zod request/response
|
||||
검증과 feature mapper를 통과한 뒤 operation별 runtime result guard에서 typed
|
||||
executor로 승격된다. 따라서 gateway 메서드는 개별 응답 cast 없이 정확한 결과를
|
||||
반환하고, operation/route/request 조합 오류는 typecheck에서 차단된다.
|
||||
HTTP adapter는 `src/adapters/http/feature-http-binding.ts`의 capability-specific
|
||||
typed binding을 사용한다. feature는 operation ID, route ID, request type, 성공
|
||||
value type, mapper만 소유한다. timeout/cancellation/auth/transport/contract
|
||||
violation을 `AppFailure`로 정규화하는 책임은 reusable HTTP capability가 소유한다.
|
||||
따라서 gateway 메서드는 transport outcome이나 runtime result guard를 반복 구현하지
|
||||
않고도 정확한 결과 타입을 반환한다.
|
||||
|
||||
route codec과 URL builder는 `src/presentation/routes/route-codecs.ts`, route type은
|
||||
`route-contract.ts`, React context/provider/hook은 `route-input.tsx`가 각각
|
||||
@@ -39,12 +40,16 @@ route codec과 URL builder는 `src/presentation/routes/route-codecs.ts`, route t
|
||||
|
||||
## 설치 지점
|
||||
|
||||
- 직렬화 계약: `src/features/installed-feature-contracts.ts`
|
||||
- component/codec: `src/features/installed-feature-runtimes.tsx`
|
||||
- bootstrap input 조립: `src/features/installed-feature-adapters.ts`
|
||||
feature가 실제 contribution을 소유한다.
|
||||
|
||||
새 기능도 이 세 지점에 contribution을 합성하되 feature ID를 generic application,
|
||||
router나 HTTP client에 하드코딩하지 않는다.
|
||||
- contract: `reference-feature-contract.ts`
|
||||
- component/codec: `REFERENCE_FEATURE_RUNTIME_CONTRIBUTION`
|
||||
- bootstrap input: `REFERENCE_FEATURE_ADAPTER_CONTRIBUTION`
|
||||
|
||||
중앙 파일인 `installed-feature-contracts.ts`,
|
||||
`installed-feature-runtimes.tsx`, `installed-feature-adapters.ts`는 선택된
|
||||
contribution을 합치는 역할만 한다. 새 기능의 내부 조립 규칙을 중앙 catalog에
|
||||
추가하지 않는다.
|
||||
|
||||
## 검증과 제거
|
||||
|
||||
|
||||
@@ -1,195 +1,41 @@
|
||||
import type { Result } from "../../../application/result.ts";
|
||||
import type { ApiFailure, FailureKind } from "../../../contracts/errors.ts";
|
||||
import type { FailureEffectCertainty } from "../../../contracts/errors.ts";
|
||||
import type { MutationIntent } from "../../../contracts/mutation-intent.ts";
|
||||
import {
|
||||
createFailure,
|
||||
kindForStatus,
|
||||
} from "../../../contracts/errors.ts";
|
||||
import type { HttpExecutionOutcome } from "../../../adapters/http/index.ts";
|
||||
createFeatureHttpBinding,
|
||||
type InstalledHttpOperationExecutor,
|
||||
} from "../../../adapters/http/index.ts";
|
||||
import { createReferenceFeatureInput } from "../application/reference-feature-api.ts";
|
||||
import {
|
||||
REFERENCE_FEATURE_ID,
|
||||
} from "../contracts/reference-feature-contract.ts";
|
||||
import { mapReferenceOperation } from "../contracts/reference-mapper.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
type RawReferenceHttpExecutor,
|
||||
type ReferenceHttpRequest,
|
||||
type ReferenceOperationId,
|
||||
REFERENCE_HTTP_OPERATIONS,
|
||||
} from "./reference-http-gateway.ts";
|
||||
|
||||
export type InstalledContractOperationExecutor = Readonly<{
|
||||
execute(
|
||||
operationId: string,
|
||||
input: unknown,
|
||||
context: Readonly<{
|
||||
routeId: string;
|
||||
signal?: AbortSignal;
|
||||
intent?: MutationIntent;
|
||||
}>,
|
||||
): Promise<HttpExecutionOutcome<unknown, unknown>>;
|
||||
}>;
|
||||
export type InstalledContractOperationExecutor =
|
||||
InstalledHttpOperationExecutor;
|
||||
|
||||
/**
|
||||
* The installed feature consumes the composed external-contract operation
|
||||
* registry through one descriptor-driven executor. Legacy ApiOperation/schema
|
||||
* registries are intentionally absent from this production composition seam.
|
||||
* Feature-owned composition seam.
|
||||
*
|
||||
* The feature contributes typed operation descriptors and its application
|
||||
* gateway. HTTP lifecycle/error normalization stays in the reusable capability
|
||||
* binding rather than being repeated by every product feature.
|
||||
*/
|
||||
export function createReferenceFeatureInstalledInput(context: Readonly<{
|
||||
contractOperations: InstalledContractOperationExecutor;
|
||||
contractOperations: InstalledHttpOperationExecutor;
|
||||
}>) {
|
||||
const rawHttp: RawReferenceHttpExecutor = Object.freeze({
|
||||
async execute(request) {
|
||||
const operationId = request.operationId;
|
||||
const input = inputFor(request);
|
||||
const signal = "signal" in request ? request.signal : undefined;
|
||||
const intent = "intent" in request ? request.intent : undefined;
|
||||
const outcome = await context.contractOperations.execute(
|
||||
operationId,
|
||||
input,
|
||||
{
|
||||
// §7.4. The gateway owns the low-cardinality route identity; losing
|
||||
// it here is what made every V3 diagnostic unattributable.
|
||||
routeId: request.routeId,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
...(intent === undefined ? {} : { intent }),
|
||||
},
|
||||
);
|
||||
return projectExecutionOutcome(operationId, outcome);
|
||||
},
|
||||
});
|
||||
const http = createFeatureHttpBinding(
|
||||
context.contractOperations,
|
||||
REFERENCE_HTTP_OPERATIONS,
|
||||
);
|
||||
|
||||
return Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
input: createReferenceFeatureInput(createReferenceHttpGateway(rawHttp)),
|
||||
input: createReferenceFeatureInput(createReferenceHttpGateway(http)),
|
||||
});
|
||||
}
|
||||
|
||||
function inputFor(
|
||||
request: ReferenceHttpRequest<ReferenceOperationId>,
|
||||
): unknown {
|
||||
switch (request.operationId) {
|
||||
case "LIST_REFERENCE_RESOURCES":
|
||||
return request.searchParams;
|
||||
case "CREATE_REFERENCE_RESOURCE":
|
||||
return request.body;
|
||||
case "GET_REFERENCE_RESOURCE":
|
||||
return request.pathParams;
|
||||
}
|
||||
}
|
||||
|
||||
function projectExecutionOutcome(
|
||||
operationId: ReferenceOperationId,
|
||||
outcome: HttpExecutionOutcome<unknown, unknown>,
|
||||
): Result<unknown, ApiFailure> {
|
||||
switch (outcome.kind) {
|
||||
case "SUCCESS": {
|
||||
const mapped = mapReferenceOperation(operationId, outcome.value);
|
||||
return mapped.ok
|
||||
? Object.freeze({ ok: true as const, value: mapped.value })
|
||||
: failure(
|
||||
"MAPPING_CONTRACT_VIOLATION",
|
||||
operationId,
|
||||
mapped.code,
|
||||
{ effect: outcome.effect },
|
||||
);
|
||||
}
|
||||
case "PROBLEM":
|
||||
return failure(
|
||||
kindForStatus(outcome.metadata.status),
|
||||
operationId,
|
||||
"CONTRACT_PROBLEM",
|
||||
{ httpStatus: outcome.metadata.status, effect: outcome.effect },
|
||||
);
|
||||
case "UNAUTHENTICATED":
|
||||
return failure("AUTH_REQUIRED", operationId, "UNAUTHENTICATED", {
|
||||
effect: outcome.effect,
|
||||
});
|
||||
case "FORBIDDEN":
|
||||
return failure("FORBIDDEN", operationId, "FORBIDDEN", {
|
||||
effect: outcome.effect,
|
||||
});
|
||||
case "RATE_LIMITED":
|
||||
return failure("RATE_LIMITED", operationId, "RATE_LIMITED", {
|
||||
...(outcome.retryAfterMs === undefined
|
||||
? {}
|
||||
: { retryAfterMs: outcome.retryAfterMs }),
|
||||
effect: outcome.effect,
|
||||
});
|
||||
case "CANCELLED":
|
||||
return failure("REQUEST_ABORTED", operationId, "REQUEST_ABORTED", {
|
||||
effect: outcome.effect,
|
||||
});
|
||||
case "AUTH_INTEGRATION_FAILURE":
|
||||
// §7.7. A configuration or collaborator breach, not a session state, so
|
||||
// it must not drive the re-authentication surface.
|
||||
return failure(
|
||||
"AUTH_INTEGRATION_FAILURE",
|
||||
operationId,
|
||||
outcome.reason,
|
||||
{ effect: outcome.effect },
|
||||
);
|
||||
case "TRANSPORT_FAILURE":
|
||||
return failure(
|
||||
outcome.failure.kind === "TIMEOUT"
|
||||
? "REQUEST_TIMEOUT"
|
||||
: outcome.failure.kind === "ABORTED_BY_SCOPE"
|
||||
? "SCOPE_GENERATION_CHANGED"
|
||||
: "NETWORK_UNREACHABLE",
|
||||
operationId,
|
||||
outcome.failure.kind,
|
||||
{ effect: outcome.effect },
|
||||
);
|
||||
case "CONTRACT_VIOLATION":
|
||||
return failure(
|
||||
failureKindForViolation(outcome.violation.kind),
|
||||
operationId,
|
||||
outcome.violation.kind,
|
||||
{ effect: outcome.effect },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function failureKindForViolation(
|
||||
violation: Extract<
|
||||
HttpExecutionOutcome<unknown, unknown>,
|
||||
{ kind: "CONTRACT_VIOLATION" }
|
||||
>["violation"]["kind"],
|
||||
): FailureKind {
|
||||
switch (violation) {
|
||||
case "CONTENT_TYPE_MISMATCH":
|
||||
return "CONTENT_TYPE_MISMATCH";
|
||||
case "RESPONSE_TOO_LARGE":
|
||||
return "RESPONSE_BODY_LIMIT";
|
||||
case "UTF8_INVALID":
|
||||
case "JSON_INVALID":
|
||||
return "MALFORMED_JSON";
|
||||
case "MAPPING_CONTRACT_VIOLATION":
|
||||
return "MAPPING_CONTRACT_VIOLATION";
|
||||
case "SCOPE_FENCED":
|
||||
return "SCOPE_GENERATION_CHANGED";
|
||||
case "SUCCESS_SCHEMA_INVALID":
|
||||
case "PROBLEM_SCHEMA_INVALID":
|
||||
case "VALIDATOR_RUNTIME_FAILURE":
|
||||
return "SCHEMA_MISMATCH";
|
||||
default:
|
||||
return "ENVELOPE_MISMATCH";
|
||||
}
|
||||
}
|
||||
|
||||
function failure(
|
||||
kind: FailureKind,
|
||||
operationId: string,
|
||||
code: string,
|
||||
details: Readonly<{
|
||||
httpStatus?: number;
|
||||
retryAfterMs?: number;
|
||||
effect?: FailureEffectCertainty;
|
||||
}> = {},
|
||||
): Result<never, ApiFailure> {
|
||||
return Object.freeze({
|
||||
ok: false as const,
|
||||
error: createFailure(kind, operationId, 0, { code, ...details }),
|
||||
});
|
||||
}
|
||||
export const REFERENCE_FEATURE_ADAPTER_CONTRIBUTION = Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
createInput: createReferenceFeatureInstalledInput,
|
||||
});
|
||||
|
||||
@@ -1,160 +1,65 @@
|
||||
import type { Result } from "../../../application/result.ts";
|
||||
import {
|
||||
createFailure,
|
||||
type ApiFailure,
|
||||
} from "../../../contracts/errors.ts";
|
||||
defineFeatureHttpOperation,
|
||||
type FeatureHttpBinding,
|
||||
} from "../../../adapters/http/index.ts";
|
||||
import type {
|
||||
ReferenceCreateCommand,
|
||||
ReferenceGateway,
|
||||
ReferenceListFilters,
|
||||
} from "../application/reference-feature-api.ts";
|
||||
import type { ReferenceResource } from "../domain/reference-resource.ts";
|
||||
import type { MutationIntent } from "../../../contracts/mutation-intent.ts";
|
||||
import {
|
||||
mapReferenceResourceListPayload,
|
||||
mapReferenceResourcePayload,
|
||||
} from "../contracts/reference-mapper.ts";
|
||||
|
||||
type ReferenceOperationMap = Readonly<{
|
||||
LIST_REFERENCE_RESOURCES: Readonly<{
|
||||
request: Readonly<{
|
||||
operationId: "LIST_REFERENCE_RESOURCES";
|
||||
routeId: "REFERENCE_RESOURCE_LIST";
|
||||
searchParams: ReferenceListFilters;
|
||||
signal?: AbortSignal;
|
||||
}>;
|
||||
value: readonly ReferenceResource[];
|
||||
}>;
|
||||
CREATE_REFERENCE_RESOURCE: Readonly<{
|
||||
request: Readonly<{
|
||||
operationId: "CREATE_REFERENCE_RESOURCE";
|
||||
routeId: "REFERENCE_RESOURCE_LIST";
|
||||
body: ReferenceCreateCommand;
|
||||
signal?: AbortSignal;
|
||||
intent?: MutationIntent;
|
||||
}>;
|
||||
value: ReferenceResource;
|
||||
}>;
|
||||
GET_REFERENCE_RESOURCE: Readonly<{
|
||||
request: Readonly<{
|
||||
operationId: "GET_REFERENCE_RESOURCE";
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL";
|
||||
pathParams: Readonly<{ resourceId: string }>;
|
||||
signal?: AbortSignal;
|
||||
}>;
|
||||
value: ReferenceResource;
|
||||
}>;
|
||||
}>;
|
||||
export const REFERENCE_HTTP_OPERATIONS = Object.freeze({
|
||||
LIST_REFERENCE_RESOURCES: defineFeatureHttpOperation<
|
||||
ReferenceListFilters,
|
||||
readonly ReferenceResource[]
|
||||
>({
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
mapSuccess: mapReferenceResourceListPayload,
|
||||
}),
|
||||
CREATE_REFERENCE_RESOURCE: defineFeatureHttpOperation<
|
||||
ReferenceCreateCommand,
|
||||
ReferenceResource
|
||||
>({
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
mapSuccess: mapReferenceResourcePayload,
|
||||
}),
|
||||
GET_REFERENCE_RESOURCE: defineFeatureHttpOperation<
|
||||
Readonly<{ resourceId: string }>,
|
||||
ReferenceResource
|
||||
>({
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
mapSuccess: mapReferenceResourcePayload,
|
||||
}),
|
||||
} as const);
|
||||
|
||||
export type ReferenceOperationId = keyof ReferenceOperationMap;
|
||||
|
||||
export type ReferenceHttpRequest<
|
||||
OperationId extends ReferenceOperationId,
|
||||
> = ReferenceOperationMap[OperationId]["request"];
|
||||
|
||||
export type ReferenceHttpResult<
|
||||
OperationId extends ReferenceOperationId,
|
||||
> = Result<ReferenceOperationMap[OperationId]["value"], ApiFailure>;
|
||||
|
||||
export type RawReferenceHttpExecutor = Readonly<{
|
||||
execute(
|
||||
request: ReferenceHttpRequest<ReferenceOperationId>,
|
||||
): Promise<Result<unknown, ApiFailure>>;
|
||||
}>;
|
||||
export type ReferenceHttpBinding = FeatureHttpBinding<
|
||||
typeof REFERENCE_HTTP_OPERATIONS
|
||||
>;
|
||||
|
||||
export function createReferenceHttpGateway(
|
||||
http: RawReferenceHttpExecutor,
|
||||
http: ReferenceHttpBinding,
|
||||
): ReferenceGateway {
|
||||
return Object.freeze({
|
||||
async list(
|
||||
filters: ReferenceListFilters,
|
||||
context?: Readonly<{ signal?: AbortSignal }>,
|
||||
) {
|
||||
const result = await http.execute({
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
searchParams: filters,
|
||||
signal: context?.signal,
|
||||
});
|
||||
return projectListResult(result);
|
||||
list(filters, context) {
|
||||
return http.execute("LIST_REFERENCE_RESOURCES", filters, context);
|
||||
},
|
||||
async create(
|
||||
command: ReferenceCreateCommand,
|
||||
context?: Readonly<{
|
||||
signal?: AbortSignal;
|
||||
intent?: MutationIntent;
|
||||
}>,
|
||||
) {
|
||||
const result = await http.execute({
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
body: command,
|
||||
signal: context?.signal,
|
||||
intent: context?.intent,
|
||||
});
|
||||
return projectResourceResult("CREATE_REFERENCE_RESOURCE", result);
|
||||
create(command, context) {
|
||||
return http.execute("CREATE_REFERENCE_RESOURCE", command, context);
|
||||
},
|
||||
async get(
|
||||
resourceId: string,
|
||||
context?: Readonly<{ signal?: AbortSignal }>,
|
||||
) {
|
||||
const result = await http.execute({
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
pathParams: { resourceId },
|
||||
signal: context?.signal,
|
||||
});
|
||||
return projectResourceResult("GET_REFERENCE_RESOURCE", result);
|
||||
get(resourceId, context) {
|
||||
return http.execute(
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
Object.freeze({ resourceId }),
|
||||
context,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function projectListResult(
|
||||
result: Result<unknown, ApiFailure>,
|
||||
): ReferenceHttpResult<"LIST_REFERENCE_RESOURCES"> {
|
||||
if (!result.ok) return result;
|
||||
if (isReferenceResourceList(result.value)) {
|
||||
return { ok: true, value: result.value };
|
||||
}
|
||||
return typedResultFailure("LIST_REFERENCE_RESOURCES");
|
||||
}
|
||||
|
||||
function projectResourceResult(
|
||||
operationId:
|
||||
| "CREATE_REFERENCE_RESOURCE"
|
||||
| "GET_REFERENCE_RESOURCE",
|
||||
result: Result<unknown, ApiFailure>,
|
||||
): Result<ReferenceResource, ApiFailure> {
|
||||
if (!result.ok) return result;
|
||||
if (isReferenceResource(result.value)) {
|
||||
return { ok: true, value: result.value };
|
||||
}
|
||||
return typedResultFailure(operationId);
|
||||
}
|
||||
|
||||
function typedResultFailure(operationId: ReferenceOperationId) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: createFailure(
|
||||
"MAPPING_CONTRACT_VIOLATION",
|
||||
operationId,
|
||||
0,
|
||||
{ code: "BOUND_RESULT_TYPE_MISMATCH" },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function isReferenceResourceList(
|
||||
value: unknown,
|
||||
): value is readonly ReferenceResource[] {
|
||||
return Array.isArray(value) && value.every(isReferenceResource);
|
||||
}
|
||||
|
||||
function isReferenceResource(value: unknown): value is ReferenceResource {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const candidate = value as Readonly<Record<string, unknown>>;
|
||||
return (
|
||||
typeof candidate.id === "string" &&
|
||||
candidate.id.length > 0 &&
|
||||
typeof candidate.displayName === "string" &&
|
||||
candidate.displayName.length > 0 &&
|
||||
(candidate.createdAt === null ||
|
||||
typeof candidate.createdAt === "string")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Result } from "../../../application/result.ts";
|
||||
import type { Result } from "../../../contracts/result.ts";
|
||||
import type {} from "../../../application/ports/in/application-api.ts";
|
||||
import {
|
||||
toReferenceView,
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
import {
|
||||
mappingFailure,
|
||||
mappingSuccess,
|
||||
type MappingResult,
|
||||
type InstalledBoundaryMapper,
|
||||
type MappingResult,
|
||||
} from "../../../contracts/boundary-mapper.ts";
|
||||
|
||||
export type ReferenceResourceView = Readonly<{
|
||||
@@ -16,7 +16,9 @@ export type ReferenceResourceView = Readonly<{
|
||||
optimistic?: boolean;
|
||||
}>;
|
||||
|
||||
function mapReferenceDto(value: unknown): MappingResult<ReferenceResource> {
|
||||
export function mapReferenceResourcePayload(
|
||||
value: unknown,
|
||||
): MappingResult<ReferenceResource> {
|
||||
if (!value || typeof value !== "object") {
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
@@ -37,28 +39,40 @@ function mapReferenceDto(value: unknown): MappingResult<ReferenceResource> {
|
||||
}
|
||||
}
|
||||
|
||||
export function mapReferenceResourceListPayload(
|
||||
payload: unknown,
|
||||
): MappingResult<readonly ReferenceResource[]> {
|
||||
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 = mapReferenceResourcePayload(item);
|
||||
if (!mapped.ok) return mapped;
|
||||
output.push(mapped.value);
|
||||
}
|
||||
return mappingSuccess(Object.freeze(output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry-facing compatibility projection. Feature adapters should prefer the
|
||||
* operation-specific mapper functions above so their result type is exact.
|
||||
*/
|
||||
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));
|
||||
return mapReferenceResourceListPayload(payload);
|
||||
}
|
||||
if (
|
||||
operationId === "CREATE_REFERENCE_RESOURCE" ||
|
||||
operationId === "GET_REFERENCE_RESOURCE"
|
||||
) {
|
||||
return mapReferenceDto(payload);
|
||||
return mapReferenceResourcePayload(payload);
|
||||
}
|
||||
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
||||
}
|
||||
@@ -71,8 +85,7 @@ export const REFERENCE_BOUNDARY_MAPPERS = Object.freeze({
|
||||
outputContractId: "ReferenceResourceList",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
maxOutputItems: 100,
|
||||
map: (input: unknown) =>
|
||||
mapReferenceOperation("LIST_REFERENCE_RESOURCES", input),
|
||||
map: mapReferenceResourceListPayload,
|
||||
}),
|
||||
ReferenceResourceMapper: Object.freeze({
|
||||
mapperId: "ReferenceResourceMapper",
|
||||
@@ -81,8 +94,7 @@ export const REFERENCE_BOUNDARY_MAPPERS = Object.freeze({
|
||||
outputContractId: "ReferenceResource",
|
||||
owner: "feature-frontend-reference-feature-vertical-slice",
|
||||
maxOutputItems: 1,
|
||||
map: (input: unknown) =>
|
||||
mapReferenceOperation("GET_REFERENCE_RESOURCE", input),
|
||||
map: mapReferenceResourcePayload,
|
||||
}),
|
||||
} satisfies Readonly<Record<string, InstalledBoundaryMapper>>);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { lazy } from "react";
|
||||
|
||||
import { REFERENCE_FEATURE_ID } from "../contracts/reference-feature-contract.ts";
|
||||
import {
|
||||
referenceResourceListQuerySchema,
|
||||
referenceResourceParamsSchema,
|
||||
@@ -28,3 +29,9 @@ export const REFERENCE_FEATURE_ROUTE_RUNTIME = {
|
||||
Component: lazy(() => import("./reference-resource-status-page.tsx")),
|
||||
}),
|
||||
} as const;
|
||||
|
||||
export const REFERENCE_FEATURE_RUNTIME_CONTRIBUTION = Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
routeCodecs: REFERENCE_FEATURE_ROUTE_CODECS,
|
||||
routeRuntime: REFERENCE_FEATURE_ROUTE_RUNTIME,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useApplication } from "../../../presentation/providers/application-provider.tsx";
|
||||
import { useApplicationFeature } from "../../../presentation/providers/application-provider.tsx";
|
||||
import {
|
||||
useApplicationMutation,
|
||||
useApplicationQuery,
|
||||
@@ -50,8 +50,32 @@ function measureResourceList(
|
||||
return { itemCount: views.length, estimatedBytes };
|
||||
}
|
||||
|
||||
const REFERENCE_CREATE_MUTATION_POLICY = Object.freeze({
|
||||
definitionId: "reference-resource-create-v1",
|
||||
definitionVersion: 1,
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
requiresIdempotencyKey: true,
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
duplicatePolicy: "REJECT_WHILE_ACTIVE" as const,
|
||||
invalidate: Object.freeze([REFERENCE_RESOURCE_INVALIDATION_TOPIC]),
|
||||
});
|
||||
|
||||
type ReferenceCreateMutationBinding = Pick<
|
||||
BoundMutation<ReferenceCreateCommand, ReferenceResourceView>,
|
||||
"scope" | "execute"
|
||||
>;
|
||||
|
||||
function bindReferenceCreateMutation(
|
||||
binding: ReferenceCreateMutationBinding,
|
||||
): BoundMutation<ReferenceCreateCommand, ReferenceResourceView> {
|
||||
return Object.freeze({
|
||||
...REFERENCE_CREATE_MUTATION_POLICY,
|
||||
...binding,
|
||||
});
|
||||
}
|
||||
|
||||
export function useReferenceFeatureInput(): ReferenceFeatureInput {
|
||||
return useApplication().features.get(REFERENCE_FEATURE_ID);
|
||||
return useApplicationFeature(REFERENCE_FEATURE_ID);
|
||||
}
|
||||
|
||||
export function useReferenceDetail(resourceId: string) {
|
||||
@@ -82,20 +106,10 @@ export function useReferenceDetail(resourceId: string) {
|
||||
export function useReferenceCreate() {
|
||||
const input = useReferenceFeatureInput();
|
||||
const scope = useServerStateScope();
|
||||
const mutation: BoundMutation<
|
||||
ReferenceCreateCommand,
|
||||
ReferenceResourceView
|
||||
> = {
|
||||
definitionId: "reference-resource-create-v1",
|
||||
definitionVersion: 1,
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
requiresIdempotencyKey: true,
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
duplicatePolicy: "REJECT_WHILE_ACTIVE",
|
||||
const mutation = bindReferenceCreateMutation({
|
||||
scope,
|
||||
execute: input.createResource,
|
||||
invalidate: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
|
||||
};
|
||||
});
|
||||
return useApplicationMutation(mutation);
|
||||
}
|
||||
|
||||
@@ -123,16 +137,11 @@ export function useReferenceFeature() {
|
||||
scope,
|
||||
),
|
||||
);
|
||||
const mutation = useApplicationMutation({
|
||||
definitionId: "reference-resource-create-v1",
|
||||
definitionVersion: 1,
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
requiresIdempotencyKey: true,
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
duplicatePolicy: "REJECT_WHILE_ACTIVE",
|
||||
scope,
|
||||
execute: input.createResource,
|
||||
invalidate: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
|
||||
});
|
||||
const mutation = useApplicationMutation(
|
||||
bindReferenceCreateMutation({
|
||||
scope,
|
||||
execute: input.createResource,
|
||||
}),
|
||||
);
|
||||
return Object.freeze({ filters, query, mutation });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user