@devmoods/express-extras 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,12 +44,7 @@ FORCE_EXIT_TIMEOUT=
44
44
 
45
45
  APP_JWT_SECRET=123456
46
46
 
47
- PGHOST=localhost
48
- PGUSER=my_database
49
- PGPASSWORD=
50
- PGDATABASE=my_database_dev
51
- PGPORT=5432
52
-
47
+ POSTGRES_URL=postgresql://user:pass@localhost:5432/my_database
53
48
  REDIS_URL=
54
49
 
55
50
  SLACK_WEBHOOK_URL=
package/dist/auth.d.ts CHANGED
@@ -12,14 +12,14 @@ export interface AuthOptions<UserModel extends Record<string, any>> {
12
12
  tokenExpiry?: number;
13
13
  onPasswordResetRequest?(user: UserModel, temporaryToken: string): void | Promise<void>;
14
14
  onPasswordChanged?(user: UserModel, newPassword: string): Promise<void>;
15
- onEmailVerificationRequest?(user: UserModel, verificationToken: string): Promise<void>;
15
+ onEmailVerificationRequest?(user: UserModel, temporaryToken: string): Promise<void>;
16
16
  onEmailVerified?(user: UserModel): Promise<void>;
17
17
  }
18
18
  export interface CreateAuth<UserModel extends Record<string, any>> {
19
19
  createRouter: () => Router;
20
20
  useCurrentUser: () => UserModel | undefined;
21
21
  middleware: (authorize?: boolean) => RequestHandler;
22
- createVerificationToken(user: UserModel): Promise<string>;
22
+ createTemporaryToken(user: UserModel): Promise<string>;
23
23
  }
24
24
  export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, onPasswordResetRequest, onPasswordChanged, onEmailVerified, onEmailVerificationRequest, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
