prostgles-server 4.2.646 → 4.2.647

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.
Files changed (66) hide show
  1. package/debug/after-commit-fix-build.log +4 -0
  2. package/debug/after-commit-fix-client-build.log +0 -0
  3. package/debug/after-commit-fix-client.log +674 -0
  4. package/debug/after-commit-fix-lint.log +4 -0
  5. package/debug/after-commit-fix-server.log +542 -0
  6. package/debug/after-commit-fix-test-build.log +0 -0
  7. package/debug/after-commit-fix-typecheck.log +8 -0
  8. package/debug/after-commit-review-build.log +4 -0
  9. package/debug/after-commit-review-regressions.log +604 -0
  10. package/debug/after-commit-review-reproductions.patch +62 -0
  11. package/debug/after-commit-review-restored-build.log +0 -0
  12. package/debug/after-commit-review-server.log +3249 -0
  13. package/debug/after-commit-review-test-build.log +0 -0
  14. package/debug/after-commit-review-typecheck.log +8 -0
  15. package/debug/after-commit-review-withUserRLS.original.ts +554 -0
  16. package/debug/after-commit-shared-query-build.log +4 -0
  17. package/debug/after-commit-shared-query-lint.log +4 -0
  18. package/debug/after-commit-shared-query-server.log +3253 -0
  19. package/dist/DboBuilder/TableHandler/TableHandler.d.ts +13 -0
  20. package/dist/DboBuilder/TableHandler/TableHandler.d.ts.map +1 -1
  21. package/dist/DboBuilder/TableHandler/TableHandler.js +13 -0
  22. package/dist/DboBuilder/TableHandler/TableHandler.js.map +1 -1
  23. package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.d.ts.map +1 -1
  24. package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.js +12 -0
  25. package/dist/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.js.map +1 -1
  26. package/dist/DboBuilder/TableHandler/isApplicableHook.js +1 -1
  27. package/dist/DboBuilder/TableHandler/isApplicableHook.js.map +1 -1
  28. package/dist/DboBuilder/TableHandler/updateBatch.d.ts.map +1 -1
  29. package/dist/DboBuilder/TableHandler/updateBatch.js +18 -0
  30. package/dist/DboBuilder/TableHandler/updateBatch.js.map +1 -1
  31. package/dist/DboBuilder/ViewHandler/getReturnTypeQuery.d.ts +1 -0
  32. package/dist/DboBuilder/ViewHandler/getReturnTypeQuery.d.ts.map +1 -1
  33. package/dist/DboBuilder/ViewHandler/getReturnTypeQuery.js +6 -4
  34. package/dist/DboBuilder/ViewHandler/getReturnTypeQuery.js.map +1 -1
  35. package/dist/Prostgles.d.ts +2 -0
  36. package/dist/Prostgles.d.ts.map +1 -1
  37. package/dist/Prostgles.js +2 -0
  38. package/dist/Prostgles.js.map +1 -1
  39. package/dist/PublishParser/publishTypesAndUtils.d.ts +15 -1
  40. package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
  41. package/dist/PublishParser/publishTypesAndUtils.js.map +1 -1
  42. package/dist/TableHooks/TableHooks.d.ts +12 -4
  43. package/dist/TableHooks/TableHooks.d.ts.map +1 -1
  44. package/dist/WebsocketAPI/getClientHandlers.d.ts +9 -0
  45. package/dist/WebsocketAPI/getClientHandlers.d.ts.map +1 -1
  46. package/dist/WebsocketAPI/getClientHandlers.js +12 -1
  47. package/dist/WebsocketAPI/getClientHandlers.js.map +1 -1
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/initProstgles.d.ts +3 -11
  52. package/dist/initProstgles.d.ts.map +1 -1
  53. package/dist/initProstgles.js +1 -11
  54. package/dist/initProstgles.js.map +1 -1
  55. package/lib/DboBuilder/TableHandler/TableHandler.ts +13 -0
  56. package/lib/DboBuilder/TableHandler/executeAfterHooksCheckAndPostValidation.ts +13 -0
  57. package/lib/DboBuilder/TableHandler/isApplicableHook.ts +1 -1
  58. package/lib/DboBuilder/TableHandler/updateBatch.ts +28 -1
  59. package/lib/DboBuilder/ViewHandler/getReturnTypeQuery.ts +8 -3
  60. package/lib/Prostgles.ts +7 -0
  61. package/lib/PublishParser/publishTypesAndUtils.ts +16 -1
  62. package/lib/TableHooks/TableHooks.ts +14 -2
  63. package/lib/WebsocketAPI/getClientHandlers.ts +37 -0
  64. package/lib/index.ts +5 -0
  65. package/lib/initProstgles.ts +7 -30
  66. package/package.json +1 -1
