@devmoods/express-extras 0.53.0 → 0.54.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/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,8 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import pg, { type PoolClient, type PoolConfig, type QueryResult, type QueryResultRow } from 'pg';
3
+ import { type SqlStatement } from './sql.js';
2
4
  export declare const escapePostgresIdentifier: typeof pg.escapeIdentifier;
3
5
  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
6
  export interface PostgresOptions extends PoolConfig, ConnectionOptions {
9
7
  onError?: (error: Error, client: any) => void;
10
8
  url?: PoolConfig['connectionString'];
@@ -22,6 +20,13 @@ type Value = null | string | number | Date;
22
20
  export interface ConnectionOptions {
23
21
  onLogStatement?: (statement: SqlStatement | string, values: unknown[] | undefined, duration: number) => void;
24
22
  }
23
+ export interface TransactionContext {
24
+ connection: Connection;
25
+ savepoints: string[];
26
+ isCommitted: boolean;
27
+ isRolledBack: boolean;
28
+ }
29
+ export declare const transactionStorage: AsyncLocalStorage<TransactionContext>;
25
30
  export declare class Connection {
26
31
  private options;
27
32
  client: PoolClient;
@@ -34,6 +39,12 @@ export declare class Connection {
34
39
  get<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T>;
35
40
  }
36
41
  export declare class Transaction extends Connection {
42
+ savepoints: string[];
43
+ isCommitted: boolean;
44
+ isRolledBack: boolean;
45
+ createSavepoint(): Promise<string>;
46
+ rollbackToSavepoint(savepointName: string): Promise<void>;
47
+ releaseSavepoint(savepointName: string): Promise<void>;
37
48
  }
38
49
  /**
39
50
  * Create a new Postgres connection pool with
@@ -49,37 +60,10 @@ export declare function createPostgres({ onError, url, ...postgresOptions }?: Po
49
60
  all: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T[]>;
50
61
  get: <T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]) => Promise<T>;
51
62
  withConnection: <T>(callback: (connection: Connection) => Promise<T>) => Promise<T>;
52
- transaction: <T>(callback: (connection: Transaction) => Promise<T> | T) => Promise<T>;
63
+ transaction: <T>(callback: () => Promise<T> | T) => Promise<T>;
64
+ getCurrentTransaction: () => Transaction | null;
53
65
  instance: pg.Pool;
54
66
  clear: (force?: boolean) => Promise<void>;
55
67
  end(): Promise<void>;
56
68
  };
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;
85
69
  export {};
package/dist/postgres.js CHANGED
@@ -1,21 +1,10 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import pg, {} from 'pg';
2
- import { format } from 'sql-formatter';
3
3
  import { config } from './config.js';
4
+ import { formatSql } from './sql.js';
4
5
  import { startTimer } from './utils.js';
5
6
  export const escapePostgresIdentifier = pg.escapeIdentifier;
6
7
  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
8
  export class DoesNotExistError extends Error {
20
9
  constructor(message) {
21
10
  super(message);
@@ -34,6 +23,7 @@ export class IntegrityError extends Error {
34
23
  this.name = 'IntegrityError';
35
24
  }
36
25
  }
26
+ export const transactionStorage = new AsyncLocalStorage();
37
27
  export class Connection {
38
28
  constructor(client, options) {
39
29
  this.options = options;
@@ -89,6 +79,28 @@ export class Connection {
89
79
  }
90
80
  }
91
81
  export class Transaction extends Connection {
82
+ constructor() {
83
+ super(...arguments);
84
+ this.savepoints = [];
85
+ this.isCommitted = false;
86
+ this.isRolledBack = false;
87
+ }
88
+ async createSavepoint() {
89
+ const savepointName = `sp_${this.savepoints.length + 1}`;
90
+ await this.query(`SAVEPOINT ${savepointName}`);
91
+ this.savepoints.push(savepointName);
92
+ return savepointName;
93
+ }
94
+ async rollbackToSavepoint(savepointName) {
95
+ await this.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
96
+ }
97
+ async releaseSavepoint(savepointName) {
98
+ await this.query(`RELEASE SAVEPOINT ${savepointName}`);
99
+ const index = this.savepoints.indexOf(savepointName);
100
+ if (index !== -1) {
101
+ this.savepoints.splice(index, 1);
102
+ }
103
+ }
92
104
  }
93
105
  /**
94
106
  * Create a new Postgres connection pool with
@@ -103,6 +115,11 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
103
115
  onError?.(e, client);
104
116
  });
105
117
  async function withConnection(callback) {
118
+ // Check if we're already in a transaction context
119
+ const txContext = transactionStorage.getStore();
120
+ if (txContext) {
121
+ return callback(txContext.connection);
122
+ }
106
123
  const client = await pool.connect();
107
124
  try {
108
125
  const result = await callback(new Connection(client, {
@@ -128,21 +145,50 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
128
145
  }
129
146
  /**
130
147
  * Run queries inside a BEGIN, COMMIT / ROLLBACK transaction.
148
+ * Supports nested transactions using SAVEPOINT.
131
149
  */
132
150
  async function transaction(callback) {
151
+ // Check if we're already in a transaction context
152
+ const existingTxContext = transactionStorage.getStore();
153
+ if (existingTxContext) {
154
+ // We're already in a transaction, create a savepoint for nested transaction
155
+ const txConnection = existingTxContext.connection;
156
+ const savepointName = await txConnection.createSavepoint();
157
+ try {
158
+ const result = await callback();
159
+ await txConnection.releaseSavepoint(savepointName);
160
+ return result;
161
+ }
162
+ catch (error) {
163
+ await txConnection.rollbackToSavepoint(savepointName);
164
+ throw error;
165
+ }
166
+ }
167
+ // Start a new top-level transaction
133
168
  const client = await pool.connect();
134
169
  try {
135
170
  await client.query('BEGIN');
136
- const result = await callback(new Transaction(client, {
171
+ const txConnection = new Transaction(client, {
137
172
  onLogStatement: postgresOptions.onLogStatement,
138
- }));
173
+ });
174
+ // Create transaction context
175
+ const txContext = {
176
+ connection: txConnection,
177
+ savepoints: [],
178
+ isCommitted: false,
179
+ isRolledBack: false,
180
+ };
181
+ // Run the callback within the transaction context
182
+ const result = await transactionStorage.run(txContext, async () => {
183
+ return await callback();
184
+ });
139
185
  await client.query('COMMIT');
186
+ txContext.isCommitted = true;
140
187
  return result;
141
188
  }
142
189
  catch (error) {
143
190
  await client.query('ROLLBACK');
144
191
  throw error;
145
- /* c8 ignore next 3 */
146
192
  }
147
193
  finally {
148
194
  client.release();
@@ -173,6 +219,14 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
173
219
  return connection.get(...args);
174
220
  });
175
221
  };
222
+ /**
223
+ * Get the current transaction from the AsyncLocalStorage context.
224
+ * Returns null if not in a transaction.
225
+ */
226
+ function getCurrentTransaction() {
227
+ const txContext = transactionStorage.getStore();
228
+ return txContext ? txContext.connection : null;
229
+ }
176
230
  return {
177
231
  insert,
178
232
  query,
@@ -181,6 +235,7 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
181
235
  get,
182
236
  withConnection,
183
237
  transaction,
238
+ getCurrentTransaction,
184
239
  instance: pool,
185
240
  clear,
186
241
  end() {
@@ -188,123 +243,3 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
188
243
  },
189
244
  };
190
245
  }
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;
271
- }
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);
306
- }
307
- statement.append(o);
308
- }
309
- return statement;
310
- }
@@ -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.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",