@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/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/api-config.ts +26 -0
- package/src/context.ts +41 -0
- package/src/encode-decode.ts +471 -0
- package/src/error.ts +30 -0
- package/src/execute.ts +67 -0
- package/src/http-client.ts +131 -0
- package/src/http-server.ts +1137 -0
- package/src/index.ts +7 -0
- package/src/swagger.ts +498 -0
- package/src/test-wrapper.ts +66 -0
- package/src/utils.ts +17 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import * as CNPJ from "@fnando/cnpj";
|
|
2
|
+
import * as CPF from "@fnando/cpf";
|
|
3
|
+
import type { AstJson, TypeDescription } from "@sdkgen/parser";
|
|
4
|
+
import { Decimal } from "decimal.js";
|
|
5
|
+
|
|
6
|
+
import type { DeepReadonly } from "./utils";
|
|
7
|
+
|
|
8
|
+
type TypeTable = AstJson["typeTable"];
|
|
9
|
+
|
|
10
|
+
const simpleStringTypes = ["string", "email", "html", "xml"];
|
|
11
|
+
const simpleTypes = ["json", "bool", "url", "int", "uint", "float", "money", "hex", "uuid", "base64", "void", ...simpleStringTypes];
|
|
12
|
+
|
|
13
|
+
type ExpandRecursively<T> = T extends object ? (T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never) : T;
|
|
14
|
+
|
|
15
|
+
type JsonType = number | string | boolean | null | JsonType[] | { [Key in string]: JsonType };
|
|
16
|
+
|
|
17
|
+
type AnyDecodedType = number | string | boolean | null | bigint | Decimal | Buffer | Date | AnyDecodedType[] | { [Key in string]: AnyDecodedType };
|
|
18
|
+
|
|
19
|
+
export type DecodedType<Type, Table extends object = {}> = TypeDescription extends Type
|
|
20
|
+
? AnyDecodedType
|
|
21
|
+
: Type extends "string" | "email" | "html" | "xml" | "url" | "hex" | "uuid" | "base64" | "cpf" | "cnpj"
|
|
22
|
+
? string
|
|
23
|
+
: Type extends "json"
|
|
24
|
+
? JsonType
|
|
25
|
+
: Type extends "bool"
|
|
26
|
+
? boolean
|
|
27
|
+
: Type extends "void"
|
|
28
|
+
? null
|
|
29
|
+
: Type extends "int" | "uint" | "float" | "money"
|
|
30
|
+
? number
|
|
31
|
+
: Type extends "bigint"
|
|
32
|
+
? bigint
|
|
33
|
+
: Type extends "bytes"
|
|
34
|
+
? Buffer
|
|
35
|
+
: Type extends "decimal"
|
|
36
|
+
? Decimal
|
|
37
|
+
: Type extends "date" | "datetime"
|
|
38
|
+
? Date
|
|
39
|
+
: Type extends `${infer X}?`
|
|
40
|
+
? DecodedType<X, Table> | null
|
|
41
|
+
: Type extends `${infer X}[]`
|
|
42
|
+
? Array<DecodedType<X, Table>>
|
|
43
|
+
: Type extends Array<string | [string, string]>
|
|
44
|
+
? DecodedEnumType<Type, Table>
|
|
45
|
+
: Type extends ReadonlyArray<string | readonly [string, string]>
|
|
46
|
+
? DecodedEnumType<Type, Table>
|
|
47
|
+
: Type extends object
|
|
48
|
+
? { -readonly [Key in keyof Type]: DecodedType<Type[Key], Table> }
|
|
49
|
+
: object extends Table
|
|
50
|
+
? never
|
|
51
|
+
: Type extends keyof Table
|
|
52
|
+
? DecodedType<Table[Type], Table>
|
|
53
|
+
: never;
|
|
54
|
+
|
|
55
|
+
type DecodedEnumType<
|
|
56
|
+
Type extends Array<string | [string, string]> | ReadonlyArray<string | readonly [string, string]>,
|
|
57
|
+
Table extends object,
|
|
58
|
+
> = Type[number] extends string ? Type[number] : DecodeTaggedEnumValueType<Type[number], Table>;
|
|
59
|
+
|
|
60
|
+
type DecodeTaggedEnumValueType<
|
|
61
|
+
ValueType extends string | [string, string] | readonly [string, string],
|
|
62
|
+
Table extends object,
|
|
63
|
+
> = ValueType extends string
|
|
64
|
+
? { tag: ValueType }
|
|
65
|
+
: ValueType extends [infer Tag, infer Struct]
|
|
66
|
+
? ExpandRecursively<{ tag: Tag } & DecodedType<Struct, Table>>
|
|
67
|
+
: ValueType extends readonly [infer Tag, infer Struct]
|
|
68
|
+
? ExpandRecursively<{ tag: Tag } & DecodedType<Struct, Table>>
|
|
69
|
+
: never;
|
|
70
|
+
|
|
71
|
+
export type EncodedType<Type, Table extends object = {}> = TypeDescription extends Type
|
|
72
|
+
? JsonType
|
|
73
|
+
: Type extends "string" | "email" | "html" | "xml" | "url" | "hex" | "uuid" | "base64" | "cpf" | "cnpj"
|
|
74
|
+
? string
|
|
75
|
+
: Type extends "json"
|
|
76
|
+
? JsonType
|
|
77
|
+
: Type extends "bool"
|
|
78
|
+
? boolean
|
|
79
|
+
: Type extends "void"
|
|
80
|
+
? null
|
|
81
|
+
: Type extends "int" | "uint" | "float" | "money"
|
|
82
|
+
? number
|
|
83
|
+
: Type extends "bigint" | "bytes" | "date" | "datetime" | "decimal"
|
|
84
|
+
? string
|
|
85
|
+
: Type extends `${infer X}?`
|
|
86
|
+
? EncodedType<X, Table> | null
|
|
87
|
+
: Type extends `${infer X}[]`
|
|
88
|
+
? Array<EncodedType<X, Table>>
|
|
89
|
+
: Type extends Array<string | [string, string]>
|
|
90
|
+
? EnumEncodedValueType<Type[number], Table>
|
|
91
|
+
: Type extends ReadonlyArray<string | readonly [string, string]>
|
|
92
|
+
? EnumEncodedValueType<Type[number], Table>
|
|
93
|
+
: Type extends object
|
|
94
|
+
? { -readonly [Key in keyof Type]: EncodedType<Type[Key], Table> }
|
|
95
|
+
: object extends Table
|
|
96
|
+
? never
|
|
97
|
+
: Type extends keyof Table
|
|
98
|
+
? EncodedType<Table[Type], Table>
|
|
99
|
+
: never;
|
|
100
|
+
|
|
101
|
+
type EnumEncodedValueType<ValueType extends string | [string, string] | readonly [string, string], Table extends object> = ValueType extends string
|
|
102
|
+
? ValueType
|
|
103
|
+
: ValueType extends [infer Tag, infer Struct]
|
|
104
|
+
? [Tag, EncodedType<Struct, Table>]
|
|
105
|
+
: ValueType extends readonly [infer Tag, infer Struct]
|
|
106
|
+
? [Tag, EncodedType<Struct, Table>]
|
|
107
|
+
: never;
|
|
108
|
+
|
|
109
|
+
class ParseError extends Error {
|
|
110
|
+
constructor(path: string, type: DeepReadonly<TypeDescription>, value: unknown) {
|
|
111
|
+
let str: string;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
str = JSON.stringify(value);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
str = String(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
super(`Invalid type at '${path}', expected ${String(type)}, got ${str}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function simpleEncodeDecode(path: string, type: string, value: unknown) {
|
|
124
|
+
if (type === "json") {
|
|
125
|
+
if (value === null || value === undefined) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return JSON.parse(JSON.stringify(value)) as unknown;
|
|
130
|
+
} else if (type === "bool") {
|
|
131
|
+
if (typeof value !== "boolean") {
|
|
132
|
+
throw new ParseError(path, type, value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return value;
|
|
136
|
+
} else if (simpleStringTypes.includes(type)) {
|
|
137
|
+
if (typeof value !== "string") {
|
|
138
|
+
throw new ParseError(path, type, value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return value;
|
|
142
|
+
} else if (type === "hex") {
|
|
143
|
+
if (typeof value !== "string" || !/^(?:[A-Fa-f0-9]{2})*$/u.test(value)) {
|
|
144
|
+
throw new ParseError(path, type, value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return value.toLowerCase();
|
|
148
|
+
} else if (type === "uuid") {
|
|
149
|
+
if (typeof value !== "string" || !/^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/u.test(value)) {
|
|
150
|
+
throw new ParseError(path, type, value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return value.toLowerCase();
|
|
154
|
+
} else if (type === "base64") {
|
|
155
|
+
if (typeof value !== "string" || Buffer.from(value, "base64").toString("base64") !== value) {
|
|
156
|
+
throw new ParseError(path, type, value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return value;
|
|
160
|
+
} else if (type === "int") {
|
|
161
|
+
if (typeof value !== "number" || (value | 0) !== value) {
|
|
162
|
+
throw new ParseError(path, type, value);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return value;
|
|
166
|
+
} else if (type === "uint") {
|
|
167
|
+
if (typeof value !== "number" || (value | 0) !== value || value < 0) {
|
|
168
|
+
throw new ParseError(path, type, value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return value;
|
|
172
|
+
} else if (type === "float") {
|
|
173
|
+
if (typeof value !== "number") {
|
|
174
|
+
throw new ParseError(path, type, value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return value;
|
|
178
|
+
} else if (type === "money") {
|
|
179
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
180
|
+
throw new ParseError(path, type, value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return value;
|
|
184
|
+
} else if (type === "url") {
|
|
185
|
+
let url: URL | undefined;
|
|
186
|
+
|
|
187
|
+
if (typeof value === "string") {
|
|
188
|
+
try {
|
|
189
|
+
url = new URL(value);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// ignore
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!url) {
|
|
196
|
+
throw new ParseError(path, type, value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return url.toString();
|
|
200
|
+
} else if (type === "void") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`Unknown type '${type}' at '${path}'`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function encode<Table extends DeepReadonly<TypeTable>, Type extends DeepReadonly<TypeDescription>>(
|
|
208
|
+
typeTable: Table,
|
|
209
|
+
path: string,
|
|
210
|
+
type: Type,
|
|
211
|
+
value: unknown,
|
|
212
|
+
): EncodedType<Type, Table> {
|
|
213
|
+
if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) {
|
|
214
|
+
throw new Error(`Invalid type at '${path}', cannot be null`);
|
|
215
|
+
} else if (Array.isArray(type)) {
|
|
216
|
+
if (type.every(tag => typeof tag === "string")) {
|
|
217
|
+
for (const tag of type) {
|
|
218
|
+
if (tag === value) {
|
|
219
|
+
return tag as EncodedType<Type, Table>;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else if (typeof value === "object" && value && "tag" in value) {
|
|
223
|
+
const { tag: tagValue, ...restValue } = value as object & { tag: unknown };
|
|
224
|
+
|
|
225
|
+
for (const entry of type) {
|
|
226
|
+
if (typeof entry === "string") {
|
|
227
|
+
if (entry === tagValue) {
|
|
228
|
+
return entry as EncodedType<Type, Table>;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
const [tag, valueType] = entry as [string, string];
|
|
232
|
+
|
|
233
|
+
if (tag === tagValue) {
|
|
234
|
+
const encodedValues = encode(typeTable, `${path}.${tag}`, valueType, restValue) as object;
|
|
235
|
+
|
|
236
|
+
// eslint-disable-next-line max-depth
|
|
237
|
+
if (Object.values(encodedValues).every(v => v === null)) {
|
|
238
|
+
return tag as EncodedType<Type, Table>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return [tag, encodedValues] as EncodedType<Type, Table>;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw new ParseError(path, type, value);
|
|
248
|
+
} else if (typeof type === "object") {
|
|
249
|
+
if (typeof value !== "object") {
|
|
250
|
+
throw new ParseError(path, type, value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const obj: Record<string, unknown> = {};
|
|
254
|
+
|
|
255
|
+
for (const key of Object.keys(type)) {
|
|
256
|
+
obj[key] = encode(typeTable, `${path}.${key}`, (type as Record<string, TypeDescription>)[key], (value as Record<string, unknown>)[key]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return obj as EncodedType<Type, Table>;
|
|
260
|
+
} else if (typeof type === "string" && type.endsWith("?")) {
|
|
261
|
+
if (value === null || value === undefined) {
|
|
262
|
+
return null as EncodedType<Type, Table>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return encode(typeTable, path, type.slice(0, type.length - 1), value) as EncodedType<Type, Table>;
|
|
266
|
+
} else if (typeof type === "string" && type.endsWith("[]")) {
|
|
267
|
+
if (!Array.isArray(value)) {
|
|
268
|
+
throw new ParseError(path, type, value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value.map((entry, index) => encode(typeTable, `${path}[${index}]`, type.slice(0, type.length - 2), entry)) as EncodedType<Type, Table>;
|
|
272
|
+
} else if (typeof type === "string" && simpleTypes.includes(type)) {
|
|
273
|
+
return simpleEncodeDecode(path, type, value) as EncodedType<Type, Table>;
|
|
274
|
+
} else if (type === "bytes") {
|
|
275
|
+
if (!(value instanceof Buffer)) {
|
|
276
|
+
throw new ParseError(path, type, value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return value.toString("base64") as EncodedType<Type, Table>;
|
|
280
|
+
} else if (type === "bigint") {
|
|
281
|
+
if (!(typeof value === "bigint")) {
|
|
282
|
+
throw new ParseError(path, type, value);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return value.toString() as EncodedType<Type, Table>;
|
|
286
|
+
} else if (type === "cpf") {
|
|
287
|
+
if (typeof value !== "string" || !CPF.isValid(value)) {
|
|
288
|
+
throw new ParseError(path, type, value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return CPF.strip(value) as EncodedType<Type, Table>;
|
|
292
|
+
} else if (type === "cnpj") {
|
|
293
|
+
if (typeof value !== "string" || !CNPJ.isValid(value)) {
|
|
294
|
+
throw new ParseError(path, type, value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return CNPJ.strip(value) as EncodedType<Type, Table>;
|
|
298
|
+
} else if (type === "date") {
|
|
299
|
+
if (!(value instanceof Date && !isNaN(value.getTime())) && !(typeof value === "string" && /^[0-9]{4}-[01][0-9]-[0123][0-9]$/u.test(value))) {
|
|
300
|
+
throw new ParseError(path, type, value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const dateValue = value instanceof Date ? value : new Date(value);
|
|
304
|
+
|
|
305
|
+
return `${dateValue.getFullYear().toString().padStart(4, "0")}-${(dateValue.getMonth() + 1).toString().padStart(2, "0")}-${dateValue
|
|
306
|
+
.getDate()
|
|
307
|
+
.toString()
|
|
308
|
+
.padStart(2, "0")}` as EncodedType<Type, Table>;
|
|
309
|
+
} else if (type === "datetime") {
|
|
310
|
+
if (
|
|
311
|
+
!(value instanceof Date && !isNaN(value.getTime())) &&
|
|
312
|
+
!(
|
|
313
|
+
typeof value === "string" &&
|
|
314
|
+
/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?(?:Z|[+-][012][0-9]:[0123456][0-9])?$/u.test(value)
|
|
315
|
+
)
|
|
316
|
+
) {
|
|
317
|
+
throw new ParseError(path, type, value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return (typeof value === "string" ? new Date(value) : value).toISOString().replace("Z", "") as EncodedType<Type, Table>;
|
|
321
|
+
} else if (type === "decimal") {
|
|
322
|
+
if (typeof value !== "number" && (typeof value !== "string" || !/^-?[0-9]+(?:\.[0-9]+)?$/u.test(value)) && !Decimal.isDecimal(value)) {
|
|
323
|
+
throw new ParseError(path, type, value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return new Decimal(value).toString() as EncodedType<Type, Table>;
|
|
327
|
+
} else {
|
|
328
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
329
|
+
const resolved = (typeTable as Record<string, TypeDescription>)[type as string];
|
|
330
|
+
|
|
331
|
+
if (resolved) {
|
|
332
|
+
return encode(typeTable, path, resolved, value) as EncodedType<Type, Table>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new Error(`Unknown type '${String(type)}' at '${path}'`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function decode<Table extends DeepReadonly<TypeTable>, Type extends DeepReadonly<TypeDescription>>(
|
|
340
|
+
typeTable: Table,
|
|
341
|
+
path: string,
|
|
342
|
+
type: Type,
|
|
343
|
+
value: unknown,
|
|
344
|
+
): DecodedType<Type, Table> {
|
|
345
|
+
if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) {
|
|
346
|
+
throw new Error(`Invalid type at '${path}', cannot be null`);
|
|
347
|
+
} else if (Array.isArray(type)) {
|
|
348
|
+
if (type.every(tag => typeof tag === "string")) {
|
|
349
|
+
for (const tag of type) {
|
|
350
|
+
if (tag === value) {
|
|
351
|
+
return tag as DecodedType<Type, Table>;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
for (const entry of type) {
|
|
356
|
+
if (typeof entry === "string") {
|
|
357
|
+
if (entry === value) {
|
|
358
|
+
return { tag: entry } as DecodedType<Type, Table>;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const [tag, valueType] = entry as [string, string];
|
|
362
|
+
|
|
363
|
+
if (tag === value) {
|
|
364
|
+
const decodedValues = decode(typeTable, `${path}.${tag}`, valueType, {}) as object;
|
|
365
|
+
|
|
366
|
+
return { ...decodedValues, tag } as DecodedType<Type, Table>;
|
|
367
|
+
} else if (Array.isArray(value) && value.length === 2 && tag === value[0]) {
|
|
368
|
+
const decodedValues = decode(typeTable, `${path}.${tag}`, valueType, value[1]) as object;
|
|
369
|
+
|
|
370
|
+
return { ...decodedValues, tag } as DecodedType<Type, Table>;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
throw new ParseError(path, type, value);
|
|
377
|
+
} else if (typeof type === "object") {
|
|
378
|
+
if (typeof value !== "object") {
|
|
379
|
+
throw new ParseError(path, type, value);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const obj: Record<string, unknown> = {};
|
|
383
|
+
|
|
384
|
+
for (const key of Object.keys(type)) {
|
|
385
|
+
obj[key] = decode(typeTable, `${path}.${key}`, (type as Record<string, TypeDescription>)[key], (value as Record<string, unknown>)[key]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return obj as DecodedType<Type, Table>;
|
|
389
|
+
} else if (typeof type === "string" && type.endsWith("?")) {
|
|
390
|
+
if (value === null || value === undefined) {
|
|
391
|
+
return null as DecodedType<Type, Table>;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return decode(typeTable, path, type.slice(0, type.length - 1), value) as unknown as DecodedType<Type, Table>;
|
|
395
|
+
} else if (typeof type === "string" && type.endsWith("[]")) {
|
|
396
|
+
if (!Array.isArray(value)) {
|
|
397
|
+
throw new ParseError(path, type, value);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return value.map((entry, index) => decode(typeTable, `${path}[${index}]`, type.slice(0, type.length - 2), entry)) as DecodedType<Type, Table>;
|
|
401
|
+
} else if (typeof type === "string" && simpleTypes.includes(type)) {
|
|
402
|
+
return simpleEncodeDecode(path, type, value) as DecodedType<Type, Table>;
|
|
403
|
+
} else if (type === "bytes") {
|
|
404
|
+
if (typeof value !== "string") {
|
|
405
|
+
throw new ParseError(path, `${String(type)} (base 64)`, value);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const buffer = Buffer.from(value, "base64");
|
|
409
|
+
|
|
410
|
+
if (buffer.toString("base64") !== value) {
|
|
411
|
+
throw new ParseError(path, `${String(type)} (base 64)`, value);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return buffer as DecodedType<Type, Table>;
|
|
415
|
+
} else if (type === "bigint") {
|
|
416
|
+
if (typeof value !== "number" && (typeof value !== "string" || !/^-?[0-9]+$/u.test(value))) {
|
|
417
|
+
throw new ParseError(path, type, value);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return BigInt(value) as DecodedType<Type, Table>;
|
|
421
|
+
} else if (type === "cpf") {
|
|
422
|
+
if (typeof value !== "string" || !CPF.isValid(value)) {
|
|
423
|
+
throw new ParseError(path, type, value);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return CPF.format(value) as DecodedType<Type, Table>;
|
|
427
|
+
} else if (type === "cnpj") {
|
|
428
|
+
if (typeof value !== "string" || !CNPJ.isValid(value)) {
|
|
429
|
+
throw new ParseError(path, type, value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return CNPJ.format(value) as DecodedType<Type, Table>;
|
|
433
|
+
} else if (type === "date") {
|
|
434
|
+
if (typeof value !== "string" || !/^[0-9]{4}-[01][0-9]-[0123][0-9]$/u.test(value)) {
|
|
435
|
+
throw new ParseError(path, type, value);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const day = parseInt(value.split("-")[2], 10);
|
|
439
|
+
const month = parseInt(value.split("-")[1], 10) - 1;
|
|
440
|
+
const year = parseInt(value.split("-")[0], 10);
|
|
441
|
+
const date = new Date(year, month, day);
|
|
442
|
+
|
|
443
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
444
|
+
throw new ParseError(path, type, value);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return date as DecodedType<Type, Table>;
|
|
448
|
+
} else if (type === "datetime") {
|
|
449
|
+
if (typeof value !== "string" || !/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?Z?$/u.test(value)) {
|
|
450
|
+
throw new ParseError(path, type, value);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return new Date(`${value.endsWith("Z") ? value : value.concat("Z")}`) as DecodedType<Type, Table>;
|
|
454
|
+
} else if (type === "decimal") {
|
|
455
|
+
if (typeof value !== "number" && (typeof value !== "string" || !/^-?[0-9]+(?:\.[0-9]+)?$/u.test(value))) {
|
|
456
|
+
throw new ParseError(path, type, value);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return new Decimal(value) as DecodedType<Type, Table>;
|
|
460
|
+
} else {
|
|
461
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
462
|
+
const resolved = (typeTable as Record<string, TypeDescription>)[type as string];
|
|
463
|
+
|
|
464
|
+
if (resolved) {
|
|
465
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
466
|
+
return decode(typeTable, path, resolved, value) as unknown as DecodedType<Type, Table>;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
throw new Error(`Unknown type '${String(type)}' at '${path}'`);
|
|
470
|
+
}
|
|
471
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export abstract class SdkgenError extends Error {
|
|
2
|
+
get type(): string {
|
|
3
|
+
return this.constructor.name;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
public toJSON(): { message: string; type: string } {
|
|
7
|
+
return {
|
|
8
|
+
message: this.message,
|
|
9
|
+
type: this.type,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export abstract class SdkgenErrorWithData<DataType> extends SdkgenError {
|
|
14
|
+
constructor(
|
|
15
|
+
message: string,
|
|
16
|
+
public data: DataType,
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public toJSON(): { data: DataType; message: string; type: string } {
|
|
22
|
+
return {
|
|
23
|
+
data: this.data,
|
|
24
|
+
message: this.message,
|
|
25
|
+
type: this.type,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Fatal extends SdkgenError {}
|
package/src/execute.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ThrowsAnnotation } from "@sdkgen/parser";
|
|
2
|
+
|
|
3
|
+
import type { BaseApiConfig } from "./api-config";
|
|
4
|
+
import type { Context, ContextReply } from "./context";
|
|
5
|
+
import { decode, encode } from "./encode-decode";
|
|
6
|
+
import { Fatal } from "./error";
|
|
7
|
+
import { has } from "./utils";
|
|
8
|
+
|
|
9
|
+
export async function executeRequest<ExtraContextT>(ctx: Context & ExtraContextT, apiConfig: BaseApiConfig<ExtraContextT>) {
|
|
10
|
+
// eslint-disable-next-line func-style
|
|
11
|
+
let next = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const functionDescription = apiConfig.astJson.functionTable[ctx.request.name];
|
|
14
|
+
const functionImplementation = apiConfig.fn[ctx.request.name];
|
|
15
|
+
|
|
16
|
+
if (!functionDescription || !functionImplementation) {
|
|
17
|
+
throw new Fatal(`Function does not exist: ${ctx.request.name}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const args = decode(apiConfig.astJson.typeTable, `${ctx.request.name}.args`, functionDescription.args, ctx.request.args);
|
|
21
|
+
|
|
22
|
+
const ret = (await functionImplementation(ctx, args)) as unknown;
|
|
23
|
+
const encodedRet = encode(apiConfig.astJson.typeTable, `${ctx.request.name}.ret`, functionDescription.ret, ret);
|
|
24
|
+
|
|
25
|
+
return { result: encodedRet } as ContextReply;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { error } as ContextReply;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (let i = apiConfig.middlewares.length - 1; i >= 0; --i) {
|
|
32
|
+
const middleware = apiConfig.middlewares[i];
|
|
33
|
+
const previousNext = next;
|
|
34
|
+
|
|
35
|
+
next = async () => {
|
|
36
|
+
try {
|
|
37
|
+
return await middleware(ctx, previousNext);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return { error } as ContextReply;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reply = await next();
|
|
45
|
+
|
|
46
|
+
// If errors, check if the error type is one of the @throws annotation. If it isn't, change to Fatal
|
|
47
|
+
if (reply.error) {
|
|
48
|
+
const functionAst = apiConfig.ast.operations.find(op => op.name === ctx.request.name);
|
|
49
|
+
|
|
50
|
+
if (functionAst) {
|
|
51
|
+
const allowedErrors = functionAst.annotations.filter(ann => ann instanceof ThrowsAnnotation).map(ann => (ann as ThrowsAnnotation).error);
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
typeof reply.error !== "object" ||
|
|
55
|
+
reply.error === null ||
|
|
56
|
+
!has(reply.error, "type") ||
|
|
57
|
+
typeof reply.error.type !== "string" ||
|
|
58
|
+
(allowedErrors.length > 0 && !allowedErrors.includes(reply.error.type)) ||
|
|
59
|
+
!apiConfig.astJson.errors.map(error => (typeof error === "string" ? error : error[0])).includes(reply.error.type)
|
|
60
|
+
) {
|
|
61
|
+
Object.defineProperty(reply.error, "type", { value: "Fatal" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return reply;
|
|
67
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable prefer-promise-reject-errors */
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { request as httpRequest } from "http";
|
|
5
|
+
import { request as httpsRequest } from "https";
|
|
6
|
+
import type { RequestOptions } from "https";
|
|
7
|
+
import { hostname } from "os";
|
|
8
|
+
import { URL } from "url";
|
|
9
|
+
|
|
10
|
+
import type { AstJson } from "@sdkgen/parser";
|
|
11
|
+
import type { PartialDeep } from "type-fest";
|
|
12
|
+
|
|
13
|
+
import type { Context } from "./context";
|
|
14
|
+
import { decode, encode } from "./encode-decode";
|
|
15
|
+
import type { SdkgenError, SdkgenErrorWithData } from "./error";
|
|
16
|
+
import type { DeepReadonly } from "./utils";
|
|
17
|
+
import { has } from "./utils";
|
|
18
|
+
|
|
19
|
+
type ErrClasses = Record<string, (new (message: string, data: any) => SdkgenErrorWithData<any>) | (new (message: string) => SdkgenError) | undefined>;
|
|
20
|
+
|
|
21
|
+
export class SdkgenHttpClient {
|
|
22
|
+
private baseUrl: URL;
|
|
23
|
+
|
|
24
|
+
extra = new Map<string, unknown>();
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
baseUrl: string,
|
|
28
|
+
private astJson: DeepReadonly<AstJson>,
|
|
29
|
+
private errClasses: ErrClasses,
|
|
30
|
+
) {
|
|
31
|
+
this.baseUrl = new URL(baseUrl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async makeRequest(ctx: PartialDeep<Context> | null, functionName: string, args: unknown): Promise<any> {
|
|
35
|
+
const func = this.astJson.functionTable[functionName];
|
|
36
|
+
|
|
37
|
+
if (!func) {
|
|
38
|
+
throw new Error(`Unknown function ${functionName}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const extra: Record<string, unknown> = {};
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of this.extra) {
|
|
44
|
+
extra[key] = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const requestBody = JSON.stringify({
|
|
48
|
+
args: encode(this.astJson.typeTable, `${functionName}.args`, func.args, args),
|
|
49
|
+
deviceInfo: ctx?.request?.deviceInfo ?? { id: hostname(), type: "node" },
|
|
50
|
+
extra: {
|
|
51
|
+
...extra,
|
|
52
|
+
...ctx?.request?.extra,
|
|
53
|
+
},
|
|
54
|
+
name: functionName,
|
|
55
|
+
requestId: ctx?.request?.id ? ctx.request.id + randomBytes(6).toString("hex") : randomBytes(16).toString("hex"),
|
|
56
|
+
version: 3,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const options: RequestOptions = {
|
|
60
|
+
hostname: this.baseUrl.hostname,
|
|
61
|
+
method: "POST",
|
|
62
|
+
path: this.baseUrl.pathname,
|
|
63
|
+
port: this.baseUrl.port,
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/sdkgen",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const encodedRet = await new Promise<unknown>((resolve, reject) => {
|
|
70
|
+
const req = (this.baseUrl.protocol === "http:" ? httpRequest : httpsRequest)(options, res => {
|
|
71
|
+
let data = "";
|
|
72
|
+
|
|
73
|
+
res.on("data", chunk => {
|
|
74
|
+
data += chunk;
|
|
75
|
+
});
|
|
76
|
+
res.on("end", () => {
|
|
77
|
+
try {
|
|
78
|
+
const response = JSON.parse(data) as object;
|
|
79
|
+
|
|
80
|
+
if (has(response, "error") && response.error) {
|
|
81
|
+
reject(response.error);
|
|
82
|
+
} else {
|
|
83
|
+
resolve(has(response, "result") ? response.result : null);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reject({ message: `${error}`, type: "Fatal" });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
res.on("error", error => {
|
|
90
|
+
reject({ message: `${error}`, type: "Fatal" });
|
|
91
|
+
});
|
|
92
|
+
res.on("aborted", () => {
|
|
93
|
+
reject({ message: "Request aborted", type: "Fatal" });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
req.on("error", error => {
|
|
98
|
+
reject({ message: `${error}`, type: "Fatal" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.write(requestBody);
|
|
102
|
+
req.end();
|
|
103
|
+
}).catch((error: object) => {
|
|
104
|
+
if (has(error, "type") && has(error, "message") && typeof error.type === "string" && typeof error.message === "string") {
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
106
|
+
const errClass = this.errClasses[error.type] ?? this.errClasses.Fatal!;
|
|
107
|
+
const errType = errClass.name;
|
|
108
|
+
|
|
109
|
+
const errorJson = this.astJson.errors.find(err => (Array.isArray(err) ? err[0] === errType : err === errType));
|
|
110
|
+
|
|
111
|
+
let newError;
|
|
112
|
+
|
|
113
|
+
if (errorJson && Array.isArray(errorJson) && has(error, "data")) {
|
|
114
|
+
newError = new errClass(error.message, decode(this.astJson.typeTable, `${errClass.name}.data`, errorJson[1], error.data));
|
|
115
|
+
} else {
|
|
116
|
+
newError = new errClass(error.message, undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!newError.type) {
|
|
120
|
+
(newError as unknown as { type: string }).type = errType;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw newError;
|
|
124
|
+
} else {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return decode(this.astJson.typeTable, `${functionName}.ret`, func.ret, encodedRet);
|
|
130
|
+
}
|
|
131
|
+
}
|