@riddl/core 0.2.5 → 0.2.7
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/adapters.js +2 -0
- package/dist/api.d.ts +2 -2
- package/dist/api.js +61 -41
- package/dist/log.d.ts +2 -0
- package/dist/log.js +4 -0
- package/dist/types.d.ts +2 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +29 -23
- package/package.json +1 -1
package/dist/adapters.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { toFetchRequest } from "./utils.js";
|
|
3
3
|
import * as set_cookie_parser from "set-cookie-parser";
|
|
4
|
+
import { logError } from "./log.js";
|
|
4
5
|
export const mountApi = (app, prefix, apiHandler) => app.use(prefix, expressAdapter(apiHandler));
|
|
5
6
|
export const setResponse = async (res, response) => {
|
|
6
7
|
for (const [key, value] of response.headers) {
|
|
@@ -10,6 +11,7 @@ export const setResponse = async (res, response) => {
|
|
|
10
11
|
: value);
|
|
11
12
|
}
|
|
12
13
|
catch (error) {
|
|
14
|
+
logError("failed to set response header", error);
|
|
13
15
|
res.getHeaderNames().forEach((name) => res.removeHeader(name));
|
|
14
16
|
res.writeHead(500).end(String(error));
|
|
15
17
|
return;
|
package/dist/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { APIOptions, API } from
|
|
2
|
-
export declare const api: ({ routeModuleMap, config, base, cors, registry, }: APIOptions) => Promise<API>;
|
|
1
|
+
import type { APIOptions, API } from './types';
|
|
2
|
+
export declare const api: ({ routeModuleMap, config, base, cors, registry, onInternalError, }: APIOptions) => Promise<API>;
|
package/dist/api.js
CHANGED
|
@@ -1,45 +1,56 @@
|
|
|
1
|
-
import { OpenAPIRegistry } from
|
|
2
|
-
import { json, recursiveAwait } from
|
|
3
|
-
import { CORS_DEFAULT, METHOD } from
|
|
4
|
-
import { log } from
|
|
5
|
-
import { parseBody, parseOutput, parseParam, parseQuery } from
|
|
6
|
-
import { cleanupTempFiles } from
|
|
7
|
-
import { parseRouteId } from
|
|
8
|
-
import { buildRouter } from
|
|
9
|
-
import { openapiHandler, registerRoutes } from
|
|
10
|
-
import { error } from
|
|
11
|
-
export const api = async ({ routeModuleMap, config, base =
|
|
12
|
-
const
|
|
13
|
-
|
|
1
|
+
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
|
2
|
+
import { json, recursiveAwait } from './utils.js';
|
|
3
|
+
import { CORS_DEFAULT, METHOD } from './constants.js';
|
|
4
|
+
import { log, logError } from './log.js';
|
|
5
|
+
import { parseBody, parseOutput, parseParam, parseQuery } from './validation.js';
|
|
6
|
+
import { cleanupTempFiles } from './multipart.js';
|
|
7
|
+
import { parseRouteId } from './parser.js';
|
|
8
|
+
import { buildRouter } from './routing.js';
|
|
9
|
+
import { openapiHandler, registerRoutes } from './openapi.js';
|
|
10
|
+
import { error } from './errors.js';
|
|
11
|
+
export const api = async ({ routeModuleMap, config, base = '', cors = true, registry = new OpenAPIRegistry(), onInternalError, }) => {
|
|
12
|
+
const reportInternalError = (error) => {
|
|
13
|
+
logError('internal error', error);
|
|
14
|
+
onInternalError === null || onInternalError === void 0 ? void 0 : onInternalError(error);
|
|
15
|
+
};
|
|
16
|
+
const { routes, middlewares } = await Object.entries(routeModuleMap).reduce(async (accPromise, [path, module]) => {
|
|
14
17
|
var _a, _b, _c;
|
|
15
18
|
const acc = await accPromise;
|
|
16
|
-
const parts = path.split(
|
|
17
|
-
const last = parts[parts.length - 1].split(
|
|
19
|
+
const parts = path.split('/');
|
|
20
|
+
const last = parts[parts.length - 1].split('.')[0];
|
|
18
21
|
parts[parts.length - 1] = last;
|
|
19
|
-
const id = parts.slice(0, -1).join(
|
|
22
|
+
const id = parts.slice(0, -1).join('/');
|
|
20
23
|
if (METHOD.test(last)) {
|
|
21
24
|
const method = last.toUpperCase();
|
|
22
|
-
const endpoint = (await module())
|
|
25
|
+
const endpoint = (await module())
|
|
26
|
+
.apiEndpoint;
|
|
23
27
|
acc.routes[id] = {
|
|
24
28
|
matcher: parseRouteId(id),
|
|
25
29
|
handlers: {
|
|
26
30
|
...((_c = (_b = (_a = acc.routes) === null || _a === void 0 ? void 0 : _a[id]) === null || _b === void 0 ? void 0 : _b.handlers) !== null && _c !== void 0 ? _c : {}),
|
|
27
31
|
[method]: endpoint,
|
|
28
|
-
}
|
|
32
|
+
},
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
|
-
if (last ===
|
|
35
|
+
if (last === 'middleware') {
|
|
32
36
|
acc.middlewares[id] = (await module());
|
|
33
37
|
}
|
|
34
38
|
return acc;
|
|
35
|
-
}, Promise.resolve({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
log(
|
|
39
|
+
}, Promise.resolve({
|
|
40
|
+
routes: {},
|
|
41
|
+
middlewares: {},
|
|
42
|
+
}));
|
|
43
|
+
log('routes: %O', routes);
|
|
44
|
+
log('middlewares: %O', middlewares);
|
|
45
|
+
log('config: %O', config);
|
|
46
|
+
log('base: %s', base);
|
|
40
47
|
registerRoutes({ routes, registry, base });
|
|
41
48
|
const router = buildRouter(routes, middlewares);
|
|
42
|
-
const effectiveCorsSettings = cors
|
|
49
|
+
const effectiveCorsSettings = cors
|
|
50
|
+
? typeof cors === 'object'
|
|
51
|
+
? cors
|
|
52
|
+
: CORS_DEFAULT
|
|
53
|
+
: undefined;
|
|
43
54
|
const handler = async (request) => {
|
|
44
55
|
var _a, _b, _c;
|
|
45
56
|
// Temp files produced by streamed multipart uploads; removed once the
|
|
@@ -47,7 +58,7 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
47
58
|
// storage before returning).
|
|
48
59
|
const tempFiles = [];
|
|
49
60
|
try {
|
|
50
|
-
log(
|
|
61
|
+
log('request: %O', request);
|
|
51
62
|
const event = {
|
|
52
63
|
request,
|
|
53
64
|
url: new URL(request.url),
|
|
@@ -55,18 +66,18 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
55
66
|
};
|
|
56
67
|
const { id, params, routeWithMiddlewares } = (_a = router(event.url.pathname)) !== null && _a !== void 0 ? _a : {};
|
|
57
68
|
if (!id || !params || !routeWithMiddlewares) {
|
|
58
|
-
throw error(404,
|
|
69
|
+
throw error(404, 'Route not found');
|
|
59
70
|
}
|
|
60
|
-
log(
|
|
71
|
+
log('route id: %s', id);
|
|
61
72
|
// handle OPTIONS
|
|
62
|
-
if (request.method.toUpperCase() ===
|
|
73
|
+
if (request.method.toUpperCase() === 'OPTIONS') {
|
|
63
74
|
return new Response(null, {
|
|
64
|
-
headers: (_b = effectiveCorsSettings) !== null && _b !== void 0 ? _b : {}
|
|
75
|
+
headers: (_b = effectiveCorsSettings) !== null && _b !== void 0 ? _b : {},
|
|
65
76
|
});
|
|
66
77
|
}
|
|
67
78
|
const endpoint = routeWithMiddlewares.handlers[request.method.toUpperCase()];
|
|
68
79
|
if (!(endpoint === null || endpoint === void 0 ? void 0 : endpoint.handle)) {
|
|
69
|
-
throw error(405,
|
|
80
|
+
throw error(405, 'Method not allowed');
|
|
70
81
|
}
|
|
71
82
|
let ctxEvent = event;
|
|
72
83
|
for (const mw of routeWithMiddlewares.middlewares) {
|
|
@@ -75,12 +86,17 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
75
86
|
const param = await parseParam(params !== null && params !== void 0 ? params : {}, endpoint);
|
|
76
87
|
const query = await parseQuery(request, endpoint);
|
|
77
88
|
const body = await parseBody(request, endpoint, tempFiles);
|
|
78
|
-
log(
|
|
79
|
-
const output = await endpoint.handle({
|
|
89
|
+
log('handler: %O', endpoint.handle);
|
|
90
|
+
const output = await endpoint.handle({
|
|
91
|
+
body,
|
|
92
|
+
query,
|
|
93
|
+
param,
|
|
94
|
+
event: ctxEvent,
|
|
95
|
+
});
|
|
80
96
|
for (const mw of [...routeWithMiddlewares.middlewares].reverse()) {
|
|
81
97
|
ctxEvent = await mw.post(ctxEvent);
|
|
82
98
|
}
|
|
83
|
-
log(
|
|
99
|
+
log('output: %O', output);
|
|
84
100
|
if (output instanceof Response) {
|
|
85
101
|
if (effectiveCorsSettings) {
|
|
86
102
|
for (const [key, value] of Object.entries(effectiveCorsSettings)) {
|
|
@@ -93,9 +109,9 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
93
109
|
if (output instanceof ReadableStream) {
|
|
94
110
|
const res = new Response(output, {
|
|
95
111
|
headers: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Connection:
|
|
112
|
+
'Content-Type': 'text/event-stream',
|
|
113
|
+
'Cache-Control': 'no-cache',
|
|
114
|
+
Connection: 'keep-alive',
|
|
99
115
|
...(effectiveCorsSettings !== null && effectiveCorsSettings !== void 0 ? effectiveCorsSettings : {}),
|
|
100
116
|
},
|
|
101
117
|
});
|
|
@@ -104,7 +120,7 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
104
120
|
if (output instanceof ArrayBuffer) {
|
|
105
121
|
const res = new Response(output, {
|
|
106
122
|
headers: {
|
|
107
|
-
|
|
123
|
+
'Content-Type': 'application/octet-stream',
|
|
108
124
|
...(effectiveCorsSettings !== null && effectiveCorsSettings !== void 0 ? effectiveCorsSettings : {}),
|
|
109
125
|
},
|
|
110
126
|
});
|
|
@@ -119,12 +135,16 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
catch (e) {
|
|
122
|
-
if ('__HTTP_ERROR__' in e) {
|
|
138
|
+
if (e && typeof e === 'object' && '__HTTP_ERROR__' in e) {
|
|
139
|
+
const httpError = e;
|
|
140
|
+
if (typeof httpError.status === 'number' && httpError.status >= 500) {
|
|
141
|
+
reportInternalError(e);
|
|
142
|
+
}
|
|
123
143
|
return json(e, {
|
|
124
|
-
status:
|
|
144
|
+
status: httpError.status,
|
|
125
145
|
});
|
|
126
146
|
}
|
|
127
|
-
|
|
147
|
+
reportInternalError(e);
|
|
128
148
|
return json({ message: 'Internal server error', status: 500 }, {
|
|
129
149
|
status: 500,
|
|
130
150
|
});
|
package/dist/log.d.ts
CHANGED
package/dist/log.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface APIOptions {
|
|
|
76
76
|
base?: string;
|
|
77
77
|
cors?: boolean | CORS;
|
|
78
78
|
registry?: OpenAPIRegistry;
|
|
79
|
+
/** Called for unexpected throws and HTTP errors with status >= 500. */
|
|
80
|
+
onInternalError?: (error: unknown) => void;
|
|
79
81
|
}
|
|
80
82
|
export interface OpenAPIOptions {
|
|
81
83
|
base?: string;
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericApiEndpoint } from
|
|
1
|
+
import { GenericApiEndpoint } from './types';
|
|
2
2
|
export declare const parseBody: (req: Request, module: GenericApiEndpoint, tempFiles?: string[]) => Promise<unknown>;
|
|
3
3
|
export declare const parseQuery: (request: Request, module: GenericApiEndpoint) => Promise<Record<string, string | string[]>>;
|
|
4
4
|
export declare const parseParam: (params: Record<string, string>, module: GenericApiEndpoint) => Promise<Record<string, string | never>>;
|
package/dist/validation.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { fromZodError } from
|
|
2
|
-
import { log } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { error } from
|
|
5
|
-
import { parseMultipart } from
|
|
1
|
+
import { fromZodError } from 'zod-validation-error';
|
|
2
|
+
import { log } from './log.js';
|
|
3
|
+
import { z } from './zod.js';
|
|
4
|
+
import { error } from './errors.js';
|
|
5
|
+
import { parseMultipart } from './multipart.js';
|
|
6
6
|
const parseJsonBody = async (req) => {
|
|
7
7
|
var _a;
|
|
8
8
|
const clonedRequest = req.clone();
|
|
9
|
-
if ((_a = clonedRequest.headers.get(
|
|
9
|
+
if ((_a = clonedRequest.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.startsWith('application/json')) {
|
|
10
10
|
try {
|
|
11
11
|
const json = await clonedRequest.json();
|
|
12
|
-
if (typeof json ===
|
|
12
|
+
if (typeof json === 'object') {
|
|
13
13
|
return json;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
catch {
|
|
17
|
-
throw error(400,
|
|
17
|
+
throw error(400, 'Invalid JSON body');
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
};
|
|
@@ -23,26 +23,29 @@ export const parseBody = async (req, module, tempFiles = []) => {
|
|
|
23
23
|
let body = {};
|
|
24
24
|
// GET, HEAD, DELETE, OPTIONS have no body
|
|
25
25
|
const method = req.method.toUpperCase();
|
|
26
|
-
if ([
|
|
26
|
+
if (['GET', 'HEAD', 'DELETE', 'OPTIONS'].includes(method)) {
|
|
27
27
|
return body;
|
|
28
28
|
}
|
|
29
|
-
const contentType = (_a = req.headers.get(
|
|
29
|
+
const contentType = (_a = req.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
|
|
30
30
|
// JSON body
|
|
31
|
-
if (contentType.startsWith(
|
|
32
|
-
body = await req
|
|
33
|
-
|
|
31
|
+
if (contentType.startsWith('application/json')) {
|
|
32
|
+
body = await req
|
|
33
|
+
.clone()
|
|
34
|
+
.json()
|
|
35
|
+
.catch(() => {
|
|
36
|
+
throw error(400, 'Invalid JSON body');
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
// Multipart form body: stream uploads to disk rather than buffering the
|
|
37
40
|
// whole payload in memory. Consume `req` directly (no `clone()` — a tee
|
|
38
41
|
// would re-buffer the unread branch, defeating the point).
|
|
39
|
-
else if (contentType.startsWith(
|
|
42
|
+
else if (contentType.startsWith('multipart/form-data')) {
|
|
40
43
|
const result = await parseMultipart(req);
|
|
41
44
|
tempFiles.push(...result.tempFiles);
|
|
42
45
|
body = result.body;
|
|
43
46
|
}
|
|
44
47
|
// URL-encoded form body (always small — buffering is fine)
|
|
45
|
-
else if (contentType.startsWith(
|
|
48
|
+
else if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
|
46
49
|
const formBody = {};
|
|
47
50
|
const form = await req.clone().formData();
|
|
48
51
|
for (const [key, value] of form.entries()) {
|
|
@@ -62,15 +65,15 @@ export const parseBody = async (req, module, tempFiles = []) => {
|
|
|
62
65
|
body = formBody;
|
|
63
66
|
}
|
|
64
67
|
// Text body
|
|
65
|
-
else if (contentType.startsWith(
|
|
68
|
+
else if (contentType.startsWith('text/plain')) {
|
|
66
69
|
body = { text: await req.clone().text() };
|
|
67
70
|
}
|
|
68
71
|
// Binary body
|
|
69
|
-
else if (contentType.startsWith(
|
|
72
|
+
else if (contentType.startsWith('application/octet-stream')) {
|
|
70
73
|
const buffer = await req.clone().arrayBuffer();
|
|
71
74
|
body = { buffer };
|
|
72
75
|
}
|
|
73
|
-
log(
|
|
76
|
+
log('body: %O', body);
|
|
74
77
|
const validator = (_b = module.Input) !== null && _b !== void 0 ? _b : z.object({});
|
|
75
78
|
const validation = validator.safeParse(body);
|
|
76
79
|
if (!validation.success) {
|
|
@@ -98,7 +101,7 @@ export const parseQuery = async (request, module) => {
|
|
|
98
101
|
query[key] = value;
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
|
-
log(
|
|
104
|
+
log('query: %O', query);
|
|
102
105
|
const validator = (_a = module.Query) !== null && _a !== void 0 ? _a : z.object({});
|
|
103
106
|
const validation = validator.safeParse(query);
|
|
104
107
|
if (!validation.success) {
|
|
@@ -111,7 +114,7 @@ export const parseQuery = async (request, module) => {
|
|
|
111
114
|
};
|
|
112
115
|
export const parseParam = async (params, module) => {
|
|
113
116
|
var _a;
|
|
114
|
-
log(
|
|
117
|
+
log('param: %O', params);
|
|
115
118
|
const validator = (_a = module.Param) !== null && _a !== void 0 ? _a : z.object({});
|
|
116
119
|
const validation = validator.safeParse(params);
|
|
117
120
|
if (!validation.success) {
|
|
@@ -125,12 +128,15 @@ export const parseOutput = async (out, module) => {
|
|
|
125
128
|
var _a;
|
|
126
129
|
let output = Array.isArray(out)
|
|
127
130
|
? out
|
|
128
|
-
: out && typeof out ===
|
|
131
|
+
: out && typeof out === 'object'
|
|
132
|
+
? out
|
|
133
|
+
: {};
|
|
129
134
|
const validator = (_a = module.Output) !== null && _a !== void 0 ? _a : z.object({});
|
|
130
135
|
const validation = await validator.spa(output);
|
|
131
136
|
if (!validation.success) {
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
const details = fromZodError(validation.error).message;
|
|
138
|
+
log.extend('error')('output: %O failed validation: %O', output, validation.error);
|
|
139
|
+
throw error(500, `Output validation failed. ${details}`);
|
|
134
140
|
}
|
|
135
141
|
else {
|
|
136
142
|
output = validation.data;
|