@devmoods/express-extras 0.26.2 → 0.27.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.
@@ -1,3 +1,4 @@
1
+ import {} from '../../postgres.js';
1
2
  export async function up(query) {
2
3
  await query('CREATE TABLE baz (id serial primary key)');
3
4
  await query('CREATE TABLE foo (id serial primary key)');
package/dist/auth.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare function createAuthToken(payload: any, secret?: string): string;
7
7
  /**
8
8
  * Decode the given JWT token.
9
9
  */
10
- export declare function decodeAuthToken(token: string): {
10
+ export declare function decodeAuthToken(token: string, secret?: string): {
11
11
  [x: string]: any;
12
12
  iss?: string | undefined;
13
13
  sub?: string | undefined;
package/dist/auth.js CHANGED
@@ -29,9 +29,10 @@ export function createAuthToken(payload, secret = AUTH_SECRET) {
29
29
  /**
30
30
  * Decode the given JWT token.
31
31
  */
32
- export function decodeAuthToken(token) {
32
+ export function decodeAuthToken(token, secret = AUTH_SECRET) {
33
33
  try {
34
- const decoded = jwt.verify(token.trim(), AUTH_SECRET);
34
+ const decoded = jwt.verify(token.trim(), secret);
35
+ /* c8 ignore next 3 */
35
36
  if (typeof decoded === 'string') {
36
37
  throw new Error('decoded value not an object');
37
38
  }
package/dist/cache.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {} from './redis.js';
1
2
  export function createRedisCacheAdapter(redis) {
2
3
  return {
3
4
  async get(key) {
package/dist/context.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import {} from 'express';
2
3
  import { v4 as uuid } from 'uuid';
3
4
  const requestContext = new AsyncLocalStorage();
4
5
  const REQUEST_ID = 'requestId';
package/dist/email.d.ts CHANGED
@@ -13,4 +13,5 @@ export interface SendEmailOptions extends nodemailer.SendMailOptions {
13
13
  export declare function createEmail({ host, port, user, pass, senderName, senderAddress, }: EmailOptions): {
14
14
  send: ({ to, subject, message, ...rest }: SendEmailOptions) => Promise<void>;
15
15
  transporter: nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo>;
16
+ readonly outbox: Record<string, any>[];
16
17
  };
package/dist/email.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as nodemailer from 'nodemailer';
2
2
  import { getLogger } from './logger.js';
3
+ const logger = getLogger('email');
3
4
  export function createEmail({ host, port, user, pass, senderName, senderAddress, }) {
4
5
  const transporter = nodemailer.createTransport({
5
6
  host,
@@ -7,15 +8,27 @@ export function createEmail({ host, port, user, pass, senderName, senderAddress,
7
8
  secure: false,
8
9
  auth: { user, pass },
9
10
  });
11
+ const outbox = [];
10
12
  const send = async ({ to, subject, message, ...rest }) => {
11
- const result = await transporter.sendMail({
13
+ const payload = {
12
14
  from: `${senderName} <${senderAddress}>`,
13
15
  to,
14
16
  subject,
15
17
  text: message,
16
18
  ...rest,
17
- });
18
- getLogger('email').info('email-sent', { email: result });
19
+ };
20
+ const result = await transporter.sendMail(payload);
21
+ outbox.push(payload);
22
+ if (outbox.length > 5) {
23
+ outbox.shift();
24
+ }
25
+ logger.info('email-sent', { email: result });
26
+ };
27
+ return {
28
+ send,
29
+ transporter,
30
+ get outbox() {
31
+ return outbox;
32
+ },
19
33
  };
20
- return { send, transporter };
21
34
  }
package/dist/errors.js CHANGED
@@ -1,6 +1,6 @@
1
- import { STATUS_CODES } from 'node:http';
2
1
  import * as Sentry from '@sentry/node';
3
2
  import { errors } from 'compose-middleware';
3
+ import { STATUS_CODES } from 'node:http';
4
4
  import { DoesNotExistError, IntegrityError } from './postgres.js';
5
5
  export class ApiError extends Error {
6
6
  }
@@ -20,14 +20,15 @@ export function makeError(status, detail, payload) {
20
20
  }
21
21
  export function mapToProblem(error) {
22
22
  if (error instanceof ApiError) {
23
- const statusMessage = STATUS_CODES[error.status || '500'];
23
+ const status = error.status || 500;
24
+ const statusMessage = STATUS_CODES[status];
24
25
  return {
25
26
  type: `/api/problems/${statusMessage
26
27
  .toLocaleLowerCase()
27
28
  .replace(/ /g, '-')}`,
28
29
  title: statusMessage,
29
- detail: error.message,
30
- status: error.status,
30
+ detail: error.message || '',
31
+ status,
31
32
  ...error.payload,
32
33
  };
33
34
  }
@@ -35,6 +36,7 @@ export function mapToProblem(error) {
35
36
  type: 'internal-error',
36
37
  title: 'Internal Server Error',
37
38
  status: 500,
39
+ detail: '',
38
40
  };
39
41
  }
40
42
  function convertToAPIError(error) {
@@ -48,9 +50,6 @@ function convertToAPIError(error) {
48
50
  }
49
51
  function _errorHandler({ onError }) {
50
52
  return (err, req, res, next) => {
51
- if (!err) {
52
- err = makeError(500);
53
- }
54
53
  onError?.(err);
55
54
  const apiError = convertToAPIError(err);
56
55
  const problem = mapToProblem(apiError);
package/dist/faktory.d.ts CHANGED
@@ -1,12 +1,14 @@
1
+ import { create } from 'faktory-worker';
1
2
  export interface JobsOptions {
2
3
  queueName: string;
4
+ faktory?: typeof create;
3
5
  }
4
- export declare function createJobs({ queueName }: JobsOptions): {
6
+ export declare function createJobs({ queueName, faktory: createFactory, }: JobsOptions): {
5
7
  job: <K extends Record<string, any>, T extends (input: K) => void | Promise<any>>(fn: T) => {
6
8
  $job: boolean;
7
9
  jobName: string;
8
10
  readonly executionCount: number;
9
- delay(args?: Parameters<T>[0] | undefined): Promise<void>;
11
+ delay(args?: Parameters<T>[0] | undefined): Promise<`${string}-${string}-${string}-${string}-${string}`>;
10
12
  execute(args: Parameters<T>[0]): Promise<any>;
11
13
  };
12
14
  queueName: string;
@@ -15,8 +17,10 @@ export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
15
17
  export interface FaktoryWorkerOptions {
16
18
  jobs: unknown[];
17
19
  queueName: string;
20
+ faktory?: typeof create;
18
21
  }
19
22
  export declare function useJobId(): string | undefined;
20
- export declare function createFaktoryWorker({ queueName, jobs, }: FaktoryWorkerOptions): Promise<{
23
+ export declare function createFaktoryWorker({ queueName, jobs, faktory: createFactory, }: FaktoryWorkerOptions): Promise<{
21
24
  start(): Promise<void>;
25
+ stop(): Promise<void>;
22
26
  }>;
package/dist/faktory.js CHANGED
@@ -5,12 +5,12 @@ import { getLogger } from './logger.js';
5
5
  import { startTimer } from './utils.js';
6
6
  const logger = getLogger('faktory');
7
7
  const JOB_ID = 'jobId';
8
- export function createJobs({ queueName }) {
9
- const faktory = create();
8
+ export function createJobs({ queueName, faktory: createFactory = create, }) {
9
+ const faktory = createFactory();
10
10
  function job(fn) {
11
11
  const jobName = fn.name;
12
12
  let executionCount = 0;
13
- if (jobName == null) {
13
+ if (!jobName) {
14
14
  throw new Error('Can not create job without a name');
15
15
  }
16
16
  return {
@@ -35,7 +35,9 @@ export function createJobs({ queueName }) {
35
35
  jobName,
36
36
  args: JSON.stringify(args),
37
37
  });
38
+ throw error;
38
39
  }
40
+ return jid;
39
41
  },
40
42
  async execute(args) {
41
43
  try {
@@ -59,8 +61,8 @@ export function useJobId() {
59
61
  return store.get(JOB_ID);
60
62
  }
61
63
  }
62
- export async function createFaktoryWorker({ queueName, jobs, }) {
63
- const faktory = create();
64
+ export async function createFaktoryWorker({ queueName, jobs, faktory: createFactory = create, }) {
65
+ const faktory = createFactory();
64
66
  const filter = (j) => !!Object.prototype.hasOwnProperty.call(j, '$job');
65
67
  const availableJobs = jobs.filter(filter);
66
68
  for (const job of availableJobs) {
@@ -99,5 +101,8 @@ export async function createFaktoryWorker({ queueName, jobs, }) {
99
101
  process.exit(1);
100
102
  }
101
103
  },
104
+ async stop() {
105
+ return faktory.stop();
106
+ },
102
107
  };
103
108
  }
package/dist/listen.d.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  import type * as http from 'node:http';
3
3
  /**
4
4
  * Listen on the given port and setup proper signal
5
- * handlers for terminating the process properly.
6
- *
7
- * Kubernetes friendly.
5
+ * handlers for terminating the process gracefully.
8
6
  */
9
7
  export declare function listen(server: http.Server, options: {
10
8
  port: number;
package/dist/listen.js CHANGED
@@ -6,9 +6,7 @@ const READINESS_PERIOD_MS = readEnv.number('READINESS_PERIOD_MS', 0);
6
6
  const FORCE_EXIT_TIMEOUT = readEnv.number('FORCE_EXIT_TIMEOUT', 5000);
7
7
  /**
8
8
  * Listen on the given port and setup proper signal
9
- * handlers for terminating the process properly.
10
- *
11
- * Kubernetes friendly.
9
+ * handlers for terminating the process gracefully.
12
10
  */
13
11
  export function listen(server, options) {
14
12
  createTerminus(server, {
package/dist/logger.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ import type { Duplex } from 'node:stream';
2
3
  import type { CreateSentry } from './sentry.js';
3
4
  export type LoggerContext = Record<string, any>;
4
5
  export interface LoggerOptions {
@@ -14,7 +15,7 @@ export interface LoggerOptions {
14
15
  * Defaults to `process.env.NODE_ENV === 'test'`
15
16
  */
16
17
  silent?: () => boolean;
17
- output?: NodeJS.WriteStream;
18
+ output?: Duplex;
18
19
  }
19
20
  export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['client']): void;
20
21
  export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
package/dist/logger.js CHANGED
@@ -24,7 +24,7 @@ function defaultGetContext() {
24
24
  ..._globalGetContext?.(),
25
25
  };
26
26
  }
27
- export function createLogger({ name, getContext, silent = () => process.env.NODE_ENV === 'test', output = process.stdout, }) {
27
+ export function createLogger({ name, getContext, silent = () => false, output = process.stdout, }) {
28
28
  if (_loggers.has(name)) {
29
29
  throw new Error(`A logger named ${name} already exists`);
30
30
  }
package/dist/migrate.d.ts CHANGED
@@ -10,6 +10,7 @@ interface MigrationFileAPI {
10
10
  down(fn: any): Promise<string> | Promise<void> | string | void;
11
11
  }
12
12
  export declare function cli(): Promise<void>;
13
+ export declare function runCli(input: string[], flags: Record<string, any>, showHelp?: () => void): Promise<number>;
13
14
  export interface MigratorOptions {
14
15
  postgres?: ReturnType<typeof createPostgres>;
15
16
  migrationTableName?: string;
package/dist/migrate.js CHANGED
@@ -14,8 +14,29 @@ Usage
14
14
  description: 'PostgreSQL migration tool',
15
15
  importMeta: import.meta,
16
16
  });
17
- const verbose = !cli.flags.silent;
18
- const args = cli.input.slice();
17
+ let exitCode = 0;
18
+ try {
19
+ exitCode = await runCli(cli.input, cli.flags, () => cli.showHelp());
20
+ }
21
+ catch (error) {
22
+ if (error instanceof CliError) {
23
+ exitCode = error.code;
24
+ }
25
+ }
26
+ finally {
27
+ process.exit(exitCode);
28
+ }
29
+ }
30
+ class CliError extends Error {
31
+ constructor(message, code) {
32
+ super(message);
33
+ this.code = code;
34
+ this.code = code;
35
+ }
36
+ }
37
+ export async function runCli(input, flags, showHelp) {
38
+ const verbose = !flags.silent;
39
+ const args = input.slice();
19
40
  const action = args.shift();
20
41
  function isValidAction(action) {
21
42
  return action != null && ['init', 'up', 'down', 'create'].includes(action);
@@ -24,8 +45,8 @@ Usage
24
45
  async function run(action, ...args) {
25
46
  try {
26
47
  if (!isValidAction(action)) {
27
- cli.showHelp();
28
- return;
48
+ showHelp?.();
49
+ throw new CliError(`Invalid action ${action}`, 2);
29
50
  }
30
51
  switch (action) {
31
52
  case 'up':
@@ -43,20 +64,19 @@ Usage
43
64
  break;
44
65
  }
45
66
  case 'create': {
46
- const createdMigration = await migrator.create(args[0], cli.flags.typescript ? 'ts' : 'sql');
67
+ const createdMigration = await migrator.create(args[0], flags.typescript ? 'ts' : 'sql');
47
68
  migrator.log(chalk.green('Created migration', createdMigration));
48
69
  break;
49
70
  }
50
71
  }
72
+ return 0;
51
73
  }
52
74
  catch (error) {
53
75
  console.error(chalk.red(error.message));
54
- }
55
- finally {
56
- process.exit(0);
76
+ throw new CliError(error.message, 1);
57
77
  }
58
78
  }
59
- run(action, ...args);
79
+ return run(action, ...args);
60
80
  }
61
81
  export class Migrator {
62
82
  constructor({ postgres = createPostgres(), pathToMigrations = path.resolve(process.cwd(), 'db', 'migrations'), migrationTableName = 'migrations', verbose = true, } = {}) {
@@ -136,9 +156,6 @@ export class Migrator {
136
156
  for (const migration of pendingMigrations) {
137
157
  this.log('Running migration', migration.filename);
138
158
  const migrator = await this.readMigrationFile(migration.filename);
139
- if (!migrator.up || !migrator.down) {
140
- throw new Error('Migrations must export both an up() and a down() function.');
141
- }
142
159
  if (migrator.useTransaction !== false) {
143
160
  await this.postgres.transaction(async (tx) => {
144
161
  await this.migrate(tx, migrator, direction);
@@ -166,9 +183,6 @@ export class Migrator {
166
183
  }
167
184
  this.log('Rolling back migration', migrationToRollback.filename);
168
185
  const migrator = await this.readMigrationFile(migrationToRollback.filename);
169
- if (!migrator.up || !migrator.down) {
170
- throw new Error('Migrations must export both an up() and a down() function.');
171
- }
172
186
  if (migrator.useTransaction !== false) {
173
187
  await this.postgres.transaction(async (tx) => {
174
188
  await this.migrate(tx, migrator, 'down');
@@ -182,6 +196,7 @@ export class Migrator {
182
196
  });
183
197
  }
184
198
  return await this.getCurrentVersion();
199
+ /* c8 ignore next 2 */
185
200
  }
186
201
  throw new Error(`Invalid direction ${direction}`);
187
202
  }
@@ -216,6 +231,7 @@ export class Migrator {
216
231
  else {
217
232
  await returnValue;
218
233
  }
234
+ /* c8 ignore next 3 */
219
235
  }
220
236
  catch (error) {
221
237
  throw new Error(`Migration failed: ${error.message}`);
@@ -230,7 +246,7 @@ export class Migrator {
230
246
  }
231
247
  async getMigrations() {
232
248
  const files = await fs.readdir(this.pathToMigrations);
233
- return (files || [])
249
+ return files
234
250
  .map((filename) => filename.match(this.filenameRegExp))
235
251
  .filter(notNullOrUndefined)
236
252
  .map(([filename, version, name]) => ({
package/dist/postgres.js CHANGED
@@ -1,4 +1,4 @@
1
- import pg from 'pg';
1
+ import pg, {} from 'pg';
2
2
  import { SQL } from 'sql-template-strings';
3
3
  import { readEnv } from './utils.js';
4
4
  export class DoesNotExistError extends Error {
@@ -38,6 +38,7 @@ export class Connection {
38
38
  const result = await this.client.query(text, values);
39
39
  if (result.rows) {
40
40
  return result.rows;
41
+ /* c8 ignore next 2*/
41
42
  }
42
43
  return [];
43
44
  }
@@ -68,9 +69,11 @@ export class Transaction extends Connection {
68
69
  *
69
70
  * @param config
70
71
  */
71
- export function createPostgres({ onError = () => { }, url = readEnv('POSTGRES_URL'), ...config } = {}) {
72
+ export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...config } = {}) {
72
73
  const pool = new pg.Pool({ connectionString: url, ...config });
73
- pool.on('error', onError);
74
+ if (onError) {
75
+ pool.on('error', onError);
76
+ }
74
77
  async function withConnection(callback) {
75
78
  const client = await pool.connect();
76
79
  try {
@@ -106,6 +109,7 @@ export function createPostgres({ onError = () => { }, url = readEnv('POSTGRES_UR
106
109
  catch (error) {
107
110
  await client.query('ROLLBACK');
108
111
  throw error;
112
+ /* c8 ignore next 3 */
109
113
  }
110
114
  finally {
111
115
  client.release();
package/dist/redis.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createClient } from 'redis';
2
2
  export interface RedisOptions {
3
3
  url?: string;
4
- onError: (error: Error) => void;
4
+ onError?: (error: Error) => void;
5
5
  }
6
6
  export type RedisClient = ReturnType<typeof createClient>;
7
- export declare function createRedis({ url, onError, }: RedisOptions): RedisClient;
7
+ export declare function createRedis({ url, onError, }?: RedisOptions): RedisClient;
package/dist/redis.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { createClient } from 'redis';
2
2
  import { readEnv } from './utils.js';
3
- export function createRedis({ url = readEnv('REDIS_URL', ''), onError, }) {
3
+ export function createRedis({ url = readEnv('REDIS_URL', ''), onError, } = {}) {
4
4
  const client = createClient({ url });
5
- client.on('error', onError);
5
+ if (onError) {
6
+ client.on('error', onError);
7
+ }
6
8
  return client;
7
9
  }
package/dist/sentry.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as Sentry from '@sentry/node';
2
+ import {} from 'express';
2
3
  import { attachSentryClientToLoggers } from './logger.js';
3
4
  import { readEnv } from './utils.js';
4
5
  /**
package/dist/validate.js CHANGED
@@ -1,4 +1,5 @@
1
- import _Ajv from 'ajv';
1
+ import _Ajv, {} from 'ajv';
2
+ import {} from 'express';
2
3
  import { BadRequest } from './errors.js';
3
4
  const Ajv = _Ajv;
4
5
  export function validateBody(schema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,6 +32,7 @@
32
32
  "@typescript-eslint/parser": "^6.4.1",
33
33
  "@vitest/coverage-v8": "^0.34.2",
34
34
  "@vitest/expect": "^0.34.2",
35
+ "@vitest/runner": "^0.34.3",
35
36
  "chalk": "^5.3.0",
36
37
  "eslint": "^8.47.0",
37
38
  "eslint-plugin-jsx-a11y": "^6.7.1",