adorn-api 1.0.11 → 1.0.13
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 +318 -620
- package/dist/adapter/express/auth.d.ts +5 -0
- package/dist/adapter/express/auth.d.ts.map +1 -0
- package/dist/adapter/express/bootstrap.d.ts.map +1 -1
- package/dist/adapter/express/coercion.d.ts +22 -0
- package/dist/adapter/express/coercion.d.ts.map +1 -0
- package/dist/adapter/express/index.d.ts +3 -50
- package/dist/adapter/express/index.d.ts.map +1 -1
- package/dist/adapter/express/merge.d.ts +0 -3
- package/dist/adapter/express/merge.d.ts.map +1 -1
- package/dist/adapter/express/openapi.d.ts +11 -0
- package/dist/adapter/express/openapi.d.ts.map +1 -0
- package/dist/adapter/express/router.d.ts +4 -0
- package/dist/adapter/express/router.d.ts.map +1 -0
- package/dist/adapter/express/swagger.d.ts +4 -0
- package/dist/adapter/express/swagger.d.ts.map +1 -0
- package/dist/adapter/express/types.d.ts +64 -0
- package/dist/adapter/express/types.d.ts.map +1 -0
- package/dist/adapter/express/validation.d.ts +10 -0
- package/dist/adapter/express/validation.d.ts.map +1 -0
- package/dist/cli.cjs +1003 -434
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1003 -434
- package/dist/cli.js.map +1 -1
- package/dist/compiler/analyze/scanControllers.d.ts +0 -1
- package/dist/compiler/analyze/scanControllers.d.ts.map +1 -1
- package/dist/compiler/cache/isStale.d.ts.map +1 -1
- package/dist/compiler/cache/writeCache.d.ts.map +1 -1
- package/dist/compiler/manifest/emit.d.ts.map +1 -1
- package/dist/compiler/manifest/format.d.ts +1 -1
- package/dist/compiler/manifest/format.d.ts.map +1 -1
- package/dist/compiler/schema/intersectionHandler.d.ts +7 -0
- package/dist/compiler/schema/intersectionHandler.d.ts.map +1 -0
- package/dist/compiler/schema/objectHandler.d.ts +20 -0
- package/dist/compiler/schema/objectHandler.d.ts.map +1 -0
- package/dist/compiler/schema/openapi.d.ts +1 -1
- package/dist/compiler/schema/openapi.d.ts.map +1 -1
- package/dist/compiler/schema/parameters.d.ts +18 -0
- package/dist/compiler/schema/parameters.d.ts.map +1 -0
- package/dist/compiler/schema/primitives.d.ts +10 -0
- package/dist/compiler/schema/primitives.d.ts.map +1 -0
- package/dist/compiler/schema/typeToJsonSchema.d.ts +3 -46
- package/dist/compiler/schema/typeToJsonSchema.d.ts.map +1 -1
- package/dist/compiler/schema/types.d.ts +54 -0
- package/dist/compiler/schema/types.d.ts.map +1 -0
- package/dist/compiler/schema/unionHandler.d.ts +10 -0
- package/dist/compiler/schema/unionHandler.d.ts.map +1 -0
- package/dist/decorators/index.d.ts +0 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/express.cjs +522 -502
- package/dist/express.cjs.map +1 -1
- package/dist/express.js +522 -502
- package/dist/express.js.map +1 -1
- package/dist/http.d.ts +1 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/index.cjs +3 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -34
- package/dist/index.js.map +1 -1
- package/dist/metal/applyListQuery.d.ts +27 -0
- package/dist/metal/applyListQuery.d.ts.map +1 -0
- package/dist/metal/index.cjs +61 -2
- package/dist/metal/index.cjs.map +1 -1
- package/dist/metal/index.d.ts +4 -0
- package/dist/metal/index.d.ts.map +1 -1
- package/dist/metal/index.js +57 -2
- package/dist/metal/index.js.map +1 -1
- package/dist/metal/listQuery.d.ts +7 -0
- package/dist/metal/listQuery.d.ts.map +1 -0
- package/dist/metal/queryOptions.d.ts +8 -0
- package/dist/metal/queryOptions.d.ts.map +1 -0
- package/dist/metal/registerMetalEntities.d.ts.map +1 -1
- package/dist/runtime/metadata/types.d.ts +0 -3
- package/dist/runtime/metadata/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/compiler/analyze/extractQueryStyle.d.ts +0 -8
- package/dist/compiler/analyze/extractQueryStyle.d.ts.map +0 -1
- package/dist/decorators/Paginated.d.ts +0 -5
- package/dist/decorators/Paginated.d.ts.map +0 -1
package/dist/express.js
CHANGED
|
@@ -8,10 +8,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
// src/runtime/polyfill.ts
|
|
9
9
|
Symbol.metadata ??= /* @__PURE__ */ Symbol("Symbol.metadata");
|
|
10
10
|
|
|
11
|
-
// src/adapter/express/
|
|
11
|
+
// src/adapter/express/router.ts
|
|
12
12
|
import { Router } from "express";
|
|
13
|
-
import {
|
|
14
|
-
import { resolve, join, isAbsolute } from "path";
|
|
13
|
+
import { join } from "path";
|
|
15
14
|
|
|
16
15
|
// src/runtime/metadata/key.ts
|
|
17
16
|
var ADORN_META = /* @__PURE__ */ Symbol.for("adorn-api/meta");
|
|
@@ -122,8 +121,7 @@ function computeBoundRoutes(controllers, manifest) {
|
|
|
122
121
|
responses: manifestOp.responses,
|
|
123
122
|
use: routeOp.use,
|
|
124
123
|
auth: routeOp.auth,
|
|
125
|
-
controllerUse: bucket.controllerUse
|
|
126
|
-
paginationConfig: routeOp.pagination
|
|
124
|
+
controllerUse: bucket.controllerUse
|
|
127
125
|
});
|
|
128
126
|
}
|
|
129
127
|
}
|
|
@@ -183,7 +181,7 @@ async function getMtime(filePath) {
|
|
|
183
181
|
async function loadArtifacts(options) {
|
|
184
182
|
const { outDir } = options;
|
|
185
183
|
const cacheKey = path.resolve(outDir);
|
|
186
|
-
|
|
184
|
+
const entry = artifactCache.get(cacheKey);
|
|
187
185
|
const openapiPath = path.join(outDir, "openapi.json");
|
|
188
186
|
const manifestPath = path.join(outDir, "manifest.json");
|
|
189
187
|
const validatorsPath = path.join(outDir, "validators.mjs");
|
|
@@ -232,351 +230,7 @@ async function loadArtifacts(options) {
|
|
|
232
230
|
};
|
|
233
231
|
}
|
|
234
232
|
|
|
235
|
-
// src/adapter/express/
|
|
236
|
-
import swaggerUi from "swagger-ui-express";
|
|
237
|
-
|
|
238
|
-
// src/adapter/express/bootstrap.ts
|
|
239
|
-
import express from "express";
|
|
240
|
-
import path2 from "path";
|
|
241
|
-
function bootstrap(options) {
|
|
242
|
-
return new Promise(async (resolve2, reject) => {
|
|
243
|
-
try {
|
|
244
|
-
const {
|
|
245
|
-
controllers,
|
|
246
|
-
port: userPort,
|
|
247
|
-
host: userHost,
|
|
248
|
-
artifactsDir: userArtifactsDir = ".adorn",
|
|
249
|
-
enableSwagger = true,
|
|
250
|
-
swaggerPath = "/docs",
|
|
251
|
-
swaggerJsonPath = "/docs/openapi.json",
|
|
252
|
-
middleware,
|
|
253
|
-
auth,
|
|
254
|
-
coerce
|
|
255
|
-
} = options;
|
|
256
|
-
if (controllers.length === 0) {
|
|
257
|
-
reject(new Error("At least one controller must be provided to bootstrap()."));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const envPort = process.env.PORT;
|
|
261
|
-
const port = userPort ?? (envPort !== void 0 ? Number(envPort) : 3e3);
|
|
262
|
-
const host = userHost ?? process.env.HOST ?? "0.0.0.0";
|
|
263
|
-
if (isNaN(port) || port < 0 || port > 65535) {
|
|
264
|
-
reject(new Error(`Invalid port: ${port}. Port must be between 0 and 65535.`));
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const absoluteArtifactsDir = path2.isAbsolute(userArtifactsDir) ? userArtifactsDir : path2.resolve(process.cwd(), userArtifactsDir);
|
|
268
|
-
const app = express();
|
|
269
|
-
app.use(express.json());
|
|
270
|
-
const router = await createExpressRouter({
|
|
271
|
-
controllers,
|
|
272
|
-
artifactsDir: absoluteArtifactsDir,
|
|
273
|
-
middleware,
|
|
274
|
-
auth,
|
|
275
|
-
coerce
|
|
276
|
-
});
|
|
277
|
-
app.use(router);
|
|
278
|
-
if (enableSwagger) {
|
|
279
|
-
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
280
|
-
const serverUrl = `http://${displayHost}:${port}`;
|
|
281
|
-
app.use(
|
|
282
|
-
setupSwagger({
|
|
283
|
-
artifactsDir: absoluteArtifactsDir,
|
|
284
|
-
jsonPath: swaggerJsonPath,
|
|
285
|
-
uiPath: swaggerPath,
|
|
286
|
-
swaggerOptions: {
|
|
287
|
-
servers: [{ url: serverUrl }]
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
const server = app.listen(port, host, () => {
|
|
293
|
-
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
294
|
-
const serverUrl = `http://${displayHost}:${port}`;
|
|
295
|
-
console.log(`\u{1F680} Server running on ${serverUrl}`);
|
|
296
|
-
if (enableSwagger) {
|
|
297
|
-
console.log(`\u{1F4DA} Swagger UI: ${serverUrl}${swaggerPath}`);
|
|
298
|
-
}
|
|
299
|
-
const result = {
|
|
300
|
-
server,
|
|
301
|
-
app,
|
|
302
|
-
url: serverUrl,
|
|
303
|
-
port,
|
|
304
|
-
host,
|
|
305
|
-
close: () => new Promise((closeResolve) => {
|
|
306
|
-
server.close(() => {
|
|
307
|
-
console.log("Server closed gracefully");
|
|
308
|
-
closeResolve();
|
|
309
|
-
});
|
|
310
|
-
})
|
|
311
|
-
};
|
|
312
|
-
resolve2(result);
|
|
313
|
-
});
|
|
314
|
-
server.on("error", (error) => {
|
|
315
|
-
if (error.code === "EADDRINUSE") {
|
|
316
|
-
reject(new Error(`Port ${port} already in use. Please choose a different port.`));
|
|
317
|
-
} else if (error.code === "EACCES") {
|
|
318
|
-
reject(new Error(`Permission denied for port ${port}. Ports below 1024 require root privileges.`));
|
|
319
|
-
} else {
|
|
320
|
-
reject(new Error(`Failed to start server: ${error.message}`));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
} catch (error) {
|
|
324
|
-
reject(error);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// src/adapter/express/index.ts
|
|
330
|
-
function normalizeCoerceOptions(coerce) {
|
|
331
|
-
return {
|
|
332
|
-
body: coerce?.body ?? false,
|
|
333
|
-
query: coerce?.query ?? false,
|
|
334
|
-
path: coerce?.path ?? false,
|
|
335
|
-
header: coerce?.header ?? false,
|
|
336
|
-
cookie: coerce?.cookie ?? false,
|
|
337
|
-
dateTime: coerce?.dateTime ?? false,
|
|
338
|
-
date: coerce?.date ?? false
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
async function createExpressRouter(options) {
|
|
342
|
-
const { controllers, artifactsDir = ".adorn", middleware = {} } = options;
|
|
343
|
-
let manifest;
|
|
344
|
-
let openapi;
|
|
345
|
-
let precompiledValidators = null;
|
|
346
|
-
if (options.manifest && options.openapi) {
|
|
347
|
-
manifest = options.manifest;
|
|
348
|
-
openapi = options.openapi;
|
|
349
|
-
if (manifest.validation.mode === "precompiled" && manifest.validation.precompiledModule) {
|
|
350
|
-
try {
|
|
351
|
-
const validatorPath = join(artifactsDir, manifest.validation.precompiledModule);
|
|
352
|
-
precompiledValidators = __require(validatorPath).validators;
|
|
353
|
-
} catch (err) {
|
|
354
|
-
console.warn(`Failed to load precompiled validators: ${err}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} else {
|
|
358
|
-
const artifacts = await loadArtifacts({ outDir: artifactsDir });
|
|
359
|
-
manifest = artifacts.manifest;
|
|
360
|
-
openapi = artifacts.openapi;
|
|
361
|
-
precompiledValidators = artifacts.validators?.validators ?? null;
|
|
362
|
-
}
|
|
363
|
-
const routes = bindRoutes({ controllers, manifest });
|
|
364
|
-
const validator = precompiledValidators ? null : createValidator();
|
|
365
|
-
const router = Router();
|
|
366
|
-
const instanceCache = /* @__PURE__ */ new Map();
|
|
367
|
-
const coerce = normalizeCoerceOptions(options.coerce);
|
|
368
|
-
function getInstance(Ctor) {
|
|
369
|
-
if (!instanceCache.has(Ctor)) {
|
|
370
|
-
instanceCache.set(Ctor, new Ctor());
|
|
371
|
-
}
|
|
372
|
-
return instanceCache.get(Ctor);
|
|
373
|
-
}
|
|
374
|
-
function resolveMiddleware(items, named = {}) {
|
|
375
|
-
return items.map((item) => {
|
|
376
|
-
if (typeof item === "string") {
|
|
377
|
-
const fn = named[item];
|
|
378
|
-
if (!fn) {
|
|
379
|
-
throw new Error(`Named middleware "${item}" not found in middleware registry`);
|
|
380
|
-
}
|
|
381
|
-
return fn;
|
|
382
|
-
}
|
|
383
|
-
return item;
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
function createAuthMiddleware(authConfig, routeAuth, globalSecurity) {
|
|
387
|
-
return async (req, res, next) => {
|
|
388
|
-
const isPublic = routeAuth === "public";
|
|
389
|
-
const hasAuthDecorator = routeAuth && routeAuth !== "public";
|
|
390
|
-
const hasGlobalSecurity = globalSecurity && globalSecurity.length > 0;
|
|
391
|
-
if (!hasAuthDecorator && !hasGlobalSecurity) {
|
|
392
|
-
return next();
|
|
393
|
-
}
|
|
394
|
-
if (isPublic) {
|
|
395
|
-
return next();
|
|
396
|
-
}
|
|
397
|
-
const authMeta = routeAuth;
|
|
398
|
-
const scheme = authMeta.scheme;
|
|
399
|
-
const requiredScopes = authMeta.scopes || [];
|
|
400
|
-
const isOptional = authMeta.optional ?? false;
|
|
401
|
-
const authRuntime = authConfig.schemes[scheme];
|
|
402
|
-
if (!authRuntime) {
|
|
403
|
-
throw new Error(`Auth scheme "${scheme}" not found in auth configuration`);
|
|
404
|
-
}
|
|
405
|
-
const result = await authRuntime.authenticate(req);
|
|
406
|
-
if (!result) {
|
|
407
|
-
if (isOptional) {
|
|
408
|
-
req.auth = null;
|
|
409
|
-
return next();
|
|
410
|
-
}
|
|
411
|
-
return authRuntime.challenge(res);
|
|
412
|
-
}
|
|
413
|
-
req.auth = result.principal;
|
|
414
|
-
if (authRuntime.authorize && requiredScopes.length > 0) {
|
|
415
|
-
if (!authRuntime.authorize(result, requiredScopes)) {
|
|
416
|
-
res.status(403).json({ error: "Forbidden", message: "Insufficient scopes" });
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
next();
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
function getSchemaByRef(ref) {
|
|
424
|
-
if (!ref.startsWith("#/components/schemas/")) return null;
|
|
425
|
-
const schemaName = ref.replace("#/components/schemas/", "");
|
|
426
|
-
return openapi.components.schemas[schemaName] || null;
|
|
427
|
-
}
|
|
428
|
-
for (const route of routes) {
|
|
429
|
-
const method = route.httpMethod.toLowerCase();
|
|
430
|
-
const openapiOperation = getOpenApiOperation(openapi, route);
|
|
431
|
-
const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
|
|
432
|
-
const bodySchema = getRequestBodySchema(openapiOperation, route.args.body?.contentType) ?? (route.args.body ? getSchemaByRef(route.args.body.schemaRef) : null);
|
|
433
|
-
const coerceBodyDates = getDateCoercionOptions(coerce, "body");
|
|
434
|
-
const coerceQueryDates = getDateCoercionOptions(coerce, "query");
|
|
435
|
-
const coercePathDates = getDateCoercionOptions(coerce, "path");
|
|
436
|
-
const coerceHeaderDates = getDateCoercionOptions(coerce, "header");
|
|
437
|
-
const coerceCookieDates = getDateCoercionOptions(coerce, "cookie");
|
|
438
|
-
const middlewareChain = [];
|
|
439
|
-
if (middleware.global) {
|
|
440
|
-
middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
|
|
441
|
-
}
|
|
442
|
-
if (route.controllerUse) {
|
|
443
|
-
middlewareChain.push(...resolveMiddleware(route.controllerUse, middleware.named || {}));
|
|
444
|
-
}
|
|
445
|
-
if (route.use) {
|
|
446
|
-
middlewareChain.push(...resolveMiddleware(route.use, middleware.named || {}));
|
|
447
|
-
}
|
|
448
|
-
if (options.auth) {
|
|
449
|
-
const authMw = createAuthMiddleware(options.auth, route.auth, openapi.security || []);
|
|
450
|
-
middlewareChain.push(authMw);
|
|
451
|
-
}
|
|
452
|
-
router[method](route.fullPath, ...middlewareChain, async (req, res, next) => {
|
|
453
|
-
try {
|
|
454
|
-
const validationErrors = precompiledValidators ? validateRequestWithPrecompiled(route, req, precompiledValidators) : validateRequest(route, req, openapi, validator);
|
|
455
|
-
if (validationErrors) {
|
|
456
|
-
return res.status(400).json(formatValidationErrors(validationErrors));
|
|
457
|
-
}
|
|
458
|
-
const instance = getInstance(route.controllerCtor);
|
|
459
|
-
const handler = instance[route.methodName];
|
|
460
|
-
if (typeof handler !== "function") {
|
|
461
|
-
throw new Error(`Method ${route.methodName} not found on controller`);
|
|
462
|
-
}
|
|
463
|
-
const args = [];
|
|
464
|
-
if (route.args.body) {
|
|
465
|
-
const coercedBody = (coerceBodyDates.date || coerceBodyDates.dateTime) && bodySchema ? coerceDatesWithSchema(req.body, bodySchema, coerceBodyDates, openapi.components.schemas) : req.body;
|
|
466
|
-
args[route.args.body.index] = coercedBody;
|
|
467
|
-
}
|
|
468
|
-
for (const pathArg of route.args.path) {
|
|
469
|
-
const rawValue = req.params[pathArg.name];
|
|
470
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", pathArg.name) ?? (pathArg.schemaRef ? getSchemaByRef(pathArg.schemaRef) : null) ?? schemaFromType(pathArg.schemaType);
|
|
471
|
-
const coerced = coerceParamValue(rawValue, paramSchema, coercePathDates, openapi.components.schemas);
|
|
472
|
-
args[pathArg.index] = coerced;
|
|
473
|
-
}
|
|
474
|
-
if (route.args.query.length > 0) {
|
|
475
|
-
const deepObjectArgs = route.args.query.filter((q) => q.serialization?.style === "deepObject");
|
|
476
|
-
const standardArgs = route.args.query.filter((q) => q.serialization?.style !== "deepObject");
|
|
477
|
-
if (deepObjectArgs.length > 0) {
|
|
478
|
-
const rawQuery = getRawQueryString(req);
|
|
479
|
-
const names = new Set(deepObjectArgs.map((q) => q.name));
|
|
480
|
-
const parsedDeep = parseDeepObjectParams(rawQuery, names);
|
|
481
|
-
for (const q of deepObjectArgs) {
|
|
482
|
-
const rawValue = parsedDeep[q.name];
|
|
483
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
|
|
484
|
-
const baseValue = rawValue === void 0 ? {} : rawValue;
|
|
485
|
-
const coerced = coerceParamValue(baseValue, paramSchema, coerceQueryDates, openapi.components.schemas);
|
|
486
|
-
args[q.index] = coerced;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (standardArgs.length > 0) {
|
|
490
|
-
const firstQueryIndex = standardArgs[0].index;
|
|
491
|
-
const allSameIndex = standardArgs.every((q) => q.index === firstQueryIndex);
|
|
492
|
-
if (allSameIndex) {
|
|
493
|
-
args[firstQueryIndex] = {};
|
|
494
|
-
for (const q of standardArgs) {
|
|
495
|
-
const parsed = parseQueryValue(req.query[q.name], q);
|
|
496
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
|
|
497
|
-
const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
|
|
498
|
-
args[firstQueryIndex][q.name] = coerced;
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
for (const q of standardArgs) {
|
|
502
|
-
const parsed = parseQueryValue(req.query[q.name], q);
|
|
503
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
|
|
504
|
-
const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
|
|
505
|
-
args[q.index] = coerced;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (route.args.paginationParamIndex !== null) {
|
|
511
|
-
const pageStr = req.query.page;
|
|
512
|
-
const pageSizeStr = req.query.pageSize;
|
|
513
|
-
const defaultPageSize = route.paginationConfig?.defaultPageSize ?? 10;
|
|
514
|
-
const page = pageStr ? parseInt(pageStr, 10) : 1;
|
|
515
|
-
const pageSize = pageSizeStr ? parseInt(pageSizeStr, 10) : defaultPageSize;
|
|
516
|
-
args[route.args.paginationParamIndex] = { page, pageSize };
|
|
517
|
-
}
|
|
518
|
-
if (route.args.headers.length > 0) {
|
|
519
|
-
const firstHeaderIndex = route.args.headers[0].index;
|
|
520
|
-
const allSameIndex = route.args.headers.every((h) => h.index === firstHeaderIndex);
|
|
521
|
-
if (allSameIndex) {
|
|
522
|
-
args[firstHeaderIndex] = {};
|
|
523
|
-
for (const h of route.args.headers) {
|
|
524
|
-
const headerValue = req.headers[h.name.toLowerCase()];
|
|
525
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
|
|
526
|
-
const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
|
|
527
|
-
args[firstHeaderIndex][h.name] = coerced ?? void 0;
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
for (const h of route.args.headers) {
|
|
531
|
-
const headerValue = req.headers[h.name.toLowerCase()];
|
|
532
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
|
|
533
|
-
const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
|
|
534
|
-
args[h.index] = coerced ?? void 0;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (route.args.cookies.length > 0) {
|
|
539
|
-
const firstCookieIndex = route.args.cookies[0].index;
|
|
540
|
-
const allSameIndex = route.args.cookies.every((c) => c.index === firstCookieIndex);
|
|
541
|
-
const cookies = parseCookies(req.headers.cookie);
|
|
542
|
-
if (allSameIndex) {
|
|
543
|
-
args[firstCookieIndex] = {};
|
|
544
|
-
for (const c of route.args.cookies) {
|
|
545
|
-
const cookieValue = cookies[c.name];
|
|
546
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
|
|
547
|
-
const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
|
|
548
|
-
args[firstCookieIndex][c.name] = coerced;
|
|
549
|
-
}
|
|
550
|
-
} else {
|
|
551
|
-
for (const c of route.args.cookies) {
|
|
552
|
-
const cookieValue = cookies[c.name];
|
|
553
|
-
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
|
|
554
|
-
const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
|
|
555
|
-
args[c.index] = coerced;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (args.length === 0) {
|
|
560
|
-
args.push(req);
|
|
561
|
-
}
|
|
562
|
-
const result = await handler.apply(instance, args);
|
|
563
|
-
const primaryResponse = route.responses[0];
|
|
564
|
-
const status = primaryResponse?.status ?? 200;
|
|
565
|
-
res.status(status).json(result);
|
|
566
|
-
} catch (error) {
|
|
567
|
-
next(error);
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
return router;
|
|
572
|
-
}
|
|
573
|
-
function getDateCoercionOptions(coerce, location) {
|
|
574
|
-
const enabled = coerce[location];
|
|
575
|
-
return {
|
|
576
|
-
dateTime: enabled && coerce.dateTime,
|
|
577
|
-
date: enabled && coerce.date
|
|
578
|
-
};
|
|
579
|
-
}
|
|
233
|
+
// src/adapter/express/openapi.ts
|
|
580
234
|
function toOpenApiPath(path3) {
|
|
581
235
|
return path3.replace(/:([^/]+)/g, "{$1}");
|
|
582
236
|
}
|
|
@@ -586,7 +240,7 @@ function getOpenApiOperation(openapi, route) {
|
|
|
586
240
|
if (!pathItem) return null;
|
|
587
241
|
return pathItem[route.httpMethod.toLowerCase()] ?? null;
|
|
588
242
|
}
|
|
589
|
-
function
|
|
243
|
+
function getParamSchemaIndex(operation) {
|
|
590
244
|
const index = /* @__PURE__ */ new Map();
|
|
591
245
|
const params = operation?.parameters ?? [];
|
|
592
246
|
for (const param of params) {
|
|
@@ -613,155 +267,6 @@ function schemaFromType(schemaType) {
|
|
|
613
267
|
if (!schemaType) return null;
|
|
614
268
|
return { type: schemaType };
|
|
615
269
|
}
|
|
616
|
-
function validateRequestWithPrecompiled(route, req, validators) {
|
|
617
|
-
const errors = [];
|
|
618
|
-
const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
|
|
619
|
-
const deepValues = deepNames.size > 0 ? parseDeepObjectParams(getRawQueryString(req), deepNames) : {};
|
|
620
|
-
if (route.args.body) {
|
|
621
|
-
const validator = validators[route.operationId]?.body;
|
|
622
|
-
if (validator) {
|
|
623
|
-
const valid = validator(req.body);
|
|
624
|
-
if (!valid) {
|
|
625
|
-
const v = validators[route.operationId].body;
|
|
626
|
-
for (const err of v.errors || []) {
|
|
627
|
-
errors.push({
|
|
628
|
-
path: `#/body${err.instancePath}`,
|
|
629
|
-
message: err.message || "Invalid value",
|
|
630
|
-
keyword: err.keyword,
|
|
631
|
-
params: err.params
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
for (const q of route.args.query) {
|
|
638
|
-
const value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
|
|
639
|
-
if (value === void 0) continue;
|
|
640
|
-
const schema = schemaFromType(q.schemaType) ?? {};
|
|
641
|
-
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
|
|
642
|
-
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
643
|
-
errors.push({
|
|
644
|
-
path: `#/query/${q.name}`,
|
|
645
|
-
message: `Schema validation not supported for query params in precompiled mode`,
|
|
646
|
-
keyword: "notSupported",
|
|
647
|
-
params: {}
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
for (const p of route.args.path) {
|
|
652
|
-
const value = req.params[p.name];
|
|
653
|
-
if (value === void 0) continue;
|
|
654
|
-
const schema = schemaFromType(p.schemaType) ?? {};
|
|
655
|
-
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
|
|
656
|
-
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
657
|
-
errors.push({
|
|
658
|
-
path: `#/path/${p.name}`,
|
|
659
|
-
message: `Schema validation not supported for path params in precompiled mode`,
|
|
660
|
-
keyword: "notSupported",
|
|
661
|
-
params: {}
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return errors.length > 0 ? errors : null;
|
|
666
|
-
}
|
|
667
|
-
function validateRequest(route, req, openapi, validator) {
|
|
668
|
-
function getSchemaByRef(ref) {
|
|
669
|
-
if (!ref.startsWith("#/components/schemas/")) return null;
|
|
670
|
-
const schemaName = ref.replace("#/components/schemas/", "");
|
|
671
|
-
return openapi.components.schemas[schemaName] || null;
|
|
672
|
-
}
|
|
673
|
-
const openapiOperation = getOpenApiOperation(openapi, route);
|
|
674
|
-
const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
|
|
675
|
-
const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
|
|
676
|
-
const deepValues = deepNames.size > 0 ? parseDeepObjectParams(getRawQueryString(req), deepNames) : {};
|
|
677
|
-
const errors = [];
|
|
678
|
-
if (route.args.body) {
|
|
679
|
-
const bodySchema = getSchemaByRef(route.args.body.schemaRef);
|
|
680
|
-
if (bodySchema) {
|
|
681
|
-
const validate = validator.compile(bodySchema);
|
|
682
|
-
const valid = validate(req.body);
|
|
683
|
-
if (!valid) {
|
|
684
|
-
for (const err of validate.errors || []) {
|
|
685
|
-
errors.push({
|
|
686
|
-
path: `#/body${err.instancePath}`,
|
|
687
|
-
message: err.message || "Invalid value",
|
|
688
|
-
keyword: err.keyword,
|
|
689
|
-
params: err.params
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
for (const q of route.args.query) {
|
|
696
|
-
const value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
|
|
697
|
-
const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name);
|
|
698
|
-
let schema = {};
|
|
699
|
-
if (openapiSchema) {
|
|
700
|
-
schema = resolveSchema(openapiSchema, openapi.components.schemas);
|
|
701
|
-
} else {
|
|
702
|
-
if (q.schemaType) {
|
|
703
|
-
const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
|
|
704
|
-
schema.type = type;
|
|
705
|
-
}
|
|
706
|
-
if (q.schemaRef && q.schemaRef.includes("Inline")) {
|
|
707
|
-
const inlineSchema = getSchemaByRef(q.schemaRef);
|
|
708
|
-
if (inlineSchema) {
|
|
709
|
-
Object.assign(schema, inlineSchema);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
|
|
714
|
-
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
715
|
-
const validate = validator.compile(schema);
|
|
716
|
-
const valid = validate(coerced);
|
|
717
|
-
if (!valid) {
|
|
718
|
-
for (const err of validate.errors || []) {
|
|
719
|
-
errors.push({
|
|
720
|
-
path: `#/query/${q.name}`,
|
|
721
|
-
message: err.message || "Invalid value",
|
|
722
|
-
keyword: err.keyword,
|
|
723
|
-
params: err.params
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
for (const p of route.args.path) {
|
|
730
|
-
const value = req.params[p.name];
|
|
731
|
-
const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", p.name);
|
|
732
|
-
let schema = {};
|
|
733
|
-
if (openapiSchema) {
|
|
734
|
-
schema = resolveSchema(openapiSchema, openapi.components.schemas);
|
|
735
|
-
} else {
|
|
736
|
-
if (p.schemaType) {
|
|
737
|
-
const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
|
|
738
|
-
schema.type = type;
|
|
739
|
-
}
|
|
740
|
-
if (p.schemaRef && p.schemaRef.includes("Inline")) {
|
|
741
|
-
const inlineSchema = getSchemaByRef(p.schemaRef);
|
|
742
|
-
if (inlineSchema) {
|
|
743
|
-
Object.assign(schema, inlineSchema);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
|
|
748
|
-
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
749
|
-
const validate = validator.compile(schema);
|
|
750
|
-
const valid = validate(coerced);
|
|
751
|
-
if (!valid) {
|
|
752
|
-
for (const err of validate.errors || []) {
|
|
753
|
-
errors.push({
|
|
754
|
-
path: `#/path/${p.name}`,
|
|
755
|
-
message: err.message || "Invalid value",
|
|
756
|
-
keyword: err.keyword,
|
|
757
|
-
params: err.params
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return errors.length > 0 ? errors : null;
|
|
764
|
-
}
|
|
765
270
|
function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
|
|
766
271
|
const ref = schema.$ref;
|
|
767
272
|
if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
|
|
@@ -774,6 +279,31 @@ function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
|
|
|
774
279
|
seen.add(name);
|
|
775
280
|
return resolveSchema(next, components, seen);
|
|
776
281
|
}
|
|
282
|
+
function getSchemaByRef(openapi, ref) {
|
|
283
|
+
if (!ref.startsWith("#/components/schemas/")) return null;
|
|
284
|
+
const schemaName = ref.replace("#/components/schemas/", "");
|
|
285
|
+
return openapi.components.schemas[schemaName] || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/adapter/express/coercion.ts
|
|
289
|
+
function normalizeCoerceOptions(coerce) {
|
|
290
|
+
return {
|
|
291
|
+
body: coerce?.body ?? false,
|
|
292
|
+
query: coerce?.query ?? false,
|
|
293
|
+
path: coerce?.path ?? false,
|
|
294
|
+
header: coerce?.header ?? false,
|
|
295
|
+
cookie: coerce?.cookie ?? false,
|
|
296
|
+
dateTime: coerce?.dateTime ?? false,
|
|
297
|
+
date: coerce?.date ?? false
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function getDateCoercionOptions(coerce, location) {
|
|
301
|
+
const enabled = coerce[location];
|
|
302
|
+
return {
|
|
303
|
+
dateTime: enabled && coerce.dateTime,
|
|
304
|
+
date: enabled && coerce.date
|
|
305
|
+
};
|
|
306
|
+
}
|
|
777
307
|
function coerceDatesWithSchema(value, schema, dateCoercion, components) {
|
|
778
308
|
if (!schema || !dateCoercion.date && !dateCoercion.dateTime) return value;
|
|
779
309
|
return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: false });
|
|
@@ -981,6 +511,408 @@ function parseCookies(cookieHeader) {
|
|
|
981
511
|
}
|
|
982
512
|
return cookies;
|
|
983
513
|
}
|
|
514
|
+
function normalizeSort(sort) {
|
|
515
|
+
if (Array.isArray(sort)) {
|
|
516
|
+
return sort.map((s) => String(s));
|
|
517
|
+
}
|
|
518
|
+
if (typeof sort === "string") {
|
|
519
|
+
return sort.split(",").map((s) => s.trim()).filter(Boolean);
|
|
520
|
+
}
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/adapter/express/auth.ts
|
|
525
|
+
function createAuthMiddleware(authConfig, routeAuth, globalSecurity) {
|
|
526
|
+
return async (req, res, next) => {
|
|
527
|
+
const isPublic = routeAuth === "public";
|
|
528
|
+
const hasAuthDecorator = routeAuth && routeAuth !== "public";
|
|
529
|
+
const hasGlobalSecurity = globalSecurity && globalSecurity.length > 0;
|
|
530
|
+
if (!hasAuthDecorator && !hasGlobalSecurity) {
|
|
531
|
+
return next();
|
|
532
|
+
}
|
|
533
|
+
if (isPublic) {
|
|
534
|
+
return next();
|
|
535
|
+
}
|
|
536
|
+
const authMeta = routeAuth;
|
|
537
|
+
const scheme = authMeta.scheme;
|
|
538
|
+
const requiredScopes = authMeta.scopes || [];
|
|
539
|
+
const isOptional = authMeta.optional ?? false;
|
|
540
|
+
const authRuntime = authConfig.schemes[scheme];
|
|
541
|
+
if (!authRuntime) {
|
|
542
|
+
throw new Error(`Auth scheme "${scheme}" not found in auth configuration`);
|
|
543
|
+
}
|
|
544
|
+
const result = await authRuntime.authenticate(req);
|
|
545
|
+
if (!result) {
|
|
546
|
+
if (isOptional) {
|
|
547
|
+
req.auth = null;
|
|
548
|
+
return next();
|
|
549
|
+
}
|
|
550
|
+
return authRuntime.challenge(res);
|
|
551
|
+
}
|
|
552
|
+
req.auth = result.principal;
|
|
553
|
+
if (authRuntime.authorize && requiredScopes.length > 0) {
|
|
554
|
+
if (!authRuntime.authorize(result, requiredScopes)) {
|
|
555
|
+
res.status(403).json({ error: "Forbidden", message: "Insufficient scopes" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
next();
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/adapter/express/validation.ts
|
|
564
|
+
function validateRequestWithPrecompiled(route, req, validators) {
|
|
565
|
+
const errors = [];
|
|
566
|
+
const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
|
|
567
|
+
const deepValues = deepNames.size > 0 ? parseDeepObjectParams(getRawQueryString(req), deepNames) : {};
|
|
568
|
+
if (route.args.body) {
|
|
569
|
+
const validator = validators[route.operationId]?.body;
|
|
570
|
+
if (validator) {
|
|
571
|
+
const valid = validator(req.body);
|
|
572
|
+
if (!valid) {
|
|
573
|
+
const v = validators[route.operationId].body;
|
|
574
|
+
for (const err of v.errors || []) {
|
|
575
|
+
errors.push({
|
|
576
|
+
path: `#/body${err.instancePath}`,
|
|
577
|
+
message: err.message || "Invalid value",
|
|
578
|
+
keyword: err.keyword,
|
|
579
|
+
params: err.params
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const q of route.args.query) {
|
|
586
|
+
let value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
|
|
587
|
+
if (q.content === "application/json" && typeof value === "string") {
|
|
588
|
+
try {
|
|
589
|
+
value = JSON.parse(value);
|
|
590
|
+
} catch {
|
|
591
|
+
errors.push({
|
|
592
|
+
path: `#/query/${q.name}`,
|
|
593
|
+
message: "Invalid JSON string",
|
|
594
|
+
keyword: "json",
|
|
595
|
+
params: {}
|
|
596
|
+
});
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return errors.length > 0 ? errors : null;
|
|
602
|
+
}
|
|
603
|
+
function validateRequest(route, req, openapi, validator) {
|
|
604
|
+
const openapiOperation = getOpenApiOperation(openapi, route);
|
|
605
|
+
const paramSchemaIndex = getParamSchemaIndex(openapiOperation);
|
|
606
|
+
const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
|
|
607
|
+
const deepValues = deepNames.size > 0 ? parseDeepObjectParams(getRawQueryString(req), deepNames) : {};
|
|
608
|
+
const errors = [];
|
|
609
|
+
if (route.args.body) {
|
|
610
|
+
const bodySchema = getSchemaByRef(openapi, route.args.body.schemaRef);
|
|
611
|
+
if (bodySchema) {
|
|
612
|
+
const validate = validator.compile(bodySchema);
|
|
613
|
+
const valid = validate(req.body);
|
|
614
|
+
if (!valid) {
|
|
615
|
+
for (const err of validate.errors || []) {
|
|
616
|
+
errors.push({
|
|
617
|
+
path: `#/body${err.instancePath}`,
|
|
618
|
+
message: err.message || "Invalid value",
|
|
619
|
+
keyword: err.keyword,
|
|
620
|
+
params: err.params
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const q of route.args.query) {
|
|
627
|
+
let value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
|
|
628
|
+
if (q.content === "application/json" && typeof value === "string") {
|
|
629
|
+
try {
|
|
630
|
+
value = JSON.parse(value);
|
|
631
|
+
} catch {
|
|
632
|
+
errors.push({
|
|
633
|
+
path: `#/query/${q.name}`,
|
|
634
|
+
message: "Invalid JSON string",
|
|
635
|
+
keyword: "json",
|
|
636
|
+
params: {}
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name);
|
|
642
|
+
let schema = {};
|
|
643
|
+
if (openapiSchema) {
|
|
644
|
+
schema = resolveSchema(openapiSchema, openapi.components.schemas);
|
|
645
|
+
} else {
|
|
646
|
+
if (q.schemaType) {
|
|
647
|
+
const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
|
|
648
|
+
schema.type = type;
|
|
649
|
+
}
|
|
650
|
+
if (q.schemaRef && q.schemaRef.includes("Inline")) {
|
|
651
|
+
const inlineSchema = getSchemaByRef(openapi, q.schemaRef);
|
|
652
|
+
if (inlineSchema) {
|
|
653
|
+
Object.assign(schema, inlineSchema);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
|
|
658
|
+
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
659
|
+
const validate = validator.compile(schema);
|
|
660
|
+
const valid = validate(coerced);
|
|
661
|
+
if (!valid) {
|
|
662
|
+
for (const err of validate.errors || []) {
|
|
663
|
+
errors.push({
|
|
664
|
+
path: `#/query/${q.name}`,
|
|
665
|
+
message: err.message || "Invalid value",
|
|
666
|
+
keyword: err.keyword,
|
|
667
|
+
params: err.params
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const p of route.args.path) {
|
|
674
|
+
const value = req.params[p.name];
|
|
675
|
+
const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", p.name);
|
|
676
|
+
let schema = {};
|
|
677
|
+
if (openapiSchema) {
|
|
678
|
+
schema = resolveSchema(openapiSchema, openapi.components.schemas);
|
|
679
|
+
} else {
|
|
680
|
+
if (p.schemaType) {
|
|
681
|
+
const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
|
|
682
|
+
schema.type = type;
|
|
683
|
+
}
|
|
684
|
+
if (p.schemaRef && p.schemaRef.includes("Inline")) {
|
|
685
|
+
const inlineSchema = getSchemaByRef(openapi, p.schemaRef);
|
|
686
|
+
if (inlineSchema) {
|
|
687
|
+
Object.assign(schema, inlineSchema);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
|
|
692
|
+
if (Object.keys(schema).length > 0 && coerced !== void 0) {
|
|
693
|
+
const validate = validator.compile(schema);
|
|
694
|
+
const valid = validate(coerced);
|
|
695
|
+
if (!valid) {
|
|
696
|
+
for (const err of validate.errors || []) {
|
|
697
|
+
errors.push({
|
|
698
|
+
path: `#/path/${p.name}`,
|
|
699
|
+
message: err.message || "Invalid value",
|
|
700
|
+
keyword: err.keyword,
|
|
701
|
+
params: err.params
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return errors.length > 0 ? errors : null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/adapter/express/router.ts
|
|
711
|
+
async function createExpressRouter(options) {
|
|
712
|
+
const { controllers, artifactsDir = ".adorn", middleware = {}, defaultPageSize = 10 } = options;
|
|
713
|
+
let manifest;
|
|
714
|
+
let openapi;
|
|
715
|
+
let precompiledValidators = null;
|
|
716
|
+
if (options.manifest && options.openapi) {
|
|
717
|
+
manifest = options.manifest;
|
|
718
|
+
openapi = options.openapi;
|
|
719
|
+
if (manifest.validation.mode === "precompiled" && manifest.validation.precompiledModule) {
|
|
720
|
+
try {
|
|
721
|
+
const validatorPath = join(artifactsDir, manifest.validation.precompiledModule);
|
|
722
|
+
precompiledValidators = __require(validatorPath).validators;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
console.warn(`Failed to load precompiled validators: ${err}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
const artifacts = await loadArtifacts({ outDir: artifactsDir });
|
|
729
|
+
manifest = artifacts.manifest;
|
|
730
|
+
openapi = artifacts.openapi;
|
|
731
|
+
precompiledValidators = artifacts.validators?.validators ?? null;
|
|
732
|
+
}
|
|
733
|
+
const routes = bindRoutes({ controllers, manifest });
|
|
734
|
+
const validator = precompiledValidators ? null : createValidator();
|
|
735
|
+
const router = Router();
|
|
736
|
+
const instanceCache = /* @__PURE__ */ new Map();
|
|
737
|
+
const coerce = normalizeCoerceOptions(options.coerce);
|
|
738
|
+
function getInstance(Ctor) {
|
|
739
|
+
if (!instanceCache.has(Ctor)) {
|
|
740
|
+
instanceCache.set(Ctor, new Ctor());
|
|
741
|
+
}
|
|
742
|
+
return instanceCache.get(Ctor);
|
|
743
|
+
}
|
|
744
|
+
function resolveMiddleware(items, named = {}) {
|
|
745
|
+
return items.map((item) => {
|
|
746
|
+
if (typeof item === "string") {
|
|
747
|
+
const fn = named[item];
|
|
748
|
+
if (!fn) {
|
|
749
|
+
throw new Error(`Named middleware "${item}" not found in middleware registry`);
|
|
750
|
+
}
|
|
751
|
+
return fn;
|
|
752
|
+
}
|
|
753
|
+
return item;
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
for (const route of routes) {
|
|
757
|
+
const method = route.httpMethod.toLowerCase();
|
|
758
|
+
const openapiOperation = getOpenApiOperation(openapi, route);
|
|
759
|
+
const paramSchemaIndex = getParamSchemaIndex(openapiOperation);
|
|
760
|
+
const bodySchema = getRequestBodySchema(openapiOperation, route.args.body?.contentType) ?? (route.args.body ? getSchemaByRef(openapi, route.args.body.schemaRef) : null);
|
|
761
|
+
const coerceBodyDates = getDateCoercionOptions(coerce, "body");
|
|
762
|
+
const coerceQueryDates = getDateCoercionOptions(coerce, "query");
|
|
763
|
+
const coercePathDates = getDateCoercionOptions(coerce, "path");
|
|
764
|
+
const coerceHeaderDates = getDateCoercionOptions(coerce, "header");
|
|
765
|
+
const coerceCookieDates = getDateCoercionOptions(coerce, "cookie");
|
|
766
|
+
const middlewareChain = [];
|
|
767
|
+
if (middleware.global) {
|
|
768
|
+
middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
|
|
769
|
+
}
|
|
770
|
+
if (route.controllerUse) {
|
|
771
|
+
middlewareChain.push(...resolveMiddleware(route.controllerUse, middleware.named || {}));
|
|
772
|
+
}
|
|
773
|
+
if (route.use) {
|
|
774
|
+
middlewareChain.push(...resolveMiddleware(route.use, middleware.named || {}));
|
|
775
|
+
}
|
|
776
|
+
if (options.auth) {
|
|
777
|
+
const authMw = createAuthMiddleware(options.auth, route.auth, openapi.security || []);
|
|
778
|
+
middlewareChain.push(authMw);
|
|
779
|
+
}
|
|
780
|
+
router[method](route.fullPath, ...middlewareChain, async (req, res, next) => {
|
|
781
|
+
try {
|
|
782
|
+
const validationErrors = precompiledValidators ? validateRequestWithPrecompiled(route, req, precompiledValidators) : validateRequest(route, req, openapi, validator);
|
|
783
|
+
if (validationErrors) {
|
|
784
|
+
return res.status(400).json(formatValidationErrors(validationErrors));
|
|
785
|
+
}
|
|
786
|
+
const instance = getInstance(route.controllerCtor);
|
|
787
|
+
const handler = instance[route.methodName];
|
|
788
|
+
if (typeof handler !== "function") {
|
|
789
|
+
throw new Error(`Method ${route.methodName} not found on controller`);
|
|
790
|
+
}
|
|
791
|
+
const args = [];
|
|
792
|
+
if (route.args.body) {
|
|
793
|
+
const coercedBody = (coerceBodyDates.date || coerceBodyDates.dateTime) && bodySchema ? coerceDatesWithSchema(req.body, bodySchema, coerceBodyDates, openapi.components.schemas) : req.body;
|
|
794
|
+
args[route.args.body.index] = coercedBody;
|
|
795
|
+
}
|
|
796
|
+
for (const pathArg of route.args.path) {
|
|
797
|
+
const rawValue = req.params[pathArg.name];
|
|
798
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", pathArg.name) ?? (pathArg.schemaRef ? getSchemaByRef(openapi, pathArg.schemaRef) : null) ?? schemaFromType(pathArg.schemaType);
|
|
799
|
+
const coerced = coerceParamValue(rawValue, paramSchema, coercePathDates, openapi.components.schemas);
|
|
800
|
+
args[pathArg.index] = coerced;
|
|
801
|
+
}
|
|
802
|
+
if (route.args.query.length > 0) {
|
|
803
|
+
const jsonArgs = route.args.query.filter((q) => q.content === "application/json");
|
|
804
|
+
const standardArgs = route.args.query.filter((q) => q.content !== "application/json");
|
|
805
|
+
const queryArgIndex = standardArgs[0]?.index;
|
|
806
|
+
if (queryArgIndex !== void 0) {
|
|
807
|
+
args[queryArgIndex] = {};
|
|
808
|
+
}
|
|
809
|
+
if (jsonArgs.length > 0) {
|
|
810
|
+
for (const q of jsonArgs) {
|
|
811
|
+
const rawValue = req.query[q.name];
|
|
812
|
+
if (rawValue === void 0 || rawValue === null) continue;
|
|
813
|
+
let parsed = rawValue;
|
|
814
|
+
if (typeof rawValue === "string" && rawValue.length > 0) {
|
|
815
|
+
try {
|
|
816
|
+
parsed = JSON.parse(rawValue);
|
|
817
|
+
} catch {
|
|
818
|
+
parsed = rawValue;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(openapi, q.schemaRef) : null) ?? schemaFromType(q.schemaType);
|
|
822
|
+
const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
|
|
823
|
+
if (!args[q.index] || typeof args[q.index] !== "object") {
|
|
824
|
+
args[q.index] = {};
|
|
825
|
+
}
|
|
826
|
+
Object.assign(args[q.index], coerced);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (standardArgs.length > 0) {
|
|
830
|
+
for (const q of standardArgs) {
|
|
831
|
+
const rawValue = req.query[q.name];
|
|
832
|
+
if (rawValue === void 0) continue;
|
|
833
|
+
const parsed = parseQueryValue(rawValue, q);
|
|
834
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(openapi, q.schemaRef) : null) ?? schemaFromType(q.schemaType);
|
|
835
|
+
const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
|
|
836
|
+
if (!args[q.index] || typeof args[q.index] !== "object") {
|
|
837
|
+
args[q.index] = {};
|
|
838
|
+
}
|
|
839
|
+
args[q.index][q.name] = coerced;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (queryArgIndex !== void 0 && args[queryArgIndex]) {
|
|
843
|
+
const queryObj = args[queryArgIndex];
|
|
844
|
+
if (queryObj.page === void 0) {
|
|
845
|
+
queryObj.page = 1;
|
|
846
|
+
}
|
|
847
|
+
if (queryObj.pageSize === void 0) {
|
|
848
|
+
queryObj.pageSize = defaultPageSize;
|
|
849
|
+
}
|
|
850
|
+
if (queryObj.sort) {
|
|
851
|
+
queryObj.sort = normalizeSort(queryObj.sort);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (route.args.headers.length > 0) {
|
|
856
|
+
const firstHeaderIndex = route.args.headers[0].index;
|
|
857
|
+
const allSameIndex = route.args.headers.every((h) => h.index === firstHeaderIndex);
|
|
858
|
+
if (allSameIndex) {
|
|
859
|
+
args[firstHeaderIndex] = {};
|
|
860
|
+
for (const h of route.args.headers) {
|
|
861
|
+
const headerValue = req.headers[h.name.toLowerCase()];
|
|
862
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(openapi, h.schemaRef) : null) ?? schemaFromType(h.schemaType);
|
|
863
|
+
const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
|
|
864
|
+
args[firstHeaderIndex][h.name] = coerced ?? void 0;
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
for (const h of route.args.headers) {
|
|
868
|
+
const headerValue = req.headers[h.name.toLowerCase()];
|
|
869
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(openapi, h.schemaRef) : null) ?? schemaFromType(h.schemaType);
|
|
870
|
+
const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
|
|
871
|
+
args[h.index] = coerced ?? void 0;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (route.args.cookies.length > 0) {
|
|
876
|
+
const firstCookieIndex = route.args.cookies[0].index;
|
|
877
|
+
const allSameIndex = route.args.cookies.every((c) => c.index === firstCookieIndex);
|
|
878
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
879
|
+
if (allSameIndex) {
|
|
880
|
+
args[firstCookieIndex] = {};
|
|
881
|
+
for (const c of route.args.cookies) {
|
|
882
|
+
const cookieValue = cookies[c.name];
|
|
883
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(openapi, c.name) : null) ?? schemaFromType(c.schemaType);
|
|
884
|
+
const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
|
|
885
|
+
args[firstCookieIndex][c.name] = coerced;
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
for (const c of route.args.cookies) {
|
|
889
|
+
const cookieValue = cookies[c.name];
|
|
890
|
+
const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(openapi, c.name) : null) ?? schemaFromType(c.schemaType);
|
|
891
|
+
const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
|
|
892
|
+
args[c.index] = coerced;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (args.length === 0) {
|
|
897
|
+
args.push(req);
|
|
898
|
+
}
|
|
899
|
+
const result = await handler.apply(instance, args);
|
|
900
|
+
const primaryResponse = route.responses[0];
|
|
901
|
+
const status = primaryResponse?.status ?? 200;
|
|
902
|
+
res.status(status).json(result);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
next(error);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return router;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/adapter/express/swagger.ts
|
|
912
|
+
import { Router as Router2 } from "express";
|
|
913
|
+
import { readFileSync } from "fs";
|
|
914
|
+
import { resolve, isAbsolute } from "path";
|
|
915
|
+
import swaggerUi from "swagger-ui-express";
|
|
984
916
|
function setupSwagger(options = {}) {
|
|
985
917
|
const {
|
|
986
918
|
artifactsDir = ".adorn",
|
|
@@ -988,7 +920,7 @@ function setupSwagger(options = {}) {
|
|
|
988
920
|
uiPath = "/docs",
|
|
989
921
|
swaggerOptions = {}
|
|
990
922
|
} = options;
|
|
991
|
-
const router =
|
|
923
|
+
const router = Router2();
|
|
992
924
|
router.get(jsonPath, (req, res) => {
|
|
993
925
|
const openApiPath = isAbsolute(artifactsDir) ? resolve(artifactsDir, "openapi.json") : resolve(process.cwd(), artifactsDir, "openapi.json");
|
|
994
926
|
const content = readFileSync(openApiPath, "utf-8");
|
|
@@ -1003,6 +935,94 @@ function setupSwagger(options = {}) {
|
|
|
1003
935
|
}));
|
|
1004
936
|
return router;
|
|
1005
937
|
}
|
|
938
|
+
|
|
939
|
+
// src/adapter/express/bootstrap.ts
|
|
940
|
+
import express from "express";
|
|
941
|
+
import path2 from "path";
|
|
942
|
+
function bootstrap(options) {
|
|
943
|
+
return new Promise((resolve2, reject) => {
|
|
944
|
+
const {
|
|
945
|
+
controllers,
|
|
946
|
+
port: userPort,
|
|
947
|
+
host: userHost,
|
|
948
|
+
artifactsDir: userArtifactsDir = ".adorn",
|
|
949
|
+
enableSwagger = true,
|
|
950
|
+
swaggerPath = "/docs",
|
|
951
|
+
swaggerJsonPath = "/docs/openapi.json",
|
|
952
|
+
middleware,
|
|
953
|
+
auth,
|
|
954
|
+
coerce
|
|
955
|
+
} = options;
|
|
956
|
+
if (controllers.length === 0) {
|
|
957
|
+
reject(new Error("At least one controller must be provided to bootstrap()."));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const envPort = process.env.PORT;
|
|
961
|
+
const port = userPort ?? (envPort !== void 0 ? Number(envPort) : 3e3);
|
|
962
|
+
const host = userHost ?? process.env.HOST ?? "0.0.0.0";
|
|
963
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
964
|
+
reject(new Error(`Invalid port: ${port}. Port must be between 0 and 65535.`));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const absoluteArtifactsDir = path2.isAbsolute(userArtifactsDir) ? userArtifactsDir : path2.resolve(process.cwd(), userArtifactsDir);
|
|
968
|
+
const app = express();
|
|
969
|
+
app.use(express.json());
|
|
970
|
+
createExpressRouter({
|
|
971
|
+
controllers,
|
|
972
|
+
artifactsDir: absoluteArtifactsDir,
|
|
973
|
+
middleware,
|
|
974
|
+
auth,
|
|
975
|
+
coerce
|
|
976
|
+
}).then((router) => {
|
|
977
|
+
app.use(router);
|
|
978
|
+
if (enableSwagger) {
|
|
979
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
980
|
+
const serverUrl = `http://${displayHost}:${port}`;
|
|
981
|
+
app.use(
|
|
982
|
+
setupSwagger({
|
|
983
|
+
artifactsDir: absoluteArtifactsDir,
|
|
984
|
+
jsonPath: swaggerJsonPath,
|
|
985
|
+
uiPath: swaggerPath,
|
|
986
|
+
swaggerOptions: {
|
|
987
|
+
servers: [{ url: serverUrl }]
|
|
988
|
+
}
|
|
989
|
+
})
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
const server = app.listen(port, host, () => {
|
|
993
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
994
|
+
const serverUrl = `http://${displayHost}:${port}`;
|
|
995
|
+
console.log(`\u{1F680} Server running on ${serverUrl}`);
|
|
996
|
+
if (enableSwagger) {
|
|
997
|
+
console.log(`\u{1F4DA} Swagger UI: ${serverUrl}${swaggerPath}`);
|
|
998
|
+
}
|
|
999
|
+
const result = {
|
|
1000
|
+
server,
|
|
1001
|
+
app,
|
|
1002
|
+
url: serverUrl,
|
|
1003
|
+
port,
|
|
1004
|
+
host,
|
|
1005
|
+
close: () => new Promise((closeResolve) => {
|
|
1006
|
+
server.close(() => {
|
|
1007
|
+
console.log("Server closed gracefully");
|
|
1008
|
+
closeResolve();
|
|
1009
|
+
});
|
|
1010
|
+
})
|
|
1011
|
+
};
|
|
1012
|
+
resolve2(result);
|
|
1013
|
+
});
|
|
1014
|
+
server.on("error", (error) => {
|
|
1015
|
+
if (error.code === "EADDRINUSE") {
|
|
1016
|
+
reject(new Error(`Port ${port} already in use. Please choose a different port.`));
|
|
1017
|
+
} else if (error.code === "EACCES") {
|
|
1018
|
+
reject(new Error(`Permission denied for port ${port}. Ports below 1024 require root privileges.`));
|
|
1019
|
+
} else {
|
|
1020
|
+
reject(new Error(`Failed to start server: ${error.message}`));
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}).catch(reject);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1006
1026
|
export {
|
|
1007
1027
|
ValidationErrorResponse,
|
|
1008
1028
|
bindRoutes,
|