Files
clean-architecture-frontend…/src/features/reference-feature/contracts/reference-mapper.ts
T

138 lines
4.1 KiB
TypeScript

import {
createReferenceResource,
type ReferenceResource,
} from "../domain/reference-resource.ts";
import {
mappingFailure,
mappingSuccess,
type InstalledBoundaryMapper,
type MappingResult,
} from "../../../contracts/boundary-mapper.ts";
import type { ReferenceResourceDto } from "./reference-schemas.ts";
export type ReferenceResourceView = Readonly<{
resourceId: string;
title: string;
createdAt: string | null;
optimistic?: boolean;
}>;
/**
* Operation-specific mapper. The HTTP contract has already validated the wire
* shape, so this mapper owns only wire -> domain construction.
*/
export function mapReferenceResourcePayload(
dto: ReferenceResourceDto,
): MappingResult<ReferenceResource> {
try {
return mappingSuccess(
createReferenceResource({
id: dto.id,
displayName: dto.name,
createdAt: dto.createdAt ?? null,
}),
);
} catch {
return mappingFailure("MAPPING_INVARIANT_REJECTED");
}
}
export function mapReferenceResourceListPayload(
payload: readonly ReferenceResourceDto[],
): MappingResult<readonly ReferenceResource[]> {
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));
}
function isReferenceResourceDto(value: unknown): value is ReferenceResourceDto {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const dto = value as Record<string, unknown>;
return (
typeof dto.id === "string" &&
typeof dto.name === "string" &&
(dto.createdAt === undefined || typeof dto.createdAt === "string")
);
}
function mapUnknownReferenceResourcePayload(
value: unknown,
): MappingResult<ReferenceResource> {
return isReferenceResourceDto(value)
? mapReferenceResourcePayload(value)
: mappingFailure("MAPPING_INVARIANT_REJECTED");
}
function mapUnknownReferenceResourceListPayload(
value: unknown,
): MappingResult<readonly ReferenceResource[]> {
if (
!Array.isArray(value) ||
value.length > 100 ||
!value.every(isReferenceResourceDto)
) {
return mappingFailure(
Array.isArray(value) && value.length > 100
? "OUTPUT_LIMIT_EXCEEDED"
: "MAPPING_INVARIANT_REJECTED",
);
}
return mapReferenceResourceListPayload(value);
}
/**
* Registry-facing compatibility projection. The registry is intentionally
* untyped, so its adapter performs the defensive shape check. Feature HTTP
* bindings use the typed operation-specific mappers above.
*/
export function mapReferenceOperation(
operationId: string,
payload: unknown,
): MappingResult<ReferenceResource | readonly ReferenceResource[]> {
if (operationId === "LIST_REFERENCE_RESOURCES") {
return mapUnknownReferenceResourceListPayload(payload);
}
if (
operationId === "CREATE_REFERENCE_RESOURCE" ||
operationId === "GET_REFERENCE_RESOURCE"
) {
return mapUnknownReferenceResourcePayload(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: mapUnknownReferenceResourceListPayload,
}),
ReferenceResourceMapper: Object.freeze({
mapperId: "ReferenceResourceMapper",
mapperVersion: 1,
inputSchemaId: "ReferenceResourcePayload",
outputContractId: "ReferenceResource",
owner: "feature-frontend-reference-feature-vertical-slice",
maxOutputItems: 1,
map: mapUnknownReferenceResourcePayload,
}),
} satisfies Readonly<Record<string, InstalledBoundaryMapper>>);
export function toReferenceView(
resource: ReferenceResource,
): ReferenceResourceView {
return Object.freeze({
resourceId: resource.id,
title: resource.displayName,
createdAt: resource.createdAt,
});
}