cloudflare-next-intl 0.8.29 → 0.8.31
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/README.md +5 -2
- package/dist/src/db/context.js +131 -5
- package/dist/src/db/db_performance.bench.d.ts +1 -0
- package/dist/src/db/db_performance.bench.js +116 -0
- package/dist/src/db/encode_param.js +11 -1
- package/dist/src/db/helpers.js +1 -1
- package/dist/src/db/inline_params.js +32 -24
- package/dist/src/db/parse_composite.js +32 -14
- package/dist/src/db/parse_statement.d.ts +0 -13
- package/dist/src/db/parse_statement.js +19 -8
- package/dist/src/db/sql_tokens.js +155 -37
- package/dist/src/firebase_auth/client/auth_actions.js +4 -1
- package/dist/src/types/types.d.ts +14 -3
- package/llms.txt +3 -2
- package/package.json +1 -1
- package/dist/src/db/supabase_rest.d.ts +0 -178
- package/dist/src/db/supabase_rest.js +0 -262
package/README.md
CHANGED
|
@@ -467,8 +467,11 @@ export default setIntlConfig({
|
|
|
467
467
|
server-side connection.
|
|
468
468
|
- `authenticatedRole` — Postgres role `withUserDb` switches its
|
|
469
469
|
call-scoped session to for the duration of your callback (`set role`, no
|
|
470
|
-
transaction involved; the session is closed when the call ends).
|
|
471
|
-
to `'authenticated'` (the Supabase RLS convention).
|
|
470
|
+
transaction involved; the session is closed when the call ends). May be a string or
|
|
471
|
+
a sync/async function returning a string. Defaults to `'authenticated'` (the Supabase RLS convention).
|
|
472
|
+
- `authenticatedRoleClaim` — Firebase custom claims field (default `'role'`) read
|
|
473
|
+
to determine the Postgres role inside `withUserDb` when `firebaseAuth` is enabled.
|
|
474
|
+
Set `false` to disable custom claim inspection and rely on `authenticatedRole`.
|
|
472
475
|
- `getUserId` — resolves the user id injected as
|
|
473
476
|
`request.jwt.claims->>'sub'` inside `withUserDb`. Omit when
|
|
474
477
|
`firebaseAuth` is configured — the uid then comes from this package's own
|
package/dist/src/db/context.js
CHANGED
|
@@ -31,6 +31,30 @@ async function resolveUserId(uid) {
|
|
|
31
31
|
throw new Error('db: withUserDb could not resolve a user id. Pass one explicitly, set ' +
|
|
32
32
|
'`db.getUserId`, or configure `firebaseAuth` so the signed-in Firebase uid is used.');
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the Postgres role for `withUserDb`'s session. When `firebaseAuth`
|
|
36
|
+
* is configured and `db.authenticatedRoleClaim` isn't `false`, the signed-in
|
|
37
|
+
* user's Firebase ID token claim (default field `'role'`) wins when present;
|
|
38
|
+
* otherwise falls back to `db.authenticatedRole` (string or sync/async
|
|
39
|
+
* function), then `DEFAULT_ROLE`.
|
|
40
|
+
*/
|
|
41
|
+
async function resolveAuthenticatedRole(db) {
|
|
42
|
+
const claimField = db.authenticatedRoleClaim;
|
|
43
|
+
if (config.firebaseAuth && claimField !== false) {
|
|
44
|
+
const { getAuthUser } = await import('../firebase_auth/server/use_auth_user_server');
|
|
45
|
+
const { user } = await getAuthUser();
|
|
46
|
+
if (user && typeof user.getIdTokenResult === 'function') {
|
|
47
|
+
const { claims } = await user.getIdTokenResult();
|
|
48
|
+
const claimValue = claims[claimField ?? 'role'];
|
|
49
|
+
if (typeof claimValue === 'string' && claimValue)
|
|
50
|
+
return claimValue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (db.authenticatedRole) {
|
|
54
|
+
return typeof db.authenticatedRole === 'function' ? await db.authenticatedRole() : db.authenticatedRole;
|
|
55
|
+
}
|
|
56
|
+
return DEFAULT_ROLE;
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* Builds a Drizzle handle backed by PostgREST. `bearerToken` decides the role
|
|
36
60
|
* Postgres sees: the anon key for public access, a user JWT for `withUserDb`.
|
|
@@ -134,6 +158,21 @@ export async function withPublicDb(fn) {
|
|
|
134
158
|
return await fn(await postgresDb(drizzleHandle, client));
|
|
135
159
|
});
|
|
136
160
|
}
|
|
161
|
+
function injectUidComment(sql, userId) {
|
|
162
|
+
if (typeof sql === 'string') {
|
|
163
|
+
if (/^(select|with)\b/i.test(sql.trimStart())) {
|
|
164
|
+
return `/* uid:${userId} */ ${sql}`;
|
|
165
|
+
}
|
|
166
|
+
return sql;
|
|
167
|
+
}
|
|
168
|
+
if (sql && typeof sql === 'object' && typeof sql.text === 'string') {
|
|
169
|
+
const obj = sql;
|
|
170
|
+
if (/^(select|with)\b/i.test(obj.text.trimStart())) {
|
|
171
|
+
return { ...obj, text: `/* uid:${userId} */ ${obj.text}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return sql;
|
|
175
|
+
}
|
|
137
176
|
/**
|
|
138
177
|
* Runs a query as the **signed-in user**, with `request.jwt.claims` and the
|
|
139
178
|
* authenticated role set on the session so RLS policies apply to their id.
|
|
@@ -153,14 +192,101 @@ export async function withUserDb(fn, uid) {
|
|
|
153
192
|
return fn(await supabaseDb(resolved.supabase, token));
|
|
154
193
|
}
|
|
155
194
|
const userId = await resolveUserId(uid);
|
|
156
|
-
const role = db
|
|
195
|
+
const role = await resolveAuthenticatedRole(db);
|
|
157
196
|
return await withDbClient(config, async (client) => {
|
|
158
197
|
const rawClient = client;
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
const setSessionState = async () => {
|
|
199
|
+
await rawClient.query(`select set_config('request.jwt.claims', $1, false)`, [JSON.stringify({ sub: userId })]);
|
|
200
|
+
await rawClient.query(`set role "${role.replace(/"/g, '""')}"`);
|
|
201
|
+
};
|
|
202
|
+
const isSelectOnly = (sql) => {
|
|
203
|
+
const text = typeof sql === 'string' ? sql : sql?.text;
|
|
204
|
+
return typeof text === 'string' && /^(select|with)\b/i.test(text.trimStart());
|
|
205
|
+
};
|
|
206
|
+
let inTransaction = false;
|
|
207
|
+
let sessionStateSet = false;
|
|
208
|
+
let gate = Promise.resolve();
|
|
209
|
+
const serialize = (op) => {
|
|
210
|
+
const run = gate.then(op, op);
|
|
211
|
+
gate = run.catch(() => undefined);
|
|
212
|
+
return run;
|
|
213
|
+
};
|
|
214
|
+
const interceptingClient = new Proxy(client, {
|
|
215
|
+
get(target, prop) {
|
|
216
|
+
if (prop === 'query') {
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
218
|
+
return async (sql, ...args) => {
|
|
219
|
+
const text = typeof sql === 'string' ? sql : (typeof sql?.text === 'string' ? sql.text : '');
|
|
220
|
+
const isBegin = /^begin\b/i.test(text.trimStart());
|
|
221
|
+
const isCommitOrRollback = /^(commit|rollback)\b/i.test(text.trimStart());
|
|
222
|
+
if (isBegin) {
|
|
223
|
+
return await serialize(async () => {
|
|
224
|
+
if (!sessionStateSet) {
|
|
225
|
+
await rawClient.query('begin');
|
|
226
|
+
inTransaction = true;
|
|
227
|
+
await setSessionState();
|
|
228
|
+
sessionStateSet = true;
|
|
229
|
+
}
|
|
230
|
+
else if (!inTransaction) {
|
|
231
|
+
await rawClient.query('begin');
|
|
232
|
+
inTransaction = true;
|
|
233
|
+
}
|
|
234
|
+
return { rows: [], rowCount: 0 };
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (isCommitOrRollback) {
|
|
238
|
+
return await serialize(async () => {
|
|
239
|
+
if (inTransaction) {
|
|
240
|
+
const res = await rawClient.query(text);
|
|
241
|
+
inTransaction = false;
|
|
242
|
+
return res;
|
|
243
|
+
}
|
|
244
|
+
return { rows: [], rowCount: 0 };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
await serialize(async () => {
|
|
248
|
+
if (!sessionStateSet) {
|
|
249
|
+
if (isSelectOnly(sql)) {
|
|
250
|
+
await rawClient.query('begin');
|
|
251
|
+
inTransaction = true;
|
|
252
|
+
await setSessionState();
|
|
253
|
+
await rawClient.query('commit');
|
|
254
|
+
inTransaction = false;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await rawClient.query('begin');
|
|
258
|
+
inTransaction = true;
|
|
259
|
+
await setSessionState();
|
|
260
|
+
}
|
|
261
|
+
sessionStateSet = true;
|
|
262
|
+
}
|
|
263
|
+
else if (!inTransaction && !isSelectOnly(sql)) {
|
|
264
|
+
await rawClient.query('begin');
|
|
265
|
+
inTransaction = true;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
269
|
+
return target.query(injectUidComment(sql, userId), ...args);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
+
const value = target[prop];
|
|
274
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
161
277
|
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
162
|
-
const drizzleHandle = drizzle(
|
|
163
|
-
|
|
278
|
+
const drizzleHandle = drizzle(interceptingClient);
|
|
279
|
+
try {
|
|
280
|
+
const result = await fn(await postgresDb(drizzleHandle, interceptingClient));
|
|
281
|
+
if (inTransaction)
|
|
282
|
+
await rawClient.query('commit');
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
if (inTransaction)
|
|
287
|
+
await rawClient.query('rollback').catch(() => undefined);
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
164
290
|
});
|
|
165
291
|
}
|
|
166
292
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import tokenizeSql from './sql_tokens';
|
|
3
|
+
import parseStatement from './parse_statement';
|
|
4
|
+
import inlineParams from './inline_params';
|
|
5
|
+
import encodeParam from './encode_param';
|
|
6
|
+
import parseWhere from './parse_where';
|
|
7
|
+
import parseComposite from './parse_composite';
|
|
8
|
+
import buildRestFilters from './rest_filters';
|
|
9
|
+
import resolveRawSql from './resolve_raw_sql';
|
|
10
|
+
import { parseExecResult } from './supabase_transport';
|
|
11
|
+
import { excluded, onConflictSet } from './helpers';
|
|
12
|
+
import { pgTable, text, integer, timestamp } from 'drizzle-orm/pg-core';
|
|
13
|
+
describe('DB Module Branch Benchmarks', () => {
|
|
14
|
+
const complexSql = 'SELECT id, name, email, created_at FROM users WHERE status = $1 AND age >= $2 AND role IN ($3, $4) ORDER BY created_at DESC LIMIT $5 OFFSET $6';
|
|
15
|
+
const insertSql = 'INSERT INTO users ("name", "email", "age", "status") VALUES ($1, $2, $3, $4), ($5, $6, $7, $8) ON CONFLICT ("email") DO UPDATE SET "name" = excluded."name", "status" = $9 RETURNING "id", "name"';
|
|
16
|
+
const updateSql = 'UPDATE users SET name = $1, status = $2 WHERE id = $3 AND active = $4 RETURNING id';
|
|
17
|
+
const deleteSql = 'DELETE FROM users WHERE tenant_id = $1 AND status = $2 AND created_at < $3 RETURNING id';
|
|
18
|
+
const sampleParams = ['active', 21, 'admin', 'user', 50, 10, 'John Doe', 'john@example.com', 30, 'pending'];
|
|
19
|
+
// Schema for helpers benchmarks
|
|
20
|
+
const usersTable = pgTable('users', {
|
|
21
|
+
id: integer('id').primaryKey(),
|
|
22
|
+
name: text('name').notNull(),
|
|
23
|
+
email: text('email').notNull(),
|
|
24
|
+
updatedAt: timestamp('updated_at'),
|
|
25
|
+
});
|
|
26
|
+
const mockTarget = {
|
|
27
|
+
eq: () => mockTarget,
|
|
28
|
+
gte: () => mockTarget,
|
|
29
|
+
in: () => mockTarget,
|
|
30
|
+
or: () => mockTarget,
|
|
31
|
+
};
|
|
32
|
+
const sampleWhereTree = {
|
|
33
|
+
kind: 'and',
|
|
34
|
+
children: [
|
|
35
|
+
{ kind: 'compare', column: 'status', operator: 'eq', value: { kind: 'param', index: 1 } },
|
|
36
|
+
{ kind: 'compare', column: 'age', operator: 'gte', value: { kind: 'param', index: 2 } },
|
|
37
|
+
{ kind: 'in', column: 'role', values: [{ kind: 'param', index: 3 }, { kind: 'param', index: 4 }], negated: false }
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
// 1. Tokenizer benchmarks
|
|
41
|
+
describe('tokenizeSql', () => {
|
|
42
|
+
bench('Select statement', () => { tokenizeSql(complexSql); });
|
|
43
|
+
bench('Insert statement', () => { tokenizeSql(insertSql); });
|
|
44
|
+
bench('Update statement', () => { tokenizeSql(updateSql); });
|
|
45
|
+
bench('Delete statement', () => { tokenizeSql(deleteSql); });
|
|
46
|
+
});
|
|
47
|
+
// 2. Parser benchmarks
|
|
48
|
+
describe('parseStatement', () => {
|
|
49
|
+
bench('Select statement', () => { parseStatement(complexSql); });
|
|
50
|
+
bench('Insert statement', () => { parseStatement(insertSql); });
|
|
51
|
+
bench('Update statement', () => { parseStatement(updateSql); });
|
|
52
|
+
bench('Delete statement', () => { parseStatement(deleteSql); });
|
|
53
|
+
});
|
|
54
|
+
// 3. Where Clause parser
|
|
55
|
+
describe('parseWhere', () => {
|
|
56
|
+
const tokens = tokenizeSql(complexSql);
|
|
57
|
+
// "WHERE status = $1 AND age >= $2 AND role IN ($3, $4)" starts after WHERE token (index 8)
|
|
58
|
+
bench('Where clause parsing', () => {
|
|
59
|
+
parseWhere(tokens, 9);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// 4. Parameter inlining
|
|
63
|
+
describe('inlineParams', () => {
|
|
64
|
+
bench('Select with 6 params', () => { inlineParams(complexSql, sampleParams); });
|
|
65
|
+
bench('Insert with 9 params', () => { inlineParams(insertSql, sampleParams); });
|
|
66
|
+
});
|
|
67
|
+
// 5. Parameter encoding
|
|
68
|
+
describe('encodeParam', () => {
|
|
69
|
+
bench('String encoding', () => { encodeParam('test string with \'quotes\' and \\ backslashes'); });
|
|
70
|
+
bench('Number encoding', () => { encodeParam(12345.678); });
|
|
71
|
+
bench('Boolean encoding', () => { encodeParam(true); });
|
|
72
|
+
bench('Date encoding', () => { encodeParam(new Date('2026-01-01T12:00:00Z')); });
|
|
73
|
+
bench('JSON Object encoding', () => { encodeParam({ a: 1, b: 'hello', c: [true, false] }); });
|
|
74
|
+
bench('Array encoding', () => { encodeParam(['foo', 'bar', 'baz']); });
|
|
75
|
+
bench('Uint8Array encoding', () => { encodeParam(new Uint8Array([1, 2, 3, 4, 255])); });
|
|
76
|
+
});
|
|
77
|
+
// 6. Composite type parsing
|
|
78
|
+
describe('parseComposite', () => {
|
|
79
|
+
bench('Simple composite row', () => { parseComposite('(123,"hello",t,"2026-01-01")'); });
|
|
80
|
+
bench('Escaped quoted composite row', () => { parseComposite('(123,"hello ""world""",t,"2026-01-01")'); });
|
|
81
|
+
});
|
|
82
|
+
// 7. REST Filters builder
|
|
83
|
+
describe('buildRestFilters', () => {
|
|
84
|
+
bench('Where tree translation', () => {
|
|
85
|
+
buildRestFilters(mockTarget, sampleWhereTree, sampleParams);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
// 8. Result Decoder
|
|
89
|
+
describe('parseExecResult', () => {
|
|
90
|
+
const sampleResult = [
|
|
91
|
+
'(1,"John Doe","john@example.com","2026-01-01")',
|
|
92
|
+
'(2,"Jane Doe","jane@example.com","2026-01-02")',
|
|
93
|
+
'(3,"Bob Smith","bob@example.com","2026-01-03")',
|
|
94
|
+
];
|
|
95
|
+
bench('Parse RPC array result', () => {
|
|
96
|
+
parseExecResult(sampleResult);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// 9. Drizzle Helpers
|
|
100
|
+
describe('helpers', () => {
|
|
101
|
+
const exUsers = excluded(usersTable);
|
|
102
|
+
bench('excluded column lookup', () => {
|
|
103
|
+
const _a = exUsers.name;
|
|
104
|
+
const _b = exUsers.email;
|
|
105
|
+
});
|
|
106
|
+
bench('onConflictSet generation', () => {
|
|
107
|
+
onConflictSet(usersTable, ['name', 'email']);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// 10. Raw SQL Resolution
|
|
111
|
+
describe('resolveRawSql', () => {
|
|
112
|
+
bench('Next.config lookup', () => {
|
|
113
|
+
resolveRawSql('/Volumes/External/own_projects/cloudflare-next-intl');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -34,11 +34,19 @@ export default function encodeParam(value) {
|
|
|
34
34
|
// Plain objects (jsonb columns) — pg sends these JSON-stringified.
|
|
35
35
|
return quoteLiteral(JSON.stringify(value));
|
|
36
36
|
}
|
|
37
|
+
const HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
|
|
37
38
|
function quoteLiteral(text) {
|
|
39
|
+
if (text.indexOf("'") === -1)
|
|
40
|
+
return `'${text}'`;
|
|
38
41
|
return `'${text.replace(/'/g, "''")}'`;
|
|
39
42
|
}
|
|
40
43
|
function bytesToHex(bytes) {
|
|
41
|
-
|
|
44
|
+
let hex = '';
|
|
45
|
+
const len = bytes.length;
|
|
46
|
+
for (let i = 0; i < len; i++) {
|
|
47
|
+
hex += HEX_TABLE[bytes[i]];
|
|
48
|
+
}
|
|
49
|
+
return hex;
|
|
42
50
|
}
|
|
43
51
|
/** Encodes a JS array as a Postgres array literal body, e.g. `{1,2,"a,b"}`. */
|
|
44
52
|
function encodeArray(value) {
|
|
@@ -56,5 +64,7 @@ function encodeArray(value) {
|
|
|
56
64
|
return `{${items.join(',')}}`;
|
|
57
65
|
}
|
|
58
66
|
function quoteArrayElement(text) {
|
|
67
|
+
if (text.indexOf('\\') === -1 && text.indexOf('"') === -1)
|
|
68
|
+
return `"${text}"`;
|
|
59
69
|
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
60
70
|
}
|
package/dist/src/db/helpers.js
CHANGED
|
@@ -23,7 +23,7 @@ export function excluded(table) {
|
|
|
23
23
|
const cols = getTableColumns(table);
|
|
24
24
|
const tableName = getTableName(table);
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
return new Proxy(
|
|
26
|
+
return new Proxy(cols, {
|
|
27
27
|
get(_, prop) {
|
|
28
28
|
const col = cols[prop];
|
|
29
29
|
if (!col) {
|
|
@@ -19,57 +19,65 @@ import encodeParam from './encode_param';
|
|
|
19
19
|
export default function inlineParams(statement, params) {
|
|
20
20
|
let result = '';
|
|
21
21
|
let i = 0;
|
|
22
|
+
let lastIndex = 0;
|
|
22
23
|
const len = statement.length;
|
|
23
24
|
while (i < len) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
25
|
+
const code = statement.charCodeAt(i);
|
|
26
|
+
if (code === 45 && statement.charCodeAt(i + 1) === 45) { // --
|
|
26
27
|
const end = statement.indexOf('\n', i);
|
|
27
28
|
const stop = end === -1 ? len : end + 1;
|
|
28
|
-
result += statement.slice(i, stop);
|
|
29
29
|
i = stop;
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
32
|
+
if (code === 47 && statement.charCodeAt(i + 1) === 42) { // /*
|
|
33
33
|
const end = statement.indexOf('*/', i + 2);
|
|
34
34
|
const stop = end === -1 ? len : end + 2;
|
|
35
|
-
result += statement.slice(i, stop);
|
|
36
35
|
i = stop;
|
|
37
36
|
continue;
|
|
38
37
|
}
|
|
39
|
-
if (
|
|
40
|
-
const start =
|
|
38
|
+
if (code === 39 || (code === 69 && statement.charCodeAt(i + 1) === 39)) { // ' or E'
|
|
39
|
+
const start = code === 69 ? i + 1 : i;
|
|
41
40
|
const end = findStringEnd(statement, start + 1);
|
|
42
|
-
result += statement.slice(i, end);
|
|
43
41
|
i = end;
|
|
44
42
|
continue;
|
|
45
43
|
}
|
|
46
|
-
if (
|
|
44
|
+
if (code === 34) { // "
|
|
47
45
|
const end = findQuotedIdentifierEnd(statement, i + 1);
|
|
48
|
-
result += statement.slice(i, end);
|
|
49
46
|
i = end;
|
|
50
47
|
continue;
|
|
51
48
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
j
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
if (code === 36) { // $
|
|
50
|
+
const nextCode = statement.charCodeAt(i + 1);
|
|
51
|
+
if (nextCode >= 48 && nextCode <= 57) {
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
while (j < len) {
|
|
54
|
+
const c = statement.charCodeAt(j);
|
|
55
|
+
if (c >= 48 && c <= 57)
|
|
56
|
+
j++;
|
|
57
|
+
else
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
const index = Number(statement.slice(i + 1, j));
|
|
61
|
+
if (index < 1 || index > params.length) {
|
|
62
|
+
throw new Error(`db: statement references $${index} but only ${params.length} param(s) were provided.`);
|
|
63
|
+
}
|
|
64
|
+
if (i > lastIndex)
|
|
65
|
+
result += statement.slice(lastIndex, i);
|
|
66
|
+
result += encodeParam(params[index - 1]);
|
|
67
|
+
i = j;
|
|
68
|
+
lastIndex = j;
|
|
69
|
+
continue;
|
|
59
70
|
}
|
|
60
|
-
result += encodeParam(params[index - 1]);
|
|
61
|
-
i = j;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (ch === '$') {
|
|
65
71
|
const tagEnd = findDollarQuoteEnd(statement, i);
|
|
66
|
-
result += statement.slice(i, tagEnd);
|
|
67
72
|
i = tagEnd;
|
|
68
73
|
continue;
|
|
69
74
|
}
|
|
70
|
-
result += ch;
|
|
71
75
|
i++;
|
|
72
76
|
}
|
|
77
|
+
if (lastIndex === 0)
|
|
78
|
+
return statement;
|
|
79
|
+
if (lastIndex < len)
|
|
80
|
+
result += statement.slice(lastIndex);
|
|
73
81
|
return result;
|
|
74
82
|
}
|
|
75
83
|
function findStringEnd(statement, from) {
|
|
@@ -24,33 +24,51 @@ export default function parseComposite(literal) {
|
|
|
24
24
|
if (len === i)
|
|
25
25
|
return fields;
|
|
26
26
|
while (true) {
|
|
27
|
-
if (literal
|
|
28
|
-
let value = '';
|
|
27
|
+
if (literal.charCodeAt(i) === 34) { // '"'
|
|
29
28
|
i++;
|
|
29
|
+
const start = i;
|
|
30
|
+
let hasEscaped = false;
|
|
30
31
|
while (i < len) {
|
|
31
|
-
if (literal
|
|
32
|
-
if (literal
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
continue;
|
|
32
|
+
if (literal.charCodeAt(i) === 34) {
|
|
33
|
+
if (literal.charCodeAt(i + 1) === 34) {
|
|
34
|
+
hasEscaped = true;
|
|
35
|
+
break;
|
|
36
36
|
}
|
|
37
37
|
break;
|
|
38
38
|
}
|
|
39
|
-
value += literal[i];
|
|
40
39
|
i++;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (!hasEscaped) {
|
|
42
|
+
fields.push(literal.slice(start, i));
|
|
43
|
+
i++; // skip closing quote
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
let value = literal.slice(start, i);
|
|
47
|
+
while (i < len) {
|
|
48
|
+
if (literal.charCodeAt(i) === 34) {
|
|
49
|
+
if (literal.charCodeAt(i + 1) === 34) {
|
|
50
|
+
value += '"';
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
value += literal[i];
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
i++; // skip closing quote
|
|
60
|
+
fields.push(value);
|
|
61
|
+
}
|
|
44
62
|
}
|
|
45
63
|
else {
|
|
46
|
-
|
|
47
|
-
while (i < len && literal
|
|
48
|
-
value += literal[i];
|
|
64
|
+
const start = i;
|
|
65
|
+
while (i < len && literal.charCodeAt(i) !== 44) {
|
|
49
66
|
i++;
|
|
50
67
|
}
|
|
68
|
+
const value = literal.slice(start, i);
|
|
51
69
|
fields.push(value === '' ? null : value);
|
|
52
70
|
}
|
|
53
|
-
if (literal
|
|
71
|
+
if (literal.charCodeAt(i) === 44) { // ','
|
|
54
72
|
i++;
|
|
55
73
|
continue;
|
|
56
74
|
}
|
|
@@ -60,17 +60,4 @@ export interface ParsedDelete {
|
|
|
60
60
|
}
|
|
61
61
|
/** Any statement the REST executor knows how to run. */
|
|
62
62
|
export type ParsedStatement = ParsedSelect | ParsedInsert | ParsedUpdate | ParsedDelete;
|
|
63
|
-
/**
|
|
64
|
-
* Parses a generated statement into the smallest description the REST
|
|
65
|
-
* executor needs, rejecting anything PostgREST's single-table API cannot do.
|
|
66
|
-
*
|
|
67
|
-
* The parser is deliberately strict: a statement it does not fully understand
|
|
68
|
-
* must raise rather than translate approximately, because the transport reads
|
|
69
|
-
* a raise as "send this to `cfni_exec` instead" and a wrong translation would
|
|
70
|
-
* silently return wrong rows.
|
|
71
|
-
*
|
|
72
|
-
* @param sql The generated statement text, `$n` placeholders included.
|
|
73
|
-
* @returns The parsed statement.
|
|
74
|
-
* @throws {UnsupportedSqlError} If the statement is outside the supported subset.
|
|
75
|
-
*/
|
|
76
63
|
export default function parseStatement(sql: string): ParsedStatement;
|
|
@@ -14,20 +14,31 @@ import UnsupportedSqlError from './unsupported_sql';
|
|
|
14
14
|
* @returns The parsed statement.
|
|
15
15
|
* @throws {UnsupportedSqlError} If the statement is outside the supported subset.
|
|
16
16
|
*/
|
|
17
|
+
const STATEMENT_CACHE = new Map();
|
|
18
|
+
const MAX_STATEMENT_CACHE = 500;
|
|
17
19
|
export default function parseStatement(sql) {
|
|
20
|
+
const cached = STATEMENT_CACHE.get(sql);
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
18
23
|
const tokens = tokenizeSql(sql);
|
|
19
24
|
const first = tokens[0];
|
|
20
25
|
if (!first || first.kind !== 'word')
|
|
21
26
|
throw new UnsupportedSqlError('empty statement');
|
|
27
|
+
let parsed;
|
|
22
28
|
if (first.value === 'select')
|
|
23
|
-
|
|
24
|
-
if (first.value === 'insert')
|
|
25
|
-
|
|
26
|
-
if (first.value === 'update')
|
|
27
|
-
|
|
28
|
-
if (first.value === 'delete')
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
parsed = parseSelect(tokens);
|
|
30
|
+
else if (first.value === 'insert')
|
|
31
|
+
parsed = parseInsert(tokens);
|
|
32
|
+
else if (first.value === 'update')
|
|
33
|
+
parsed = parseUpdate(tokens);
|
|
34
|
+
else if (first.value === 'delete')
|
|
35
|
+
parsed = parseDelete(tokens);
|
|
36
|
+
else
|
|
37
|
+
throw new UnsupportedSqlError(`statement type "${first.value}"`);
|
|
38
|
+
if (STATEMENT_CACHE.size >= MAX_STATEMENT_CACHE)
|
|
39
|
+
STATEMENT_CACHE.clear();
|
|
40
|
+
STATEMENT_CACHE.set(sql, parsed);
|
|
41
|
+
return parsed;
|
|
31
42
|
}
|
|
32
43
|
function parseSelect(tokens) {
|
|
33
44
|
let index = 1;
|