@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 +2 -0
- package/dist/index.js +2 -0
- package/dist/middleware.js +10 -1
- package/dist/migrate.d.ts +3 -3
- package/dist/migrate.js +19 -18
- package/dist/postgres.d.ts +20 -34
- package/dist/postgres.js +91 -139
- package/dist/rate-limit.d.ts +22 -0
- package/dist/rate-limit.js +35 -0
- package/dist/sql.d.ts +33 -0
- package/dist/sql.js +134 -0
- package/dist/validate.js +1 -0
- package/package.json +1 -1
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';
|
package/dist/middleware.js
CHANGED
|
@@ -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(
|
|
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 {
|
|
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(
|
|
37
|
-
updateCurrentVersion(
|
|
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
|
|
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 (
|
|
142
|
-
await this.migrate(
|
|
143
|
-
await this.updateCurrentVersion(
|
|
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 (
|
|
148
|
-
await this.migrate(
|
|
149
|
-
await this.updateCurrentVersion(
|
|
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 (
|
|
169
|
-
await this.migrate(
|
|
170
|
-
await this.updateCurrentVersion(
|
|
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 (
|
|
175
|
-
await this.migrate(
|
|
176
|
-
await this.updateCurrentVersion(
|
|
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(
|
|
201
|
+
async migrate(migrator, direction) {
|
|
201
202
|
try {
|
|
202
203
|
const queryWithLog = (statement) => {
|
|
203
204
|
this.log(chalk.yellow(formatSql(statement)));
|
|
204
|
-
return
|
|
205
|
+
return this.postgres.query(statement);
|
|
205
206
|
};
|
|
206
|
-
const returnValue = await migrator[direction](queryWithLog.bind(
|
|
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(
|
|
222
|
-
return
|
|
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`);
|
package/dist/postgres.d.ts
CHANGED
|
@@ -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
|
|
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: (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
return (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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