feat: read the profile's topics from Studio, and add working-copy deletion
Two things an author could not control from Studio. The profile's "주요 관심 주제" was four strings in the JSX. Creating or removing a topic in Studio changed nothing, and correcting the list meant a rebuild and a redeploy. It now renders the published topic list. The old literal opened with "Backend Architecture", which no record in the catalogue actually carries — the profile was advertising a topic that did not exist, and nothing could have caught that while the list lived in the markup. The working-copy list gained a delete control. It routes by kind because the contract and the storage both do: Case and Reference share one table split by type, Question is its own. Decision has no delete — its lifecycle is accept, reject, supersede, which records what happened rather than erasing it — so the control does not appear for it. The list summary carries no version, so deletion reads the working copy first and uses the version it finds. A stale version from a list left open should fail as a conflict, not delete whatever is there now.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"packageId": "@tech-log/management-contract",
|
||||
"version": "1.0.0",
|
||||
"digest": "sha256:e16a9695e9034a273c769792ea8901fd706c61514137228b2c20e1b117ea0c01",
|
||||
"sourceRevision": "0c10a4a",
|
||||
"digest": "sha256:9efc7709b29404db96a7cc7acecdd9ec7b27cb95f3389e5da2c4953e2e3e23aa",
|
||||
"sourceRevision": "332b11f",
|
||||
"operationIds": [
|
||||
"createCaseDraft",
|
||||
"getCaseForEdit",
|
||||
|
||||
@@ -902,7 +902,7 @@ export interface components {
|
||||
* @description `INTERNAL_ERROR` 는 이 기능이 아니라 스켈레톤의 공통 처리기가 내는 코드다. 계약이 그것까지 열거해야 500 응답이 계약을 벗어나지 않는다.
|
||||
* @enum {string}
|
||||
*/
|
||||
code: "AUTHENTICATION_REQUIRED" | "STUDIO_ACCESS_DENIED" | "REQUEST_VALIDATION_FAILED" | "VERSION_CONFLICT" | "TOPIC_NOT_FOUND" | "TOPIC_NAME_TAKEN" | "TOPIC_SLUG_TAKEN" | "TOPIC_IN_USE" | "PROJECT_NOT_FOUND" | "PROJECT_SLUG_TAKEN" | "PROJECT_IN_USE" | "RELEASE_NOT_FOUND" | "RELEASE_VERSION_TAKEN" | "RELEASE_NOT_PUBLISHABLE" | "INTERNAL_ERROR";
|
||||
code: "AUTHENTICATION_REQUIRED" | "STUDIO_ACCESS_DENIED" | "REQUEST_VALIDATION_FAILED" | "VERSION_CONFLICT" | "TOPIC_NOT_FOUND" | "TOPIC_NAME_TAKEN" | "TOPIC_SLUG_TAKEN" | "TOPIC_IN_USE" | "PROJECT_NOT_FOUND" | "PROJECT_SLUG_TAKEN" | "PROJECT_IN_USE" | "RELEASE_NOT_FOUND" | "RELEASE_VERSION_TAKEN" | "RELEASE_NOT_PUBLISHABLE" | "DOCUMENT_NOT_FOUND" | "DOCUMENT_PUBLISHED" | "DOCUMENT_IN_USE" | "QUESTION_NOT_FOUND" | "QUESTION_IN_USE" | "INTERNAL_ERROR";
|
||||
/** @enum {string} */
|
||||
category: "VALIDATION" | "AUTH" | "AUTHZ" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMIT" | "TRANSIENT_DEPENDENCY" | "PERMANENT_DEPENDENCY" | "DATA_INTEGRITY" | "INTERNAL";
|
||||
message: string;
|
||||
@@ -1922,7 +1922,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -1931,7 +1931,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
@@ -1940,7 +1940,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
@@ -1949,7 +1949,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Conflict */
|
||||
@@ -1958,7 +1958,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Content */
|
||||
@@ -1967,7 +1967,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Internal Server Error */
|
||||
@@ -1976,7 +1976,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -2258,7 +2258,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -2267,7 +2267,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
@@ -2276,7 +2276,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
@@ -2285,7 +2285,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Conflict */
|
||||
@@ -2294,7 +2294,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Content */
|
||||
@@ -2303,7 +2303,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Internal Server Error */
|
||||
@@ -2312,7 +2312,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -3936,7 +3936,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -3945,7 +3945,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
@@ -3954,7 +3954,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
@@ -3963,7 +3963,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Conflict */
|
||||
@@ -3972,7 +3972,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Content */
|
||||
@@ -3981,7 +3981,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Internal Server Error */
|
||||
@@ -3990,7 +3990,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDetails"];
|
||||
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ info:
|
||||
description: |-
|
||||
⚠ 봉투 결정(ADR-006) 부분 반영 — 이 파일에는 두 모양이 공존한다.
|
||||
|
||||
topics 4개, projects 5개, releases 7개를 studio-v1.yaml과 같은 방식으로
|
||||
변환했다 (`application/json` + ErrorEnvelope / <Payload>Envelope). 그 16개가
|
||||
구현된 것이자 소비자가 있는 것이기 때문이다.
|
||||
topics 4개, projects 5개, releases 7개, 그리고 작업본 삭제 3개
|
||||
(deleteCaseDraft / deleteReferenceDraft / deleteQuestion)를 studio-v1.yaml과
|
||||
같은 방식으로 변환했다 (`application/json` + ErrorEnvelope / <Payload>Envelope).
|
||||
그 19개가 구현된 것이자 소비자가 있는 것이기 때문이다.
|
||||
|
||||
나머지 63개는 아직 bare payload + `application/problem+json` + ProblemDetails
|
||||
나머지 60개는 아직 bare payload + `application/problem+json` + ProblemDetails
|
||||
다. 구현에 착수할 때 같은 방식으로 따라온다 — 소비자가 없는 operation을 미리
|
||||
변환해 두면 검증되지 않은 모양이 계약에 고정된다.
|
||||
|
||||
@@ -260,45 +261,45 @@ paths:
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'403':
|
||||
description: Forbidden
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'409':
|
||||
description: Conflict
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'422':
|
||||
description: Unprocessable Content
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'500':
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -509,45 +510,45 @@ paths:
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'403':
|
||||
description: Forbidden
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'409':
|
||||
description: Conflict
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'422':
|
||||
description: Unprocessable Content
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'500':
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1795,45 +1796,45 @@ paths:
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'403':
|
||||
description: Forbidden
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'409':
|
||||
description: Conflict
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'422':
|
||||
description: Unprocessable Content
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
'500':
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/problem+json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -5270,6 +5271,11 @@ components:
|
||||
- RELEASE_NOT_FOUND
|
||||
- RELEASE_VERSION_TAKEN
|
||||
- RELEASE_NOT_PUBLISHABLE
|
||||
- DOCUMENT_NOT_FOUND
|
||||
- DOCUMENT_PUBLISHED
|
||||
- DOCUMENT_IN_USE
|
||||
- QUESTION_NOT_FOUND
|
||||
- QUESTION_IN_USE
|
||||
- INTERNAL_ERROR
|
||||
description: '`INTERNAL_ERROR` 는 이 기능이 아니라 스켈레톤의 공통 처리기가 내는 코드다. 계약이 그것까지 열거해야 500 응답이 계약을 벗어나지 않는다.'
|
||||
category:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"packageId": "@tech-log/public-contract",
|
||||
"version": "2.0.0",
|
||||
"digest": "sha256:8ac71425b38658f34641102b4c2e6e21288c811efebdb92c0a46fb9d4790e23e",
|
||||
"sourceRevision": "0c10a4a",
|
||||
"sourceRevision": "332b11f",
|
||||
"operationIds": [
|
||||
"getPublicSite",
|
||||
"getPublicHome",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"packageId": "@tech-log/studio-contract",
|
||||
"version": "3.0.0",
|
||||
"digest": "sha256:6cae9924403d0761f401643a022980b8e04183eea0d890c143c9fbbbbc7431e4",
|
||||
"sourceRevision": "0c10a4a",
|
||||
"sourceRevision": "332b11f",
|
||||
"operationIds": [
|
||||
"getStudioSession",
|
||||
"getStudioDashboard",
|
||||
|
||||
@@ -34,6 +34,11 @@ const MANAGEMENT_ERROR_CODES = Object.freeze([
|
||||
"RELEASE_NOT_FOUND",
|
||||
"RELEASE_VERSION_TAKEN",
|
||||
"RELEASE_NOT_PUBLISHABLE",
|
||||
"DOCUMENT_NOT_FOUND",
|
||||
"DOCUMENT_PUBLISHED",
|
||||
"DOCUMENT_IN_USE",
|
||||
"QUESTION_NOT_FOUND",
|
||||
"QUESTION_IN_USE",
|
||||
"INTERNAL_ERROR",
|
||||
]);
|
||||
|
||||
@@ -159,6 +164,7 @@ const byId = (input: never) => {
|
||||
const T = "/api/v1/studio/topics";
|
||||
const P = "/api/v1/studio/projects";
|
||||
const R = "/api/v1/studio/releases";
|
||||
const D = "/api/v1/studio";
|
||||
|
||||
const HTTP_CONTRACTS = Object.freeze([
|
||||
readOperation("listStudioTopics", T, 131_072),
|
||||
@@ -330,6 +336,63 @@ const HTTP_CONTRACTS = Object.freeze([
|
||||
});
|
||||
},
|
||||
),
|
||||
writeOperation(
|
||||
"deleteCaseDraft",
|
||||
"DELETE",
|
||||
`${D}/cases/{id}`,
|
||||
{
|
||||
acceptedStatuses: [204],
|
||||
emptyBodyStatuses: [204],
|
||||
requestByteLimit: 1_024,
|
||||
responseByteLimit: 1_024,
|
||||
},
|
||||
(input: never) => {
|
||||
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({ id: value.id }),
|
||||
queryEntries: NO_QUERY,
|
||||
body: { expectedVersion: value.expectedVersion },
|
||||
});
|
||||
},
|
||||
),
|
||||
writeOperation(
|
||||
"deleteReferenceDraft",
|
||||
"DELETE",
|
||||
`${D}/references/{id}`,
|
||||
{
|
||||
acceptedStatuses: [204],
|
||||
emptyBodyStatuses: [204],
|
||||
requestByteLimit: 1_024,
|
||||
responseByteLimit: 1_024,
|
||||
},
|
||||
(input: never) => {
|
||||
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({ id: value.id }),
|
||||
queryEntries: NO_QUERY,
|
||||
body: { expectedVersion: value.expectedVersion },
|
||||
});
|
||||
},
|
||||
),
|
||||
writeOperation(
|
||||
"deleteQuestion",
|
||||
"DELETE",
|
||||
`${D}/questions/{id}`,
|
||||
{
|
||||
acceptedStatuses: [204],
|
||||
emptyBodyStatuses: [204],
|
||||
requestByteLimit: 1_024,
|
||||
responseByteLimit: 1_024,
|
||||
},
|
||||
(input: never) => {
|
||||
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
|
||||
return Object.freeze({
|
||||
pathValues: Object.freeze({ id: value.id }),
|
||||
queryEntries: NO_QUERY,
|
||||
body: { expectedVersion: value.expectedVersion },
|
||||
});
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
export const TECH_LOG_MANAGEMENT_OPERATION_IDS = Object.freeze(
|
||||
|
||||
Reference in New Issue
Block a user