@devmoods/express-extras 0.53.0 → 0.54.1

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/index.d.ts CHANGED
@@ -14,10 +14,12 @@ export * from './logger.js';
14
14
  export * from './manage.js';
15
15
  export * from './middleware.js';
16
16
  export * from './postgres.js';
17
+ export * from './rate-limit.js';
17
18
  export * from './redis.js';
18
19
  export * from './routes.js';
19
20
  export * from './sentry.js';
20
21
  export * from './slack.js';
22
+ export * from './sql.js';
21
23
  export * from './utils.js';
22
24
  export * from './validate.js';
23
25
  export * from './webapi.js';
package/dist/index.js CHANGED
@@ -15,10 +15,12 @@ export * from './logger.js';
15
15
  export * from './manage.js';
16
16
  export * from './middleware.js';
17
17
  export * from './postgres.js';
18
+ export * from './rate-limit.js';
18
19
  export * from './redis.js';
19
20
  export * from './routes.js';
20
21
  export * from './sentry.js';
21
22
  export * from './slack.js';
23
+ export * from './sql.js';
22
24
  export * from './utils.js';
23
25
  export * from './validate.js';
24
26
  export * from './webapi.js';
@@ -68,8 +68,17 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
68
68
  ].filter(Boolean));
69
69
  }
