@devmoods/express-extras 0.65.0 → 0.66.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.
Files changed (54) hide show
  1. package/dist/auth/emailAndPassword.js +4 -4
  2. package/dist/auth/errors.d.ts +2 -2
  3. package/dist/auth/errors.js +4 -4
  4. package/dist/auth/google.js +2 -2
  5. package/dist/auth/passwordless.js +1 -1
  6. package/dist/auth/token.js +3 -1
  7. package/dist/auth/tokenStorage.d.ts +16 -0
  8. package/dist/auth/tokenStorage.js +44 -0
  9. package/dist/auth/types.d.ts +1 -3
  10. package/dist/auth.d.ts +7 -15
  11. package/dist/auth.js +29 -41
  12. package/dist/channels.d.ts +19 -1
  13. package/dist/channels.js +23 -34
  14. package/dist/composeMiddleware.d.ts +15 -0
  15. package/dist/composeMiddleware.js +63 -0
  16. package/dist/config.d.ts +6 -0
  17. package/dist/config.js +3 -0
  18. package/dist/context.d.ts +4 -0
  19. package/dist/context.js +8 -0
  20. package/dist/csrf.d.ts +1 -0
  21. package/dist/csrf.js +6 -2
  22. package/dist/errors.d.ts +7 -7
  23. package/dist/errors.js +11 -11
  24. package/dist/faktory.d.ts +13 -11
  25. package/dist/faktory.js +6 -6
  26. package/dist/listen.d.ts +18 -9
  27. package/dist/listen.js +64 -38
  28. package/dist/logger.d.ts +23 -4
  29. package/dist/logger.js +65 -45
  30. package/dist/manage.d.ts +2 -2
  31. package/dist/manage.js +12 -12
  32. package/dist/middleware.d.ts +4 -2
  33. package/dist/middleware.js +10 -3
  34. package/dist/rate-limit.d.ts +14 -4
  35. package/dist/rate-limit.js +10 -5
  36. package/dist/redis.d.ts +0 -3
  37. package/dist/redis.js +0 -23
  38. package/dist/router.js +1 -1
  39. package/dist/routes.d.ts +5 -4
  40. package/dist/routes.js +34 -23
  41. package/dist/sentry.js +5 -3
  42. package/dist/streams.d.ts +7 -0
  43. package/dist/streams.js +29 -0
  44. package/dist/utils.d.ts +0 -2
  45. package/dist/utils.js +0 -8
  46. package/dist/validate.d.ts +33 -12
  47. package/dist/validate.js +6 -1
  48. package/dist/vite/defineViteConfig.js +1 -0
  49. package/dist/vite/ssr.d.ts +20 -1
  50. package/dist/vite/ssr.js +6 -1
  51. package/dist/vitest/matchers.d.ts +4 -0
  52. package/dist/vitest/matchers.js +17 -2
  53. package/dist/vitest/testClient.d.ts +1 -0
  54. package/package.json +18 -20
package/dist/manage.js CHANGED
@@ -33,11 +33,13 @@ export function registerDefaultCommands(app, program = defaultProgram) {
33
33
  }
34
34
  if (options.openapi) {
35
35
  const routes = toOpenAPISpec(app);
36
+ // eslint-disable-next-line no-console
36
37
  console.log(JSON.stringify(routes, null, 2));
37
38
  return;
38
39
  }
39
40
  const routes = parseRoutes(app);
40
41
  if (options.json) {
42
+ // eslint-disable-next-line no-console
41
43
  console.log(JSON.stringify(routes, null, 2));
42
44
  return;
43
45
  }
