@sdkgen/node-runtime 0.0.0-dev.20231002135133 → 0.0.0-dev.20231002144112

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/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { BaseApiConfig } from "./api-config";
2
+ export { Context, ContextReply, ContextRequest } from "./context";
3
+ export { decode, encode, DecodedType, EncodedType } from "./encode-decode";
4
+ export { Fatal, SdkgenError, SdkgenErrorWithData } from "./error";
5
+ export { SdkgenHttpClient } from "./http-client";
6
+ export { SdkgenHttpServer } from "./http-server";
7
+ export { apiTestWrapper } from "./test-wrapper";
package/src/swagger.ts ADDED
@@ -0,0 +1,498 @@
1
+ import type { ErrorNode, Type } from "@sdkgen/parser";
2
+ import {
3
+ StatusCodeAnnotation,
4
+ ThrowsAnnotation,
5
+ DecimalPrimitiveType,
6
+ JsonPrimitiveType,
7
+ ArrayType,
8
+ Base64PrimitiveType,
9
+ BigIntPrimitiveType,
10
+ BoolPrimitiveType,
11
+ BytesPrimitiveType,
12
+ CnpjPrimitiveType,
13
+ CpfPrimitiveType,
14
+ DatePrimitiveType,
15
+ DateTimePrimitiveType,
16
+ DescriptionAnnotation,
17
+ EmailPrimitiveType,
18
+ EnumType,
19
+ FloatPrimitiveType,
20
+ HexPrimitiveType,
21
+ HtmlPrimitiveType,
22
+ IntPrimitiveType,
23
+ MoneyPrimitiveType,
24
+ OptionalType,
25
+ RestAnnotation,
26
+ StringPrimitiveType,
27
+ StructType,
28
+ TypeReference,
29
+ UIntPrimitiveType,
30
+ UrlPrimitiveType,
31
+ UuidPrimitiveType,
32
+ VoidPrimitiveType,
33
+ } from "@sdkgen/parser";
34
+ import type { JSONSchema } from "json-schema-typed";
35
+ import staticFilesHandler from "serve-handler";
36
+ import { getAbsoluteFSPath as getSwaggerUiAssetPath } from "swagger-ui-dist";
37
+
38
+ import type { BaseApiConfig } from "./api-config";
39
+ import type { SdkgenHttpServer } from "./http-server";
40
+
41
+ const swaggerUiAssetPath = getSwaggerUiAssetPath();
42
+
43
+ function objectFromEntries<T>(entries: Array<[string, T]>) {
44
+ return Object.assign({}, ...Array.from(entries, ([k, v]) => ({ [k]: v }))) as Record<string, T>;
45
+ }
46
+
47
+ function typeToSchema(definitions: Record<string, JSONSchema | undefined>, type: Type): JSONSchema & object {
48
+ if (type instanceof EnumType) {
49
+ return {
50
+ enum: type.values.map(x => x.value),
51
+ type: "string",
52
+ };
53
+ } else if (type instanceof StructType) {
54
+ return {
55
+ properties: objectFromEntries(
56
+ type.fields.map(field => [
57
+ field.name,
58
+ {
59
+ description:
60
+ field.annotations
61
+ .filter(x => x instanceof DescriptionAnnotation)
62
+ .map(x => (x as DescriptionAnnotation).text)
63
+ .join(" ") || undefined,
64
+ ...typeToSchema(definitions, field.type),
65
+ },
66
+ ]),
67
+ ),
68
+ required: type.fields.filter(f => !(f.type instanceof OptionalType)).map(f => f.name),
69
+ type: "object",
70
+ additionalProperties: false,
71
+ };
72
+ } else if (
73
+ type instanceof StringPrimitiveType ||
74
+ type instanceof UuidPrimitiveType ||
75
+ type instanceof HexPrimitiveType ||
76
+ type instanceof HtmlPrimitiveType ||
77
+ type instanceof Base64PrimitiveType
78
+ ) {
79
+ return {
80
+ type: "string",
81
+ };
82
+ } else if (type instanceof UrlPrimitiveType) {
83
+ return {
84
+ format: "uri",
85
+ type: "string",
86
+ };
87
+ } else if (type instanceof DatePrimitiveType) {
88
+ return {
89
+ format: "date",
90
+ type: "string",
91
+ };
92
+ } else if (type instanceof DateTimePrimitiveType) {
93
+ return {
94
+ format: "date-time",
95
+ type: "string",
96
+ };
97
+ } else if (type instanceof CpfPrimitiveType) {
98
+ return {
99
+ type: "string",
100
+ };
101
+ } else if (type instanceof CnpjPrimitiveType) {
102
+ return {
103
+ type: "string",
104
+ };
105
+ } else if (type instanceof BoolPrimitiveType) {
106
+ return {
107
+ type: "boolean",
108
+ };
109
+ } else if (type instanceof BytesPrimitiveType) {
110
+ return {
111
+ format: "byte" as never,
112
+ type: "string",
113
+ };
114
+ } else if (type instanceof IntPrimitiveType) {
115
+ return {
116
+ format: "int32" as never,
117
+ type: "integer",
118
+ };
119
+ } else if (type instanceof UIntPrimitiveType) {
120
+ return {
121
+ format: "int32" as never,
122
+ minimum: 0,
123
+ type: "integer",
124
+ };
125
+ } else if (type instanceof MoneyPrimitiveType) {
126
+ return {
127
+ format: "int64" as never,
128
+ type: "integer",
129
+ };
130
+ } else if (type instanceof FloatPrimitiveType) {
131
+ return {
132
+ type: "number",
133
+ };
134
+ } else if (type instanceof EmailPrimitiveType) {
135
+ return {
136
+ type: "string",
137
+ };
138
+ } else if (type instanceof BigIntPrimitiveType) {
139
+ return {
140
+ type: "string",
141
+ };
142
+ } else if (type instanceof DecimalPrimitiveType) {
143
+ return {
144
+ type: "string",
145
+ };
146
+ } else if (type instanceof JsonPrimitiveType) {
147
+ return {};
148
+ } else if (type instanceof OptionalType) {
149
+ return {
150
+ oneOf: [typeToSchema(definitions, type.base), { type: "null" }],
151
+ };
152
+ } else if (type instanceof ArrayType) {
153
+ return {
154
+ items: typeToSchema(definitions, type.base),
155
+ type: "array",
156
+ };
157
+ } else if (type instanceof TypeReference) {
158
+ if (!definitions[type.name]) {
159
+ definitions[type.name] = typeToSchema(definitions, type.type);
160
+ }
161
+
162
+ return { $ref: `#/components/schemas/${type.name}` };
163
+ }
164
+
165
+ throw new Error(`Unhandled type ${type.constructor.name}`);
166
+ }
167
+
168
+ function getSwaggerJson<ExtraContextT>(apiConfig: BaseApiConfig<ExtraContextT>) {
169
+ const schemas: Record<string, JSONSchema | undefined> = {};
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ const paths: Record<string, any> = {};
172
+
173
+ for (const op of apiConfig.ast.operations) {
174
+ const throwAnnotations = op.annotations.filter(ann => ann instanceof ThrowsAnnotation) as ThrowsAnnotation[];
175
+ let possibleErrors = throwAnnotations.map(ann => apiConfig.ast.errors.find(err => err.name === ann.error)).filter(x => x) as ErrorNode[];
176
+
177
+ if (possibleErrors.length === 0) {
178
+ possibleErrors = apiConfig.ast.errors;
179
+ }
180
+
181
+ const errorsByStatus = new Map<number, ErrorNode[]>();
182
+
183
+ for (const error of possibleErrors) {
184
+ const statusAnnotation = error.annotations.find(ann => ann instanceof StatusCodeAnnotation) as StatusCodeAnnotation | undefined;
185
+ const statusCode = statusAnnotation ? statusAnnotation.statusCode : error.name === "Fatal" ? 500 : 400;
186
+
187
+ const errorList = errorsByStatus.get(statusCode) ?? [];
188
+
189
+ errorList.push(error);
190
+ errorsByStatus.set(statusCode, errorList);
191
+ }
192
+
193
+ const errorResponses = Object.fromEntries(
194
+ [...errorsByStatus.entries()].map(([status, errors]) => [
195
+ status,
196
+ {
197
+ description: errors
198
+ .map(error => error.name)
199
+ .sort((a, b) => a.localeCompare(b))
200
+ .join("<br>"),
201
+ content: {
202
+ "application/json": {
203
+ schema: {
204
+ anyOf: errors.map(error => ({
205
+ properties: {
206
+ message: {
207
+ type: "string",
208
+ },
209
+ type: {
210
+ enum: [error.name],
211
+ type: "string",
212
+ },
213
+ ...(error.dataType instanceof VoidPrimitiveType
214
+ ? {}
215
+ : {
216
+ data: typeToSchema(schemas, error.dataType),
217
+ }),
218
+ },
219
+ required: ["type", "message", ...(error.dataType instanceof VoidPrimitiveType ? [] : ["data"])],
220
+ type: "object",
221
+ additionalProperties: false,
222
+ })),
223
+ },
224
+ },
225
+ },
226
+ },
227
+ ]),
228
+ );
229
+
230
+ for (const ann of op.annotations) {
231
+ if (ann instanceof RestAnnotation) {
232
+ if (!paths[ann.path]) {
233
+ paths[ann.path] = {};
234
+ }
235
+
236
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
237
+ paths[ann.path][ann.method.toLowerCase()] = {
238
+ operationId: op.name,
239
+ parameters: [
240
+ ...ann.pathVariables.map(name => ({
241
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
242
+ arg: op.args.find(arg => arg.name === name)!,
243
+ location: "path",
244
+ name,
245
+ })),
246
+ ...ann.queryVariables.map(name => ({
247
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
248
+ arg: op.args.find(arg => arg.name === name)!,
249
+ location: "query",
250
+ name,
251
+ })),
252
+ ...[...ann.headers.entries()].map(([header, name]) => ({
253
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
254
+ arg: op.args.find(arg => arg.name === name)!,
255
+ location: "header",
256
+ name: header,
257
+ })),
258
+ ].map(({ name, location, arg }) => ({
259
+ description:
260
+ arg.annotations
261
+ .filter(x => x instanceof DescriptionAnnotation)
262
+ .map(x => (x as DescriptionAnnotation).text)
263
+ .join(" ") || undefined,
264
+ in: location,
265
+ name,
266
+ required: !(arg.type instanceof OptionalType),
267
+ schema: typeToSchema(schemas, arg.type),
268
+ })),
269
+ requestBody: ann.bodyVariable
270
+ ? {
271
+ content: {
272
+ ...(() => {
273
+ const bodyType = op.args.find(arg => arg.name === ann.bodyVariable)?.type;
274
+
275
+ return bodyType instanceof BoolPrimitiveType ||
276
+ bodyType instanceof IntPrimitiveType ||
277
+ bodyType instanceof UIntPrimitiveType ||
278
+ bodyType instanceof FloatPrimitiveType ||
279
+ bodyType instanceof StringPrimitiveType ||
280
+ bodyType instanceof DatePrimitiveType ||
281
+ bodyType instanceof DateTimePrimitiveType ||
282
+ bodyType instanceof MoneyPrimitiveType ||
283
+ bodyType instanceof CpfPrimitiveType ||
284
+ bodyType instanceof CnpjPrimitiveType ||
285
+ bodyType instanceof EmailPrimitiveType ||
286
+ bodyType instanceof HtmlPrimitiveType ||
287
+ bodyType instanceof UuidPrimitiveType ||
288
+ bodyType instanceof HexPrimitiveType ||
289
+ bodyType instanceof BytesPrimitiveType ||
290
+ bodyType instanceof Base64PrimitiveType
291
+ ? {
292
+ [bodyType instanceof HtmlPrimitiveType ? "text/html" : "text/plain"]: {
293
+ schema: typeToSchema(schemas, bodyType),
294
+ },
295
+ }
296
+ : {};
297
+ })(),
298
+ "application/json": {
299
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
300
+ schema: typeToSchema(schemas, op.args.find(arg => arg.name === ann.bodyVariable)!.type),
301
+ },
302
+ },
303
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304
+ required: !(op.args.find(arg => arg.name === ann.bodyVariable)!.type instanceof OptionalType),
305
+ }
306
+ : undefined,
307
+ responses: {
308
+ ...(op.returnType instanceof OptionalType || op.returnType instanceof VoidPrimitiveType
309
+ ? { [ann.method === "GET" ? "404" : "204"]: {} }
310
+ : {}),
311
+ ...(op.returnType instanceof VoidPrimitiveType
312
+ ? {}
313
+ : {
314
+ 200: {
315
+ description: "",
316
+ content: {
317
+ ...(() => {
318
+ return op.returnType instanceof BoolPrimitiveType ||
319
+ op.returnType instanceof IntPrimitiveType ||
320
+ op.returnType instanceof UIntPrimitiveType ||
321
+ op.returnType instanceof FloatPrimitiveType ||
322
+ op.returnType instanceof StringPrimitiveType ||
323
+ op.returnType instanceof DatePrimitiveType ||
324
+ op.returnType instanceof DateTimePrimitiveType ||
325
+ op.returnType instanceof MoneyPrimitiveType ||
326
+ op.returnType instanceof CpfPrimitiveType ||
327
+ op.returnType instanceof CnpjPrimitiveType ||
328
+ op.returnType instanceof EmailPrimitiveType ||
329
+ op.returnType instanceof UuidPrimitiveType ||
330
+ op.returnType instanceof HexPrimitiveType ||
331
+ op.returnType instanceof BytesPrimitiveType ||
332
+ op.returnType instanceof Base64PrimitiveType
333
+ ? {
334
+ "text/plain": {
335
+ schema: typeToSchema(schemas, op.returnType),
336
+ },
337
+ }
338
+ : {};
339
+ })(),
340
+ "application/json": {
341
+ schema: typeToSchema(schemas, op.returnType),
342
+ },
343
+ },
344
+ },
345
+ }),
346
+ ...errorResponses,
347
+ },
348
+ summary:
349
+ op.annotations
350
+ .filter(x => x instanceof DescriptionAnnotation)
351
+ .map(x => (x as DescriptionAnnotation).text)
352
+ .join(" ") || undefined,
353
+ tags: [ann.path.split("/")[1]],
354
+ };
355
+ }
356
+ }
357
+ }
358
+
359
+ const securitySchemes = {
360
+ bearerAuth: {
361
+ type: "http",
362
+ scheme: "bearer",
363
+ },
364
+ };
365
+
366
+ const security = [
367
+ {
368
+ bearerAuth: [],
369
+ },
370
+ ];
371
+
372
+ return {
373
+ openapi: "3.0.0",
374
+ info: {
375
+ title: "",
376
+ version: "",
377
+ },
378
+ paths,
379
+ components: {
380
+ schemas,
381
+ securitySchemes,
382
+ },
383
+ security,
384
+ };
385
+ }
386
+
387
+ export function setupSwagger<ExtraContextT>(server: SdkgenHttpServer<ExtraContextT>): void {
388
+ server.addHttpHandler("GET", "/swagger", (req, res) => {
389
+ if (!server.introspection) {
390
+ res.statusCode = 404;
391
+ res.end();
392
+ return;
393
+ }
394
+
395
+ res.setHeader("content-type", "text/html");
396
+ res.write(`
397
+ <!DOCTYPE html>
398
+ <html lang="en">
399
+ <head>
400
+ <meta charset="UTF-8">
401
+ <title>Swagger UI</title>
402
+ <link rel="stylesheet" type="text/css" href="/swagger/swagger-ui.css" >
403
+ <link rel="icon" type="image/png" href="/swagger/favicon-32x32.png" sizes="32x32" />
404
+ <link rel="icon" type="image/png" href="/swagger/favicon-16x16.png" sizes="16x16" />
405
+ <style>
406
+ html {
407
+ box-sizing: border-box;
408
+ overflow: -moz-scrollbars-vertical;
409
+ overflow-y: scroll;
410
+ }
411
+
412
+ *, *:before, *:after {
413
+ box-sizing: inherit;
414
+ }
415
+
416
+ body {
417
+ margin: 0;
418
+ background: #fafafa;
419
+ }
420
+
421
+ .topbar {
422
+ display: none !important;
423
+ }
424
+ </style>
425
+ </head>
426
+
427
+ <body>
428
+ <div id="swagger-ui"></div>
429
+ <script src="swagger/swagger-ui-bundle.js"> </script>
430
+ <script src="swagger/swagger-ui-standalone-preset.js"> </script>
431
+ <script>
432
+ window.onload = function() {
433
+ window.ui = SwaggerUIBundle({
434
+ spec: {
435
+ ...${JSON.stringify(getSwaggerJson(server.apiConfig))},
436
+ servers: [{ url: location.origin + location.pathname.replace(/\\/swagger$/, "") }]
437
+ },
438
+ dom_id: '#swagger-ui',
439
+ deepLinking: true,
440
+ presets: [
441
+ SwaggerUIBundle.presets.apis,
442
+ SwaggerUIStandalonePreset,
443
+ ],
444
+ plugins: [
445
+ SwaggerUIBundle.plugins.DownloadUrl,
446
+ ],
447
+ layout: "StandaloneLayout"
448
+ });
449
+ }
450
+ </script>
451
+ </body>
452
+ </html>
453
+ `);
454
+ res.end();
455
+ });
456
+
457
+ server.addHttpHandler("GET", /^\/swagger.*/u, (req, res) => {
458
+ if (!server.introspection) {
459
+ res.statusCode = 404;
460
+ res.end();
461
+ return;
462
+ }
463
+
464
+ if (req.url) {
465
+ req.url = req.url.replace(/\/swagger/u, "");
466
+ }
467
+
468
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
469
+ staticFilesHandler(req, res, {
470
+ cleanUrls: false,
471
+ directoryListing: false,
472
+ etag: true,
473
+ public: swaggerUiAssetPath,
474
+ }).catch(e => {
475
+ console.error(e);
476
+ res.statusCode = 500;
477
+ res.write(`${e}`);
478
+ res.end();
479
+ });
480
+ });
481
+
482
+ server.addHttpHandler("GET", "/swagger.json", (req, res) => {
483
+ if (!server.introspection) {
484
+ res.statusCode = 404;
485
+ res.end();
486
+ return;
487
+ }
488
+
489
+ try {
490
+ res.write(JSON.stringify(getSwaggerJson(server.apiConfig)));
491
+ } catch (error) {
492
+ console.error(error);
493
+ res.statusCode = 500;
494
+ }
495
+
496
+ res.end();
497
+ });
498
+ }
@@ -0,0 +1,66 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
7
+ import { randomBytes } from "crypto";
8
+
9
+ import type { BaseApiConfig } from "./api-config";
10
+ import type { Context } from "./context";
11
+ import { decode, encode } from "./encode-decode";
12
+ import { executeRequest } from "./execute";
13
+
14
+ export function apiTestWrapper<ExtraContextT, ApiT extends BaseApiConfig<ExtraContextT>>(api: ApiT, extraContext: Partial<ExtraContextT> = {}): ApiT {
15
+ const wrappedApi: ApiT = new (api.constructor as any)();
16
+
17
+ for (const functionName of Object.keys(api.astJson.functionTable)) {
18
+ wrappedApi.fn[functionName] = async (partialCtx: Partial<Context>, args: any) => {
19
+ const encodedArgs = encode(api.astJson.typeTable, `fn.${functionName}.args`, (api.astJson.functionTable as any)[functionName].args, args);
20
+
21
+ const ctx: Context = {
22
+ ...extraContext,
23
+ ...partialCtx,
24
+ request: {
25
+ args: encodedArgs as Record<string, unknown>,
26
+ deviceInfo: partialCtx.request?.deviceInfo ?? {
27
+ fingerprint: null,
28
+ id: randomBytes(16).toString("hex"),
29
+ language: null,
30
+ platform: null,
31
+ timezone: null,
32
+ type: "test",
33
+ version: null,
34
+ },
35
+ extra: partialCtx.request?.extra ?? {},
36
+ files: partialCtx.request?.files ?? [],
37
+ headers: partialCtx.request?.headers ?? {},
38
+ id: partialCtx.request?.id ?? randomBytes(16).toString("hex"),
39
+ ip: partialCtx.request?.ip ?? "0.0.0.0",
40
+ name: functionName,
41
+ version: 3,
42
+ },
43
+ response: {
44
+ headers: new Map(),
45
+ },
46
+ };
47
+
48
+ const reply = await executeRequest(ctx as Context & ExtraContextT, api);
49
+
50
+ if (reply.error) {
51
+ throw reply.error;
52
+ } else {
53
+ const decodedRet = decode(
54
+ api.astJson.typeTable,
55
+ `fn.${functionName}.ret`,
56
+ (api.astJson.functionTable as any)[functionName].ret,
57
+ JSON.parse(JSON.stringify(reply.result)),
58
+ );
59
+
60
+ return decodedRet;
61
+ }
62
+ };
63
+ }
64
+
65
+ return wrappedApi;
66
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type DeepReadonly<T> = T extends undefined | null | boolean | string | number | Function
2
+ ? T
3
+ : T extends []
4
+ ? readonly []
5
+ : T extends [infer U, ...infer Rest]
6
+ ? readonly [DeepReadonly<U>, ...DeepReadonly<Rest>]
7
+ : T extends Array<infer U>
8
+ ? ReadonlyArray<DeepReadonly<U>>
9
+ : T extends Map<infer K, infer V>
10
+ ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
11
+ : T extends Set<infer U>
12
+ ? ReadonlySet<DeepReadonly<U>>
13
+ : { readonly [K in keyof T]: DeepReadonly<T[K]> };
14
+
15
+ export function has<P extends PropertyKey>(target: object, property: P): target is { [K in P]: unknown } {
16
+ return property in target;
17
+ }