feat: 기능 추가 과정중
This commit is contained in:
@@ -12,13 +12,34 @@ reference implementation이다.
|
||||
- `presentation`: route input을 query/form controller로 연결하는 inbound
|
||||
adapter, 독립 form schema/command mapper와 list/detail/form/status page
|
||||
|
||||
generic application은 `features.get(featureId)` catalog만 제공한다. feature hook이
|
||||
자신의 input shape를 확인하며 page는 HTTP client, storage, auth owner, output
|
||||
port나 TanStack API를 직접 import하지 않는다.
|
||||
generic application은 비어 있는 `ApplicationFeatureInputs`와 typed
|
||||
`features.has/get` registry만 소유한다. 이 feature의 application API가 module
|
||||
augmentation으로 `"reference-feature": ReferenceFeatureInput`을 기여하므로
|
||||
등록되지 않은 ID와 잘못된 input shape는 typecheck에서 거절된다. feature hook은
|
||||
별도 cast나 runtime shape 확인 없이 정확한 input type을 받는다. 다만 동적 호출로
|
||||
설치되지 않은 ID가 들어오는 경우를 위해 `get`의 runtime guard도 유지한다.
|
||||
|
||||
예측 가능한 실패는 `src/application/result.ts`의 공통
|
||||
`Result<Value, Failure = AppFailure>`로 반환한다. `AppFailure.kind`는
|
||||
`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에서 차단된다.
|
||||
|
||||
route codec과 URL builder는 `src/presentation/routes/route-codecs.ts`, route type은
|
||||
`route-contract.ts`, React context/provider/hook은 `route-input.tsx`가 각각
|
||||
소유한다. reference page는 `app-router.tsx`를 역참조하지 않고 좁은
|
||||
`useRouteInput` 경계만 사용하므로 lazy route와 router 사이의 순환 의존을 만들지
|
||||
않는다. page는 HTTP client, storage, auth owner, output port나 TanStack API를
|
||||
직접 import하지 않는다.
|
||||
|
||||
## 설치 지점
|
||||
|
||||
- 직렬화 계약: `src/features/installed-feature-contracts.js`
|
||||
- 직렬화 계약: `src/features/installed-feature-contracts.ts`
|
||||
- component/codec: `src/features/installed-feature-runtimes.tsx`
|
||||
- bootstrap input 조립: `src/features/installed-feature-adapters.ts`
|
||||
|
||||
@@ -35,5 +56,13 @@ corepack pnpm test:sample-removal
|
||||
첫 명령은 URL filter와 query key/HTTP request의 동일성, schema/mapper, 모든
|
||||
query/mutation/form 상태와 production composition을 검증한다. 두 번째 명령은 임시
|
||||
복제본에서 이 source/test 디렉터리를 제거하고 installed catalog를 빈 목록으로
|
||||
재생성한 뒤 typecheck, architecture, registry, unit/integration, home smoke,
|
||||
production build와 source/built fixture ID 잔여 0개를 검사한다.
|
||||
재생성한다. feature 소유 coverage include, risk-policy 행과 test-evidence
|
||||
contribution도 제거한 뒤 typecheck, architecture, registry, unit/integration,
|
||||
coverage, source evidence, home smoke, production build와 source/built fixture ID
|
||||
잔여 0개를 검사한다. generic feature registry의 성공 경로는 reference와 무관한
|
||||
unit test가 소유하므로 feature 삭제 후에도 공통 boundary coverage가 유지된다.
|
||||
|
||||
CI의 negative type fixture는 잘못된 feature ID/input shape, registry에 없는
|
||||
failure kind, operation과 route ID의 불일치가 실제로 컴파일 실패하는지도
|
||||
검증한다. Architecture fixture는 허용 edge뿐 아니라 unresolved import, 금지 계층
|
||||
edge와 TypeScript 순환 의존을 각각 거절해야 통과한다.
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
import { createReferenceFeatureInput } from "../application/reference-feature-api.js";
|
||||
import type { ApiOperation } from "../../../contracts/api-operations.ts";
|
||||
import { createReferenceFeatureInput } from "../application/reference-feature-api.ts";
|
||||
import {
|
||||
REFERENCE_FEATURE_CONTRACT,
|
||||
REFERENCE_FEATURE_ID,
|
||||
} from "../contracts/reference-feature-contract.js";
|
||||
import { mapReferenceOperation } from "../contracts/reference-mapper.js";
|
||||
} from "../contracts/reference-feature-contract.ts";
|
||||
import {
|
||||
validateReferencePayload,
|
||||
validateReferenceRequest,
|
||||
} from "../contracts/reference-schemas.js";
|
||||
import { createReferenceHttpGateway } from "./reference-http-gateway.js";
|
||||
mapWithBoundaryRegistry,
|
||||
type MappingResult,
|
||||
} from "../../../contracts/boundary-mapper.ts";
|
||||
import { validateWithRuntimeSchemaRegistry } from "../../../contracts/schema-registry.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
type RawReferenceHttpExecutor,
|
||||
} from "./reference-http-gateway.ts";
|
||||
|
||||
type HttpContract = Readonly<{
|
||||
getOperation(operationId: string): unknown;
|
||||
validatePayload: typeof validateReferencePayload;
|
||||
validateRequest: typeof validateReferenceRequest;
|
||||
mapPayload: typeof mapReferenceOperation;
|
||||
getOperation(operationId: string): ApiOperation;
|
||||
validatePayload(schemaId: string, value: unknown): ReturnType<
|
||||
typeof validateWithRuntimeSchemaRegistry
|
||||
>;
|
||||
validateRequest(schemaId: string, value: unknown): ReturnType<
|
||||
typeof validateWithRuntimeSchemaRegistry
|
||||
>;
|
||||
validatePath(schemaId: string, value: unknown): ReturnType<
|
||||
typeof validateWithRuntimeSchemaRegistry
|
||||
>;
|
||||
mapPayload(operationId: string, payload: unknown): MappingResult<unknown>;
|
||||
}>;
|
||||
|
||||
type HttpExecutor = Parameters<typeof createReferenceHttpGateway>[0];
|
||||
type HttpExecutor = RawReferenceHttpExecutor;
|
||||
|
||||
export function createReferenceFeatureInstalledInput(context: Readonly<{
|
||||
createHttpClient(contract: HttpContract): HttpExecutor;
|
||||
}>) {
|
||||
const operations =
|
||||
REFERENCE_FEATURE_CONTRACT.apiOperations as Readonly<Record<string, unknown>>;
|
||||
const http = context.createHttpClient({
|
||||
REFERENCE_FEATURE_CONTRACT.apiOperations as Readonly<Record<string, ApiOperation>>;
|
||||
const schemas = REFERENCE_FEATURE_CONTRACT.runtimeSchemas;
|
||||
const mappers = REFERENCE_FEATURE_CONTRACT.mappers;
|
||||
const validate = (schemaId: string, value: unknown) =>
|
||||
validateWithRuntimeSchemaRegistry(schemaId, value, schemas);
|
||||
const rawHttp = context.createHttpClient({
|
||||
getOperation(operationId) {
|
||||
const operation = operations[operationId];
|
||||
if (!operation) {
|
||||
@@ -32,12 +47,19 @@ export function createReferenceFeatureInstalledInput(context: Readonly<{
|
||||
}
|
||||
return operation;
|
||||
},
|
||||
validatePayload: validateReferencePayload,
|
||||
validateRequest: validateReferenceRequest,
|
||||
mapPayload: mapReferenceOperation,
|
||||
validatePayload: validate,
|
||||
validateRequest: validate,
|
||||
validatePath: validate,
|
||||
mapPayload(operationId, payload) {
|
||||
const operation = operations[operationId];
|
||||
if (!operation?.mapperId) {
|
||||
return { ok: false, code: "MAPPING_INVARIANT_REJECTED" };
|
||||
}
|
||||
return mapWithBoundaryRegistry(operation.mapperId, payload, mappers);
|
||||
},
|
||||
});
|
||||
return Object.freeze({
|
||||
featureId: REFERENCE_FEATURE_ID,
|
||||
input: createReferenceFeatureInput(createReferenceHttpGateway(http)),
|
||||
input: createReferenceFeatureInput(createReferenceHttpGateway(rawHttp)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
import type { ApiFailure } from "../../../contracts/errors.js";
|
||||
import type { Result } from "../../../application/result.ts";
|
||||
import {
|
||||
createFailure,
|
||||
type ApiFailure,
|
||||
} from "../../../contracts/errors.ts";
|
||||
import type {
|
||||
ReferenceCreateCommand,
|
||||
ReferenceGateway,
|
||||
ReferenceListFilters,
|
||||
} from "../application/reference-feature-api.js";
|
||||
import type { ReferenceResource } from "../domain/reference-resource.js";
|
||||
} from "../application/reference-feature-api.ts";
|
||||
import type { ReferenceResource } from "../domain/reference-resource.ts";
|
||||
|
||||
type HttpResult =
|
||||
| Readonly<{ ok: true; value: unknown }>
|
||||
| Readonly<{ ok: false; error: ApiFailure }>;
|
||||
|
||||
type HttpExecutor = Readonly<{
|
||||
execute(
|
||||
type ReferenceOperationMap = Readonly<{
|
||||
LIST_REFERENCE_RESOURCES: Readonly<{
|
||||
request: Readonly<{
|
||||
operationId: string;
|
||||
routeId: string;
|
||||
pathParams?: Record<string, string | number>;
|
||||
searchParams?: unknown;
|
||||
body?: unknown;
|
||||
operationId: "LIST_REFERENCE_RESOURCES";
|
||||
routeId: "REFERENCE_RESOURCE_LIST";
|
||||
searchParams: ReferenceListFilters;
|
||||
signal?: AbortSignal;
|
||||
}>,
|
||||
): Promise<HttpResult>;
|
||||
}>;
|
||||
value: readonly ReferenceResource[];
|
||||
}>;
|
||||
CREATE_REFERENCE_RESOURCE: Readonly<{
|
||||
request: Readonly<{
|
||||
operationId: "CREATE_REFERENCE_RESOURCE";
|
||||
routeId: "REFERENCE_RESOURCE_LIST";
|
||||
body: ReferenceCreateCommand;
|
||||
}>;
|
||||
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 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 function createReferenceHttpGateway(
|
||||
http: HttpExecutor,
|
||||
http: RawReferenceHttpExecutor,
|
||||
): ReferenceGateway {
|
||||
return Object.freeze({
|
||||
async list(
|
||||
@@ -36,22 +69,15 @@ export function createReferenceHttpGateway(
|
||||
searchParams: filters,
|
||||
signal: context?.signal,
|
||||
});
|
||||
return result.ok
|
||||
? {
|
||||
ok: true as const,
|
||||
value: result.value as readonly ReferenceResource[],
|
||||
}
|
||||
: result;
|
||||
return projectListResult(result);
|
||||
},
|
||||
async create(command: Readonly<{ name: string; note?: string }>) {
|
||||
async create(command: ReferenceCreateCommand) {
|
||||
const result = await http.execute({
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
body: command,
|
||||
});
|
||||
return result.ok
|
||||
? { ok: true as const, value: result.value as ReferenceResource }
|
||||
: result;
|
||||
return projectResourceResult("CREATE_REFERENCE_RESOURCE", result);
|
||||
},
|
||||
async get(
|
||||
resourceId: string,
|
||||
@@ -63,9 +89,61 @@ export function createReferenceHttpGateway(
|
||||
pathParams: { resourceId },
|
||||
signal: context?.signal,
|
||||
});
|
||||
return result.ok
|
||||
? { ok: true as const, value: result.value as ReferenceResource }
|
||||
: result;
|
||||
return projectResourceResult("GET_REFERENCE_RESOURCE", result);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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,9 +1,10 @@
|
||||
import type { ApiFailure } from "../../../contracts/errors.js";
|
||||
import type { Result } from "../../../application/result.ts";
|
||||
import type {} from "../../../application/ports/in/application-api.ts";
|
||||
import {
|
||||
toReferenceView,
|
||||
type ReferenceResourceView,
|
||||
} from "../contracts/reference-mapper.js";
|
||||
import type { ReferenceResource } from "../domain/reference-resource.js";
|
||||
} from "../contracts/reference-mapper.ts";
|
||||
import type { ReferenceResource } from "../domain/reference-resource.ts";
|
||||
|
||||
export type ReferenceListFilters = Readonly<{
|
||||
cursor?: string;
|
||||
@@ -11,9 +12,12 @@ export type ReferenceListFilters = Readonly<{
|
||||
tags?: readonly string[];
|
||||
}>;
|
||||
|
||||
export type ReferenceResult<Value> =
|
||||
| Readonly<{ ok: true; value: Value }>
|
||||
| Readonly<{ ok: false; error: ApiFailure }>;
|
||||
export type ReferenceResult<Value> = Result<Value>;
|
||||
|
||||
export type ReferenceCreateCommand = Readonly<{
|
||||
name: string;
|
||||
note?: string;
|
||||
}>;
|
||||
|
||||
export type ReferenceFeatureInput = Readonly<{
|
||||
listResources(
|
||||
@@ -21,7 +25,7 @@ export type ReferenceFeatureInput = Readonly<{
|
||||
context?: Readonly<{ signal?: AbortSignal }>,
|
||||
): Promise<ReferenceResult<readonly ReferenceResourceView[]>>;
|
||||
createResource(
|
||||
command: Readonly<{ name: string; note?: string }>,
|
||||
command: ReferenceCreateCommand,
|
||||
): Promise<ReferenceResult<ReferenceResourceView>>;
|
||||
getResource(
|
||||
resourceId: string,
|
||||
@@ -29,13 +33,19 @@ export type ReferenceFeatureInput = Readonly<{
|
||||
): Promise<ReferenceResult<ReferenceResourceView>>;
|
||||
}>;
|
||||
|
||||
declare module "../../../application/ports/in/application-api.ts" {
|
||||
interface ApplicationFeatureInputs {
|
||||
"reference-feature": ReferenceFeatureInput;
|
||||
}
|
||||
}
|
||||
|
||||
export type ReferenceGateway = Readonly<{
|
||||
list(
|
||||
filters: ReferenceListFilters,
|
||||
context?: Readonly<{ signal?: AbortSignal }>,
|
||||
): Promise<ReferenceResult<readonly ReferenceResource[]>>;
|
||||
create(
|
||||
command: Readonly<{ name: string; note?: string }>,
|
||||
command: ReferenceCreateCommand,
|
||||
): Promise<ReferenceResult<ReferenceResource>>;
|
||||
get(
|
||||
resourceId: string,
|
||||
|
||||
+83
-10
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@ import { lazy } from "react";
|
||||
import {
|
||||
referenceResourceListQuerySchema,
|
||||
referenceResourceParamsSchema,
|
||||
} from "../contracts/reference-schemas.js";
|
||||
} from "../contracts/reference-schemas.ts";
|
||||
|
||||
export const REFERENCE_FEATURE_ROUTE_CODECS = {
|
||||
ReferenceResourceListQuery: referenceResourceListQuerySchema,
|
||||
@@ -13,18 +13,18 @@ export const REFERENCE_FEATURE_ROUTE_CODECS = {
|
||||
export const REFERENCE_FEATURE_ROUTE_RUNTIME = {
|
||||
REFERENCE_RESOURCE_LIST: Object.freeze({
|
||||
moduleId: "reference-resource-page",
|
||||
Component: lazy(() => import("./reference-resource-page.js")),
|
||||
Component: lazy(() => import("./reference-resource-page.tsx")),
|
||||
}),
|
||||
REFERENCE_RESOURCE_DETAIL: Object.freeze({
|
||||
moduleId: "reference-resource-detail-page",
|
||||
Component: lazy(() => import("./reference-resource-detail-page.js")),
|
||||
Component: lazy(() => import("./reference-resource-detail-page.tsx")),
|
||||
}),
|
||||
REFERENCE_RESOURCE_FORM: Object.freeze({
|
||||
moduleId: "reference-resource-form-page",
|
||||
Component: lazy(() => import("./reference-resource-form-page.js")),
|
||||
Component: lazy(() => import("./reference-resource-form-page.tsx")),
|
||||
}),
|
||||
REFERENCE_RESOURCE_STATUS: Object.freeze({
|
||||
moduleId: "reference-resource-status-page",
|
||||
Component: lazy(() => import("./reference-resource-status-page.js")),
|
||||
Component: lazy(() => import("./reference-resource-status-page.tsx")),
|
||||
}),
|
||||
} as const;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { Link } from "react-router-dom";
|
||||
import {
|
||||
AsyncSurface,
|
||||
DetailPage,
|
||||
} from "../../../presentation/design-system/index.js";
|
||||
import { useRouteInput } from "../../../presentation/routes/app-router.js";
|
||||
import { useLocale } from "../../../presentation/i18n/index.js";
|
||||
import { useReferenceDetail } from "./use-reference-feature.js";
|
||||
} from "../../../presentation/design-system/index.ts";
|
||||
import { useRouteInput } from "../../../presentation/routes/route-input.tsx";
|
||||
import { useLocale } from "../../../presentation/i18n/index.ts";
|
||||
import { useReferenceFailureAction } from "./use-reference-failure-action.ts";
|
||||
import { useReferenceDetail } from "./use-reference-feature.ts";
|
||||
|
||||
export default function ReferenceResourceDetailPage() {
|
||||
const { date, message } = useLocale();
|
||||
@@ -14,6 +15,7 @@ export default function ReferenceResourceDetailPage() {
|
||||
const resourceId = String(route.params.resourceId);
|
||||
const { query } = useReferenceDetail(resourceId);
|
||||
const resource = query.data;
|
||||
const failureAction = useReferenceFailureAction(query.state.failure);
|
||||
|
||||
return (
|
||||
<DetailPage
|
||||
@@ -43,7 +45,11 @@ export default function ReferenceResourceDetailPage() {
|
||||
)
|
||||
}
|
||||
feedback={
|
||||
<AsyncSurface state={query.state} onRetry={query.retry}>
|
||||
<AsyncSurface
|
||||
state={query.state}
|
||||
onAction={failureAction}
|
||||
onRetry={query.retry}
|
||||
>
|
||||
{resource ? (
|
||||
<p>이 영역에는 제품별 상세 section을 조립할 수 있습니다.</p>
|
||||
) : null}
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
FormField,
|
||||
useAppForm,
|
||||
useDirtyNavigationGuard,
|
||||
} from "../../../presentation/design-system/index.js";
|
||||
} from "../../../presentation/design-system/index.ts";
|
||||
import {
|
||||
REFERENCE_FORM_DEFAULTS,
|
||||
referenceResourceFormSchema,
|
||||
toCreateReferenceCommand,
|
||||
type ReferenceResourceFormValues,
|
||||
} from "./reference-resource-form.js";
|
||||
import { useReferenceCreate } from "./use-reference-feature.js";
|
||||
} from "./reference-resource-form.ts";
|
||||
import { useReferenceCreate } from "./use-reference-feature.ts";
|
||||
|
||||
const FIELD_LABELS = Object.freeze({
|
||||
name: "새 항목 이름",
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
AsyncSurface,
|
||||
Button,
|
||||
CollectionPage,
|
||||
} from "../../../presentation/design-system/index.js";
|
||||
import { useReferenceFeature } from "./use-reference-feature.js";
|
||||
} from "../../../presentation/design-system/index.ts";
|
||||
import { useReferenceFailureAction } from "./use-reference-failure-action.ts";
|
||||
import { useReferenceFeature } from "./use-reference-feature.ts";
|
||||
|
||||
export default function ReferenceResourcePage() {
|
||||
const navigate = useNavigate();
|
||||
const { filters, query } = useReferenceFeature();
|
||||
const failureAction = useReferenceFailureAction(query.state.failure);
|
||||
|
||||
return (
|
||||
<CollectionPage
|
||||
@@ -37,7 +39,11 @@ export default function ReferenceResourcePage() {
|
||||
query.data ? `총 ${query.data.length}개 항목` : "결과 확인 중"
|
||||
}
|
||||
>
|
||||
<AsyncSurface state={query.state} onRetry={query.retry}>
|
||||
<AsyncSurface
|
||||
state={query.state}
|
||||
onAction={failureAction}
|
||||
onRetry={query.retry}
|
||||
>
|
||||
<ul aria-label="Reference resources">
|
||||
{(query.data ?? []).map((resource) => (
|
||||
<li key={resource.resourceId}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { StatusPage } from "../../../presentation/design-system/index.js";
|
||||
import { StatusPage } from "../../../presentation/design-system/index.ts";
|
||||
|
||||
export default function ReferenceResourceStatusPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useCallback } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import type { AppFailure } from "../../../contracts/errors.ts";
|
||||
import { useSession } from "../../../presentation/providers/session-provider.tsx";
|
||||
|
||||
const REFERENCE_SUPPORT_ROUTE = "/examples/reference-resources/status";
|
||||
|
||||
/**
|
||||
* Reference queries own concrete destinations for generic application failure
|
||||
* actions. Retry remains query-owned; guarded release reloads remain in the
|
||||
* chunk recovery boundary.
|
||||
*/
|
||||
export function useReferenceFailureAction(
|
||||
failure: AppFailure | undefined,
|
||||
): (() => void) | undefined {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { beginSignIn } = useSession();
|
||||
const action = failure?.action;
|
||||
const handleAction = useCallback(() => {
|
||||
if (action === "reauth") {
|
||||
const returnTo = `${location.pathname}${location.search}${location.hash}`;
|
||||
void beginSignIn(returnTo).catch(() => {
|
||||
void navigate("/");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === "navigate") {
|
||||
void navigate("/");
|
||||
return;
|
||||
}
|
||||
if (action === "contact-support") {
|
||||
void navigate(REFERENCE_SUPPORT_ROUTE);
|
||||
}
|
||||
}, [
|
||||
action,
|
||||
beginSignIn,
|
||||
location.hash,
|
||||
location.pathname,
|
||||
location.search,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
return action === "reauth" ||
|
||||
action === "navigate" ||
|
||||
action === "contact-support"
|
||||
? handleAction
|
||||
: undefined;
|
||||
}
|
||||
@@ -1,81 +1,115 @@
|
||||
import { useApplication } from "../../../presentation/providers/application-provider.js";
|
||||
import { useApplication } from "../../../presentation/providers/application-provider.tsx";
|
||||
import {
|
||||
useApplicationMutation,
|
||||
useApplicationQuery,
|
||||
} from "../../../presentation/adapters/query/application-query.js";
|
||||
import { useRouteInput } from "../../../presentation/routes/app-router.js";
|
||||
import type { ReferenceResourceView } from "../contracts/reference-mapper.js";
|
||||
} from "../../../presentation/adapters/query/application-query.ts";
|
||||
import { useRouteInput } from "../../../presentation/routes/route-input.tsx";
|
||||
import {
|
||||
REFERENCE_FEATURE_ID,
|
||||
referenceQueryKeys,
|
||||
} from "../contracts/reference-feature-contract.js";
|
||||
REFERENCE_RESOURCE_INVALIDATION_TOPIC,
|
||||
} from "../contracts/reference-feature-contract.ts";
|
||||
import type {
|
||||
ReferenceCreateCommand,
|
||||
ReferenceFeatureInput,
|
||||
ReferenceListFilters,
|
||||
} from "../application/reference-feature-api.js";
|
||||
} from "../application/reference-feature-api.ts";
|
||||
import type { ReferenceResourceView } from "../contracts/reference-mapper.ts";
|
||||
import {
|
||||
bindQuery,
|
||||
defineServerStateProfile,
|
||||
type BoundMutation,
|
||||
} from "../../../contracts/server-state.ts";
|
||||
import { useServerStateScope } from "../../../presentation/adapters/query/server-state-scope-provider.tsx";
|
||||
|
||||
const REFERENCE_READ_PROFILE = defineServerStateProfile({
|
||||
profileId: "reference-resource-read-v1",
|
||||
staleTimeMs: 30_000,
|
||||
gcTimeMs: 300_000,
|
||||
refetchOnMount: true,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
retryOwner: "TRANSPORT",
|
||||
maxResultItems: 100,
|
||||
maxEstimatedResultBytes: 262_144,
|
||||
});
|
||||
|
||||
export function useReferenceFeatureInput(): ReferenceFeatureInput {
|
||||
const candidate = useApplication().features.get(REFERENCE_FEATURE_ID);
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== "object" ||
|
||||
typeof (candidate as ReferenceFeatureInput).listResources !== "function" ||
|
||||
typeof (candidate as ReferenceFeatureInput).createResource !== "function" ||
|
||||
typeof (candidate as ReferenceFeatureInput).getResource !== "function"
|
||||
) {
|
||||
throw new Error("Reference feature application input is invalid");
|
||||
}
|
||||
return candidate as ReferenceFeatureInput;
|
||||
return useApplication().features.get(REFERENCE_FEATURE_ID);
|
||||
}
|
||||
|
||||
export function useReferenceDetail(resourceId: string) {
|
||||
const input = useReferenceFeatureInput();
|
||||
const query = useApplicationQuery({
|
||||
queryKey: referenceQueryKeys.detail(resourceId),
|
||||
execute: ({ signal }) => input.getResource(resourceId, { signal }),
|
||||
});
|
||||
const scope = useServerStateScope();
|
||||
const query = useApplicationQuery(
|
||||
bindQuery(
|
||||
{
|
||||
definitionId: "reference-resource-detail-v1",
|
||||
definitionVersion: 1,
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
namespace: "reference-resource",
|
||||
namespaceVersion: 1,
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
profile: REFERENCE_READ_PROFILE,
|
||||
execute: (selectedResourceId: string, { signal }) =>
|
||||
input.getResource(selectedResourceId, { signal }),
|
||||
},
|
||||
resourceId,
|
||||
scope,
|
||||
),
|
||||
);
|
||||
return Object.freeze({ query });
|
||||
}
|
||||
|
||||
export function useReferenceCreate() {
|
||||
const input = useReferenceFeatureInput();
|
||||
return useApplicationMutation({
|
||||
const scope = useServerStateScope();
|
||||
const mutation: BoundMutation<
|
||||
ReferenceCreateCommand,
|
||||
ReferenceResourceView
|
||||
> = {
|
||||
definitionId: "reference-resource-create-v1",
|
||||
definitionVersion: 1,
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
duplicatePolicy: "JOIN_IDENTICAL",
|
||||
scope,
|
||||
execute: input.createResource,
|
||||
invalidate: [referenceQueryKeys.all()],
|
||||
currentData: true,
|
||||
});
|
||||
invalidate: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
|
||||
};
|
||||
return useApplicationMutation(mutation);
|
||||
}
|
||||
|
||||
export function useReferenceFeature() {
|
||||
const input = useReferenceFeatureInput();
|
||||
const scope = useServerStateScope();
|
||||
const routeInput = useRouteInput();
|
||||
const filters = routeInput.search as ReferenceListFilters;
|
||||
const queryKey = referenceQueryKeys.list(filters);
|
||||
const query = useApplicationQuery({
|
||||
queryKey,
|
||||
execute: ({ signal }) => input.listResources(filters, { signal }),
|
||||
});
|
||||
const mutation = useApplicationMutation({
|
||||
execute: input.createResource,
|
||||
invalidate: [referenceQueryKeys.all()],
|
||||
currentData: true,
|
||||
optimistic: {
|
||||
queryKey,
|
||||
update(previous, command: Readonly<{ name: string }>) {
|
||||
const current = Array.isArray(previous)
|
||||
? (previous as readonly ReferenceResourceView[])
|
||||
: [];
|
||||
return [
|
||||
...current,
|
||||
{
|
||||
resourceId: `optimistic:${command.name}`,
|
||||
title: command.name,
|
||||
createdAt: null,
|
||||
optimistic: true,
|
||||
},
|
||||
];
|
||||
const query = useApplicationQuery(
|
||||
bindQuery(
|
||||
{
|
||||
definitionId: "reference-resource-list-v1",
|
||||
definitionVersion: 1,
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
namespace: "reference-resource",
|
||||
namespaceVersion: 1,
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
profile: REFERENCE_READ_PROFILE,
|
||||
execute: (selectedFilters: ReferenceListFilters, { signal }) =>
|
||||
input.listResources(selectedFilters, { signal }),
|
||||
},
|
||||
},
|
||||
filters,
|
||||
scope,
|
||||
),
|
||||
);
|
||||
const mutation = useApplicationMutation({
|
||||
definitionId: "reference-resource-create-v1",
|
||||
definitionVersion: 1,
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
owner: REFERENCE_FEATURE_ID,
|
||||
duplicatePolicy: "JOIN_IDENTICAL",
|
||||
scope,
|
||||
execute: input.createResource,
|
||||
invalidate: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
|
||||
});
|
||||
return Object.freeze({ filters, query, mutation });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user