@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 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 "./types";
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 "@asteasolutions/zod-to-openapi";
2
- import { json, recursiveAwait } from "./utils.js";
3
- import { CORS_DEFAULT, METHOD } from "./constants.js";
4
- import { log } 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(), }) => {
12
- const { routes, middlewares } = await Object.entries(routeModuleMap)
13
- .reduce(async (accPromise, [path, module]) => {
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(".")[0];
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()).apiEndpoint;
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 === "middleware") {
35
+ if (last === 'middleware') {
32
36
  acc.middlewares[id] = (await module());
33
37
  }
34
38
  return acc;
35
- }, Promise.resolve({ routes: {}, middlewares: {} }));
36
- log("routes: %O", routes);
37
- log("middlewares: %O", middlewares);
38
- log("config: %O", config);
39
- log("base: %s", base);
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 ? typeof cors === "object" ? cors : CORS_DEFAULT : undefined;
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("request: %O", request);
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, "Route not found");
69
+ throw error(404, 'Route not found');
59
70
  }
60
- log("route id: %s", id);
71
+ log('route id: %s', id);
61
72
  // handle OPTIONS
62
- if (request.method.toUpperCase() === "OPTIONS") {
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, "Method not allowed");
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("handler: %O", endpoint.handle);
79
- const output = await endpoint.handle({ body, query, param, event: ctxEvent });
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("output: %O", output);
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
- "Content-Type": "text/event-stream",
97
- "Cache-Control": "no-cache",
98
- Connection: "keep-alive",
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
- "Content-Type": "application/octet-stream",
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: e.status,
144
+ status: httpError.status,
125
145
  });
126
146
  }
127
- log("internal error: %O", e);
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
@@ -1,2 +1,4 @@
1
1
  import debug from "debug";
2
2
  export declare const log: debug.Debugger;
3
+ /** Always-on stderr log for internal errors (independent of DEBUG). */
4
+ export declare const logError: (message: string, error: unknown) => void;
package/dist/log.js CHANGED
@@ -1,2 +1,6 @@
1
1
  import debug from "debug";
2
2
  export const log = debug("riddl");
3
+ /** Always-on stderr log for internal errors (independent of DEBUG). */
4
+ export const logError = (message, error) => {
5
+ console.error(`riddl: ${message}`, error);
6
+ };
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;
@@ -1,4 +1,4 @@
1
- import { GenericApiEndpoint } from "./types";
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>>;
@@ -1,20 +1,20 @@
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";
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("content-type")) === null || _a === void 0 ? void 0 : _a.startsWith("application/json")) {
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 === "object") {
12
+ if (typeof json === 'object') {
13
13
  return json;
14
14
  }
15
15
  }
16
16
  catch {
17
- throw error(400, "Invalid JSON body");
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 (["GET", "HEAD", "DELETE", "OPTIONS"].includes(method)) {
26
+ if (['GET', 'HEAD', 'DELETE', 'OPTIONS'].includes(method)) {
27
27
  return body;
28
28
  }
29
- const contentType = (_a = req.headers.get("content-type")) !== null && _a !== void 0 ? _a : "";
29
+ const contentType = (_a = req.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
30
30
  // JSON body
31
- if (contentType.startsWith("application/json")) {
32
- body = await req.clone().json().catch(() => {
33
- throw error(400, "Invalid JSON body");
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("multipart/form-data")) {
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("application/x-www-form-urlencoded")) {
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("text/plain")) {
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("application/octet-stream")) {
72
+ else if (contentType.startsWith('application/octet-stream')) {
70
73
  const buffer = await req.clone().arrayBuffer();
71
74
  body = { buffer };
72
75
  }
73
- log("body: %O", body);
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("query: %O", query);
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("param: %O", params);
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 === "object" ? 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
- log.extend("error")("output: %O failed validation: %O", output, validation.error);
133
- throw error(500, "Output validation failed. Please report this error to the developer.");
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddl/core",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "dependencies": {