feat: contain DTO mapping at the HTTP boundary

This commit is contained in:
donghyeon-ka
2026-07-25 21:02:06 +09:00
parent 3e126c0ddd
commit a9a7db0231
6 changed files with 178 additions and 8 deletions
+34 -7
View File
@@ -3,7 +3,9 @@ import { getApiOperation } from "../../contracts/api-operations.js";
import {
createFailure as failure,
kindForStatus as statusKind,
normalizeUnknownFailure,
} from "../../contracts/errors.js";
import { mapOperationPayload } from "./resource-mapper.js";
import { retryDelay, shouldRetry } from "./retry-policy.js";
import {
validateEnvelope,
@@ -49,6 +51,7 @@ const noAuthSession =
* random?: () => number,
* validatePayload?: (schemaId: string, value: unknown) =>
* { success: true, data: unknown } | { success: false },
* mapPayload?: (operationId: string, payload: unknown) => unknown,
* idempotencyKeyFactory?: () => string
* }} dependencies
*/
@@ -59,6 +62,7 @@ export function createHttpClient(dependencies) {
const random = dependencies.random ?? Math.random;
const validatePayload =
dependencies.validatePayload ?? validateOperationPayload;
const mapPayload = dependencies.mapPayload ?? mapOperationPayload;
const idempotencyKeyFactory =
dependencies.idempotencyKeyFactory ?? (() => crypto.randomUUID());
@@ -195,7 +199,13 @@ export function createHttpClient(dependencies) {
}
const response = await fetcher(request);
return await parseResponse(response, operation, attempt, validatePayload);
return await parseResponse(
response,
operation,
attempt,
validatePayload,
mapPayload,
);
} catch {
if (timedOut) {
return {
@@ -254,9 +264,16 @@ export function createHttpClient(dependencies) {
* @param {number} attempt
* @param {(schemaId: string, value: unknown) =>
* { success: true, data: unknown } | { success: false }} validatePayload
* @param {(operationId: string, payload: unknown) => unknown} mapPayload
* @returns {Promise<HttpResult>}
*/
async function parseResponse(response, operation, attempt, validatePayload) {
async function parseResponse(
response,
operation,
attempt,
validatePayload,
mapPayload,
) {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.toLowerCase().includes("application/json")) {
return {
@@ -311,11 +328,21 @@ async function parseResponse(response, operation, attempt, validatePayload) {
};
}
return {
ok: true,
value: structuredClone(payload.data),
meta: safeMeta(envelopeRecord.meta),
};
try {
return {
ok: true,
value: mapPayload(operation.operationId, payload.data),
meta: safeMeta(envelopeRecord.meta),
};
} catch (error) {
return {
ok: false,
error: normalizeUnknownFailure(error, {
operationId: operation.operationId,
attempt,
}),
};
}
}
const kind = statusKind(response.status);