feat: Studio 응답 봉투를 전송 경계에서 언랩한다
studio-v1.yaml v3.0.0(ADR-006)에 맞춰 계약을 재생성하고, 성공은
{success,data,meta}, 실패는 {success,error,meta} 봉투를 전송 경계에서
언랩하는 envelopeData/envelopeError validator를 도입한다. 앱·도메인
계층은 기존과 같은 payload/ProblemDetails 모양을 계속 받고,
StudioGateway 포트 시그니처는 무변경이다.
- tech-log-studio-contract-contribution.ts: envelopeData/envelopeError
도입, 18개 operation의 outputValidator를 passthrough에서 envelopeData로
교체
- studio-error-mapping.ts: 봉투 오류의 status(항상 0)를
outcome.metadata.status로 덮는다. SafeResponseMetadata.status가
실제 필드명이며(httpStatus 아님) PROBLEM outcome에서 필수 필드다
- contract.ts: 삭제된 ProblemDetails 생성 스키마를 손으로 유지 — 앱
계층·mock 게이트웨이가 그 모양을 계속 소비한다
- asset-upload-transport.ts: multipart 업로드는 일반 계약 런타임을
거치지 않는 별도 seam이지만 같은 wire 봉투를 쓴다 — envelopeData/
envelopeError를 재사용해 이 경로도 언랩한다 (브리프 파일 목록 밖의
발견, report에 기록)
- 테스트: 신규 studio-envelope-unwrap.test.ts(TDD) + 봉투 뼈대를 직접
만드는 기존 테스트(asset-upload-transport, studio-csrf-composition,
contract-generation)를 봉투 형태로 갱신
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d84b57bb3f
commit
25a6b63d27
@@ -1,11 +1,21 @@
|
||||
import { StudioGatewayError } from "../../application/ports/studio-gateway-error.ts";
|
||||
import type { UploadAssetForm } from "../../application/ports/studio-asset-gateway.ts";
|
||||
import type { Asset, ProblemDetails } from "../../contracts/studio/contract.ts";
|
||||
import { envelopeData, envelopeError } from "../../contracts/tech-log-studio-contract-contribution.ts";
|
||||
import type { StudioAssetUploadTransport } from "./http-studio-asset-gateway.ts";
|
||||
import { STUDIO_ERROR_CODES } from "./studio-error-mapping.ts";
|
||||
|
||||
const CODES = new Set<string>(STUDIO_ERROR_CODES);
|
||||
|
||||
/**
|
||||
* canonical `uploadStudioAsset`도 다른 18개 operation과 같은 봉투(ADR-006)를
|
||||
* 쓴다 — 이 seam만 일반 계약 런타임을 안 거칠 뿐이지 wire format은 같다.
|
||||
* 그래서 `tech-log-studio-contract-contribution.ts`의 언랩 validator를 그대로
|
||||
* 재사용한다: 봉투 뼈대 검증 로직이 두 곳에서 따로 드리프트하는 것을 막는다.
|
||||
*/
|
||||
const UPLOAD_DATA = envelopeData<Asset>("uploadStudioAssetOutput");
|
||||
const UPLOAD_PROBLEM = envelopeError();
|
||||
|
||||
/**
|
||||
* The contract runtime can only express `requestBody: "NONE" | "JSON"` and the
|
||||
* low-level client always serializes the body as JSON (see
|
||||
@@ -81,8 +91,9 @@ export function createAssetUploadTransport(
|
||||
}
|
||||
|
||||
if (response.status === 201 || response.status === 200) {
|
||||
let body: unknown;
|
||||
try {
|
||||
return (await response.json()) as Asset;
|
||||
body = await response.json();
|
||||
} catch {
|
||||
// M1 (fix round 1). This port's contract is `StudioGatewayError`;
|
||||
// a malformed success body must not throw a raw `SyntaxError` out
|
||||
@@ -91,16 +102,33 @@ export function createAssetUploadTransport(
|
||||
`Upload returned status ${response.status} with a body that could not be parsed as JSON.`,
|
||||
);
|
||||
}
|
||||
const parsed = UPLOAD_DATA.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// 봉투 뼈대(`{success:true, data, meta}`)가 아니다 — payload는
|
||||
// 통과시키되 봉투 자체는 반드시 검증한다(다른 18개 operation과 동일
|
||||
// 원칙, ADR-006).
|
||||
throw unavailable(
|
||||
`Upload returned status ${response.status} with a body that did not match the response envelope.`,
|
||||
);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
let problem: ProblemDetails | null;
|
||||
let problemBody: unknown = null;
|
||||
try {
|
||||
problem = (await response.json()) as ProblemDetails;
|
||||
problemBody = await response.json();
|
||||
} catch {
|
||||
problem = null;
|
||||
problemBody = null;
|
||||
}
|
||||
if (problem && typeof problem.code === "string" && CODES.has(problem.code)) {
|
||||
throw new StudioGatewayError(problem);
|
||||
const parsedProblem = problemBody === null ? null : UPLOAD_PROBLEM.safeParse(problemBody);
|
||||
if (parsedProblem && parsedProblem.success && CODES.has(parsedProblem.data.code)) {
|
||||
// `studio-error-mapping.ts`의 PROBLEM 분기와 같은 캐스트: 봉투는 이미
|
||||
// `code`를 `apiErrorSchema`의 enum으로 검증했으므로(`CODES.has` 확인도
|
||||
// 통과) `ProblemDetails["code"]`로 좁혀도 안전하다. envelope에는 HTTP
|
||||
// status가 없다 (`envelopeError`가 0으로 둔다) — 이 seam은 실제 status를
|
||||
// 이미 들고 있으므로 바로 덮는다.
|
||||
const problem = parsedProblem.data as unknown as ProblemDetails;
|
||||
throw new StudioGatewayError({ ...problem, status: response.status });
|
||||
}
|
||||
// Fix round 2, item 2. The real status is passed through (not the
|
||||
// hardcoded 503 default) so `http-studio-asset-gateway.ts`'s
|
||||
|
||||
Reference in New Issue
Block a user