@devmoods/express-extras 0.52.3 → 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/auth/passwordless.d.ts +1 -1
- package/dist/auth/passwordless.js +20 -14
- package/dist/auth/types.d.ts +3 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +32 -4
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/errors.js +4 -0
- 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 +17 -33
- package/dist/postgres.js +71 -136
- 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 +14 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AnyUser, type AuthProvider } from './types.js';
|
|
2
2
|
export interface PasswordlessAuthProviderOptions<UserModel = AnyUser> {
|
|
3
3
|
getUserByEmail(email: string): Promise<UserModel | null>;
|
|
4
|
-
onLoginRequest(user: UserModel, temporaryToken: string): Promise<void>;
|
|
4
|
+
onLoginRequest(user: UserModel, temporaryToken: string, otp: string): Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
export declare function PasswordlessAuthProvider<UserModel extends AnyUser>(options: PasswordlessAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createTokenResponse } from './token.js';
|
|
2
2
|
import {} from './types.js';
|
|
3
|
-
import { Unauthorized } from '../errors.js';
|
|
4
3
|
import { getLogger } from '../logger.js';
|
|
5
4
|
import { route } from '../middleware.js';
|
|
6
5
|
import { DoesNotExistError } from '../postgres.js';
|
|
@@ -20,19 +19,25 @@ export function PasswordlessAuthProvider(options) {
|
|
|
20
19
|
};
|
|
21
20
|
return {
|
|
22
21
|
setupRoutes(router, context) {
|
|
23
|
-
router.post('/passwordless/login
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
router.post('/passwordless/login', validateBody({
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
type: { type: 'string', enum: ['otp', 'token'] },
|
|
26
|
+
otp: { type: 'string' },
|
|
27
|
+
token: { type: 'string' },
|
|
28
|
+
},
|
|
29
|
+
required: ['type'],
|
|
30
|
+
}), route(async (req, res) => {
|
|
31
|
+
const { token: temporaryToken, otp, type: tokenType } = req.body;
|
|
32
|
+
const verify = tokenType === 'otp'
|
|
33
|
+
? context.verifyOtp
|
|
34
|
+
: context.verifyTemporaryToken;
|
|
35
|
+
const tokenValue = tokenType === 'otp' ? otp : temporaryToken;
|
|
36
|
+
const uid = await verify(tokenValue, null, false);
|
|
37
|
+
const user = await context.getUserById(uid);
|
|
38
|
+
await verify(tokenValue, user);
|
|
34
39
|
const accessToken = await context.login(req, res, user);
|
|
35
|
-
logger.info('passwordless login', { uid: user.id });
|
|
40
|
+
logger.info('passwordless login', { tokenType, uid: user.id });
|
|
36
41
|
if (req.accepts('html')) {
|
|
37
42
|
return res.redirect('/');
|
|
38
43
|
}
|
|
@@ -55,7 +60,8 @@ export function PasswordlessAuthProvider(options) {
|
|
|
55
60
|
return 202;
|
|
56
61
|
}
|
|
57
62
|
const token = await context.createTemporaryToken(user);
|
|
58
|
-
await
|
|
63
|
+
const otp = await context.createOtp(user);
|
|
64
|
+
await options.onLoginRequest(user, token, otp);
|
|
59
65
|
logger.info('passwordless login requested', { email });
|
|
60
66
|
return 202;
|
|
61
67
|
}));
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -35,8 +35,10 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
35
35
|
tokenStorage: AuthOptions<UserModel>['tokenStorage'];
|
|
36
36
|
setCurrentUser: (user: UserModel) => void;
|
|
37
37
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
38
|
-
verifyTemporaryToken(token: string, user: UserModel): Promise<
|
|
38
|
+
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
39
39
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
40
|
+
createOtp(user: UserModel): Promise<string>;
|
|
41
|
+
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
40
42
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
41
43
|
login: (req: Request, res: Response, user: UserModel, isRefresh?: boolean) => Promise<AccessToken>;
|
|
42
44
|
getUserById(id: string): Promise<UserModel>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
12
12
|
before: () => RequestHandler;
|
|
13
13
|
isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
|
|
14
14
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
15
|
-
verifyTemporaryToken(token: string, user: UserModel): Promise<
|
|
15
|
+
verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
|
|
16
16
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
17
|
+
createOtp(user: UserModel): Promise<string>;
|
|
18
|
+
verifyOtp(otp: string, user: UserModel | null): Promise<string>;
|
|
17
19
|
logout: (req: Request, res: Response) => Promise<void>;
|
|
18
20
|
}
|
|
19
21
|
export declare class RedisTokenStorage extends TokenStorage {
|
package/dist/auth.js
CHANGED
|
@@ -71,6 +71,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
71
71
|
createTemporaryToken,
|
|
72
72
|
verifyTemporaryToken,
|
|
73
73
|
decodeTemporaryToken,
|
|
74
|
+
createOtp,
|
|
75
|
+
verifyOtp,
|
|
74
76
|
middleware,
|
|
75
77
|
login,
|
|
76
78
|
getUserById,
|
|
@@ -185,6 +187,24 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
185
187
|
}
|
|
186
188
|
};
|
|
187
189
|
}
|
|
190
|
+
async function createOtp(user) {
|
|
191
|
+
const otp = crypto.randomInt(100000, 999999).toString();
|
|
192
|
+
await tokenStorage.set(otp, String(user.id), config.value.AUTH_OTP_EXPIRES_IN);
|
|
193
|
+
return otp;
|
|
194
|
+
}
|
|
195
|
+
async function verifyOtp(otp, user, consume = true) {
|
|
196
|
+
const storedUid = await tokenStorage.get(otp);
|
|
197
|
+
if (!storedUid) {
|
|
198
|
+
throw new InvalidToken('Invalid OTP');
|
|
199
|
+
}
|
|
200
|
+
if (user && storedUid !== user.id) {
|
|
201
|
+
throw new InvalidToken('Invalid OTP');
|
|
202
|
+
}
|
|
203
|
+
if (consume) {
|
|
204
|
+
await tokenStorage.invalidate(otp);
|
|
205
|
+
}
|
|
206
|
+
return storedUid;
|
|
207
|
+
}
|
|
188
208
|
/**
|
|
189
209
|
* Create a short-lived token tied to a user that can be
|
|
190
210
|
* used for different kinds of verification.
|
|
@@ -213,7 +233,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
213
233
|
function decodeTemporaryToken(token) {
|
|
214
234
|
return JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'));
|
|
215
235
|
}
|
|
216
|
-
async function verifyTemporaryToken(token, user) {
|
|
236
|
+
async function verifyTemporaryToken(token, user, consume = true) {
|
|
217
237
|
const decoded = decodeTemporaryToken(token);
|
|
218
238
|
const expectedSignature = crypto
|
|
219
239
|
.createHmac('sha256', config.value.AUTH_SECRET)
|
|
@@ -223,14 +243,20 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
223
243
|
if (expectedSignature !== decoded.signature) {
|
|
224
244
|
throw new InvalidToken('Invalid signature');
|
|
225
245
|
}
|
|
226
|
-
if (decoded.payload.uid !== user.id) {
|
|
246
|
+
if (user && decoded.payload.uid !== user.id) {
|
|
227
247
|
throw new InvalidToken(`Invalid user ${user.id}`);
|
|
228
248
|
}
|
|
229
249
|
const storedUid = await tokenStorage.get(token);
|
|
230
|
-
if (storedUid
|
|
250
|
+
if (!storedUid) {
|
|
251
|
+
throw new InvalidToken('Invalid token not found');
|
|
252
|
+
}
|
|
253
|
+
if (user && storedUid !== user.id) {
|
|
231
254
|
throw new InvalidToken(`Invalid user ${user.id}`);
|
|
232
255
|
}
|
|
233
|
-
|
|
256
|
+
if (consume) {
|
|
257
|
+
await tokenStorage.invalidate(token);
|
|
258
|
+
}
|
|
259
|
+
return storedUid;
|
|
234
260
|
}
|
|
235
261
|
return {
|
|
236
262
|
before: () => middleware(false),
|
|
@@ -241,6 +267,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
241
267
|
createTemporaryToken,
|
|
242
268
|
verifyTemporaryToken,
|
|
243
269
|
decodeTemporaryToken,
|
|
270
|
+
createOtp,
|
|
271
|
+
verifyOtp,
|
|
244
272
|
logout,
|
|
245
273
|
};
|
|
246
274
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const config: {
|
|
|
5
5
|
PUBLIC_URL: string;
|
|
6
6
|
AUTH_TOKEN_EXPIRES_IN: number;
|
|
7
7
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
8
|
+
AUTH_OTP_EXPIRES_IN: number;
|
|
8
9
|
AUTH_SECRET: string;
|
|
9
10
|
AUTH_COOKIE_SAME_SITE: string;
|
|
10
11
|
REDIS_URL: string;
|
|
@@ -37,6 +38,7 @@ export declare function extendConfig<T extends Record<string, unknown>>(appConfi
|
|
|
37
38
|
PUBLIC_URL: string;
|
|
38
39
|
AUTH_TOKEN_EXPIRES_IN: number;
|
|
39
40
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
41
|
+
AUTH_OTP_EXPIRES_IN: number;
|
|
40
42
|
AUTH_SECRET: string;
|
|
41
43
|
AUTH_COOKIE_SAME_SITE: string;
|
|
42
44
|
REDIS_URL: string;
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ const baseConfig = {
|
|
|
8
8
|
PUBLIC_URL: readEnv('PUBLIC_URL', ''),
|
|
9
9
|
AUTH_TOKEN_EXPIRES_IN: readEnv.number('AUTH_TOKEN_EXPIRES', 3600 * 24 * 30), // 30 days
|
|
10
10
|
AUTH_TEMP_TOKEN_EXPIRES_IN: readEnv.number('AUTH_TEMP_TOKEN_EXPIRES', 3600),
|
|
11
|
+
AUTH_OTP_EXPIRES_IN: readEnv.number('AUTH_OTP_EXPIRES', 5 * 60),
|
|
11
12
|
AUTH_SECRET: readEnv('AUTH_JWT_SECRET', ''),
|
|
12
13
|
AUTH_COOKIE_SAME_SITE: readEnv('AUTH_COOKIE_SAME_SITE', 'strict'),
|
|
13
14
|
REDIS_URL: readEnv('REDIS_URL', ''),
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,7 @@ import { STATUS_CODES } from 'node:http';
|
|
|
2
2
|
import * as Sentry from '@sentry/node';
|
|
3
3
|
import { errors } from 'compose-middleware';
|
|
4
4
|
import {} from 'express';
|
|
5
|
+
import { InvalidToken } from './auth/errors.js';
|
|
5
6
|
import { getLogger } from './logger.js';
|
|
6
7
|
import { DoesNotExistError, IntegrityError } from './postgres.js';
|
|
7
8
|
export class ApiError extends Error {
|
|
@@ -48,6 +49,9 @@ function convertToAPIError(error) {
|
|
|
48
49
|
if (error instanceof DoesNotExistError) {
|
|
49
50
|
return NotFound(error.message);
|
|
50
51
|
}
|
|
52
|
+
if (error instanceof InvalidToken) {
|
|
53
|
+
return Unauthorized(error.message);
|
|
54
|
+
}
|
|
51
55
|
return error;
|
|
52
56
|
}
|
|
53
57
|
function _errorHandler({ onError }) {
|
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,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: (
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "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",
|
|
@@ -46,31 +46,31 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@devmoods/eslint-config": "^2.6.0",
|
|
48
48
|
"@types/koa-compose": "^3.2.8",
|
|
49
|
-
"@types/node": "22.13.
|
|
49
|
+
"@types/node": "22.13.5",
|
|
50
50
|
"@types/supertest": "^6.0.2",
|
|
51
51
|
"@vitejs/plugin-react": "^4.3.4",
|
|
52
|
-
"@vitest/coverage-v8": "^3.0.
|
|
53
|
-
"arktype": "^2.0
|
|
52
|
+
"@vitest/coverage-v8": "^3.0.7",
|
|
53
|
+
"arktype": "^2.1.0",
|
|
54
54
|
"cookiejar": "^2.1.4",
|
|
55
|
-
"eslint": "^9.
|
|
55
|
+
"eslint": "^9.21.0",
|
|
56
56
|
"faktory-worker": "^4.6.0",
|
|
57
|
-
"prettier": "3.5.
|
|
57
|
+
"prettier": "3.5.2",
|
|
58
58
|
"supertest": "7.0.0",
|
|
59
59
|
"ts-node": "10.9.2",
|
|
60
|
-
"tsx": "^4.19.
|
|
60
|
+
"tsx": "^4.19.3",
|
|
61
61
|
"typescript": "5.7.3",
|
|
62
|
-
"vite": "^6.1.
|
|
63
|
-
"vitest": "^3.0.
|
|
62
|
+
"vite": "^6.1.1",
|
|
63
|
+
"vitest": "^3.0.7",
|
|
64
64
|
"vitest-fail-on-console": "^0.7.1"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@devmoods/fetch": "^4.0.1",
|
|
68
|
-
"@sentry/node": "^9.
|
|
69
|
-
"@sentry/profiling-node": "^9.
|
|
68
|
+
"@sentry/node": "^9.2.0",
|
|
69
|
+
"@sentry/profiling-node": "^9.2.0",
|
|
70
70
|
"@types/bcrypt": "^5.0.2",
|
|
71
71
|
"@types/cookie-parser": "^1.4.8",
|
|
72
72
|
"@types/express": "^5.0.0",
|
|
73
|
-
"@types/jsonwebtoken": "^9.0.
|
|
73
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
74
74
|
"@types/nodemailer": "^6.4.17",
|
|
75
75
|
"@types/pg": "^8.11.11",
|
|
76
76
|
"@types/superagent": "^8.1.9",
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"pg": "^8.13.3",
|
|
91
91
|
"redis": "^4.7.0",
|
|
92
92
|
"sonda": "^0.7.1",
|
|
93
|
-
"sql-formatter": "^15.4.
|
|
94
|
-
"uuid": "^11.0
|
|
93
|
+
"sql-formatter": "^15.4.11",
|
|
94
|
+
"uuid": "^11.1.0"
|
|
95
95
|
},
|
|
96
96
|
"prettier": {
|
|
97
97
|
"singleQuote": true
|