@devmoods/express-extras 0.54.0 → 0.55.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.
@@ -19,7 +19,7 @@ export declare function createAccessToken(payload: any, version?: string, secret
19
19
  * Decode the given JWT token.
20
20
  */
21
21
  export declare function decodeAccessToken(token: string, secret?: string): {
22
- [x: string]: any;
22
+ [key: string]: any;
23
23
  iss?: string | undefined;
24
24
  sub?: string | undefined;
25
25
  aud?: string | string[] | undefined;
package/dist/auth.d.ts CHANGED
@@ -19,8 +19,8 @@ export interface CreateAuth<UserModel extends AnyUser> {
19
19
  logout: (req: Request, res: Response) => Promise<void>;
20
20
  }
21
21
  export declare class RedisTokenStorage extends TokenStorage {
22
- private readonly redis;
23
- private readonly keyPrefix;
22
+ redis: RedisClient;
23
+ keyPrefix: string;
24
24
  constructor(redis: RedisClient, keyPrefix?: string);
25
25
  set(token: string, userId: string, expiresIn: number): Promise<void>;
26
26
  get(token: string): Promise<string | null>;
package/dist/auth.js CHANGED
@@ -16,6 +16,8 @@ export * from './auth/emailAndPassword.js';
16
16
  export * from './auth/passwordless.js';
17
17
  export * from './auth/token.js';
18
18
  export class RedisTokenStorage extends TokenStorage {
19
+ redis;
20
+ keyPrefix;
19
21
  constructor(redis, keyPrefix = `${config.value.REDIS_KEY_PREFIX}auth:`) {
20
22
  super();
21
23
  this.redis = redis;
@@ -1,8 +1,8 @@
1
- import { type Request } from 'express';
1
+ import { type Request, type RequestHandler } from 'express';
2
2
  import { type RedisClient } from './redis.js';
3
3
  export declare function createEventStream<Channels extends string>(redis: RedisClient): {
4
4
  sse: ({ channels, }: {
5
5
  channels: (req: Request) => string[] | Promise<string[]>;
6
- }) => import("express").RequestHandler<Record<string, string>, Record<string, any>, unknown, Record<string, any>, Record<string, any>>;
6
+ }) => RequestHandler;
7
7
  publish: (channel: Channels, message: unknown) => Promise<number>;
8
8
  };
package/dist/channels.js CHANGED
@@ -2,62 +2,66 @@ import {} from 'express';
2
2
  import { config } from './config.js';
3
3
  import { requestContext } from './context.js';
4
4
  import { getLogger } from './logger.js';
5
- import { route } from './middleware.js';
6
5
  import {} from './redis.js';
7
6
  const logger = getLogger();
8
7
  export function createEventStream(redis) {
9
8
  const keyPrefix = `${config.value.REDIS_KEY_PREFIX}events:`;
10
9
  const withKeyPrefix = (key) => `${keyPrefix}${key}`;
11
10
  const sse = ({ channels, }) => {
12
- return route(async (req, res) => {
13
- const headers = {
14
- 'Content-Type': 'text/event-stream',
15
- 'Cache-Control': 'no-cache',
16
- Connection: 'keep-alive',
17
- };
18
- res.writeHead(200, headers);
19
- res.write('retry: 15000\n');
20
- const heartbeat = setInterval(() => {
21
- try {
22
- if (!res.writableEnded) {
23
- res.write('event: heartbeat\n');
24
- res.write('data: ping\n\n');
25
- }
26
- }
27
- catch (error) {
28
- logger.error(error);
29
- }
30
- }, 15000);
31
- const store = requestContext.getStore();
32
- const listener = (value) => {
33
- try {
34
- requestContext.run(store, () => {
11
+ return async (req, res, next) => {
12
+ try {
13
+ const headers = {
14
+ 'Content-Type': 'text/event-stream',
15
+ 'Cache-Control': 'no-cache',
16
+ Connection: 'keep-alive',
17
+ };
18
+ res.writeHead(200, headers);
19
+ res.write('retry: 15000\n');
20
+ const heartbeat = setInterval(() => {
21
+ try {
35
22
  if (!res.writableEnded) {
36
- res.write('event: data\n');
37
- res.write(`data: ${JSON.stringify(value)}\n\n`);
23
+ res.write('event: heartbeat\n');
24
+ res.write('data: ping\n\n');
38
25
  }
26
+ }
27
+ catch (error) {
28
+ logger.error(error);
29
+ }
30
+ }, 15000);
31
+ const store = requestContext.getStore();
32
+ const listener = (value) => {
33
+ try {
34
+ requestContext.run(store, () => {
35
+ if (!res.writableEnded) {
36
+ res.write('event: data\n');
37
+ res.write(`data: ${JSON.stringify(value)}\n\n`);
38
+ }
39
+ });
40
+ }
41
+ catch (error) {
42
+ logger.error(error);
43
+ }
44
+ };
45
+ const subscriber = redis.duplicate();
46
+ await subscriber.connect();
47
+ const _channels = await channels(req);
48
+ await subscriber.subscribe(_channels.map(withKeyPrefix), (message) => {
49
+ listener(JSON.parse(message));
50
+ });
51
+ logger.info('event stream subscribe', { channels: _channels });
52
+ res.on('close', () => {
53
+ requestContext.run(store, () => {
54
+ logger.info('event stream close');
55
+ clearInterval(heartbeat);
56
+ subscriber.quit();
57
+ res.end();
39
58
  });
40
- }
41
- catch (error) {
42
- logger.error(error);
43
- }
44
- };
45
- const subscriber = redis.duplicate();
46
- await subscriber.connect();
47
- const _channels = await channels(req);
48
- await subscriber.subscribe(_channels.map(withKeyPrefix), (message) => {
49
- listener(JSON.parse(message));
50
- });
51
- logger.info('event stream subscribe', { channels: _channels });
52
- res.on('close', () => {
53
- requestContext.run(store, () => {
54
- logger.info('event stream close');
55
- clearInterval(heartbeat);
56
- subscriber.quit();
57
- res.end();
58
59
  });
59
- });
60
- });
60
+ }
61
+ catch (error) {
62
+ next(error);
63
+ }
64
+ };
61
65
  };
62
66
  const publish = (channel, message) => {
63
67
  return redis.publish(withKeyPrefix(channel), JSON.stringify(message));
package/dist/cli.d.ts CHANGED
@@ -3,7 +3,7 @@ export interface CLIOptions {
3
3
  printStackTraces?: boolean;
4
4
  }
5
5
  export declare class CLI {
6
- private options;
6
+ options: CLIOptions;
7
7
  constructor(options?: CLIOptions);
8
8
  writeLine(message: string, output?: WriteStream): void;
9
9
  greenLine(message: string, output?: WriteStream): void;
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import {} from 'node:tty';
4
4
  import chalk from 'chalk';
5
5
  import { startTimer } from './utils.js';
6
6
  export class CLI {
7
+ options;
7
8
  constructor(options = {}) {
8
9
  this.options = options;
9
10
  }
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ErrorHandler } from 'compose-middleware';
1
2
  type AnyPayload = Record<string, unknown>;
2
3
  export declare class ApiError extends Error {
3
4
  status: number | null | undefined;
@@ -37,5 +38,5 @@ export interface ErrorHandlerOptions {
37
38
  *
38
39
  * Composed together with the Sentry error handler.
39
40
  */
40
- export declare function errorHandler({ onError }?: ErrorHandlerOptions): import("compose-middleware").ErrorHandler<unknown, unknown, void>;
41
+ export declare function errorHandler({ onError }?: ErrorHandlerOptions): ErrorHandler<any, any, void>;
41
42
  export {};
package/dist/errors.js CHANGED
@@ -6,6 +6,8 @@ import { InvalidToken } from './auth/errors.js';
6
6
  import { getLogger } from './logger.js';
7
7
  import { DoesNotExistError, IntegrityError } from './postgres.js';
8
8
  export class ApiError extends Error {
9
+ status;
10
+ payload;
9
11
  }
10
12
  export const BadRequest = (reason) => makeError(400, 'Invalid Params', { reason });
11
13
  export const Forbidden = (detail = '') => makeError(403, detail);
@@ -88,6 +90,8 @@ export function errorHandler({ onError } = {}) {
88
90
  return process.env.NODE_ENV === 'production';
89
91
  },
90
92
  }),
91
- _errorHandler({ onError: onError || _defaultErrorHandler }),
93
+ _errorHandler({
94
+ onError: onError || _defaultErrorHandler,
95
+ }),
92
96
  ]);
93
97
  }
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from './middleware.js';
16
16
  export * from './postgres.js';
17
17
  export * from './rate-limit.js';
18
18
  export * from './redis.js';
19
+ export * from './router.js';
19
20
  export * from './routes.js';
20
21
  export * from './sentry.js';
21
22
  export * from './slack.js';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './middleware.js';
17
17
  export * from './postgres.js';
18
18
  export * from './rate-limit.js';
19
19
  export * from './redis.js';
20
+ export * from './router.js';
20
21
  export * from './routes.js';
21
22
  export * from './sentry.js';
22
23
  export * from './slack.js';
package/dist/manage.js CHANGED
@@ -4,7 +4,7 @@ import { hashPassword } from './auth.js';
4
4
  import { CLI } from './cli.js';
5
5
  import { config } from './config.js';
6
6
  import { asCli } from './migrate.js';
7
- import { parseRoutes } from './routes.js';
7
+ import { parseRoutes, toOpenAPISpec } from './routes.js';
8
8
  export function registerManagementCommand(register) {
9
9
  const cli = new CLI({ printStackTraces: process.argv.includes('--verbose') });
10
10
  register(program.command.bind(program), cli);
@@ -26,7 +26,8 @@ export function registerDefaultCommands(app) {
26
26
  return;
27
27
  }
28
28
  if (options.openapi) {
29
- console.error('Not implemented yet');
29
+ const routes = toOpenAPISpec(app);
30
+ console.log(JSON.stringify(routes, null, 2));
30
31
  return;
31
32
  }
32
33
  const routes = parseRoutes(app);
@@ -48,7 +48,7 @@ export declare function useCspNonce(): string;
48
48
  /**
49
49
  * Register middleware that must come before the app routes.
50
50
  */
51
- export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<unknown, unknown, void>;
51
+ export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
52
52
  /**
53
53
  * Middleware that sends a 404
54
54
  *
@@ -61,10 +61,10 @@ export declare function notFound(): RequestHandler;
61
61
  */
62
62
  export declare function after(): RequestHandler;
63
63
  type ResponseHeaders = Record<string, string>;
64
- type AnyResponseBody = Record<string, any> | string;
64
+ type AnyResponseBody = Record<string, any> | Record<string, any>[] | string;
65
65
  type HttpResponseCode = number;
66
66
  type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | TResponseBody[] | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
67
67
  /**
68
68
  * Alias of `route()`
69
69
  */
70
- export declare function asyncMiddleware<RequestBody, RequestQuery extends Record<string, any> = Record<string, any>, ResponseBody extends AnyResponseBody = Record<string, any>, ParamKeys extends string = string>(fn: (req: Request<Record<ParamKeys, string>, ResponseBody, RequestBody, RequestQuery, Record<string, any>>, res: Response<ResponseBody, object>, next: NextFunction) => ResponseWrapper<ResponseBody> | Promise<ResponseWrapper<ResponseBody>>): RequestHandler<Record<string, string>, ResponseBody, RequestBody, RequestQuery>;
70
+ export declare function asyncMiddleware<RequestBody, RequestQuery extends Record<string, any> = Record<string, any>, ResponseBody extends AnyResponseBody = Record<string, any>, ParamKeys extends string = string>(fn: (req: Request<Record<ParamKeys, string>, ResponseBody, RequestBody, RequestQuery, Record<string, any>>, res: Response<ResponseBody, object>, next: NextFunction) => ResponseWrapper<ResponseBody> | Promise<ResponseWrapper<ResponseBody>>): RequestHandler<Record<ParamKeys, string>, ResponseBody, RequestBody, RequestQuery>;
@@ -151,6 +151,9 @@ export function asyncMiddleware(fn) {
151
151
  .then(doubleCsrfProtection(req))
152
152
  .then(() => fn(req, res, next))
153
153
  .then((result) => {
154
+ if (typeof result === 'undefined') {
155
+ return result;
156
+ }
154
157
  if (Array.isArray(result) &&
155
158
  (result.length === 2 || result.length === 3) &&
156
159
  typeof result[0] === 'number') {
package/dist/migrate.js CHANGED
@@ -55,8 +55,12 @@ export const asCli = (command, cli) => {
55
55
  }));
56
56
  };
57
57
  export class Migrator {
58
+ postgres;
59
+ migrationTableName;
60
+ pathToMigrations;
61
+ filenameRegExp = /(\d+)_(.*)\.(js|ts|mjs|mts|sql)/;
62
+ verbose;
58
63
  constructor({ postgres = createPostgres({ onLogStatement: () => { } }), pathToMigrations = path.resolve(process.cwd(), 'db', 'migrations'), migrationTableName = 'migrations', verbose = true, } = {}) {
59
- this.filenameRegExp = /(\d+)_(.*)\.(js|ts|mjs|mts|sql)/;
60
64
  this.postgres = postgres;
61
65
  this.pathToMigrations = pathToMigrations;
62
66
  this.migrationTableName = migrationTableName;
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { type RequestHandler } from 'express';
2
3
  import pg, { type PoolClient, type PoolConfig, type QueryResult, type QueryResultRow } from 'pg';
3
4
  import { type SqlStatement } from './sql.js';
4
5
  export declare const escapePostgresIdentifier: typeof pg.escapeIdentifier;
@@ -28,10 +29,10 @@ export interface TransactionContext {
28
29
  }
29
30
  export declare const transactionStorage: AsyncLocalStorage<TransactionContext>;
30
31
  export declare class Connection {
31
- private options;
32
32
  client: PoolClient;
33
+ options: ConnectionOptions;
33
34
  constructor(client: PoolClient, options: ConnectionOptions);
34
- private onLogStatement;
35
+ private logStatement;
35
36
  query<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
36
37
  all<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T[]>;
37
38
  insert<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
@@ -66,4 +67,5 @@ export declare function createPostgres({ onError, url, ...postgresOptions }?: Po
66
67
  clear: (force?: boolean) => Promise<void>;
67
68
  end(): Promise<void>;
68
69
  };
70
+ export declare function withTransaction(postgres: ReturnType<typeof createPostgres>): RequestHandler;
69
71
  export {};
package/dist/postgres.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import {} from 'express';
2
3
  import pg, {} from 'pg';
3
4
  import { config } from './config.js';
4
5
  import { formatSql } from './sql.js';
@@ -25,11 +26,13 @@ export class IntegrityError extends Error {
25
26
  }
26
27
  export const transactionStorage = new AsyncLocalStorage();
27
28
  export class Connection {
29
+ client;
30
+ options;
28
31
  constructor(client, options) {
29
- this.options = options;
30
32
  this.client = client;
33
+ this.options = options;
31
34
  }
32
- onLogStatement(text, values, duration) {
35
+ logStatement(text, values, duration) {
33
36
  if (this.options.onLogStatement) {
34
37
  return this.options.onLogStatement(text, values, duration);
35
38
  }
@@ -49,7 +52,7 @@ export class Connection {
49
52
  throw error;
50
53
  }
51
54
  finally {
52
- this.onLogStatement?.(text, values, timer.getDuration());
55
+ this.logStatement?.(text, values, timer.getDuration());
53
56
  }
54
57
  }
55
58
  async all(text, values) {
@@ -79,12 +82,9 @@ export class Connection {
79
82
  }
80
83
  }
81
84
  export class Transaction extends Connection {
82
- constructor() {
83
- super(...arguments);
84
- this.savepoints = [];
85
- this.isCommitted = false;
86
- this.isRolledBack = false;
87
- }
85
+ savepoints = [];
86
+ isCommitted = false;
87
+ isRolledBack = false;
88
88
  async createSavepoint() {
89
89
  const savepointName = `sp_${this.savepoints.length + 1}`;
90
90
  await this.query(`SAVEPOINT ${savepointName}`);
@@ -166,11 +166,11 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
166
166
  }
167
167
  // Start a new top-level transaction
168
168
  const client = await pool.connect();
169
+ const txConnection = new Transaction(client, {
170
+ onLogStatement: postgresOptions.onLogStatement,
171
+ });
169
172
  try {
170
- await client.query('BEGIN');
171
- const txConnection = new Transaction(client, {
172
- onLogStatement: postgresOptions.onLogStatement,
173
- });
173
+ await txConnection.query('BEGIN');
174
174
  // Create transaction context
175
175
  const txContext = {
176
176
  connection: txConnection,
@@ -182,12 +182,12 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
182
182
  const result = await transactionStorage.run(txContext, async () => {
183
183
  return await callback();
184
184
  });
185
- await client.query('COMMIT');
185
+ await txConnection.query('COMMIT');
186
186
  txContext.isCommitted = true;
187
187
  return result;
188
188
  }
189
189
  catch (error) {
190
- await client.query('ROLLBACK');
190
+ await txConnection.query('ROLLBACK');
191
191
  throw error;
192
192
  }
193
193
  finally {
@@ -243,3 +243,19 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
243
243
  },
244
244
  };
245
245
  }
246
+ export function withTransaction(postgres) {
247
+ return async (req, res, next) => {
248
+ try {
249
+ await postgres.transaction(async () => {
250
+ await new Promise((resolve, reject) => {
251
+ res.on('finish', resolve);
252
+ res.on('error', reject);
253
+ next();
254
+ });
255
+ });
256
+ }
257
+ catch (error) {
258
+ next(error);
259
+ }
260
+ };
261
+ }
@@ -0,0 +1,19 @@
1
+ import { type StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { type NextFunction, type Request, type RequestHandler, type Response } from 'express';
3
+ import { type ValidationSpec } from './validate.js';
4
+ type ResponseHeaders = Record<string, string>;
5
+ type HttpResponseCode = number;
6
+ type ResponseWrapper<TResponseBody> = TResponseBody | TResponseBody[] | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
7
+ export declare class TypedRouter {
8
+ #private;
9
+ router: import("express-serve-static-core").Router;
10
+ get<Path extends string, RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined>(path: Path, validation?: ValidationSpec<RequestBodySchema, ResponseBodySchema, RequestQuerySchema>, ...middleware: RequestHandler[]): (handler: (req: Request<(Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_6 ? T_6 extends `/${Rest}` ? T_6 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_6 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_4 ? { [K_2 in keyof T_4]: (`/${Rest}` extends infer T_5 ? T_5 extends `/${Rest}` ? T_5 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_5 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {}) extends infer T ? { [K in keyof T]: (Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_3 ? T_3 extends `/${Rest}` ? T_3 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_3 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_1 ? { [K_2 in keyof T_1]: (`/${Rest}` extends infer T_2 ? T_2 extends `/${Rest}` ? T_2 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_2 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {})[K]; } : never, ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, RequestBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuerySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestQuerySchema> : Record<string, string | string[]>, Record<string, any>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, Record<string, any>>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>) => void;
11
+ post<Path extends string, RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined>(path: Path, validation?: ValidationSpec<RequestBodySchema, ResponseBodySchema, RequestQuerySchema>, ...middleware: RequestHandler[]): (handler: (req: Request<(Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_6 ? T_6 extends `/${Rest}` ? T_6 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_6 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_4 ? { [K_2 in keyof T_4]: (`/${Rest}` extends infer T_5 ? T_5 extends `/${Rest}` ? T_5 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_5 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {}) extends infer T ? { [K in keyof T]: (Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_3 ? T_3 extends `/${Rest}` ? T_3 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_3 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_1 ? { [K_2 in keyof T_1]: (`/${Rest}` extends infer T_2 ? T_2 extends `/${Rest}` ? T_2 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_2 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {})[K]; } : never, ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, RequestBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuerySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestQuerySchema> : Record<string, string | string[]>, Record<string, any>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, Record<string, any>>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>) => void;
12
+ put<Path extends string, RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined>(path: Path, validation?: ValidationSpec<RequestBodySchema, ResponseBodySchema, RequestQuerySchema>, ...middleware: RequestHandler[]): (handler: (req: Request<(Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_6 ? T_6 extends `/${Rest}` ? T_6 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_6 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_4 ? { [K_2 in keyof T_4]: (`/${Rest}` extends infer T_5 ? T_5 extends `/${Rest}` ? T_5 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_5 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {}) extends infer T ? { [K in keyof T]: (Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_3 ? T_3 extends `/${Rest}` ? T_3 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_3 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_1 ? { [K_2 in keyof T_1]: (`/${Rest}` extends infer T_2 ? T_2 extends `/${Rest}` ? T_2 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_2 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {})[K]; } : never, ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, RequestBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuerySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestQuerySchema> : Record<string, string | string[]>, Record<string, any>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, Record<string, any>>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>) => void;
13
+ delete<Path extends string, RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined>(path: Path, validation?: ValidationSpec<RequestBodySchema, ResponseBodySchema, RequestQuerySchema>, ...middleware: RequestHandler[]): (handler: (req: Request<(Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_6 ? T_6 extends `/${Rest}` ? T_6 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_6 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_4 ? { [K_2 in keyof T_4]: (`/${Rest}` extends infer T_5 ? T_5 extends `/${Rest}` ? T_5 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_5 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {}) extends infer T ? { [K in keyof T]: (Path extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & ((`/${Rest}` extends infer T_3 ? T_3 extends `/${Rest}` ? T_3 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_3 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never) extends infer T_1 ? { [K_2 in keyof T_1]: (`/${Rest}` extends infer T_2 ? T_2 extends `/${Rest}` ? T_2 extends `${string}:${infer Param}/${infer Rest}` ? { [K_1 in Param extends `${infer P}?` ? P : Param]: string; } & /*elided*/ any : T_2 extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {} : never : never)[K_2]; } : never) : Path extends `${string}:${infer Param_1}` ? { [K_3 in Param_1 extends `${infer P_1}?` ? P_1 : Param_1]: string; } : {})[K]; } : never, ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, RequestBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuerySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<RequestQuerySchema> : Record<string, string | string[]>, Record<string, any>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown, Record<string, any>>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any, any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>) => void;
14
+ use(...middleware: RequestHandler[]): this;
15
+ use(...middleware: [string, ...RequestHandler[]]): this;
16
+ getRouter(): import("express-serve-static-core").Router;
17
+ }
18
+ export declare function createRouter(): TypedRouter;
19
+ export {};
package/dist/router.js ADDED
@@ -0,0 +1,38 @@
1
+ import {} from '@standard-schema/spec';
2
+ import { Router, } from 'express';
3
+ import { asyncMiddleware } from './middleware.js';
4
+ import { validateStandardSchema } from './validate.js';
5
+ export class TypedRouter {
6
+ router = Router();
7
+ get(path, validation = {}, ...middleware) {
8
+ return this.#handle('get', path, validation, ...middleware);
9
+ }
10
+ post(path, validation = {}, ...middleware) {
11
+ return this.#handle('post', path, validation, ...middleware);
12
+ }
13
+ put(path, validation = {}, ...middleware) {
14
+ return this.#handle('put', path, validation, ...middleware);
15
+ }
16
+ delete(path, validation = {}, ...middleware) {
17
+ return this.#handle('delete', path, validation, ...middleware);
18
+ }
19
+ #handle(method, path, validation, ...middleware) {
20
+ return (handler) => {
21
+ this.router[method](path, validateStandardSchema({
22
+ body: validation.request,
23
+ query: validation.query,
24
+ response: validation.response,
25
+ }), ...middleware, asyncMiddleware(handler));
26
+ };
27
+ }
28
+ use(...middleware) {
29
+ this.router.use(...middleware);
30
+ return this;
31
+ }
32
+ getRouter() {
33
+ return this.router;
34
+ }
35
+ }
36
+ export function createRouter() {
37
+ return new TypedRouter();
38
+ }
package/dist/routes.d.ts CHANGED
@@ -14,9 +14,23 @@ export declare function parseRoutes(app: Express): {
14
14
  } | null;
15
15
  query: {} | null;
16
16
  body: {} | null;
17
+ response: {} | null;
17
18
  }[];
18
19
  export interface Key {
19
20
  name: string;
20
21
  optional: boolean;
21
22
  offset: number;
22
23
  }
24
+ export declare function toOpenAPISpec(app: Express, info?: {
25
+ title: string;
26
+ version: string;
27
+ description?: string;
28
+ }): {
29
+ openapi: string;
30
+ info: {
31
+ description?: string | undefined;
32
+ title: string;
33
+ version: string;
34
+ };
35
+ paths: Record<string, any>;
36
+ };
package/dist/routes.js CHANGED
@@ -4,7 +4,11 @@ export function createDocsRouter(app, extras = {}) {
4
4
  return route(async () => {
5
5
  return {
6
6
  ...extras,
7
- routes: parseRoutes(app),
7
+ ...toOpenAPISpec(app, {
8
+ title: extras.title || 'API Documentation',
9
+ version: extras.version || '1.0.0',
10
+ description: extras.description,
11
+ }),
8
12
  };
9
13
  });
10
14
  }
@@ -21,6 +25,9 @@ export function parseRoutes(app) {
21
25
  body: metadata
22
26
  ?.filter((m) => typeof m === 'object' && m != null && 'bodySchema' in m)
23
27
  .at(-1)?.bodySchema ?? null,
28
+ response: metadata
29
+ ?.filter((m) => typeof m === 'object' && m != null && 'responseSchema' in m)
30
+ .at(-1)?.responseSchema ?? null,
24
31
  };
25
32
  });
26
33
  }
@@ -47,39 +54,11 @@ const buildPathParamsSchema = (pathParams) => {
47
54
  };
48
55
  // Fork of express-route-parser
49
56
  class ExpressPathParser {
57
+ _appPaths;
50
58
  getPaths() {
51
59
  return this._appPaths;
52
60
  }
53
61
  constructor(app) {
54
- this.traverse = (path, layer, keys) => {
55
- keys = [...keys, ...layer.keys];
56
- if (layer.name === 'router' && layer.handle) {
57
- for (const l of layer.handle.stack) {
58
- this.traverse(`${path || ''}/${pathRegexParser(layer.regexp, layer.keys)}`, l, keys);
59
- }
60
- }
61
- if (!layer.route || !layer.route.stack.length) {
62
- return;
63
- }
64
- this._appPaths.push(this.parseRouteLayer(layer, keys, path));
65
- };
66
- this.parseRouteLayer = (layer, keys, basePath) => {
67
- const lastRequestHandler = layer.route.stack[layer.route.stack.length - 1];
68
- const pathParams = keys.map((key) => {
69
- return { name: key.name, in: 'path', required: !key.optional };
70
- });
71
- const filtered = layer.route.stack.filter((element) => element?.handle?.metadata);
72
- const path = (basePath + layer.route.path).replace(/\/{2,}/gi, '/');
73
- if (filtered.length === 0) {
74
- return { path, pathParams, method: lastRequestHandler.method };
75
- }
76
- return {
77
- path,
78
- pathParams,
79
- method: lastRequestHandler.method,
80
- metadata: filtered.map((f) => f.handle.metadata),
81
- };
82
- };
83
62
  this._appPaths = [];
84
63
  const router = app._router;
85
64
  if (router) {
@@ -88,6 +67,35 @@ class ExpressPathParser {
88
67
  }
89
68
  }
90
69
  }
70
+ traverse = (path, layer, keys) => {
71
+ keys = [...keys, ...layer.keys];
72
+ if (layer.name === 'router' && layer.handle) {
73
+ for (const l of layer.handle.stack) {
74
+ this.traverse(`${path || ''}/${pathRegexParser(layer.regexp, layer.keys)}`, l, keys);
75
+ }
76
+ }
77
+ if (!layer.route || !layer.route.stack.length) {
78
+ return;
79
+ }
80
+ this._appPaths.push(this.parseRouteLayer(layer, keys, path));
81
+ };
82
+ parseRouteLayer = (layer, keys, basePath) => {
83
+ const lastRequestHandler = layer.route.stack[layer.route.stack.length - 1];
84
+ const pathParams = keys.map((key) => {
85
+ return { name: key.name, in: 'path', required: !key.optional };
86
+ });
87
+ const filtered = layer.route.stack.filter((element) => element?.handle?.metadata);
88
+ const path = (basePath + layer.route.path).replace(/\/{2,}/gi, '/');
89
+ if (filtered.length === 0) {
90
+ return { path, pathParams, method: lastRequestHandler.method };
91
+ }
92
+ return {
93
+ path,
94
+ pathParams,
95
+ method: lastRequestHandler.method,
96
+ metadata: filtered.map((f) => f.handle.metadata),
97
+ };
98
+ };
91
99
  }
92
100
  const pathRegexParser = (layerRegexPath, keys) => {
93
101
  if (typeof layerRegexPath === 'string') {
@@ -135,3 +143,95 @@ const mapKeysToPath = (layerRegexPath, keys) => {
135
143
  .replace(/\\/gi, '')
136
144
  .replace(/\/{2,}/gi, '/');
137
145
  };
146
+ export function toOpenAPISpec(app, info = {
147
+ title: 'API Documentation',
148
+ version: '1.0.0',
149
+ }) {
150
+ const routes = parseRoutes(app);
151
+ const paths = {};
152
+ for (const routeInfo of routes) {
153
+ const { method, path, params, query, body, response } = routeInfo;
154
+ // Convert Express path params to OpenAPI path params
155
+ const openApiPath = path.replace(/:([^/]+)/g, '{$1}');
156
+ if (!paths[openApiPath]) {
157
+ paths[openApiPath] = {};
158
+ }
159
+ const parameters = [];
160
+ // Add path parameters
161
+ if (params) {
162
+ parameters.push(...Object.keys(params.properties).map((name) => ({
163
+ name,
164
+ in: 'path',
165
+ required: params.required?.includes(name) ?? true,
166
+ schema: params.properties[name],
167
+ })));
168
+ }
169
+ // Add query parameters
170
+ if (query && typeof query === 'object' && 'properties' in query) {
171
+ const querySchema = query;
172
+ parameters.push(...Object.keys(querySchema.properties || {}).map((name) => ({
173
+ name,
174
+ in: 'query',
175
+ required: Array.isArray(querySchema.required) &&
176
+ querySchema.required.includes(name),
177
+ schema: (querySchema.properties || {})[name],
178
+ })));
179
+ }
180
+ paths[openApiPath][method.toLowerCase()] = {
181
+ parameters,
182
+ ...(body && {
183
+ requestBody: {
184
+ content: {
185
+ 'application/json': {
186
+ schema: body,
187
+ },
188
+ },
189
+ },
190
+ }),
191
+ responses: {
192
+ ...(response
193
+ ? {
194
+ '200': {
195
+ description: 'Successful response',
196
+ content: {
197
+ 'application/json': {
198
+ schema: response,
199
+ },
200
+ },
201
+ },
202
+ }
203
+ : {
204
+ '200': {
205
+ description: 'Successful response',
206
+ },
207
+ }),
208
+ '400': {
209
+ description: 'Bad Request',
210
+ content: {
211
+ 'application/json': {
212
+ schema: {
213
+ type: 'object',
214
+ properties: {
215
+ type: { type: 'string' },
216
+ title: { type: 'string' },
217
+ detail: { type: 'string' },
218
+ status: { type: 'number' },
219
+ },
220
+ required: ['type', 'title', 'status'],
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ };
227
+ }
228
+ return {
229
+ openapi: '3.1.0',
230
+ info: {
231
+ title: info.title,
232
+ version: info.version,
233
+ ...(info.description && { description: info.description }),
234
+ },
235
+ paths,
236
+ };
237
+ }
package/dist/sql.js CHANGED
@@ -4,6 +4,7 @@ const isRaw = (value) => {
4
4
  return (typeof value === 'object' && value !== null && 'raw' in value && !!value.raw);
5
5
  };
6
6
  export class SqlStatement {
7
+ stringsAndValues;
7
8
  constructor(strings, values) {
8
9
  this.stringsAndValues = [];
9
10
  this._extract(strings, values);
@@ -1,6 +1,5 @@
1
- import { type Options } from 'ajv';
2
- import { Type } from 'arktype';
3
- import { type RequestHandler } from 'express';
1
+ import { type StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { type NextFunction, type RequestHandler, type Request, type Response } from 'express';
4
3
  export type ValidationSchema = {
5
4
  type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
6
5
  format?: string;
@@ -28,27 +27,40 @@ export type ValidationSchema = {
28
27
  title?: string;
29
28
  description?: string;
30
29
  } | boolean;
31
- type AnySchema = Type | ValidationSchema;
32
- export declare function validateBody(schema: AnySchema): RequestHandler;
33
- export declare function validateQuery(schema: AnySchema): RequestHandler;
34
- export declare function validateParams(schema: AnySchema): RequestHandler;
30
+ type LegacyCompatibleSchema = ValidationSchema | StandardSchemaV1;
31
+ export declare function validateBody(schema: LegacyCompatibleSchema): RequestHandler;
32
+ export declare function validateQuery(schema: LegacyCompatibleSchema): RequestHandler;
33
+ export declare function validateParams(schema: LegacyCompatibleSchema): RequestHandler;
34
+ type ValidationErrors<Field extends string = string> = Record<Field, string[]>;
35
35
  export type ValidationResult = {
36
36
  isValid: true;
37
37
  errors: null;
38
38
  } | {
39
39
  isValid: false;
40
- errors: {
41
- [key: string]: any;
42
- };
40
+ errors: ValidationErrors;
43
41
  };
44
- export declare function compileValidateSchema(schema: ValidationSchema, options?: Options): (data: unknown) => {
45
- readonly isValid: true;
46
- readonly errors: null;
47
- } | {
48
- readonly isValid: false;
49
- readonly errors: {
50
- [key: string]: any[];
42
+ export interface ValidationSpec<RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined> {
43
+ request?: RequestBodySchema;
44
+ response?: ResponseBodySchema;
45
+ query?: RequestQuerySchema;
46
+ }
47
+ export declare function validateSchema(data: unknown, schema: StandardSchemaV1<any>): Promise<ValidationResult>;
48
+ export declare function validateStandardSchema(schemas: {
49
+ query?: StandardSchemaV1<any>;
50
+ body?: StandardSchemaV1<any>;
51
+ params?: StandardSchemaV1<any>;
52
+ response?: StandardSchemaV1<any>;
53
+ }): {
54
+ (req: Request, res: Response, next: NextFunction): Promise<void>;
55
+ metadata: {
56
+ bodySchema?: ValidationSchema;
57
+ querySchema?: ValidationSchema;
58
+ paramsSchema?: ValidationSchema;
59
+ responseSchema?: ValidationSchema;
51
60
  };
52
61
  };
53
- export declare function validateSchema(data: any, schema: AnySchema, options?: Options): ValidationResult;
62
+ type WithJsonSchema<T> = T & {
63
+ toJsonSchema: () => ValidationSchema;
64
+ };
65
+ export declare function jsonSchema(schema: ValidationSchema): WithJsonSchema<StandardSchemaV1<Record<string, any>>>;
54
66
  export {};
package/dist/validate.js CHANGED
@@ -1,120 +1,130 @@
1
+ import { standardValidate, ValidationError } from '@devmoods/fetch';
2
+ import {} from '@standard-schema/spec';
1
3
  import _Ajv, {} from 'ajv';
2
- import { Type } from 'arktype';
3
4
  import {} from 'express';
4
5
  import { BadRequest } from './errors.js';
5
6
  const Ajv = _Ajv;
6
- const toValidationSchema = (schema) => {
7
- if (schema instanceof Type) {
8
- return schema.toJsonSchema();
7
+ function toStandardSchema(schema) {
8
+ if (typeof schema === 'boolean' ||
9
+ (typeof schema === 'object' && schema != null && !('~standard' in schema))) {
10
+ return jsonSchema(schema);
9
11
  }
10
12
  return schema;
11
- };
13
+ }
12
14
  export function validateBody(schema) {
13
- schema = toValidationSchema(schema);
14
- const validate = compileValidateSchema(schema);
15
- const middleware = (req, res, next) => {
16
- const result = validate(req.body);
17
- if (!result.isValid) {
18
- return next(BadRequest({ body: result.errors }));
19
- }
20
- next();
21
- };
22
- middleware.metadata = { bodySchema: schema };
23
- return middleware;
15
+ return validateStandardSchema({ body: toStandardSchema(schema) });
24
16
  }
25
17
  export function validateQuery(schema) {
26
- schema = toValidationSchema(schema);
27
- const validate = compileValidateSchema(schema, { coerceTypes: true });
28
- const middleware = (req, res, next) => {
29
- const result = validate(req.query);
30
- if (!result.isValid) {
31
- return next(BadRequest({ query: result.errors }));
32
- }
33
- next();
34
- };
35
- middleware.metadata = { querySchema: schema };
36
- return middleware;
18
+ return validateStandardSchema({ query: toStandardSchema(schema) });
37
19
  }
38
20
  export function validateParams(schema) {
39
- schema = toValidationSchema(schema);
40
- const validate = compileValidateSchema(schema, { coerceTypes: true });
41
- const middleware = (req, res, next) => {
42
- const result = validate(req.params);
43
- if (!result.isValid) {
44
- return next(BadRequest({ params: result.errors }));
45
- }
46
- next();
47
- };
48
- middleware.metadata = { paramsSchema: schema };
49
- return middleware;
50
- }
51
- function createAjv(options) {
52
- const ajv = new Ajv({
53
- allErrors: true,
54
- validateFormats: false,
55
- ...options,
56
- });
57
- ajv.addKeyword('$enumNames');
58
- ajv.addKeyword('$bitiforms');
59
- ajv.addKeyword('$forms');
60
- return ajv;
21
+ return validateStandardSchema({ params: toStandardSchema(schema) });
61
22
  }
62
- export function compileValidateSchema(schema, options = {}) {
63
- const ajv = createAjv(options);
64
- const validate = ajv.compile(schema);
65
- return (data) => {
66
- const valid = validate(data);
67
- if (valid) {
68
- return {
69
- isValid: true,
70
- errors: null,
71
- };
72
- }
73
- const errors = transformAjvErrors(validate.errors);
74
- validate.errors = null;
23
+ export async function validateSchema(data, schema) {
24
+ try {
25
+ await standardValidate(schema, data);
26
+ }
27
+ catch (error) {
75
28
  return {
76
29
  isValid: false,
77
- errors,
30
+ errors: (error instanceof ValidationError
31
+ ? error.issues.map((issue) => [
32
+ String(issue.path ?? '/'),
33
+ issue.message,
34
+ ])
35
+ : []).reduce((result, item) => {
36
+ const [key, value] = item;
37
+ if (Array.isArray(result[key])) {
38
+ result[key].push(value);
39
+ }
40
+ else {
41
+ result[key] = [value];
42
+ }
43
+ return result;
44
+ }, {}),
78
45
  };
46
+ }
47
+ return { isValid: true, errors: null };
48
+ }
49
+ export function validateStandardSchema(schemas) {
50
+ const middleware = async (req, res, next) => {
51
+ try {
52
+ for (const [key, schema] of Object.entries(schemas)) {
53
+ if (key === 'response') {
54
+ continue;
55
+ }
56
+ if (schema) {
57
+ const result = await validateSchema(req[key], schema);
58
+ if (!result.isValid) {
59
+ return next(BadRequest({ [key]: result.errors }));
60
+ }
61
+ }
62
+ }
63
+ next();
64
+ }
65
+ catch (error) {
66
+ next(error);
67
+ }
79
68
  };
69
+ const metadata = {};
70
+ if (schemas.body) {
71
+ metadata.bodySchema = toJsonSchema(schemas.body);
72
+ }
73
+ if (schemas.query) {
74
+ metadata.querySchema = toJsonSchema(schemas.query);
75
+ }
76
+ if (schemas.params) {
77
+ metadata.paramsSchema = toJsonSchema(schemas.params);
78
+ }
79
+ if (schemas.response) {
80
+ metadata.responseSchema = toJsonSchema(schemas.response);
81
+ }
82
+ middleware.metadata = metadata;
83
+ return middleware;
80
84
  }
81
- export function validateSchema(data, schema, options = {}) {
82
- schema = toValidationSchema(schema);
83
- const ajv = createAjv(options);
84
- const valid = ajv.validate(schema, data);
85
- if (valid) {
86
- return {
87
- isValid: true,
88
- errors: null,
89
- };
85
+ function toJsonSchema(standardSchema) {
86
+ if ('toJsonSchema' in standardSchema &&
87
+ typeof standardSchema.toJsonSchema === 'function') {
88
+ return standardSchema.toJsonSchema();
90
89
  }
91
- const errors = transformAjvErrors(ajv.errors);
92
- ajv.errors = null;
93
- return {
94
- isValid: false,
95
- errors,
96
- };
90
+ return {};
97
91
  }
98
92
  function transformAjvErrors(errors) {
99
93
  /* c8 ignore next 3 */
100
94
  if (errors == null) {
101
95
  return {};
102
96
  }
103
- return errors
104
- .map((error) => {
105
- const { instancePath, keyword, params } = error;
97
+ return errors.map((error) => {
98
+ const { instancePath, keyword } = error;
106
99
  const property = instancePath || '/';
107
- const messageWithArgs = { message: `errors.${keyword}`, args: params };
108
- return [property, messageWithArgs];
109
- })
110
- .reduce((result, item) => {
111
- const [key, value] = item;
112
- if (Array.isArray(result[key])) {
113
- result[key].push(value);
114
- }
115
- else {
116
- result[key] = [value];
117
- }
118
- return result;
119
- }, {});
100
+ return { path: property, message: keyword };
101
+ });
102
+ }
103
+ export function jsonSchema(schema) {
104
+ const ajv = new Ajv({
105
+ allErrors: true,
106
+ validateFormats: false,
107
+ });
108
+ ajv.addKeyword('$enumNames');
109
+ ajv.addKeyword('$bitiforms');
110
+ ajv.addKeyword('$forms');
111
+ const validate = ajv.compile(schema);
112
+ return {
113
+ toJsonSchema() {
114
+ return schema;
115
+ },
116
+ '~standard': {
117
+ version: 1,
118
+ vendor: 'devmoods-ajv',
119
+ validate(value) {
120
+ const valid = validate(value);
121
+ if (valid) {
122
+ return { value };
123
+ }
124
+ const issues = transformAjvErrors(validate.errors);
125
+ validate.errors = null;
126
+ return { issues };
127
+ },
128
+ },
129
+ };
120
130
  }
@@ -3,4 +3,4 @@ export interface SsrMiddlewareOptions<InitialData = unknown> {
3
3
  getInitialData?: (req: Request) => Promise<InitialData>;
4
4
  useDevServer?: boolean;
5
5
  }
6
- export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<import("compose-middleware").RequestHandler<unknown, unknown, void>>;
6
+ export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<import("compose-middleware").RequestHandler<any, any, void>>;
package/dist/vite/ssr.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import {} from 'compose-middleware';
3
4
  import express, {} from 'express';
4
5
  import { createServer as createViteServer } from 'vite';
5
6
  import {} from './types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,33 +46,35 @@
46
46
  "devDependencies": {
47
47
  "@devmoods/eslint-config": "^2.6.0",
48
48
  "@types/koa-compose": "^3.2.8",
49
- "@types/node": "22.13.5",
49
+ "@types/node": "22.13.10",
50
50
  "@types/supertest": "^6.0.2",
51
51
  "@vitejs/plugin-react": "^4.3.4",
52
- "@vitest/coverage-v8": "^3.0.7",
53
- "arktype": "^2.1.0",
52
+ "@vitest/coverage-v8": "^3.0.8",
53
+ "arktype": "^2.1.9",
54
54
  "cookiejar": "^2.1.4",
55
- "eslint": "^9.21.0",
56
- "faktory-worker": "^4.6.0",
57
- "prettier": "3.5.2",
55
+ "eslint": "^9.22.0",
56
+ "faktory-worker": "^4.7.0",
57
+ "prettier": "3.5.3",
58
58
  "supertest": "7.0.0",
59
59
  "ts-node": "10.9.2",
60
60
  "tsx": "^4.19.3",
61
- "typescript": "5.7.3",
62
- "vite": "^6.1.1",
63
- "vitest": "^3.0.7",
61
+ "typescript": "5.8.2",
62
+ "vite": "^6.2.1",
63
+ "vitest": "^3.0.8",
64
64
  "vitest-fail-on-console": "^0.7.1"
65
65
  },
66
66
  "dependencies": {
67
- "@devmoods/fetch": "^4.0.1",
68
- "@sentry/node": "^9.2.0",
69
- "@sentry/profiling-node": "^9.2.0",
67
+ "@devmoods/fetch": "^4.1.0",
68
+ "@sentry/node": "^9.5.0",
69
+ "@sentry/profiling-node": "^9.5.0",
70
+ "@standard-schema/spec": "^1.0.0",
70
71
  "@types/bcrypt": "^5.0.2",
71
72
  "@types/cookie-parser": "^1.4.8",
72
73
  "@types/express": "^5.0.0",
73
74
  "@types/jsonwebtoken": "^9.0.9",
74
75
  "@types/nodemailer": "^6.4.17",
75
76
  "@types/pg": "^8.11.11",
77
+ "@types/qs": "^6.9.18",
76
78
  "@types/superagent": "^8.1.9",
77
79
  "@types/uuid": "^10.0.0",
78
80
  "ajv": "^8.17.1",
@@ -87,7 +89,7 @@
87
89
  "helmet": "^8.0.0",
88
90
  "jsonwebtoken": "^9.0.2",
89
91
  "nodemailer": "^6.10.0",
90
- "pg": "^8.13.3",
92
+ "pg": "^8.14.0",
91
93
  "redis": "^4.7.0",
92
94
  "sonda": "^0.7.1",
93
95
  "sql-formatter": "^15.4.11",