@faapi/faapi 0.0.0-canary.22b65a2 → 0.0.0-canary.2a7b6a3
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/README.md +7 -1
- package/dist/cli/index.js +1634 -884
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +151 -27
- package/dist/index.js +379 -221
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,129 +1,3 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
-
};
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// src/utils/isPlainObject.ts
|
|
12
|
-
function isPlainObject(value) {
|
|
13
|
-
if (value === null || typeof value !== "object") {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const proto = Object.getPrototypeOf(value);
|
|
20
|
-
return proto === null || proto === Object.prototype;
|
|
21
|
-
}
|
|
22
|
-
var init_isPlainObject = __esm({
|
|
23
|
-
"src/utils/isPlainObject.ts"() {
|
|
24
|
-
"use strict";
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// src/response/toResponse.ts
|
|
29
|
-
var toResponse_exports = {};
|
|
30
|
-
__export(toResponse_exports, {
|
|
31
|
-
toResponse: () => toResponse
|
|
32
|
-
});
|
|
33
|
-
async function toResponse(value, meta) {
|
|
34
|
-
if (value instanceof Promise) {
|
|
35
|
-
return toResponse(await value, meta);
|
|
36
|
-
}
|
|
37
|
-
const applyMeta = (headers2) => {
|
|
38
|
-
if (!meta) return;
|
|
39
|
-
for (const [key, val] of Object.entries(meta.headers)) {
|
|
40
|
-
headers2.set(key, val);
|
|
41
|
-
}
|
|
42
|
-
for (const cookie of meta.setCookies ?? []) {
|
|
43
|
-
headers2.append("set-cookie", cookie);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
if (value instanceof Response) {
|
|
47
|
-
if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
|
|
48
|
-
const headers2 = new Headers(value.headers);
|
|
49
|
-
applyMeta(headers2);
|
|
50
|
-
return new Response(value.body, {
|
|
51
|
-
status: meta.status ?? value.status,
|
|
52
|
-
headers: headers2
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
if (value === null || value === void 0) {
|
|
58
|
-
const status = meta?.status ?? 204;
|
|
59
|
-
const headers2 = new Headers();
|
|
60
|
-
applyMeta(headers2);
|
|
61
|
-
return new Response(null, { status, headers: headers2 });
|
|
62
|
-
}
|
|
63
|
-
if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
|
|
64
|
-
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
65
|
-
applyMeta(headers2);
|
|
66
|
-
return new Response(value, {
|
|
67
|
-
status: meta?.status ?? 200,
|
|
68
|
-
headers: headers2
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
|
|
72
|
-
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
73
|
-
applyMeta(headers2);
|
|
74
|
-
return new Response(value, {
|
|
75
|
-
status: meta?.status ?? 200,
|
|
76
|
-
headers: headers2
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (value instanceof Uint8Array) {
|
|
80
|
-
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
81
|
-
applyMeta(headers2);
|
|
82
|
-
return new Response(value, {
|
|
83
|
-
status: meta?.status ?? 200,
|
|
84
|
-
headers: headers2
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (isPlainObject(value) || Array.isArray(value)) {
|
|
88
|
-
const body2 = JSON.stringify(value);
|
|
89
|
-
const headers2 = new Headers({ "Content-Type": "application/json" });
|
|
90
|
-
applyMeta(headers2);
|
|
91
|
-
return new Response(body2, {
|
|
92
|
-
status: meta?.status ?? 200,
|
|
93
|
-
headers: headers2
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
if (typeof value === "string") {
|
|
97
|
-
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
98
|
-
applyMeta(headers2);
|
|
99
|
-
return new Response(value, {
|
|
100
|
-
status: meta?.status ?? 200,
|
|
101
|
-
headers: headers2
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
105
|
-
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
106
|
-
applyMeta(headers2);
|
|
107
|
-
return new Response(String(value), {
|
|
108
|
-
status: meta?.status ?? 200,
|
|
109
|
-
headers: headers2
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
const body = JSON.stringify(value);
|
|
113
|
-
const headers = new Headers({ "Content-Type": "application/json" });
|
|
114
|
-
applyMeta(headers);
|
|
115
|
-
return new Response(body, {
|
|
116
|
-
status: meta?.status ?? 200,
|
|
117
|
-
headers
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
var init_toResponse = __esm({
|
|
121
|
-
"src/response/toResponse.ts"() {
|
|
122
|
-
"use strict";
|
|
123
|
-
init_isPlainObject();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
1
|
// src/ast/createProgram.ts
|
|
128
2
|
import ts from "typescript";
|
|
129
3
|
var programCache = /* @__PURE__ */ new Map();
|
|
@@ -272,6 +146,9 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
272
146
|
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.KeyOfKeyword) {
|
|
273
147
|
return resolveKeyOf(typeNode, checker);
|
|
274
148
|
}
|
|
149
|
+
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.ReadonlyKeyword) {
|
|
150
|
+
return resolveTypeNode(typeNode.type, checker, visited);
|
|
151
|
+
}
|
|
275
152
|
if (ts2.isTypeReferenceNode(typeNode)) {
|
|
276
153
|
return resolveTypeReference(typeNode, checker, visited);
|
|
277
154
|
}
|
|
@@ -284,7 +161,11 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
|
|
|
284
161
|
const name = member.name.getText();
|
|
285
162
|
const optional = !!member.questionToken;
|
|
286
163
|
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
287
|
-
|
|
164
|
+
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
165
|
+
validateConstraints(constraints, type, name);
|
|
166
|
+
properties.push(
|
|
167
|
+
constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
|
|
168
|
+
);
|
|
288
169
|
}
|
|
289
170
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
290
171
|
const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
|
|
@@ -366,7 +247,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
366
247
|
if (typeName === "Date") {
|
|
367
248
|
return { kind: "date" };
|
|
368
249
|
}
|
|
369
|
-
if (typeName === "Array" && typeNode.typeArguments?.length === 1) {
|
|
250
|
+
if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
|
|
370
251
|
return {
|
|
371
252
|
kind: "array",
|
|
372
253
|
element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
|
|
@@ -491,7 +372,12 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
491
372
|
const name = member.name.getText();
|
|
492
373
|
const optional = !!member.questionToken;
|
|
493
374
|
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
494
|
-
|
|
375
|
+
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
376
|
+
validateConstraints(constraints, type, name);
|
|
377
|
+
propMap.set(
|
|
378
|
+
name,
|
|
379
|
+
constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
|
|
380
|
+
);
|
|
495
381
|
}
|
|
496
382
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
497
383
|
const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
|
|
@@ -504,6 +390,142 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
504
390
|
}
|
|
505
391
|
return { kind: "object", properties };
|
|
506
392
|
}
|
|
393
|
+
var NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
394
|
+
"max",
|
|
395
|
+
"min",
|
|
396
|
+
"int",
|
|
397
|
+
"positive",
|
|
398
|
+
"negative",
|
|
399
|
+
"nonnegative",
|
|
400
|
+
"nonpositive"
|
|
401
|
+
]);
|
|
402
|
+
var LENGTH_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
403
|
+
"maxLength",
|
|
404
|
+
"minLength",
|
|
405
|
+
"length"
|
|
406
|
+
]);
|
|
407
|
+
var STRING_FORMAT_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
408
|
+
"regex",
|
|
409
|
+
"email",
|
|
410
|
+
"url",
|
|
411
|
+
"uuid"
|
|
412
|
+
]);
|
|
413
|
+
function extractConstraintsFromJsDoc(node, fieldName) {
|
|
414
|
+
const jsDocs = ts2.getJSDocCommentsAndTags(node).filter((entry) => ts2.isJSDoc(entry));
|
|
415
|
+
if (jsDocs.length === 0) return [];
|
|
416
|
+
const constraints = [];
|
|
417
|
+
for (const jsDoc of jsDocs) {
|
|
418
|
+
if (!jsDoc.tags) continue;
|
|
419
|
+
for (const tag of jsDoc.tags) {
|
|
420
|
+
const constraint = parseJsDocTag(tag, fieldName);
|
|
421
|
+
if (constraint) constraints.push(constraint);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return constraints;
|
|
425
|
+
}
|
|
426
|
+
function getTagCommentText(tag) {
|
|
427
|
+
const comment = tag.comment;
|
|
428
|
+
if (typeof comment === "string") return comment;
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
431
|
+
function parseJsDocTag(tag, fieldName) {
|
|
432
|
+
const tagName = tag.tagName.text;
|
|
433
|
+
switch (tagName) {
|
|
434
|
+
// 数值约束(带值)
|
|
435
|
+
case "max":
|
|
436
|
+
case "min": {
|
|
437
|
+
const value = parseNumberValue(tag, fieldName, tagName);
|
|
438
|
+
return { kind: tagName, value };
|
|
439
|
+
}
|
|
440
|
+
// 长度约束(带值)
|
|
441
|
+
case "maxLength":
|
|
442
|
+
case "minLength":
|
|
443
|
+
case "length": {
|
|
444
|
+
const value = parseNumberValue(tag, fieldName, tagName);
|
|
445
|
+
return { kind: tagName, value };
|
|
446
|
+
}
|
|
447
|
+
// 正则约束(带 /pattern/flags 值)
|
|
448
|
+
case "regex":
|
|
449
|
+
case "pattern": {
|
|
450
|
+
const text = getTagCommentText(tag);
|
|
451
|
+
if (!text) {
|
|
452
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981 /pattern/flags \u5F62\u5F0F\u7684\u503C`);
|
|
453
|
+
}
|
|
454
|
+
const regex = parseRegexLiteral(text.trim(), fieldName);
|
|
455
|
+
return { kind: "regex", pattern: regex.pattern, flags: regex.flags };
|
|
456
|
+
}
|
|
457
|
+
// 数值约束(无值)
|
|
458
|
+
case "int":
|
|
459
|
+
case "positive":
|
|
460
|
+
case "negative":
|
|
461
|
+
case "nonnegative":
|
|
462
|
+
case "nonpositive":
|
|
463
|
+
return { kind: tagName };
|
|
464
|
+
// 字符串格式约束(无值)
|
|
465
|
+
case "email":
|
|
466
|
+
case "url":
|
|
467
|
+
case "uuid":
|
|
468
|
+
return { kind: tagName };
|
|
469
|
+
default:
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function parseNumberValue(tag, fieldName, tagName) {
|
|
474
|
+
const text = getTagCommentText(tag);
|
|
475
|
+
if (!text) {
|
|
476
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981\u4E00\u4E2A\u6570\u5B57\u503C`);
|
|
477
|
+
}
|
|
478
|
+
const trimmed = text.trim();
|
|
479
|
+
const num = Number(trimmed);
|
|
480
|
+
if (!Number.isFinite(num)) {
|
|
481
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u7684\u503C "${trimmed}" \u4E0D\u662F\u6709\u6548\u6570\u5B57`);
|
|
482
|
+
}
|
|
483
|
+
return num;
|
|
484
|
+
}
|
|
485
|
+
function parseRegexLiteral(text, fieldName) {
|
|
486
|
+
const match = /^\/(.+)\/([gimsuy]*)$/.exec(text);
|
|
487
|
+
if (!match) {
|
|
488
|
+
throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u4E0D\u662F /pattern/flags \u5F62\u5F0F`);
|
|
489
|
+
}
|
|
490
|
+
const [, pattern, flags] = match;
|
|
491
|
+
if (!pattern) {
|
|
492
|
+
throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u7684 pattern \u90E8\u5206\u4E3A\u7A7A`);
|
|
493
|
+
}
|
|
494
|
+
return flags ? { pattern, flags } : { pattern };
|
|
495
|
+
}
|
|
496
|
+
function validateConstraints(constraints, type, fieldName) {
|
|
497
|
+
if (constraints.length === 0) return;
|
|
498
|
+
for (const constraint of constraints) {
|
|
499
|
+
const kind = constraint.kind;
|
|
500
|
+
if (NUMBER_CONSTRAINT_KINDS.has(kind)) {
|
|
501
|
+
if (type.kind !== "number") {
|
|
502
|
+
throw new SchemaExtractionError(
|
|
503
|
+
fieldName,
|
|
504
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E number \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (LENGTH_CONSTRAINT_KINDS.has(kind)) {
|
|
510
|
+
if (type.kind !== "string" && type.kind !== "array") {
|
|
511
|
+
throw new SchemaExtractionError(
|
|
512
|
+
fieldName,
|
|
513
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u6216 array \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (STRING_FORMAT_CONSTRAINT_KINDS.has(kind)) {
|
|
519
|
+
if (type.kind !== "string") {
|
|
520
|
+
throw new SchemaExtractionError(
|
|
521
|
+
fieldName,
|
|
522
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
507
529
|
|
|
508
530
|
// src/ast/extractHandlerTypes.ts
|
|
509
531
|
function extractTypeInfo(program, filePath, typeName) {
|
|
@@ -988,6 +1010,58 @@ async function loadConfig(rootDir, outDir) {
|
|
|
988
1010
|
return null;
|
|
989
1011
|
}
|
|
990
1012
|
|
|
1013
|
+
// src/errors/FaapiError.ts
|
|
1014
|
+
var FaapiError = class extends Error {
|
|
1015
|
+
constructor(code, message, statusCode) {
|
|
1016
|
+
super(message);
|
|
1017
|
+
this.code = code;
|
|
1018
|
+
this.statusCode = statusCode;
|
|
1019
|
+
this.name = "FaapiError";
|
|
1020
|
+
}
|
|
1021
|
+
code;
|
|
1022
|
+
statusCode;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// src/errors/httpErrors.ts
|
|
1026
|
+
function deriveStatusCode(issues) {
|
|
1027
|
+
const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
|
|
1028
|
+
return has400 ? 400 : 422;
|
|
1029
|
+
}
|
|
1030
|
+
var ValidationError = class extends FaapiError {
|
|
1031
|
+
constructor(message, issues) {
|
|
1032
|
+
super("VALIDATION_ERROR", message, deriveStatusCode(issues));
|
|
1033
|
+
this.issues = issues;
|
|
1034
|
+
this.name = "ValidationError";
|
|
1035
|
+
}
|
|
1036
|
+
issues;
|
|
1037
|
+
};
|
|
1038
|
+
var RouteNotFoundError = class extends FaapiError {
|
|
1039
|
+
constructor(path9) {
|
|
1040
|
+
super("ROUTE_NOT_FOUND", `Route not found: ${path9}`, 404);
|
|
1041
|
+
this.name = "RouteNotFoundError";
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
var MethodNotAllowedError = class extends FaapiError {
|
|
1045
|
+
constructor(method, path9, allowedMethods) {
|
|
1046
|
+
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path9}`, 405);
|
|
1047
|
+
this.allowedMethods = allowedMethods;
|
|
1048
|
+
this.name = "MethodNotAllowedError";
|
|
1049
|
+
}
|
|
1050
|
+
allowedMethods;
|
|
1051
|
+
};
|
|
1052
|
+
var InternalError = class extends FaapiError {
|
|
1053
|
+
constructor(message) {
|
|
1054
|
+
super("INTERNAL_ERROR", message, 500);
|
|
1055
|
+
this.name = "InternalError";
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
var ModuleLoadError = class extends FaapiError {
|
|
1059
|
+
constructor(filePath, reason) {
|
|
1060
|
+
super("MODULE_LOAD_ERROR", `Failed to load module ${filePath}: ${reason}`, 500);
|
|
1061
|
+
this.name = "ModuleLoadError";
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
991
1065
|
// src/cli/createAppCore.ts
|
|
992
1066
|
import fs4 from "fs";
|
|
993
1067
|
import path7 from "path";
|
|
@@ -1417,52 +1491,6 @@ async function parseMultipart(request) {
|
|
|
1417
1491
|
return { fields, files };
|
|
1418
1492
|
}
|
|
1419
1493
|
|
|
1420
|
-
// src/errors/FaapiError.ts
|
|
1421
|
-
var FaapiError = class extends Error {
|
|
1422
|
-
constructor(code, message, statusCode) {
|
|
1423
|
-
super(message);
|
|
1424
|
-
this.code = code;
|
|
1425
|
-
this.statusCode = statusCode;
|
|
1426
|
-
this.name = "FaapiError";
|
|
1427
|
-
}
|
|
1428
|
-
code;
|
|
1429
|
-
statusCode;
|
|
1430
|
-
};
|
|
1431
|
-
|
|
1432
|
-
// src/errors/httpErrors.ts
|
|
1433
|
-
function deriveStatusCode(issues) {
|
|
1434
|
-
const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
|
|
1435
|
-
return has400 ? 400 : 422;
|
|
1436
|
-
}
|
|
1437
|
-
var ValidationError = class extends FaapiError {
|
|
1438
|
-
constructor(message, issues) {
|
|
1439
|
-
super("VALIDATION_ERROR", message, deriveStatusCode(issues));
|
|
1440
|
-
this.issues = issues;
|
|
1441
|
-
this.name = "ValidationError";
|
|
1442
|
-
}
|
|
1443
|
-
issues;
|
|
1444
|
-
};
|
|
1445
|
-
var RouteNotFoundError = class extends FaapiError {
|
|
1446
|
-
constructor(path9) {
|
|
1447
|
-
super("ROUTE_NOT_FOUND", `Route not found: ${path9}`, 404);
|
|
1448
|
-
this.name = "RouteNotFoundError";
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
var MethodNotAllowedError = class extends FaapiError {
|
|
1452
|
-
constructor(method, path9, allowedMethods) {
|
|
1453
|
-
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path9}`, 405);
|
|
1454
|
-
this.allowedMethods = allowedMethods;
|
|
1455
|
-
this.name = "MethodNotAllowedError";
|
|
1456
|
-
}
|
|
1457
|
-
allowedMethods;
|
|
1458
|
-
};
|
|
1459
|
-
var InternalError = class extends FaapiError {
|
|
1460
|
-
constructor(message) {
|
|
1461
|
-
super("INTERNAL_ERROR", message, 500);
|
|
1462
|
-
this.name = "InternalError";
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
|
|
1466
1494
|
// src/runtime/resolveInput.ts
|
|
1467
1495
|
async function resolveInput(method, request) {
|
|
1468
1496
|
const inputType = getInputTypeForMethod(method);
|
|
@@ -1503,8 +1531,106 @@ async function resolveInput(method, request) {
|
|
|
1503
1531
|
return queryToObject(url.searchParams);
|
|
1504
1532
|
}
|
|
1505
1533
|
|
|
1506
|
-
// src/
|
|
1507
|
-
|
|
1534
|
+
// src/utils/isPlainObject.ts
|
|
1535
|
+
function isPlainObject(value) {
|
|
1536
|
+
if (value === null || typeof value !== "object") {
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
if (Array.isArray(value)) {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
const proto = Object.getPrototypeOf(value);
|
|
1543
|
+
return proto === null || proto === Object.prototype;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/response/toResponse.ts
|
|
1547
|
+
async function toResponse(value, meta) {
|
|
1548
|
+
if (value instanceof Promise) {
|
|
1549
|
+
return toResponse(await value, meta);
|
|
1550
|
+
}
|
|
1551
|
+
const applyMeta = (headers2) => {
|
|
1552
|
+
if (!meta) return;
|
|
1553
|
+
for (const [key, val] of Object.entries(meta.headers)) {
|
|
1554
|
+
headers2.set(key, val);
|
|
1555
|
+
}
|
|
1556
|
+
for (const cookie of meta.setCookies ?? []) {
|
|
1557
|
+
headers2.append("set-cookie", cookie);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
if (value instanceof Response) {
|
|
1561
|
+
if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
|
|
1562
|
+
const headers2 = new Headers(value.headers);
|
|
1563
|
+
applyMeta(headers2);
|
|
1564
|
+
return new Response(value.body, {
|
|
1565
|
+
status: meta.status ?? value.status,
|
|
1566
|
+
headers: headers2
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
return value;
|
|
1570
|
+
}
|
|
1571
|
+
if (value === null || value === void 0) {
|
|
1572
|
+
const status = meta?.status ?? 204;
|
|
1573
|
+
const headers2 = new Headers();
|
|
1574
|
+
applyMeta(headers2);
|
|
1575
|
+
return new Response(null, { status, headers: headers2 });
|
|
1576
|
+
}
|
|
1577
|
+
if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
|
|
1578
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1579
|
+
applyMeta(headers2);
|
|
1580
|
+
return new Response(value, {
|
|
1581
|
+
status: meta?.status ?? 200,
|
|
1582
|
+
headers: headers2
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
|
|
1586
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1587
|
+
applyMeta(headers2);
|
|
1588
|
+
return new Response(value, {
|
|
1589
|
+
status: meta?.status ?? 200,
|
|
1590
|
+
headers: headers2
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
if (value instanceof Uint8Array) {
|
|
1594
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1595
|
+
applyMeta(headers2);
|
|
1596
|
+
return new Response(value, {
|
|
1597
|
+
status: meta?.status ?? 200,
|
|
1598
|
+
headers: headers2
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
if (isPlainObject(value) || Array.isArray(value)) {
|
|
1602
|
+
const body2 = JSON.stringify(value);
|
|
1603
|
+
const headers2 = new Headers({ "Content-Type": "application/json" });
|
|
1604
|
+
applyMeta(headers2);
|
|
1605
|
+
return new Response(body2, {
|
|
1606
|
+
status: meta?.status ?? 200,
|
|
1607
|
+
headers: headers2
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (typeof value === "string") {
|
|
1611
|
+
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
1612
|
+
applyMeta(headers2);
|
|
1613
|
+
return new Response(value, {
|
|
1614
|
+
status: meta?.status ?? 200,
|
|
1615
|
+
headers: headers2
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1619
|
+
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
1620
|
+
applyMeta(headers2);
|
|
1621
|
+
return new Response(String(value), {
|
|
1622
|
+
status: meta?.status ?? 200,
|
|
1623
|
+
headers: headers2
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
const body = JSON.stringify(value);
|
|
1627
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1628
|
+
applyMeta(headers);
|
|
1629
|
+
return new Response(body, {
|
|
1630
|
+
status: meta?.status ?? 200,
|
|
1631
|
+
headers
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1508
1634
|
|
|
1509
1635
|
// src/injection/injectParams.ts
|
|
1510
1636
|
function getBuiltinInjectionValue(type, ctx, body) {
|
|
@@ -1727,16 +1853,13 @@ function mapZodCode(zodCode, message) {
|
|
|
1727
1853
|
return "TYPE_MISMATCH";
|
|
1728
1854
|
case "unrecognized_keys":
|
|
1729
1855
|
return "INVALID_FORMAT";
|
|
1730
|
-
case "
|
|
1856
|
+
case "invalid_value":
|
|
1731
1857
|
case "invalid_string":
|
|
1732
|
-
case "invalid_date":
|
|
1733
1858
|
case "too_small":
|
|
1734
1859
|
case "too_big":
|
|
1735
1860
|
case "invalid_intersection_types":
|
|
1736
1861
|
case "not_multiple_of":
|
|
1737
1862
|
return "INVALID_VALUE";
|
|
1738
|
-
case "not_finite":
|
|
1739
|
-
return "COERCE_FAILED";
|
|
1740
1863
|
case "custom":
|
|
1741
1864
|
return "INVALID_VALUE";
|
|
1742
1865
|
default:
|
|
@@ -1762,7 +1885,7 @@ function getClientIp(req) {
|
|
|
1762
1885
|
const first = xff.split(",")[0]?.trim();
|
|
1763
1886
|
if (first) return first;
|
|
1764
1887
|
}
|
|
1765
|
-
const remote = req.socket
|
|
1888
|
+
const remote = req.socket?.remoteAddress;
|
|
1766
1889
|
if (remote) {
|
|
1767
1890
|
if (remote.startsWith("::ffff:")) {
|
|
1768
1891
|
return remote.slice(7);
|
|
@@ -1835,14 +1958,7 @@ function nodeHttpToWebHeaders(req) {
|
|
|
1835
1958
|
}
|
|
1836
1959
|
return headers;
|
|
1837
1960
|
}
|
|
1838
|
-
function buildErrorResponse(err
|
|
1839
|
-
if (errorFormat) {
|
|
1840
|
-
try {
|
|
1841
|
-
const res = errorFormat(err, ctx);
|
|
1842
|
-
if (res) return res;
|
|
1843
|
-
} catch {
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1961
|
+
function buildErrorResponse(err) {
|
|
1846
1962
|
try {
|
|
1847
1963
|
return formatErrorResponse(err);
|
|
1848
1964
|
} catch {
|
|
@@ -1932,7 +2048,7 @@ async function sendResponseToSocket(socket, response) {
|
|
|
1932
2048
|
socket.destroy();
|
|
1933
2049
|
}
|
|
1934
2050
|
function attachWebSocket(options) {
|
|
1935
|
-
const { server, routesRef, rootDir, config,
|
|
2051
|
+
const { server, routesRef, rootDir, config, globalMiddlewares } = options;
|
|
1936
2052
|
const wss = new WebSocketServer({ noServer: true });
|
|
1937
2053
|
server.on("upgrade", async (req, socket, head) => {
|
|
1938
2054
|
const currentWsRoutes = routesRef.wsCurrent;
|
|
@@ -1989,7 +2105,7 @@ function attachWebSocket(options) {
|
|
|
1989
2105
|
console.error("[faapi] WS \u63E1\u624B\u540E\u4E2D\u95F4\u4EF6\u629B\u9519:", err);
|
|
1990
2106
|
return;
|
|
1991
2107
|
}
|
|
1992
|
-
response = buildErrorResponse(err
|
|
2108
|
+
response = buildErrorResponse(err);
|
|
1993
2109
|
}
|
|
1994
2110
|
if (upgraded) {
|
|
1995
2111
|
return;
|
|
@@ -2071,12 +2187,51 @@ function collectNamedTypes(type, ctx) {
|
|
|
2071
2187
|
}
|
|
2072
2188
|
}
|
|
2073
2189
|
}
|
|
2074
|
-
function runtimeTypeToZodExpression(type, ctx) {
|
|
2190
|
+
function runtimeTypeToZodExpression(type, ctx, constraints) {
|
|
2075
2191
|
const expr = baseExpression(type, ctx);
|
|
2192
|
+
const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
|
|
2076
2193
|
if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
|
|
2077
|
-
return wrapCoercePreprocess(type.kind,
|
|
2194
|
+
return wrapCoercePreprocess(type.kind, withConstraints);
|
|
2195
|
+
}
|
|
2196
|
+
return withConstraints;
|
|
2197
|
+
}
|
|
2198
|
+
function applyConstraints(baseExpr, constraints, typeKind) {
|
|
2199
|
+
const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
|
|
2200
|
+
return `${baseExpr}${suffix}`;
|
|
2201
|
+
}
|
|
2202
|
+
function constraintToZodChain(constraint, _typeKind) {
|
|
2203
|
+
switch (constraint.kind) {
|
|
2204
|
+
case "max":
|
|
2205
|
+
return `.max(${constraint.value})`;
|
|
2206
|
+
case "min":
|
|
2207
|
+
return `.min(${constraint.value})`;
|
|
2208
|
+
case "int":
|
|
2209
|
+
return ".int()";
|
|
2210
|
+
case "positive":
|
|
2211
|
+
return ".positive()";
|
|
2212
|
+
case "negative":
|
|
2213
|
+
return ".negative()";
|
|
2214
|
+
case "nonnegative":
|
|
2215
|
+
return ".nonnegative()";
|
|
2216
|
+
case "nonpositive":
|
|
2217
|
+
return ".nonpositive()";
|
|
2218
|
+
case "maxLength":
|
|
2219
|
+
return `.max(${constraint.value})`;
|
|
2220
|
+
case "minLength":
|
|
2221
|
+
return `.min(${constraint.value})`;
|
|
2222
|
+
case "length":
|
|
2223
|
+
return `.length(${constraint.value})`;
|
|
2224
|
+
case "regex": {
|
|
2225
|
+
const flags = constraint.flags ?? "";
|
|
2226
|
+
return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
|
|
2227
|
+
}
|
|
2228
|
+
case "email":
|
|
2229
|
+
return ".email()";
|
|
2230
|
+
case "url":
|
|
2231
|
+
return ".url()";
|
|
2232
|
+
case "uuid":
|
|
2233
|
+
return ".uuid()";
|
|
2078
2234
|
}
|
|
2079
|
-
return expr;
|
|
2080
2235
|
}
|
|
2081
2236
|
function baseExpression(type, ctx) {
|
|
2082
2237
|
switch (type.kind) {
|
|
@@ -2174,8 +2329,9 @@ function generateTupleExpression(elements, ctx) {
|
|
|
2174
2329
|
}
|
|
2175
2330
|
function generateObjectExpression(properties, ctx) {
|
|
2176
2331
|
const fields = properties.map((prop) => {
|
|
2177
|
-
const expr = runtimeTypeToZodExpression(prop.type, ctx);
|
|
2178
|
-
|
|
2332
|
+
const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
|
|
2333
|
+
const finalExpr = prop.optional ? `${expr}.optional()` : expr;
|
|
2334
|
+
return `${JSON.stringify(prop.name)}: ${finalExpr}`;
|
|
2179
2335
|
});
|
|
2180
2336
|
return `z.object({ ${fields.join(", ")} })`;
|
|
2181
2337
|
}
|
|
@@ -2405,8 +2561,6 @@ function createServer(options) {
|
|
|
2405
2561
|
appDir,
|
|
2406
2562
|
outDir,
|
|
2407
2563
|
cors: corsOption,
|
|
2408
|
-
responseFormat,
|
|
2409
|
-
errorFormat,
|
|
2410
2564
|
onError,
|
|
2411
2565
|
config,
|
|
2412
2566
|
wsRoutes,
|
|
@@ -2445,8 +2599,6 @@ function createServer(options) {
|
|
|
2445
2599
|
req,
|
|
2446
2600
|
res,
|
|
2447
2601
|
configMiddlewares,
|
|
2448
|
-
responseFormat,
|
|
2449
|
-
errorFormat,
|
|
2450
2602
|
onError,
|
|
2451
2603
|
config,
|
|
2452
2604
|
globalMiddlewares,
|
|
@@ -2458,11 +2610,11 @@ function createServer(options) {
|
|
|
2458
2610
|
});
|
|
2459
2611
|
});
|
|
2460
2612
|
if (routesRef.wsCurrent.length > 0) {
|
|
2461
|
-
attachWebSocket({ server, routesRef, rootDir, config,
|
|
2613
|
+
attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares });
|
|
2462
2614
|
}
|
|
2463
2615
|
return { server, routesRef };
|
|
2464
2616
|
}
|
|
2465
|
-
async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMiddlewares,
|
|
2617
|
+
async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
|
|
2466
2618
|
const request = toWebRequest(req, bodyLimit);
|
|
2467
2619
|
const method = request.method.toUpperCase();
|
|
2468
2620
|
const urlPath = new URL(request.url).pathname;
|
|
@@ -2490,22 +2642,13 @@ async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMi
|
|
|
2490
2642
|
}
|
|
2491
2643
|
const body = hasBody(route.method) ? result.data : void 0;
|
|
2492
2644
|
const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
|
|
2493
|
-
|
|
2645
|
+
const response = await invokeHandler(
|
|
2494
2646
|
routeModule.handler,
|
|
2495
2647
|
ctx,
|
|
2496
2648
|
body,
|
|
2497
2649
|
route.middlewares,
|
|
2498
2650
|
mergedInjectors
|
|
2499
2651
|
);
|
|
2500
|
-
if (responseFormat && response.status >= 200 && response.status < 300) {
|
|
2501
|
-
const contentType = response.headers.get("Content-Type") ?? "";
|
|
2502
|
-
if (contentType.includes("application/json")) {
|
|
2503
|
-
const data = await response.json();
|
|
2504
|
-
const formatted = responseFormat(data, ctx);
|
|
2505
|
-
const { toResponse: toResponse2 } = await Promise.resolve().then(() => (init_toResponse(), toResponse_exports));
|
|
2506
|
-
response = await toResponse2(formatted, meta);
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
2652
|
return response;
|
|
2510
2653
|
};
|
|
2511
2654
|
try {
|
|
@@ -2522,7 +2665,7 @@ async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMi
|
|
|
2522
2665
|
}
|
|
2523
2666
|
await sendNodeResponse(response, res);
|
|
2524
2667
|
} catch (err) {
|
|
2525
|
-
const errorResponse = buildErrorResponse(err
|
|
2668
|
+
const errorResponse = buildErrorResponse(err);
|
|
2526
2669
|
await sendNodeResponse(mergeMeta(errorResponse, meta), res);
|
|
2527
2670
|
if (onError) {
|
|
2528
2671
|
try {
|
|
@@ -2725,8 +2868,6 @@ var DEFAULT_PORT = 3e3;
|
|
|
2725
2868
|
var ROUTES_FILE = "faapi-routes.js";
|
|
2726
2869
|
var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2727
2870
|
"cors",
|
|
2728
|
-
"responseFormat",
|
|
2729
|
-
"errorFormat",
|
|
2730
2871
|
"lifecycle",
|
|
2731
2872
|
"middlewares",
|
|
2732
2873
|
"injectors",
|
|
@@ -2772,8 +2913,6 @@ async function createAppBase(options) {
|
|
|
2772
2913
|
appDir,
|
|
2773
2914
|
outDir,
|
|
2774
2915
|
cors: config?.cors ?? true,
|
|
2775
|
-
responseFormat: config?.responseFormat,
|
|
2776
|
-
errorFormat: config?.errorFormat,
|
|
2777
2916
|
onError: config?.lifecycle?.onError,
|
|
2778
2917
|
config: config ?? void 0,
|
|
2779
2918
|
wsRoutes,
|
|
@@ -2786,6 +2925,7 @@ async function createAppBase(options) {
|
|
|
2786
2925
|
const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
|
|
2787
2926
|
rootDir,
|
|
2788
2927
|
routes: sorted,
|
|
2928
|
+
getRoutes: () => sorted,
|
|
2789
2929
|
server,
|
|
2790
2930
|
config: pluginConfig
|
|
2791
2931
|
});
|
|
@@ -2851,6 +2991,11 @@ async function createAppBase(options) {
|
|
|
2851
2991
|
setHeader(name, value) {
|
|
2852
2992
|
this._headers[name.toLowerCase()] = value;
|
|
2853
2993
|
},
|
|
2994
|
+
appendHeader(name, value) {
|
|
2995
|
+
const key = name.toLowerCase();
|
|
2996
|
+
const existing = this._headers[key];
|
|
2997
|
+
this._headers[key] = existing ? `${existing}, ${value}` : value;
|
|
2998
|
+
},
|
|
2854
2999
|
writeHead(status, headers) {
|
|
2855
3000
|
this.statusCode = status;
|
|
2856
3001
|
if (headers) {
|
|
@@ -2892,6 +3037,7 @@ async function createAppBase(options) {
|
|
|
2892
3037
|
host: "localhost",
|
|
2893
3038
|
"content-type": body !== void 0 ? "application/json" : void 0
|
|
2894
3039
|
};
|
|
3040
|
+
mockReq.socket = { remoteAddress: "127.0.0.1" };
|
|
2895
3041
|
if (body !== void 0) {
|
|
2896
3042
|
mockReq.push(JSON.stringify(body));
|
|
2897
3043
|
}
|
|
@@ -2904,8 +3050,13 @@ async function createAppBase(options) {
|
|
|
2904
3050
|
async close() {
|
|
2905
3051
|
if (closed) return;
|
|
2906
3052
|
closed = true;
|
|
2907
|
-
server
|
|
2908
|
-
|
|
3053
|
+
const s = server;
|
|
3054
|
+
if (typeof s.closeIdleConnections === "function") {
|
|
3055
|
+
s.closeIdleConnections();
|
|
3056
|
+
}
|
|
3057
|
+
if (typeof s.closeAllConnections === "function") {
|
|
3058
|
+
s.closeAllConnections();
|
|
3059
|
+
}
|
|
2909
3060
|
if (config?.lifecycle?.onClose) {
|
|
2910
3061
|
await config.lifecycle.onClose({ rootDir, routes: sorted, server });
|
|
2911
3062
|
}
|
|
@@ -3156,7 +3307,13 @@ async function createProdApp(options) {
|
|
|
3156
3307
|
return app;
|
|
3157
3308
|
}
|
|
3158
3309
|
export {
|
|
3310
|
+
FaapiError,
|
|
3311
|
+
InternalError,
|
|
3312
|
+
MethodNotAllowedError,
|
|
3313
|
+
ModuleLoadError,
|
|
3314
|
+
RouteNotFoundError,
|
|
3159
3315
|
SchemaExtractionError,
|
|
3316
|
+
ValidationError,
|
|
3160
3317
|
collectRouteSchemaSources,
|
|
3161
3318
|
cors,
|
|
3162
3319
|
createProdApp as createApp,
|
|
@@ -3168,6 +3325,7 @@ export {
|
|
|
3168
3325
|
helmet,
|
|
3169
3326
|
invalidateProgramCache,
|
|
3170
3327
|
loadConfig,
|
|
3171
|
-
logger
|
|
3328
|
+
logger,
|
|
3329
|
+
resolveTypeNode
|
|
3172
3330
|
};
|
|
3173
3331
|
//# sourceMappingURL=index.js.map
|