@riddl/core 0.2.6 → 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/api.d.ts +1 -1
- package/dist/api.js +55 -42
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +29 -23
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { APIOptions, API } from
|
|
1
|
+
import type { APIOptions, API } from './types';
|
|
2
2
|
export declare const api: ({ routeModuleMap, config, base, cors, registry, onInternalError, }: APIOptions) => Promise<API>;
|
package/dist/api.js
CHANGED
|
@@ -1,49 +1,56 @@
|
|
|
1
|
-
import { OpenAPIRegistry } from
|
|
2
|
-
import { json, recursiveAwait } from
|
|
3
|
-
import { CORS_DEFAULT, METHOD } from
|
|
4
|
-
import { log, logError } 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 =
|
|
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
12
|
const reportInternalError = (error) => {
|
|
13
|
-
logError(
|
|
13
|
+
logError('internal error', error);
|
|
14
14
|
onInternalError === null || onInternalError === void 0 ? void 0 : onInternalError(error);
|
|
15
15
|
};
|
|
16
|
-
const { routes, middlewares } = await Object.entries(routeModuleMap)
|
|
17
|
-
.reduce(async (accPromise, [path, module]) => {
|
|
16
|
+
const { routes, middlewares } = await Object.entries(routeModuleMap).reduce(async (accPromise, [path, module]) => {
|
|
18
17
|
var _a, _b, _c;
|
|
19
18
|
const acc = await accPromise;
|
|
20
|
-
const parts = path.split(
|
|
21
|
-
const last = parts[parts.length - 1].split(
|
|
19
|
+
const parts = path.split('/');
|
|
20
|
+
const last = parts[parts.length - 1].split('.')[0];
|
|
22
21
|
parts[parts.length - 1] = last;
|
|
23
|
-
const id = parts.slice(0, -1).join(
|
|
22
|
+
const id = parts.slice(0, -1).join('/');
|
|
24
23
|
if (METHOD.test(last)) {
|
|
25
24
|
const method = last.toUpperCase();
|
|
26
|
-
const endpoint = (await module())
|
|
25
|
+
const endpoint = (await module())
|
|
26
|
+
.apiEndpoint;
|
|
27
27
|
acc.routes[id] = {
|
|
28
28
|
matcher: parseRouteId(id),
|
|
29
29
|
handlers: {
|
|
30
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 : {}),
|
|
31
31
|
[method]: endpoint,
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
if (last ===
|
|
35
|
+
if (last === 'middleware') {
|
|
36
36
|
acc.middlewares[id] = (await module());
|
|
37
37
|
}
|
|
38
38
|
return acc;
|
|
39
|
-
}, Promise.resolve({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
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);
|
|
44
47
|
registerRoutes({ routes, registry, base });
|
|
45
48
|
const router = buildRouter(routes, middlewares);
|
|
46
|
-
const effectiveCorsSettings = cors
|
|
49
|
+
const effectiveCorsSettings = cors
|
|
50
|
+
? typeof cors === 'object'
|
|
51
|
+
? cors
|
|
52
|
+
: CORS_DEFAULT
|
|
53
|
+
: undefined;
|
|
47
54
|
const handler = async (request) => {
|
|
48
55
|
var _a, _b, _c;
|
|
49
56
|
// Temp files produced by streamed multipart uploads; removed once the
|
|
@@ -51,7 +58,7 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
51
58
|
// storage before returning).
|
|
52
59
|
const tempFiles = [];
|
|
53
60
|
try {
|
|
54
|
-
log(
|
|
61
|
+
log('request: %O', request);
|
|
55
62
|
const event = {
|
|
56
63
|
request,
|
|
57
64
|
url: new URL(request.url),
|
|
@@ -59,18 +66,18 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
59
66
|
};
|
|
60
67
|
const { id, params, routeWithMiddlewares } = (_a = router(event.url.pathname)) !== null && _a !== void 0 ? _a : {};
|
|
61
68
|
if (!id || !params || !routeWithMiddlewares) {
|
|
62
|
-
throw error(404,
|
|
69
|
+
throw error(404, 'Route not found');
|
|
63
70
|
}
|
|
64
|
-
log(
|
|
71
|
+
log('route id: %s', id);
|
|
65
72
|
// handle OPTIONS
|
|
66
|
-
if (request.method.toUpperCase() ===
|
|
73
|
+
if (request.method.toUpperCase() === 'OPTIONS') {
|
|
67
74
|
return new Response(null, {
|
|
68
|
-
headers: (_b = effectiveCorsSettings) !== null && _b !== void 0 ? _b : {}
|
|
75
|
+
headers: (_b = effectiveCorsSettings) !== null && _b !== void 0 ? _b : {},
|
|
69
76
|
});
|
|
70
77
|
}
|
|
71
78
|
const endpoint = routeWithMiddlewares.handlers[request.method.toUpperCase()];
|
|
72
79
|
if (!(endpoint === null || endpoint === void 0 ? void 0 : endpoint.handle)) {
|
|
73
|
-
throw error(405,
|
|
80
|
+
throw error(405, 'Method not allowed');
|
|
74
81
|
}
|
|
75
82
|
let ctxEvent = event;
|
|
76
83
|
for (const mw of routeWithMiddlewares.middlewares) {
|
|
@@ -79,12 +86,17 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
79
86
|
const param = await parseParam(params !== null && params !== void 0 ? params : {}, endpoint);
|
|
80
87
|
const query = await parseQuery(request, endpoint);
|
|
81
88
|
const body = await parseBody(request, endpoint, tempFiles);
|
|
82
|
-
log(
|
|
83
|
-
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
|
+
});
|
|
84
96
|
for (const mw of [...routeWithMiddlewares.middlewares].reverse()) {
|
|
85
97
|
ctxEvent = await mw.post(ctxEvent);
|
|
86
98
|
}
|
|
87
|
-
log(
|
|
99
|
+
log('output: %O', output);
|
|
88
100
|
if (output instanceof Response) {
|
|
89
101
|
if (effectiveCorsSettings) {
|
|
90
102
|
for (const [key, value] of Object.entries(effectiveCorsSettings)) {
|
|
@@ -97,9 +109,9 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
97
109
|
if (output instanceof ReadableStream) {
|
|
98
110
|
const res = new Response(output, {
|
|
99
111
|
headers: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Connection:
|
|
112
|
+
'Content-Type': 'text/event-stream',
|
|
113
|
+
'Cache-Control': 'no-cache',
|
|
114
|
+
Connection: 'keep-alive',
|
|
103
115
|
...(effectiveCorsSettings !== null && effectiveCorsSettings !== void 0 ? effectiveCorsSettings : {}),
|
|
104
116
|
},
|
|
105
117
|
});
|
|
@@ -108,7 +120,7 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
108
120
|
if (output instanceof ArrayBuffer) {
|
|
109
121
|
const res = new Response(output, {
|
|
110
122
|
headers: {
|
|
111
|
-
|
|
123
|
+
'Content-Type': 'application/octet-stream',
|
|
112
124
|
...(effectiveCorsSettings !== null && effectiveCorsSettings !== void 0 ? effectiveCorsSettings : {}),
|
|
113
125
|
},
|
|
114
126
|
});
|
|
@@ -123,12 +135,13 @@ export const api = async ({ routeModuleMap, config, base = "", cors = true, regi
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
catch (e) {
|
|
126
|
-
if ('__HTTP_ERROR__' in e) {
|
|
127
|
-
|
|
138
|
+
if (e && typeof e === 'object' && '__HTTP_ERROR__' in e) {
|
|
139
|
+
const httpError = e;
|
|
140
|
+
if (typeof httpError.status === 'number' && httpError.status >= 500) {
|
|
128
141
|
reportInternalError(e);
|
|
129
142
|
}
|
|
130
143
|
return json(e, {
|
|
131
|
-
status:
|
|
144
|
+
status: httpError.status,
|
|
132
145
|
});
|
|
133
146
|
}
|
|
134
147
|
reportInternalError(e);
|
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;
|