@devmoods/express-extras 0.40.1 → 0.42.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/dist/auth.d.ts CHANGED
@@ -30,7 +30,10 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
30
30
  redis: AuthOptions<UserModel>['redis'];
31
31
  setCurrentUser: (user: UserModel) => void;
32
32
  createTemporaryToken(user: UserModel): Promise<string>;
33
- middleware: (authorize?: boolean) => RequestHandler;
33
+ middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
34
+ login: (res: Response, user: UserModel) => Promise<{
35
+ token: string;
36
+ }>;
34
37
  }
35
38
  export type AnyUser = Record<string, any>;
36
39
  export interface AuthProvider<UserModel extends AnyUser> {
@@ -45,9 +48,12 @@ export interface AuthOptions<UserModel extends AnyUser> {
45
48
  export interface CreateAuth<UserModel extends AnyUser> {
46
49
  createRouter: () => Router;
47
50
  useCurrentUser: () => UserModel | undefined;
48
- middleware: (authorize?: boolean) => RequestHandler;
51
+ middleware: (options: AuthMiddlewareOptions<UserModel>) => RequestHandler;
49
52
  createTemporaryToken(user: UserModel): Promise<string>;
50
53
  }
54
+ export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {
55
+ isAuthorized?: (user: UserModel) => Promise<boolean> | boolean;
56
+ };
51
57
  export declare function createAuth<UserModel extends AnyUser>({ getUserByToken, filterUser, redis, providers, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
52
58
  /**
53
59
  * Extract a Bearer token from the HTTP Authorization header
package/dist/auth.js CHANGED
@@ -4,7 +4,7 @@ import { Router, } from 'express';
4
4
  import jwt from 'jsonwebtoken';
5
5
  import { v4 } from 'uuid';
6
6
  import { setContext, useContext } from './context.js';
7
- import { BadRequest, NotFound, Unauthorized } from './errors.js';
7
+ import { ApiError, BadRequest, Forbidden, NotFound, Unauthorized, } from './errors.js';
8
8
  import { getLogger } from './logger.js';
9
9
  import { route } from './middleware.js';
10
10
  import {} from './redis.js';
@@ -57,10 +57,7 @@ export function EmailAndPasswordAuthProvider(options) {
57
57
  }), route(async ({ body: { email, password } }, res) => {
58
58
  try {
59
59
  const user = await options.login(email, (realPassword) => checkIfPasswordsMatch(realPassword, password));
60
- const token = createAuthToken(context.filterUser(user));
61
- await context.redis.set(token, '1', { EX: TOKEN_EXPIRES_IN });
62
- setAccessTokenCookie(res, token);
63
- context.setCurrentUser(user);
60
+ const { token } = await context.login(res, user);
64
61
  logger.info('access token created');
65
62
  return [201, { token }];
66
63
  }
@@ -146,12 +143,20 @@ export function EmailAndPasswordAuthProvider(options) {
146
143
  export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
147
144
  const useCurrentUser = () => useContext('currentUser');
148
145
  const setCurrentUser = (user) => setContext('currentUser', user);
146
+ const login = async (res, user) => {
147
+ const token = createAuthToken(filterUser(user));
148
+ await redis.set(token, '1', { EX: TOKEN_EXPIRES_IN });
149
+ setAccessTokenCookie(res, token);
150
+ setCurrentUser(user);
151
+ return { token };
152
+ };
149
153
  const providerContext = {
150
154
  filterUser,
151
155
  redis,
152
156
  setCurrentUser,
153
157
  createTemporaryToken,
154
158
  middleware,
159
+ login,
155
160
  };
156
161
  const createRouter = () => {
157
162
  const router = Router();
@@ -201,19 +206,24 @@ export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
201
206
  }
202
207
  return router;
203
208
  };
204
- function middleware(authorize = true) {
209
+ function middleware(options = {}) {
210
+ let authenticate = true;
211
+ if (typeof options === 'boolean') {
212
+ authenticate = options;
213
+ options = {};
214
+ }
205
215
  return async (req, res, next) => {
206
216
  try {
207
217
  const token = extractAccessTokenFromRequest(req);
208
218
  if (!token) {
209
- if (!authorize) {
219
+ if (!authenticate) {
210
220
  return next();
211
221
  }
212
222
  throw new InvalidToken('token missing');
213
223
  }
214
224
  const valid = await redis.get(token);
215
225
  if (!valid) {
216
- if (!authorize) {
226
+ if (!authenticate) {
217
227
  logger.warn('request with invalid access token');
218
228
  return next();
219
229
  }
@@ -221,13 +231,20 @@ export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
221
231
  }
222
232
  const decoded = decodeAuthToken(token);
223
233
  const user = await getUserByToken(decoded);
234
+ if (options.isAuthorized && !(await options.isAuthorized(user))) {
235
+ throw Forbidden('Permission denied');
236
+ }
224
237
  setCurrentUser(user);
225
238
  next();
226
239
  }
227
240
  catch (error) {
228
241
  logger.error(error);
229
- next(Unauthorized('Invalid token'));
230
- return;
242
+ if (error instanceof ApiError) {
243
+ next(error);
244
+ }
245
+ else {
246
+ next(Unauthorized('Invalid token'));
247
+ }
231
248
  }
232
249
  };
233
250
  }
package/dist/csrf.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type RequestHandler, type Request, type Response } from 'express';
2
+ export declare const getCsrfSecret: () => string;
2
3
  export interface CsrfMiddlewareOptions {
3
4
  excempt?: boolean;
4
5
  cookieName?: string;
package/dist/csrf.js CHANGED
@@ -9,6 +9,13 @@ const defaultCookieName = () => {
9
9
  ? '__Host-devmoods.x-csrf-token'
10
10
  : 'devmoods.x-csrf-token';
11
11
  };
12
+ export const getCsrfSecret = () => {
13
+ const secret = readEnv('CSRF_SECRET', '');
14
+ if (!secret) {
15
+ throw new Error('CSRF_SECRET is not set');
16
+ }
17
+ return secret;
18
+ };
12
19
  export function csrf({ excempt = false, sessionIdentifier, cookieName, } = {}) {
13
20
  return (req, res, next) => {
14
21
  const existingOptions = useCsrfOptions();
@@ -34,7 +41,7 @@ function useCsrfOptions() {
34
41
  });
35
42
  }
36
43
  const generateTokenAndHash = (req, { overwrite = false } = {}) => {
37
- const secret = readEnv('CSRF_SECRET', '');
44
+ const secret = getCsrfSecret();
38
45
  const { sessionIdentifier, cookieName } = useCsrfOptions();
39
46
  const csrfCookie = req.cookies[cookieName];
40
47
  if (typeof csrfCookie === 'string' && !overwrite) {
@@ -84,7 +91,7 @@ const validateRequest = (req) => {
84
91
  }
85
92
  const [csrfToken, csrfTokenHash] = csrfCookie.split('|');
86
93
  const csrfTokenFromRequest = req.headers['x-csrf-token'];
87
- const secret = readEnv('CSRF_SECRET', '');
94
+ const secret = getCsrfSecret();
88
95
  if (!csrfTokenFromRequest) {
89
96
  throw Forbidden('Missing CSRF token');
90
97
  }
@@ -38,7 +38,7 @@ export declare function after(): RequestHandler;
38
38
  type ResponseHeaders = Record<string, string>;
39
39
  type AnyResponseBody = Record<string, any> | string;
40
40
  type HttpResponseCode = number;
41
- type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
41
+ type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | TResponseBody[] | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
42
42
  /**
43
43
  * Alias of `route()`
44
44
  */
@@ -1,7 +1,8 @@
1
1
  import pg, { type PoolClient, type PoolConfig, type QueryResult, type QueryResultRow } from 'pg';
2
2
  export declare const escapePostgresIdentifier: typeof pg.escapeIdentifier;
3
3
  export declare const escapePostgresLiteral: typeof pg.escapeLiteral;
4
- export interface PostgresOptions extends PoolConfig {
4
+ export declare const formatSql: (statement: SqlStatement | string) => string;
5
+ export interface PostgresOptions extends PoolConfig, ConnectionOptions {
5
6
  onError?: (error: Error, client: any) => void;
6
7
  url?: PoolConfig['connectionString'];
7
8
  }
@@ -15,14 +16,19 @@ export declare class IntegrityError extends Error {
15
16
  constructor(message: string);
16
17
  }
17
18
  type Value = null | string | number | Date;
19
+ export interface ConnectionOptions {
20
+ onLogStatement?: (statement: SqlStatement | string, values: unknown[] | undefined, duration: number) => void;
21
+ }
18
22
  export declare class Connection {
23
+ private options;
19
24
  client: PoolClient;
20
- constructor(client: PoolClient);
21
- query<T extends QueryResultRow>(text: string | SQLStatement, values?: Value[]): Promise<QueryResult<T>>;
22
- all<T>(text: string | SQLStatement, values?: Value[]): Promise<T[]>;
23
- insert<T extends QueryResultRow>(text: string | SQLStatement, values?: Value[]): Promise<QueryResult<T>>;
24
- first<T>(text: string | SQLStatement, values?: Value[]): Promise<T | null>;
25
- get<T>(text: string | SQLStatement, values?: Value[]): Promise<T>;
25
+ constructor(client: PoolClient, options: ConnectionOptions);
26
+ private onLogStatement;
27
+ query<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
28
+ all<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T[]>;
29
+ insert<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
30
+ first<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T | null>;
31
+ get<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T>;
26
32
  }
27
33
  export declare class Transaction extends Connection {
28
34
  }
@@ -34,27 +40,27 @@ export declare class Transaction extends Connection {
34
40
  * @param config
35
41
  */
36
42
  export declare function createPostgres({ onError, url, ...config }?: PostgresOptions): {
37
- insert: <T extends QueryResultRow>(text: string | SQLStatement, values?: Value[]) => Promise<QueryResult<T>>;
38
- query: <T extends QueryResultRow>(text: string | SQLStatement, values?: Value[]) => Promise<QueryResult<T>>;
39
- first: <T>(text: string | SQLStatement, values?: Value[]) => Promise<T | null>;
40
- all: <T>(text: string | SQLStatement, values?: Value[]) => Promise<T[]>;
41
- get: <T>(text: string | SQLStatement, values?: Value[]) => Promise<T>;
43
+ insert: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<QueryResult<T>>;
44
+ query: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<QueryResult<T>>;
45
+ first: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T | null>;
46
+ all: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T[]>;
47
+ get: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T>;
42
48
  withConnection: <T>(callback: (connection: Connection) => Promise<T>) => Promise<T>;
43
49
  transaction: <T>(callback: (connection: Transaction) => Promise<T> | T) => Promise<T>;
44
50
  instance: pg.Pool;
45
51
  clear: (force?: boolean) => Promise<void>;
46
52
  end(): Promise<void>;
47
53
  };
48
- declare class SQLStatement {
54
+ export declare class SqlStatement {
49
55
  stringsAndValues: [string, unknown | undefined][];
50
56
  constructor(strings: string[], values: unknown[]);
51
57
  _extract(strings: string[], values: unknown[]): void;
52
- append(statement: SQLStatement | string): this;
58
+ append(statement: SqlStatement | string): this;
53
59
  get values(): ({} | null)[];
54
60
  get text(): string;
55
61
  }
56
62
  export declare const sql: {
57
- (strings: TemplateStringsArray, ...values: unknown[]): SQLStatement;
63
+ (strings: TemplateStringsArray, ...values: unknown[]): SqlStatement;
58
64
  identifier(name: string): {
59
65
  name: string;
60
66
  raw: boolean;
@@ -63,14 +69,14 @@ export declare const sql: {
63
69
  name: string;
64
70
  raw: boolean;
65
71
  };
66
- literal(name: string): {
72
+ literal(name: string | null): {
67
73
  name: string;
68
74
  raw: boolean;
69
75
  };
70
76
  spreadUpdate: typeof spreadUpdate;
71
77
  spreadInsert: typeof spreadInsert;
72
78
  };
73
- declare function spreadUpdate(data: Record<string, any>): SQLStatement;
74
- declare function spreadInsert<TData extends Record<string, any>>(data: TData | TData[]): SQLStatement;
75
- export declare function joinStatements(statements: SQLStatement[], separator: string): SQLStatement;
79
+ declare function spreadUpdate(data: Record<string, any>): SqlStatement;
80
+ declare function spreadInsert<TData extends Record<string, any>>(data: TData | TData[]): SqlStatement;
81
+ export declare function joinStatements(statements: SqlStatement[], separator: string): SqlStatement;
76
82
  export {};
package/dist/postgres.js CHANGED
@@ -1,7 +1,17 @@
1
1
  import pg, {} from 'pg';
2
- import { readEnv } from './utils.js';
2
+ import { format } from 'sql-formatter';
3
+ import { readEnv, startTimer } from './utils.js';
3
4
  export const escapePostgresIdentifier = pg.escapeIdentifier;
4
5
  export const escapePostgresLiteral = pg.escapeLiteral;
6
+ export const formatSql = (statement) => {
7
+ const value = statement instanceof SqlStatement ? statement.text : statement;
8
+ return format(value, {
9
+ language: 'postgresql',
10
+ keywordCase: 'upper',
11
+ tabWidth: 2,
12
+ useTabs: false,
13
+ });
14
+ };
5
15
  export class DoesNotExistError extends Error {
6
16
  constructor(message) {
7
17
  super(message);
@@ -21,10 +31,20 @@ export class IntegrityError extends Error {
21
31
  }
22
32
  }
23
33
  export class Connection {
24
- constructor(client) {
34
+ constructor(client, options) {
35
+ this.options = options;
25
36
  this.client = client;
26
37
  }
38
+ onLogStatement(text, values, duration) {
39
+ if (this.options.onLogStatement) {
40
+ return this.options.onLogStatement(text, values, duration);
41
+ }
42
+ if (readEnv.boolean('LOG_SQL', false)) {
43
+ console.log(`\n${formatSql(text)}\n\nValues: ${JSON.stringify(values)}\nDuration: ${duration}ms\n`);
44
+ }
45
+ }
27
46
  async query(text, values) {
47
+ const timer = startTimer();
28
48
  try {
29
49
  return await this.client.query(text, values);
30
50
  }
@@ -34,9 +54,12 @@ export class Connection {
34
54
  }
35
55
  throw error;
36
56
  }
57
+ finally {
58
+ this.onLogStatement?.(text, values, timer.getDuration());
59
+ }
37
60
  }
38
61
  async all(text, values) {
39
- const result = await this.client.query(text, values);
62
+ const result = await this.query(text, values);
40
63
  if (result.rows) {
41
64
  return result.rows;
42
65
  /* c8 ignore next 2*/
@@ -47,11 +70,11 @@ export class Connection {
47
70
  return await this.query(text, values);
48
71
  }
49
72
  async first(text, values) {
50
- const result = await this.client.query(text, values);
73
+ const result = await this.query(text, values);
51
74
  return result.rows && result.rows.length ? result.rows[0] : null;
52
75
  }
53
76
  async get(text, values) {
54
- const result = await this.client.query(text, values);
77
+ const result = await this.query(text, values);
55
78
  if (result.rowCount === 0) {
56
79
  throw new DoesNotExistError('Row matching query not found');
57
80
  }
@@ -78,7 +101,7 @@ export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...conf
78
101
  async function withConnection(callback) {
79
102
  const client = await pool.connect();
80
103
  try {
81
- const result = await callback(new Connection(client));
104
+ const result = await callback(new Connection(client, { onLogStatement: config.onLogStatement }));
82
105
  return result;
83
106
  }
84
107
  finally {
@@ -104,7 +127,7 @@ export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...conf
104
127
  const client = await pool.connect();
105
128
  try {
106
129
  await client.query('BEGIN');
107
- const result = await callback(new Transaction(client));
130
+ const result = await callback(new Transaction(client, { onLogStatement: config.onLogStatement }));
108
131
  await client.query('COMMIT');
109
132
  return result;
110
133
  }
@@ -160,7 +183,7 @@ export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...conf
160
183
  const isRaw = (value) => {
161
184
  return (typeof value === 'object' && value !== null && 'raw' in value && !!value.raw);
162
185
  };
163
- class SQLStatement {
186
+ export class SqlStatement {
164
187
  constructor(strings, values) {
165
188
  this.stringsAndValues = [];
166
189
  this._extract(strings, values);
@@ -172,7 +195,7 @@ class SQLStatement {
172
195
  if (isRaw(value)) {
173
196
  this.stringsAndValues.push([string + value.name, undefined]);
174
197
  }
175
- else if (value instanceof SQLStatement) {
198
+ else if (value instanceof SqlStatement) {
176
199
  this.stringsAndValues.push([string, undefined]);
177
200
  this.stringsAndValues.push(...value.stringsAndValues);
178
201
  }
@@ -182,7 +205,7 @@ class SQLStatement {
182
205
  }
183
206
  }
184
207
  append(statement) {
185
- if (statement instanceof SQLStatement) {
208
+ if (statement instanceof SqlStatement) {
186
209
  this.stringsAndValues.push(...statement.stringsAndValues);
187
210
  }
188
211
  else {
@@ -211,14 +234,16 @@ class SQLStatement {
211
234
  }
212
235
  }
213
236
  export const sql = (strings, ...values) => {
214
- return new SQLStatement(Array.from(strings), values);
237
+ return new SqlStatement(Array.from(strings), values);
215
238
  };
216
239
  function spreadUpdate(data) {
217
240
  const entries = Object.entries(data);
218
241
  if (entries.length === 0) {
219
242
  return sql ``;
220
243
  }
221
- return joinStatements(entries.map(([key, value]) => sql `${sql.identifier(key)} = ${value}`), ', ');
244
+ return joinStatements(entries
245
+ .filter(([, value]) => typeof value !== 'undefined')
246
+ .map(([key, value]) => sql `${sql.identifier(key)} = ${value == null ? sql.raw('NULL') : value}`), ', ');
222
247
  }
223
248
  function spreadInsert(data) {
224
249
  const rows = Array.isArray(data) ? data : [data];
@@ -257,6 +282,9 @@ sql.raw = (name) => {
257
282
  * Inject a literal value into the template string
258
283
  */
259
284
  sql.literal = (name) => {
285
+ if (name === null) {
286
+ return { name: 'NULL', raw: true };
287
+ }
260
288
  return { name: escapePostgresLiteral(name), raw: true };
261
289
  };
262
290
  sql.spreadUpdate = spreadUpdate;
package/dist/utils.d.ts CHANGED
@@ -4,6 +4,7 @@ import { type EventEmitter } from 'node:events';
4
4
  *
5
5
  */
6
6
  export declare function startTimer(): {
7
+ reset(): void;
7
8
  getDuration(): number;
8
9
  };
9
10
  export declare function readEnv(key: string): string | undefined;
package/dist/utils.js CHANGED
@@ -4,8 +4,11 @@ import {} from 'node:events';
4
4
  *
5
5
  */
6
6
  export function startTimer() {
7
- const start = process.hrtime();
7
+ let start = process.hrtime();
8
8
  return {
9
+ reset() {
10
+ start = process.hrtime();
11
+ },
9
12
  getDuration() {
10
13
  const elapsed = process.hrtime(start);
11
14
  return (elapsed[0] * 1e9 + elapsed[1]) / 1e9;
@@ -39,12 +39,12 @@ export type ValidationResult = {
39
39
  [key: string]: any;
40
40
  };
41
41
  };
42
- export declare function compileValidateSchema(schema: ValidationSchema, options?: Options): (data: any) => {
43
- isValid: boolean;
44
- errors: null;
42
+ export declare function compileValidateSchema(schema: ValidationSchema, options?: Options): (data: unknown) => {
43
+ readonly isValid: true;
44
+ readonly errors: null;
45
45
  } | {
46
- isValid: boolean;
47
- errors: {
46
+ readonly isValid: false;
47
+ readonly errors: {
48
48
  [key: string]: any[];
49
49
  };
50
50
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.40.1",
3
+ "version": "0.42.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,31 +20,31 @@
20
20
  "devDependencies": {
21
21
  "@devmoods/eslint-config": "^1.15.0",
22
22
  "@types/koa-compose": "^3.2.8",
23
- "@types/node": "22.9.3",
23
+ "@types/node": "22.10.1",
24
24
  "@types/supertest": "^6.0.2",
25
- "@typescript-eslint/eslint-plugin": "^8.15.0",
26
- "@typescript-eslint/parser": "^8.15.0",
27
- "@vitest/coverage-v8": "^2.1.5",
25
+ "@typescript-eslint/eslint-plugin": "^8.17.0",
26
+ "@typescript-eslint/parser": "^8.17.0",
27
+ "@vitest/coverage-v8": "^2.1.8",
28
28
  "cookiejar": "^2.1.4",
29
29
  "eslint": "^8.57.0",
30
30
  "eslint-plugin-import": "^2.31.0",
31
31
  "eslint-plugin-prettier": "^5.2.1",
32
32
  "faktory-worker": "^4.6.0",
33
- "prettier": "3.3.3",
33
+ "prettier": "3.4.1",
34
34
  "supertest": "7.0.0",
35
35
  "ts-node": "10.9.2",
36
36
  "tsx": "^4.19.2",
37
37
  "typescript": "5.7.2",
38
- "vitest": "^2.1.5",
38
+ "vitest": "^2.1.8",
39
39
  "vitest-fail-on-console": "^0.7.1"
40
40
  },
41
41
  "dependencies": {
42
42
  "@devmoods/fetch": "^4.0.1",
43
- "@sentry/node": "^8.40.0",
44
- "@sentry/profiling-node": "^8.40.0",
43
+ "@sentry/node": "^8.42.0",
44
+ "@sentry/profiling-node": "^8.42.0",
45
45
  "@types/bcrypt": "^5.0.2",
46
46
  "@types/body-parser": "^1.19.5",
47
- "@types/cookie-parser": "^1.4.7",
47
+ "@types/cookie-parser": "^1.4.8",
48
48
  "@types/express": "^5.0.0",
49
49
  "@types/jsonwebtoken": "^9.0.7",
50
50
  "@types/nodemailer": "^6.4.17",
@@ -58,13 +58,14 @@
58
58
  "commander": "^12.1.0",
59
59
  "compose-middleware": "^5.0.1",
60
60
  "cookie-parser": "^1.4.7",
61
- "dotenv": "^16.4.5",
61
+ "dotenv": "^16.4.6",
62
62
  "express": "^4.21.1",
63
63
  "helmet": "^8.0.0",
64
64
  "jsonwebtoken": "^9.0.2",
65
65
  "nodemailer": "^6.9.16",
66
66
  "pg": "^8.13.1",
67
67
  "redis": "^4.7.0",
68
+ "sql-formatter": "^15.4.6",
68
69
  "uuid": "^11.0.3"
69
70
  },
70
71
  "prettier": {