@@ -58,6 +60,7 @@ export function registerDefaultCommands(app, program = defaultProgram) {
58
60
  .option('--json', 'Output JSON')
59
61
  .action(cli.wrap(async (options) => {
60
62
  if (options.json) {
63
+ // eslint-disable-next-line no-console
61
64
  console.log(JSON.stringify(config, null, 2));
62
65
  return;
63
66
  }
@@ -76,10 +79,11 @@ export function registerDefaultCommands(app, program = defaultProgram) {
76
79
  path: openApiFile,
77
80
  },
78
81
  output: {
79
- format: false,
82
+ format: null,
80
83
  path: options.output,
81
- lint: false,
84
+ lint: null,
82
85
  tsConfigPath: resolve('./tsconfig.json'),
86
+ importFileExtension: '.js',
83
87
  },
84
88
  plugins: [
85
89
  {
@@ -89,6 +93,7 @@ export function registerDefaultCommands(app, program = defaultProgram) {
89
93
  {
90
94
  asClass: true,
91
95
  classStructure: 'off',
96
+ /* c8 ignore next 1 */
92
97
  classNameBuilder: (name) => `${name}Service`,
93
98
  name: '@hey-api/sdk',
94
99
  },
@@ -101,16 +106,11 @@ export function registerDefaultCommands(app, program = defaultProgram) {
101
106
  .option('--output <string>', 'Output file path', 'src/types/db.gen.ts')
102
107
  .option('--connection <string>', 'Database connection string')
103
108
  .action(cli.wrap(async (options) => {
104
- try {
105
- await generateDatabaseTypes({
106
- outputPath: options.output,
107
- connectionString: options.connection || config.POSTGRES_URL,
108
- });
109
- cli.writeLine(`✅ Database types generated successfully at: ${options.output}\n`);
110
- }
111
- catch (error) {
112
- cli.writeLine(`❌ Failed to generate database types: ${error}\n`);
113
- }
109
+ await generateDatabaseTypes({
110
+ outputPath: options.output,
111
+ connectionString: options.connection || config.POSTGRES_URL,
112
+ });
113
+ cli.writeLine(`Database types generated successfully at: ${options.output}\n`);
114
114
  }));
115
115
  }, program);
116
116
  }
@@ -1,7 +1,7 @@
1
1
  import { type ServerResponse, type IncomingMessage } from 'node:http';
2
- import { compose } from 'compose-middleware';
3
2
  import { type NextFunction, type RequestHandler, type Request, type Response, json as jsonParser, urlencoded as urlencodedParser } from 'express';
4
3
  import { type createAuth } from './auth.js';
4
+ import { compose } from './composeMiddleware.js';
5
5
  import { type CsrfMiddlewareOptions } from './csrf.js';
6
6
  export { urlencodedParser, jsonParser };
7
7
  export { asyncMiddleware as route };
@@ -50,10 +50,12 @@ export interface BeforeMiddlewareOptions {
50
50
  auth?: ReturnType<typeof createAuth>;
51
51
  }
52
52
  export declare function useCspNonce(): string;
53
+ export interface BeforeMiddleware extends RequestHandler {
54
+ }
53
55
  /**
54
56
  * Register middleware that must come before the app routes.
55
57
  */
56
- export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
58
+ export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth, }?: BeforeMiddlewareOptions): BeforeMiddleware;
57
59
  /**
58
60
  * Middleware that sends a 404
59
61
  *
@@ -2,11 +2,11 @@ import * as crypto from 'node:crypto';
2
2
  import {} from 'node:http';
3
3
  import { config } from '@devmoods/config';
4
4
  import { startTimer } from '@devmoods/observability';
5
- import { compose } from 'compose-middleware';
6
5
  import cookieParser from 'cookie-parser';
7
6
  import { json as jsonParser, urlencoded as urlencodedParser, } from 'express';
8
7
  import helmet from 'helmet';
9
8
  import {} from './auth.js';
9
+ import { compose } from './composeMiddleware.js';
10
10
  import { ANONYMOUS_SESSION_KEY, setRequestContext, useRequestContext, withRequestContext, } from './context.js';
11
11
  import { csrf, doubleCsrfProtection, } from './csrf.js';
12
12
  import { NotFound } from './errors.js';
@@ -17,6 +17,7 @@ export { asyncMiddleware as route };
17
17
  export { compose as composeMiddleware };
18
18
  const CSP_NONCE_KEY = 'csp-nonce';
19
19
  export function useCspNonce() {
20
+ /* c8 ignore next 1 */
20
21
  return useRequestContext(CSP_NONCE_KEY) ?? '__CSP_NOT_SET__';
21
22
  }
22
23
  /**
@@ -32,7 +33,7 @@ export function before({ cookies = true, helmet: useHelmet = true, json: useJson
32
33
  : useCsrf
33
34
  ? csrf()
34
35
  : undefined,
35
- auth && auth.before(),
36
+ auth && auth.middleware(false),
36
37
  logRequestDuration(),
37
38
  setCspNonce(),
38
39
  useHelmet &&
@@ -96,6 +97,7 @@ function logRequestDuration() {
96
97
  path: req.path,
97
98
  query: req.query,
98
99
  ip: req.ip,
100
+ /* c8 ignore next 5 */
99
101
  userAgent: req.headers['user-agent'] ?? 'unknown',
100
102
  accept: req.headers['accept'] ?? 'unknown',
101
103
  host: req.headers['host'] ?? 'unknown',
@@ -204,6 +206,11 @@ export function asyncMiddleware(fn) {
204
206
  res.status(result).end();
205
207
  }
206
208
  else if (typeof result !== 'undefined') {
209
+ // @ts-expect-error custom value
210
+ const successStatus = req.__successStatus;
211
+ if (successStatus != null) {
212
+ res.status(successStatus);
213
+ }
207
214
  res.send(result);
208
215
  }
209
216
  })