25
25
  /**
package/dist/auth.js CHANGED
@@ -8,10 +8,12 @@ import { BadRequest, NotFound, Unauthorized } from './errors.js';
8
8
  import { getLogger } from './logger.js';
9
9
  import { route } from './middleware.js';
10
10
  import { validateBody, validateParams, } from './validate.js';
11
+ import { readEnv } from './utils.js';
11
12
  const logger = getLogger('auth');
12
- const TOKEN_EXPIRES_IN = 3600 * 48;
13
- const TEMP_TOKEN_EXIRES_IN = 3600;
14
- export function createAuthToken(payload, secret = process.env.APP_JWT_SECRET) {
13
+ const TOKEN_EXPIRES_IN = readEnv.number('AUTH_TOKEN_EXPIRES', 3600 * 48);
14
+ const TEMP_TOKEN_EXIRES_IN = readEnv.number('AUTH_TEMP_TOKEN_EXPIRES', 3600);
15
+ const AUTH_SECRET = readEnv('AUTH_JWT_SECRET', '');
16
+ export function createAuthToken(payload, secret = AUTH_SECRET) {
15
17
  return jwt.sign(payload, secret, { expiresIn: TOKEN_EXPIRES_IN });
16
18
  }
17
19
  export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onPasswordResetRequest, onPasswordChanged, onEmailVerified, onEmailVerificationRequest, }) {
@@ -92,11 +94,8 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
92
94
  throw new Error('Not applicable for login method');
93
95
  }
94
96
  const user = await emailAndPassword(email, skipPasswordCheck);
95
- const verificationToken = crypto.randomBytes(32).toString('hex');
96
- await redis.set(verificationToken, user.email, {
97
- EX: TEMP_TOKEN_EXIRES_IN,
98
- });
99
- await onEmailVerificationRequest?.(user, verificationToken);
97
+ const temporaryToken = await createTemporaryToken(user);
98
+ await onEmailVerificationRequest?.(user, temporaryToken);
100
99
  return 202;
101
100
  }));
102
101
  router.post('/verify-email', validateBody({
@@ -155,10 +154,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
155
154
  }
156
155
  try {
157
156
  const user = await emailAndPassword(email, skipPasswordCheck);
158
- const temporaryToken = crypto.randomBytes(32).toString('hex');
159
- await redis.set(temporaryToken, user.email, {
160
- EX: TEMP_TOKEN_EXIRES_IN,
161
- });
157
+ const temporaryToken = await createTemporaryToken(user);
162
158
  await onPasswordResetRequest?.(user, temporaryToken);
163
159
  logger.info('password reset request', { email });
164
160
  }
@@ -199,7 +195,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
199
195
  }
200
196
  throw new InvalidToken('token expired');
201
197
  }
202
- const decoded = jwt.verify(token.trim(), process.env.APP_JWT_SECRET);
198
+ const decoded = jwt.verify(token.trim(), AUTH_SECRET);
203
199
  if (typeof decoded === 'string') {
204
200
  throw new Error('Failed to decode token');
205
201
  }
@@ -214,7 +210,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
214
210
  }
215
211
  };
216
212
  }
217
- async function createVerificationToken(user) {
213
+ async function createTemporaryToken(user) {
218
214
  try {
219
215
  const temporaryToken = crypto.randomBytes(32).toString('hex');
220
216
  await redis.set(temporaryToken, user.email, {
@@ -231,7 +227,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
231
227
  createRouter,
232
228
  useCurrentUser,
233
229
  middleware,
234
- createVerificationToken,
230
+ createTemporaryToken,
235
231
  };
236
232
  }
237
233
  /**
package/dist/faktory.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare function createJobs({ queueName }: JobsOptions): {
11
11
  };
12
12
  queueName: string;
13
13
  };
14
- export type Task = ReturnType<ReturnType<typeof createJobs>['job']>;
14
+ export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
15
15
  export interface FaktoryWorkerOptions {
16
16
  jobs: unknown[];
17
17
  queueName: string;
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export * from './redis.js';
11
11
  export * from './routes.js';
12
12
  export * from './sentry.js';
13
13
  export * from './slack.js';
14
+ export * from './utils.js';
14
15
  export * from './validate.js';
15
- export declare const envPort: (defaultPort?: number) => number;
package/dist/index.js CHANGED
@@ -11,5 +11,5 @@ export * from './redis.js';
11
11
  export * from './routes.js';
12
12
  export * from './sentry.js';
13
13
  export * from './slack.js';
14
+ export * from './utils.js';
14
15
  export * from './validate.js';
15
- export const envPort = (defaultPort = 3000) => parseInt(process.env.PORT, 10) || defaultPort;
package/dist/listen.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createTerminus } from '@godaddy/terminus';
2
2
  import { getLogger } from './logger.js';
3
+ import { readEnv } from './utils.js';
3
4
  const logger = getLogger('http');
4
- const READINESS_PERIOD_MS = parseInt(process.env.READINESS_PERIOD_MS || '', 10) || 0;
5
- const FORCE_EXIT_TIMEOUT = parseInt(process.env.FORCE_EXIT_TIMEOUT || '', 10) || 1000;
5
+ const READINESS_PERIOD_MS = readEnv.number('READINESS_PERIOD_MS', 0);
6
+ const FORCE_EXIT_TIMEOUT = readEnv.number('FORCE_EXIT_TIMEOUT', 5000);
6
7
  /**
7
8
  * Listen on the given port and setup proper signal
8
9
  * handlers for terminating the process properly.
@@ -2,6 +2,7 @@ import pg, { type PoolClient, type QueryResult, type QueryResultRow } from 'pg';
2
2
  import { SQL, type SQLStatement } from 'sql-template-strings';
3
3
  export interface PostgresOptions extends pg.PoolConfig {
4
4
  onError?: (error: Error, client: any) => void;
5
+ url?: pg.PoolConfig['connectionString'];
5
6
  }
6
7
  export declare class DoesNotExistError extends Error {
7
8
  constructor(message: string);
@@ -31,7 +32,7 @@ export declare class Transaction extends Connection {
31
32
  *
32
33
  * @param config
33
34
  */
