@sdk-it/typescript 0.14.5 → 0.16.0
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.js +818 -747
- package/dist/index.js.map +4 -4
- package/dist/lib/client.d.ts +1 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/emitters/interface.d.ts.map +1 -1
- package/dist/lib/emitters/zod.d.ts +4 -4
- package/dist/lib/emitters/zod.d.ts.map +1 -1
- package/dist/lib/generate.d.ts +1 -0
- package/dist/lib/generate.d.ts.map +1 -1
- package/dist/lib/generator.d.ts +5 -8
- package/dist/lib/generator.d.ts.map +1 -1
- package/dist/lib/readme-generator.d.ts.map +1 -1
- package/dist/lib/sdk.d.ts +18 -13
- package/dist/lib/sdk.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +7 -14
- package/dist/lib/utils.d.ts.map +1 -1
- package/package.json +2 -3
- package/dist/lib/json-zod.d.ts +0 -10
- package/dist/lib/json-zod.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
// packages/typescript/src/lib/generate.ts
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join as join2 } from "node:path";
|
|
3
3
|
import { npmRunPathEnv } from "npm-run-path";
|
|
4
|
+
import { spinalcase as spinalcase3 } from "stringcase";
|
|
4
5
|
import { getFolderExports, methods, writeFiles } from "@sdk-it/core";
|
|
5
6
|
|
|
6
|
-
// packages/typescript/src/lib/generator.ts
|
|
7
|
-
import { get as get2, merge } from "lodash-es";
|
|
8
|
-
import { camelcase as camelcase2, pascalcase, spinalcase as spinalcase2 } from "stringcase";
|
|
9
|
-
import { removeDuplicates as removeDuplicates3 } from "@sdk-it/core";
|
|
10
|
-
|
|
11
|
-
// packages/typescript/src/lib/utils.ts
|
|
12
|
-
import { get } from "lodash-es";
|
|
13
|
-
import { removeDuplicates as removeDuplicates2 } from "@sdk-it/core";
|
|
14
|
-
|
|
15
|
-
// packages/typescript/src/lib/sdk.ts
|
|
16
|
-
import { camelcase, spinalcase } from "stringcase";
|
|
17
|
-
import { removeDuplicates, toLitObject as toLitObject2 } from "@sdk-it/core";
|
|
18
|
-
|
|
19
7
|
// packages/typescript/src/lib/client.ts
|
|
20
8
|
import { toLitObject } from "@sdk-it/core";
|
|
21
9
|
var client_default = (spec) => {
|
|
@@ -42,8 +30,8 @@ var client_default = (spec) => {
|
|
|
42
30
|
return `
|
|
43
31
|
import { fetchType, sendRequest } from './http/${spec.makeImport("send-request")}';
|
|
44
32
|
import z from 'zod';
|
|
45
|
-
import type { Endpoints } from '
|
|
46
|
-
import schemas from '
|
|
33
|
+
import type { Endpoints } from './api/${spec.makeImport("schemas")}';
|
|
34
|
+
import schemas from './api/${spec.makeImport("schemas")}';
|
|
47
35
|
import {
|
|
48
36
|
createBaseUrlInterceptor,
|
|
49
37
|
createHeadersInterceptor,
|
|
@@ -97,290 +85,278 @@ export class ${spec.name} {
|
|
|
97
85
|
}`;
|
|
98
86
|
};
|
|
99
87
|
|
|
100
|
-
// packages/typescript/src/lib/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
88
|
+
// packages/typescript/src/lib/generator.ts
|
|
89
|
+
import { get as get2, merge } from "lodash-es";
|
|
90
|
+
import { join } from "node:path";
|
|
91
|
+
import { camelcase as camelcase2, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
|
|
92
|
+
import { followRef as followRef4, forEachOperation, isRef as isRef5 } from "@sdk-it/core";
|
|
93
|
+
|
|
94
|
+
// packages/typescript/src/lib/emitters/zod.ts
|
|
95
|
+
import { cleanRef, followRef, isRef, parseRef } from "@sdk-it/core";
|
|
96
|
+
var ZodDeserialzer = class {
|
|
97
|
+
circularRefTracker = /* @__PURE__ */ new Set();
|
|
98
|
+
#spec;
|
|
99
|
+
#onRef;
|
|
100
|
+
constructor(spec, onRef) {
|
|
101
|
+
this.#spec = spec;
|
|
102
|
+
this.#onRef = onRef;
|
|
113
103
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Handle objects (properties, additionalProperties).
|
|
106
|
+
*/
|
|
107
|
+
object(schema) {
|
|
108
|
+
const properties = schema.properties || {};
|
|
109
|
+
const propEntries = Object.entries(properties).map(([key, propSchema]) => {
|
|
110
|
+
const isRequired = (schema.required ?? []).includes(key);
|
|
111
|
+
return `'${key}': ${this.handle(propSchema, isRequired)}`;
|
|
112
|
+
});
|
|
113
|
+
let additionalProps = "";
|
|
114
|
+
if (schema.additionalProperties) {
|
|
115
|
+
if (typeof schema.additionalProperties === "object") {
|
|
116
|
+
const addPropZod = this.handle(schema.additionalProperties, true);
|
|
117
|
+
additionalProps = `.catchall(${addPropZod})`;
|
|
118
|
+
} else if (schema.additionalProperties === true) {
|
|
119
|
+
additionalProps = `.catchall(z.unknown())`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return `z.object({${propEntries.join(", ")}})${additionalProps}`;
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Handle arrays (items could be a single schema or a tuple (array of schemas)).
|
|
126
|
+
* In JSON Schema 2020-12, `items` can be an array → tuple style.
|
|
127
|
+
*/
|
|
128
|
+
array(schema, required = false) {
|
|
129
|
+
const { items } = schema;
|
|
130
|
+
if (!items) {
|
|
131
|
+
return `z.array(z.unknown())${appendOptional(required)}`;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(items)) {
|
|
134
|
+
const tupleItems = items.map((sub) => this.handle(sub, true));
|
|
135
|
+
const base = `z.tuple([${tupleItems.join(", ")}])`;
|
|
136
|
+
return `${base}${appendOptional(required)}`;
|
|
137
|
+
}
|
|
138
|
+
const itemsSchema = this.handle(items, true);
|
|
139
|
+
return `z.array(${itemsSchema})${appendOptional(required)}`;
|
|
120
140
|
}
|
|
121
|
-
|
|
122
|
-
return `${
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
#suffixes = (defaultValue, required, nullable) => {
|
|
142
|
+
return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${appendOptional(required)}`;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Convert a basic type (string | number | boolean | object | array, etc.) to Zod.
|
|
146
|
+
* We'll also handle .optional() if needed.
|
|
147
|
+
*/
|
|
148
|
+
normal(type, schema, required = false, nullable = false) {
|
|
149
|
+
switch (type) {
|
|
150
|
+
case "string":
|
|
151
|
+
return `${this.string(schema)}${this.#suffixes(JSON.stringify(schema.default), required, nullable)}`;
|
|
152
|
+
case "number":
|
|
153
|
+
case "integer": {
|
|
154
|
+
const { base, defaultValue } = this.number(schema);
|
|
155
|
+
return `${base}${this.#suffixes(defaultValue, required, nullable)}`;
|
|
156
|
+
}
|
|
157
|
+
case "boolean":
|
|
158
|
+
return `z.boolean()${this.#suffixes(schema.default, required, nullable)}`;
|
|
159
|
+
case "object":
|
|
160
|
+
return `${this.object(schema)}${this.#suffixes(JSON.stringify(schema.default), required, nullable)}`;
|
|
161
|
+
case "array":
|
|
162
|
+
return this.array(schema, required);
|
|
163
|
+
case "null":
|
|
164
|
+
return `z.null()${appendOptional(required)}`;
|
|
165
|
+
default:
|
|
166
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
167
|
+
}
|
|
126
168
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this
|
|
133
|
-
this
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
ref($ref, required) {
|
|
170
|
+
const schemaName = cleanRef($ref).split("/").pop();
|
|
171
|
+
if (this.circularRefTracker.has(schemaName)) {
|
|
172
|
+
return schemaName;
|
|
173
|
+
}
|
|
174
|
+
this.circularRefTracker.add(schemaName);
|
|
175
|
+
this.#onRef?.(
|
|
176
|
+
schemaName,
|
|
177
|
+
this.handle(followRef(this.#spec, $ref), required)
|
|
178
|
+
);
|
|
179
|
+
this.circularRefTracker.delete(schemaName);
|
|
180
|
+
return schemaName;
|
|
137
181
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
allOf(schemas, required) {
|
|
183
|
+
const allOfSchemas = schemas.map((sub) => this.handle(sub, true));
|
|
184
|
+
if (allOfSchemas.length === 0) {
|
|
185
|
+
return `z.unknown()`;
|
|
186
|
+
}
|
|
187
|
+
if (allOfSchemas.length === 1) {
|
|
188
|
+
return `${allOfSchemas[0]}${appendOptional(required)}`;
|
|
189
|
+
}
|
|
190
|
+
return `${this.#toIntersection(allOfSchemas)}${appendOptional(required)}`;
|
|
141
191
|
}
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
#toIntersection(schemas) {
|
|
193
|
+
const [left, ...right] = schemas;
|
|
194
|
+
if (!right.length) {
|
|
195
|
+
return left;
|
|
196
|
+
}
|
|
197
|
+
return `z.intersection(${left}, ${this.#toIntersection(right)})`;
|
|
144
198
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
${
|
|
149
|
-
}
|
|
199
|
+
anyOf(schemas, required) {
|
|
200
|
+
const anyOfSchemas = schemas.map((sub) => this.handle(sub, true));
|
|
201
|
+
if (anyOfSchemas.length === 1) {
|
|
202
|
+
return `${anyOfSchemas[0]}${appendOptional(required)}`;
|
|
203
|
+
}
|
|
204
|
+
return `z.union([${anyOfSchemas.join(", ")}])${appendOptional(required)}`;
|
|
150
205
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
|
|
158
|
-
for (const operation of operations) {
|
|
159
|
-
const schemaName = camelcase(`${operation.name} schema`);
|
|
160
|
-
const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
|
|
161
|
-
const inputContent = schema;
|
|
162
|
-
for (const schema2 of commonImports) {
|
|
163
|
-
if (inputContent.includes(schema2)) {
|
|
164
|
-
imports.add(
|
|
165
|
-
`import { ${schema2} } from './schemas/${makeImport(spinalcase(schema2))}';`
|
|
166
|
-
);
|
|
206
|
+
oneOf(schemas, required) {
|
|
207
|
+
const oneOfSchemas = schemas.map((sub) => {
|
|
208
|
+
if (isRef(sub)) {
|
|
209
|
+
const { model } = parseRef(sub.$ref);
|
|
210
|
+
if (this.circularRefTracker.has(model)) {
|
|
211
|
+
return `${model}${appendOptional(required)}`;
|
|
167
212
|
}
|
|
168
213
|
}
|
|
169
|
-
|
|
214
|
+
return this.handle(sub, true);
|
|
215
|
+
});
|
|
216
|
+
if (oneOfSchemas.length === 1) {
|
|
217
|
+
return `${oneOfSchemas[0]}${appendOptional(required)}`;
|
|
170
218
|
}
|
|
171
|
-
|
|
219
|
+
return `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}`;
|
|
172
220
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const schema2 of commonImports) {
|
|
177
|
-
const preciseMatch = new RegExp(`\\b${schema2}\\b`);
|
|
178
|
-
if (preciseMatch.test(content) && schema2 !== name) {
|
|
179
|
-
output.push(
|
|
180
|
-
`import { ${schema2} } from './${makeImport(spinalcase(schema2))}';`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
output.push(content);
|
|
185
|
-
return [
|
|
186
|
-
[`inputs/schemas/${spinalcase(name)}.ts`, output.join("\n")],
|
|
187
|
-
...acc
|
|
188
|
-
];
|
|
189
|
-
}, []);
|
|
190
|
-
return {
|
|
191
|
-
...Object.fromEntries(schemas),
|
|
192
|
-
...inputs
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function generateSDK(spec) {
|
|
196
|
-
const emitter = new Emitter(spec.makeImport);
|
|
197
|
-
const schemaEndpoint = new SchemaEndpoint(spec.makeImport);
|
|
198
|
-
const errors = [];
|
|
199
|
-
for (const [name, operations] of Object.entries(spec.operations)) {
|
|
200
|
-
emitter.addImport(
|
|
201
|
-
`import type * as ${camelcase(name)} from './inputs/${spec.makeImport(spinalcase(name))}';`
|
|
202
|
-
);
|
|
203
|
-
schemaEndpoint.addImport(
|
|
204
|
-
`import * as ${camelcase(name)} from './inputs/${spec.makeImport(spinalcase(name))}';`
|
|
205
|
-
);
|
|
206
|
-
for (const operation of operations) {
|
|
207
|
-
const schemaName = camelcase(`${operation.name} schema`);
|
|
208
|
-
const schemaRef = `${camelcase(name)}.${schemaName}`;
|
|
209
|
-
const output = operation.formatOutput();
|
|
210
|
-
const inputHeaders = [];
|
|
211
|
-
const inputQuery = [];
|
|
212
|
-
const inputBody = [];
|
|
213
|
-
const inputParams = [];
|
|
214
|
-
for (const [name2, prop] of Object.entries(operation.inputs)) {
|
|
215
|
-
if (prop.in === "headers" || prop.in === "header") {
|
|
216
|
-
inputHeaders.push(`"${name2}"`);
|
|
217
|
-
} else if (prop.in === "query") {
|
|
218
|
-
inputQuery.push(`"${name2}"`);
|
|
219
|
-
} else if (prop.in === "body") {
|
|
220
|
-
inputBody.push(`"${name2}"`);
|
|
221
|
-
} else if (prop.in === "path") {
|
|
222
|
-
inputParams.push(`"${name2}"`);
|
|
223
|
-
} else if (prop.in === "internal") {
|
|
224
|
-
continue;
|
|
225
|
-
} else {
|
|
226
|
-
throw new Error(
|
|
227
|
-
`Unknown source ${prop.in} in ${name2} ${JSON.stringify(
|
|
228
|
-
prop
|
|
229
|
-
)} in ${operation.name}`
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
emitter.addImport(
|
|
234
|
-
`import type {${output.import}} from './outputs/${spec.makeImport(spinalcase(operation.name))}';`
|
|
235
|
-
);
|
|
236
|
-
errors.push(...operation.errors ?? []);
|
|
237
|
-
const addTypeParser = Object.keys(operation.schemas).length > 1;
|
|
238
|
-
for (const type in operation.schemas ?? {}) {
|
|
239
|
-
let typePrefix = "";
|
|
240
|
-
if (addTypeParser && type !== "json") {
|
|
241
|
-
typePrefix = `${type} `;
|
|
242
|
-
}
|
|
243
|
-
const input = `typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}`;
|
|
244
|
-
const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
|
|
245
|
-
emitter.addEndpoint(
|
|
246
|
-
endpoint,
|
|
247
|
-
`{input: z.infer<${input}>; output: ${output.use}; error: ${(operation.errors ?? ["ServerError"]).concat(`ParseError<${input}>`).join("|")}}`
|
|
248
|
-
);
|
|
249
|
-
schemaEndpoint.addEndpoint(
|
|
250
|
-
endpoint,
|
|
251
|
-
`{
|
|
252
|
-
schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
|
|
253
|
-
deserializer: ${operation.parser === "chunked" ? "chunked" : "buffered"},
|
|
254
|
-
toRequest(input: Endpoints['${endpoint}']['input']) {
|
|
255
|
-
const endpoint = '${endpoint}';
|
|
256
|
-
return toRequest(endpoint, ${operation.outgoingContentType || "nobody"}(input, {
|
|
257
|
-
inputHeaders: [${inputHeaders}],
|
|
258
|
-
inputQuery: [${inputQuery}],
|
|
259
|
-
inputBody: [${inputBody}],
|
|
260
|
-
inputParams: [${inputParams}],
|
|
261
|
-
}));
|
|
262
|
-
},
|
|
263
|
-
}`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
221
|
+
enum(values) {
|
|
222
|
+
if (values.length === 1) {
|
|
223
|
+
return `z.literal(${values.join(", ")})`;
|
|
266
224
|
}
|
|
225
|
+
return `z.enum([${values.join(", ")}])`;
|
|
267
226
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Handle a `string` schema with possible format keywords (JSON Schema).
|
|
229
|
+
*/
|
|
230
|
+
string(schema) {
|
|
231
|
+
let base = "z.string()";
|
|
232
|
+
switch (schema.format) {
|
|
233
|
+
case "date-time":
|
|
234
|
+
case "datetime":
|
|
235
|
+
base = "z.coerce.date()";
|
|
236
|
+
break;
|
|
237
|
+
case "date":
|
|
238
|
+
base = "z.coerce.date() /* or z.string() if you want raw date strings */";
|
|
239
|
+
break;
|
|
240
|
+
case "time":
|
|
241
|
+
base = "z.string() /* optionally add .regex(...) for HH:MM:SS format */";
|
|
242
|
+
break;
|
|
243
|
+
case "email":
|
|
244
|
+
base = "z.string().email()";
|
|
245
|
+
break;
|
|
246
|
+
case "uuid":
|
|
247
|
+
base = "z.string().uuid()";
|
|
248
|
+
break;
|
|
249
|
+
case "url":
|
|
250
|
+
case "uri":
|
|
251
|
+
base = "z.string().url()";
|
|
252
|
+
break;
|
|
253
|
+
case "ipv4":
|
|
254
|
+
base = 'z.string().ip({version: "v4"})';
|
|
255
|
+
break;
|
|
256
|
+
case "ipv6":
|
|
257
|
+
base = 'z.string().ip({version: "v6"})';
|
|
258
|
+
break;
|
|
259
|
+
case "phone":
|
|
260
|
+
base = "z.string() /* or add .regex(...) for phone formats */";
|
|
261
|
+
break;
|
|
262
|
+
case "byte":
|
|
263
|
+
case "binary":
|
|
264
|
+
base = "z.instanceof(Blob) /* consider base64 check if needed */";
|
|
265
|
+
break;
|
|
266
|
+
case "int64":
|
|
267
|
+
base = "z.string() /* or z.bigint() if your app can handle it */";
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
return base;
|
|
295
273
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Handle number/integer constraints from OpenAPI/JSON Schema.
|
|
276
|
+
* In 3.1, exclusiveMinimum/Maximum hold the actual numeric threshold,
|
|
277
|
+
* rather than a boolean toggling `minimum`/`maximum`.
|
|
278
|
+
*/
|
|
279
|
+
number(schema) {
|
|
280
|
+
let defaultValue = schema.default;
|
|
281
|
+
let base = "z.number()";
|
|
282
|
+
if (schema.format === "int64") {
|
|
283
|
+
base = "z.bigint()";
|
|
284
|
+
if (schema.default !== void 0) {
|
|
285
|
+
defaultValue = `BigInt(${schema.default})`;
|
|
286
|
+
}
|
|
305
287
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
throw new Error(`Ref security schemas are not supported`);
|
|
288
|
+
if (schema.format === "int32") {
|
|
289
|
+
base += ".int()";
|
|
309
290
|
}
|
|
310
|
-
if (schema.
|
|
311
|
-
|
|
312
|
-
in: staticIn ?? "header",
|
|
313
|
-
schema: "z.string().optional().transform((val) => (val ? `Bearer ${val}` : undefined))",
|
|
314
|
-
optionName: "token"
|
|
315
|
-
};
|
|
316
|
-
continue;
|
|
291
|
+
if (typeof schema.exclusiveMinimum === "number") {
|
|
292
|
+
base += `.gt(${schema.exclusiveMinimum})`;
|
|
317
293
|
}
|
|
318
|
-
if (schema.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
continue;
|
|
294
|
+
if (typeof schema.exclusiveMaximum === "number") {
|
|
295
|
+
base += `.lt(${schema.exclusiveMaximum})`;
|
|
296
|
+
}
|
|
297
|
+
if (typeof schema.minimum === "number") {
|
|
298
|
+
base += schema.format === "int64" ? `.min(BigInt(${schema.minimum}))` : `.min(${schema.minimum})`;
|
|
299
|
+
}
|
|
300
|
+
if (typeof schema.maximum === "number") {
|
|
301
|
+
base += schema.format === "int64" ? `.max(BigInt(${schema.maximum}))` : `.max(${schema.maximum})`;
|
|
302
|
+
}
|
|
303
|
+
if (typeof schema.multipleOf === "number") {
|
|
304
|
+
base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
|
|
330
305
|
}
|
|
306
|
+
return { base, defaultValue };
|
|
331
307
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const merged = {};
|
|
336
|
-
for (const i of imports) {
|
|
337
|
-
merged[i.moduleSpecifier] = merged[i.moduleSpecifier] ?? {
|
|
338
|
-
moduleSpecifier: i.moduleSpecifier,
|
|
339
|
-
defaultImport: i.defaultImport,
|
|
340
|
-
namespaceImport: i.namespaceImport,
|
|
341
|
-
namedImports: []
|
|
342
|
-
};
|
|
343
|
-
if (i.namedImports) {
|
|
344
|
-
merged[i.moduleSpecifier].namedImports.push(...i.namedImports);
|
|
308
|
+
handle(schema, required) {
|
|
309
|
+
if (isRef(schema)) {
|
|
310
|
+
return `${this.ref(schema.$ref, true)}${appendOptional(required)}`;
|
|
345
311
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
function importsToString(...imports) {
|
|
350
|
-
return imports.map((it) => {
|
|
351
|
-
if (it.defaultImport) {
|
|
352
|
-
return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
|
|
312
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
313
|
+
return this.allOf(schema.allOf ?? [], required);
|
|
353
314
|
}
|
|
354
|
-
if (
|
|
355
|
-
return
|
|
315
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
316
|
+
return this.anyOf(schema.anyOf ?? [], required);
|
|
356
317
|
}
|
|
357
|
-
if (
|
|
358
|
-
return
|
|
318
|
+
if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length) {
|
|
319
|
+
return this.oneOf(schema.oneOf ?? [], required);
|
|
359
320
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
321
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
322
|
+
const enumVals = schema.enum.map((val) => JSON.stringify(val));
|
|
323
|
+
const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
|
|
324
|
+
return `${this.enum(enumVals)}${this.#suffixes(defaultValue, required, false)}`;
|
|
325
|
+
}
|
|
326
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
327
|
+
if (!types.length) {
|
|
328
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
329
|
+
}
|
|
330
|
+
if ("nullable" in schema && schema.nullable) {
|
|
331
|
+
types.push("null");
|
|
332
|
+
} else if (schema.default === null) {
|
|
333
|
+
types.push("null");
|
|
334
|
+
}
|
|
335
|
+
if (types.length > 1) {
|
|
336
|
+
const realTypes = types.filter((t) => t !== "null");
|
|
337
|
+
if (realTypes.length === 1 && types.includes("null")) {
|
|
338
|
+
return this.normal(realTypes[0], schema, required, true);
|
|
377
339
|
}
|
|
340
|
+
const subSchemas = types.map((t) => this.normal(t, schema, false));
|
|
341
|
+
return `z.union([${subSchemas.join(", ")}])${appendOptional(required)}`;
|
|
378
342
|
}
|
|
343
|
+
return this.normal(types[0], schema, required, false);
|
|
379
344
|
}
|
|
380
|
-
|
|
345
|
+
};
|
|
346
|
+
function appendOptional(isRequired) {
|
|
347
|
+
return isRequired ? "" : ".optional()";
|
|
348
|
+
}
|
|
349
|
+
function appendDefault(defaultValue) {
|
|
350
|
+
return defaultValue !== void 0 || typeof defaultValue !== "undefined" ? `.default(${defaultValue})` : "";
|
|
381
351
|
}
|
|
382
352
|
|
|
353
|
+
// packages/typescript/src/lib/sdk.ts
|
|
354
|
+
import { get } from "lodash-es";
|
|
355
|
+
import { camelcase, pascalcase, spinalcase } from "stringcase";
|
|
356
|
+
import { followRef as followRef3, isRef as isRef4, toLitObject as toLitObject2 } from "@sdk-it/core";
|
|
357
|
+
|
|
383
358
|
// packages/typescript/src/lib/emitters/interface.ts
|
|
359
|
+
import { cleanRef as cleanRef2, followRef as followRef2, isRef as isRef2, parseRef as parseRef2 } from "@sdk-it/core";
|
|
384
360
|
var TypeScriptDeserialzer = class {
|
|
385
361
|
circularRefTracker = /* @__PURE__ */ new Set();
|
|
386
362
|
#spec;
|
|
@@ -499,7 +475,7 @@ var TypeScriptDeserialzer = class {
|
|
|
499
475
|
case "integer":
|
|
500
476
|
return this.number(schema, required);
|
|
501
477
|
case "boolean":
|
|
502
|
-
return
|
|
478
|
+
return appendOptional2("boolean", required);
|
|
503
479
|
case "object":
|
|
504
480
|
return this.object(schema, required);
|
|
505
481
|
case "array":
|
|
@@ -507,18 +483,22 @@ var TypeScriptDeserialzer = class {
|
|
|
507
483
|
case "null":
|
|
508
484
|
return "null";
|
|
509
485
|
default:
|
|
510
|
-
|
|
486
|
+
console.warn(`Unknown type: ${type}`);
|
|
487
|
+
return appendOptional2("any", required);
|
|
511
488
|
}
|
|
512
489
|
}
|
|
513
490
|
ref($ref, required) {
|
|
514
|
-
const schemaName =
|
|
491
|
+
const schemaName = cleanRef2($ref).split("/").pop();
|
|
515
492
|
if (this.circularRefTracker.has(schemaName)) {
|
|
516
493
|
return schemaName;
|
|
517
494
|
}
|
|
518
495
|
this.circularRefTracker.add(schemaName);
|
|
519
|
-
this.#onRef(
|
|
496
|
+
this.#onRef?.(
|
|
497
|
+
schemaName,
|
|
498
|
+
this.handle(followRef2(this.#spec, $ref), required)
|
|
499
|
+
);
|
|
520
500
|
this.circularRefTracker.delete(schemaName);
|
|
521
|
-
return
|
|
501
|
+
return appendOptional2(schemaName, required);
|
|
522
502
|
}
|
|
523
503
|
allOf(schemas) {
|
|
524
504
|
const allOfTypes = schemas.map((sub) => this.handle(sub, true));
|
|
@@ -526,29 +506,29 @@ var TypeScriptDeserialzer = class {
|
|
|
526
506
|
}
|
|
527
507
|
anyOf(schemas, required) {
|
|
528
508
|
const anyOfTypes = schemas.map((sub) => this.handle(sub, true));
|
|
529
|
-
return
|
|
509
|
+
return appendOptional2(
|
|
530
510
|
anyOfTypes.length > 1 ? `${anyOfTypes.join(" | ")}` : anyOfTypes[0],
|
|
531
511
|
required
|
|
532
512
|
);
|
|
533
513
|
}
|
|
534
514
|
oneOf(schemas, required) {
|
|
535
515
|
const oneOfTypes = schemas.map((sub) => {
|
|
536
|
-
if (
|
|
537
|
-
const { model } =
|
|
516
|
+
if (isRef2(sub)) {
|
|
517
|
+
const { model } = parseRef2(sub.$ref);
|
|
538
518
|
if (this.circularRefTracker.has(model)) {
|
|
539
519
|
return model;
|
|
540
520
|
}
|
|
541
521
|
}
|
|
542
522
|
return this.handle(sub, false);
|
|
543
523
|
});
|
|
544
|
-
return
|
|
524
|
+
return appendOptional2(
|
|
545
525
|
oneOfTypes.length > 1 ? `${oneOfTypes.join(" | ")}` : oneOfTypes[0],
|
|
546
526
|
required
|
|
547
527
|
);
|
|
548
528
|
}
|
|
549
529
|
enum(values, required) {
|
|
550
530
|
const enumValues = values.map((val) => typeof val === "string" ? `'${val}'` : `${val}`).join(" | ");
|
|
551
|
-
return
|
|
531
|
+
return appendOptional2(enumValues, required);
|
|
552
532
|
}
|
|
553
533
|
/**
|
|
554
534
|
* Handle string type with formats
|
|
@@ -571,17 +551,17 @@ var TypeScriptDeserialzer = class {
|
|
|
571
551
|
default:
|
|
572
552
|
type = "string";
|
|
573
553
|
}
|
|
574
|
-
return
|
|
554
|
+
return appendOptional2(type, required);
|
|
575
555
|
}
|
|
576
556
|
/**
|
|
577
557
|
* Handle number/integer types with formats
|
|
578
558
|
*/
|
|
579
559
|
number(schema, required) {
|
|
580
560
|
const type = schema.format === "int64" ? "bigint" : "number";
|
|
581
|
-
return
|
|
561
|
+
return appendOptional2(type, required);
|
|
582
562
|
}
|
|
583
563
|
handle(schema, required) {
|
|
584
|
-
if (
|
|
564
|
+
if (isRef2(schema)) {
|
|
585
565
|
return this.ref(schema.$ref, required);
|
|
586
566
|
}
|
|
587
567
|
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
@@ -601,16 +581,16 @@ var TypeScriptDeserialzer = class {
|
|
|
601
581
|
if ("properties" in schema) {
|
|
602
582
|
return this.object(schema, required);
|
|
603
583
|
}
|
|
604
|
-
return
|
|
584
|
+
return appendOptional2("any", required);
|
|
605
585
|
}
|
|
606
586
|
if (types.length > 1) {
|
|
607
587
|
const realTypes = types.filter((t) => t !== "null");
|
|
608
588
|
if (realTypes.length === 1 && types.includes("null")) {
|
|
609
589
|
const tsType = this.normal(realTypes[0], schema, false);
|
|
610
|
-
return
|
|
590
|
+
return appendOptional2(`${tsType} | null`, required);
|
|
611
591
|
}
|
|
612
592
|
const typeResults = types.map((t) => this.normal(t, schema, false));
|
|
613
|
-
return
|
|
593
|
+
return appendOptional2(typeResults.join(" | "), required);
|
|
614
594
|
}
|
|
615
595
|
return this.normal(types[0], schema, required);
|
|
616
596
|
}
|
|
@@ -622,275 +602,228 @@ var TypeScriptDeserialzer = class {
|
|
|
622
602
|
return `interface ${name} ${content}`;
|
|
623
603
|
}
|
|
624
604
|
};
|
|
625
|
-
function
|
|
605
|
+
function appendOptional2(type, isRequired) {
|
|
626
606
|
return isRequired ? type : `${type} | undefined`;
|
|
627
607
|
}
|
|
628
|
-
|
|
629
|
-
// packages/typescript/src/lib/
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Handle objects (properties, additionalProperties).
|
|
640
|
-
*/
|
|
641
|
-
object(schema, required = false) {
|
|
642
|
-
const properties = schema.properties || {};
|
|
643
|
-
const propEntries = Object.entries(properties).map(([key, propSchema]) => {
|
|
644
|
-
const isRequired = (schema.required ?? []).includes(key);
|
|
645
|
-
const zodPart = this.handle(propSchema, isRequired);
|
|
646
|
-
return `'${key}': ${zodPart}`;
|
|
647
|
-
});
|
|
648
|
-
let additionalProps = "";
|
|
649
|
-
if (schema.additionalProperties) {
|
|
650
|
-
if (typeof schema.additionalProperties === "object") {
|
|
651
|
-
const addPropZod = this.handle(schema.additionalProperties, true);
|
|
652
|
-
additionalProps = `.catchall(${addPropZod})`;
|
|
653
|
-
} else if (schema.additionalProperties === true) {
|
|
654
|
-
additionalProps = `.catchall(z.unknown())`;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const objectSchema = `z.object({${propEntries.join(", ")}})${additionalProps}`;
|
|
658
|
-
return `${objectSchema}${appendOptional2(required)}`;
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Handle arrays (items could be a single schema or a tuple (array of schemas)).
|
|
662
|
-
* In JSON Schema 2020-12, `items` can be an array → tuple style.
|
|
663
|
-
*/
|
|
664
|
-
array(schema, required = false) {
|
|
665
|
-
const { items } = schema;
|
|
666
|
-
if (!items) {
|
|
667
|
-
return `z.array(z.unknown())${appendOptional2(required)}`;
|
|
668
|
-
}
|
|
669
|
-
if (Array.isArray(items)) {
|
|
670
|
-
const tupleItems = items.map((sub) => this.handle(sub, true));
|
|
671
|
-
const base = `z.tuple([${tupleItems.join(", ")}])`;
|
|
672
|
-
return `${base}${appendOptional2(required)}`;
|
|
673
|
-
}
|
|
674
|
-
const itemsSchema = this.handle(items, true);
|
|
675
|
-
return `z.array(${itemsSchema})${appendOptional2(required)}`;
|
|
676
|
-
}
|
|
677
|
-
#suffixes = (defaultValue, required, nullable) => {
|
|
678
|
-
return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${appendOptional2(required)}`;
|
|
679
|
-
};
|
|
680
|
-
/**
|
|
681
|
-
* Convert a basic type (string | number | boolean | object | array, etc.) to Zod.
|
|
682
|
-
* We'll also handle .optional() if needed.
|
|
683
|
-
*/
|
|
684
|
-
normal(type, schema, required = false, nullable = false) {
|
|
685
|
-
switch (type) {
|
|
686
|
-
case "string":
|
|
687
|
-
return `${this.string(schema)}${this.#suffixes(JSON.stringify(schema.default), required, nullable)}`;
|
|
688
|
-
case "number":
|
|
689
|
-
case "integer": {
|
|
690
|
-
const { base, defaultValue } = this.number(schema);
|
|
691
|
-
return `${base}${this.#suffixes(defaultValue, required, nullable)}`;
|
|
692
|
-
}
|
|
693
|
-
case "boolean":
|
|
694
|
-
return `z.boolean()${this.#suffixes(schema.default, required, nullable)}`;
|
|
695
|
-
case "object":
|
|
696
|
-
return this.object(schema, true);
|
|
697
|
-
case "array":
|
|
698
|
-
return this.array(schema, required);
|
|
699
|
-
case "null":
|
|
700
|
-
return `z.null()${appendOptional2(required)}`;
|
|
701
|
-
default:
|
|
702
|
-
return `z.unknown()${appendOptional2(required)}`;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
ref($ref, required) {
|
|
706
|
-
const schemaName = cleanRef($ref).split("/").pop();
|
|
707
|
-
if (this.circularRefTracker.has(schemaName)) {
|
|
708
|
-
return schemaName;
|
|
709
|
-
}
|
|
710
|
-
this.circularRefTracker.add(schemaName);
|
|
711
|
-
this.#onRef?.(
|
|
712
|
-
schemaName,
|
|
713
|
-
this.handle(followRef(this.#spec, $ref), required)
|
|
714
|
-
);
|
|
715
|
-
this.circularRefTracker.delete(schemaName);
|
|
716
|
-
return schemaName;
|
|
717
|
-
}
|
|
718
|
-
allOf(schemas) {
|
|
719
|
-
const allOfSchemas = schemas.map((sub) => this.handle(sub, true));
|
|
720
|
-
if (allOfSchemas.length === 0) {
|
|
721
|
-
return `z.unknown()`;
|
|
722
|
-
}
|
|
723
|
-
if (allOfSchemas.length === 1) {
|
|
724
|
-
return allOfSchemas[0];
|
|
725
|
-
}
|
|
726
|
-
return this.#toIntersection(allOfSchemas);
|
|
727
|
-
}
|
|
728
|
-
#toIntersection(schemas) {
|
|
729
|
-
const [left, ...right] = schemas;
|
|
730
|
-
if (!right.length) {
|
|
731
|
-
return left;
|
|
732
|
-
}
|
|
733
|
-
return `z.intersection(${left}, ${this.#toIntersection(right)})`;
|
|
734
|
-
}
|
|
735
|
-
anyOf(schemas, required) {
|
|
736
|
-
const anyOfSchemas = schemas.map((sub) => this.handle(sub, false));
|
|
737
|
-
if (anyOfSchemas.length === 1) {
|
|
738
|
-
return anyOfSchemas[0];
|
|
739
|
-
}
|
|
740
|
-
return anyOfSchemas.length > 1 ? `z.union([${anyOfSchemas.join(", ")}])${appendOptional2(required)}` : (
|
|
741
|
-
// Handle an invalid anyOf with one schema
|
|
742
|
-
anyOfSchemas[0]
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
oneOf(schemas, required) {
|
|
746
|
-
const oneOfSchemas = schemas.map((sub) => {
|
|
747
|
-
if (isRef(sub)) {
|
|
748
|
-
const { model } = parseRef(sub.$ref);
|
|
749
|
-
if (this.circularRefTracker.has(model)) {
|
|
750
|
-
return model;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
return this.handle(sub, true);
|
|
754
|
-
});
|
|
755
|
-
if (oneOfSchemas.length === 1) {
|
|
756
|
-
return oneOfSchemas[0];
|
|
757
|
-
}
|
|
758
|
-
return oneOfSchemas.length > 1 ? `z.union([${oneOfSchemas.join(", ")}])${appendOptional2(required)}` : (
|
|
759
|
-
// Handle an invalid oneOf with one schema
|
|
760
|
-
oneOfSchemas[0]
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
enum(values, required) {
|
|
764
|
-
const enumVals = values.map((val) => JSON.stringify(val)).join(", ");
|
|
765
|
-
if (values.length === 1) {
|
|
766
|
-
return `z.literal(${enumVals})${appendOptional2(required)}`;
|
|
767
|
-
}
|
|
768
|
-
return `z.enum([${enumVals}])${appendOptional2(required)}`;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Handle a `string` schema with possible format keywords (JSON Schema).
|
|
772
|
-
*/
|
|
773
|
-
string(schema) {
|
|
774
|
-
let base = "z.string()";
|
|
775
|
-
switch (schema.format) {
|
|
776
|
-
case "date-time":
|
|
777
|
-
case "datetime":
|
|
778
|
-
base = "z.coerce.date()";
|
|
779
|
-
break;
|
|
780
|
-
case "date":
|
|
781
|
-
base = "z.coerce.date() /* or z.string() if you want raw date strings */";
|
|
782
|
-
break;
|
|
783
|
-
case "time":
|
|
784
|
-
base = "z.string() /* optionally add .regex(...) for HH:MM:SS format */";
|
|
785
|
-
break;
|
|
786
|
-
case "email":
|
|
787
|
-
base = "z.string().email()";
|
|
788
|
-
break;
|
|
789
|
-
case "uuid":
|
|
790
|
-
base = "z.string().uuid()";
|
|
791
|
-
break;
|
|
792
|
-
case "url":
|
|
793
|
-
case "uri":
|
|
794
|
-
base = "z.string().url()";
|
|
795
|
-
break;
|
|
796
|
-
case "ipv4":
|
|
797
|
-
base = 'z.string().ip({version: "v4"})';
|
|
798
|
-
break;
|
|
799
|
-
case "ipv6":
|
|
800
|
-
base = 'z.string().ip({version: "v6"})';
|
|
801
|
-
break;
|
|
802
|
-
case "phone":
|
|
803
|
-
base = "z.string() /* or add .regex(...) for phone formats */";
|
|
804
|
-
break;
|
|
805
|
-
case "byte":
|
|
806
|
-
case "binary":
|
|
807
|
-
base = "z.instanceof(Blob) /* consider base64 check if needed */";
|
|
808
|
-
break;
|
|
809
|
-
case "int64":
|
|
810
|
-
base = "z.string() /* or z.bigint() if your app can handle it */";
|
|
811
|
-
break;
|
|
812
|
-
default:
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
return base;
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Handle number/integer constraints from OpenAPI/JSON Schema.
|
|
819
|
-
* In 3.1, exclusiveMinimum/Maximum hold the actual numeric threshold,
|
|
820
|
-
* rather than a boolean toggling `minimum`/`maximum`.
|
|
821
|
-
*/
|
|
822
|
-
number(schema) {
|
|
823
|
-
let defaultValue = schema.default;
|
|
824
|
-
let base = "z.number()";
|
|
825
|
-
if (schema.format === "int64") {
|
|
826
|
-
base = "z.bigint()";
|
|
827
|
-
if (schema.default !== void 0) {
|
|
828
|
-
defaultValue = `BigInt(${schema.default})`;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (schema.format === "int32") {
|
|
832
|
-
base += ".int()";
|
|
833
|
-
}
|
|
834
|
-
if (typeof schema.exclusiveMinimum === "number") {
|
|
835
|
-
base += `.gt(${schema.exclusiveMinimum})`;
|
|
836
|
-
}
|
|
837
|
-
if (typeof schema.exclusiveMaximum === "number") {
|
|
838
|
-
base += `.lt(${schema.exclusiveMaximum})`;
|
|
608
|
+
|
|
609
|
+
// packages/typescript/src/lib/utils.ts
|
|
610
|
+
import { isRef as isRef3, removeDuplicates } from "@sdk-it/core";
|
|
611
|
+
function securityToOptions(security2, securitySchemes, staticIn) {
|
|
612
|
+
securitySchemes ??= {};
|
|
613
|
+
const options = {};
|
|
614
|
+
for (const it of security2) {
|
|
615
|
+
const [name] = Object.keys(it);
|
|
616
|
+
if (!name) {
|
|
617
|
+
continue;
|
|
839
618
|
}
|
|
840
|
-
|
|
841
|
-
|
|
619
|
+
const schema = securitySchemes[name];
|
|
620
|
+
if (isRef3(schema)) {
|
|
621
|
+
throw new Error(`Ref security schemas are not supported`);
|
|
842
622
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
623
|
+
if (schema.type === "http") {
|
|
624
|
+
options["authorization"] = {
|
|
625
|
+
in: staticIn ?? "header",
|
|
626
|
+
schema: "z.string().optional().transform((val) => (val ? `Bearer ${val}` : undefined))",
|
|
627
|
+
optionName: "token"
|
|
628
|
+
};
|
|
629
|
+
continue;
|
|
845
630
|
}
|
|
846
|
-
if (
|
|
847
|
-
|
|
631
|
+
if (schema.type === "apiKey") {
|
|
632
|
+
if (!schema.in) {
|
|
633
|
+
throw new Error(`apiKey security schema must have an "in" field`);
|
|
634
|
+
}
|
|
635
|
+
if (!schema.name) {
|
|
636
|
+
throw new Error(`apiKey security schema must have a "name" field`);
|
|
637
|
+
}
|
|
638
|
+
options[schema.name] = {
|
|
639
|
+
in: staticIn ?? schema.in,
|
|
640
|
+
schema: "z.string().optional()"
|
|
641
|
+
};
|
|
642
|
+
continue;
|
|
848
643
|
}
|
|
849
|
-
return { base, defaultValue };
|
|
850
644
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
645
|
+
return options;
|
|
646
|
+
}
|
|
647
|
+
function mergeImports(...imports) {
|
|
648
|
+
const merged = {};
|
|
649
|
+
for (const it of imports) {
|
|
650
|
+
merged[it.moduleSpecifier] = merged[it.moduleSpecifier] ?? {
|
|
651
|
+
moduleSpecifier: it.moduleSpecifier,
|
|
652
|
+
defaultImport: it.defaultImport,
|
|
653
|
+
namespaceImport: it.namespaceImport,
|
|
654
|
+
namedImports: []
|
|
655
|
+
};
|
|
656
|
+
for (const named of it.namedImports) {
|
|
657
|
+
if (!merged[it.moduleSpecifier].namedImports.some(
|
|
658
|
+
(x) => x.name === named.name
|
|
659
|
+
)) {
|
|
660
|
+
merged[it.moduleSpecifier].namedImports.push(named);
|
|
661
|
+
}
|
|
860
662
|
}
|
|
861
|
-
|
|
862
|
-
|
|
663
|
+
}
|
|
664
|
+
return Object.values(merged);
|
|
665
|
+
}
|
|
666
|
+
function importsToString(...imports) {
|
|
667
|
+
return imports.map((it) => {
|
|
668
|
+
if (it.defaultImport) {
|
|
669
|
+
return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
|
|
863
670
|
}
|
|
864
|
-
if (
|
|
865
|
-
return
|
|
671
|
+
if (it.namespaceImport) {
|
|
672
|
+
return `import * as ${it.namespaceImport} from '${it.moduleSpecifier}'`;
|
|
866
673
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
return `z.unknown()${appendOptional2(required)}`;
|
|
674
|
+
if (it.namedImports) {
|
|
675
|
+
return `import {${removeDuplicates(it.namedImports, (it2) => it2.name).map((n) => `${n.isTypeOnly ? "type" : ""} ${n.name}`).join(", ")}} from '${it.moduleSpecifier}'`;
|
|
870
676
|
}
|
|
871
|
-
|
|
872
|
-
|
|
677
|
+
throw new Error(`Invalid import ${JSON.stringify(it)}`);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function exclude(list, exclude2) {
|
|
681
|
+
return list.filter((it) => !exclude2.includes(it));
|
|
682
|
+
}
|
|
683
|
+
function useImports(content, ...imports) {
|
|
684
|
+
const output = [];
|
|
685
|
+
for (const it of mergeImports(...imports)) {
|
|
686
|
+
const singleImport = it.defaultImport ?? it.namespaceImport;
|
|
687
|
+
if (singleImport && content.includes(singleImport)) {
|
|
688
|
+
output.push(importsToString(it).join("\n"));
|
|
689
|
+
} else if (it.namedImports.length) {
|
|
690
|
+
for (const namedImport of it.namedImports) {
|
|
691
|
+
if (content.includes(namedImport.name)) {
|
|
692
|
+
output.push(importsToString(it).join("\n"));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
873
695
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
696
|
+
}
|
|
697
|
+
return output;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// packages/typescript/src/lib/sdk.ts
|
|
701
|
+
function generateInputs(operationsSet, commonZod, makeImport) {
|
|
702
|
+
const commonImports = commonZod.keys().toArray();
|
|
703
|
+
const inputs = {};
|
|
704
|
+
for (const [name, operations] of Object.entries(operationsSet)) {
|
|
705
|
+
const output = [];
|
|
706
|
+
const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
|
|
707
|
+
for (const operation of operations) {
|
|
708
|
+
const schemaName = camelcase(`${operation.name} schema`);
|
|
709
|
+
const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
|
|
710
|
+
const inputContent = schema;
|
|
711
|
+
for (const schema2 of commonImports) {
|
|
712
|
+
if (inputContent.includes(schema2)) {
|
|
713
|
+
imports.add(
|
|
714
|
+
`import { ${schema2} } from './schemas/${makeImport(spinalcase(schema2))}';`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
878
717
|
}
|
|
879
|
-
|
|
880
|
-
return `z.union([${subSchemas.join(", ")}])${appendOptional2(required)}`;
|
|
718
|
+
output.push(inputContent);
|
|
881
719
|
}
|
|
882
|
-
|
|
720
|
+
inputs[`inputs/${spinalcase(name)}.ts`] = [...imports, ...output].join("\n") + "\n";
|
|
883
721
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
722
|
+
const schemas = commonZod.entries().reduce((acc, [name, schema]) => {
|
|
723
|
+
const output = [`import { z } from 'zod';`];
|
|
724
|
+
const content = `export const ${name} = ${schema};`;
|
|
725
|
+
for (const schema2 of commonImports) {
|
|
726
|
+
const preciseMatch = new RegExp(`\\b${schema2}\\b`);
|
|
727
|
+
if (preciseMatch.test(content) && schema2 !== name) {
|
|
728
|
+
output.push(
|
|
729
|
+
`import { ${schema2} } from './${makeImport(spinalcase(schema2))}';`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
output.push(content);
|
|
734
|
+
return [
|
|
735
|
+
[`inputs/schemas/${spinalcase(name)}.ts`, output.join("\n")],
|
|
736
|
+
...acc
|
|
737
|
+
];
|
|
738
|
+
}, []);
|
|
739
|
+
return {
|
|
740
|
+
...Object.fromEntries(schemas),
|
|
741
|
+
...inputs
|
|
742
|
+
};
|
|
887
743
|
}
|
|
888
|
-
function
|
|
889
|
-
|
|
744
|
+
function toEndpoint(groupName, spec, specOperation, operation, utils) {
|
|
745
|
+
const schemaName = camelcase(`${operation.name} schema`);
|
|
746
|
+
const schemaRef = `${camelcase(groupName)}.${schemaName}`;
|
|
747
|
+
const inputHeaders = [];
|
|
748
|
+
const inputQuery = [];
|
|
749
|
+
const inputBody = [];
|
|
750
|
+
const inputParams = [];
|
|
751
|
+
const schemas = [];
|
|
752
|
+
const responses = [];
|
|
753
|
+
for (const [name, prop] of Object.entries(operation.inputs)) {
|
|
754
|
+
if (prop.in === "headers" || prop.in === "header") {
|
|
755
|
+
inputHeaders.push(`"${name}"`);
|
|
756
|
+
} else if (prop.in === "query") {
|
|
757
|
+
inputQuery.push(`"${name}"`);
|
|
758
|
+
} else if (prop.in === "body") {
|
|
759
|
+
inputBody.push(`"${name}"`);
|
|
760
|
+
} else if (prop.in === "path") {
|
|
761
|
+
inputParams.push(`"${name}"`);
|
|
762
|
+
} else if (prop.in === "internal") {
|
|
763
|
+
continue;
|
|
764
|
+
} else {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Unknown source ${prop.in} in ${name} ${JSON.stringify(
|
|
767
|
+
prop
|
|
768
|
+
)} in ${operation.name}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
specOperation.responses ??= {};
|
|
773
|
+
const outputs = [];
|
|
774
|
+
const statusesCount = Object.keys(specOperation.responses).filter((status) => {
|
|
775
|
+
const statusCode = +status;
|
|
776
|
+
return statusCode >= 200 && statusCode < 300;
|
|
777
|
+
}).length > 1;
|
|
778
|
+
for (const status in specOperation.responses) {
|
|
779
|
+
const response = isRef4(
|
|
780
|
+
specOperation.responses[status]
|
|
781
|
+
) ? followRef3(
|
|
782
|
+
spec,
|
|
783
|
+
specOperation.responses[status].$ref
|
|
784
|
+
) : specOperation.responses[status];
|
|
785
|
+
const handled = handleResponse(
|
|
786
|
+
spec,
|
|
787
|
+
operation.name,
|
|
788
|
+
status,
|
|
789
|
+
response,
|
|
790
|
+
utils,
|
|
791
|
+
true
|
|
792
|
+
// statusesCount,
|
|
793
|
+
);
|
|
794
|
+
responses.push(handled);
|
|
795
|
+
outputs.push(...handled.outputs);
|
|
796
|
+
}
|
|
797
|
+
const addTypeParser = Object.keys(operation.schemas).length > 1;
|
|
798
|
+
for (const type in operation.schemas ?? {}) {
|
|
799
|
+
let typePrefix = "";
|
|
800
|
+
if (addTypeParser && type !== "json") {
|
|
801
|
+
typePrefix = `${type} `;
|
|
802
|
+
}
|
|
803
|
+
const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
|
|
804
|
+
schemas.push(
|
|
805
|
+
`"${endpoint}": {
|
|
806
|
+
schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
|
|
807
|
+
output:[${outputs.join(",")}],
|
|
808
|
+
toRequest(input: z.infer<typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}>) {
|
|
809
|
+
const endpoint = '${endpoint}';
|
|
810
|
+
return toRequest(endpoint, ${operation.outgoingContentType || "nobody"}(input, {
|
|
811
|
+
inputHeaders: [${inputHeaders}],
|
|
812
|
+
inputQuery: [${inputQuery}],
|
|
813
|
+
inputBody: [${inputBody}],
|
|
814
|
+
inputParams: [${inputParams}],
|
|
815
|
+
}));
|
|
816
|
+
},
|
|
817
|
+
}`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return { responses, schemas };
|
|
890
821
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
822
|
+
var statusCodeToResponseMap = {
|
|
823
|
+
"200": "Ok",
|
|
824
|
+
"201": "Created",
|
|
825
|
+
"202": "Accepted",
|
|
826
|
+
"204": "NoContent",
|
|
894
827
|
"400": "BadRequest",
|
|
895
828
|
"401": "Unauthorized",
|
|
896
829
|
"402": "PaymentRequired",
|
|
@@ -909,18 +842,94 @@ var statusCdeToMessageMap = {
|
|
|
909
842
|
"503": "ServiceUnavailable",
|
|
910
843
|
"504": "GatewayTimeout"
|
|
911
844
|
};
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
845
|
+
function handleResponse(spec, operationName, status, response, utils, numbered) {
|
|
846
|
+
const schemas = {};
|
|
847
|
+
const imports = {};
|
|
848
|
+
const endpointImports = {
|
|
849
|
+
ParseError: {
|
|
850
|
+
defaultImport: void 0,
|
|
851
|
+
isTypeOnly: false,
|
|
852
|
+
moduleSpecifier: utils.makeImport(`../http/parser`),
|
|
853
|
+
namedImports: [{ isTypeOnly: false, name: "ParseError" }],
|
|
854
|
+
namespaceImport: void 0
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
const responses = [];
|
|
858
|
+
const outputs = [];
|
|
859
|
+
const typeScriptDeserialzer = new TypeScriptDeserialzer(
|
|
860
|
+
spec,
|
|
861
|
+
(schemaName, zod) => {
|
|
862
|
+
schemas[schemaName] = zod;
|
|
863
|
+
imports[schemaName] = {
|
|
864
|
+
defaultImport: void 0,
|
|
865
|
+
isTypeOnly: true,
|
|
866
|
+
moduleSpecifier: `../models/${utils.makeImport(schemaName)}`,
|
|
867
|
+
namedImports: [{ isTypeOnly: true, name: schemaName }],
|
|
868
|
+
namespaceImport: void 0
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
const statusCode = +status;
|
|
873
|
+
const parser = (response.headers ?? {})["Transfer-Encoding"] ? "chunked" : "buffered";
|
|
874
|
+
const statusName = statusCodeToResponseMap[status] || "APIResponse";
|
|
875
|
+
const interfaceName = pascalcase(
|
|
876
|
+
operationName + ` output${numbered ? status : ""}`
|
|
877
|
+
);
|
|
878
|
+
if (statusCode === 204) {
|
|
879
|
+
outputs.push(statusName);
|
|
880
|
+
} else {
|
|
881
|
+
if (status.endsWith("XX")) {
|
|
882
|
+
outputs.push(`APIError<${interfaceName}>`);
|
|
883
|
+
} else {
|
|
884
|
+
outputs.push(
|
|
885
|
+
parser !== "buffered" ? `{type: ${statusName}<${interfaceName}>, parser: ${parser}}` : `${statusName}<${interfaceName}>`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
920
888
|
}
|
|
921
|
-
|
|
889
|
+
const responseContent = get(response, ["content"]);
|
|
890
|
+
const isJson = responseContent && responseContent["application/json"];
|
|
891
|
+
const responseSchema = isJson ? typeScriptDeserialzer.handle(
|
|
892
|
+
responseContent["application/json"].schema,
|
|
893
|
+
true
|
|
894
|
+
) : "void";
|
|
895
|
+
responses.push({
|
|
896
|
+
name: interfaceName,
|
|
897
|
+
schema: responseSchema
|
|
898
|
+
});
|
|
899
|
+
const statusGroup = +status.slice(0, 1);
|
|
900
|
+
if (statusCode >= 400 || statusGroup >= 4) {
|
|
901
|
+
endpointImports[statusCodeToResponseMap[status] ?? "APIError"] = {
|
|
902
|
+
moduleSpecifier: utils.makeImport("../http/response"),
|
|
903
|
+
namedImports: [{ name: statusCodeToResponseMap[status] ?? "APIError" }]
|
|
904
|
+
};
|
|
905
|
+
endpointImports[interfaceName] = {
|
|
906
|
+
isTypeOnly: true,
|
|
907
|
+
moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
|
|
908
|
+
namedImports: [{ isTypeOnly: true, name: interfaceName }]
|
|
909
|
+
};
|
|
910
|
+
} else if (statusCode >= 200 && statusCode < 300 || statusCode >= 2 || statusGroup <= 3) {
|
|
911
|
+
endpointImports[statusName] = {
|
|
912
|
+
moduleSpecifier: utils.makeImport("../http/response"),
|
|
913
|
+
namedImports: [
|
|
914
|
+
{
|
|
915
|
+
isTypeOnly: false,
|
|
916
|
+
name: statusName
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
};
|
|
920
|
+
endpointImports[interfaceName] = {
|
|
921
|
+
defaultImport: void 0,
|
|
922
|
+
isTypeOnly: true,
|
|
923
|
+
moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
|
|
924
|
+
namedImports: [{ isTypeOnly: true, name: interfaceName }],
|
|
925
|
+
namespaceImport: void 0
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return { schemas, imports, endpointImports, responses, outputs };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// packages/typescript/src/lib/generator.ts
|
|
922
932
|
function generateCode(config) {
|
|
923
|
-
const commonSchemas = {};
|
|
924
933
|
const commonZod = /* @__PURE__ */ new Map();
|
|
925
934
|
const commonZodImports = [];
|
|
926
935
|
const zodDeserialzer = new ZodDeserialzer(config.spec, (model, schema) => {
|
|
@@ -935,191 +944,244 @@ function generateCode(config) {
|
|
|
935
944
|
});
|
|
936
945
|
const groups = {};
|
|
937
946
|
const outputs = {};
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
for (const param of [...parameters, ...operation.parameters ?? []]) {
|
|
949
|
-
if (isRef(param)) {
|
|
950
|
-
throw new Error(`Found reference in parameter ${param.$ref}`);
|
|
951
|
-
}
|
|
952
|
-
if (!param.schema) {
|
|
953
|
-
throw new Error(`Schema not found for parameter ${param.name}`);
|
|
954
|
-
}
|
|
955
|
-
inputs[param.name] = {
|
|
956
|
-
in: param.in,
|
|
957
|
-
schema: ""
|
|
958
|
-
};
|
|
959
|
-
additionalProperties.push(param);
|
|
947
|
+
const endpoints = {};
|
|
948
|
+
forEachOperation(config, (entry, operation) => {
|
|
949
|
+
console.log(`Processing ${entry.method} ${entry.path}`);
|
|
950
|
+
groups[entry.groupName] ??= [];
|
|
951
|
+
endpoints[entry.groupName] ??= [];
|
|
952
|
+
const inputs = {};
|
|
953
|
+
const additionalProperties = [];
|
|
954
|
+
for (const param of operation.parameters ?? []) {
|
|
955
|
+
if (isRef5(param)) {
|
|
956
|
+
throw new Error(`Found reference in parameter ${param.$ref}`);
|
|
960
957
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
([name, value]) => ({
|
|
968
|
-
name,
|
|
969
|
-
required: false,
|
|
970
|
-
schema: {
|
|
971
|
-
type: "string"
|
|
972
|
-
},
|
|
973
|
-
in: value.in
|
|
974
|
-
})
|
|
975
|
-
)
|
|
976
|
-
);
|
|
977
|
-
const types = {};
|
|
978
|
-
const shortContenTypeMap = {
|
|
979
|
-
"application/json": "json",
|
|
980
|
-
"application/x-www-form-urlencoded": "urlencoded",
|
|
981
|
-
"multipart/form-data": "formdata",
|
|
982
|
-
"application/xml": "xml",
|
|
983
|
-
"text/plain": "text"
|
|
958
|
+
if (!param.schema) {
|
|
959
|
+
throw new Error(`Schema not found for parameter ${param.name}`);
|
|
960
|
+
}
|
|
961
|
+
inputs[param.name] = {
|
|
962
|
+
in: param.in,
|
|
963
|
+
schema: ""
|
|
984
964
|
};
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
...acc,
|
|
999
|
-
[p.name]: p.schema
|
|
1000
|
-
}),
|
|
1001
|
-
{}
|
|
1002
|
-
)
|
|
1003
|
-
});
|
|
1004
|
-
Object.assign(inputs, bodyInputs(config, ctSchema));
|
|
1005
|
-
types[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
|
|
1006
|
-
}
|
|
1007
|
-
if (content["application/json"]) {
|
|
1008
|
-
outgoingContentType = "json";
|
|
1009
|
-
} else if (content["application/x-www-form-urlencoded"]) {
|
|
1010
|
-
outgoingContentType = "urlencoded";
|
|
1011
|
-
} else if (content["multipart/form-data"]) {
|
|
1012
|
-
outgoingContentType = "formdata";
|
|
1013
|
-
} else {
|
|
1014
|
-
outgoingContentType = "json";
|
|
1015
|
-
}
|
|
1016
|
-
} else {
|
|
1017
|
-
const properties = additionalProperties.reduce(
|
|
1018
|
-
(acc, p) => ({
|
|
1019
|
-
...acc,
|
|
1020
|
-
[p.name]: p.schema
|
|
1021
|
-
}),
|
|
1022
|
-
{}
|
|
1023
|
-
);
|
|
1024
|
-
types[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
|
|
1025
|
-
{
|
|
1026
|
-
type: "object",
|
|
1027
|
-
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
1028
|
-
properties
|
|
965
|
+
additionalProperties.push(param);
|
|
966
|
+
}
|
|
967
|
+
const security2 = operation.security ?? [];
|
|
968
|
+
const securitySchemes = config.spec.components?.securitySchemes ?? {};
|
|
969
|
+
const securityOptions = securityToOptions(security2, securitySchemes);
|
|
970
|
+
Object.assign(inputs, securityOptions);
|
|
971
|
+
additionalProperties.push(
|
|
972
|
+
...Object.entries(securityOptions).map(
|
|
973
|
+
([name, value]) => ({
|
|
974
|
+
name,
|
|
975
|
+
required: false,
|
|
976
|
+
schema: {
|
|
977
|
+
type: "string"
|
|
1029
978
|
},
|
|
1030
|
-
|
|
1031
|
-
)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
}
|
|
1051
|
-
if (statusCode >= 200 && statusCode < 300) {
|
|
1052
|
-
foundResponse = true;
|
|
1053
|
-
const responseContent = get2(response, ["content"]);
|
|
1054
|
-
const isJson = responseContent && responseContent["application/json"];
|
|
1055
|
-
if ((response.headers ?? {})["Transfer-Encoding"]) {
|
|
1056
|
-
parser = "chunked";
|
|
1057
|
-
}
|
|
1058
|
-
const typeScriptDeserialzer = new TypeScriptDeserialzer(
|
|
1059
|
-
config.spec,
|
|
1060
|
-
(schemaName, zod) => {
|
|
1061
|
-
commonSchemas[schemaName] = zod;
|
|
1062
|
-
responsesImports[schemaName] = {
|
|
1063
|
-
defaultImport: void 0,
|
|
1064
|
-
isTypeOnly: true,
|
|
1065
|
-
moduleSpecifier: `../models/${config.makeImport(schemaName)}`,
|
|
1066
|
-
namedImports: [{ isTypeOnly: true, name: schemaName }],
|
|
1067
|
-
namespaceImport: void 0
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
);
|
|
1071
|
-
const responseSchema = isJson ? typeScriptDeserialzer.handle(
|
|
1072
|
-
responseContent["application/json"].schema,
|
|
1073
|
-
true
|
|
1074
|
-
) : statusCode === 204 ? "void" : "ReadableStream";
|
|
1075
|
-
responses.push(responseSchema);
|
|
979
|
+
in: value.in
|
|
980
|
+
})
|
|
981
|
+
)
|
|
982
|
+
);
|
|
983
|
+
const schemas = {};
|
|
984
|
+
const shortContenTypeMap = {
|
|
985
|
+
"application/json": "json",
|
|
986
|
+
"application/x-www-form-urlencoded": "urlencoded",
|
|
987
|
+
"multipart/form-data": "formdata",
|
|
988
|
+
"application/xml": "xml",
|
|
989
|
+
"text/plain": "text"
|
|
990
|
+
};
|
|
991
|
+
let outgoingContentType;
|
|
992
|
+
if (operation.requestBody && Object.keys(operation.requestBody).length) {
|
|
993
|
+
const content = isRef5(operation.requestBody) ? get2(followRef4(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
|
|
994
|
+
for (const type in content) {
|
|
995
|
+
const ctSchema = isRef5(content[type].schema) ? followRef4(config.spec, content[type].schema.$ref) : content[type].schema;
|
|
996
|
+
if (!ctSchema) {
|
|
997
|
+
console.warn(`Schema not found for ${type}`);
|
|
998
|
+
continue;
|
|
1076
999
|
}
|
|
1000
|
+
const schema = merge({}, ctSchema, {
|
|
1001
|
+
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
1002
|
+
properties: additionalProperties.reduce(
|
|
1003
|
+
(acc, p) => ({
|
|
1004
|
+
...acc,
|
|
1005
|
+
[p.name]: p.schema
|
|
1006
|
+
}),
|
|
1007
|
+
{}
|
|
1008
|
+
)
|
|
1009
|
+
});
|
|
1010
|
+
Object.assign(inputs, bodyInputs(config, ctSchema));
|
|
1011
|
+
schemas[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
|
|
1077
1012
|
}
|
|
1078
|
-
if (
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
);
|
|
1013
|
+
if (content["application/json"]) {
|
|
1014
|
+
outgoingContentType = "json";
|
|
1015
|
+
} else if (content["application/x-www-form-urlencoded"]) {
|
|
1016
|
+
outgoingContentType = "urlencoded";
|
|
1017
|
+
} else if (content["multipart/form-data"]) {
|
|
1018
|
+
outgoingContentType = "formdata";
|
|
1085
1019
|
} else {
|
|
1086
|
-
|
|
1087
|
-
`export type ${pascalcase(operationName + " output")} = ${responses[0]};`
|
|
1088
|
-
);
|
|
1020
|
+
outgoingContentType = "json";
|
|
1089
1021
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1022
|
+
} else {
|
|
1023
|
+
const properties = additionalProperties.reduce(
|
|
1024
|
+
(acc, p) => ({
|
|
1025
|
+
...acc,
|
|
1026
|
+
[p.name]: p.schema
|
|
1027
|
+
}),
|
|
1028
|
+
{}
|
|
1092
1029
|
);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1030
|
+
schemas[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
|
|
1031
|
+
{
|
|
1032
|
+
type: "object",
|
|
1033
|
+
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
1034
|
+
properties
|
|
1035
|
+
},
|
|
1036
|
+
true
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const endpoint = toEndpoint(
|
|
1040
|
+
entry.groupName,
|
|
1041
|
+
config.spec,
|
|
1042
|
+
operation,
|
|
1043
|
+
{
|
|
1104
1044
|
outgoingContentType,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1045
|
+
name: entry.name,
|
|
1046
|
+
type: "http",
|
|
1047
|
+
trigger: entry,
|
|
1048
|
+
schemas,
|
|
1049
|
+
inputs
|
|
1050
|
+
},
|
|
1051
|
+
{ makeImport: config.makeImport }
|
|
1052
|
+
);
|
|
1053
|
+
const output = [`import z from 'zod';`];
|
|
1054
|
+
const responses = endpoint.responses.flatMap((it) => it.responses);
|
|
1055
|
+
const responsesImports = endpoint.responses.flatMap(
|
|
1056
|
+
(it) => Object.values(it.imports)
|
|
1057
|
+
);
|
|
1058
|
+
if (responses.length) {
|
|
1059
|
+
output.push(
|
|
1060
|
+
...responses.map((it) => `export type ${it.name} = ${it.schema};`)
|
|
1061
|
+
);
|
|
1062
|
+
} else {
|
|
1063
|
+
output.push(`export type ${pascalcase2(entry.name + " output")} = void;`);
|
|
1064
|
+
}
|
|
1065
|
+
output.unshift(...useImports(output.join(""), ...responsesImports));
|
|
1066
|
+
outputs[`${spinalcase2(entry.name)}.ts`] = output.join("\n");
|
|
1067
|
+
endpoints[entry.groupName].push(endpoint);
|
|
1068
|
+
groups[entry.groupName].push({
|
|
1069
|
+
name: entry.name,
|
|
1070
|
+
type: "http",
|
|
1071
|
+
inputs,
|
|
1072
|
+
outgoingContentType,
|
|
1073
|
+
schemas,
|
|
1074
|
+
trigger: entry
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
const commonSchemas = Object.values(endpoints).reduce(
|
|
1078
|
+
(acc, endpoint) => ({
|
|
1079
|
+
...acc,
|
|
1080
|
+
...endpoint.reduce(
|
|
1081
|
+
(acc2, { responses }) => ({
|
|
1082
|
+
...acc2,
|
|
1083
|
+
...responses.reduce(
|
|
1084
|
+
(acc3, it) => ({ ...acc3, ...it.schemas }),
|
|
1085
|
+
{}
|
|
1086
|
+
)
|
|
1110
1087
|
}),
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1088
|
+
{}
|
|
1089
|
+
)
|
|
1090
|
+
}),
|
|
1091
|
+
{}
|
|
1092
|
+
);
|
|
1093
|
+
const allSchemas = Object.keys(endpoints).map((it) => ({
|
|
1094
|
+
import: `import ${camelcase2(it)} from './${config.makeImport(spinalcase2(it))}';`,
|
|
1095
|
+
use: ` ...${camelcase2(it)}`
|
|
1096
|
+
}));
|
|
1097
|
+
const imports = [
|
|
1098
|
+
'import z from "zod";',
|
|
1099
|
+
`import type { ParseError } from '${config.makeImport("../http/parser")}';`,
|
|
1100
|
+
`import type { ServerError } from '${config.makeImport("../http/response")}';`,
|
|
1101
|
+
`import type { OutputType, Parser, Type } from '../http/send-request.ts';`
|
|
1102
|
+
];
|
|
1103
|
+
return {
|
|
1104
|
+
groups,
|
|
1105
|
+
commonSchemas,
|
|
1106
|
+
commonZod,
|
|
1107
|
+
outputs,
|
|
1108
|
+
clientFiles: {},
|
|
1109
|
+
endpoints: {
|
|
1110
|
+
[`${join("api", config.makeImport("schemas"))}`]: `
|
|
1111
|
+
${imports.join("\n")}
|
|
1112
|
+
${allSchemas.map((it) => it.import).join("\n")}
|
|
1113
|
+
|
|
1114
|
+
const schemas = {
|
|
1115
|
+
${allSchemas.map((it) => it.use).join(",\n")}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
type Output<T extends OutputType> = T extends {
|
|
1120
|
+
parser: Parser;
|
|
1121
|
+
type: Type<any>;
|
|
1122
|
+
}
|
|
1123
|
+
? InstanceType<T['type']>
|
|
1124
|
+
: T extends Type<any>
|
|
1125
|
+
? InstanceType<T>
|
|
1126
|
+
: never;
|
|
1127
|
+
|
|
1128
|
+
export type Endpoints = {
|
|
1129
|
+
[K in keyof typeof schemas]: {
|
|
1130
|
+
input: z.infer<(typeof schemas)[K]['schema']>;
|
|
1131
|
+
output: (typeof schemas)[K]['output'] extends [
|
|
1132
|
+
infer Single extends OutputType,
|
|
1133
|
+
]
|
|
1134
|
+
? Output<Single>
|
|
1135
|
+
: (typeof schemas)[K]['output'] extends readonly [
|
|
1136
|
+
...infer Tuple extends OutputType[],
|
|
1137
|
+
]
|
|
1138
|
+
? { [I in keyof Tuple]: Output<Tuple[I]> }[number]
|
|
1139
|
+
: never;
|
|
1140
|
+
error: ServerError | ParseError<(typeof schemas)[K]['schema']>;
|
|
1141
|
+
};
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
export default schemas;
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
`.trim(),
|
|
1148
|
+
...Object.fromEntries(
|
|
1149
|
+
Object.entries(endpoints).map(([name, endpoint]) => {
|
|
1150
|
+
const imps = importsToString(
|
|
1151
|
+
...mergeImports(
|
|
1152
|
+
...endpoint.flatMap(
|
|
1153
|
+
(it) => it.responses.flatMap(
|
|
1154
|
+
(it2) => Object.values(it2.endpointImports)
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
);
|
|
1159
|
+
return [
|
|
1160
|
+
[
|
|
1161
|
+
join("api", config.makeImport(spinalcase2(name))),
|
|
1162
|
+
`${[
|
|
1163
|
+
...imps,
|
|
1164
|
+
// ...imports,
|
|
1165
|
+
`import z from 'zod';`,
|
|
1166
|
+
`import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${config.makeImport("../http/request")}';`,
|
|
1167
|
+
`import { chunked, buffered } from "${config.makeImport("../http/parse-response")}";`,
|
|
1168
|
+
`import * as ${camelcase2(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
|
|
1169
|
+
].join(
|
|
1170
|
+
"\n"
|
|
1171
|
+
)}
|
|
1172
|
+
export default {
|
|
1173
|
+
${endpoint.flatMap((it) => it.schemas).join(",\n")}
|
|
1174
|
+
}`
|
|
1175
|
+
]
|
|
1176
|
+
];
|
|
1177
|
+
}).flat()
|
|
1178
|
+
)
|
|
1116
1179
|
}
|
|
1117
|
-
}
|
|
1118
|
-
return { groups, commonSchemas, commonZod, outputs };
|
|
1180
|
+
};
|
|
1119
1181
|
}
|
|
1120
1182
|
function toProps(spec, schemaOrRef, aggregator = []) {
|
|
1121
|
-
if (
|
|
1122
|
-
const schema =
|
|
1183
|
+
if (isRef5(schemaOrRef)) {
|
|
1184
|
+
const schema = followRef4(spec, schemaOrRef.$ref);
|
|
1123
1185
|
return toProps(spec, schema, aggregator);
|
|
1124
1186
|
} else if (schemaOrRef.type === "object") {
|
|
1125
1187
|
for (const [name] of Object.entries(schemaOrRef.properties ?? {})) {
|
|
@@ -1164,22 +1226,22 @@ function bodyInputs(config, ctSchema) {
|
|
|
1164
1226
|
}
|
|
1165
1227
|
|
|
1166
1228
|
// packages/typescript/src/lib/http/interceptors.txt
|
|
1167
|
-
var interceptors_default = "import { type RequestConfig } from './request.ts';\n\nexport interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig>|RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (getBaseUrl: () => string)
|
|
1229
|
+
var interceptors_default = "import { type RequestConfig } from './request.ts';\n\nexport interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig>|RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (\n getBaseUrl: () => string,\n): Interceptor => {\n return {\n before({ init, url }) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.dir('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
|
|
1168
1230
|
|
|
1169
1231
|
// packages/typescript/src/lib/http/parse-response.txt
|
|
1170
|
-
var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nexport async function handleError(response: Response) {\n try {\n if (response.status >= 400 && response.status < 500) {\n const body = (await response.json()) as Record<string, any>;\n return {\n status: response.status,\n body: body,\n };\n }\n return new Error(\n `An error occurred while fetching the data. Status: ${response.status}`,\n );\n } catch (error) {\n return error as any;\n }\n}\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body
|
|
1232
|
+
var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nexport async function handleError(response: Response) {\n try {\n if (response.status >= 400 && response.status < 500) {\n const body = (await response.json()) as Record<string, any>;\n return {\n status: response.status,\n body: body,\n };\n }\n return new Error(\n `An error occurred while fetching the data. Status: ${response.status}`,\n );\n } catch (error) {\n return error as any;\n }\n}\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body!;\n}\n\nexport async function buffered(response: Response) {\n const contentType = response.headers.get("Content-Type");\n if (!contentType) {\n throw new Error("Content-Type header is missing");\n }\n\n if (response.status === 204) {\n return null;\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case "application/json":\n return response.json();\n case "text/plain":\n return response.text();\n case "text/html":\n return response.text();\n case "text/xml":\n case "application/xml":\n return response.text();\n case "application/x-www-form-urlencoded": {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case "multipart/form-data":\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n';
|
|
1171
1233
|
|
|
1172
1234
|
// packages/typescript/src/lib/http/parser.txt
|
|
1173
|
-
var parser_default =
|
|
1235
|
+
var parser_default = 'import { z } from "zod";\n\nexport class ParseError<T extends z.ZodType<any, any, any>> {\n public data: z.typeToFlattenedError<T, z.ZodIssue>;\n constructor(data: z.typeToFlattenedError<T, z.ZodIssue>) {\n this.data = data;\n }\n}\n\nexport function parse<T extends z.ZodType<any, any, any>>(schema: T, input: unknown) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const error = result.error.flatten((issue) => issue);\n return [null, new ParseError(error)];\n }\n return [result.data as z.infer<T>, null];\n}\n';
|
|
1174
1236
|
|
|
1175
1237
|
// packages/typescript/src/lib/http/request.txt
|
|
1176
1238
|
var request_default = "type Init = Omit<RequestInit, 'headers'> & { headers: Headers; };\nexport type RequestConfig = { init: Init; url: URL };\nexport type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\nexport type ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart' | 'formdata';\nexport type Endpoint =\n | `${ContentType} ${Method} ${string}`\n | `${Method} ${string}`;\n\nexport type BodyInit =\n | ArrayBuffer\n | Blob\n | FormData\n | URLSearchParams\n | null\n | string;\n\nexport function createUrl(path: string, query: URLSearchParams) {\n const url = new URL(path, `local://`);\n url.search = query.toString();\n return url;\n}\n\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n protected input: Input;\n protected props: Props;\n\n constructor(\n input: Input,\n props: Props,\n ) {\n this.input = input;\n this.props = props;\n }\n\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Record<string, string>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Record<string, string>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass NoBodySerializer extends Serializer {\n getBody(): BodyInit | null {\n return null;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function nobody(input: Input, props: Props) {\n return new NoBodySerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n): RequestConfig {\n const [method, path] = endpoint.split(' ');\n const pathVariable = template(path, input.params);\n\n return {\n url: createUrl(pathVariable, input.query),\n init: {\n method: method,\n headers: new Headers(input.headers),\n body: method === 'GET' ? undefined : input.body,\n },\n }\n}\n";
|
|
1177
1239
|
|
|
1178
1240
|
// packages/typescript/src/lib/http/response.txt
|
|
1179
|
-
var response_default = "export
|
|
1241
|
+
var response_default = "export class APIResponse<Body = unknown, Status extends number = number> {\n static status: number;\n status: Status;\n data: Body;\n\n constructor(status: Status, data: Body) {\n this.status = status;\n this.data = data;\n }\n}\n\nexport class APIError<Body, Status extends number = number> extends APIResponse<\n Body,\n Status\n> {}\n\n// 2xx Success\nexport class Ok<T> extends APIResponse<T, 200> {\n static status = 200;\n}\nexport class Created<T> extends APIResponse<T, 201> {}\nexport class Accepted<T> extends APIResponse<T, 202> {}\nexport class NoContent extends APIResponse<null, 204> {}\n\n// 4xx Client Errors\nexport class BadRequest<T> extends APIError<T, 400> {}\nexport class Unauthorized<T = { message: string }> extends APIError<T, 401> {}\nexport class PaymentRequired<T = { message: string }> extends APIError<\n T,\n 402\n> {}\nexport class Forbidden<T = { message: string }> extends APIError<T, 403> {}\nexport class NotFound<T = { message: string }> extends APIError<T, 404> {}\nexport class MethodNotAllowed<T = { message: string }> extends APIError<\n T,\n 405\n> {}\nexport class NotAcceptable<T = { message: string }> extends APIError<T, 406> {}\nexport class Conflict<T = { message: string }> extends APIError<T, 409> {}\nexport class Gone<T = { message: string }> extends APIError<T, 410> {}\nexport class UnprocessableEntity<\n T = { message: string; errors?: Record<string, string[]> },\n> extends APIError<T, 422> {}\nexport class TooManyRequests<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 429> {}\nexport class PayloadTooLarge<T = { message: string }> extends APIError<\n T,\n 413\n> {}\nexport class UnsupportedMediaType<T = { message: string }> extends APIError<\n T,\n 415\n> {}\n\n// 5xx Server Errors\nexport class InternalServerError<T = { message: string }> extends APIError<\n T,\n 500\n> {}\nexport class NotImplemented<T = { message: string }> extends APIError<T, 501> {}\nexport class BadGateway<T = { message: string }> extends APIError<T, 502> {}\nexport class ServiceUnavailable<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 503> {}\nexport class GatewayTimeout<T = { message: string }> extends APIError<T, 504> {}\n\nexport type ClientError =\n | BadRequest<{ message: string }>\n | Unauthorized\n | PaymentRequired\n | Forbidden\n | NotFound\n | MethodNotAllowed\n | NotAcceptable\n | Conflict\n | Gone\n | UnprocessableEntity\n | TooManyRequests;\n\nexport type ServerError =\n | InternalServerError\n | NotImplemented\n | BadGateway\n | ServiceUnavailable\n | GatewayTimeout;\n\nexport type ProblematicResponse = ClientError | ServerError;\n";
|
|
1180
1242
|
|
|
1181
1243
|
// packages/typescript/src/lib/http/send-request.txt
|
|
1182
|
-
var send_request_default = "\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n
|
|
1244
|
+
var send_request_default = "export interface Type<T> {\n new (...args: any[]): T;\n}\nexport type Parser = (\n response: Response,\n) => Promise<unknown> | ReadableStream<any>;\nexport type OutputType =\n | Type<APIResponse>\n | { parser: Parser; type: Type<APIResponse> };\ntype Constructor<T> = new (...args: any[]) => T;\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n output: OutputType[];\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: unknown,\n route: RequestSchema,\n options: {\n fetch?: z.infer<typeof fetchType>;\n interceptors?: Interceptor[];\n signal?: AbortSignal;\n },\n) {\n const { interceptors = [] } = options;\n const [parsedInput, parseError] = parse(route.schema, input);\n if (parseError) {\n return [null as never, { ...parseError, kind: 'parse' } as never] as const;\n }\n\n let config = route.toRequest(parsedInput as never);\n for (const interceptor of interceptors) {\n if (interceptor.before) {\n config = await interceptor.before(config);\n }\n }\n\n let response = await (options.fetch ?? fetch)(\n new Request(config.url, config.init),\n {\n ...config.init,\n signal: options.signal,\n },\n );\n\n for (let i = interceptors.length - 1; i >= 0; i--) {\n const interceptor = interceptors[i];\n if (interceptor.after) {\n response = await interceptor.after(response.clone());\n }\n }\n\n let output: Constructor<APIResponse> | null = APIResponse;\n let parser: Parser = buffered;\n for (const outputType of route.output) {\n if ('parser' in outputType) {\n parser = outputType.parser;\n if (isTypeOf(outputType.type, APIResponse)) {\n if (response.status === outputType.type.status) {\n output = outputType.type;\n break;\n }\n }\n } else if (isTypeOf(outputType, APIResponse)) {\n if (response.status === outputType.status) {\n output = outputType;\n break;\n }\n }\n }\n\n const data = new output(response.status, await parser(response));\n if (response.ok) {\n return [data as never, null] as const;\n }\n return [null as never, data as never] as const;\n}\n\nexport function isTypeOf<T extends Type<APIResponse>>(\n instance: any,\n baseType: T,\n): instance is T {\n if (instance === baseType) {\n return true;\n }\n const prototype = Object.getPrototypeOf(instance);\n if (prototype === null) {\n return false;\n }\n return isTypeOf(prototype, baseType);\n}\n";
|
|
1183
1245
|
|
|
1184
1246
|
// packages/typescript/src/lib/generate.ts
|
|
1185
1247
|
function security(spec) {
|
|
@@ -1207,21 +1269,14 @@ async function generate(spec, settings) {
|
|
|
1207
1269
|
const makeImport = (moduleSpecifier) => {
|
|
1208
1270
|
return settings.useTsExtension ? `${moduleSpecifier}.ts` : moduleSpecifier;
|
|
1209
1271
|
};
|
|
1210
|
-
const { commonSchemas, groups, outputs, commonZod } = generateCode({
|
|
1272
|
+
const { commonSchemas, endpoints, groups, outputs, commonZod, clientFiles } = generateCode({
|
|
1211
1273
|
spec,
|
|
1212
1274
|
style: "github",
|
|
1213
|
-
target: "javascript",
|
|
1214
1275
|
makeImport
|
|
1215
1276
|
});
|
|
1216
|
-
const output = settings.mode === "full" ?
|
|
1277
|
+
const output = settings.mode === "full" ? join2(settings.output, "src") : settings.output;
|
|
1217
1278
|
const options = security(spec);
|
|
1218
|
-
const
|
|
1219
|
-
name: settings.name || "Client",
|
|
1220
|
-
operations: groups,
|
|
1221
|
-
servers: spec.servers?.map((server) => server.url) || [],
|
|
1222
|
-
options,
|
|
1223
|
-
makeImport
|
|
1224
|
-
});
|
|
1279
|
+
const clientName = settings.name || "Client";
|
|
1225
1280
|
const inputFiles = generateInputs(groups, commonZod, makeImport);
|
|
1226
1281
|
await writeFiles(output, {
|
|
1227
1282
|
"outputs/.gitkeep": "",
|
|
@@ -1229,24 +1284,33 @@ async function generate(spec, settings) {
|
|
|
1229
1284
|
"models/.getkeep": ""
|
|
1230
1285
|
// 'README.md': readme,
|
|
1231
1286
|
});
|
|
1232
|
-
await writeFiles(
|
|
1287
|
+
await writeFiles(join2(output, "http"), {
|
|
1233
1288
|
"interceptors.ts": interceptors_default,
|
|
1234
1289
|
"parse-response.ts": parse_response_default,
|
|
1235
1290
|
"send-request.ts": `import z from 'zod';
|
|
1236
1291
|
import type { Interceptor } from './${makeImport("interceptors")}';
|
|
1237
|
-
import {
|
|
1292
|
+
import { buffered } from './${makeImport("parse-response")}';
|
|
1238
1293
|
import { parse } from './${makeImport("parser")}';
|
|
1239
|
-
import type { RequestConfig } from '
|
|
1294
|
+
import type { RequestConfig } from './${makeImport("request")}';
|
|
1295
|
+
import { APIResponse } from './${makeImport("response")}';
|
|
1296
|
+
|
|
1240
1297
|
${send_request_default}`,
|
|
1241
1298
|
"response.ts": response_default,
|
|
1242
1299
|
"parser.ts": parser_default,
|
|
1243
1300
|
"request.ts": request_default
|
|
1244
1301
|
});
|
|
1245
|
-
await writeFiles(
|
|
1302
|
+
await writeFiles(join2(output, "outputs"), outputs);
|
|
1246
1303
|
const modelsImports = Object.entries(commonSchemas).map(([name]) => name);
|
|
1247
1304
|
await writeFiles(output, {
|
|
1305
|
+
"client.ts": client_default({
|
|
1306
|
+
name: clientName,
|
|
1307
|
+
servers: (spec.servers ?? []).map((server) => server.url) || [],
|
|
1308
|
+
options,
|
|
1309
|
+
makeImport
|
|
1310
|
+
}),
|
|
1248
1311
|
...clientFiles,
|
|
1249
1312
|
...inputFiles,
|
|
1313
|
+
...endpoints,
|
|
1250
1314
|
...Object.fromEntries(
|
|
1251
1315
|
Object.entries(commonSchemas).map(([name, schema]) => [
|
|
1252
1316
|
`models/${name}.ts`,
|
|
@@ -1261,29 +1325,36 @@ ${send_request_default}`,
|
|
|
1261
1325
|
)
|
|
1262
1326
|
});
|
|
1263
1327
|
const folders = [
|
|
1264
|
-
getFolderExports(
|
|
1328
|
+
getFolderExports(join2(output, "outputs"), settings.useTsExtension),
|
|
1265
1329
|
getFolderExports(
|
|
1266
|
-
|
|
1330
|
+
join2(output, "inputs"),
|
|
1267
1331
|
settings.useTsExtension,
|
|
1268
1332
|
["ts"],
|
|
1269
|
-
(dirent) => dirent.isDirectory() && dirent.name
|
|
1333
|
+
(dirent) => dirent.isDirectory() && ["schemas"].includes(dirent.name)
|
|
1270
1334
|
),
|
|
1271
|
-
getFolderExports(
|
|
1335
|
+
getFolderExports(join2(output, "api"), settings.useTsExtension),
|
|
1336
|
+
getFolderExports(
|
|
1337
|
+
join2(output, "http"),
|
|
1338
|
+
settings.useTsExtension,
|
|
1339
|
+
["ts"],
|
|
1340
|
+
(dirent) => dirent.name !== "response.ts"
|
|
1341
|
+
)
|
|
1272
1342
|
];
|
|
1273
1343
|
if (modelsImports.length) {
|
|
1274
1344
|
folders.push(
|
|
1275
|
-
getFolderExports(
|
|
1345
|
+
getFolderExports(join2(output, "models"), settings.useTsExtension)
|
|
1276
1346
|
);
|
|
1277
1347
|
}
|
|
1278
|
-
const [outputIndex, inputsIndex, httpIndex, modelsIndex] = await Promise.all(folders);
|
|
1348
|
+
const [outputIndex, inputsIndex, apiIndex, httpIndex, modelsIndex] = await Promise.all(folders);
|
|
1279
1349
|
await writeFiles(output, {
|
|
1350
|
+
"api/index.ts": apiIndex,
|
|
1280
1351
|
"outputs/index.ts": outputIndex,
|
|
1281
1352
|
"inputs/index.ts": inputsIndex || null,
|
|
1282
1353
|
"http/index.ts": httpIndex,
|
|
1283
1354
|
...modelsImports.length ? { "models/index.ts": modelsIndex } : {}
|
|
1284
1355
|
});
|
|
1285
1356
|
await writeFiles(output, {
|
|
1286
|
-
"index.ts": await getFolderExports(output, settings.useTsExtension)
|
|
1357
|
+
"index.ts": await getFolderExports(output, settings.useTsExtension, ["ts"])
|
|
1287
1358
|
});
|
|
1288
1359
|
if (settings.mode === "full") {
|
|
1289
1360
|
await writeFiles(settings.output, {
|
|
@@ -1291,7 +1362,7 @@ ${send_request_default}`,
|
|
|
1291
1362
|
ignoreIfExists: true,
|
|
1292
1363
|
content: JSON.stringify(
|
|
1293
1364
|
{
|
|
1294
|
-
name: "sdk",
|
|
1365
|
+
name: settings.name ? `@${spinalcase3(clientName.toLowerCase())}/sdk` : "sdk",
|
|
1295
1366
|
type: "module",
|
|
1296
1367
|
main: "./src/index.ts",
|
|
1297
1368
|
dependencies: {
|