File without changes
@@ -0,0 +1,8 @@
1
+
2
+ > prostgles-server@4.2.646 typecheck
3
+ > tsc --noEmit && npm run typecheck:tests
4
+
5
+
6
+ > prostgles-server@4.2.646 typecheck:tests
7
+ > tsc --noEmit -p tests/server && tsc --noEmit -p tests/client
8
+
@@ -0,0 +1,554 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { once } from "node:events";
3
+ import { createServer } from "node:http";
4
+ import { test } from "node:test";
5
+ import express from "express";
6
+ import pgPromise from "pg-promise";
7
+ import prostgles, { type ProstglesInitOptions } from "prostgles-server";
8
+ import { fetchSyncServerData } from "prostgles-server/dist/PubSubManager/SyncReplication/fetchSyncServerData";
9
+ import type { Subscription } from "../../dist/PubSubManager/PubSubManager";
10
+ import type { DB, DBHandlerServer } from "../../dist/Prostgles";
11
+ import type { LocalParams } from "../../dist/DboBuilder/DboBuilderTypes";
12
+ import { getConnectionDetails } from "prostgles-server/dist/DboBuilder/runSql/getAdminClient";
13
+ import type { TableHandler } from "../../dist/DboBuilder/TableHandler/TableHandler";
14
+ import type { withUserRLS as WithUserRLS } from "../../dist/DboBuilder/dboBuilderUtils";
15
+ import type { ParsedTableRule } from "../../dist/PublishParser/PublishParser";
16
+
17
+ export const testWithUserRLS = async (
18
+ dbo: DBHandlerServer,
19
+ db: DB,
20
+ withUserRLS: typeof WithUserRLS,
21
+ ) => {
22
+ const requestUser = {
23
+ id: "00000000-0000-0000-0000-000000000002",
24
+ type: "admin",
25
+ tenant_id: 42,
26
+ };
27
+ const localParams: LocalParams = {
28
+ isRemoteRequest: { user: requestUser, clientInfo: undefined },
29
+ };
30
+ const anonymousParams: LocalParams = {
31
+ isRemoteRequest: { clientInfo: undefined },
32
+ };
33
+ const readUser = `SELECT prostgles.user()::jsonb AS "user"`;
34
+ const assertUser = async (tx: Pick<pgPromise.ITask<{}>, "one">, user: object = requestUser) => {
35
+ assert.deepEqual((await tx.one(readUser)).user, user);
36
+ };
37
+
38
+ await test("RLS context is isolated across implicit transactions, commit, rollback and pool reuse", async () => {
39
+ const pgp = pgPromise();
40
+ const pool = pgp({ ...db.$pool.options, password: db.$pool.options.password, max: 1 });
41
+ try {
42
+ const { pid } = await pool.one<{ pid: number }>("SELECT pg_backend_pid() AS pid");
43
+ const assertReset = async () => {
44
+ assert.deepEqual(await pool.one(`${readUser}, pg_backend_pid() AS pid`), {
45
+ user: {},
46
+ pid,
47
+ });
48
+ };
49
+ assert.deepEqual(
50
+ await pool.one(
51
+ withUserRLS(
52
+ localParams,
53
+ `SELECT prostgles.user_id()::text AS id,
54
+ prostgles.user_type() AS type, prostgles.user('tenant_id') AS tenant`,
55
+ ),
56
+ ),
57
+ { id: requestUser.id, type: requestUser.type, tenant: "42" },
58
+ );
59
+ await assertReset();
60
+
61
+ for (const rollback of [false, true]) {
62
+ const abort = new Error("rollback RLS test");
63
+ const transaction = pool.tx(async (tx) => {
64
+ await tx.none(withUserRLS(localParams, ""));
65
+ // Separate statements and local dbx-style calls must keep the transaction's user.
66
+ await assertUser(tx);
67
+ await tx.one(withUserRLS(undefined, "SELECT 1", true));
68
+ await assertUser(tx);
69
+ if (rollback) throw abort;
70
+ });
71
+ if (rollback) await assert.rejects(transaction, (error) => error === abort);
72
+ else await transaction;
73
+ await assertReset();
74
+ }
75
+ await assert.rejects(
76
+ pool.tx(async (tx) => {
77
+ await tx.none(withUserRLS(localParams, ""));
78
+ await tx.one("SELECT 1 / 0");
79
+ }),
80
+ { code: "22012" },
81
+ );
82
+ await assertReset();
83
+
84
+ const secondUser = {
85
+ ...requestUser,
86
+ id: "00000000-0000-0000-0000-000000000003",
87
+ };
88
+ await pool.tx(async (tx) => {
89
+ await tx.none(
90
+ withUserRLS({ isRemoteRequest: { user: secondUser, clientInfo: undefined } }, ""),
91
+ );
92
+ await assertUser(tx, secondUser);
93
+ await tx.none(withUserRLS(anonymousParams, "", true));
94
+ await assertUser(tx, {});
95
+ });
96
+ await assertReset();
97
+ } finally {
98
+ await pool.$pool.end();
99
+ }
100
+ });
101
+
102
+ const table = dbo.rec!;
103
+ await test("subscription and sync reads isolate users with a real RLS policy", async () => {
104
+ const manager = await table.dboBuilder.getPubSubManager();
105
+ const rollback = new Error("rollback RLS fixture");
106
+ await assert.rejects(
107
+ db.tx(async (tx) => {
108
+ // The suite connects as a superuser; use an unprivileged role to exercise RLS.
109
+ const role = pgPromise.as.name(`rls_test_${process.pid}_${Date.now()}`);
110
+ await tx.none(`
111
+ CREATE ROLE ${role};
112
+ GRANT USAGE ON SCHEMA public, prostgles TO ${role};
113
+ GRANT SELECT ON rec TO ${role};
114
+ INSERT INTO rec (id) VALUES (-42001), (-42002);
115
+ ALTER TABLE rec ENABLE ROW LEVEL SECURITY;
116
+ CREATE POLICY rls_test ON rec USING (id = prostgles.user('tenant_id')::integer);
117
+ SET LOCAL ROLE ${role};
118
+ `);
119
+ for (const tenantId of [-42001, -42002, undefined]) {
120
+ const params: LocalParams = {
121
+ isRemoteRequest: {
122
+ clientInfo: undefined,
123
+ user: tenantId === undefined ? undefined : { ...requestUser, tenant_id: tenantId },
124
+ },
125
+ tx: { t: tx, dbTX: table.dboBuilder.dbo },
126
+ };
127
+ const expected = tenantId === undefined ? [] : [{ id: tenantId }];
128
+ const table_rules = {
129
+ select: { fields: "*", filterFields: "*", orderByFields: "*" },
130
+ } as const;
131
+ const result = await manager.getSubData({
132
+ table_info: { name: "rec" },
133
+ filter: {},
134
+ selectParams: { select: ["id"] },
135
+ onData: () => {},
136
+ localParams: params,
137
+ table_rules,
138
+ } as unknown as Subscription);
139
+ assert.deepEqual(result, { data: expected });
140
+ assert.deepEqual(
141
+ await fetchSyncServerData(
142
+ {
143
+ tableHandler: table,
144
+ localParams: params,
145
+ from_synced: undefined,
146
+ offset: undefined,
147
+ },
148
+ {
149
+ filter: {},
150
+ id_fields: ["id"],
151
+ synced_field: "id",
152
+ batch_size: 10,
153
+ params: { select: ["id"] },
154
+ table_rules,
155
+ },
156
+ ),
157
+ expected,
158
+ );
159
+ }
160
+ throw rollback;
161
+ }),
162
+ (error) => error === rollback,
163
+ );
164
+ });
165
+
166
+ await test("ordinary beforeEach hooks transform inputs before validation", async () => {
167
+ await table.dboBuilder.getTX(async (dbx) => {
168
+ const rec = dbx.rec as TableHandler;
169
+ // Negative fixture IDs avoid collisions with generated positive IDs.
170
+ const rows = {
171
+ allowed: { id: -43001 },
172
+ denied: { id: -43002 },
173
+ transformed: { id: -43003 },
174
+ stripped: { id: -43004 },
175
+ statementOnly: { id: -43005 },
176
+ missing: { id: -43999 },
177
+ };
178
+ await rec.insert([rows.allowed, rows.denied]);
179
+ const calls: number[][] = [];
180
+ const inputs: object[] = [];
181
+ rec.hooks = {
182
+ beforeEach: [
183
+ {
184
+ commands: { insert: 1, update: 1 },
185
+ validate: async ({ data, command, filter, dbx }) => {
186
+ inputs.push({ ...data });
187
+ // Pre-validation hooks can consume fields that are not database columns.
188
+ if ("parent" in data) {
189
+ data.parent_id = data.parent;
190
+ delete data.parent;
191
+ }
192
+ if (command === "update") {
193
+ calls.push((await dbx.rec!.find(filter)).map((row) => row.id));
194
+ } else {
195
+ // Trusted hooks may add fields in place that clients cannot supply.
196
+ data.parent_id = rows.allowed.id;
197
+ }
198
+ return { row: data };
199
+ },
200
+ },
201
+ ],
202
+ };
203
+ const rules: ParsedTableRule = {
204
+ insert: { fields: ["id"], returningFields: "*" },
205
+ update: {
206
+ fields: ["parent_id"],
207
+ filterFields: "*",
208
+ returningFields: "*",
209
+ forcedFilter: rows.allowed,
210
+ },
211
+ };
212
+ const inserted = await rec.insert(
213
+ { ...rows.transformed, parent: rows.allowed.id },
214
+ { returning: "*" },
215
+ undefined,
216
+ rules,
217
+ );
218
+ assert.equal(inserted.parent_id, rows.allowed.id);
219
+ assert.deepEqual(inputs.at(-1), { ...rows.transformed, parent: rows.allowed.id });
220
+ // Fields left by the hook still undergo the usual validation.
221
+ await assert.rejects(() =>
222
+ rec.insert({ ...rows.stripped, recf: null }, undefined, undefined, rules),
223
+ );
224
+ await assert.rejects(() => rec.update(rows.allowed, { recf: null }, undefined, rules));
225
+ assert.equal(inputs.length, 3);
226
+ calls.length = 0;
227
+ assert.deepEqual(
228
+ await rec.update(rows.denied, { parent: rows.allowed.id }, { returning: "*" }, rules),
229
+ [],
230
+ );
231
+ assert.deepEqual(
232
+ await rec.update(rows.missing, { parent: rows.allowed.id }, { returning: "*" }, rules),
233
+ [],
234
+ );
235
+ await rec.update({}, { parent: rows.allowed.id }, undefined, rules);
236
+ assert.deepEqual(calls, [[], [], [rows.allowed.id]]);
237
+ assert.equal((await rec.findOne(rows.denied))!.parent_id, null);
238
+ const stripped = await rec.insert(
239
+ { ...rows.stripped, recf: null },
240
+ { returning: "*", removeDisallowedFields: true },
241
+ undefined,
242
+ rules,
243
+ );
244
+ assert.equal(stripped.parent_id, rows.allowed.id);
245
+ assert.deepEqual(inputs.at(-1), { ...rows.stripped, recf: null });
246
+ const count = inputs.length;
247
+ assert.equal(
248
+ typeof (await rec.insert(
249
+ { ...rows.statementOnly },
250
+ { returnType: "statement" },
251
+ undefined,
252
+ rules,
253
+ )),
254
+ "string",
255
+ );
256
+ assert.equal(
257
+ typeof (await rec.update(
258
+ {},
259
+ { parent: rows.allowed.id },
260
+ { returnType: "statement" },
261
+ rules,
262
+ )),
263
+ "string",
264
+ );
265
+ await rec.updateBatch([[{}, { parent: rows.denied.id }]], undefined, undefined, rules);
266
+ assert.equal(inputs.length, count + 3);
267
+ assert.equal((await rec.findOne(rows.allowed))!.parent_id, rows.denied.id);
268
+ assert.equal(await rec.findOne(rows.statementOnly), undefined);
269
+ await rec.delete({ id: { $in: Object.values(rows).map(({ id }) => id) } });
270
+ });
271
+ });
272
+
273
+ await test("multi false is checked after beforeEach and rolls back the update", async () => {
274
+ let calls = 0;
275
+ const rows = [{ id: -43001 }, { id: -43002 }];
276
+ const filter = { id: { $in: rows.map(({ id }) => id) } };
277
+ await assert.rejects(
278
+ table.dboBuilder.getTX(async (dbx) => {
279
+ const rec = dbx.rec as TableHandler;
280
+ await rec.insert(rows);
281
+ rec.hooks = {
282
+ beforeEach: [
283
+ {
284
+ commands: { update: 1 },
285
+ validate: () => {
286
+ calls++;
287
+ },
288
+ },
289
+ ],
290
+ };
291
+ await rec.update(filter, { parent_id: rows[0]!.id }, { multi: false });
292
+ }),
293
+ (error: unknown) => JSON.stringify(error).includes("More than 1 row modified"),
294
+ );
295
+ assert.equal(calls, 1);
296
+ assert.equal(await table.count(filter), 0);
297
+ });
298
+
299
+ await test("ordinary beforeEach hooks run before PostgreSQL UPDATE policy checks", async () => {
300
+ const abort = new Error("rollback hook RLS fixture");
301
+ await assert.rejects(
302
+ table.dboBuilder.getTX(async (dbx, tx) => {
303
+ const rec = dbx.rec as TableHandler;
304
+ const row = { id: -43001 };
305
+ await rec.insert(row);
306
+ let calls = 0;
307
+ rec.hooks = {
308
+ beforeEach: [
309
+ {
310
+ commands: { update: 1 },
311
+ validate: () => {
312
+ calls++;
313
+ },
314
+ },
315
+ ],
316
+ };
317
+ const role = pgPromise.as.name(`hook_rls_${process.pid}`);
318
+ await tx.none(`
319
+ CREATE ROLE ${role};
320
+ GRANT USAGE ON SCHEMA public, prostgles TO ${role};
321
+ -- Statement triggers still run when RLS prevents every row update.
322
+ GRANT SELECT ON prostgles.v_triggers TO ${role};
323
+ GRANT SELECT, UPDATE ON rec TO ${role};
324
+ ALTER TABLE rec ENABLE ROW LEVEL SECURITY;
325
+ CREATE POLICY hook_select ON rec FOR SELECT TO ${role} USING (true);
326
+ CREATE POLICY hook_update ON rec FOR UPDATE TO ${role} USING (false);
327
+ SET LOCAL ROLE ${role};
328
+ `);
329
+ assert.deepEqual(await rec.update(row, { parent_id: null }, { returning: "*" }), []);
330
+ assert.equal(calls, 1);
331
+ throw abort;
332
+ }),
333
+ (error) => error === abort,
334
+ );
335
+ });
336
+
337
+ await test("before and after hooks preserve RLS across dbx and raw tx calls", async () => {
338
+ await table.dboBuilder.getTX(async (dbx) => {
339
+ const rec = dbx.rec as TableHandler;
340
+ let beforeCalls = 0;
341
+ let afterCalls = 0;
342
+ rec.hooks = {
343
+ beforeEach: [
344
+ {
345
+ commands: { insert: 1 },
346
+ validate: async ({ tx, dbx }) => {
347
+ await assertUser(tx);
348
+ await dbx.rec!.find({});
349
+ await assertUser(tx);
350
+ beforeCalls++;
351
+ },
352
+ },
353
+ {
354
+ commands: { insert: 1 },
355
+ validate: async ({ tx }) => {
356
+ await assertUser(tx);
357
+ beforeCalls++;
358
+ },
359
+ },
360
+ ],
361
+ afterEach: [
362
+ {
363
+ commands: { insert: 1 },
364
+ validate: async ({ tx, dbx }) => {
365
+ await assertUser(tx);
366
+ await dbx.rec!.count({});
367
+ await assertUser(tx);
368
+ afterCalls++;
369
+ },
370
+ },
371
+ ],
372
+ };
373
+ const row = await rec.insert({}, { returning: "*" }, undefined, undefined, localParams);
374
+ await rec.delete({ id: row.id });
375
+ assert.equal(beforeCalls, 2);
376
+ assert.equal(afterCalls, 1);
377
+ });
378
+ });
379
+
380
+ await test(
381
+ "post-transaction hooks receive committed and authenticated context",
382
+ { timeout: 15_000 },
383
+ async (t) => {
384
+ const schemaName = `delete_context_${process.pid}_${Date.now()}`;
385
+ const tableName = `${schemaName}.rec`;
386
+ const schema = pgPromise.as.name(schemaName);
387
+ const app = express();
388
+ const http = createServer(app);
389
+ let instance:
390
+ | Pick<
391
+ Awaited<ReturnType<typeof prostgles>>,
392
+ "db" | "destroy" | "getClientDBHandlers"
393
+ >
394
+ | undefined;
395
+ const afterCommitResults: {
396
+ ids: number[];
397
+ committedRowCount: number;
398
+ hasTransactionalHandlers: boolean;
399
+ context: unknown;
400
+ }[] = [];
401
+ app.use(express.json());
402
+
403
+ try {
404
+ await db.none(
405
+ `CREATE SCHEMA ${schema}; CREATE TABLE ${schema}.rec (
406
+ id INTEGER PRIMARY KEY,
407
+ value TEXT,
408
+ other TEXT
409
+ )`,
410
+ );
411
+ http.listen(0, "127.0.0.1");
412
+ await once(http, "listening");
413
+ const address = http.address();
414
+ assert(address && typeof address === "object");
415
+
416
+ instance = await prostgles({
417
+ dbConnection: getConnectionDetails(db) as unknown as ProstglesInitOptions["dbConnection"],
418
+ schemaFilter: { [schemaName]: 1 },
419
+ transactions: true,
420
+ publish: "*",
421
+ tableHooks: {
422
+ [tableName]: {
423
+ afterCommit: [
424
+ {
425
+ commands: { insert: 1, update: 1 },
426
+ changedFields: ["value"],
427
+ run: async (args) => {
428
+ // @ts-expect-error afterCommit does not expose the completed transaction.
429
+ args.tx;
430
+ // @ts-expect-error afterCommit does not expose transaction-bound handlers.
431
+ args.dbx;
432
+ const ids = args.rows.map(({ id }) => id as number);
433
+ const { clientDb } = await args.getClientDBHandlers(
434
+ { userId: requestUser.id },
435
+ { tables: { [tableName]: { select: true } } },
436
+ );
437
+ afterCommitResults.push({
438
+ ids,
439
+ committedRowCount: await clientDb[tableName]!.count!({
440
+ id: { $in: ids },
441
+ }),
442
+ hasTransactionalHandlers: "tx" in args || "dbx" in args,
443
+ context: args.context,
444
+ });
445
+ },
446
+ },
447
+ ],
448
+ onInsteadOfDelete: async ({ tx, dbx }) => {
449
+ const before = (await tx.one(readUser)).user;
450
+ await (dbx[tableName] as TableHandler).find({});
451
+ const after = (await tx.one(readUser)).user;
452
+ return [{ before, after }];
453
+ },
454
+ },
455
+ },
456
+ auth: {
457
+ sessionFields: "*",
458
+ sidKeyName: "token",
459
+ findUser: () => requestUser,
460
+ getUser: (sid) =>
461
+ sid === "authenticated" ?
462
+ {
463
+ user: requestUser,
464
+ clientUser: { id: requestUser.id, type: requestUser.type },
465
+ }
466
+ : undefined,
467
+ },
468
+ restApi: { expressApp: app, path: "/rls-context" },
469
+ createContext: () => ({ source: "afterCommit" }),
470
+ onReady: () => {},
471
+ });
472
+
473
+ await t.test("afterCommit runs after commit, filters changed fields and skips rollback", async () => {
474
+ assert(instance);
475
+ const committedIds = [-44001, -44002];
476
+ await instance.db[tableName]!.insertMany!(
477
+ committedIds.map((id) => ({ id, value: "initial" })),
478
+ );
479
+ assert.deepEqual(afterCommitResults, [
480
+ {
481
+ ids: committedIds,
482
+ committedRowCount: committedIds.length,
483
+ hasTransactionalHandlers: false,
484
+ context: { source: "afterCommit" },
485
+ },
486
+ ]);
487
+ await instance.db[tableName]!.update!({ id: committedIds[0] }, { other: "ignored" });
488
+ assert.equal(afterCommitResults.length, 1);
489
+ await instance.db[tableName]!.update!({ id: committedIds[0] }, { value: "updated" });
490
+ assert.deepEqual(afterCommitResults[1], {
491
+ ids: [committedIds[0]],
492
+ committedRowCount: 1,
493
+ hasTransactionalHandlers: false,
494
+ context: { source: "afterCommit" },
495
+ });
496
+
497
+ const rollback = new Error("rollback afterCommit test");
498
+ await assert.rejects(
499
+ instance.db.tx(async (dbx) => {
500
+ await dbx[tableName]!.insert!({ id: -44003, value: "rolled back" });
501
+ throw rollback;
502
+ }),
503
+ (error) => error === rollback,
504
+ );
505
+ assert.equal(afterCommitResults.length, 2);
506
+ });
507
+
508
+ const url = `http://127.0.0.1:${address.port}/rls-context/db/${tableName}/delete`;
509
+ const deleteRequest = async (sid?: string) => {
510
+ const response = await fetch(url, {
511
+ method: "POST",
512
+ headers: {
513
+ "Content-Type": "application/json",
514
+ ...(sid && {
515
+ Authorization: `Bearer ${Buffer.from(sid).toString("base64")}`,
516
+ }),
517
+ },
518
+ body: JSON.stringify([{ id: -1 }]),
519
+ });
520
+ const result = await response.json();
521
+ assert.equal(response.status, 200, JSON.stringify(result));
522
+ return result;
523
+ };
524
+
525
+ assert.deepEqual(await deleteRequest("authenticated"), [
526
+ { before: requestUser, after: requestUser },
527
+ ]);
528
+ assert.deepEqual(await deleteRequest("anonymous"), [{ before: {}, after: {} }]);
529
+ await t.test("server-user requests preserve tenant_id from sessionFields", async () => {
530
+ assert(instance);
531
+ // findUser returns the same user as getUser, whose sessionFields includes tenant_id.
532
+ const serverUser = await instance.getClientDBHandlers(
533
+ { userId: requestUser.id },
534
+ undefined,
535
+ );
536
+ const result = await serverUser.clientDb[tableName]!.delete!({ id: -1 });
537
+ assert.deepEqual(
538
+ result,
539
+ [{ before: requestUser, after: requestUser }],
540
+ "PostgreSQL hooks must receive tenant_id for both HTTP and server-user requests",
541
+ );
542
+ });
543
+ } finally {
544
+ await instance?.destroy();
545
+ if (http.listening) {
546
+ await new Promise<void>((resolve, reject) =>
547
+ http.close((error) => (error ? reject(error) : resolve())),
548
+ );
549
+ }
550
+ await db.none(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
551
+ }
552
+ },
553
+ );
554
+ };
@@ -0,0 +1,4 @@
1
+
2
+ > prostgles-server@4.2.646 build
3
+ > tsc
4
+
@@ -0,0 +1,4 @@
1
+
2
+ > prostgles-server@4.2.646 lint
3
+ > eslint . --ext .ts --quiet
4
+