@@ -221,10 +228,10 @@ export function withTransaction(wrapper) {
221
228
  res.on('error', reject);
222
229
  next();
223
230
  });
231
+ /* c8 ignore next 5 */
224
232
  });
225
233
  }
226
234
  catch (error) {
227
- /* c8 ignore next 2 */
228
235
  next(error);
229
236
  }
230
237
  };
@@ -1,5 +1,15 @@
1
1
  import { type RequestHandler, type Request } from 'express';
2
2
  import { type RedisClient } from './redis.js';
3
+ export interface RateLimitMiddleware extends RequestHandler {
4
+ /**
5
+ * Clear the rate limit for a specific key
6
+ */
7
+ clear: (key: string) => Promise<void>;
8
+ /**
9
+ * Clear all rate limits
10
+ */
11
+ clearAll: () => Promise<number>;
12
+ }
3
13
  export interface RateLimitOptions {
4
14
  /**
5
15
  * Maximum number of requests allowed within the window
@@ -17,7 +27,7 @@ export interface RateLimitOptions {
17
27
  */
18
28
  getKey?: (req: Request) => string;
19
29
  }
20
- export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey, }?: RateLimitOptions): RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>> & {
21
- clear: (key: string) => Promise<void>;
22
- clearAll: () => Promise<number>;
23
- };
30
+ /**
31
+ * Returns a middleware for rate limiting requests.
32
+ */
33
+ export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey, }?: RateLimitOptions): RateLimitMiddleware;
@@ -2,9 +2,14 @@ import { config } from '@devmoods/config';
2
2
  import {} from 'express';
3
3
  import { makeError } from './errors.js';
4
4
  import {} from './redis.js';
