@extk/expressive 0.6.2 → 0.8.0

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 CHANGED
@@ -16,6 +16,29 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## Table of Contents
20
+
21
+ - [What is this?](#what-is-this)
22
+ - [Install](#install)
23
+ - [Quick Start](#quick-start)
24
+ - [Error Handling](#error-handling)
25
+ - [OpenAPI / Swagger](#openapi--swagger)
26
+ - [File uploads](#file-uploads)
27
+ - [Using Zod schemas for OpenAPI](#using-zod-schemas-for-openapi)
28
+ - [Middleware](#middleware)
29
+ - [getApiErrorHandlerMiddleware](#getapierrorhandlermiddleware)
30
+ - [getApiNotFoundMiddleware](#getapinotfoundmiddleware)
31
+ - [getGlobalNotFoundMiddleware](#getglobalnotfoundmiddleware)
32
+ - [getGlobalErrorHandlerMiddleware](#getglobalerrorhandlermiddleware)
33
+ - [getBasicAuthMiddleware](#getbasicauthmiddleware)
34
+ - [silently](#silently)
35
+ - [Logging](#logging)
36
+ - [Utilities](#utilities)
37
+ - [API Response Format](#api-response-format)
38
+ - [License](#license)
39
+
40
+ ---
41
+
19
42
  ## What is this?
20
43
 
21
44
  `@extk/expressive` is an opinionated toolkit for Express 5 that wires up the things every API needs but nobody wants to set up from scratch:
@@ -171,6 +194,38 @@ addRoute(
171
194
  );
172
195
  ```
173
196
 
197
+ ### File uploads
198
+
199
+ Use `SWG.singleFileSchema` for a single file field, or `SWG.formDataSchema` for a custom multipart body:
200
+
201
+ ```ts
202
+ // single file — field name defaults to 'file', required defaults to true
203
+ addRoute({
204
+ method: 'post',
205
+ path: '/upload',
206
+ oapi: {
207
+ requestBody: SWG.singleFileSchema(),
208
+ // requestBody: SWG.singleFileSchema('avatar', true),
209
+ },
210
+ }, handler);
211
+
212
+ // custom multipart schema with multiple fields
213
+ addRoute({
214
+ method: 'post',
215
+ path: '/upload/rich',
216
+ oapi: {
217
+ requestBody: SWG.formDataSchema({
218
+ type: 'object',
219
+ properties: {
220
+ file: { type: 'string', format: 'binary' },
221
+ title: { type: 'string' },
222
+ },
223
+ required: ['file'],
224
+ }),
225
+ },
226
+ }, handler);
227
+ ```
228
+
174
229
  Configure security schemes via the swagger builder:
175
230
 
176
231
  ```ts
@@ -261,6 +316,55 @@ addRoute({
261
316
 
262
317
  This way your Zod schemas serve as the single source of truth for both runtime validation and API documentation.
263
318
 
319
+ ## Middleware
320
+
321
+ All middleware factories are returned from `bootstrap()`.
322
+
323
+ ### `getApiErrorHandlerMiddleware(errorMapper?)`
324
+
325
+ Express error handler for API routes. Catches `ApiError` subclasses, handles malformed JSON, and falls back to `InternalError` for unknown errors. Pass an optional `errorMapper` to map third-party errors (e.g. Zod, Multer) to typed `ApiError` instances.
326
+
327
+ ```ts
328
+ app.use(getApiErrorHandlerMiddleware((err) => {
329
+ if (err.name === 'ZodError') return new SchemaValidationError('Validation failed').setData(err.issues);
330
+ return null;
331
+ }));
332
+ ```
333
+
334
+ ### `getApiNotFoundMiddleware()`
335
+
336
+ Returns a JSON `404` response for unmatched API routes.
337
+
338
+ ```ts
339
+ app.use(getApiNotFoundMiddleware());
340
+ // { status: 'error', message: 'GET /unknown not found', errorCode: 'NOT_FOUND' }
341
+ ```
342
+
343
+ ### `getGlobalNotFoundMiddleware(content?)`
344
+
345
+ Returns a plain-text `404`. Useful as the last catch-all for non-API routes. Defaults to `¯\_(ツ)_/¯`.
346
+
347
+ ```ts
348
+ app.use(getGlobalNotFoundMiddleware());
349
+ app.use(getGlobalNotFoundMiddleware('Not found'));
350
+ ```
351
+
352
+ ### `getGlobalErrorHandlerMiddleware()`
353
+
354
+ Minimal error handler that logs and responds with a plain-text `500`. Use this outside of API route groups where JSON responses aren't expected.
355
+
356
+ ### `getBasicAuthMiddleware(basicAuthBase64, realm?)`
357
+
358
+ Protects a route or the Swagger UI with HTTP Basic auth. Accepts a pre-encoded base64 `user:password` string.
359
+
360
+ ```ts
361
+ expressiveServer()
362
+ .withSwagger(
363
+ { config: swaggerDoc },
364
+ getBasicAuthMiddleware(process.env.SWAGGER_AUTH!, 'API Docs'),
365
+ )
366
+ ```
367
+
264
368
  ## silently
265
369
 
266
370
  `silently` runs a function — sync or async — and suppresses any errors it throws. Errors are forwarded to `alertHandler` (if configured) or logged via the container logger.
package/dist/index.d.mts CHANGED
@@ -33,61 +33,85 @@ type Container = {
33
33
  alertHandler?: AlertHandler;
34
34
  };
35
35
 
36
+ type Nullable<T extends string> = T | [T, 'null'] | ['null', T];
36
37
  type NumericConfigs = {
37
38
  minimum?: number;
38
39
  maximum?: number;
39
- exclusiveMinimum?: boolean;
40
- exclusiveMaximum?: boolean;
40
+ exclusiveMinimum?: number;
41
+ exclusiveMaximum?: number;
41
42
  multipleOf?: number;
42
43
  };
43
44
  type NumberSchema = {
44
- type: 'number';
45
+ type: Nullable<'number'>;
45
46
  format?: 'float' | 'double';
46
47
  } & NumericConfigs;
47
48
  type IntegerSchema = {
48
- type: 'integer';
49
+ type: Nullable<'integer'>;
49
50
  format?: 'int32' | 'int64';
50
51
  } & NumericConfigs;
51
52
  type StringSchema = {
52
- type: 'string';
53
+ type: Nullable<'string'>;
53
54
  minLength?: number;
54
55
  maxLength?: number;
55
56
  format?: 'date' | 'date-time' | 'password' | 'byte' | 'binary' | 'email' | 'uuid' | 'uri' | 'hostname' | 'ipv4' | 'ipv6' | OtherString;
56
57
  pattern?: string;
57
58
  };
58
59
  type BooleanSchema = {
59
- type: 'boolean';
60
+ type: Nullable<'boolean'>;
61
+ };
62
+ type NullSchema = {
63
+ type: 'null';
60
64
  };
61
65
  type ArraySchema = {
62
- type: 'array';
63
- items: Partial<Schema>;
66
+ type: Nullable<'array'>;
67
+ items?: Schema;
64
68
  minItems?: number;
65
69
  maxItems?: number;
66
70
  uniqueItems?: boolean;
67
71
  };
68
72
  type ObjectSchema = {
69
- type: 'object';
73
+ type: Nullable<'object'>;
70
74
  properties?: Record<string, Schema>;
71
75
  required?: string[];
72
76
  additionalProperties?: boolean | Schema;
73
77
  minProperties?: number;
74
78
  maxProperties?: number;
75
79
  };
76
- type BaseSchema = ({
77
- nullable?: boolean;
78
- enum?: unknown[];
80
+ type Discriminator = {
81
+ propertyName: string;
82
+ mapping?: Record<string, string>;
83
+ };
84
+ type CommonSchemaProps = {
85
+ title?: string;
79
86
  description?: string;
87
+ example?: unknown;
80
88
  default?: unknown;
81
- } & (StringSchema | NumberSchema | IntegerSchema | BooleanSchema | ArraySchema | ObjectSchema)) | {
82
- $ref: string;
89
+ enum?: unknown[];
90
+ const?: unknown;
91
+ readOnly?: boolean;
92
+ writeOnly?: boolean;
93
+ deprecated?: boolean;
83
94
  };
84
- type Schema = BaseSchema | {
95
+ type BaseSchema = CommonSchemaProps & (StringSchema | NumberSchema | IntegerSchema | BooleanSchema | NullSchema | ArraySchema | ObjectSchema | {
96
+ $ref: string;
97
+ });
98
+ type Schema = // circular
99
+ BaseSchema | (CommonSchemaProps & {
85
100
  allOf: Schema[];
86
- } | {
101
+ discriminator?: Discriminator;
102
+ }) | (CommonSchemaProps & {
87
103
  anyOf: Schema[];
88
- } | {
104
+ discriminator?: Discriminator;
105
+ }) | (CommonSchemaProps & {
89
106
  oneOf: Schema[];
90
- } | OtherUnknown;
107
+ discriminator?: Discriminator;
108
+ }) | (CommonSchemaProps & {
109
+ not: Schema;
110
+ }) | (CommonSchemaProps & {
111
+ if: Schema;
112
+ then?: Schema;
113
+ else?: Schema;
114
+ }) | OtherUnknown;
91
115
  type Content = {
92
116
  description?: string;
93
117
  content?: Partial<Record<ContentType, {
@@ -95,7 +119,7 @@ type Content = {
95
119
  }>>;
96
120
  };
97
121
  type Param = {
98
- in: 'path' | 'query' | 'headers';
122
+ in: 'path' | 'query' | 'header' | 'cookie';
99
123
  name: string;
100
124
  description: string;
101
125
  required: boolean;
@@ -107,6 +131,7 @@ type Servers = {
107
131
  }[];
108
132
  type PathItem = {
109
133
  summary?: string;
134
+ description?: string;
110
135
  requestBody?: Content;
111
136
  parameters?: Param[];
112
137
  servers?: Servers;
@@ -116,13 +141,14 @@ type PathItem = {
116
141
  deprecated?: boolean;
117
142
  security?: AuthMethod[];
118
143
  };
144
+ /** { BearerAuth: [] } | { OAuth2: ['read', 'write'] } */
119
145
  type AuthMethod = Record<string, string[]>;
120
146
  type SecurityScheme = {
121
147
  type: 'http';
122
148
  scheme: 'basic' | 'bearer';
123
149
  } | {
124
150
  type: 'apiKey';
125
- in: 'header';
151
+ in: 'header' | 'query' | 'cookie';
126
152
  name: string;
127
153
  } | {
128
154
  type: 'openIdConnect';
@@ -234,8 +260,9 @@ declare class SwaggerBuilder {
234
260
  withSecuritySchemes(schemes: Record<string, SecurityScheme>): this;
235
261
  withSchemas(schemas: Record<string, Schema>): this;
236
262
  withDefaultSecurity(globalAuthMethods: AuthMethod[]): this;
237
- get(): SwaggerConfig;
263
+ build(): SwaggerConfig;
238
264
  }
265
+ declare function singleFileSchema(field?: string, required?: boolean): Content;
239
266
  declare function formDataSchema(schema: Schema): Content;
240
267
  declare function jsonSchema(schema: Schema): Content;
241
268
  declare function jsonSchemaRef(name: string): Content;
@@ -251,6 +278,7 @@ declare const SWG: {
251
278
  formDataSchema: typeof formDataSchema;
252
279
  jsonSchema: typeof jsonSchema;
253
280
  jsonSchemaRef: typeof jsonSchemaRef;
281
+ singleFileSchema: typeof singleFileSchema;
254
282
  security: (name: string) => AuthMethod;
255
283
  securitySchemes: {
256
284
  readonly BasicAuth: () => SecurityScheme;
@@ -279,6 +307,7 @@ declare class ApiErrorResponse<T = undefined> {
279
307
  readonly errorCode: string;
280
308
  readonly errors?: T;
281
309
  constructor(message: string, errorCode: string, errors?: T);
310
+ static fromApiError(err: ApiError): ApiErrorResponse<unknown>;
282
311
  }
283
312
 
284
313
  declare class ApiResponse<T = undefined> {
@@ -290,8 +319,10 @@ declare class ApiResponse<T = undefined> {
290
319
  declare function bootstrap(container: Container): {
291
320
  swaggerBuilder: () => SwaggerBuilder;
292
321
  silently: (fn: () => Promise<void> | void) => Promise<void>;
293
- notFoundMiddleware: (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
294
- getErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
322
+ getGlobalNotFoundMiddleware: (content?: string) => (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
323
+ getApiNotFoundMiddleware: () => (req: express.Request, res: express.Response, _next: express.NextFunction) => void;
324
+ getApiErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
325
+ getGlobalErrorHandlerMiddleware: () => (err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => void;
295
326
  getBasicAuthMiddleware: (basicAuthBase64: string, basicRealm?: string) => (req: express.Request, res: express.Response, next: express.NextFunction) => void;
296
327
  expressiveServer: (configs?: {
297
328
  app?: express.Express;
package/dist/index.d.ts CHANGED
@@ -33,61 +33,85 @@ type Container = {
33
33
  alertHandler?: AlertHandler;
34
34
  };
35
35
 
36
+ type Nullable<T extends string> = T | [T, 'null'] | ['null', T];
36
37
  type NumericConfigs = {
37
38
  minimum?: number;
38
39
  maximum?: number;
39
- exclusiveMinimum?: boolean;
40
- exclusiveMaximum?: boolean;
40
+ exclusiveMinimum?: number;
41
+ exclusiveMaximum?: number;
41
42
  multipleOf?: number;
42
43
  };
43
44
  type NumberSchema = {
44
- type: 'number';
45
+ type: Nullable<'number'>;
45
46
  format?: 'float' | 'double';
46
47
  } & NumericConfigs;
47
48
  type IntegerSchema = {
48
- type: 'integer';
49
+ type: Nullable<'integer'>;
49
50
  format?: 'int32' | 'int64';
50
51
  } & NumericConfigs;
51
52
  type StringSchema = {
52
- type: 'string';
53
+ type: Nullable<'string'>;
53
54
  minLength?: number;
54
55
  maxLength?: number;
55
56
  format?: 'date' | 'date-time' | 'password' | 'byte' | 'binary' | 'email' | 'uuid' | 'uri' | 'hostname' | 'ipv4' | 'ipv6' | OtherString;
56
57
  pattern?: string;
57
58
  };
58
59
  type BooleanSchema = {
59
- type: 'boolean';
60
+ type: Nullable<'boolean'>;
61
+ };
62
+ type NullSchema = {
63
+ type: 'null';
60
64
  };
61
65
  type ArraySchema = {
62
- type: 'array';
63
- items: Partial<Schema>;
66
+ type: Nullable<'array'>;
67
+ items?: Schema;
64
68
  minItems?: number;
65
69
  maxItems?: number;
66
70
  uniqueItems?: boolean;
67
71
  };
68
72
  type ObjectSchema = {
69
- type: 'object';
73
+ type: Nullable<'object'>;
70
74
  properties?: Record<string, Schema>;
71
75
  required?: string[];
72
76
  additionalProperties?: boolean | Schema;
73
77
  minProperties?: number;
74
78
  maxProperties?: number;
75
79
  };
76
- type BaseSchema = ({
77
- nullable?: boolean;
78
- enum?: unknown[];
80
+ type Discriminator = {
81
+ propertyName: string;
82
+ mapping?: Record<string, string>;
83
+ };
84
+ type CommonSchemaProps = {
85
+ title?: string;
79
86
  description?: string;
87
+ example?: unknown;
80
88
  default?: unknown;
81
- } & (StringSchema | NumberSchema | IntegerSchema | BooleanSchema | ArraySchema | ObjectSchema)) | {
82
- $ref: string;
89
+ enum?: unknown[];
90
+ const?: unknown;
91
+ readOnly?: boolean;
92
+ writeOnly?: boolean;
93
+ deprecated?: boolean;
83
94
  };
84
- type Schema = BaseSchema | {
95
+ type BaseSchema = CommonSchemaProps & (StringSchema | NumberSchema | IntegerSchema | BooleanSchema | NullSchema | ArraySchema | ObjectSchema | {
96
+ $ref: string;
97
+ });
98
+ type Schema = // circular
99
+ BaseSchema | (CommonSchemaProps & {
85
100
  allOf: Schema[];
86
- } | {
101
+ discriminator?: Discriminator;
102
+ }) | (CommonSchemaProps & {
87
103
  anyOf: Schema[];
88
- } | {
104
+ discriminator?: Discriminator;
105
+ }) | (CommonSchemaProps & {
89
106
  oneOf: Schema[];
90
- } | OtherUnknown;
107
+ discriminator?: Discriminator;
108
+ }) | (CommonSchemaProps & {
109
+ not: Schema;
110
+ }) | (CommonSchemaProps & {
111
+ if: Schema;
112
+ then?: Schema;
113
+ else?: Schema;
114
+ }) | OtherUnknown;
91
115
  type Content = {
92
116
  description?: string;
93
117
  content?: Partial<Record<ContentType, {
@@ -95,7 +119,7 @@ type Content = {
95
119
  }>>;
96
120
  };
97
121
  type Param = {
98
- in: 'path' | 'query' | 'headers';
122
+ in: 'path' | 'query' | 'header' | 'cookie';
99
123
  name: string;
100
124
  description: string;
101
125
  required: boolean;
@@ -107,6 +131,7 @@ type Servers = {
107
131
  }[];
108
132
  type PathItem = {
109
133
  summary?: string;
134
+ description?: string;
110
135
  requestBody?: Content;
111
136
  parameters?: Param[];
112
137
  servers?: Servers;
@@ -116,13 +141,14 @@ type PathItem = {
116
141
  deprecated?: boolean;
117
142
  security?: AuthMethod[];
118
143
  };
144
+ /** { BearerAuth: [] } | { OAuth2: ['read', 'write'] } */
119
145
  type AuthMethod = Record<string, string[]>;
120
146
  type SecurityScheme = {
121
147
  type: 'http';
122
148
  scheme: 'basic' | 'bearer';
123
149
  } | {
124
150
  type: 'apiKey';
125
- in: 'header';
151
+ in: 'header' | 'query' | 'cookie';
126
152
  name: string;
127
153
  } | {
128
154
  type: 'openIdConnect';
@@ -234,8 +260,9 @@ declare class SwaggerBuilder {
234
260
  withSecuritySchemes(schemes: Record<string, SecurityScheme>): this;
235
261
  withSchemas(schemas: Record<string, Schema>): this;
236
262
  withDefaultSecurity(globalAuthMethods: AuthMethod[]): this;
237
- get(): SwaggerConfig;
263
+ build(): SwaggerConfig;
238
264
  }
265
+ declare function singleFileSchema(field?: string, required?: boolean): Content;
239
266
  declare function formDataSchema(schema: Schema): Content;
240
267
  declare function jsonSchema(schema: Schema): Content;
241
268
  declare function jsonSchemaRef(name: string): Content;
@@ -251,6 +278,7 @@ declare const SWG: {
251
278
  formDataSchema: typeof formDataSchema;
252
279
  jsonSchema: typeof jsonSchema;
253
280
  jsonSchemaRef: typeof jsonSchemaRef;
281
+ singleFileSchema: typeof singleFileSchema;
254
282
  security: (name: string) => AuthMethod;
255
283
  securitySchemes: {
256
284
  readonly BasicAuth: () => SecurityScheme;
@@ -279,6 +307,7 @@ declare class ApiErrorResponse<T = undefined> {
279
307
  readonly errorCode: string;
280
308
  readonly errors?: T;
281
309
  constructor(message: string, errorCode: string, errors?: T);
310
+ static fromApiError(err: ApiError): ApiErrorResponse<unknown>;
282
311
  }
283
312
 
284
313
  declare class ApiResponse<T = undefined> {
@@ -290,8 +319,10 @@ declare class ApiResponse<T = undefined> {
290
319
  declare function bootstrap(container: Container): {
291
320
  swaggerBuilder: () => SwaggerBuilder;
292
321
  silently: (fn: () => Promise<void> | void) => Promise<void>;
293
- notFoundMiddleware: (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
294
- getErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
322
+ getGlobalNotFoundMiddleware: (content?: string) => (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
323
+ getApiNotFoundMiddleware: () => (req: express.Request, res: express.Response, _next: express.NextFunction) => void;
324
+ getApiErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
325
+ getGlobalErrorHandlerMiddleware: () => (err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => void;
295
326
  getBasicAuthMiddleware: (basicAuthBase64: string, basicRealm?: string) => (req: express.Request, res: express.Response, next: express.NextFunction) => void;
296
327
  expressiveServer: (configs?: {
297
328
  app?: express.Express;
package/dist/index.js CHANGED
@@ -91,7 +91,7 @@ var SwaggerBuilder = class {
91
91
  this.swaggerDoc.security = globalAuthMethods;
92
92
  return this;
93
93
  }
94
- get() {
94
+ build() {
95
95
  return this.swaggerDoc;
96
96
  }
97
97
  };
@@ -131,6 +131,22 @@ var security = (name) => {
131
131
  }
132
132
  return securityRegistry[name];
133
133
  };
134
+ function singleFileSchema(field = "file", required = true) {
135
+ const schema = {
136
+ type: "object",
137
+ properties: {
138
+ [field]: { type: "string", format: "binary" }
139
+ },
140
+ ...required && { required: [field] }
141
+ };
142
+ return {
143
+ content: {
144
+ "multipart/form-data": {
145
+ schema
146
+ }
147
+ }
148
+ };
149
+ }
134
150
  function formDataSchema(schema) {
135
151
  return {
136
152
  content: {
@@ -168,7 +184,7 @@ function queryParam(id, schema, required = true, description = "", name) {
168
184
  return param("query", id, schema, required, description, name);
169
185
  }
170
186
  function headerParam(id, schema, required = true, description = "", name) {
171
- return param("headers", id, schema, required, description, name);
187
+ return param("header", id, schema, required, description, name);
172
188
  }
173
189
  function convertExpressPath(path2) {
174
190
  return path2.replace(/:([a-zA-Z0-9_*]+)/g, "{$1}");
@@ -188,6 +204,7 @@ var SWG = {
188
204
  formDataSchema,
189
205
  jsonSchema,
190
206
  jsonSchemaRef,
207
+ singleFileSchema,
191
208
  security,
192
209
  securitySchemes
193
210
  };
@@ -403,16 +420,26 @@ var ApiErrorResponse = class {
403
420
  this.errorCode = errorCode;
404
421
  this.errors = errors;
405
422
  }
423
+ static fromApiError(err) {
424
+ return new this(err.message, err.code, err.data);
425
+ }
406
426
  };
407
427
 
408
428
  // src/middleware.ts
409
429
  var buildMiddleware = (container) => {
410
430
  const { logger, alertHandler } = container;
411
431
  return {
412
- notFoundMiddleware: (_req, res, _next) => {
413
- res.status(404).send("\xAF\\_(\u30C4)_/\xAF").end();
432
+ getGlobalNotFoundMiddleware: (content) => {
433
+ return (_req, res, _next) => {
434
+ res.status(404).send(content ?? "\xAF\\_(\u30C4)_/\xAF").end();
435
+ };
436
+ },
437
+ getApiNotFoundMiddleware: () => {
438
+ return (req, res, _next) => {
439
+ res.status(404).json(new ApiErrorResponse(`${req.method} ${req.path} not found`, "NOT_FOUND")).end();
440
+ };
414
441
  },
415
- getErrorHandlerMiddleware: (errorMapper) => {
442
+ getApiErrorHandlerMiddleware: (errorMapper) => {
416
443
  return async (err, req, res, _next) => {
417
444
  let finalError;
418
445
  const customMappedError = errorMapper && errorMapper(err);
@@ -441,6 +468,16 @@ var buildMiddleware = (container) => {
441
468
  res.status(finalError.httpStatusCode).json(new ApiErrorResponse(finalError.message, finalError.code, finalError.data));
442
469
  };
443
470
  },
471
+ getGlobalErrorHandlerMiddleware: () => {
472
+ return (err, _req, res, _next) => {
473
+ logger.error(err);
474
+ if (err instanceof ApiError) {
475
+ res.status(err.httpStatusCode).send(err.message);
476
+ } else {
477
+ res.status(500).send("Something went wrong");
478
+ }
479
+ };
480
+ },
444
481
  getBasicAuthMiddleware: (basicAuthBase64, basicRealm) => {
445
482
  return (req, res, next) => {
446
483
  try {
package/dist/index.mjs CHANGED
@@ -30,7 +30,7 @@ var SwaggerBuilder = class {
30
30
  this.swaggerDoc.security = globalAuthMethods;
31
31
  return this;
32
32
  }
33
- get() {
33
+ build() {
34
34
  return this.swaggerDoc;
35
35
  }
36
36
  };
@@ -70,6 +70,22 @@ var security = (name) => {
70
70
  }
71
71
  return securityRegistry[name];
72
72
  };
73
+ function singleFileSchema(field = "file", required = true) {
74
+ const schema = {
75
+ type: "object",
76
+ properties: {
77
+ [field]: { type: "string", format: "binary" }
78
+ },
79
+ ...required && { required: [field] }
80
+ };
81
+ return {
82
+ content: {
83
+ "multipart/form-data": {
84
+ schema
85
+ }
86
+ }
87
+ };
88
+ }
73
89
  function formDataSchema(schema) {
74
90
  return {
75
91
  content: {
@@ -107,7 +123,7 @@ function queryParam(id, schema, required = true, description = "", name) {
107
123
  return param("query", id, schema, required, description, name);
108
124
  }
109
125
  function headerParam(id, schema, required = true, description = "", name) {
110
- return param("headers", id, schema, required, description, name);
126
+ return param("header", id, schema, required, description, name);
111
127
  }
112
128
  function convertExpressPath(path2) {
113
129
  return path2.replace(/:([a-zA-Z0-9_*]+)/g, "{$1}");
@@ -127,6 +143,7 @@ var SWG = {
127
143
  formDataSchema,
128
144
  jsonSchema,
129
145
  jsonSchemaRef,
146
+ singleFileSchema,
130
147
  security,
131
148
  securitySchemes
132
149
  };
@@ -342,16 +359,26 @@ var ApiErrorResponse = class {
342
359
  this.errorCode = errorCode;
343
360
  this.errors = errors;
344
361
  }
362
+ static fromApiError(err) {
363
+ return new this(err.message, err.code, err.data);
364
+ }
345
365
  };
346
366
 
347
367
  // src/middleware.ts
348
368
  var buildMiddleware = (container) => {
349
369
  const { logger, alertHandler } = container;
350
370
  return {
351
- notFoundMiddleware: (_req, res, _next) => {
352
- res.status(404).send("\xAF\\_(\u30C4)_/\xAF").end();
371
+ getGlobalNotFoundMiddleware: (content) => {
372
+ return (_req, res, _next) => {
373
+ res.status(404).send(content ?? "\xAF\\_(\u30C4)_/\xAF").end();
374
+ };
375
+ },
376
+ getApiNotFoundMiddleware: () => {
377
+ return (req, res, _next) => {
378
+ res.status(404).json(new ApiErrorResponse(`${req.method} ${req.path} not found`, "NOT_FOUND")).end();
379
+ };
353
380
  },
354
- getErrorHandlerMiddleware: (errorMapper) => {
381
+ getApiErrorHandlerMiddleware: (errorMapper) => {
355
382
  return async (err, req, res, _next) => {
356
383
  let finalError;
357
384
  const customMappedError = errorMapper && errorMapper(err);
@@ -380,6 +407,16 @@ var buildMiddleware = (container) => {
380
407
  res.status(finalError.httpStatusCode).json(new ApiErrorResponse(finalError.message, finalError.code, finalError.data));
381
408
  };
382
409
  },
410
+ getGlobalErrorHandlerMiddleware: () => {
411
+ return (err, _req, res, _next) => {
412
+ logger.error(err);
413
+ if (err instanceof ApiError) {
414
+ res.status(err.httpStatusCode).send(err.message);
415
+ } else {
416
+ res.status(500).send("Something went wrong");
417
+ }
418
+ };
419
+ },
383
420
  getBasicAuthMiddleware: (basicAuthBase64, basicRealm) => {
384
421
  return (req, res, next) => {
385
422
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extk/expressive",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "type": "commonjs",
5
5
  "publishConfig": {
6
6
  "access": "public"