@xemahq/api-client-generator 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/custom-fetch-template.d.ts +1 -1
- package/dist/lib/custom-fetch-template.d.ts.map +1 -1
- package/dist/lib/custom-fetch-template.js +28 -30
- package/dist/lib/custom-fetch-template.js.map +1 -1
- package/dist/lib/seed-openapi-env.d.ts.map +1 -1
- package/dist/lib/seed-openapi-env.js +0 -1
- package/dist/lib/seed-openapi-env.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/custom-fetch-template.ts +28 -30
- package/src/lib/seed-openapi-env.ts +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CANONICAL_CUSTOM_FETCH = "/**\n * Configurable fetch wrapper for Orval-generated clients.\n *\n * Consumers must call `configureClient()` before using any endpoint function.\n * The baseUrl is prepended to the relative paths generated by Orval.\n */\n\nexport interface ClientConfig {\n /** Static base URL (e.g. 'http://localhost:3140') \u2014 no trailing slash. */\n baseUrl?: string;\n /** Per-request base-URL resolver (service-registry-safe). */\n baseUrlResolver?: () => string | Promise<string>;\n /** Optional async callback to get an auth token. */\n getAuthToken?: () => Promise<string>;\n /** Optional callback returning headers to inject on every request. */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /** Optional callback invoked on 401 before a single retry. */\n onUnauthorized?: () => Promise<void>;\n /** Maximum retry attempts for transient failures (default: 3). */\n maxRetries?: number;\n}\n\nexport class ClientError extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: unknown,\n ) {\n super(`HTTP ${status} from ${url}`);\n this.name = 'ClientError';\n }\n}\n\nlet clientConfig: ClientConfig | null = null;\n\nexport function configureClient(config: ClientConfig): void {\n clientConfig = config;\n}\n\nexport function getClientConfig(): ClientConfig {\n if (!clientConfig) {\n throw new Error(\n 'Client not configured. Call configureClient({ baseUrl }) before using endpoint functions.',\n );\n }\n return clientConfig;\n}\n\nconst RETRYABLE_STATUSES = [429, 502, 503, 504];\n\nasync function buildHeaders(\n config: ClientConfig,\n callerHeaders: RequestInit['headers'],\n): Promise<Headers> {\n const headers = new Headers(callerHeaders);\n if (config.getHeaders) {\n const globalHeaders = await Promise.resolve(config.getHeaders());\n for (const [key, value] of Object.entries(globalHeaders)) {\n if (!headers.has(key)) {\n headers.set(key, value);\n }\n }\n }\n if (config.getAuthToken && !headers.has('Authorization')) {\n const token = await config.getAuthToken();\n headers.set('Authorization', `Bearer ${token}`);\n }\n return headers;\n}\n\nexport const customFetch = async <T>(\n url: string,\n options: RequestInit,\n): Promise<T> => {\n const config = getClientConfig();\n const base = config.baseUrlResolver\n ? await config.baseUrlResolver()\n : config.baseUrl;\n if (base === undefined) {\n throw new Error(\n 'Client not configured: set baseUrl or baseUrlResolver via configureClient().',\n );\n }\n const fullUrl = `${base}${url}`;\n const maxRetries = config.maxRetries ?? 3;\n\n const headers = await buildHeaders(config, options.headers);\n const requestInit: RequestInit = { ...options, headers };\n\n let delay = 1000;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(fullUrl, requestInit);\n\n if (response.status === 401 && config.onUnauthorized && attempt === 0) {\n await config.onUnauthorized();\n const refreshedHeaders = await buildHeaders(config, options.headers);\n const retryResponse = await fetch(fullUrl, { ...options, headers: refreshedHeaders });\n const retryBody = await parseBody(retryResponse);\n if (retryResponse.status >= 400) {\n throw new ClientError(retryResponse.status, fullUrl, retryBody);\n }\n return
|
|
1
|
+
export declare const CANONICAL_CUSTOM_FETCH = "/**\n * Configurable fetch wrapper for Orval-generated clients.\n *\n * Consumers must call `configureClient()` before using any endpoint function.\n * The baseUrl is prepended to the relative paths generated by Orval.\n */\n\nexport interface ClientConfig {\n /** Static base URL (e.g. 'http://localhost:3140') \u2014 no trailing slash. */\n baseUrl?: string;\n /** Per-request base-URL resolver (service-registry-safe). */\n baseUrlResolver?: () => string | Promise<string>;\n /** Optional async callback to get an auth token. */\n getAuthToken?: () => Promise<string>;\n /** Optional callback returning headers to inject on every request. */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /** Optional callback invoked on 401 before a single retry. */\n onUnauthorized?: () => Promise<void>;\n /** Maximum retry attempts for transient failures (default: 3). */\n maxRetries?: number;\n}\n\nexport class ClientError extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: unknown,\n ) {\n super(`HTTP ${status} from ${url}`);\n this.name = 'ClientError';\n }\n}\n\nlet clientConfig: ClientConfig | null = null;\n\nexport function configureClient(config: ClientConfig): void {\n clientConfig = config;\n}\n\nexport function getClientConfig(): ClientConfig {\n if (!clientConfig) {\n throw new Error(\n 'Client not configured. Call configureClient({ baseUrl }) before using endpoint functions.',\n );\n }\n return clientConfig;\n}\n\nconst RETRYABLE_STATUSES = [429, 502, 503, 504];\n\nasync function buildHeaders(\n config: ClientConfig,\n callerHeaders: RequestInit['headers'],\n): Promise<Headers> {\n const headers = new Headers(callerHeaders);\n if (config.getHeaders) {\n const globalHeaders = await Promise.resolve(config.getHeaders());\n for (const [key, value] of Object.entries(globalHeaders)) {\n if (!headers.has(key)) {\n headers.set(key, value);\n }\n }\n }\n if (config.getAuthToken && !headers.has('Authorization')) {\n const token = await config.getAuthToken();\n headers.set('Authorization', `Bearer ${token}`);\n }\n return headers;\n}\n\nexport const customFetch = async <T>(\n url: string,\n options: RequestInit,\n): Promise<T> => {\n const config = getClientConfig();\n const base = config.baseUrlResolver\n ? await config.baseUrlResolver()\n : config.baseUrl;\n if (base === undefined) {\n throw new Error(\n 'Client not configured: set baseUrl or baseUrlResolver via configureClient().',\n );\n }\n const fullUrl = `${base}${url}`;\n const maxRetries = config.maxRetries ?? 3;\n\n const headers = await buildHeaders(config, options.headers);\n const requestInit: RequestInit = { ...options, headers };\n\n let delay = 1000;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(fullUrl, requestInit);\n\n if (response.status === 401 && config.onUnauthorized && attempt === 0) {\n await config.onUnauthorized();\n const refreshedHeaders = await buildHeaders(config, options.headers);\n const retryResponse = await fetch(fullUrl, { ...options, headers: refreshedHeaders });\n const retryBody = await parseBody(retryResponse);\n if (retryResponse.status >= 400) {\n throw new ClientError(retryResponse.status, fullUrl, retryBody);\n }\n return retryBody as T;\n }\n\n if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {\n const body = await parseBody(response);\n if (response.status >= 400) {\n throw new ClientError(response.status, fullUrl, body);\n }\n return body as T;\n }\n\n const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));\n const waitMs = retryAfter ?? addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n } catch (error) {\n if (error instanceof ClientError) throw error;\n lastError = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) throw lastError;\n const waitMs = addJitter(delay);\n await sleep(waitMs);\n delay = Math.min(delay * 2, 30_000);\n }\n }\n\n throw lastError ?? new Error(`All retries exhausted for ${fullUrl}`);\n};\n\n/*\n * THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.\n *\n * Xema services wrap every 2xx payload in a { data: T } envelope via the global\n * ResponseEnvelopeInterceptor (platform-common), and the generator now emits\n * types that describe THAT ENVELOPE \u2014 every endpoint returns\n * Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner\n * T. Consumers read the data property themselves.\n *\n * This template used to peel data, justified by a comment claiming generated\n * types describe the inner T. That stopped being true when the generator moved\n * to envelope-typed returns, and the comment outlived the fact \u2014 so the peel\n * then contradicted every type in every package it seeded. Four clients shipped\n * that way (resource-governance x3, workload-runtime-api): declared\n * *DataEnvelope, returned the already-peeled inner object, so .data read\n * undefined at runtime on every non-paginated endpoint. Paginated calls hid\n * it, because the old peel deliberately preserved an envelope carrying\n * pagination.\n *\n * ensureCustomFetch seeds this template into any client that lacks a\n * transport, so a peel here is not one bug \u2014 it is re-imported into every new\n * client generated from now on, which is exactly how those four were born after\n * the hand-fix of their siblings.\n *\n * Enforced fleet-wide by check-client-transport-envelope in xema-base, which\n * reads the RETURN SITES rather than banning a name \u2014 a rename, an inline\n * .data, or ?? body.data all walk past a string ban.\n */\n\nasync function parseBody(response: Response): Promise<unknown> {\n const contentType = response.headers.get('content-type');\n if (contentType?.includes('application/json')) {\n return response.json();\n }\n if (response.status === 204) {\n return undefined;\n }\n return response.text();\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (!isNaN(seconds) && seconds >= 0) return seconds * 1000;\n const date = new Date(value);\n if (!isNaN(date.getTime())) return Math.max(0, date.getTime() - Date.now());\n return undefined;\n}\n\nfunction addJitter(delay: number): number {\n return delay + (Math.random() * 2 - 1) * delay * 0.25;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nexport default customFetch;\n";
|
|
2
2
|
//# sourceMappingURL=custom-fetch-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-fetch-template.d.ts","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"custom-fetch-template.d.ts","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,6yNA8LlC,CAAC"}
|
|
@@ -105,7 +105,7 @@ export const customFetch = async <T>(
|
|
|
105
105
|
if (retryResponse.status >= 400) {
|
|
106
106
|
throw new ClientError(retryResponse.status, fullUrl, retryBody);
|
|
107
107
|
}
|
|
108
|
-
return
|
|
108
|
+
return retryBody as T;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {
|
|
@@ -113,7 +113,7 @@ export const customFetch = async <T>(
|
|
|
113
113
|
if (response.status >= 400) {
|
|
114
114
|
throw new ClientError(response.status, fullUrl, body);
|
|
115
115
|
}
|
|
116
|
-
return
|
|
116
|
+
return body as T;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));
|
|
@@ -133,36 +133,34 @@ export const customFetch = async <T>(
|
|
|
133
133
|
throw lastError ?? new Error(\`All retries exhausted for \${fullUrl}\`);
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
*
|
|
138
|
-
* the global ResponseEnvelopeInterceptor (platform-common). Generated types
|
|
139
|
-
* describe the INNER T (controllers declare their return type as T, not
|
|
140
|
-
* { data: T }), so the client must peel data to keep the static types and the
|
|
141
|
-
* wire shape consistent — otherwise consumers read fields off the envelope and
|
|
142
|
-
* get undefined.
|
|
136
|
+
/*
|
|
137
|
+
* THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.
|
|
143
138
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
139
|
+
* Xema services wrap every 2xx payload in a { data: T } envelope via the global
|
|
140
|
+
* ResponseEnvelopeInterceptor (platform-common), and the generator now emits
|
|
141
|
+
* types that describe THAT ENVELOPE — every endpoint returns
|
|
142
|
+
* Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner
|
|
143
|
+
* T. Consumers read the data property themselves.
|
|
144
|
+
*
|
|
145
|
+
* This template used to peel data, justified by a comment claiming generated
|
|
146
|
+
* types describe the inner T. That stopped being true when the generator moved
|
|
147
|
+
* to envelope-typed returns, and the comment outlived the fact — so the peel
|
|
148
|
+
* then contradicted every type in every package it seeded. Four clients shipped
|
|
149
|
+
* that way (resource-governance x3, workload-runtime-api): declared
|
|
150
|
+
* *DataEnvelope, returned the already-peeled inner object, so .data read
|
|
151
|
+
* undefined at runtime on every non-paginated endpoint. Paginated calls hid
|
|
152
|
+
* it, because the old peel deliberately preserved an envelope carrying
|
|
153
|
+
* pagination.
|
|
154
|
+
*
|
|
155
|
+
* ensureCustomFetch seeds this template into any client that lacks a
|
|
156
|
+
* transport, so a peel here is not one bug — it is re-imported into every new
|
|
157
|
+
* client generated from now on, which is exactly how those four were born after
|
|
158
|
+
* the hand-fix of their siblings.
|
|
159
|
+
*
|
|
160
|
+
* Enforced fleet-wide by check-client-transport-envelope in xema-base, which
|
|
161
|
+
* reads the RETURN SITES rather than banning a name — a rename, an inline
|
|
162
|
+
* .data, or ?? body.data all walk past a string ban.
|
|
150
163
|
*/
|
|
151
|
-
function unwrapEnvelope(body: unknown): unknown {
|
|
152
|
-
if (
|
|
153
|
-
body === null ||
|
|
154
|
-
typeof body !== 'object' ||
|
|
155
|
-
Array.isArray(body) ||
|
|
156
|
-
!('data' in body)
|
|
157
|
-
) {
|
|
158
|
-
return body;
|
|
159
|
-
}
|
|
160
|
-
const envelope = body as { data: unknown; pagination?: unknown };
|
|
161
|
-
if ('pagination' in envelope) {
|
|
162
|
-
return envelope;
|
|
163
|
-
}
|
|
164
|
-
return envelope.data;
|
|
165
|
-
}
|
|
166
164
|
|
|
167
165
|
async function parseBody(response: Response): Promise<unknown> {
|
|
168
166
|
const contentType = response.headers.get('content-type');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-fetch-template.js","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":";;;AAOa,QAAA,sBAAsB,GAAG
|
|
1
|
+
{"version":3,"file":"custom-fetch-template.js","sourceRoot":"","sources":["../../src/lib/custom-fetch-template.ts"],"names":[],"mappings":";;;AAOa,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8LrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-openapi-env.d.ts","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":"AA0CA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI9D;AAMD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"seed-openapi-env.d.ts","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":"AA0CA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI9D;AAMD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAmFtD"}
|
|
@@ -36,7 +36,6 @@ function seedOpenApiEnv(serviceId) {
|
|
|
36
36
|
JWT_PUBLIC_KEY_URL: 'http://localhost:8180/realms/dummy/protocol/openid-connect/certs',
|
|
37
37
|
JWT_ISSUER: 'http://localhost:8180/realms/dummy',
|
|
38
38
|
GATEWAY_JWT_SECRET: 'dummy-gateway-jwt-secret-for-openapi-extraction',
|
|
39
|
-
SHARE_TOKEN_SECRET: 'dummy-share-token-secret',
|
|
40
39
|
OPENCODE_WORKER_PASSWORD: 'dummy-worker-password',
|
|
41
40
|
XEMA_ETCD_ENDPOINTS: 'localhost:2379',
|
|
42
41
|
EVENT_HUB_URL: 'http://localhost:3128',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-openapi-env.js","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":";;AA0CA,oDAIC;AAMD,
|
|
1
|
+
{"version":3,"file":"seed-openapi-env.js","sourceRoot":"","sources":["../../src/lib/seed-openapi-env.ts"],"names":[],"mappings":";;AA0CA,oDAIC;AAMD,wCAmFC;AAvHD,MAAM,wBAAwB,GAC5B,kEAAkE,CAAC;AACrE,MAAM,gBAAgB,GAAG,8CAA8C,CAAC;AAGxE,SAAS,IAAI,CAAC,IAA4B;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAGD,MAAM,eAAe,GAAG,eAAe,CAAC;AAYxC,SAAgB,oBAAoB,CAAC,SAAiB;IACpD,OAAO,WAAW,SAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE;SACzD,WAAW,EAAE;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAMD,SAAgB,cAAc,CAAC,SAAiB;IAC9C,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,QAAQ,EAAE,aAAa;QASvB,uBAAuB,EAAE,GAAG;QAO5B,YAAY,EAAE,2CAA2C,eAAe,WAAW,MAAM,EAAE;QAC3F,OAAO,EAAE,WAAW;QACpB,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,eAAe;QACxB,SAAS,EAAE,MAAM;QACjB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,OAAO;QACpB,SAAS,EAAE,wBAAwB;QACnC,cAAc,EAAE,uBAAuB;QAGvC,gBAAgB,EAAE,uBAAuB;QACzC,2BAA2B,EAAE,aAAa;QAC1C,kBAAkB,EAChB,kEAAkE;QACpE,UAAU,EAAE,oCAAoC;QAChD,kBAAkB,EAAE,iDAAiD;QACrE,wBAAwB,EAAE,uBAAuB;QAKjD,mBAAmB,EAAE,gBAAgB;QAMrC,aAAa,EAAE,uBAAuB;QACtC,WAAW,EAAE,uCAAuC;QAGpD,eAAe,EAAE,uBAAuB;QACxC,cAAc,EAAE,uBAAuB;QACvC,oBAAoB,EAAE,uBAAuB;QAC7C,mBAAmB,EAAE,uBAAuB;QAC5C,sBAAsB,EAAE,uBAAuB;QAC/C,sBAAsB,EAAE,uBAAuB;QAC/C,wBAAwB,EAAE,uBAAuB;QACjD,yBAAyB,EAAE,uBAAuB;QAClD,6BAA6B,EAAE,uBAAuB;QACtD,cAAc,EAAE,uBAAuB;QACvC,eAAe,EAAE,uBAAuB;QACxC,2BAA2B,EAAE,uBAAuB;QAGpD,kBAAkB,EAAE,sBAAsB;QAC1C,oBAAoB,EAAE,gCAAgC;QACtD,SAAS,EAAE,uBAAuB;QAClC,gBAAgB,EAAE,aAAa;QAC/B,gBAAgB,EAAE,kBAAkB;QAGpC,qBAAqB,EAAE,wBAAwB;QAC/C,qBAAqB,EAAE,gBAAgB;QACvC,2BAA2B,EACzB,kEAAkE;QACpE,uBAAuB,EACrB,kEAAkE;QAGpE,yBAAyB,EAAE,+BAA+B;QAC1D,uBAAuB,EAAE,6BAA6B;KACvD,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/api-client-generator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Layer 1 — the single, central OpenAPI + ORVAL client generator for every Xema NestJS service. Replaces the per-service orval.config.ts / scripts/extract-openapi.ts / scripts/build-client-index.js trio with two bins (xema-openapi, xema-client-generate). Extracts one OpenAPI document per ApiSurface (public/internal/operator) using the @XemaApiSurface decorator metadata, generates one ORVAL client package per non-empty surface (all sharing the owning service's version), and stamps deterministic `xema` provenance metadata into each generated client package.json. No heuristics: a route ships in a surface only if it (or its controller) is explicitly classified.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xema",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"prettier": "3.6.2",
|
|
64
64
|
"reflect-metadata": "0.2.2",
|
|
65
65
|
"typescript": "5.9.3",
|
|
66
|
-
"@xemahq/xema-decorators": "0.5.
|
|
66
|
+
"@xemahq/xema-decorators": "0.5.3"
|
|
67
67
|
},
|
|
68
68
|
"exports": {
|
|
69
69
|
".": {
|
|
@@ -109,7 +109,7 @@ export const customFetch = async <T>(
|
|
|
109
109
|
if (retryResponse.status >= 400) {
|
|
110
110
|
throw new ClientError(retryResponse.status, fullUrl, retryBody);
|
|
111
111
|
}
|
|
112
|
-
return
|
|
112
|
+
return retryBody as T;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (!RETRYABLE_STATUSES.includes(response.status) || attempt >= maxRetries) {
|
|
@@ -117,7 +117,7 @@ export const customFetch = async <T>(
|
|
|
117
117
|
if (response.status >= 400) {
|
|
118
118
|
throw new ClientError(response.status, fullUrl, body);
|
|
119
119
|
}
|
|
120
|
-
return
|
|
120
|
+
return body as T;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const retryAfter = parseRetryAfter(response.headers.get('Retry-After'));
|
|
@@ -137,36 +137,34 @@ export const customFetch = async <T>(
|
|
|
137
137
|
throw lastError ?? new Error(\`All retries exhausted for \${fullUrl}\`);
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
140
|
+
/*
|
|
141
|
+
* THE TRANSPORT RETURNS THE BODY UNCHANGED. Do not reintroduce an unwrap here.
|
|
142
|
+
*
|
|
143
|
+
* Xema services wrap every 2xx payload in a { data: T } envelope via the global
|
|
144
|
+
* ResponseEnvelopeInterceptor (platform-common), and the generator now emits
|
|
145
|
+
* types that describe THAT ENVELOPE — every endpoint returns
|
|
146
|
+
* Promise<XDataEnvelope> or Promise<XPaginatedEnvelope>, never a bare inner
|
|
147
|
+
* T. Consumers read the data property themselves.
|
|
148
|
+
*
|
|
149
|
+
* This template used to peel data, justified by a comment claiming generated
|
|
150
|
+
* types describe the inner T. That stopped being true when the generator moved
|
|
151
|
+
* to envelope-typed returns, and the comment outlived the fact — so the peel
|
|
152
|
+
* then contradicted every type in every package it seeded. Four clients shipped
|
|
153
|
+
* that way (resource-governance x3, workload-runtime-api): declared
|
|
154
|
+
* *DataEnvelope, returned the already-peeled inner object, so .data read
|
|
155
|
+
* undefined at runtime on every non-paginated endpoint. Paginated calls hid
|
|
156
|
+
* it, because the old peel deliberately preserved an envelope carrying
|
|
157
|
+
* pagination.
|
|
158
|
+
*
|
|
159
|
+
* ensureCustomFetch seeds this template into any client that lacks a
|
|
160
|
+
* transport, so a peel here is not one bug — it is re-imported into every new
|
|
161
|
+
* client generated from now on, which is exactly how those four were born after
|
|
162
|
+
* the hand-fix of their siblings.
|
|
147
163
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* { data: T[], pagination: {...} }; consumers read both fields, so that envelope
|
|
152
|
-
* is kept intact (detected by the presence of pagination). Any body without a
|
|
153
|
-
* top-level data field is returned as-is.
|
|
164
|
+
* Enforced fleet-wide by check-client-transport-envelope in xema-base, which
|
|
165
|
+
* reads the RETURN SITES rather than banning a name — a rename, an inline
|
|
166
|
+
* .data, or ?? body.data all walk past a string ban.
|
|
154
167
|
*/
|
|
155
|
-
function unwrapEnvelope(body: unknown): unknown {
|
|
156
|
-
if (
|
|
157
|
-
body === null ||
|
|
158
|
-
typeof body !== 'object' ||
|
|
159
|
-
Array.isArray(body) ||
|
|
160
|
-
!('data' in body)
|
|
161
|
-
) {
|
|
162
|
-
return body;
|
|
163
|
-
}
|
|
164
|
-
const envelope = body as { data: unknown; pagination?: unknown };
|
|
165
|
-
if ('pagination' in envelope) {
|
|
166
|
-
return envelope;
|
|
167
|
-
}
|
|
168
|
-
return envelope.data;
|
|
169
|
-
}
|
|
170
168
|
|
|
171
169
|
async function parseBody(response: Response): Promise<unknown> {
|
|
172
170
|
const contentType = response.headers.get('content-type');
|
|
@@ -86,7 +86,6 @@ export function seedOpenApiEnv(serviceId: string): void {
|
|
|
86
86
|
'http://localhost:8180/realms/dummy/protocol/openid-connect/certs',
|
|
87
87
|
JWT_ISSUER: 'http://localhost:8180/realms/dummy',
|
|
88
88
|
GATEWAY_JWT_SECRET: 'dummy-gateway-jwt-secret-for-openapi-extraction',
|
|
89
|
-
SHARE_TOKEN_SECRET: 'dummy-share-token-secret',
|
|
90
89
|
OPENCODE_WORKER_PASSWORD: 'dummy-worker-password',
|
|
91
90
|
|
|
92
91
|
// Service registry (etcd-only autodiscovery). `createKernelState` fail-fasts
|