@sdk-it/typescript 0.12.9 → 0.12.10
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 3.js +977 -0
- package/dist/index 4.js +1228 -0
- package/dist/index.d 3.ts +3 -0
- package/dist/index.d 4.ts +3 -0
- package/dist/index.d.ts 3.map +1 -0
- package/dist/index.d.ts 4.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js 3.map +7 -0
- package/dist/index.js 4.map +7 -0
- package/dist/index.js.map +2 -2
- package/dist/lib/generate.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index 3.js
ADDED
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
// packages/typescript/src/lib/generate.ts
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { npmRunPathEnv } from "npm-run-path";
|
|
4
|
+
|
|
5
|
+
// packages/core/dist/index.js
|
|
6
|
+
import ts, { TypeFlags, symbolName } from "typescript";
|
|
7
|
+
import debug from "debug";
|
|
8
|
+
import ts2 from "typescript";
|
|
9
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
11
|
+
var deriveSymbol = Symbol.for("serialize");
|
|
12
|
+
var $types = Symbol.for("types");
|
|
13
|
+
var logger = debug("january:client");
|
|
14
|
+
var methods = [
|
|
15
|
+
"get",
|
|
16
|
+
"post",
|
|
17
|
+
"put",
|
|
18
|
+
"patch",
|
|
19
|
+
"delete",
|
|
20
|
+
"trace",
|
|
21
|
+
"head"
|
|
22
|
+
];
|
|
23
|
+
async function exist(file) {
|
|
24
|
+
return stat(file).then(() => true).catch(() => false);
|
|
25
|
+
}
|
|
26
|
+
async function writeFiles(dir, contents) {
|
|
27
|
+
return Promise.all(
|
|
28
|
+
Object.entries(contents).map(async ([file, content]) => {
|
|
29
|
+
const filePath = isAbsolute(file) ? file : join2(dir, file);
|
|
30
|
+
await mkdir(dirname2(filePath), { recursive: true });
|
|
31
|
+
if (typeof content === "string") {
|
|
32
|
+
await writeFile(filePath, content, "utf-8");
|
|
33
|
+
} else {
|
|
34
|
+
if (content.ignoreIfExists) {
|
|
35
|
+
if (!await exist(filePath)) {
|
|
36
|
+
await writeFile(filePath, content.content, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
async function getFolderExports(folder, extensions = ["ts"]) {
|
|
44
|
+
const files = await readdir(folder, { withFileTypes: true });
|
|
45
|
+
const exports = [];
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (file.isDirectory()) {
|
|
48
|
+
exports.push(`export * from './${file.name}';`);
|
|
49
|
+
} else if (file.name !== "index.ts" && extensions.includes(getExt(file.name))) {
|
|
50
|
+
exports.push(`export * from './${file.name}';`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return exports.join("\n");
|
|
54
|
+
}
|
|
55
|
+
var getExt = (fileName) => {
|
|
56
|
+
if (!fileName) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
const lastDot = fileName.lastIndexOf(".");
|
|
60
|
+
if (lastDot === -1) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
const ext = fileName.slice(lastDot + 1).split("/").filter(Boolean).join("");
|
|
64
|
+
if (ext === fileName) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
return ext || "txt";
|
|
68
|
+
};
|
|
69
|
+
function removeDuplicates(data, accessor) {
|
|
70
|
+
return [...new Map(data.map((x) => [accessor(x), x])).values()];
|
|
71
|
+
}
|
|
72
|
+
function toLitObject(obj, accessor = (value) => value) {
|
|
73
|
+
return `{${Object.keys(obj).map((key) => `${key}: ${accessor(obj[key])}`).join(", ")}}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// packages/typescript/src/lib/generator.ts
|
|
77
|
+
import { get as get2, merge } from "lodash-es";
|
|
78
|
+
import { camelcase as camelcase2, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
|
|
79
|
+
|
|
80
|
+
// packages/typescript/src/lib/json-zod.ts
|
|
81
|
+
import { get } from "lodash-es";
|
|
82
|
+
function cleanRef(ref) {
|
|
83
|
+
return ref.replace(/^#\//, "");
|
|
84
|
+
}
|
|
85
|
+
function parseRef(ref) {
|
|
86
|
+
const parts = ref.split(ref);
|
|
87
|
+
const [model] = parts.splice(-1);
|
|
88
|
+
return { model, path: parts.join("/") };
|
|
89
|
+
}
|
|
90
|
+
function followRef(spec, ref) {
|
|
91
|
+
const pathParts = cleanRef(ref).split("/");
|
|
92
|
+
const entry = get(spec, pathParts);
|
|
93
|
+
if (entry && "$ref" in entry) {
|
|
94
|
+
return followRef(spec, entry.$ref);
|
|
95
|
+
}
|
|
96
|
+
return entry;
|
|
97
|
+
}
|
|
98
|
+
function jsonSchemaToZod(spec, schema, required = false, onRef, circularRefTracker = /* @__PURE__ */ new Set()) {
|
|
99
|
+
if ("$ref" in schema) {
|
|
100
|
+
const schemaName = cleanRef(schema.$ref).split("/").pop();
|
|
101
|
+
if (circularRefTracker.has(schemaName)) {
|
|
102
|
+
return schemaName;
|
|
103
|
+
}
|
|
104
|
+
circularRefTracker.add(schemaName);
|
|
105
|
+
onRef(
|
|
106
|
+
schemaName,
|
|
107
|
+
jsonSchemaToZod(
|
|
108
|
+
spec,
|
|
109
|
+
followRef(spec, schema.$ref),
|
|
110
|
+
required,
|
|
111
|
+
onRef,
|
|
112
|
+
circularRefTracker
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
circularRefTracker.delete(schemaName);
|
|
116
|
+
return schemaName;
|
|
117
|
+
}
|
|
118
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
119
|
+
const allOfSchemas = schema.allOf.map(
|
|
120
|
+
(sub) => jsonSchemaToZod(spec, sub, true, onRef, circularRefTracker)
|
|
121
|
+
);
|
|
122
|
+
return allOfSchemas.length ? `z.intersection(${allOfSchemas.join(", ")})` : allOfSchemas[0];
|
|
123
|
+
}
|
|
124
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
125
|
+
const anyOfSchemas = schema.anyOf.map(
|
|
126
|
+
(sub) => jsonSchemaToZod(spec, sub, false, onRef, circularRefTracker)
|
|
127
|
+
);
|
|
128
|
+
return anyOfSchemas.length > 1 ? `z.union([${anyOfSchemas.join(", ")}])${appendOptional(required)}` : (
|
|
129
|
+
// Handle an invalid anyOf with one schema
|
|
130
|
+
anyOfSchemas[0]
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
134
|
+
const oneOfSchemas = schema.oneOf.map((sub) => {
|
|
135
|
+
if ("$ref" in sub) {
|
|
136
|
+
const { model } = parseRef(sub.$ref);
|
|
137
|
+
if (circularRefTracker.has(model)) {
|
|
138
|
+
return model;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return jsonSchemaToZod(spec, sub, false, onRef, circularRefTracker);
|
|
142
|
+
});
|
|
143
|
+
return oneOfSchemas.length > 1 ? `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}` : (
|
|
144
|
+
// Handle an invalid oneOf with one schema
|
|
145
|
+
oneOfSchemas[0]
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
149
|
+
const enumVals = schema.enum.map((val) => JSON.stringify(val)).join(", ");
|
|
150
|
+
return `z.enum([${enumVals}])${appendOptional(required)}`;
|
|
151
|
+
}
|
|
152
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
153
|
+
if (!types.length) {
|
|
154
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
155
|
+
}
|
|
156
|
+
if (types.length > 1) {
|
|
157
|
+
const realTypes = types.filter((t) => t !== "null");
|
|
158
|
+
if (realTypes.length === 1 && types.includes("null")) {
|
|
159
|
+
const typeZod = basicTypeToZod(
|
|
160
|
+
realTypes[0],
|
|
161
|
+
schema,
|
|
162
|
+
spec,
|
|
163
|
+
false,
|
|
164
|
+
onRef,
|
|
165
|
+
circularRefTracker
|
|
166
|
+
);
|
|
167
|
+
return `${typeZod}.nullable()${appendOptional(required)}`;
|
|
168
|
+
}
|
|
169
|
+
const subSchemas = types.map(
|
|
170
|
+
(t) => basicTypeToZod(t, schema, spec, false, onRef, circularRefTracker)
|
|
171
|
+
);
|
|
172
|
+
return `z.union([${subSchemas.join(", ")}])${appendOptional(required)}`;
|
|
173
|
+
}
|
|
174
|
+
return basicTypeToZod(
|
|
175
|
+
types[0],
|
|
176
|
+
schema,
|
|
177
|
+
spec,
|
|
178
|
+
required,
|
|
179
|
+
onRef,
|
|
180
|
+
circularRefTracker
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function basicTypeToZod(type, schema, spec, required = false, onRef, refProcessingStack) {
|
|
184
|
+
switch (type) {
|
|
185
|
+
case "string":
|
|
186
|
+
return handleString(schema, required);
|
|
187
|
+
case "number":
|
|
188
|
+
case "integer":
|
|
189
|
+
return handleNumber(schema, required);
|
|
190
|
+
case "boolean":
|
|
191
|
+
return `z.boolean()${appendDefault(schema.default)}${appendOptional(required)}`;
|
|
192
|
+
case "object":
|
|
193
|
+
return handleObject(schema, spec, required, onRef, refProcessingStack);
|
|
194
|
+
case "array":
|
|
195
|
+
return handleArray(schema, spec, required, onRef, refProcessingStack);
|
|
196
|
+
case "null":
|
|
197
|
+
return `z.null()${appendOptional(required)}`;
|
|
198
|
+
default:
|
|
199
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function handleString(schema, required) {
|
|
203
|
+
let base = "z.string()";
|
|
204
|
+
switch (schema.format) {
|
|
205
|
+
case "date-time":
|
|
206
|
+
case "datetime":
|
|
207
|
+
base = "z.coerce.date()";
|
|
208
|
+
break;
|
|
209
|
+
case "date":
|
|
210
|
+
base = "z.coerce.date() /* or z.string() if you want raw date strings */";
|
|
211
|
+
break;
|
|
212
|
+
case "time":
|
|
213
|
+
base = "z.string() /* optionally add .regex(...) for HH:MM:SS format */";
|
|
214
|
+
break;
|
|
215
|
+
case "email":
|
|
216
|
+
base = "z.string().email()";
|
|
217
|
+
break;
|
|
218
|
+
case "uuid":
|
|
219
|
+
base = "z.string().uuid()";
|
|
220
|
+
break;
|
|
221
|
+
case "url":
|
|
222
|
+
case "uri":
|
|
223
|
+
base = "z.string().url()";
|
|
224
|
+
break;
|
|
225
|
+
case "ipv4":
|
|
226
|
+
base = 'z.string().ip({version: "v4"})';
|
|
227
|
+
break;
|
|
228
|
+
case "ipv6":
|
|
229
|
+
base = 'z.string().ip({version: "v6"})';
|
|
230
|
+
break;
|
|
231
|
+
case "phone":
|
|
232
|
+
base = "z.string() /* or add .regex(...) for phone formats */";
|
|
233
|
+
break;
|
|
234
|
+
case "byte":
|
|
235
|
+
case "binary":
|
|
236
|
+
base = "z.instanceof(Blob) /* consider base64 check if needed */";
|
|
237
|
+
break;
|
|
238
|
+
case "int64":
|
|
239
|
+
base = "z.string() /* or z.bigint() if your app can handle it */";
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
return `${base}${appendDefault(schema.default)}${appendOptional(required)}`;
|
|
245
|
+
}
|
|
246
|
+
function handleNumber(schema, required) {
|
|
247
|
+
let defaultValue = schema.default !== void 0 ? `.default(${schema.default})` : ``;
|
|
248
|
+
let base = "z.number()";
|
|
249
|
+
if (schema.format === "int64") {
|
|
250
|
+
base = "z.bigint()";
|
|
251
|
+
if (schema.default !== void 0) {
|
|
252
|
+
defaultValue = `.default(BigInt(${schema.default}))`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (schema.format === "int32") {
|
|
256
|
+
base += ".int()";
|
|
257
|
+
}
|
|
258
|
+
if (typeof schema.exclusiveMinimum === "number") {
|
|
259
|
+
base += `.gt(${schema.exclusiveMinimum})`;
|
|
260
|
+
}
|
|
261
|
+
if (typeof schema.exclusiveMaximum === "number") {
|
|
262
|
+
base += `.lt(${schema.exclusiveMaximum})`;
|
|
263
|
+
}
|
|
264
|
+
if (typeof schema.minimum === "number") {
|
|
265
|
+
base += schema.format === "int64" ? `.min(BigInt(${schema.minimum}))` : `.min(${schema.minimum})`;
|
|
266
|
+
}
|
|
267
|
+
if (typeof schema.maximum === "number") {
|
|
268
|
+
base += schema.format === "int64" ? `.max(BigInt(${schema.maximum}))` : `.max(${schema.maximum})`;
|
|
269
|
+
}
|
|
270
|
+
if (typeof schema.multipleOf === "number") {
|
|
271
|
+
base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
|
|
272
|
+
}
|
|
273
|
+
return `${base}${defaultValue}${appendOptional(required)}`;
|
|
274
|
+
}
|
|
275
|
+
function handleObject(schema, spec, required = false, onRef, refProcessingStack) {
|
|
276
|
+
const properties = schema.properties || {};
|
|
277
|
+
const propEntries = Object.entries(properties).map(([key, propSchema]) => {
|
|
278
|
+
const isRequired = (schema.required ?? []).includes(key);
|
|
279
|
+
const zodPart = jsonSchemaToZod(
|
|
280
|
+
spec,
|
|
281
|
+
propSchema,
|
|
282
|
+
isRequired,
|
|
283
|
+
onRef,
|
|
284
|
+
refProcessingStack
|
|
285
|
+
);
|
|
286
|
+
return `'${key}': ${zodPart}`;
|
|
287
|
+
});
|
|
288
|
+
let additionalProps = "";
|
|
289
|
+
if (schema.additionalProperties) {
|
|
290
|
+
if (typeof schema.additionalProperties === "object") {
|
|
291
|
+
const addPropZod = jsonSchemaToZod(
|
|
292
|
+
spec,
|
|
293
|
+
schema.additionalProperties,
|
|
294
|
+
true,
|
|
295
|
+
onRef,
|
|
296
|
+
refProcessingStack
|
|
297
|
+
);
|
|
298
|
+
additionalProps = `.catchall(${addPropZod})`;
|
|
299
|
+
} else if (schema.additionalProperties === true) {
|
|
300
|
+
additionalProps = `.catchall(z.unknown())`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const objectSchema = `z.object({${propEntries.join(", ")}})${additionalProps}`;
|
|
304
|
+
return `${objectSchema}${appendOptional(required)}`;
|
|
305
|
+
}
|
|
306
|
+
function handleArray(schema, spec, required = false, onRef, refProcessingStack) {
|
|
307
|
+
const { items } = schema;
|
|
308
|
+
if (!items) {
|
|
309
|
+
return `z.array(z.unknown())${appendOptional(required)}`;
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(items)) {
|
|
312
|
+
const tupleItems = items.map(
|
|
313
|
+
(sub) => jsonSchemaToZod(spec, sub, true, onRef, refProcessingStack)
|
|
314
|
+
);
|
|
315
|
+
const base = `z.tuple([${tupleItems.join(", ")}])`;
|
|
316
|
+
return `${base}${appendOptional(required)}`;
|
|
317
|
+
}
|
|
318
|
+
const itemsSchema = jsonSchemaToZod(
|
|
319
|
+
spec,
|
|
320
|
+
items,
|
|
321
|
+
true,
|
|
322
|
+
onRef,
|
|
323
|
+
refProcessingStack
|
|
324
|
+
);
|
|
325
|
+
return `z.array(${itemsSchema})${appendOptional(required)}`;
|
|
326
|
+
}
|
|
327
|
+
function appendOptional(isRequired) {
|
|
328
|
+
return isRequired ? "" : ".optional()";
|
|
329
|
+
}
|
|
330
|
+
function appendDefault(defaultValue) {
|
|
331
|
+
return defaultValue !== void 0 ? `.default(${JSON.stringify(defaultValue)})` : "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// packages/typescript/src/lib/sdk.ts
|
|
335
|
+
import { camelcase, pascalcase, spinalcase } from "stringcase";
|
|
336
|
+
|
|
337
|
+
// packages/typescript/src/lib/client.ts
|
|
338
|
+
var client_default = (spec) => {
|
|
339
|
+
const optionsEntries = Object.entries(spec.options).map(
|
|
340
|
+
([key, value]) => [`'${key}'`, value]
|
|
341
|
+
);
|
|
342
|
+
const defaultHeaders = `{${optionsEntries.filter(([, value]) => value.in === "header").map(
|
|
343
|
+
([key, value]) => `${key}: this.options[${value.optionName ? `'${value.optionName}'` : key}]`
|
|
344
|
+
).join(",\n")}}`;
|
|
345
|
+
const defaultInputs = `{${optionsEntries.filter(([, value]) => value.in === "input").map(
|
|
346
|
+
([key, value]) => `${key}: this.options[${value.optionName ? `'${value.optionName}'` : key}]`
|
|
347
|
+
).join(",\n")}}`;
|
|
348
|
+
const specOptions = {
|
|
349
|
+
...Object.fromEntries(
|
|
350
|
+
optionsEntries.map(([key, value]) => [value.optionName ?? key, value])
|
|
351
|
+
),
|
|
352
|
+
fetch: {
|
|
353
|
+
schema: "fetchType"
|
|
354
|
+
},
|
|
355
|
+
baseUrl: {
|
|
356
|
+
schema: spec.servers.length ? `z.enum(servers).default(servers[0])` : "z.string()"
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
return `
|
|
360
|
+
import { fetchType, sendRequest } from './http/send-request.ts';
|
|
361
|
+
import z from 'zod';
|
|
362
|
+
import type { Endpoints } from './endpoints.ts';
|
|
363
|
+
import schemas from './schemas.ts';
|
|
364
|
+
${spec.servers.length ? `const servers = ${JSON.stringify(spec.servers, null, 2)} as const` : ""}
|
|
365
|
+
const optionsSchema = z.object(${toLitObject(specOptions, (x) => x.schema)});
|
|
366
|
+
|
|
367
|
+
type ${spec.name}Options = z.infer<typeof optionsSchema>;
|
|
368
|
+
|
|
369
|
+
export class ${spec.name} {
|
|
370
|
+
|
|
371
|
+
constructor(public options: ${spec.name}Options) {}
|
|
372
|
+
|
|
373
|
+
async request<E extends keyof Endpoints>(
|
|
374
|
+
endpoint: E,
|
|
375
|
+
input: Endpoints[E]['input'],
|
|
376
|
+
): Promise<readonly [Endpoints[E]['output'], Endpoints[E]['error'] | null]> {
|
|
377
|
+
const route = schemas[endpoint];
|
|
378
|
+
return sendRequest(Object.assign(this.#defaultInputs, input), route, {
|
|
379
|
+
baseUrl: this.options.baseUrl,
|
|
380
|
+
fetch: this.options.fetch,
|
|
381
|
+
headers: this.defaultHeaders,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
get defaultHeaders() {
|
|
386
|
+
return ${defaultHeaders}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
get #defaultInputs() {
|
|
390
|
+
return ${defaultInputs}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
setOptions(options: Partial<${spec.name}Options>) {
|
|
394
|
+
const validated = optionsSchema.partial().parse(options);
|
|
395
|
+
|
|
396
|
+
for (const key of Object.keys(validated) as (keyof ${spec.name}Options)[]) {
|
|
397
|
+
if (validated[key] !== undefined) {
|
|
398
|
+
(this.options[key] as typeof validated[typeof key]) = validated[key]!;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}`;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// packages/typescript/src/lib/sdk.ts
|
|
406
|
+
var SchemaEndpoint = class {
|
|
407
|
+
#imports = [
|
|
408
|
+
`import z from 'zod';`,
|
|
409
|
+
'import type { Endpoints } from "./endpoints.ts";',
|
|
410
|
+
`import { toRequest, json, urlencoded, formdata, createUrl } from './http/request.ts';`,
|
|
411
|
+
`import type { ParseError } from './http/parser.ts';`
|
|
412
|
+
];
|
|
413
|
+
#endpoints = [];
|
|
414
|
+
addEndpoint(endpoint, operation) {
|
|
415
|
+
this.#endpoints.push(` "${endpoint}": ${operation},`);
|
|
416
|
+
}
|
|
417
|
+
addImport(value) {
|
|
418
|
+
this.#imports.push(value);
|
|
419
|
+
}
|
|
420
|
+
complete() {
|
|
421
|
+
return `${this.#imports.join("\n")}
|
|
422
|
+
export default {
|
|
423
|
+
${this.#endpoints.join("\n")}
|
|
424
|
+
}`;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var Emitter = class {
|
|
428
|
+
imports = [
|
|
429
|
+
`import z from 'zod';`,
|
|
430
|
+
`import type { ParseError } from './http/parser.ts';`
|
|
431
|
+
];
|
|
432
|
+
endpoints = [];
|
|
433
|
+
addEndpoint(endpoint, operation) {
|
|
434
|
+
this.endpoints.push(` "${endpoint}": ${operation};`);
|
|
435
|
+
}
|
|
436
|
+
addImport(value) {
|
|
437
|
+
this.imports.push(value);
|
|
438
|
+
}
|
|
439
|
+
complete() {
|
|
440
|
+
return `${this.imports.join("\n")}
|
|
441
|
+
export interface Endpoints {
|
|
442
|
+
${this.endpoints.join("\n")}
|
|
443
|
+
}`;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var StreamEmitter = class extends Emitter {
|
|
447
|
+
complete() {
|
|
448
|
+
return `${this.imports.join("\n")}
|
|
449
|
+
export interface StreamEndpoints {
|
|
450
|
+
${this.endpoints.join("\n")}
|
|
451
|
+
}`;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
function generateClientSdk(spec) {
|
|
455
|
+
const emitter = new Emitter();
|
|
456
|
+
const streamEmitter = new StreamEmitter();
|
|
457
|
+
const schemas = {};
|
|
458
|
+
const schemasImports = [];
|
|
459
|
+
const schemaEndpoint = new SchemaEndpoint();
|
|
460
|
+
const errors = [];
|
|
461
|
+
for (const [name, operations] of Object.entries(spec.operations)) {
|
|
462
|
+
const featureSchemaFileName = camelcase(name);
|
|
463
|
+
schemas[featureSchemaFileName] = [`import z from 'zod';`];
|
|
464
|
+
emitter.addImport(
|
|
465
|
+
`import * as ${featureSchemaFileName} from './inputs/${featureSchemaFileName}.ts';`
|
|
466
|
+
);
|
|
467
|
+
streamEmitter.addImport(
|
|
468
|
+
`import * as ${featureSchemaFileName} from './inputs/${featureSchemaFileName}.ts';`
|
|
469
|
+
);
|
|
470
|
+
schemaEndpoint.addImport(
|
|
471
|
+
`import * as ${featureSchemaFileName} from './inputs/${featureSchemaFileName}.ts';`
|
|
472
|
+
);
|
|
473
|
+
for (const operation of operations) {
|
|
474
|
+
const schemaName = camelcase(`${operation.name} schema`);
|
|
475
|
+
const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject(operation.schemas)};`;
|
|
476
|
+
schemas[featureSchemaFileName].push(schema);
|
|
477
|
+
schemasImports.push(
|
|
478
|
+
...operation.imports.map((it) => (it.namedImports ?? []).map((it2) => it2.name)).flat()
|
|
479
|
+
);
|
|
480
|
+
const schemaRef = `${featureSchemaFileName}.${schemaName}`;
|
|
481
|
+
const output = operation.formatOutput();
|
|
482
|
+
const inputHeaders = [];
|
|
483
|
+
const inputQuery = [];
|
|
484
|
+
const inputBody = [];
|
|
485
|
+
const inputParams = [];
|
|
486
|
+
for (const [name2, prop] of Object.entries(operation.inputs)) {
|
|
487
|
+
if (prop.in === "headers" || prop.in === "header") {
|
|
488
|
+
inputHeaders.push(`"${name2}"`);
|
|
489
|
+
} else if (prop.in === "query") {
|
|
490
|
+
inputQuery.push(`"${name2}"`);
|
|
491
|
+
} else if (prop.in === "body") {
|
|
492
|
+
inputBody.push(`"${name2}"`);
|
|
493
|
+
} else if (prop.in === "path") {
|
|
494
|
+
inputParams.push(`"${name2}"`);
|
|
495
|
+
} else if (prop.in === "internal") {
|
|
496
|
+
continue;
|
|
497
|
+
} else {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`Unknown source ${prop.in} in ${name2} ${JSON.stringify(
|
|
500
|
+
prop
|
|
501
|
+
)} in ${operation.name}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (operation.type === "sse") {
|
|
506
|
+
const input = `z.infer<typeof ${schemaRef}>`;
|
|
507
|
+
const endpoint = `${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
|
|
508
|
+
streamEmitter.addImport(
|
|
509
|
+
`import type {${pascalcase(operation.name)}} from './outputs/${spinalcase(operation.name)}';`
|
|
510
|
+
);
|
|
511
|
+
streamEmitter.addEndpoint(
|
|
512
|
+
endpoint,
|
|
513
|
+
`{input: ${input}, output: ${output.use}}`
|
|
514
|
+
);
|
|
515
|
+
schemaEndpoint.addEndpoint(
|
|
516
|
+
endpoint,
|
|
517
|
+
`{
|
|
518
|
+
schema: ${schemaRef},
|
|
519
|
+
toRequest(input: StreamEndpoints['${endpoint}']['input'], init: {baseUrl:string; headers?: Partial<Record<string, string>>}) {
|
|
520
|
+
const endpoint = '${endpoint}';
|
|
521
|
+
return toRequest(endpoint, json(input, {
|
|
522
|
+
inputHeaders: [${inputHeaders}],
|
|
523
|
+
inputQuery: [${inputQuery}],
|
|
524
|
+
inputBody: [${inputBody}],
|
|
525
|
+
inputParams: [${inputParams}],
|
|
526
|
+
}), init);
|
|
527
|
+
},
|
|
528
|
+
}`
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
emitter.addImport(
|
|
532
|
+
`import type {${output.import}} from './outputs/${spinalcase(operation.name)}';`
|
|
533
|
+
);
|
|
534
|
+
errors.push(...operation.errors ?? []);
|
|
535
|
+
const addTypeParser = Object.keys(operation.schemas).length > 1;
|
|
536
|
+
for (const type in operation.schemas ?? {}) {
|
|
537
|
+
let typePrefix = "";
|
|
538
|
+
if (addTypeParser && type !== "json") {
|
|
539
|
+
typePrefix = `${type} `;
|
|
540
|
+
}
|
|
541
|
+
const input = `typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}`;
|
|
542
|
+
const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
|
|
543
|
+
emitter.addEndpoint(
|
|
544
|
+
endpoint,
|
|
545
|
+
`{input: z.infer<${input}>; output: ${output.use}; error: ${(operation.errors ?? ["ServerError"]).concat(`ParseError<${input}>`).join("|")}}`
|
|
546
|
+
);
|
|
547
|
+
schemaEndpoint.addEndpoint(
|
|
548
|
+
endpoint,
|
|
549
|
+
`{
|
|
550
|
+
schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
|
|
551
|
+
toRequest(input: Endpoints['${endpoint}']['input'], init: {baseUrl:string; headers?: Partial<Record<string, string>>}) {
|
|
552
|
+
const endpoint = '${endpoint}';
|
|
553
|
+
return toRequest(endpoint, ${operation.contentType || "json"}(input, {
|
|
554
|
+
inputHeaders: [${inputHeaders}],
|
|
555
|
+
inputQuery: [${inputQuery}],
|
|
556
|
+
inputBody: [${inputBody}],
|
|
557
|
+
inputParams: [${inputParams}],
|
|
558
|
+
}), init);
|
|
559
|
+
},
|
|
560
|
+
}`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
emitter.addImport(
|
|
567
|
+
`import type { ${removeDuplicates(errors, (it) => it).join(", ")} } from './http/response.ts';`
|
|
568
|
+
);
|
|
569
|
+
return {
|
|
570
|
+
...Object.fromEntries(
|
|
571
|
+
Object.entries(schemas).map(([key, value]) => [
|
|
572
|
+
`inputs/${key}.ts`,
|
|
573
|
+
[
|
|
574
|
+
schemasImports.length ? `import {${removeDuplicates(schemasImports, (it) => it)}} from '../zod';` : "",
|
|
575
|
+
spec.commonZod ? 'import * as commonZod from "../zod.ts";' : "",
|
|
576
|
+
...value
|
|
577
|
+
].map((it) => it.trim()).filter(Boolean).join("\n") + "\n"
|
|
578
|
+
// add a newline at the end
|
|
579
|
+
])
|
|
580
|
+
),
|
|
581
|
+
"client.ts": client_default(spec),
|
|
582
|
+
"schemas.ts": schemaEndpoint.complete(),
|
|
583
|
+
"endpoints.ts": emitter.complete()
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// packages/typescript/src/lib/utils.ts
|
|
588
|
+
function isRef(obj) {
|
|
589
|
+
return "$ref" in obj;
|
|
590
|
+
}
|
|
591
|
+
function securityToOptions(security2, securitySchemas, staticIn) {
|
|
592
|
+
securitySchemas ??= {};
|
|
593
|
+
const options = {};
|
|
594
|
+
for (const it of security2) {
|
|
595
|
+
const [name] = Object.keys(it);
|
|
596
|
+
const schema = securitySchemas[name];
|
|
597
|
+
if (isRef(schema)) {
|
|
598
|
+
throw new Error(`Ref security schemas are not supported`);
|
|
599
|
+
}
|
|
600
|
+
if (schema.type === "http") {
|
|
601
|
+
options["authorization"] = {
|
|
602
|
+
in: staticIn ?? "header",
|
|
603
|
+
schema: "z.string().optional()",
|
|
604
|
+
optionName: "token"
|
|
605
|
+
};
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (schema.type === "apiKey") {
|
|
609
|
+
if (!schema.in) {
|
|
610
|
+
throw new Error(`apiKey security schema must have an "in" field`);
|
|
611
|
+
}
|
|
612
|
+
if (!schema.name) {
|
|
613
|
+
throw new Error(`apiKey security schema must have a "name" field`);
|
|
614
|
+
}
|
|
615
|
+
options[schema.name] = {
|
|
616
|
+
in: staticIn ?? schema.in,
|
|
617
|
+
schema: "z.string().optional()"
|
|
618
|
+
};
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return options;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// packages/typescript/src/lib/generator.ts
|
|
626
|
+
var responses = {
|
|
627
|
+
"400": "BadRequest",
|
|
628
|
+
"401": "Unauthorized",
|
|
629
|
+
"402": "PaymentRequired",
|
|
630
|
+
"403": "Forbidden",
|
|
631
|
+
"404": "NotFound",
|
|
632
|
+
"405": "MethodNotAllowed",
|
|
633
|
+
"406": "NotAcceptable",
|
|
634
|
+
"409": "Conflict",
|
|
635
|
+
"413": "PayloadTooLarge",
|
|
636
|
+
"410": "Gone",
|
|
637
|
+
"422": "UnprocessableEntity",
|
|
638
|
+
"429": "TooManyRequests",
|
|
639
|
+
"500": "InternalServerError",
|
|
640
|
+
"501": "NotImplemented",
|
|
641
|
+
"502": "BadGateway",
|
|
642
|
+
"503": "ServiceUnavailable",
|
|
643
|
+
"504": "GatewayTimeout"
|
|
644
|
+
};
|
|
645
|
+
var defaults = {
|
|
646
|
+
target: "javascript",
|
|
647
|
+
style: "github",
|
|
648
|
+
operationId: (operation, path, method) => {
|
|
649
|
+
if (operation.operationId) {
|
|
650
|
+
return spinalcase2(operation.operationId);
|
|
651
|
+
}
|
|
652
|
+
return operation.operationId || camelcase2(`${method} ${path.replace(/\//g, " ")}`);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
function generateCode(config) {
|
|
656
|
+
const groups = {};
|
|
657
|
+
const commonSchemas = {};
|
|
658
|
+
const outputs = {};
|
|
659
|
+
for (const [path, methods2] of Object.entries(config.spec.paths ?? {})) {
|
|
660
|
+
for (const [method, operation] of Object.entries(methods2)) {
|
|
661
|
+
const formatOperationId = config.operationId ?? defaults.operationId;
|
|
662
|
+
const operationName = formatOperationId(operation, path, method);
|
|
663
|
+
console.log(`Processing ${method} ${path}`);
|
|
664
|
+
const groupName = (operation.tags ?? ["unknown"])[0];
|
|
665
|
+
groups[groupName] ??= [];
|
|
666
|
+
const inputs = {};
|
|
667
|
+
const imports = [];
|
|
668
|
+
const additionalProperties = [];
|
|
669
|
+
for (const param of operation.parameters ?? []) {
|
|
670
|
+
if (isRef(param)) {
|
|
671
|
+
throw new Error(`Found reference in parameter ${param.$ref}`);
|
|
672
|
+
}
|
|
673
|
+
if (!param.schema) {
|
|
674
|
+
throw new Error(`Schema not found for parameter ${param.name}`);
|
|
675
|
+
}
|
|
676
|
+
inputs[param.name] = {
|
|
677
|
+
in: param.in,
|
|
678
|
+
schema: ""
|
|
679
|
+
};
|
|
680
|
+
additionalProperties.push(param);
|
|
681
|
+
}
|
|
682
|
+
const security2 = operation.security ?? [];
|
|
683
|
+
const securitySchemas = config.spec.components?.securitySchemes ?? {};
|
|
684
|
+
const securityOptions = securityToOptions(security2, securitySchemas);
|
|
685
|
+
Object.assign(inputs, securityOptions);
|
|
686
|
+
additionalProperties.push(
|
|
687
|
+
...Object.entries(securityOptions).map(
|
|
688
|
+
([name, value]) => ({
|
|
689
|
+
name,
|
|
690
|
+
required: false,
|
|
691
|
+
schema: {
|
|
692
|
+
type: "string"
|
|
693
|
+
},
|
|
694
|
+
in: value.in
|
|
695
|
+
})
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
const types = {};
|
|
699
|
+
const shortContenTypeMap = {
|
|
700
|
+
"application/json": "json",
|
|
701
|
+
"application/x-www-form-urlencoded": "urlencoded",
|
|
702
|
+
"multipart/form-data": "formdata",
|
|
703
|
+
"application/xml": "xml",
|
|
704
|
+
"text/plain": "text"
|
|
705
|
+
};
|
|
706
|
+
let contentType;
|
|
707
|
+
if (operation.requestBody && Object.keys(operation.requestBody).length) {
|
|
708
|
+
const content = isRef(operation.requestBody) ? get2(followRef(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
|
|
709
|
+
for (const type in content) {
|
|
710
|
+
const schema = isRef(content[type].schema) ? followRef(config.spec, content[type].schema.$ref) : content[type].schema;
|
|
711
|
+
types[shortContenTypeMap[type]] = jsonSchemaToZod(
|
|
712
|
+
config.spec,
|
|
713
|
+
merge(schema, {
|
|
714
|
+
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
715
|
+
properties: additionalProperties.reduce(
|
|
716
|
+
(acc, p) => ({
|
|
717
|
+
...acc,
|
|
718
|
+
[p.name]: p.schema
|
|
719
|
+
}),
|
|
720
|
+
{}
|
|
721
|
+
)
|
|
722
|
+
}),
|
|
723
|
+
true,
|
|
724
|
+
(schemaName, zod) => {
|
|
725
|
+
commonSchemas[schemaName] = zod;
|
|
726
|
+
imports.push({
|
|
727
|
+
defaultImport: void 0,
|
|
728
|
+
isTypeOnly: false,
|
|
729
|
+
moduleSpecifier: "../zod",
|
|
730
|
+
namedImports: [{ isTypeOnly: false, name: schemaName }],
|
|
731
|
+
namespaceImport: void 0
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (content["application/json"]) {
|
|
737
|
+
contentType = "json";
|
|
738
|
+
} else if (content["application/x-www-form-urlencoded"]) {
|
|
739
|
+
contentType = "urlencoded";
|
|
740
|
+
} else if (content["multipart/form-data"]) {
|
|
741
|
+
contentType = "formdata";
|
|
742
|
+
} else {
|
|
743
|
+
contentType = "json";
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
types[shortContenTypeMap["application/json"]] = jsonSchemaToZod(
|
|
747
|
+
config.spec,
|
|
748
|
+
{
|
|
749
|
+
type: "object",
|
|
750
|
+
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
751
|
+
properties: additionalProperties.reduce(
|
|
752
|
+
(acc, p) => ({
|
|
753
|
+
...acc,
|
|
754
|
+
[p.name]: p.schema
|
|
755
|
+
}),
|
|
756
|
+
{}
|
|
757
|
+
)
|
|
758
|
+
},
|
|
759
|
+
true,
|
|
760
|
+
(schemaName, zod) => {
|
|
761
|
+
commonSchemas[schemaName] = zod;
|
|
762
|
+
imports.push({
|
|
763
|
+
defaultImport: void 0,
|
|
764
|
+
isTypeOnly: false,
|
|
765
|
+
moduleSpecifier: "./zod",
|
|
766
|
+
namedImports: [{ isTypeOnly: false, name: schemaName }],
|
|
767
|
+
namespaceImport: void 0
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const errors = [];
|
|
773
|
+
operation.responses ??= {};
|
|
774
|
+
let foundResponse = false;
|
|
775
|
+
const output = [`import z from 'zod';`];
|
|
776
|
+
for (const status in operation.responses) {
|
|
777
|
+
const response = operation.responses[status];
|
|
778
|
+
const statusCode = +status;
|
|
779
|
+
if (statusCode >= 400) {
|
|
780
|
+
errors.push(responses[status] ?? "ProblematicResponse");
|
|
781
|
+
}
|
|
782
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
783
|
+
foundResponse = true;
|
|
784
|
+
const responseContent = get2(response, ["content"]);
|
|
785
|
+
const isJson = responseContent && responseContent["application/json"];
|
|
786
|
+
const responseSchema = isJson ? jsonSchemaToZod(
|
|
787
|
+
config.spec,
|
|
788
|
+
responseContent["application/json"].schema,
|
|
789
|
+
true,
|
|
790
|
+
(schemaName, zod) => {
|
|
791
|
+
commonSchemas[schemaName] = zod;
|
|
792
|
+
imports.push({
|
|
793
|
+
defaultImport: void 0,
|
|
794
|
+
isTypeOnly: false,
|
|
795
|
+
moduleSpecifier: "../zod",
|
|
796
|
+
namedImports: [{ isTypeOnly: false, name: schemaName }],
|
|
797
|
+
namespaceImport: void 0
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
) : "z.instanceof(ReadableStream)";
|
|
801
|
+
output.push(
|
|
802
|
+
importsToString(mergeImports(Object.values(imports).flat())).join(
|
|
803
|
+
"\n"
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
output.push(
|
|
807
|
+
`export const ${pascalcase2(operationName + " output")} = ${responseSchema}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (!foundResponse) {
|
|
812
|
+
output.push(
|
|
813
|
+
`export const ${pascalcase2(operationName + " output")} = z.void()`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
outputs[`${spinalcase2(operationName)}.ts`] = output.join("\n");
|
|
817
|
+
groups[groupName].push({
|
|
818
|
+
name: operationName,
|
|
819
|
+
type: "http",
|
|
820
|
+
imports: mergeImports(Object.values(imports).flat()),
|
|
821
|
+
inputs,
|
|
822
|
+
errors: errors.length ? errors : ["ServerError"],
|
|
823
|
+
contentType,
|
|
824
|
+
schemas: types,
|
|
825
|
+
formatOutput: () => ({
|
|
826
|
+
import: pascalcase2(operationName + " output"),
|
|
827
|
+
use: `z.infer<typeof ${pascalcase2(operationName + " output")}>`
|
|
828
|
+
}),
|
|
829
|
+
trigger: {
|
|
830
|
+
path,
|
|
831
|
+
method
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { groups, commonSchemas, outputs };
|
|
837
|
+
}
|
|
838
|
+
function mergeImports(imports) {
|
|
839
|
+
const merged = {};
|
|
840
|
+
for (const i of imports) {
|
|
841
|
+
merged[i.moduleSpecifier] = merged[i.moduleSpecifier] ?? {
|
|
842
|
+
moduleSpecifier: i.moduleSpecifier,
|
|
843
|
+
defaultImport: i.defaultImport,
|
|
844
|
+
namespaceImport: i.namespaceImport,
|
|
845
|
+
namedImports: []
|
|
846
|
+
};
|
|
847
|
+
if (i.namedImports) {
|
|
848
|
+
merged[i.moduleSpecifier].namedImports.push(...i.namedImports);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return Object.values(merged);
|
|
852
|
+
}
|
|
853
|
+
function importsToString(imports) {
|
|
854
|
+
return imports.map((i) => {
|
|
855
|
+
if (i.defaultImport) {
|
|
856
|
+
return `import ${i.defaultImport} from '${i.moduleSpecifier}'`;
|
|
857
|
+
}
|
|
858
|
+
if (i.namespaceImport) {
|
|
859
|
+
return `import * as ${i.namespaceImport} from '${i.moduleSpecifier}'`;
|
|
860
|
+
}
|
|
861
|
+
if (i.namedImports) {
|
|
862
|
+
return `import {${removeDuplicates(i.namedImports, (it) => it.name).map((n) => n.name).join(", ")}} from '${i.moduleSpecifier}'`;
|
|
863
|
+
}
|
|
864
|
+
throw new Error(`Invalid import ${JSON.stringify(i)}`);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// packages/typescript/src/lib/http/client.txt
|
|
869
|
+
var client_default2 = "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 async function parseResponse(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 const isChunked = response.headers.get('Transfer-Encoding') === 'chunked';\n if (isChunked) {\n return response.body!;\n // return handleChunkedResponse(response, contentType);\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";
|
|
870
|
+
|
|
871
|
+
// packages/typescript/src/lib/http/parser.txt
|
|
872
|
+
var parser_default = "import { z } from 'zod';\n\nexport type ParseError<T extends z.ZodType<any, any, any>> = {\n kind: 'parse';\n} & z.inferFlattenedErrors<T>;\n\nexport function parse<T extends z.ZodType>(\n schema: T,\n input: unknown,\n) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const errors = result.error.flatten((issue) => issue);\n return [null, errors];\n }\n return [result.data as z.infer<T>, null];\n}\n";
|
|
873
|
+
|
|
874
|
+
// packages/typescript/src/lib/http/request.txt
|
|
875
|
+
var request_default = "type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\ntype ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart';\ntype Endpoint = `${ContentType} ${Method} ${string}` | `${Method} ${string}`;\n\nexport function createUrl(base: string, path: string, query: URLSearchParams) {\n const url = new URL(path, base);\n url.search = query.toString();\n return url;\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\ninterface ToRequest {\n <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ): Request;\n urlencoded: <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ) => Request;\n}\n\nfunction _json(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body: Record<string, any> = {};\n for (const prop of props.inputBody) {\n body[prop] = input[prop];\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body: JSON.stringify(body),\n query,\n params,\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\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 constructor(\n protected input: Input,\n protected props: Props,\n ) {}\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Partial<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: Partial<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 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 formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function _urlencoded(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body = new URLSearchParams();\n for (const prop of props.inputBody) {\n body.set(prop, input[prop]);\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body,\n query,\n params,\n headers: {},\n };\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n): Request {\n const [method, path] = endpoint.split(' ');\n\n const headers = new Headers(\n Object.entries({\n ...defaults?.headers,\n ...input.headers,\n }).filter(truthyEntry),\n );\n const pathVariable = template(path, input.params);\n\n const url = createUrl(defaults.baseUrl, pathVariable, input.query);\n return new Request(url, {\n method: method,\n headers: headers,\n body: method === 'GET' ? undefined : input.body,\n });\n}\n\nfunction truthyEntry(entry: [string, unknown]): entry is [string, string] {\n return entry[1] !== undefined;\n}\n";
|
|
876
|
+
|
|
877
|
+
// packages/typescript/src/lib/http/response.txt
|
|
878
|
+
var response_default = "export interface ApiResponse<Status extends number, Body extends unknown> {\n kind: 'response';\n status: Status;\n body: Body;\n}\n\n// 4xx Client Errors\nexport type BadRequest = ApiResponse<400, { message: string }>;\nexport type Unauthorized = ApiResponse<401, { message: string }>;\nexport type PaymentRequired = ApiResponse<402, { message: string }>;\nexport type Forbidden = ApiResponse<403, { message: string }>;\nexport type NotFound = ApiResponse<404, { message: string }>;\nexport type MethodNotAllowed = ApiResponse<405, { message: string }>;\nexport type NotAcceptable = ApiResponse<406, { message: string }>;\nexport type Conflict = ApiResponse<409, { message: string }>;\nexport type Gone = ApiResponse<410, { message: string }>;\nexport type UnprocessableEntity = ApiResponse<422, { message: string; errors?: Record<string, string[]> }>;\nexport type TooManyRequests = ApiResponse<429, { message: string; retryAfter?: string }>;\nexport type PayloadTooLarge = ApiResponse<413, { message: string; }>;\nexport type UnsupportedMediaType = ApiResponse<415, { message: string; }>;\n\n// 5xx Server Errors\nexport type InternalServerError = ApiResponse<500, { message: string }>;\nexport type NotImplemented = ApiResponse<501, { message: string }>;\nexport type BadGateway = ApiResponse<502, { message: string }>;\nexport type ServiceUnavailable = ApiResponse<503, { message: string; retryAfter?: string }>;\nexport type GatewayTimeout = ApiResponse<504, { message: string }>;\n\nexport type ClientError =\n | BadRequest\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";
|
|
879
|
+
|
|
880
|
+
// packages/typescript/src/lib/http/send-request.txt
|
|
881
|
+
var send_request_default = "import z from 'zod';\n\nimport { handleError, parseResponse } from './parse-response.ts';\nimport { parse } from './parser.ts';\n\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (\n input: any,\n init: { baseUrl: string; headers?: Partial<Record<string, string>> },\n ) => any;\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: any,\n route: RequestSchema,\n options: {\n baseUrl: string;\n fetch?: z.infer<typeof fetchType>;\n headers?: Partial<Record<string, string>>;\n },\n) {\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 const request = route.toRequest(parsedInput as never, {\n headers: options.headers,\n baseUrl: options.baseUrl,\n });\n const response = await (options.fetch ?? fetch)(request);\n if (response.ok) {\n const data = await parseResponse(response);\n return [data, null] as const;\n }\n const error = await handleError(response);\n return [null as never, { ...error, kind: 'response' }] as const;\n}\n";
|
|
882
|
+
|
|
883
|
+
// packages/typescript/src/lib/generate.ts
|
|
884
|
+
function security(spec) {
|
|
885
|
+
const security2 = spec.security || [];
|
|
886
|
+
const components = spec.components || {};
|
|
887
|
+
const securitySchemas = components.securitySchemes || {};
|
|
888
|
+
const paths = Object.values(spec.paths ?? {});
|
|
889
|
+
const options = securityToOptions(security2, securitySchemas);
|
|
890
|
+
for (const it of paths) {
|
|
891
|
+
for (const method of methods) {
|
|
892
|
+
const operation = it[method];
|
|
893
|
+
if (!operation) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
Object.assign(
|
|
897
|
+
options,
|
|
898
|
+
securityToOptions(operation.security || [], securitySchemas, "input")
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return options;
|
|
903
|
+
}
|
|
904
|
+
async function generate(spec, settings) {
|
|
905
|
+
const { commonSchemas, groups, outputs } = generateCode({
|
|
906
|
+
spec,
|
|
907
|
+
style: "github",
|
|
908
|
+
target: "javascript"
|
|
909
|
+
});
|
|
910
|
+
const output = settings.mode === "full" ? join(settings.output, "src") : settings.output;
|
|
911
|
+
const options = security(spec);
|
|
912
|
+
const clientFiles = generateClientSdk({
|
|
913
|
+
name: settings.name || "Client",
|
|
914
|
+
operations: groups,
|
|
915
|
+
servers: spec.servers?.map((server) => server.url) || [],
|
|
916
|
+
options
|
|
917
|
+
});
|
|
918
|
+
await writeFiles(output, {
|
|
919
|
+
"outputs/index.ts": "",
|
|
920
|
+
"inputs/index.ts": ""
|
|
921
|
+
// 'README.md': readme,
|
|
922
|
+
});
|
|
923
|
+
await writeFiles(join(output, "http"), {
|
|
924
|
+
"parse-response.ts": client_default2,
|
|
925
|
+
"send-request.ts": send_request_default,
|
|
926
|
+
"response.ts": response_default,
|
|
927
|
+
"parser.ts": parser_default,
|
|
928
|
+
"request.ts": request_default
|
|
929
|
+
});
|
|
930
|
+
await writeFiles(join(output, "outputs"), outputs);
|
|
931
|
+
await writeFiles(output, {
|
|
932
|
+
...clientFiles,
|
|
933
|
+
"zod.ts": `import z from 'zod';
|
|
934
|
+
${Object.entries(commonSchemas).map(([name, schema]) => `export const ${name} = ${schema};`).join("\n")}`
|
|
935
|
+
});
|
|
936
|
+
const [index, outputIndex, inputsIndex, httpIndex] = await Promise.all([
|
|
937
|
+
getFolderExports(output),
|
|
938
|
+
getFolderExports(join(output, "outputs")),
|
|
939
|
+
getFolderExports(join(output, "inputs")),
|
|
940
|
+
getFolderExports(join(output, "http"))
|
|
941
|
+
]);
|
|
942
|
+
await writeFiles(output, {
|
|
943
|
+
"index.ts": index,
|
|
944
|
+
"outputs/index.ts": outputIndex,
|
|
945
|
+
"inputs/index.ts": inputsIndex,
|
|
946
|
+
"http/index.ts": httpIndex
|
|
947
|
+
});
|
|
948
|
+
if (settings.mode === "full") {
|
|
949
|
+
await writeFiles(settings.output, {
|
|
950
|
+
"package.json": {
|
|
951
|
+
ignoreIfExists: true,
|
|
952
|
+
content: `{"type":"module","main":"./src/index.ts","dependencies":{"fast-content-type-parse":"^2.0.1"}}`
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
await settings.formatCode?.({
|
|
957
|
+
output,
|
|
958
|
+
env: npmRunPathEnv()
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// packages/typescript/src/lib/watcher.ts
|
|
963
|
+
import { watch as nodeWatch } from "node:fs/promises";
|
|
964
|
+
import { debounceTime, from } from "rxjs";
|
|
965
|
+
function watch(path) {
|
|
966
|
+
return from(
|
|
967
|
+
nodeWatch(path, {
|
|
968
|
+
persistent: true,
|
|
969
|
+
recursive: true
|
|
970
|
+
})
|
|
971
|
+
).pipe(debounceTime(400));
|
|
972
|
+
}
|
|
973
|
+
export {
|
|
974
|
+
generate,
|
|
975
|
+
watch
|
|
976
|
+
};
|
|
977
|
+
//# sourceMappingURL=index.js.map
|