prostgles-server 4.2.642 → 4.2.644

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 (173) hide show
  1. package/debug/caught-request-fix-build.log +4 -0
  2. package/debug/caught-request-fix-test-build.log +0 -0
  3. package/debug/caught-request-subscription-failure.log +603 -0
  4. package/debug/caught-validation-build.log +4 -0
  5. package/debug/caught-validation-fix-build.log +4 -0
  6. package/debug/caught-validation-fix-test-build.log +0 -0
  7. package/debug/caught-validation-test-build.log +0 -0
  8. package/debug/client-db-tx-build.log +4 -0
  9. package/debug/client-db-tx-test-server.log +3248 -0
  10. package/debug/client-db-tx-tests-build.log +0 -0
  11. package/debug/client-handler-shape-build.log +4 -0
  12. package/debug/client-handler-shape-test-server.log +544 -0
  13. package/debug/client-handler-shape-tests-build.log +0 -0
  14. package/debug/column-definitions-test-server.log +152 -164
  15. package/debug/fixes-review-build.log +4 -0
  16. package/debug/fixes-review-client-schema.backup +745 -0
  17. package/debug/fixes-review-generated-schema.backup +489 -0
  18. package/debug/fixes-review-probes-build.log +0 -0
  19. package/debug/fixes-review-probes.patch +43 -0
  20. package/debug/fixes-review-restore-build.log +0 -0
  21. package/debug/fixes-review-tests-build.log +0 -0
  22. package/debug/fixes-review-typecheck.log +8 -0
  23. package/debug/function-client-db-tx-build.log +4 -0
  24. package/debug/function-client-db-tx-tests-build.log +0 -0
  25. package/debug/review-current-fixes-build.log +4 -0
  26. package/debug/review-current-fixes-client.backup +745 -0
  27. package/debug/review-current-fixes-probes-build.log +0 -0
  28. package/debug/review-current-fixes-probes.patch +55 -0
  29. package/debug/review-current-fixes-restore-build.log +0 -0
  30. package/debug/review-current-fixes-rls.backup +478 -0
  31. package/debug/review-current-fixes-schema.backup +489 -0
  32. package/debug/review-current-fixes-test-build.log +0 -0
  33. package/debug/review-current-fixes-typecheck.log +8 -0
  34. package/debug/review-user-identity-build.log +4 -0
  35. package/debug/review-user-identity-probes-build.log +0 -0
  36. package/debug/review-user-identity-probes.log +558 -0
  37. package/debug/review-user-identity-probes.patch +45 -0
  38. package/debug/review-user-identity-restore-build.log +0 -0
  39. package/debug/review-user-identity-rls.backup +463 -0
  40. package/debug/review-user-identity-schema.backup +720 -0
  41. package/debug/review-user-identity-server.log +3248 -0
  42. package/debug/review-user-identity-test-build.log +0 -0
  43. package/debug/review-user-identity-typecheck.log +8 -0
  44. package/debug/schema-types-focused-strict-check.log +0 -0
  45. package/debug/schema-types-strict-check.log +38 -0
  46. package/debug/schema-types-tests-build.log +0 -0
  47. package/debug/security-review-baseline-tests.log +3250 -0
  48. package/debug/security-review-build.log +4 -0
  49. package/debug/security-review-checkfilter-repro.log +545 -0
  50. package/debug/security-review-clientSchemaTypes.spec.ts.backup +553 -0
  51. package/debug/security-review-repro-build.log +0 -0
  52. package/debug/security-review-repro.spec.ts +591 -0
  53. package/debug/security-review-restore-build.log +0 -0
  54. package/debug/security-review-server-tests.log +3254 -0
  55. package/debug/security-review-test-build.log +0 -0
  56. package/debug/server-user-lint.log +4 -0
  57. package/debug/server-user-test-server.log +538 -0
  58. package/debug/server-user-typecheck.log +8 -0
  59. package/debug/test-check-eslint.log +6 -0
  60. package/debug/test-check-lint-baseline.json +1 -0
  61. package/debug/test-check-npm-test.log +789 -0
  62. package/debug/test-check-types.log +8 -0
  63. package/debug/test-config-npm-test.log +223 -0
  64. package/debug/test-config-strict-null-check.log +50 -0
  65. package/debug/test-config-typecheck.log +8 -0
  66. package/debug/tryRunP-test-quick.log +78 -0
  67. package/debug/tryRunP-typecheck.log +42 -0
  68. package/debug/user-identity-direct-call-build.log +0 -0
  69. package/debug/user-identity-direct-call-schema.backup +489 -0
  70. package/debug/user-identity-regressions-build.log +4 -0
  71. package/debug/user-identity-regressions-generated-schema.backup +489 -0
  72. package/debug/user-identity-regressions-test-build.log +0 -0
  73. package/dist/Auth/AuthHandler.js +1 -1
  74. package/dist/Auth/AuthHandler.js.map +1 -1
  75. package/dist/Auth/AuthTypes.d.ts +11 -9
  76. package/dist/Auth/AuthTypes.d.ts.map +1 -1
  77. package/dist/Auth/AuthTypes.js.map +1 -1
  78. package/dist/Auth/utils/getClientRequestIPsInfo.d.ts +2 -1
  79. package/dist/Auth/utils/getClientRequestIPsInfo.d.ts.map +1 -1
  80. package/dist/Auth/utils/getClientRequestIPsInfo.js +8 -1
  81. package/dist/Auth/utils/getClientRequestIPsInfo.js.map +1 -1
  82. package/dist/Auth/utils/getSessionUser.d.ts +4 -3
  83. package/dist/Auth/utils/getSessionUser.d.ts.map +1 -1
  84. package/dist/Auth/utils/getSessionUser.js +4 -2
  85. package/dist/Auth/utils/getSessionUser.js.map +1 -1
  86. package/dist/Auth/utils/getSessionUser.spec.js +2 -4
  87. package/dist/Auth/utils/getSessionUser.spec.js.map +1 -1
  88. package/dist/Auth/utils/handleGetUser.d.ts.map +1 -1
  89. package/dist/Auth/utils/handleGetUser.js +16 -0
  90. package/dist/Auth/utils/handleGetUser.js.map +1 -1
  91. package/dist/Auth/utils/serverSideRequest.d.ts +17 -0
  92. package/dist/Auth/utils/serverSideRequest.d.ts.map +1 -0
  93. package/dist/Auth/utils/serverSideRequest.js +20 -0
  94. package/dist/Auth/utils/serverSideRequest.js.map +1 -0
  95. package/dist/DboBuilder/DboBuilder.d.ts.map +1 -1
  96. package/dist/DboBuilder/DboBuilder.js +4 -2
  97. package/dist/DboBuilder/DboBuilder.js.map +1 -1
  98. package/dist/DboBuilder/TableHandler/TableHandler.d.ts +1 -1
  99. package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts +0 -4
  100. package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts.map +1 -1
  101. package/dist/DboBuilder/ViewHandler/ViewHandler.js +5 -8
  102. package/dist/DboBuilder/ViewHandler/ViewHandler.js.map +1 -1
  103. package/dist/DboBuilder/ViewHandler/count.d.ts.map +1 -1
  104. package/dist/DboBuilder/ViewHandler/count.js +2 -1
  105. package/dist/DboBuilder/ViewHandler/count.js.map +1 -1
  106. package/dist/DboBuilder/ViewHandler/find.d.ts.map +1 -1
  107. package/dist/DboBuilder/ViewHandler/find.js +2 -1
  108. package/dist/DboBuilder/ViewHandler/find.js.map +1 -1
  109. package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.d.ts +9 -2
  110. package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.d.ts.map +1 -1
  111. package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.js +57 -23
  112. package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.js.map +1 -1
  113. package/dist/DboBuilder/ViewHandler/size.d.ts.map +1 -1
  114. package/dist/DboBuilder/ViewHandler/size.js +2 -1
  115. package/dist/DboBuilder/ViewHandler/size.js.map +1 -1
  116. package/dist/DboBuilder/dboBuilderUtils.d.ts +1 -0
  117. package/dist/DboBuilder/dboBuilderUtils.d.ts.map +1 -1
  118. package/dist/DboBuilder/dboBuilderUtils.js +2 -1
  119. package/dist/DboBuilder/dboBuilderUtils.js.map +1 -1
  120. package/dist/PublishParser/PublishParser.d.ts.map +1 -1
  121. package/dist/PublishParser/PublishParser.js +2 -1
  122. package/dist/PublishParser/PublishParser.js.map +1 -1
  123. package/dist/PublishParser/defineServerFunction.d.ts +2 -0
  124. package/dist/PublishParser/defineServerFunction.d.ts.map +1 -1
  125. package/dist/PublishParser/defineServerFunction.js.map +1 -1
  126. package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
  127. package/dist/PublishParser/getPublishedTableNames.js +2 -1
  128. package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
  129. package/dist/PublishParser/publishTypesAndUtils.d.ts +1 -1
  130. package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
  131. package/dist/StorageClient/StorageClientTypes.d.ts +5 -4
  132. package/dist/StorageClient/StorageClientTypes.d.ts.map +1 -1
  133. package/dist/WebsocketAPI/getClientHandlers.d.ts +5 -0
  134. package/dist/WebsocketAPI/getClientHandlers.d.ts.map +1 -1
  135. package/dist/WebsocketAPI/getClientHandlers.js +33 -14
  136. package/dist/WebsocketAPI/getClientHandlers.js.map +1 -1
  137. package/dist/index.d.ts +1 -0
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +3 -1
  140. package/dist/index.js.map +1 -1
  141. package/dist/initProstgles.d.ts +12 -2
  142. package/dist/initProstgles.d.ts.map +1 -1
  143. package/dist/initProstgles.js +10 -1
  144. package/dist/initProstgles.js.map +1 -1
  145. package/dist/runClientRequest.d.ts +2 -1
  146. package/dist/runClientRequest.d.ts.map +1 -1
  147. package/dist/runClientRequest.js +7 -4
  148. package/dist/runClientRequest.js.map +1 -1
  149. package/eslint.config.mjs +21 -1
  150. package/lib/Auth/AuthHandler.ts +1 -1
  151. package/lib/Auth/AuthTypes.ts +12 -8
  152. package/lib/Auth/utils/getClientRequestIPsInfo.ts +11 -2
  153. package/lib/Auth/utils/getSessionUser.spec.ts +4 -6
  154. package/lib/Auth/utils/getSessionUser.ts +8 -5
  155. package/lib/Auth/utils/handleGetUser.ts +13 -0
  156. package/lib/Auth/utils/serverSideRequest.ts +27 -0
  157. package/lib/DboBuilder/DboBuilder.ts +7 -2
  158. package/lib/DboBuilder/ViewHandler/ViewHandler.ts +11 -11
  159. package/lib/DboBuilder/ViewHandler/count.ts +2 -1
  160. package/lib/DboBuilder/ViewHandler/find.ts +2 -1
  161. package/lib/DboBuilder/ViewHandler/getDbHandlerWithAbort.ts +72 -29
  162. package/lib/DboBuilder/ViewHandler/size.ts +2 -1
  163. package/lib/DboBuilder/dboBuilderUtils.ts +2 -0
  164. package/lib/PublishParser/PublishParser.ts +2 -1
  165. package/lib/PublishParser/defineServerFunction.ts +3 -5
  166. package/lib/PublishParser/getPublishedTableNames.ts +2 -1
  167. package/lib/PublishParser/publishTypesAndUtils.ts +1 -1
  168. package/lib/StorageClient/StorageClientTypes.ts +5 -4
  169. package/lib/WebsocketAPI/getClientHandlers.ts +63 -29
  170. package/lib/index.ts +3 -3
  171. package/lib/initProstgles.ts +31 -5
  172. package/lib/runClientRequest.ts +10 -3
  173. package/package.json +5 -3
