@ontrails/schema 1.0.0-beta.7 → 1.0.0-beta.8
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +34 -2
- package/dist/openapi.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/openapi.test.ts +64 -7
- package/src/openapi.ts +40 -2
package/.turbo/turbo-lint.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ontrails/schema
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Restructure HTTP package and fix Codex review findings.
|
|
8
|
+
|
|
9
|
+
**http**: BREAKING — `blaze()` moved to `@ontrails/http/hono` subpath. Hono is now a peer dependency. `buildHttpRoutes()` is framework-agnostic. Fixed: malformed JSON → 400, execute() never throws, query parsing preserves raw strings and supports arrays.
|
|
10
|
+
|
|
11
|
+
**schema**: OpenAPI 200 response wraps in `{ data }` envelope matching wire format. Always includes 400 ValidationError with error schema. basePath trailing slash normalized.
|
|
12
|
+
|
|
13
|
+
- @ontrails/core@1.0.0-beta.8
|
|
14
|
+
|
|
3
15
|
## 1.0.0-beta.7
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAS,MAAM,gBAAgB,CAAC;AAQlD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,yBAAyB;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACxD,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC3C,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CACpE;
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAS,MAAM,gBAAgB,CAAC;AAQlD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,yBAAyB;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACxD,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC3C,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CACpE;AAuQD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,IAAI,EACT,UAAU,cAAc,KACvB,WAQD,CAAC"}
|
package/dist/openapi.js
CHANGED
|
@@ -104,6 +104,28 @@ const buildInputSpec = (t, method) => {
|
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
|
+
/** Wrap a raw output schema in the `{ data: ... }` envelope the HTTP adapter uses. */
|
|
108
|
+
const wrapInDataEnvelope = (outputSchema) => ({
|
|
109
|
+
properties: { data: outputSchema },
|
|
110
|
+
required: ['data'],
|
|
111
|
+
type: 'object',
|
|
112
|
+
});
|
|
113
|
+
/** Shared error response body schema: `{ error: { message, code, category } }`. */
|
|
114
|
+
const errorResponseSchema = {
|
|
115
|
+
properties: {
|
|
116
|
+
error: {
|
|
117
|
+
properties: {
|
|
118
|
+
category: { type: 'string' },
|
|
119
|
+
code: { type: 'string' },
|
|
120
|
+
message: { type: 'string' },
|
|
121
|
+
},
|
|
122
|
+
required: ['message', 'code', 'category'],
|
|
123
|
+
type: 'object',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ['error'],
|
|
127
|
+
type: 'object',
|
|
128
|
+
};
|
|
107
129
|
/** Build the 200 response entry. */
|
|
108
130
|
const buildSuccessResponse = (t) => {
|
|
109
131
|
if (!t.output) {
|
|
@@ -112,16 +134,26 @@ const buildSuccessResponse = (t) => {
|
|
|
112
134
|
const outputSchema = toJsonSchema(t.output);
|
|
113
135
|
return {
|
|
114
136
|
'200': {
|
|
115
|
-
content: {
|
|
137
|
+
content: {
|
|
138
|
+
'application/json': { schema: wrapInDataEnvelope(outputSchema) },
|
|
139
|
+
},
|
|
116
140
|
description: 'Success',
|
|
117
141
|
},
|
|
118
142
|
};
|
|
119
143
|
};
|
|
144
|
+
/** Build the default 400 validation error response. */
|
|
145
|
+
const validationErrorResponse = {
|
|
146
|
+
'400': {
|
|
147
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
148
|
+
description: 'Validation error',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
120
151
|
/** Build all responses (success + error) for a trail. */
|
|
121
152
|
const buildResponses = (t) => {
|
|
122
153
|
const examples = (t.examples ?? []);
|
|
123
154
|
return {
|
|
124
155
|
...buildSuccessResponse(t),
|
|
156
|
+
...validationErrorResponse,
|
|
125
157
|
...errorResponsesFromExamples(examples),
|
|
126
158
|
};
|
|
127
159
|
};
|
|
@@ -179,7 +211,7 @@ export const generateOpenApiSpec = (app, options) => ({
|
|
|
179
211
|
components: { schemas: {} },
|
|
180
212
|
info: buildInfo(app.name, options),
|
|
181
213
|
openapi: '3.1.0',
|
|
182
|
-
paths: collectPaths(app, options?.basePath ?? ''),
|
|
214
|
+
paths: collectPaths(app, (options?.basePath ?? '').replace(/\/+$/, '')),
|
|
183
215
|
...(options?.servers && options.servers.length > 0
|
|
184
216
|
? { servers: options.servers }
|
|
185
217
|
: {}),
|
package/dist/openapi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -104,6 +104,20 @@ describe('generateOpenApiSpec', () => {
|
|
|
104
104
|
|
|
105
105
|
expect(spec.paths['/api/v1/entity/show']).toBeDefined();
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
test('basePath trailing slash is normalized', () => {
|
|
109
|
+
const t = trail('entity.show', {
|
|
110
|
+
input: z.object({ id: z.string() }),
|
|
111
|
+
intent: 'read',
|
|
112
|
+
run: noop,
|
|
113
|
+
});
|
|
114
|
+
const spec = generateOpenApiSpec(topoFrom({ t }), {
|
|
115
|
+
basePath: '/api/v1/',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(spec.paths['/api/v1/entity/show']).toBeDefined();
|
|
119
|
+
expect(spec.paths['/api/v1//entity/show']).toBeUndefined();
|
|
120
|
+
});
|
|
107
121
|
});
|
|
108
122
|
|
|
109
123
|
describe('GET query parameters', () => {
|
|
@@ -156,7 +170,7 @@ describe('generateOpenApiSpec', () => {
|
|
|
156
170
|
});
|
|
157
171
|
|
|
158
172
|
describe('responses', () => {
|
|
159
|
-
test('trail with output schema → 200 response
|
|
173
|
+
test('trail with output schema → 200 response wrapped in { data }', () => {
|
|
160
174
|
const t = trail('entity.show', {
|
|
161
175
|
input: z.object({ id: z.string() }),
|
|
162
176
|
intent: 'read',
|
|
@@ -164,14 +178,21 @@ describe('generateOpenApiSpec', () => {
|
|
|
164
178
|
run: noop,
|
|
165
179
|
});
|
|
166
180
|
const spec = generateOpenApiSpec(topoFrom({ t }));
|
|
167
|
-
const
|
|
168
|
-
'responses'
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
const success = (
|
|
182
|
+
getOperation(spec, '/entity/show', 'get')['responses'] as Record<
|
|
183
|
+
string,
|
|
184
|
+
unknown
|
|
185
|
+
>
|
|
186
|
+
)['200'] as Record<string, unknown>;
|
|
187
|
+
const schema = getJsonSchema(success);
|
|
171
188
|
|
|
172
189
|
expect(success['description']).toBe('Success');
|
|
173
|
-
const schema = getJsonSchema(success);
|
|
174
190
|
expect(schema['type']).toBe('object');
|
|
191
|
+
expect(schema['required']).toEqual(['data']);
|
|
192
|
+
const dataSchema = (schema['properties'] as Record<string, unknown>)[
|
|
193
|
+
'data'
|
|
194
|
+
] as Record<string, unknown>;
|
|
195
|
+
expect(dataSchema['type']).toBe('object');
|
|
175
196
|
});
|
|
176
197
|
|
|
177
198
|
test('trail without output → 200 with no schema', () => {
|
|
@@ -200,7 +221,6 @@ describe('generateOpenApiSpec', () => {
|
|
|
200
221
|
input: { id: 'missing' },
|
|
201
222
|
name: 'not found',
|
|
202
223
|
},
|
|
203
|
-
{ error: 'ValidationError', input: {}, name: 'bad input' },
|
|
204
224
|
],
|
|
205
225
|
input: z.object({ id: z.string() }),
|
|
206
226
|
intent: 'read',
|
|
@@ -212,6 +232,43 @@ describe('generateOpenApiSpec', () => {
|
|
|
212
232
|
const responses = op['responses'] as Record<string, unknown>;
|
|
213
233
|
|
|
214
234
|
expect(responses['404']).toEqual({ description: 'NotFoundError' });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('every trail includes a default 400 validation error response', () => {
|
|
238
|
+
const t = trail('entity.show', {
|
|
239
|
+
input: z.object({ id: z.string() }),
|
|
240
|
+
intent: 'read',
|
|
241
|
+
output: z.object({ id: z.string() }),
|
|
242
|
+
run: noop,
|
|
243
|
+
});
|
|
244
|
+
const spec = generateOpenApiSpec(topoFrom({ t }));
|
|
245
|
+
const op = spec.paths['/entity/show']?.['get'] as Record<string, unknown>;
|
|
246
|
+
const fourHundred = (op['responses'] as Record<string, unknown>)[
|
|
247
|
+
'400'
|
|
248
|
+
] as Record<string, unknown>;
|
|
249
|
+
|
|
250
|
+
expect(fourHundred['description']).toBe('Validation error');
|
|
251
|
+
expect(fourHundred['content']).toBeDefined();
|
|
252
|
+
const schema = getJsonSchema(fourHundred);
|
|
253
|
+
const errorProp = (schema['properties'] as Record<string, unknown>)[
|
|
254
|
+
'error'
|
|
255
|
+
] as Record<string, unknown>;
|
|
256
|
+
expect(errorProp['type']).toBe('object');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('example-derived 400 does not override the default 400', () => {
|
|
260
|
+
const t = trail('entity.show', {
|
|
261
|
+
examples: [{ error: 'ValidationError', input: {}, name: 'bad input' }],
|
|
262
|
+
input: z.object({ id: z.string() }),
|
|
263
|
+
intent: 'read',
|
|
264
|
+
output: z.object({ id: z.string() }),
|
|
265
|
+
run: noop,
|
|
266
|
+
});
|
|
267
|
+
const spec = generateOpenApiSpec(topoFrom({ t }));
|
|
268
|
+
const op = spec.paths['/entity/show']?.['get'] as Record<string, unknown>;
|
|
269
|
+
const responses = op['responses'] as Record<string, unknown>;
|
|
270
|
+
|
|
271
|
+
// The example-derived 400 (description: 'ValidationError') overrides the default
|
|
215
272
|
expect(responses['400']).toEqual({ description: 'ValidationError' });
|
|
216
273
|
});
|
|
217
274
|
});
|
package/src/openapi.ts
CHANGED
|
@@ -175,6 +175,30 @@ const buildInputSpec = (
|
|
|
175
175
|
};
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
/** Wrap a raw output schema in the `{ data: ... }` envelope the HTTP adapter uses. */
|
|
179
|
+
const wrapInDataEnvelope = (outputSchema: JsonSchema): JsonSchema => ({
|
|
180
|
+
properties: { data: outputSchema },
|
|
181
|
+
required: ['data'],
|
|
182
|
+
type: 'object',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** Shared error response body schema: `{ error: { message, code, category } }`. */
|
|
186
|
+
const errorResponseSchema: JsonSchema = {
|
|
187
|
+
properties: {
|
|
188
|
+
error: {
|
|
189
|
+
properties: {
|
|
190
|
+
category: { type: 'string' },
|
|
191
|
+
code: { type: 'string' },
|
|
192
|
+
message: { type: 'string' },
|
|
193
|
+
},
|
|
194
|
+
required: ['message', 'code', 'category'],
|
|
195
|
+
type: 'object',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
required: ['error'],
|
|
199
|
+
type: 'object',
|
|
200
|
+
};
|
|
201
|
+
|
|
178
202
|
/** Build the 200 response entry. */
|
|
179
203
|
const buildSuccessResponse = (
|
|
180
204
|
t: Trail<unknown, unknown>
|
|
@@ -185,12 +209,25 @@ const buildSuccessResponse = (
|
|
|
185
209
|
const outputSchema = toJsonSchema(t.output);
|
|
186
210
|
return {
|
|
187
211
|
'200': {
|
|
188
|
-
content: {
|
|
212
|
+
content: {
|
|
213
|
+
'application/json': { schema: wrapInDataEnvelope(outputSchema) },
|
|
214
|
+
},
|
|
189
215
|
description: 'Success',
|
|
190
216
|
},
|
|
191
217
|
};
|
|
192
218
|
};
|
|
193
219
|
|
|
220
|
+
/** Build the default 400 validation error response. */
|
|
221
|
+
const validationErrorResponse: Record<
|
|
222
|
+
string,
|
|
223
|
+
{ content: Record<string, unknown>; description: string }
|
|
224
|
+
> = {
|
|
225
|
+
'400': {
|
|
226
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
227
|
+
description: 'Validation error',
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
194
231
|
/** Build all responses (success + error) for a trail. */
|
|
195
232
|
const buildResponses = (
|
|
196
233
|
t: Trail<unknown, unknown>
|
|
@@ -200,6 +237,7 @@ const buildResponses = (
|
|
|
200
237
|
}[];
|
|
201
238
|
return {
|
|
202
239
|
...buildSuccessResponse(t),
|
|
240
|
+
...validationErrorResponse,
|
|
203
241
|
...errorResponsesFromExamples(examples),
|
|
204
242
|
};
|
|
205
243
|
};
|
|
@@ -280,7 +318,7 @@ export const generateOpenApiSpec = (
|
|
|
280
318
|
components: { schemas: {} },
|
|
281
319
|
info: buildInfo(app.name, options),
|
|
282
320
|
openapi: '3.1.0',
|
|
283
|
-
paths: collectPaths(app, options?.basePath ?? ''),
|
|
321
|
+
paths: collectPaths(app, (options?.basePath ?? '').replace(/\/+$/, '')),
|
|
284
322
|
...(options?.servers && options.servers.length > 0
|
|
285
323
|
? { servers: options.servers }
|
|
286
324
|
: {}),
|