34
- export declare function createPostgres({ onError, ...config }?: PostgresOptions): {
35
+ export declare function createPostgres({ onError, url, ...config }?: PostgresOptions): {
35
36
  insert: <T extends pg.QueryResultRow>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<pg.QueryResult<T>>;
36
37
  query: <T_1 extends pg.QueryResultRow>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<pg.QueryResult<T_1>>;
37
38
  first: <T_2>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<T_2 | null>;
package/dist/postgres.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import pg from 'pg';
2
2
  import { SQL } from 'sql-template-strings';
3
+ import { readEnv } from './utils.js';
3
4
  export class DoesNotExistError extends Error {
4
5
  constructor(message) {
5
6
  super(message);
@@ -67,8 +68,8 @@ export class Transaction extends Connection {
67
68
  *
68
69
  * @param config
69
70
  */
70
- export function createPostgres({ onError = () => { }, ...config } = {}) {
71
- const pool = new pg.Pool(config);
71
+ export function createPostgres({ onError = () => { }, url = readEnv('POSTGRES_URL'), ...config } = {}) {
72
+ const pool = new pg.Pool({ connectionString: url, ...config });
72
73
  pool.on('error', onError);
73
74
  async function withConnection(callback) {
74
75
  const client = await pool.connect();
package/dist/redis.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createClient } from 'redis';
2
- export function createRedis({ url = process.env.REDIS_URL, onError, }) {
2
+ import { readEnv } from './utils.js';
3
+ export function createRedis({ url = readEnv('REDIS_URL', ''), onError, }) {
3
4
  const client = createClient({ url });
4
5
  client.on('error', onError);
5
6
  return client;
package/dist/sentry.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import * as Sentry from '@sentry/node';
2
+ import { attachSentryClientToLoggers } from './logger.js';
3
+ import { readEnv } from './utils.js';
2
4
  /**
3
5
  * Create a pre-configured Sentry client
4
6
  */
5
7
  export function createSentry(options = {}) {
8
+ const client = Sentry;
6
9
  return {
7
- client: Sentry,
10
+ client,
8
11
  init(app, getOptions = (defaultOptions) => defaultOptions) {
12
+ attachSentryClientToLoggers(client);
9
13
  Sentry.init(getOptions(createSentryOptions(app, options)));
10
14
  },
11
15
  };
@@ -15,7 +19,7 @@ export function createSentry(options = {}) {
15
19
  */
16
20
  export function createSentryOptions(app, options = {}) {
17
21
  return {
18
- dsn: process.env.SENTRY_DSN,
22
+ dsn: readEnv('SENTRY_DSN'),
19
23
  integrations: [
20
24
  new Sentry.Integrations.Http({ tracing: true }),
21
25
  app && new Sentry.Integrations.Express({ app }),
@@ -0,0 +1 @@
1
+ import 'dotenv/config';
@@ -0,0 +1,9 @@
1
+ import 'dotenv/config';
2
+ import * as crypto from 'node:crypto';
3
+ // @ts-ignore
4
+ import createFetchMock from 'vitest-fetch-mock';
5
+ import { vi } from 'vitest';
6
+ const fetchMocker = createFetchMock(vi);
7
+ fetchMocker.enableMocks();
8
+ // @ts-ignore
9
+ globalThis.crypto = crypto;
package/dist/slack.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createFetch } from '@devmoods/fetch';
2
2
  import { getLogger } from './logger.js';
3
+ import { readEnv } from './utils.js';
3
4
  const logger = getLogger('slack');
4
5
  /**
5
6
  * Send messages to Slack using Incoming Webhooks
@@ -14,7 +15,7 @@ const logger = getLogger('slack');
14
15
  *
15
16
  * See https://api.slack.com/messaging/webhooks
16
17
  */
17
- export function createSlack({ webhookUrl = process.env.SLACK_WEBHOOK_URL, dryRun = process.env.NODE_ENV !== 'production', } = {}) {
18
+ export function createSlack({ webhookUrl = readEnv('SLACK_WEBHOOK_URL', ''), dryRun = process.env.NODE_ENV !== 'production', } = {}) {
18
19
  const fetch = createFetch();
19
20
  /**
20
21
  * Post a message to Slack
package/dist/utils.d.ts CHANGED
@@ -5,3 +5,13 @@
5
5
  export declare function startTimer(): {
6
6
  getDuration(): number;
7
7
  };
8
+ export declare function readEnv(key: string): string | undefined;
9
+ export declare function readEnv(key: string, defaultValue: string): string;
10
+ export declare namespace readEnv {
11
+ var number: typeof readEnvNumber;
12
+ var boolean: typeof readEnvBoolean;
13
+ }
14
+ declare function readEnvNumber(key: string, defaultValue: number): number;
15
+ declare function readEnvBoolean(key: string, defaultValue: boolean): boolean;
16
+ export declare const envPort: (defaultPort?: number) => number;
17
+ export {};
package/dist/utils.js CHANGED
@@ -11,3 +11,17 @@ export function startTimer() {
11
11
  },
12
12
  };
13
13
  }
14
+ export function readEnv(key, defaultValue) {
15
+ const value = process.env[key];
16
+ return value ?? defaultValue;
17
+ }
18
+ readEnv.number = readEnvNumber;
19
+ readEnv.boolean = readEnvBoolean;
20
+ function readEnvNumber(key, defaultValue) {
21
+ const str = readEnv(key, `${defaultValue}`);
22
+ return parseFloat(str);
23
+ }
24
+ function readEnvBoolean(key, defaultValue) {
25
+ return ['true', '1', 't'].includes(readEnv(key, `${defaultValue}`).toLowerCase());
26
+ }
27
+ export const envPort = (defaultPort = 3000) => readEnv.number('PORT', defaultPort);
@@ -44,7 +44,7 @@ export declare function compileValidateSchema(schema: ValidationSchema, options?
44
44
  errors: null;
45
45
  } | {
46
46
  isValid: boolean;
47
- errors: never[] | {
47
+ errors: {
48
48
  [key: string]: any[];
49
49
  };
50
50
  };
package/dist/validate.js CHANGED
@@ -45,7 +45,7 @@ export function compileValidateSchema(schema, options = {}) {
45
45
  errors: null,
46
46
  };
47
47
  }
48
- const errors = transformAjvErrors(validate.errors) || {};
48
+ const errors = transformAjvErrors(validate.errors);
49
49
  validate.errors = null;
50
50
  return {
51
51
  isValid: false,
@@ -65,7 +65,7 @@ export function validateSchema(data, schema, options = {}) {
65
65
  errors: null,
66
66
  };
67
67
  }
68
- const errors = transformAjvErrors(ajv.errors) || {};
68
+ const errors = transformAjvErrors(ajv.errors);
69
69
  ajv.errors = null;
70
70
  return {
71
71
  isValid: false,
@@ -73,8 +73,9 @@ export function validateSchema(data, schema, options = {}) {
73
73
  };
74
74
  }
75
75
  function transformAjvErrors(errors) {
76
+ /* c8 ignore next 3 */
76
77
  if (errors == null) {
77
- return [];
78
+ return {};
78
79
  }
79
80
  return errors
80
81
  .map((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,10 +26,11 @@
26
26
  "@devmoods/eslint-config": "^1.2.0",
27
27
  "@sentry/types": "^7.61.1",
28
28
  "@types/express-serve-static-core": "^4.17.35",
29
- "@types/node": "20.4.7",
29
+ "@types/node": "20.4.8",
30
30
  "@types/supertest": "^2.0.12",
31
- "@typescript-eslint/eslint-plugin": "^6.2.1",
32
- "@typescript-eslint/parser": "^6.2.1",
31
+ "@typescript-eslint/eslint-plugin": "^6.3.0",
32
+ "@typescript-eslint/parser": "^6.3.0",
33
+ "@vitest/coverage-v8": "^0.34.1",
33
34
  "@vitest/expect": "^0.34.1",
34
35
  "chalk": "^5.3.0",
35
36
  "eslint": "^8.46.0",
@@ -44,7 +45,8 @@
44
45
  "supertest": "6.3.3",
45
46
  "ts-node": "10.9.1",
46
47
  "typescript": "5.1.6",
47
- "vitest": "^0.34.1"
48
+ "vitest": "^0.34.1",
49
+ "vitest-fetch-mock": "^0.2.2"
48
50
  },
49
51
  "dependencies": {
50
52
  "@devmoods/fetch": "^3.3.0",
@@ -101,7 +103,7 @@
101
103
  "start:worker": "ts-node --esm example/worker.ts",
102
104
  "clean": "rm -rf dist",
103
105
  "lint": "eslint src",
104
- "test": "NODE_ENV=test vitest",
106
+ "test": "NODE_ENV=test DOTENV_CONFIG_PATH=.env.test vitest",
105
107
  "build": "pnpm run clean && tsc --project tsconfig.build.json",
106
108
  "prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
107
109
  "import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",