70
70
  function logRequestDuration() {
71
+ const FILTERED_PATHS = [
72
+ '/src',
73
+ '/node_modules',
74
+ '/@fs',
75
+ '/@vite',
76
+ '/@react-refresh',
77
+ '/@id',
78
+ '/analytics.js',
79
+ ];
71
80
  return (req, res, next) => {
72
- if (req.path.startsWith('/src')) {
81
+ if (FILTERED_PATHS.some((p) => req.path.startsWith(p))) {
73
82
  return next();
74
83
  }
75
84
  const timer = startTimer();
package/dist/migrate.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type CLI } from './cli.js';
3
- import { type Connection, createPostgres } from './postgres.js';
3
+ import { createPostgres } from './postgres.js';
4
4
  interface Migration {
5
5
  name: string;
6
6
  filename: string;
@@ -33,8 +33,8 @@ export declare class Migrator {
33
33
  log(...args: any[]): void;
34
34
  executeMigration(migrations: Migration[], currentVersion: number, direction: 'up' | 'down'): Promise<number>;
35
35
  readMigrationFile(filename: string): Promise<MigrationFileAPI>;
36
- migrate(connection: Connection, migrator: MigrationFileAPI, direction: 'up' | 'down'): Promise<void>;
37
- updateCurrentVersion(connection: Connection, version: number): Promise<import("pg").QueryResult<import("pg").QueryResultRow>>;
36
+ migrate(migrator: MigrationFileAPI, direction: 'up' | 'down'): Promise<void>;
37
+ updateCurrentVersion(version: number): Promise<import("pg").QueryResult<import("pg").QueryResultRow>>;
38
38
  getCurrentVersion(): Promise<number>;
39
39
  getMigrations(): Promise<{
40
40
  filename: string;
package/dist/migrate.js CHANGED
@@ -3,7 +3,8 @@ import * as path from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import {} from 'commander';
5
5
  import {} from './cli.js';
6
- import { createPostgres, formatSql, sql } from './postgres.js';
6
+ import { createPostgres } from './postgres.js';
7
+ import { formatSql, sql } from './sql.js';
7
8
  export const asCli = (command, cli) => {
8
9
  command.command('up').action(cli.wrap(async () => {
9
10
  const migrator = new Migrator();
@@ -138,15 +139,15 @@ export class Migrator {
138
139
  this.log('Running migration', migration.filename);
139
140
  const migrator = await this.readMigrationFile(migration.filename);
140
141
  if (migrator.useTransaction !== false) {
141
- await this.postgres.transaction(async (tx) => {
142
- await this.migrate(tx, migrator, direction);
143
- await this.updateCurrentVersion(tx, migration.version);
142
+ await this.postgres.transaction(async () => {
143
+ await this.migrate(migrator, direction);
144
+ await this.updateCurrentVersion(migration.version);
144
145
  });
145
146
  }
146
147
  else {
147
- await this.postgres.withConnection(async (tx) => {
148
- await this.migrate(tx, migrator, direction);
149
- await this.updateCurrentVersion(tx, migration.version);
148
+ await this.postgres.withConnection(async () => {
149
+ await this.migrate(migrator, direction);
150
+ await this.updateCurrentVersion(migration.version);
150
151
  });
151
152
  }
152
153
  }
@@ -165,15 +166,15 @@ export class Migrator {
165
166
  this.log('Rolling back migration', migrationToRollback.filename);
166
167
  const migrator = await this.readMigrationFile(migrationToRollback.filename);
167
168
  if (migrator.useTransaction !== false) {
168
- await this.postgres.transaction(async (tx) => {
169
- await this.migrate(tx, migrator, 'down');
170
- await this.updateCurrentVersion(tx, updatedVersion);
169
+ await this.postgres.transaction(async () => {
170
+ await this.migrate(migrator, 'down');
171
+ await this.updateCurrentVersion(updatedVersion);
171
172
  });
172
173
  }
173
174
  else {
174
- await this.postgres.withConnection(async (tx) => {
175
- await this.migrate(tx, migrator, 'down');
176
- await this.updateCurrentVersion(tx, updatedVersion);
175
+ await this.postgres.withConnection(async () => {
176
+ await this.migrate(migrator, 'down');
177
+ await this.updateCurrentVersion(updatedVersion);
177
178
  });
178
179
  }
179
180
  return await this.getCurrentVersion();
@@ -197,13 +198,13 @@ export class Migrator {
197
198
  },
198
199
  };
199
200
  }
200
- async migrate(connection, migrator, direction) {
201
+ async migrate(migrator, direction) {
201
202
  try {
202
203
  const queryWithLog = (statement) => {
203
204
  this.log(chalk.yellow(formatSql(statement)));
204
- return connection.query(statement);
205
+ return this.postgres.query(statement);
205
206
  };
206
- const returnValue = await migrator[direction](queryWithLog.bind(connection));
207
+ const returnValue = await migrator[direction](queryWithLog.bind(this.postgres));
207
208
  if (typeof returnValue === 'string') {
208
209
  if (returnValue !== '') {
209
210
  await queryWithLog(returnValue);
@@ -218,8 +219,8 @@ export class Migrator {
218
219
  throw new Error(`Migration failed: ${error.message}`);
219
220
  }
220
221
  }
221
- updateCurrentVersion(connection, version) {
222
- return connection.query(`INSERT INTO ${this.migrationTableName} (version, run_at) VALUES ($1, $2)`, [version, new Date()]);
222
+ updateCurrentVersion(version) {
223
+ return this.postgres.query(`INSERT INTO ${this.migrationTableName} (version, run_at) VALUES ($1, $2)`, [version, new Date()]);
223
224
  }
224
225
  async getCurrentVersion() {
225
226
  const result = await this.postgres.first(`SELECT version FROM ${this.migrationTableName} ORDER BY run_at DESC LIMIT 1`);
@@ -1,10 +1,9 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { type RequestHandler } from 'express';
1
3
  import pg, { type PoolClient, type PoolConfig, type QueryResult, type QueryResultRow } from 'pg';
4
+ import { type SqlStatement } from './sql.js';
2
5
  export declare const escapePostgresIdentifier: typeof pg.escapeIdentifier;
3
6
  export declare const escapePostgresLiteral: typeof pg.escapeLiteral;
4
- /**
5
- * Returns a pretty-printed SQL statement.
6
- */
7
- export declare const formatSql: (statement: SqlStatement | string) => string;
8
7
  export interface PostgresOptions extends PoolConfig, ConnectionOptions {
9
8
  onError?: (error: Error, client: any) => void;
10
9
  url?: PoolConfig['connectionString'];
@@ -22,11 +21,18 @@ type Value = null | string | number | Date;
22
21
  export interface ConnectionOptions {
23
22
  onLogStatement?: (statement: SqlStatement | string, values: unknown[] | undefined, duration: number) => void;
24
23
  }
24
+ export interface TransactionContext {
25
+ connection: Connection;
26
+ savepoints: string[];
27
+ isCommitted: boolean;
28
+ isRolledBack: boolean;
29
+ }
30
+ export declare const transactionStorage: AsyncLocalStorage<TransactionContext>;
25
31
  export declare class Connection {
26
32
  private options;
27
33
  client: PoolClient;
28
34
  constructor(client: PoolClient, options: ConnectionOptions);
29
- private onLogStatement;
35
+ private logStatement;
30
36
  query<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
31
37
  all<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T[]>;
32
38
  insert<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
@@ -34,6 +40,12 @@ export declare class Connection {
34
40
  get<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T>;
35
41
  }
36
42
  export declare class Transaction extends Connection {
43
+ savepoints: string[];
44
+ isCommitted: boolean;
45
+ isRolledBack: boolean;
46
+ createSavepoint(): Promise<string>;
47
+ rollbackToSavepoint(savepointName: string): Promise<void>;
48
+ releaseSavepoint(savepointName: string): Promise<void>;
37
49
  }
38
50
  /**
39
51
  * Create a new Postgres connection pool with
@@ -49,37 +61,11 @@ export declare function createPostgres({ onError, url, ...postgresOptions }?: Po
49
61
  all: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T[]>;
50
62
  get: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T>;
51
63
  withConnection: <T>(callback: (connection: Connection) => Promise<T>) => Promise<T>;
52
- transaction: <T>(callback: (connection: Transaction) => Promise<T> | T) => Promise<T>;
64
+ transaction: <T>(callback: () => Promise<T> | T) => Promise<T>;
65
+ getCurrentTransaction: () => Transaction | null;
53
66
  instance: pg.Pool;
54
67
  clear: (force?: boolean) => Promise<void>;
55
68
  end(): Promise<void>;
56
69
  };
57
- export declare class SqlStatement {
58
- stringsAndValues: [string, unknown | undefined][];
59
- constructor(strings: string[], values: unknown[]);
60
- _extract(strings: string[], values: unknown[]): void;
61
- append(statement: SqlStatement | string): this;
62
- get values(): ({} | null)[];
63
- get text(): string;
64
- }
65
- export declare const sql: {
66
- (strings: TemplateStringsArray, ...values: unknown[]): SqlStatement;
67
- identifier(name: string): {
68
- name: string;
69
- raw: boolean;
70
- };
71
- raw(name: string): {
72
- name: string;
73
- raw: boolean;
74
- };
75
- literal(name: string | null): {
76
- name: string;
77
- raw: boolean;
78
- };
79
- spreadUpdate: typeof spreadUpdate;
80
- spreadInsert: typeof spreadInsert;
81
- };
82
- declare function spreadUpdate(data: Record<string, any>): SqlStatement;
83
- declare function spreadInsert<TData extends Record<string, any>>(data: TData | TData[]): SqlStatement;
84
- export declare function joinStatements(statements: SqlStatement[], separator: string): SqlStatement;
70
+ export declare function withTransaction(postgres: ReturnType<typeof createPostgres>): RequestHandler;
85
71
  export {};
package/dist/postgres.js CHANGED
@@ -1,21 +1,11 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import {} from 'express';
1
3
  import pg, {} from 'pg';
2
- import { format } from 'sql-formatter';
3
4
  import { config } from './config.js';
5
+ import { formatSql } from './sql.js';
4
6
  import { startTimer } from './utils.js';
5
7
  export const escapePostgresIdentifier = pg.escapeIdentifier;
6
8
  export const escapePostgresLiteral = pg.escapeLiteral;
7
- /**
8
- * Returns a pretty-printed SQL statement.
9
- */
10
- export const formatSql = (statement) => {
11
- const value = statement instanceof SqlStatement ? statement.text : statement;
12
- return format(value, {
13
- language: 'postgresql',
14
- keywordCase: 'upper',
15
- tabWidth: 2,
16
- useTabs: false,
17
- });
18
- };
19
9
  export class DoesNotExistError extends Error {
20
10
  constructor(message) {
21
11
  super(message);
@@ -34,12 +24,13 @@ export class IntegrityError extends Error {
34
24
  this.name = 'IntegrityError';
35
25
  }
36
26
  }
27
+ export const transactionStorage = new AsyncLocalStorage();
37
28
  export class Connection {
38
29
  constructor(client, options) {
39
30
  this.options = options;
40
31
  this.client = client;
41
32
  }
42
- onLogStatement(text, values, duration) {
33
+ logStatement(text, values, duration) {
43
34
  if (this.options.onLogStatement) {
44
35
  return this.options.onLogStatement(text, values, duration);
45
36
  }
@@ -59,7 +50,7 @@ export class Connection {
59
50
  throw error;
60
51
  }
61
52
  finally {
62
- this.onLogStatement?.(text, values, timer.getDuration());
53
+ this.logStatement?.(text, values, timer.getDuration());
63
54
  }
64
55
  }
65
56
  async all(text, values) {
@@ -89,6 +80,28 @@ export class Connection {
89
80
  }
90
81
  }
91
82
  export class Transaction extends Connection {
83
+ constructor() {
84
+ super(...arguments);
85
+ this.savepoints = [];
86
+ this.isCommitted = false;
87
+ this.isRolledBack = false;
88
+ }
89
+ async createSavepoint() {
90
+ const savepointName = `sp_${this.savepoints.length + 1}`;
91
+ await this.query(`SAVEPOINT ${savepointName}`);
92
+ this.savepoints.push(savepointName);
93
+ return savepointName;
94
+ }
95
+ async rollbackToSavepoint(savepointName) {
96
+ await this.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
97
+ }
98
+ async releaseSavepoint(savepointName) {
99
+ await this.query(`RELEASE SAVEPOINT ${savepointName}`);
100
+ const index = this.savepoints.indexOf(savepointName);
101
+ if (index !== -1) {
102
+ this.savepoints.splice(index, 1);
103
+ }
104
+ }
92
105
  }
93
106
  /**
94
107
  * Create a new Postgres connection pool with
@@ -103,6 +116,11 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
103
116
  onError?.(e, client);
104
117
  });
105
118
  async function withConnection(callback) {
119
+ // Check if we're already in a transaction context
120
+ const txContext = transactionStorage.getStore();
121
+ if (txContext) {
122
+ return callback(txContext.connection);
123
+ }
106
124
  const client = await pool.connect();
107
125
  try {
108
126
  const result = await callback(new Connection(client, {
@@ -128,21 +146,50 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
128
146
  }
129
147
  /**
130
148
  * Run queries inside a BEGIN, COMMIT / ROLLBACK transaction.
149
+ * Supports nested transactions using SAVEPOINT.
131
150
  */
132
151
  async function transaction(callback) {
152
+ // Check if we're already in a transaction context
153
+ const existingTxContext = transactionStorage.getStore();
154
+ if (existingTxContext) {
155
+ // We're already in a transaction, create a savepoint for nested transaction
156
+ const txConnection = existingTxContext.connection;
157
+ const savepointName = await txConnection.createSavepoint();
158
+ try {
159
+ const result = await callback();
160
+ await txConnection.releaseSavepoint(savepointName);
161
+ return result;
162
+ }
163
+ catch (error) {
164
+ await txConnection.rollbackToSavepoint(savepointName);
165
+ throw error;
166
+ }
167
+ }
168
+ // Start a new top-level transaction
133
169
  const client = await pool.connect();
170
+ const txConnection = new Transaction(client, {
171
+ onLogStatement: postgresOptions.onLogStatement,
172
+ });
134
173
  try {
135
- await client.query('BEGIN');
136
- const result = await callback(new Transaction(client, {
137
- onLogStatement: postgresOptions.onLogStatement,
138
- }));
139
- await client.query('COMMIT');
174
+ await txConnection.query('BEGIN');
175
+ // Create transaction context
176
+ const txContext = {
177
+ connection: txConnection,
178
+ savepoints: [],
179
+ isCommitted: false,
180
+ isRolledBack: false,
181
+ };
182
+ // Run the callback within the transaction context
183
+ const result = await transactionStorage.run(txContext, async () => {
184
+ return await callback();
185
+ });
186
+ await txConnection.query('COMMIT');
187
+ txContext.isCommitted = true;
140
188
  return result;
141
189
  }
142
190
  catch (error) {
143
- await client.query('ROLLBACK');
191
+ await txConnection.query('ROLLBACK');
144
192
  throw error;
145
- /* c8 ignore next 3 */
146
193
  }
147
194
  finally {
148
195
  client.release();
@@ -173,6 +220,14 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
173
220
  return connection.get(...args);
174
221
  });
175
222
  };
223
+ /**
224
+ * Get the current transaction from the AsyncLocalStorage context.
225
+ * Returns null if not in a transaction.
226
+ */
227
+ function getCurrentTransaction() {
228
+ const txContext = transactionStorage.getStore();
229
+ return txContext ? txContext.connection : null;
230
+ }
176
231
  return {
177
232
  insert,
178
233
  query,
@@ -181,6 +236,7 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
181
236
  get,
182
237
  withConnection,
183
238
  transaction,
239
+ getCurrentTransaction,
184
240
  instance: pool,
185
241
  clear,
186
242
  end() {
@@ -188,123 +244,19 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
188
244
  },
189
245
  };
190
246
  }
191
- const isRaw = (value) => {
192
- return (typeof value === 'object' && value !== null && 'raw' in value && !!value.raw);
193
- };
194
- export class SqlStatement {
195
- constructor(strings, values) {
196
- this.stringsAndValues = [];
197
- this._extract(strings, values);
198
- }
199
- _extract(strings, values) {
200
- for (let i = 0; i < strings.length; i++) {
201
- const string = strings[i];
202
- const value = values[i];
203
- if (isRaw(value)) {
204
- this.stringsAndValues.push([string + value.name, undefined]);
205
- }
206
- else if (value instanceof SqlStatement) {
207
- this.stringsAndValues.push([string, undefined]);
208
- this.stringsAndValues.push(...value.stringsAndValues);
209
- }
210
- else {
211
- this.stringsAndValues.push([string, value]);
212
- }
213
- }
214
- }
215
- append(statement) {
216
- if (statement instanceof SqlStatement) {
217
- this.stringsAndValues.push(...statement.stringsAndValues);
218
- }
219
- else {
220
- this.stringsAndValues.push([statement, undefined]);
221
- }
222
- return this;
223
- }
224
- get values() {
225
- return this.stringsAndValues
226
- .map(([, value]) => value)
227
- .filter((x) => x !== undefined);
228
- }
229
- get text() {
230
- let i = 1;
231
- let result = '';
232
- for (const [string, value] of this.stringsAndValues) {
233
- if (value == null) {
234
- result += string;
235
- }
236
- else {
237
- result += `${string}$${i}`;
238
- i += 1;
239
- }
240
- }
241
- return result;
242
- }
243
- }
244
- export const sql = (strings, ...values) => {
245
- return new SqlStatement(Array.from(strings), values);
246
- };
247
- function spreadUpdate(data) {
248
- const entries = Object.entries(data);
249
- if (entries.length === 0) {
250
- return sql ``;
251
- }
252
- return joinStatements(entries
253
- .filter(([, value]) => typeof value !== 'undefined')
254
- .map(([key, value]) => sql `${sql.identifier(key)} = ${value == null ? sql.raw('NULL') : value}`), ', ');
255
- }
256
- function spreadInsert(data) {
257
- const rows = Array.isArray(data) ? data : [data];
258
- if (rows.length === 0) {
259
- return sql ``;
260
- }
261
- const statement = sql `(`;
262
- statement.append(Object.keys(rows[0])
263
- .map((k) => escapePostgresIdentifier(k))
264
- .join(', '));
265
- statement.append(sql `) VALUES `);
266
- const toInsert = [];
267
- for (const row of rows) {
268
- const values = Object.values(row);
269
- if (values.length === 0) {
270
- continue;
247
+ export function withTransaction(postgres) {
248
+ return async (req, res, next) => {
249
+ try {
250
+ await postgres.transaction(async () => {
251
+ await new Promise((resolve, reject) => {
252
+ res.on('finish', resolve);
253
+ res.on('error', reject);
254
+ next();
255
+ });
256
+ });
271
257
  }
272
- toInsert.push(sql `(${joinStatements(values.map((value) => sql `${value ?? sql.raw('NULL')}`), ', ')})`);
273
- }
274
- statement.append(joinStatements(toInsert, ', '));
275
- return statement;
276
- }
277
- /**
278
- * Inject an identifier into the template string
279
- */
280
- sql.identifier = (name) => {
281
- return { name: escapePostgresIdentifier(name), raw: true };
282
- };
283
- /**
284
- * Inject a raw value into the template string
285
- */
286
- sql.raw = (name) => {
287
- return { name, raw: true };
288
- };
289
- /**
290
- * Inject a literal value into the template string
291
- */
292
- sql.literal = (name) => {
293
- if (name === null) {
294
- return { name: 'NULL', raw: true };
295
- }
296
- return { name: escapePostgresLiteral(name), raw: true };
297
- };
298
- sql.spreadUpdate = spreadUpdate;
299
- sql.spreadInsert = spreadInsert;
300
- export function joinStatements(statements, separator) {
301
- let i = 0;
302
- const statement = sql ``;
303
- for (const o of statements) {
304
- if (i++ !== 0) {
305
- statement.append(separator);
258
+ catch (error) {
259
+ next(error);
306
260
  }
307
- statement.append(o);
308
- }
309
- return statement;
261
+ };
310
262
  }
@@ -0,0 +1,22 @@
1
+ import { type RequestHandler, type Request } from 'express';
2
+ import { type RedisClient } from './redis.js';
3
+ export interface RateLimitOptions {
4
+ /**
5
+ * Maximum number of requests allowed within the window
6
+ * @default 100
7
+ */
8
+ max?: number;
9
+ /**
10
+ * Time window in seconds
11
+ * @default 60
12
+ */
13
+ windowSizeInSeconds?: number;
14
+ /**
15
+ * Function to generate a key for rate limiting
16
+ * @default IP-based rate limiting
17
+ */
18
+ getKey?: (req: Request) => string;
19
+ }
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
+ };
@@ -0,0 +1,35 @@
1
+ import {} from 'express';
2
+ import { config } from './config.js';
3
+ import { makeError } from './errors.js';
4
+ import {} from './redis.js';
5
+ export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60, getKey = (req) => req.ip || 'unknown', } = {}) {
6
+ const handler = async (req, res, next) => {
7
+ try {
8
+ const key = `${config.value.REDIS_KEY_PREFIX}rate-limit:${getKey(req)}`;
9
+ // Use Redis to track requests
10
+ const currentCount = await redis.get(key);
11
+ const count = currentCount ? parseInt(currentCount, 10) : 0;
12
+ // Set headers for rate limiting info
13
+ res.setHeader('X-RateLimit-Limit', max.toString());
14
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count - 1).toString());
15
+ if (count >= max) {
16
+ throw makeError(429, 'Rate limit exceeded');
17
+ }
18
+ if (count === 0) {
19
+ await redis.set(key, '1', { EX: windowSizeInSeconds });
20
+ }
21
+ else {
22
+ await redis.incr(key);
23
+ }
24
+ next();
25
+ }
26
+ catch (error) {
27
+ next(error);
28
+ }
29
+ };
30
+ const clear = async (key) => {
31
+ await redis.del(`rate-limit:${key}`);
32
+ };
33
+ handler.clear = clear;
34
+ return handler;
35
+ }
package/dist/sql.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export declare class SqlStatement {
2
+ stringsAndValues: [string, unknown | undefined][];
3
+ constructor(strings: string[], values: unknown[]);
4
+ _extract(strings: string[], values: unknown[]): void;
5
+ append(statement: SqlStatement | string): this;
6
+ get values(): ({} | null)[];
7
+ get text(): string;
8
+ }
9
+ export declare const sql: {
10
+ (strings: TemplateStringsArray, ...values: unknown[]): SqlStatement;
11
+ identifier(name: string): {
12
+ name: string;
13
+ raw: boolean;
14
+ };
15
+ raw(name: string): {
16
+ name: string;
17
+ raw: boolean;
18
+ };
19
+ literal(name: string | null): {
20
+ name: string;
21
+ raw: boolean;
22
+ };
23
+ spreadUpdate: typeof spreadUpdate;
24
+ spreadInsert: typeof spreadInsert;
25
+ };
26
+ declare function spreadUpdate(data: Record<string, any>): SqlStatement;
27
+ declare function spreadInsert<TData extends Record<string, any>>(data: TData | TData[]): SqlStatement;
28
+ export declare function joinStatements(statements: SqlStatement[], separator: string): SqlStatement;
29
+ /**
30
+ * Returns a pretty-printed SQL statement.
31
+ */
32
+ export declare const formatSql: (statement: SqlStatement | string) => string;
33
+ export {};
package/dist/sql.js ADDED
@@ -0,0 +1,134 @@
1
+ import { format } from 'sql-formatter';
2
+ import { escapePostgresIdentifier, escapePostgresLiteral } from './postgres.js';
3
+ const isRaw = (value) => {
4
+ return (typeof value === 'object' && value !== null && 'raw' in value && !!value.raw);
5
+ };
6
+ export class SqlStatement {
7
+ constructor(strings, values) {
8
+ this.stringsAndValues = [];
9
+ this._extract(strings, values);
10
+ }
11
+ _extract(strings, values) {
12
+ for (let i = 0; i < strings.length; i++) {
13
+ const string = strings[i];
14
+ const value = values[i];
15
+ if (isRaw(value)) {
16
+ this.stringsAndValues.push([string + value.name, undefined]);
17
+ }
18
+ else if (value instanceof SqlStatement) {
19
+ this.stringsAndValues.push([string, undefined]);
20
+ this.stringsAndValues.push(...value.stringsAndValues);
21
+ }
22
+ else {
23
+ this.stringsAndValues.push([string, value]);
24
+ }
25
+ }
26
+ }
27
+ append(statement) {
28
+ if (statement instanceof SqlStatement) {
29
+ this.stringsAndValues.push(...statement.stringsAndValues);
30
+ }
31
+ else {
32
+ this.stringsAndValues.push([statement, undefined]);
33
+ }
34
+ return this;
35
+ }
36
+ get values() {
37
+ return this.stringsAndValues
38
+ .map(([, value]) => value)
39
+ .filter((x) => x !== undefined);
40
+ }
41
+ get text() {
42
+ let i = 1;
43
+ let result = '';
44
+ for (const [string, value] of this.stringsAndValues) {
45
+ if (value == null) {
46
+ result += string;
47
+ }
48
+ else {
49
+ result += `${string}$${i}`;
50
+ i += 1;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+ }
56
+ export const sql = (strings, ...values) => {
57
+ return new SqlStatement(Array.from(strings), values);
58
+ };
59
+ function spreadUpdate(data) {
60
+ const entries = Object.entries(data);
61
+ if (entries.length === 0) {
62
+ return sql ``;
63
+ }
64
+ return joinStatements(entries
65
+ .filter(([, value]) => typeof value !== 'undefined')
66
+ .map(([key, value]) => sql `${sql.identifier(key)} = ${value == null ? sql.raw('NULL') : value}`), ', ');
67
+ }
68
+ function spreadInsert(data) {
69
+ const rows = Array.isArray(data) ? data : [data];
70
+ if (rows.length === 0) {
71
+ return sql ``;
72
+ }
73
+ const statement = sql `(`;
74
+ statement.append(Object.keys(rows[0])
75
+ .map((k) => escapePostgresIdentifier(k))
76
+ .join(', '));
77
+ statement.append(sql `) VALUES `);
78
+ const toInsert = [];
79
+ for (const row of rows) {
80
+ const values = Object.values(row);
81
+ if (values.length === 0) {
82
+ continue;
83
+ }
84
+ toInsert.push(sql `(${joinStatements(values.map((value) => sql `${value ?? sql.raw('NULL')}`), ', ')})`);
85
+ }
86
+ statement.append(joinStatements(toInsert, ', '));
87
+ return statement;
88
+ }
89
+ /**
90
+ * Inject an identifier into the template string
91
+ */
92
+ sql.identifier = (name) => {
93
+ return { name: escapePostgresIdentifier(name), raw: true };
94
+ };
95
+ /**
96
+ * Inject a raw value into the template string
97
+ */
98
+ sql.raw = (name) => {
99
+ return { name, raw: true };
100
+ };
101
+ /**
102
+ * Inject a literal value into the template string
103
+ */
104
+ sql.literal = (name) => {
105
+ if (name === null) {
106
+ return { name: 'NULL', raw: true };
107
+ }
108
+ return { name: escapePostgresLiteral(name), raw: true };
109
+ };
110
+ sql.spreadUpdate = spreadUpdate;
111
+ sql.spreadInsert = spreadInsert;
112
+ export function joinStatements(statements, separator) {
113
+ let i = 0;
114
+ const statement = sql ``;
115
+ for (const o of statements) {
116
+ if (i++ !== 0) {
117
+ statement.append(separator);
118
+ }
119
+ statement.append(o);
120
+ }
121
+ return statement;
122
+ }
123
+ /**
124
+ * Returns a pretty-printed SQL statement.
125
+ */
126
+ export const formatSql = (statement) => {
127
+ const value = statement instanceof SqlStatement ? statement.text : statement;
128
+ return format(value, {
129
+ language: 'postgresql',
130
+ keywordCase: 'upper',
131
+ tabWidth: 2,
132
+ useTabs: false,
133
+ });
134
+ };
package/dist/validate.js CHANGED
@@ -56,6 +56,7 @@ function createAjv(options) {
56
56
  });
57
57
  ajv.addKeyword('$enumNames');
58
58
  ajv.addKeyword('$bitiforms');
59
+ ajv.addKeyword('$forms');
59
60
  return ajv;
60
61
  }
61
62
  export function compileValidateSchema(schema, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.53.0",
3
+ "version": "0.54.1",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",