File without changes
@@ -0,0 +1,55 @@
1
+ --- a/tests/server/clientSchemaTypes.spec.ts
2
+ +++ b/tests/server/clientSchemaTypes.spec.ts
3
+ @@ -78,11 +78,13 @@
4
+ const serverUsers = new Map([
5
+ ["user-guest", { id: "user-guest", type: "guest" }],
6
+ ["user-admin", { id: "user-admin", type: "admin" }],
7
+ + ["guest", { id: "guest", type: "admin" }],
8
+ ]);
9
+ instance = await prostgles({
10
+ dbConnection: {
11
+ ...getConnectionDetails(db),
12
+ options: `-c search_path=${schemaName}`,
13
+ + application_name: suffix,
14
+ } as unknown as ProstglesInitOptions["dbConnection"],
15
+ tsGeneratedTypesDir: generatedTypesDir,
16
+ transactions: true,
17
+ @@ -485,6 +487,38 @@
18
+ "A rejected guest abort must not leave a pending cancellation for the admin",
19
+ );
20
+ });
21
+ + await t.test("review: a session ID cannot cancel a matching server user ID", async () => {
22
+ + assert(instance);
23
+ + const admin = await instance.getClientDBHandlers({ userId: "guest" }, undefined);
24
+ + const find = admin.clientDb[tableName]!.find! as ViewHandler["find"];
25
+ + const abortSignalId = randomUUID();
26
+ + let queryResult: Promise<unknown> | undefined;
27
+ + let abortError: unknown;
28
+ + await db.tx(async (tx) => {
29
+ + await tx.none(`LOCK TABLE ${schemaName}.${tableName} IN ACCESS EXCLUSIVE MODE`);
30
+ + queryResult = find({}, {}, { abortSignalId }).then(
31
+ + (rows) => ({ rows }), (error: unknown) => ({ error }),
32
+ + );
33
+ + let blocked = false;
34
+ + for (let attempt = 0; attempt < 100; attempt++) {
35
+ + const row = await db.one<{ blocked: boolean }>(
36
+ + "SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE application_name LIKE $1 AND wait_event_type = 'Lock') AS blocked",
37
+ + [`%${suffix}`],
38
+ + );
39
+ + if (row.blocked) { blocked = true; break; }
40
+ + await new Promise((resolve) => setTimeout(resolve, 10));
41
+ + }
42
+ + assert(blocked, "The server-user query must be active before the guest attempts cancellation");
43
+ + abortError = await new Promise((resolve) => {
44
+ + sockets[0]!.emit(CHANNELS.DEFAULT, {
45
+ + tableName, command: "abort", param1: { command: "find", abortSignalId },
46
+ + }, (error: unknown) => resolve(error));
47
+ + });
48
+ + if (!abortError) await queryResult;
49
+ + });
50
+ + const result = await queryResult;
51
+ + assert(abortError, `Guest SID cancelled server user ID: ${JSON.stringify(result)}`);
52
+ + });
53
+ const viewerSocket = io.sockets.sockets.get(sockets[2]!.id!);
54
+ assert(viewerSocket);
55
+ const viewerHandlers = await instance.getClientDBHandlers(
File without changes
@@ -0,0 +1,478 @@
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
+ "onInsteadOfDelete receives the authenticated or anonymous request 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<Awaited<ReturnType<typeof prostgles>>, "destroy" | "getClientDBHandlers"> | undefined;
391
+ app.use(express.json());
392
+
393
+ try {
394
+ await db.none(
395
+ `CREATE SCHEMA ${schema}; CREATE TABLE ${schema}.rec (id INTEGER PRIMARY KEY)`,
396
+ );
397
+ http.listen(0, "127.0.0.1");
398
+ await once(http, "listening");
399
+ const address = http.address();
400
+ assert(address && typeof address === "object");
401
+
402
+ instance = await prostgles({
403
+ dbConnection: getConnectionDetails(db) as unknown as ProstglesInitOptions["dbConnection"],
404
+ schemaFilter: { [schemaName]: 1 },
405
+ publish: "*",
406
+ tableHooks: {
407
+ [tableName]: {
408
+ onInsteadOfDelete: async ({ tx, dbx }) => {
409
+ const before = (await tx.one(readUser)).user;
410
+ await (dbx[tableName] as TableHandler).find({});
411
+ const after = (await tx.one(readUser)).user;
412
+ return [{ before, after }];
413
+ },
414
+ },
415
+ },
416
+ auth: {
417
+ sessionFields: "*",
418
+ sidKeyName: "token",
419
+ findUser: () => requestUser,
420
+ getUser: (sid) =>
421
+ sid === "authenticated" ?
422
+ {
423
+ user: requestUser,
424
+ clientUser: { id: requestUser.id, type: requestUser.type },
425
+ }
426
+ : undefined,
427
+ },
428
+ restApi: { expressApp: app, path: "/rls-context" },
429
+ onReady: () => {},
430
+ });
431
+
432
+ const url = `http://127.0.0.1:${address.port}/rls-context/db/${tableName}/delete`;
433
+ const deleteRequest = async (sid?: string) => {
434
+ const response = await fetch(url, {
435
+ method: "POST",
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ ...(sid && {
439
+ Authorization: `Bearer ${Buffer.from(sid).toString("base64")}`,
440
+ }),
441
+ },
442
+ body: JSON.stringify([{ id: -1 }]),
443
+ });
444
+ const result = await response.json();
445
+ assert.equal(response.status, 200, JSON.stringify(result));
446
+ return result;
447
+ };
448
+
449
+ assert.deepEqual(await deleteRequest("authenticated"), [
450
+ { before: requestUser, after: requestUser },
451
+ ]);
452
+ assert.deepEqual(await deleteRequest("anonymous"), [{ before: {}, after: {} }]);
453
+ await t.test("server-user requests preserve tenant_id from sessionFields", async () => {
454
+ assert(instance);
455
+ // findUser returns the same user as getUser, whose sessionFields includes tenant_id.
456
+ const serverUser = await instance.getClientDBHandlers(
457
+ { userId: requestUser.id },
458
+ undefined,
459
+ );
460
+ const result = await serverUser.clientDb[tableName]!.delete!({ id: -1 });
461
+ assert.deepEqual(
462
+ result,
463
+ [{ before: requestUser, after: requestUser }],
464
+ "PostgreSQL hooks must receive tenant_id for both HTTP and server-user requests",
465
+ );
466
+ });
467
+ } finally {
468
+ await instance?.destroy();
469
+ if (http.listening) {
470
+ await new Promise<void>((resolve, reject) =>
471
+ http.close((error) => (error ? reject(error) : resolve())),
472
+ );
473
+ }
474
+ await db.none(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
475
+ }
476
+ },
477
+ );
478
+ };