5
- export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60, getKey = (req) => req.ip || 'unknown', } = {}) {
5
+ /**
6
+ * Returns a middleware for rate limiting requests.
7
+ */
8
+ export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60,
9
+ /* c8 ignore next 1 */
10
+ getKey = (req) => req.ip || 'unknown', } = {}) {
6
11
  const keyPrefix = `${config.REDIS_KEY_PREFIX}rate-limit:`;
7
- const handler = async (req, res, next) => {
12
+ const middleware = async (req, res, next) => {
8
13
  try {
9
14
  const key = `${keyPrefix}${getKey(req)}`;
10
15
  // Use Redis to track requests
@@ -51,7 +56,7 @@ export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60, getKey =
51
56
  } while (cursor !== '0');
52
57
  return deletedCount;
53
58
  };
54
- handler.clear = clear;
55
- handler.clearAll = clearAll;
56
- return handler;
59
+ middleware.clear = clear;
60
+ middleware.clearAll = clearAll;
61
+ return middleware;
57
62
  }
package/dist/redis.d.ts CHANGED
@@ -5,6 +5,3 @@ export interface RedisOptions {
5
5
  }
6
6
  export type RedisClient = ReturnType<typeof createClient>;
7
7
  export declare function createRedis({ url, onError, }?: RedisOptions): RedisClient;
8
- export declare function connectRedis(redis: RedisClient, shouldContinue?: boolean): Promise<void>;
9
- export declare function stopRedis(redis: RedisClient, timeout?: number): Promise<void>;
10
- export declare function pingRedis(redis: RedisClient, timeout?: number): Promise<string>;
package/dist/redis.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { config } from '@devmoods/config';
2
2
  import { createClient } from 'redis';
3
- import { withTimeout } from './utils.js';
4
3
  export function createRedis({ url = config.REDIS_URL, onError, } = {}) {
5
4
  const client = createClient({ url });
6
5
  // Always attach a listener even if
@@ -10,25 +9,3 @@ export function createRedis({ url = config.REDIS_URL, onError, } = {}) {
10
9
  });
11
10
  return client;
12
11
  }
13
- export async function connectRedis(redis, shouldContinue = true) {
14
- try {
15
- await withTimeout(redis.connect(), 10000);
16
- }
17
- catch (error) {
18
- if (shouldContinue) {
19
- return;
20
- }
21
- throw new Error('Failed to connect to Redis', { cause: error });
22
- }
23
- }
24
- export async function stopRedis(redis, timeout = 10000) {
25
- try {
26
- return await withTimeout(redis.close(), timeout);
27
- }
28
- catch {
29
- return redis.destroy();
30
- }
31
- }
32
- export function pingRedis(redis, timeout = 1000) {
33
- return withTimeout(redis.ping(), timeout);
34
- }
package/dist/router.js CHANGED
@@ -22,7 +22,7 @@ export class TypedRouter {
22
22
  body: validation.request,
23
23
  query: validation.query,
24
24
  response: validation.response,
25
- }), ...middleware, asyncMiddleware(handler));
25
+ }, { successStatus: validation.successStatus }), ...middleware, asyncMiddleware(handler));
26
26
  return this;
27
27
  }
