@orpc/openapi 0.0.0-next.1431467 → 0.0.0-next.16739f4

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/index.mjs CHANGED
@@ -1,16 +1,13 @@
1
- import { isProcedure, eachAllContractProcedure } from '@orpc/server';
2
- import { OpenApiBuilder } from 'openapi3-ts/oas31';
3
- export { OpenApiBuilder } from 'openapi3-ts/oas31';
4
- import { findDeepMatches, isObject, get, omit, group } from '@orpc/shared';
5
- import { fallbackORPCErrorStatus } from '@orpc/client';
1
+ import { isProcedure, resolveContractProcedures } from '@orpc/server';
2
+ import { fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
3
+ import { toHttpPath } from '@orpc/client/standard';
6
4
  import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
7
- import { OpenAPIJsonSerializer } from '@orpc/openapi-client/standard';
8
- export { Format as JSONSchemaFormat, keywords as JSONSchemaKeywords } from 'json-schema-typed/draft-2020-12';
9
- import { t as toOpenAPI31RoutePattern } from './shared/openapi.BHG_gu5Z.mjs';
10
- export { s as standardizeHTTPPath } from './shared/openapi.BHG_gu5Z.mjs';
5
+ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
6
+ import { isObject, findDeepMatches, toArray, clone } from '@orpc/shared';
7
+ export { Format as JSONSchemaFormat } from 'json-schema-typed/draft-2020-12';
11
8
 
12
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
13
- function setOperationExtender(o, extend) {
10
+ function customOpenAPIOperation(o, extend) {
14
11
  return new Proxy(o, {
15
12
  get(target, prop, receiver) {
16
13
  if (prop === OPERATION_EXTENDER_SYMBOL) {
@@ -20,248 +17,269 @@ function setOperationExtender(o, extend) {
20
17
  }
21
18
  });
22
19
  }
23
- function getOperationExtender(o) {
20
+ function getCustomOpenAPIOperation(o) {
24
21
  return o[OPERATION_EXTENDER_SYMBOL];
25
22
  }
26
- function extendOperation(operation, procedure) {
27
- const operationExtenders = [];
28
- for (const errorItem of Object.values(procedure["~orpc"].errorMap)) {
29
- const maybeExtender = getOperationExtender(errorItem);
23
+ function applyCustomOpenAPIOperation(operation, contract) {
24
+ const operationCustoms = [];
25
+ for (const errorItem of Object.values(contract["~orpc"].errorMap)) {
26
+ const maybeExtender = errorItem ? getCustomOpenAPIOperation(errorItem) : void 0;
30
27
  if (maybeExtender) {
31
- operationExtenders.push(maybeExtender);
28
+ operationCustoms.push(maybeExtender);
32
29
  }
33
30
  }
34
- if (isProcedure(procedure)) {
35
- for (const middleware of procedure["~orpc"].middlewares) {
36
- const maybeExtender = getOperationExtender(middleware);
31
+ if (isProcedure(contract)) {
32
+ for (const middleware of contract["~orpc"].middlewares) {
33
+ const maybeExtender = getCustomOpenAPIOperation(middleware);
37
34
  if (maybeExtender) {
38
- operationExtenders.push(maybeExtender);
35
+ operationCustoms.push(maybeExtender);
39
36
  }
40
37
  }
41
38
  }
42
39
  let currentOperation = operation;
43
- for (const extender of operationExtenders) {
44
- if (typeof extender === "function") {
45
- currentOperation = extender(currentOperation, procedure);
40
+ for (const custom of operationCustoms) {
41
+ if (typeof custom === "function") {
42
+ currentOperation = custom(currentOperation, contract);
46
43
  } else {
47
44
  currentOperation = {
48
45
  ...currentOperation,
49
- ...extender
46
+ ...custom
50
47
  };
51
48
  }
52
49
  }
53
50
  return currentOperation;
54
51
  }
55
52
 
56
- class OpenAPIContentBuilder {
57
- constructor(schemaUtils) {
58
- this.schemaUtils = schemaUtils;
59
- }
60
- build(jsonSchema, options) {
61
- const isFileSchema = this.schemaUtils.isFileSchema.bind(this.schemaUtils);
62
- const [matches, schema] = this.schemaUtils.filterSchemaBranches(jsonSchema, isFileSchema);
63
- const files = matches;
64
- const content = {};
65
- for (const file of files) {
66
- content[file.contentMediaType] = {
67
- schema: file
68
- };
69
- }
70
- const isStillHasFileSchema = findDeepMatches(isFileSchema, schema).values.length > 0;
71
- if (schema !== void 0) {
72
- content[isStillHasFileSchema ? "multipart/form-data" : "application/json"] = {
73
- schema,
74
- ...options
75
- };
76
- }
77
- return content;
78
- }
79
- }
53
+ const LOGIC_KEYWORDS = [
54
+ "$dynamicRef",
55
+ "$ref",
56
+ "additionalItems",
57
+ "additionalProperties",
58
+ "allOf",
59
+ "anyOf",
60
+ "const",
61
+ "contains",
62
+ "contentEncoding",
63
+ "contentMediaType",
64
+ "contentSchema",
65
+ "dependencies",
66
+ "dependentRequired",
67
+ "dependentSchemas",
68
+ "else",
69
+ "enum",
70
+ "exclusiveMaximum",
71
+ "exclusiveMinimum",
72
+ "format",
73
+ "if",
74
+ "items",
75
+ "maxContains",
76
+ "maximum",
77
+ "maxItems",
78
+ "maxLength",
79
+ "maxProperties",
80
+ "minContains",
81
+ "minimum",
82
+ "minItems",
83
+ "minLength",
84
+ "minProperties",
85
+ "multipleOf",
86
+ "not",
87
+ "oneOf",
88
+ "pattern",
89
+ "patternProperties",
90
+ "prefixItems",
91
+ "properties",
92
+ "propertyNames",
93
+ "required",
94
+ "then",
95
+ "type",
96
+ "unevaluatedItems",
97
+ "unevaluatedProperties",
98
+ "uniqueItems"
99
+ ];
80
100
 
81
- class OpenAPIError extends Error {
101
+ function isFileSchema(schema) {
102
+ return isObject(schema) && schema.type === "string" && typeof schema.contentMediaType === "string";
82
103
  }
83
-
84
- class OpenAPIInputStructureParser {
85
- constructor(schemaConverter, schemaUtils, pathParser) {
86
- this.schemaConverter = schemaConverter;
87
- this.schemaUtils = schemaUtils;
88
- this.pathParser = pathParser;
104
+ function isObjectSchema(schema) {
105
+ return isObject(schema) && schema.type === "object";
106
+ }
107
+ function isAnySchema(schema) {
108
+ if (schema === true) {
109
+ return true;
89
110
  }
90
- parse(contract, structure) {
91
- const [_, inputSchema] = this.schemaConverter.convert(contract["~orpc"].inputSchema, "input");
92
- const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route?.method);
93
- const httpPath = contract["~orpc"].route?.path;
94
- if (this.schemaUtils.isAnySchema(inputSchema)) {
95
- return {
96
- paramsSchema: void 0,
97
- querySchema: void 0,
98
- headersSchema: void 0,
99
- bodySchema: void 0
100
- };
101
- }
102
- if (structure === "detailed") {
103
- return this.parseDetailedSchema(inputSchema);
104
- } else {
105
- return this.parseCompactSchema(inputSchema, method, httpPath);
106
- }
111
+ if (Object.keys(schema).every((k) => !LOGIC_KEYWORDS.includes(k))) {
112
+ return true;
107
113
  }
108
- parseDetailedSchema(inputSchema) {
109
- if (!this.schemaUtils.isObjectSchema(inputSchema)) {
110
- throw new OpenAPIError(`When input structure is 'detailed', input schema must be an object.`);
111
- }
112
- if (inputSchema.properties && Object.keys(inputSchema.properties).some((key) => !["params", "query", "headers", "body"].includes(key))) {
113
- throw new OpenAPIError(`When input structure is 'detailed', input schema must be only can contain 'params', 'query', 'headers' and 'body' properties.`);
114
- }
115
- let paramsSchema = inputSchema.properties?.params;
116
- let querySchema = inputSchema.properties?.query;
117
- let headersSchema = inputSchema.properties?.headers;
118
- const bodySchema = inputSchema.properties?.body;
119
- if (paramsSchema !== void 0 && this.schemaUtils.isAnySchema(paramsSchema)) {
120
- paramsSchema = void 0;
121
- }
122
- if (paramsSchema !== void 0 && !this.schemaUtils.isObjectSchema(paramsSchema)) {
123
- throw new OpenAPIError(`When input structure is 'detailed', params schema in input schema must be an object.`);
124
- }
125
- if (querySchema !== void 0 && this.schemaUtils.isAnySchema(querySchema)) {
126
- querySchema = void 0;
127
- }
128
- if (querySchema !== void 0 && !this.schemaUtils.isObjectSchema(querySchema)) {
129
- throw new OpenAPIError(`When input structure is 'detailed', query schema in input schema must be an object.`);
130
- }
131
- if (headersSchema !== void 0 && this.schemaUtils.isAnySchema(headersSchema)) {
132
- headersSchema = void 0;
114
+ return false;
115
+ }
116
+ function separateObjectSchema(schema, separatedProperties) {
117
+ if (Object.keys(schema).some((k) => k !== "type" && k !== "properties" && k !== "required" && LOGIC_KEYWORDS.includes(k))) {
118
+ return [{ type: "object" }, schema];
119
+ }
120
+ const matched = { ...schema };
121
+ const rest = { ...schema };
122
+ matched.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
123
+ acc[key] = value;
124
+ return acc;
125
+ }, {});
126
+ matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
127
+ matched.examples = schema.examples?.map((example) => {
128
+ if (!isObject(example)) {
129
+ return example;
133
130
  }
134
- if (headersSchema !== void 0 && !this.schemaUtils.isObjectSchema(headersSchema)) {
135
- throw new OpenAPIError(`When input structure is 'detailed', headers schema in input schema must be an object.`);
131
+ return Object.entries(example).reduce((acc, [key, value]) => {
132
+ if (separatedProperties.includes(key)) {
133
+ acc[key] = value;
134
+ }
135
+ return acc;
136
+ }, {});
137
+ });
138
+ rest.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => !separatedProperties.includes(key)).reduce((acc, [key, value]) => {
139
+ acc[key] = value;
140
+ return acc;
141
+ }, {});
142
+ rest.required = schema.required?.filter((key) => !separatedProperties.includes(key));
143
+ rest.examples = schema.examples?.map((example) => {
144
+ if (!isObject(example)) {
145
+ return example;
136
146
  }
137
- return { paramsSchema, querySchema, headersSchema, bodySchema };
147
+ return Object.entries(example).reduce((acc, [key, value]) => {
148
+ if (!separatedProperties.includes(key)) {
149
+ acc[key] = value;
150
+ }
151
+ return acc;
152
+ }, {});
153
+ });
154
+ return [matched, rest];
155
+ }
156
+ function filterSchemaBranches(schema, check, matches = []) {
157
+ if (check(schema)) {
158
+ matches.push(schema);
159
+ return [matches, void 0];
138
160
  }
139
- parseCompactSchema(inputSchema, method, httpPath) {
140
- const dynamic = httpPath ? this.pathParser.parseDynamicParams(httpPath) : [];
141
- if (dynamic.length === 0) {
142
- if (method === "GET") {
143
- let querySchema = inputSchema;
144
- if (querySchema !== void 0 && this.schemaUtils.isAnySchema(querySchema)) {
145
- querySchema = void 0;
161
+ if (isObject(schema)) {
162
+ for (const keyword of ["anyOf", "oneOf"]) {
163
+ if (schema[keyword] && Object.keys(schema).every(
164
+ (k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
165
+ )) {
166
+ const rest = schema[keyword].map((s) => filterSchemaBranches(s, check, matches)[1]).filter((v) => !!v);
167
+ if (rest.length === 1 && typeof rest[0] === "object") {
168
+ return [matches, { ...schema, [keyword]: void 0, ...rest[0] }];
146
169
  }
147
- if (querySchema !== void 0 && !this.schemaUtils.isObjectSchema(querySchema)) {
148
- throw new OpenAPIError(`When input structure is 'compact' and method is 'GET', input schema must be an object.`);
149
- }
150
- return {
151
- paramsSchema: void 0,
152
- querySchema,
153
- headersSchema: void 0,
154
- bodySchema: void 0
155
- };
170
+ return [matches, { ...schema, [keyword]: rest }];
156
171
  }
157
- return {
158
- paramsSchema: void 0,
159
- querySchema: void 0,
160
- headersSchema: void 0,
161
- bodySchema: inputSchema
162
- };
163
- }
164
- if (!this.schemaUtils.isObjectSchema(inputSchema)) {
165
- throw new OpenAPIError(`When input structure is 'compact' and path has dynamic parameters, input schema must be an object.`);
166
172
  }
167
- const [params, rest] = this.schemaUtils.separateObjectSchema(inputSchema, dynamic.map((v) => v.name));
168
- return {
169
- paramsSchema: params,
170
- querySchema: method === "GET" ? rest : void 0,
171
- headersSchema: void 0,
172
- bodySchema: method !== "GET" ? rest : void 0
173
- };
174
173
  }
174
+ return [matches, schema];
175
+ }
176
+ function applySchemaOptionality(required, schema) {
177
+ if (required) {
178
+ return schema;
179
+ }
180
+ return {
181
+ anyOf: [
182
+ schema,
183
+ { not: {} }
184
+ ]
185
+ };
175
186
  }
176
187
 
177
- class OpenAPIOutputStructureParser {
178
- constructor(schemaConverter, schemaUtils) {
179
- this.schemaConverter = schemaConverter;
180
- this.schemaUtils = schemaUtils;
188
+ function toOpenAPIPath(path) {
189
+ return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
190
+ }
191
+ function toOpenAPIMethod(method) {
192
+ return method.toLocaleLowerCase();
193
+ }
194
+ function toOpenAPIContent(schema) {
195
+ const content = {};
196
+ const [matches, restSchema] = filterSchemaBranches(schema, isFileSchema);
197
+ for (const file of matches) {
198
+ content[file.contentMediaType] = {
199
+ schema: toOpenAPISchema(file)
200
+ };
181
201
  }
182
- parse(contract, structure) {
183
- const [_, outputSchema] = this.schemaConverter.convert(contract["~orpc"].outputSchema, "output");
184
- if (this.schemaUtils.isAnySchema(outputSchema)) {
185
- return {
186
- headersSchema: void 0,
187
- bodySchema: void 0
202
+ if (restSchema !== void 0) {
203
+ content["application/json"] = {
204
+ schema: toOpenAPISchema(restSchema)
205
+ };
206
+ const isStillHasFileSchema = findDeepMatches((v) => isObject(v) && isFileSchema(v), restSchema).values.length > 0;
207
+ if (isStillHasFileSchema) {
208
+ content["multipart/form-data"] = {
209
+ schema: toOpenAPISchema(restSchema)
188
210
  };
189
211
  }
190
- if (structure === "detailed") {
191
- return this.parseDetailedSchema(outputSchema);
192
- } else {
193
- return this.parseCompactSchema(outputSchema);
194
- }
195
212
  }
196
- parseDetailedSchema(outputSchema) {
197
- if (!this.schemaUtils.isObjectSchema(outputSchema)) {
198
- throw new OpenAPIError(`When output structure is 'detailed', output schema must be an object.`);
199
- }
200
- if (outputSchema.properties && Object.keys(outputSchema.properties).some((key) => !["headers", "body"].includes(key))) {
201
- throw new OpenAPIError(`When output structure is 'detailed', output schema must be only can contain 'headers' and 'body' properties.`);
202
- }
203
- let headersSchema = outputSchema.properties?.headers;
204
- const bodySchema = outputSchema.properties?.body;
205
- if (headersSchema !== void 0 && this.schemaUtils.isAnySchema(headersSchema)) {
206
- headersSchema = void 0;
207
- }
208
- if (headersSchema !== void 0 && !this.schemaUtils.isObjectSchema(headersSchema)) {
209
- throw new OpenAPIError(`When output structure is 'detailed', headers schema in output schema must be an object.`);
213
+ return content;
214
+ }
215
+ function toOpenAPIEventIteratorContent([yieldsRequired, yieldsSchema], [returnsRequired, returnsSchema]) {
216
+ return {
217
+ "text/event-stream": {
218
+ schema: toOpenAPISchema({
219
+ oneOf: [
220
+ {
221
+ type: "object",
222
+ properties: {
223
+ event: { const: "message" },
224
+ data: yieldsSchema,
225
+ id: { type: "string" },
226
+ retry: { type: "number" }
227
+ },
228
+ required: yieldsRequired ? ["event", "data"] : ["event"]
229
+ },
230
+ {
231
+ type: "object",
232
+ properties: {
233
+ event: { const: "done" },
234
+ data: returnsSchema,
235
+ id: { type: "string" },
236
+ retry: { type: "number" }
237
+ },
238
+ required: returnsRequired ? ["event", "data"] : ["event"]
239
+ },
240
+ {
241
+ type: "object",
242
+ properties: {
243
+ event: { const: "error" },
244
+ data: {},
245
+ id: { type: "string" },
246
+ retry: { type: "number" }
247
+ },
248
+ required: ["event"]
249
+ }
250
+ ]
251
+ })
210
252
  }
211
- return { headersSchema, bodySchema };
212
- }
213
- parseCompactSchema(outputSchema) {
214
- return {
215
- headersSchema: void 0,
216
- bodySchema: outputSchema
217
- };
253
+ };
254
+ }
255
+ function toOpenAPIParameters(schema, parameterIn) {
256
+ const parameters = [];
257
+ for (const key in schema.properties) {
258
+ const keySchema = schema.properties[key];
259
+ parameters.push({
260
+ name: key,
261
+ in: parameterIn,
262
+ required: schema.required?.includes(key),
263
+ style: parameterIn === "query" ? "deepObject" : void 0,
264
+ explode: parameterIn === "query" ? true : void 0,
265
+ schema: toOpenAPISchema(keySchema)
266
+ });
218
267
  }
268
+ return parameters;
219
269
  }
220
-
221
- class OpenAPIParametersBuilder {
222
- build(paramIn, jsonSchema, options) {
223
- const parameters = [];
224
- for (const name in jsonSchema.properties) {
225
- const schema = jsonSchema.properties[name];
226
- const paramExamples = jsonSchema.examples?.filter((example) => {
227
- return isObject(example) && name in example;
228
- }).map((example) => {
229
- return example[name];
230
- });
231
- const paramSchema = {
232
- examples: paramExamples?.length ? paramExamples : void 0,
233
- ...schema === true ? {} : schema === false ? { not: {} } : schema
234
- };
235
- const paramExample = get(options?.example, [name]);
236
- parameters.push({
237
- name,
238
- in: paramIn,
239
- required: typeof options?.required === "boolean" ? options.required : jsonSchema.required?.includes(name) ?? false,
240
- schema: paramSchema,
241
- example: paramExample,
242
- style: options?.style
243
- });
244
- }
245
- return parameters;
270
+ function checkParamsSchema(schema, params) {
271
+ const properties = Object.keys(schema.properties ?? {});
272
+ const required = schema.required ?? [];
273
+ if (properties.length !== params.length || properties.some((v) => !params.includes(v))) {
274
+ return false;
246
275
  }
247
- buildHeadersObject(jsonSchema, options) {
248
- const parameters = this.build("header", jsonSchema, options);
249
- const headersObject = {};
250
- for (const param of parameters) {
251
- headersObject[param.name] = omit(param, ["name", "in"]);
252
- }
253
- return headersObject;
276
+ if (required.length !== params.length || required.some((v) => !params.includes(v))) {
277
+ return false;
254
278
  }
279
+ return true;
255
280
  }
256
-
257
- class OpenAPIPathParser {
258
- parseDynamicParams(path) {
259
- const raws = path.match(/\{([^}]+)\}/g) ?? [];
260
- return raws.map((raw) => {
261
- const name = raw.slice(1, -1).split(":")[0];
262
- return { name, raw };
263
- });
264
- }
281
+ function toOpenAPISchema(schema) {
282
+ return schema === true ? {} : schema === false ? { not: {} } : schema;
265
283
  }
266
284
 
267
285
  class CompositeSchemaConverter {
@@ -269,324 +287,240 @@ class CompositeSchemaConverter {
269
287
  constructor(converters) {
270
288
  this.converters = converters;
271
289
  }
272
- convert(schema, strategy) {
290
+ async convert(schema, options) {
273
291
  for (const converter of this.converters) {
274
- if (converter.condition(schema, strategy)) {
275
- return converter.convert(schema, strategy);
292
+ if (await converter.condition(schema, options)) {
293
+ return converter.convert(schema, options);
276
294
  }
277
295
  }
278
296
  return [false, {}];
279
297
  }
280
298
  }
281
299
 
282
- const NON_LOGIC_KEYWORDS = [
283
- // Core Documentation Keywords
284
- "$anchor",
285
- "$comment",
286
- "$defs",
287
- "$id",
288
- "title",
289
- "description",
290
- // Value Keywords
291
- "default",
292
- "deprecated",
293
- "examples",
294
- // Metadata Keywords
295
- "$schema",
296
- "definitions",
297
- // Legacy, but still used
298
- "readOnly",
299
- "writeOnly",
300
- // Display and UI Hints
301
- "contentMediaType",
302
- "contentEncoding",
303
- "format",
304
- // Custom Extensions
305
- "$vocabulary",
306
- "$dynamicAnchor",
307
- "$dynamicRef"
308
- ];
309
-
310
- class SchemaUtils {
311
- isFileSchema(schema) {
312
- return isObject(schema) && schema.type === "string" && typeof schema.contentMediaType === "string";
313
- }
314
- isObjectSchema(schema) {
315
- return isObject(schema) && schema.type === "object";
316
- }
317
- isAnySchema(schema) {
318
- return schema === true || Object.keys(schema).filter((key) => !NON_LOGIC_KEYWORDS.includes(key)).length === 0;
300
+ class OpenAPIGeneratorError extends Error {
301
+ }
302
+ class OpenAPIGenerator {
303
+ serializer;
304
+ converter;
305
+ constructor(options = {}) {
306
+ this.serializer = new StandardOpenAPIJsonSerializer(options);
307
+ this.converter = new CompositeSchemaConverter(toArray(options.schemaConverters));
319
308
  }
320
- isUndefinableSchema(schema) {
321
- const [matches] = this.filterSchemaBranches(schema, (schema2) => {
322
- if (typeof schema2 === "boolean") {
323
- return schema2;
324
- }
325
- return Object.keys(schema2).filter((key) => !NON_LOGIC_KEYWORDS.includes(key)).length === 0;
309
+ /**
310
+ * Generates OpenAPI specifications from oRPC routers/contracts.
311
+ *
312
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
313
+ */
314
+ async generate(router, base) {
315
+ const doc = clone(base);
316
+ doc.openapi = "3.1.1";
317
+ const contracts = [];
318
+ await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
319
+ contracts.push({ contract, path });
326
320
  });
327
- return matches.length > 0;
321
+ const errors = [];
322
+ for (const { contract, path } of contracts) {
323
+ const operationId = path.join(".");
324
+ try {
325
+ const def = contract["~orpc"];
326
+ const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
327
+ const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
328
+ const operationObjectRef = {
329
+ operationId,
330
+ summary: def.route.summary,
331
+ description: def.route.description,
332
+ deprecated: def.route.deprecated,
333
+ tags: def.route.tags?.map((tag) => tag)
334
+ };
335
+ await this.#request(operationObjectRef, def);
336
+ await this.#successResponse(operationObjectRef, def);
337
+ await this.#errorResponse(operationObjectRef, def);
338
+ doc.paths ??= {};
339
+ doc.paths[httpPath] ??= {};
340
+ doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
341
+ } catch (e) {
342
+ if (!(e instanceof OpenAPIGeneratorError)) {
343
+ throw e;
344
+ }
345
+ errors.push(
346
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
347
+ ${e.message}`
348
+ );
349
+ }
350
+ }
351
+ if (errors.length) {
352
+ throw new OpenAPIGeneratorError(
353
+ `Some error occurred during OpenAPI generation:
354
+
355
+ ${errors.join("\n\n")}`
356
+ );
357
+ }
358
+ return this.serializer.serialize(doc)[0];
328
359
  }
329
- separateObjectSchema(schema, separatedProperties) {
330
- const matched = { ...schema };
331
- const rest = { ...schema };
332
- matched.properties = Object.entries(schema.properties ?? {}).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
333
- acc[key] = value;
334
- return acc;
335
- }, {});
336
- matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
337
- matched.examples = schema.examples?.map((example) => {
338
- if (!isObject(example)) {
339
- return example;
360
+ async #request(ref, def) {
361
+ const method = fallbackContractConfig("defaultMethod", def.route.method);
362
+ const details = getEventIteratorSchemaDetails(def.inputSchema);
363
+ if (details) {
364
+ ref.requestBody = {
365
+ required: true,
366
+ content: toOpenAPIEventIteratorContent(
367
+ await this.converter.convert(details.yields, { strategy: "input" }),
368
+ await this.converter.convert(details.returns, { strategy: "input" })
369
+ )
370
+ };
371
+ return;
372
+ }
373
+ const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
374
+ const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
375
+ let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
376
+ if (isAnySchema(schema) && !dynamicParams?.length) {
377
+ return;
378
+ }
379
+ if (inputStructure === "compact") {
380
+ if (dynamicParams?.length) {
381
+ const error2 = new OpenAPIGeneratorError(
382
+ 'When input structure is "compact", and path has dynamic params, input schema must be an object with all dynamic params as required.'
383
+ );
384
+ if (!isObjectSchema(schema)) {
385
+ throw error2;
386
+ }
387
+ const [paramsSchema, rest] = separateObjectSchema(schema, dynamicParams);
388
+ schema = rest;
389
+ required = rest.required ? rest.required.length !== 0 : false;
390
+ if (!checkParamsSchema(paramsSchema, dynamicParams)) {
391
+ throw error2;
392
+ }
393
+ ref.parameters ??= [];
394
+ ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
340
395
  }
341
- return Object.entries(example).reduce((acc, [key, value]) => {
342
- if (separatedProperties.includes(key)) {
343
- acc[key] = value;
396
+ if (method === "GET") {
397
+ if (!isObjectSchema(schema)) {
398
+ throw new OpenAPIGeneratorError(
399
+ 'When method is "GET", input schema must satisfy: object | any | unknown'
400
+ );
344
401
  }
345
- return acc;
346
- }, {});
347
- });
348
- rest.properties = Object.entries(schema.properties ?? {}).filter(([key]) => !separatedProperties.includes(key)).reduce((acc, [key, value]) => {
349
- acc[key] = value;
350
- return acc;
351
- }, {});
352
- rest.required = schema.required?.filter((key) => !separatedProperties.includes(key));
353
- rest.examples = schema.examples?.map((example) => {
354
- if (!isObject(example)) {
355
- return example;
402
+ ref.parameters ??= [];
403
+ ref.parameters.push(...toOpenAPIParameters(schema, "query"));
404
+ } else {
405
+ ref.requestBody = {
406
+ required,
407
+ content: toOpenAPIContent(schema)
408
+ };
356
409
  }
357
- return Object.entries(example).reduce((acc, [key, value]) => {
358
- if (!separatedProperties.includes(key)) {
359
- acc[key] = value;
410
+ return;
411
+ }
412
+ const error = new OpenAPIGeneratorError(
413
+ 'When input structure is "detailed", input schema must satisfy: { params?: Record<string, unknown>, query?: Record<string, unknown>, headers?: Record<string, unknown>, body?: unknown }'
414
+ );
415
+ if (!isObjectSchema(schema)) {
416
+ throw error;
417
+ }
418
+ if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
419
+ throw new OpenAPIGeneratorError(
420
+ 'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
421
+ );
422
+ }
423
+ for (const from of ["params", "query", "headers"]) {
424
+ const fromSchema = schema.properties?.[from];
425
+ if (fromSchema !== void 0) {
426
+ if (!isObjectSchema(fromSchema)) {
427
+ throw error;
360
428
  }
361
- return acc;
362
- }, {});
363
- });
364
- return [matched, rest];
429
+ const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
430
+ ref.parameters ??= [];
431
+ ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
432
+ }
433
+ }
434
+ if (schema.properties?.body !== void 0) {
435
+ ref.requestBody = {
436
+ required: schema.required?.includes("body"),
437
+ content: toOpenAPIContent(schema.properties.body)
438
+ };
439
+ }
365
440
  }
366
- filterSchemaBranches(schema, check, matches = []) {
367
- if (check(schema)) {
368
- matches.push(schema);
369
- return [matches, void 0];
441
+ async #successResponse(ref, def) {
442
+ const outputSchema = def.outputSchema;
443
+ const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
444
+ const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
445
+ const eventIteratorSchemaDetails = getEventIteratorSchemaDetails(outputSchema);
446
+ const outputStructure = fallbackContractConfig("defaultOutputStructure", def.route.outputStructure);
447
+ if (eventIteratorSchemaDetails) {
448
+ ref.responses ??= {};
449
+ ref.responses[status] = {
450
+ description,
451
+ content: toOpenAPIEventIteratorContent(
452
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
453
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
454
+ )
455
+ };
456
+ return;
370
457
  }
371
- if (typeof schema === "boolean") {
372
- return [matches, schema];
458
+ const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
459
+ ref.responses ??= {};
460
+ ref.responses[status] = {
461
+ description
462
+ };
463
+ if (outputStructure === "compact") {
464
+ ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
465
+ return;
373
466
  }
374
- if (schema.anyOf && Object.keys(schema).every(
375
- (k) => k === "anyOf" || NON_LOGIC_KEYWORDS.includes(k)
376
- )) {
377
- const anyOf = schema.anyOf.map((s) => this.filterSchemaBranches(s, check, matches)[1]).filter((v) => !!v);
378
- if (anyOf.length === 1 && typeof anyOf[0] === "object") {
379
- return [matches, { ...schema, anyOf: void 0, ...anyOf[0] }];
380
- }
381
- return [matches, { ...schema, anyOf }];
467
+ const error = new OpenAPIGeneratorError(
468
+ 'When output structure is "detailed", output schema must satisfy: { headers?: Record<string, unknown>, body?: unknown }'
469
+ );
470
+ if (!isObjectSchema(json)) {
471
+ throw error;
382
472
  }
383
- if (schema.oneOf && Object.keys(schema).every(
384
- (k) => k === "oneOf" || NON_LOGIC_KEYWORDS.includes(k)
385
- )) {
386
- const oneOf = schema.oneOf.map((s) => this.filterSchemaBranches(s, check, matches)[1]).filter((v) => !!v);
387
- if (oneOf.length === 1 && typeof oneOf[0] === "object") {
388
- return [matches, { ...schema, oneOf: void 0, ...oneOf[0] }];
473
+ if (json.properties?.headers !== void 0) {
474
+ if (!isObjectSchema(json.properties.headers)) {
475
+ throw error;
476
+ }
477
+ for (const key in json.properties.headers.properties) {
478
+ ref.responses[status].headers ??= {};
479
+ ref.responses[status].headers[key] = {
480
+ schema: toOpenAPISchema(json.properties.headers.properties[key]),
481
+ required: json.properties.headers.required?.includes(key)
482
+ };
389
483
  }
390
- return [matches, { ...schema, oneOf }];
391
484
  }
392
- return [matches, schema];
393
- }
394
- }
395
-
396
- class OpenAPIGenerator {
397
- contentBuilder;
398
- parametersBuilder;
399
- schemaConverter;
400
- schemaUtils;
401
- jsonSerializer;
402
- pathParser;
403
- inputStructureParser;
404
- outputStructureParser;
405
- errorHandlerStrategy;
406
- ignoreUndefinedPathProcedures;
407
- considerMissingTagDefinitionAsError;
408
- strictErrorResponses;
409
- constructor(options) {
410
- this.parametersBuilder = options?.parametersBuilder ?? new OpenAPIParametersBuilder();
411
- this.schemaConverter = new CompositeSchemaConverter(options?.schemaConverters ?? []);
412
- this.schemaUtils = options?.schemaUtils ?? new SchemaUtils();
413
- this.jsonSerializer = options?.jsonSerializer ?? new OpenAPIJsonSerializer();
414
- this.contentBuilder = options?.contentBuilder ?? new OpenAPIContentBuilder(this.schemaUtils);
415
- this.pathParser = new OpenAPIPathParser();
416
- this.inputStructureParser = options?.inputStructureParser ?? new OpenAPIInputStructureParser(this.schemaConverter, this.schemaUtils, this.pathParser);
417
- this.outputStructureParser = options?.outputStructureParser ?? new OpenAPIOutputStructureParser(this.schemaConverter, this.schemaUtils);
418
- this.errorHandlerStrategy = options?.errorHandlerStrategy ?? "throw";
419
- this.ignoreUndefinedPathProcedures = options?.ignoreUndefinedPathProcedures ?? false;
420
- this.considerMissingTagDefinitionAsError = options?.considerMissingTagDefinitionAsError ?? false;
421
- this.strictErrorResponses = options?.strictErrorResponses ?? true;
485
+ if (json.properties?.body !== void 0) {
486
+ ref.responses[status].content = toOpenAPIContent(
487
+ applySchemaOptionality(json.required?.includes("body") ?? false, json.properties.body)
488
+ );
489
+ }
422
490
  }
423
- async generate(router, doc) {
424
- const builder = new OpenApiBuilder({
425
- ...doc,
426
- openapi: "3.1.1"
427
- });
428
- const rootTags = doc.tags?.map((tag) => tag.name) ?? [];
429
- await eachAllContractProcedure({
430
- path: [],
431
- router
432
- }, ({ contract, path }) => {
433
- try {
434
- const def = contract["~orpc"];
435
- if (this.ignoreUndefinedPathProcedures && def.route?.path === void 0) {
436
- return;
437
- }
438
- const method = fallbackContractConfig("defaultMethod", def.route?.method);
439
- const httpPath = def.route?.path ? toOpenAPI31RoutePattern(def.route?.path) : `/${path.map(encodeURIComponent).join("/")}`;
440
- const { parameters, requestBody } = (() => {
441
- const eventIteratorSchemaDetails = getEventIteratorSchemaDetails(def.inputSchema);
442
- if (eventIteratorSchemaDetails) {
443
- const requestBody3 = {
444
- required: true,
445
- content: {
446
- "text/event-stream": {
447
- schema: {
448
- oneOf: [
449
- {
450
- type: "object",
451
- properties: {
452
- event: { type: "string", const: "message" },
453
- data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, "input")[1],
454
- id: { type: "string" },
455
- retry: { type: "number" }
456
- },
457
- required: ["event", "data"]
458
- },
459
- {
460
- type: "object",
461
- properties: {
462
- event: { type: "string", const: "done" },
463
- data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, "input")[1],
464
- id: { type: "string" },
465
- retry: { type: "number" }
466
- },
467
- required: ["event", "data"]
468
- },
469
- {
470
- type: "object",
471
- properties: {
472
- event: { type: "string", const: "error" },
473
- data: {},
474
- id: { type: "string" },
475
- retry: { type: "number" }
476
- },
477
- required: ["event", "data"]
478
- }
479
- ]
480
- }
481
- }
482
- }
483
- };
484
- return { requestBody: requestBody3, parameters: [] };
485
- }
486
- const inputStructure = fallbackContractConfig("defaultInputStructure", def.route?.inputStructure);
487
- const { paramsSchema, querySchema, headersSchema, bodySchema } = this.inputStructureParser.parse(contract, inputStructure);
488
- const params = paramsSchema ? this.parametersBuilder.build("path", paramsSchema, {
489
- required: true
490
- }) : [];
491
- const query = querySchema ? this.parametersBuilder.build("query", querySchema) : [];
492
- const headers = headersSchema ? this.parametersBuilder.build("header", headersSchema) : [];
493
- const parameters2 = [...params, ...query, ...headers];
494
- const requestBody2 = bodySchema !== void 0 ? {
495
- required: this.schemaUtils.isUndefinableSchema(bodySchema),
496
- content: this.contentBuilder.build(bodySchema)
497
- } : void 0;
498
- return { parameters: parameters2, requestBody: requestBody2 };
499
- })();
500
- const { responses } = (() => {
501
- const eventIteratorSchemaDetails = getEventIteratorSchemaDetails(def.outputSchema);
502
- if (eventIteratorSchemaDetails) {
503
- const responses3 = {};
504
- responses3[fallbackContractConfig("defaultSuccessStatus", def.route?.successStatus)] = {
505
- description: fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription),
506
- content: {
507
- "text/event-stream": {
508
- schema: {
509
- oneOf: [
510
- {
511
- type: "object",
512
- properties: {
513
- event: { type: "string", const: "message" },
514
- data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, "input")[1],
515
- id: { type: "string" },
516
- retry: { type: "number" }
517
- },
518
- required: ["event", "data"]
519
- },
520
- {
521
- type: "object",
522
- properties: {
523
- event: { type: "string", const: "done" },
524
- data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, "input")[1],
525
- id: { type: "string" },
526
- retry: { type: "number" }
527
- },
528
- required: ["event", "data"]
529
- },
530
- {
531
- type: "object",
532
- properties: {
533
- event: { type: "string", const: "error" },
534
- data: {},
535
- id: { type: "string" },
536
- retry: { type: "number" }
537
- },
538
- required: ["event", "data"]
539
- }
540
- ]
541
- }
542
- }
543
- }
544
- };
545
- return { responses: responses3 };
546
- }
547
- const outputStructure = fallbackContractConfig("defaultOutputStructure", def.route?.outputStructure);
548
- const { headersSchema: resHeadersSchema, bodySchema: resBodySchema } = this.outputStructureParser.parse(contract, outputStructure);
549
- const responses2 = {};
550
- responses2[fallbackContractConfig("defaultSuccessStatus", def.route?.successStatus)] = {
551
- description: fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription),
552
- content: resBodySchema !== void 0 ? this.contentBuilder.build(resBodySchema) : void 0,
553
- headers: resHeadersSchema !== void 0 ? this.parametersBuilder.buildHeadersObject(resHeadersSchema) : void 0
554
- };
555
- return { responses: responses2 };
556
- })();
557
- const errors = group(Object.entries(def.errorMap ?? {}).filter(([_, config]) => config).map(([code, config]) => ({
558
- ...config,
559
- code,
560
- status: fallbackORPCErrorStatus(code, config?.status)
561
- })), (error) => error.status);
562
- for (const status in errors) {
563
- const configs = errors[status];
564
- if (!configs || configs.length === 0) {
565
- continue;
566
- }
567
- const schemas = configs.map(({ data, code, message }) => {
568
- const json = {
569
- type: "object",
570
- properties: {
571
- defined: { const: true },
572
- code: { const: code },
573
- status: { const: Number(status) },
574
- message: { type: "string", default: message },
575
- data: {}
576
- },
577
- required: ["defined", "code", "status", "message"]
578
- };
579
- if (data) {
580
- const dataJson = this.schemaConverter.convert(data, "output")[1];
581
- json.properties.data = dataJson;
582
- if (!this.schemaUtils.isUndefinableSchema(dataJson)) {
583
- json.required.push("data");
584
- }
585
- }
586
- return json;
587
- });
588
- if (this.strictErrorResponses) {
589
- schemas.push({
491
+ async #errorResponse(ref, def) {
492
+ const errorMap = def.errorMap;
493
+ const errors = {};
494
+ for (const code in errorMap) {
495
+ const config = errorMap[code];
496
+ if (!config) {
497
+ continue;
498
+ }
499
+ const status = fallbackORPCErrorStatus(code, config.status);
500
+ const message = fallbackORPCErrorMessage(code, config.message);
501
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
502
+ errors[status] ??= [];
503
+ errors[status].push({
504
+ type: "object",
505
+ properties: {
506
+ defined: { const: true },
507
+ code: { const: code },
508
+ status: { const: status },
509
+ message: { type: "string", default: message },
510
+ data: dataSchema
511
+ },
512
+ required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
513
+ });
514
+ }
515
+ ref.responses ??= {};
516
+ for (const status in errors) {
517
+ const schemas = errors[status];
518
+ ref.responses[status] = {
519
+ description: status,
520
+ content: toOpenAPIContent({
521
+ oneOf: [
522
+ ...schemas,
523
+ {
590
524
  type: "object",
591
525
  properties: {
592
526
  defined: { const: false },
@@ -596,61 +530,16 @@ class OpenAPIGenerator {
596
530
  data: {}
597
531
  },
598
532
  required: ["defined", "code", "status", "message"]
599
- });
600
- }
601
- const contentSchema = schemas.length === 1 ? schemas[0] : {
602
- oneOf: schemas
603
- };
604
- responses[status] = {
605
- description: status,
606
- content: this.contentBuilder.build(contentSchema)
607
- };
608
- }
609
- if (this.considerMissingTagDefinitionAsError && def.route?.tags) {
610
- const missingTag = def.route?.tags.find((tag) => !rootTags.includes(tag));
611
- if (missingTag !== void 0) {
612
- throw new OpenAPIError(
613
- `Tag "${missingTag}" is missing definition. Please define it in OpenAPI root tags object`
614
- );
615
- }
616
- }
617
- const operation = {
618
- summary: def.route?.summary,
619
- description: def.route?.description,
620
- deprecated: def.route?.deprecated,
621
- tags: def.route?.tags ? [...def.route.tags] : void 0,
622
- operationId: path.join("."),
623
- parameters: parameters.length ? parameters : void 0,
624
- requestBody,
625
- responses
626
- };
627
- const extendedOperation = extendOperation(operation, contract);
628
- builder.addPath(httpPath, {
629
- [method.toLocaleLowerCase()]: extendedOperation
630
- });
631
- } catch (e) {
632
- if (e instanceof OpenAPIError) {
633
- const error = new OpenAPIError(`
634
- Generate OpenAPI Error: ${e.message}
635
- Happened at path: ${path.join(".")}
636
- `, { cause: e });
637
- if (this.errorHandlerStrategy === "throw") {
638
- throw error;
639
- }
640
- if (this.errorHandlerStrategy === "log") {
641
- console.error(error);
642
- }
643
- } else {
644
- throw e;
645
- }
646
- }
647
- });
648
- return this.jsonSerializer.serialize(builder.getSpec())[0];
533
+ }
534
+ ]
535
+ })
536
+ };
537
+ }
649
538
  }
650
539
  }
651
540
 
652
541
  const oo = {
653
- spec: setOperationExtender
542
+ spec: customOpenAPIOperation
654
543
  };
655
544
 
656
- export { CompositeSchemaConverter, NON_LOGIC_KEYWORDS, OpenAPIContentBuilder, OpenAPIGenerator, OpenAPIParametersBuilder, OpenAPIPathParser, SchemaUtils, extendOperation, getOperationExtender, oo, setOperationExtender, toOpenAPI31RoutePattern };
545
+ export { CompositeSchemaConverter, LOGIC_KEYWORDS, OpenAPIGenerator, applyCustomOpenAPIOperation, applySchemaOptionality, checkParamsSchema, customOpenAPIOperation, filterSchemaBranches, getCustomOpenAPIOperation, isAnySchema, isFileSchema, isObjectSchema, oo, separateObjectSchema, toOpenAPIContent, toOpenAPIEventIteratorContent, toOpenAPIMethod, toOpenAPIParameters, toOpenAPIPath, toOpenAPISchema };