28
28
  use(...middleware) {
package/dist/routes.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type Express, type RequestHandler } from 'express';
2
- export declare function createDocsRouter(app: Express, extras?: Record<string, any>): RequestHandler;
3
- export declare function parseRoutes(app: Express): {
1
+ import { type Application, type RequestHandler } from 'express';
2
+ export declare function createDocsRouter(app: Application, extras?: Record<string, any>): RequestHandler;
3
+ export declare function parseRoutes(app: Application): {
4
4
  method: string;
5
5
  path: string;
6
6
  params: {
@@ -15,6 +15,7 @@ export declare function parseRoutes(app: Express): {
15
15
  query: {} | null;
16
16
  body: {} | null;
17
17
  response: {} | null;
18
+ successStatus: {} | null;
18
19
  }[];
19
20
  export interface Key {
20
21
  name: string;
@@ -23,7 +24,7 @@ export interface Key {
23
24
  }
24
25
  export declare function extractTagsFromPath(path: string): string[];
25
26
  export declare function convertPathToOperationId(method: string, path: string): string;
26
- export declare function toOpenAPISpec(app: Express, info?: {
27
+ export declare function toOpenAPISpec(app: Application, info?: {
27
28
  title: string;
28
29
  version: string;
29
30
  description?: string;
package/dist/routes.js CHANGED
@@ -28,6 +28,9 @@ export function parseRoutes(app) {
28
28
  response: metadata
29
29
  ?.filter((m) => typeof m === 'object' && m != null && 'responseSchema' in m)
30
30
  .at(-1)?.responseSchema ?? null,
31
+ successStatus: metadata
32
+ ?.filter((m) => typeof m === 'object' && m != null && 'successStatus' in m)
33
+ .at(-1)?.successStatus ?? null,
31
34
  };
32
35
  });
33
36
  }
@@ -35,6 +38,7 @@ const buildPathParamsSchema = (pathParams) => {
35
38
  const required = [];
36
39
  const params = [];
37
40
  for (const param of pathParams) {
41
+ /* c8 ignore next 3 */
38
42
  if (param.in !== 'path') {
39
43
  continue;
40
44
  }
@@ -54,12 +58,12 @@ const buildPathParamsSchema = (pathParams) => {
54
58
  };
55
59
  // Fork of express-route-parser
56
60
  class ExpressPathParser {
57
- _appPaths;
61
+ #appPaths;
58
62
  getPaths() {
59
- return this._appPaths;
63
+ return this.#appPaths;
60
64
  }
61
65
  constructor(app) {
62
- this._appPaths = [];
66
+ this.#appPaths = [];
63
67
  const router = app._router;
64
68
  if (router) {
65
69
  for (const layer of router.stack) {
@@ -77,7 +81,7 @@ class ExpressPathParser {
77
81
  if (!layer.route || !layer.route.stack.length) {
78
82
  return;
79
83
  }
80
- this._appPaths.push(this.parseRouteLayer(layer, keys, path));
84
+ this.#appPaths.push(this.parseRouteLayer(layer, keys, path));
81
85
  };
82
86
  parseRouteLayer = (layer, keys, basePath) => {
83
87
  const lastRequestHandler = layer.route.stack[layer.route.stack.length - 1];
@@ -98,19 +102,18 @@ class ExpressPathParser {
98
102
  };
99
103
  }
100
104
  const pathRegexParser = (layerRegexPath, keys) => {
105
+ /* c8 ignore next 3 */
101
106
  if (typeof layerRegexPath === 'string') {
102
107
  return layerRegexPath;
103
108
  }
104
109
  if (layerRegexPath.fast_slash) {
105
110
  return '';
106
111
  }
112
+ /* c8 ignore next 3 */
107
113
  if (layerRegexPath.fast_star) {
108
114
  return '*';
109
115
  }
110
- let mappedPath = '';
111
- if (keys && keys.length) {
112
- mappedPath = mapKeysToPath(layerRegexPath, keys);
113
- }
116
+ const mappedPath = mapKeysToPath(layerRegexPath, keys);
114
117
  const match = layerRegexPath
115
118
  .toString()
116
119
  .replace('\\/?', '')
@@ -121,12 +124,13 @@ const pathRegexParser = (layerRegexPath, keys) => {
121
124
  }
122
125
  if (mappedPath) {
123
126
  return mappedPath.slice(1);
127
+ /* c8 ignore next 3 */
124
128
  }
125
129
  return layerRegexPath.toString();
126
130
  };
127
131
  const mapKeysToPath = (layerRegexPath, keys) => {
128
132
  if (!keys || keys.length === 0) {
129
- throw Error('must include atleast one key to map');
133
+ return '';
130
134
  }
131
135
  let convertedSubPath = layerRegexPath.toString();
132
136
  for (const key of keys) {
@@ -183,7 +187,7 @@ export function toOpenAPISpec(app, info = {
183
187
  const paths = {};
184
188
  const allTags = new Set();
185
189
  for (const routeInfo of routes) {
186
- const { method, path, params, query, body, response } = routeInfo;
190
+ const { method, path, params, query, body, response, successStatus } = routeInfo;
187
191
  if (!path.startsWith('/api/')) {
188
192
  continue;
189
193
  }
@@ -202,6 +206,7 @@ export function toOpenAPISpec(app, info = {
202
206
  parameters.push(...Object.keys(params.properties).map((name) => ({
203
207
  name,
204
208
  in: 'path',
209
+ /* c8 ignore next 1 */
205
210
  required: params.required?.includes(name) ?? true,
206
211
  schema: params.properties[name],
207
212
  })));
@@ -209,12 +214,14 @@ export function toOpenAPISpec(app, info = {
209
214
  // Add query parameters
210
215
  if (query && typeof query === 'object' && 'properties' in query) {
211
216
  const querySchema = query;
212
- parameters.push(...Object.keys(querySchema.properties || {}).map((name) => ({
217
+ /* c8 ignore next 1 */
218
+ const properties = querySchema.properties || {};
219
+ parameters.push(...Object.keys(properties).map((name) => ({
213
220
  name,
214
221
  in: 'query',
215
222
  required: Array.isArray(querySchema.required) &&
216
223
  querySchema.required.includes(name),
217
- schema: (querySchema.properties || {})[name],
224
+ schema: properties[name],
218
225
  })));
219
226
  }
220
227
  paths[openApiPath][method.toLowerCase()] = {
@@ -231,22 +238,26 @@ export function toOpenAPISpec(app, info = {
231
238
  },
232
239
  }),
233
240
  responses: {
234
- ...(response
235
- ? {
236
- '200': {
237
- description: 'Successful response',
238
- content: {
239
- 'application/json': {
240
- schema: response,
241
+ ...(() => {
242
+ const code = String(successStatus ?? 200);
243
+ if (response) {
244
+ return {
245
+ [code]: {
246
+ description: 'Successful response',
247
+ content: {
248
+ 'application/json': {
249
+ schema: response,
250
+ },
241
251
  },
242
252
  },
243
- },
253
+ };
244
254
  }
245
- : {
246
- '200': {
255
+ return {
256
+ [code]: {
247
257
  description: 'Successful response',
248
258
  },
249
- }),
259
+ };
260
+ })(),
250
261
  '400': {
251
262
  description: 'Bad Request',
252
263
  content: {
package/dist/sentry.js CHANGED
@@ -4,11 +4,13 @@ import { nodeProfilingIntegration } from '@sentry/profiling-node';
4
4
  export function sentryOptions(options = {}) {
5
5
  return {
6
6
  dsn: config.SENTRY_DSN,
7
- tracesSampleRate: 1.0,
8
- profilesSampleRate: 1.0,
7
+ tracesSampleRate: config.SENTRY_TRACES_SAMPLE_RATE,
8
+ profilesSampleRate: config.SENTRY_PROFILES_SAMPLE_RATE,
9
9
  integrations: [
10
10
  nodeProfilingIntegration(),
11
- eventLoopBlockIntegration({ threshold: 300 }),
11
+ eventLoopBlockIntegration({
12
+ threshold: config.SENTRY_EVENT_LOOP_THRESHOLD,
13
+ }),
12
14
  ],
13
15
  enableLogs: true,
14
16
  ...options,
@@ -0,0 +1,7 @@
1
+ import { type Writable } from 'node:stream';
2
+ export interface ClientState {
3
+ draining: boolean;
4
+ queue: string[];
5
+ maxQueue: number;
6
+ }
7
+ export declare function writeChunk(writable: Writable, chunk: string, state: ClientState): void;
@@ -0,0 +1,29 @@
1
+ import {} from 'node:stream';
2
+ export function writeChunk(writable, chunk, state) {
3
+ // If we're already draining, just enqueue.
4
+ if (state.draining) {
5
+ if (state.queue.length >= state.maxQueue) {
6
+ // Drop the oldest chunk to keep memory bounded.
7
+ state.queue.shift();
8
+ }
9
+ state.queue.push(chunk);
10
+ return;
11
+ }
12
+ if (writable.write(chunk)) {
13
+ return;
14
+ }
15
+ // Buffer is full — start draining
16
+ state.draining = true;
17
+ const flush = () => {
18
+ while (state.queue.length) {
19
+ const next = state.queue.shift();
20
+ if (!writable.write(next)) {
21
+ state.draining = true;
22
+ writable.once('drain', flush);
23
+ return;
24
+ }
25
+ }
26
+ state.draining = false;
27
+ };
28
+ writable.once('drain', flush);
29
+ }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type EventEmitter } from 'node:events';
2
1
  import { withTimeout, TimeoutError, delay } from '@devmoods/fetch';
3
2
  export { withTimeout, TimeoutError, delay };
4
3
  export declare function readEnv(key: string): string | undefined;
@@ -10,4 +9,3 @@ export declare namespace readEnv {
10
9
  declare function readEnvNumber(key: string, defaultValue: number): number;
11
10
  declare function readEnvBoolean(key: string, defaultValue: boolean): boolean;
12
11
  export declare const envPort: (defaultPort?: number) => number;
13
- export declare function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown>;
package/dist/utils.js CHANGED
@@ -1,4 +1,3 @@
1
- import {} from 'node:events';
2
1
  import { withTimeout, TimeoutError, delay } from '@devmoods/fetch';
3
2
  export { withTimeout, TimeoutError, delay };
4
3
  export function readEnv(key, defaultValue) {
@@ -15,10 +14,3 @@ function readEnvBoolean(key, defaultValue) {
15
14
  return ['true', '1', 't'].includes(readEnv(key, `${defaultValue}`).toLowerCase());
16
15
  }
17
16
  export const envPort = (defaultPort = 3000) => readEnv.number('PORT', defaultPort);
18
- export async function waitForEvent(emitter, event) {
19
- return new Promise((resolve) => {
20
- emitter.once(event, (...args) => {
21
- resolve([args]);
22
- });
23
- });
24
- }
@@ -1,5 +1,5 @@
1
1
  import { type StandardSchemaV1 } from '@standard-schema/spec';
2
- import { type NextFunction, type RequestHandler, type Request, type Response } from 'express';
2
+ import { type RequestHandler } from 'express';
3
3
  export type ValidationSchema = {
4
4
  type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
5
5
  format?: string;
@@ -39,29 +39,50 @@ export type ValidationResult = {
39
39
  isValid: false;
40
40
  errors: ValidationErrors;
41
41
  };
42
- export interface ValidationSpec<RequestBodySchema extends StandardSchemaV1<any> | undefined = undefined, ResponseBodySchema extends StandardSchemaV1<any> | undefined = undefined, RequestQuerySchema extends StandardSchemaV1<any> | undefined = undefined, RequestParamsSchema extends StandardSchemaV1<any> | undefined = undefined> {
42
+ export interface ValidationSpec<RequestBodySchema extends StandardSchemaV1 | undefined = undefined, ResponseBodySchema extends StandardSchemaV1 | undefined = undefined, RequestQuerySchema extends StandardSchemaV1 | undefined = undefined, RequestParamsSchema extends StandardSchemaV1 | undefined = undefined> {
43
+ /**
44
+ * Standard schema describing the HTTP request body.
45
+ */
43
46
  request?: RequestBodySchema;
47
+ /**
48
+ * Standard schema describing the return HTTP response body.
49
+ */
44
50
  response?: ResponseBodySchema;
51
+ /**
52
+ * Schema describing the shape of the `query` parameters.
53
+ * All values should be strings.
54
+ */
45
55
  query?: RequestQuerySchema;
56
+ /**
57
+ * Schema describing the shape of the `params` parameters.
58
+ */
46
59
  params?: RequestParamsSchema;
60
+ /**
61
+ * Expected success HTTP status code for this route (e.g., 200, 201, 204).
62
+ * Used by documentation generators to infer correct response code.
63
+ */
64
+ successStatus?: number;
47
65
  }
48
- export declare function validateSchema(data: unknown, schema: StandardSchemaV1<any>): Promise<ValidationResult>;
49
- export declare function validateStandardSchema(schemas: {
50
- query?: StandardSchemaV1<any>;
51
- body?: StandardSchemaV1<any>;
52
- params?: StandardSchemaV1<any>;
53
- response?: StandardSchemaV1<any>;
54
- }): {
55
- (req: Request, res: Response, next: NextFunction): Promise<void>;
66
+ export declare function validateSchema(data: unknown, schema: StandardSchemaV1): Promise<ValidationResult>;
67
+ export interface ValidateStandardSchemaMiddleware extends RequestHandler {
56
68
  metadata: {
57
69
  bodySchema?: ValidationSchema;
58
70
  querySchema?: ValidationSchema;
59
71
  paramsSchema?: ValidationSchema;
60
72
  responseSchema?: ValidationSchema;
73
+ successStatus?: number;
61
74
  };
62
- };
75
+ }
76
+ export declare function validateStandardSchema(schemas: {
77
+ query?: StandardSchemaV1;
78
+ body?: StandardSchemaV1;
79
+ params?: StandardSchemaV1;
80
+ response?: StandardSchemaV1;
81
+ }, extraMetadata?: {
82
+ successStatus?: number;
83
+ }): ValidateStandardSchemaMiddleware;
63
84
  type WithJsonSchema<T> = T & {
64
85
  toJsonSchema: () => ValidationSchema;
65
86
  };
66
- export declare function jsonSchema(schema: ValidationSchema): WithJsonSchema<StandardSchemaV1<Record<string, any>>>;
87
+ export declare function jsonSchema<Input>(schema: ValidationSchema): WithJsonSchema<StandardSchemaV1<Input>>;
67
88
  export {};
package/dist/validate.js CHANGED
@@ -47,9 +47,13 @@ export async function validateSchema(data, schema) {
47
47
  }
48
48
  return { isValid: true, errors: null };
49
49
  }
50
- export function validateStandardSchema(schemas) {
50
+ export function validateStandardSchema(schemas, extraMetadata) {
51
51
  const middleware = async (req, res, next) => {
52
52
  try {
53
+ if (extraMetadata?.successStatus != null) {
54
+ // @ts-expect-error custom value
55
+ req.__successStatus = extraMetadata.successStatus;
56
+ }
53
57
  for (const [key, schema] of Object.entries(schemas)) {
54
58
  if (key === 'response') {
55
59
  continue;
@@ -81,6 +85,7 @@ export function validateStandardSchema(schemas) {
81
85
  if (schemas.response) {
82
86
  metadata.responseSchema = toJsonSchema(schemas.response);
83
87
  }
88
+ Object.assign(metadata, extraMetadata);
84
89
  middleware.metadata = metadata;
85
90
  Object.defineProperty(middleware, 'name', {
86
91
  value: validateStandardSchema.name,
@@ -82,6 +82,7 @@ export function defineViteConfig({ base = '', reactCompiler = false, ssr = false
82
82
  globalSetup,
83
83
  include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
84
84
  globals: true,
85
+ restoreMocks: true,
85
86
  },
86
87
  });
87
88
  }
@@ -1,7 +1,26 @@
1
+ import { type RequestHandler } from 'express';
1
2
  export interface SsrMiddlewareOptions<InitialData = unknown> {
3
+ /**
4
+ * Root directory where Vite will look up files.
5
+ */
2
6
  root?: string;
7
+ /**
8
+ * Function to get the server side data for the request.
9
+ */
3
10
  getInitialData?: (req: Request) => Promise<InitialData>;
11
+ /**
12
+ * Whether or not to use the dev server.
13
+ *
14
+ * Uses `config.VITE_USE_DEV_SERVER` if not provided.
15
+ */
4
16
  useDevServer?: boolean;
5
17
  }
6
- export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<import("compose-middleware").RequestHandler<any, any, void>>;
18
+ export interface SsrMiddleware extends RequestHandler {
19
+ }
20
+ /**
21
+ * Returns a middleware for setting up SSR using Vite middleware mode.
22
+ *
23
+ * See more: https://vite.dev/guide/ssr
24
+ */
25
+ export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<SsrMiddleware>;
7
26
  export declare function replaceSsrPlaceholders(template: string, values: Record<string, unknown>): string;