prostgles-server 4.2.642 → 4.2.643

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 (86) 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/function-client-db-tx-build.log +4 -0
  16. package/debug/function-client-db-tx-tests-build.log +0 -0
  17. package/debug/schema-types-focused-strict-check.log +0 -0
  18. package/debug/schema-types-strict-check.log +38 -0
  19. package/debug/schema-types-tests-build.log +0 -0
  20. package/debug/security-review-baseline-tests.log +3250 -0
  21. package/debug/security-review-build.log +4 -0
  22. package/debug/security-review-checkfilter-repro.log +545 -0
  23. package/debug/security-review-clientSchemaTypes.spec.ts.backup +553 -0
  24. package/debug/security-review-repro-build.log +0 -0
  25. package/debug/security-review-repro.spec.ts +591 -0
  26. package/debug/security-review-restore-build.log +0 -0
  27. package/debug/security-review-server-tests.log +3254 -0
  28. package/debug/security-review-test-build.log +0 -0
  29. package/debug/test-check-eslint.log +6 -0
  30. package/debug/test-check-lint-baseline.json +1 -0
  31. package/debug/test-check-npm-test.log +789 -0
  32. package/debug/test-check-types.log +8 -0
  33. package/debug/test-config-npm-test.log +223 -0
  34. package/debug/test-config-strict-null-check.log +50 -0
  35. package/debug/test-config-typecheck.log +8 -0
  36. package/debug/tryRunP-test-quick.log +78 -0
  37. package/debug/tryRunP-typecheck.log +42 -0
  38. package/dist/Auth/AuthTypes.d.ts +3 -3
  39. package/dist/Auth/AuthTypes.d.ts.map +1 -1
  40. package/dist/Auth/AuthTypes.js.map +1 -1
  41. package/dist/DboBuilder/DboBuilder.d.ts.map +1 -1
  42. package/dist/DboBuilder/DboBuilder.js +4 -2
  43. package/dist/DboBuilder/DboBuilder.js.map +1 -1
  44. package/dist/DboBuilder/TableHandler/TableHandler.d.ts +1 -1
  45. package/dist/DboBuilder/dboBuilderUtils.d.ts +1 -0
  46. package/dist/DboBuilder/dboBuilderUtils.d.ts.map +1 -1
  47. package/dist/DboBuilder/dboBuilderUtils.js +2 -1
  48. package/dist/DboBuilder/dboBuilderUtils.js.map +1 -1
  49. package/dist/PublishParser/PublishParser.d.ts.map +1 -1
  50. package/dist/PublishParser/PublishParser.js +2 -1
  51. package/dist/PublishParser/PublishParser.js.map +1 -1
  52. package/dist/PublishParser/defineServerFunction.d.ts +2 -0
  53. package/dist/PublishParser/defineServerFunction.d.ts.map +1 -1
  54. package/dist/PublishParser/defineServerFunction.js.map +1 -1
  55. package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
  56. package/dist/PublishParser/getPublishedTableNames.js +2 -1
  57. package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
  58. package/dist/PublishParser/publishTypesAndUtils.d.ts +1 -1
  59. package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
  60. package/dist/StorageClient/StorageClientTypes.d.ts +5 -4
  61. package/dist/StorageClient/StorageClientTypes.d.ts.map +1 -1
  62. package/dist/WebsocketAPI/getClientHandlers.d.ts +5 -0
  63. package/dist/WebsocketAPI/getClientHandlers.d.ts.map +1 -1
  64. package/dist/WebsocketAPI/getClientHandlers.js +33 -14
  65. package/dist/WebsocketAPI/getClientHandlers.js.map +1 -1
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +3 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/runClientRequest.d.ts +2 -1
  71. package/dist/runClientRequest.d.ts.map +1 -1
  72. package/dist/runClientRequest.js +4 -2
  73. package/dist/runClientRequest.js.map +1 -1
  74. package/eslint.config.mjs +21 -1
  75. package/lib/Auth/AuthTypes.ts +3 -2
  76. package/lib/DboBuilder/DboBuilder.ts +7 -2
  77. package/lib/DboBuilder/dboBuilderUtils.ts +2 -0
  78. package/lib/PublishParser/PublishParser.ts +2 -1
  79. package/lib/PublishParser/defineServerFunction.ts +3 -5
  80. package/lib/PublishParser/getPublishedTableNames.ts +2 -1
  81. package/lib/PublishParser/publishTypesAndUtils.ts +1 -1
  82. package/lib/StorageClient/StorageClientTypes.ts +5 -4
  83. package/lib/WebsocketAPI/getClientHandlers.ts +63 -29
  84. package/lib/index.ts +3 -3
  85. package/lib/runClientRequest.ts +5 -1
  86. package/package.json +5 -3
@@ -0,0 +1,591 @@
1
+ import { DB_GENERATED_NAMES } from "prostgles-server/dist/DBSchemaBuilder/constants";
2
+ import { strict as assert } from "node:assert";
3
+ import { randomUUID } from "node:crypto";
4
+ import { once } from "node:events";
5
+ import { createServer } from "node:http";
6
+ import path from "node:path";
7
+ import { readFileSync } from "node:fs";
8
+ import { test } from "node:test";
9
+ import prostgles, {
10
+ type DBHandlerServer,
11
+ defineFunction,
12
+ getLocalStorageClient,
13
+ type ProstglesInitOptions,
14
+ type PublishContextValue,
15
+ } from "prostgles-server";
16
+ import type { DB } from "prostgles-server/dist/Prostgles";
17
+ import { getConnectionDetails } from "prostgles-server/dist/DboBuilder/runSql/getAdminClient";
18
+ import { CHANNELS, type ClientSchema } from "prostgles-types";
19
+ import { Server } from "socket.io";
20
+ import { io as createClient, type Socket } from "socket.io-client";
21
+ import ts from "typescript";
22
+ import express from "express";
23
+
24
+ export const testClientSchemaTypes = async (db: DB) => {
25
+ await test(
26
+ "client publish profiles compile at startup and follow user types",
27
+ { timeout: 30000 },
28
+ async () => {
29
+ const http = createServer();
30
+ const io = new Server(http);
31
+ const sockets: Socket[] = [];
32
+ let instance: Awaited<ReturnType<typeof prostgles>> | undefined;
33
+ const suffix = randomUUID().slice(0, 8);
34
+ const schemaName = `client_schema_types_${suffix}`;
35
+ const tableName = "correspondence";
36
+ const privateTableName = "private_table";
37
+ const fileTableName = "files";
38
+ const generatedTypesDir = path.resolve("debug/client-schema-types", suffix);
39
+ let readAuthFromDatabase = false;
40
+ try {
41
+ await db.none(`
42
+ CREATE SCHEMA ${schemaName};
43
+ CREATE TABLE ${schemaName}.${tableName} (
44
+ id SERIAL PRIMARY KEY,
45
+ body TEXT NOT NULL,
46
+ created_by TEXT NOT NULL,
47
+ note TEXT,
48
+ internal TEXT NOT NULL DEFAULT 'default',
49
+ synced BIGINT NOT NULL
50
+ );
51
+ CREATE TABLE ${schemaName}.${privateTableName} (id SERIAL PRIMARY KEY);
52
+ `);
53
+ http.listen(0, "127.0.0.1");
54
+ await once(http, "listening");
55
+ const address = http.address();
56
+ assert(address && typeof address === "object");
57
+ const userIdContextValue = {
58
+ $prostglesContext: { objectName: "user", objectPropertyName: "id" },
59
+ } as const satisfies PublishContextValue<{ user: { id: string } }>;
60
+ instance = await prostgles({
61
+ dbConnection: {
62
+ ...getConnectionDetails(db),
63
+ max: 2,
64
+ connectionTimeoutMillis: 1000,
65
+ options: `-c search_path=${schemaName}`,
66
+ } as unknown as ProstglesInitOptions["dbConnection"],
67
+ tsGeneratedTypesDir: generatedTypesDir,
68
+ transactions: true,
69
+ schemaFilter: { [schemaName]: 1 },
70
+ tableConfig: {
71
+ [tableName]: {
72
+ syncConfig: { id_fields: ["id"], synced_field: "synced" },
73
+ },
74
+ },
75
+ fileTable: {
76
+ tableName: fileTableName,
77
+ expressApp: express(),
78
+ storageClient: getLocalStorageClient({
79
+ localFolderPath: path.join(generatedTypesDir, "files"),
80
+ }),
81
+ },
82
+ io,
83
+ auth: {
84
+ sidKeyName: "token",
85
+ getUser: async (sid, _dbo, authDb) => {
86
+ if (!sid) return undefined;
87
+ if (readAuthFromDatabase) await authDb.one("SELECT 1");
88
+ return {
89
+ user: { id: `user-${sid}`, type: sid },
90
+ clientUser: { id: sid, type: sid },
91
+ };
92
+ },
93
+ findUser: async (filter) =>
94
+ "$and" in filter && filter.$and?.every((condition) => condition.id === "user-guest") ?
95
+ { id: "user-guest", type: "guest" }
96
+ : undefined,
97
+ },
98
+ functions: {
99
+ guest: {
100
+ userFilter: { id: "user-guest" },
101
+ functions: {
102
+ transactionTest: defineFunction({
103
+ input: { fail: "boolean" },
104
+ run: ({ fail }, { withClientDbTx }) =>
105
+ withClientDbTx(async (tx) => {
106
+ const row = await tx[tableName]!.insert!(
107
+ { body: fail ? "function-rollback" : "function-commit" },
108
+ { returning: "*" },
109
+ );
110
+ if (fail) {
111
+ await tx[tableName]!.update!({ id: row.id }, { internal: "forbidden" });
112
+ }
113
+ return row;
114
+ }),
115
+ }),
116
+ },
117
+ },
118
+ },
119
+ publish: [
120
+ {
121
+ name: "GuestDBSchema",
122
+ userTypes: ["guest", "member"],
123
+ publish: {
124
+ [fileTableName]: { select: "*", insert: "*", update: "*" },
125
+ [tableName]: {
126
+ select: {
127
+ fields: "*",
128
+ forcedFilter: {
129
+ created_by: userIdContextValue as unknown as string,
130
+ },
131
+ },
132
+ update: {
133
+ fields: ["body", "note", "created_by", "synced"],
134
+ disableMethods: { updateBatch: 1 },
135
+ forcedData: {
136
+ created_by: userIdContextValue as unknown as string,
137
+ },
138
+ },
139
+ insert: {
140
+ fields: { internal: 0 },
141
+ checkFilter: { body: { $ne: "review-denied" } },
142
+ forcedData: {
143
+ created_by: userIdContextValue as unknown as string,
144
+ },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ {
150
+ name: "AdminDBSchema",
151
+ userTypes: ["admin"],
152
+ publish: {
153
+ [tableName]: { select: "*", insert: "*", update: "*" },
154
+ [privateTableName]: { select: "*" },
155
+ },
156
+ },
157
+ {
158
+ name: "ViewerDBSchema",
159
+ userTypes: ["viewer"],
160
+ publish: { [tableName]: { select: "*" } },
161
+ },
162
+ ],
163
+ onReady: () => {},
164
+ });
165
+ const { tsSchema } = await instance.getTSSchema();
166
+ assert.doesNotMatch(tsSchema, /\bimport(?:\s|\()/);
167
+ assert.equal(
168
+ readFileSync(path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`), "utf8"),
169
+ tsSchema,
170
+ );
171
+ await instance.reWriteDBSchema();
172
+ assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
173
+ assert.equal(io.sockets.sockets.size, 0);
174
+ checkTypes(
175
+ tsSchema,
176
+ `
177
+ import type { TableHandler, InsertDataWithNested, ClientSchemaFor } from "prostgles-types";
178
+ import type { DBHandlerClient } from "prostgles-client";
179
+ import type { DBOFullyTypedClient, InitResult, SessionUser } from "../server/node_modules/prostgles-server";
180
+ import type { RestrictedFunctionContext, UnrestrictedFunctionContext } from "../server/node_modules/prostgles-server/dist/PublishParser/defineServerFunction";
181
+ import type { getClientHandlers } from "../server/node_modules/prostgles-server/dist/WebsocketAPI/getClientHandlers";
182
+ type Name = "${tableName}";
183
+ type FileName = "${fileTableName}";
184
+ declare const guest: TableHandler<GuestDBSchema, Name>;
185
+ declare const files: TableHandler<GuestDBSchema, FileName>;
186
+ declare const admin: TableHandler<AdminDBSchema, Name>;
187
+ declare const combined: TableHandler<ClientDBSchema, Name>;
188
+ declare const server: TableHandler<${DB_GENERATED_NAMES.SCHEMA}, Name>;
189
+ declare const viewer: TableHandler<ViewerDBSchema, Name>;
190
+ declare const client: DBHandlerClient<ClientDBSchema>;
191
+ declare const adminClient: DBHandlerClient<AdminDBSchema>;
192
+ declare const restricted: RestrictedFunctionContext<GuestDBSchema>;
193
+ declare const unrestricted: UnrestrictedFunctionContext<DBGeneratedSchema>;
194
+ declare const serverClient: DBOFullyTypedClient<ClientDBSchema>;
195
+ declare const handlers: Awaited<ReturnType<typeof getClientHandlers<GuestDBSchema>>>;
196
+ declare const instance: InitResult<DBGeneratedSchema, SessionUser, undefined, ClientDBSchema>;
197
+ declare const guestClientSchema: ClientSchemaFor<ClientSchemas, "guest">;
198
+ guestClientSchema satisfies GuestDBSchema;
199
+ async () => {
200
+ const combinedHandlers = await instance.getClientDBHandlers(null as never, undefined);
201
+ const guestHandlers = await instance.getClientDBHandlers<GuestDBSchema>(null as never, undefined);
202
+ await combinedHandlers.clientDb["${privateTableName}"]?.find();
203
+ // @ts-expect-error the guest profile does not publish private_table
204
+ await guestHandlers.clientDb["${privateTableName}"].find();
205
+ const restrictedRow = await restricted.dbo["${tableName}"].insert({ body: "hello" }, { returning: "*" });
206
+ restrictedRow.created_by satisfies string;
207
+ await handlers.clientDb["${tableName}"].update({}, { body: "changed" });
208
+ const transactionResult = await handlers.withClientDbTx(async (tx) => {
209
+ const row = await tx["${tableName}"].insert({ body: "transaction" }, { returning: "*" });
210
+ row.created_by satisfies string;
211
+ // @ts-expect-error transactions preserve profile field restrictions
212
+ await tx["${tableName}"].update({}, { internal: "private" });
213
+ // @ts-expect-error transactions do not expose unpublished tables
214
+ await tx["${privateTableName}"].find();
215
+ // @ts-expect-error transactions do not expose raw SQL
216
+ tx.sql("SELECT 1");
217
+ // @ts-expect-error transactions do not expose nested transactions
218
+ tx.tx(() => {});
219
+ return row.id;
220
+ });
221
+ transactionResult satisfies number;
222
+ restricted.withClientDbTx satisfies typeof handlers.withClientDbTx;
223
+ // @ts-expect-error callbacks do not receive the unrestricted SQL transaction
224
+ handlers.withClientDbTx((tx, rawTx) => rawTx.any("SELECT 1"));
225
+ // @ts-expect-error getClientHandlers preserves profile field restrictions
226
+ await handlers.clientDb["${tableName}"].update({}, { internal: "private" });
227
+ // @ts-expect-error restricted functions respect forced insert fields
228
+ await restricted.dbo["${tableName}"].insert({ body: "hello", created_by: "other" });
229
+ // @ts-expect-error restricted functions cannot start transactions
230
+ restricted.dbo.tx(() => {});
231
+ // @ts-expect-error client wrappers do not expose isView
232
+ restricted.dbo["${tableName}"].isView;
233
+ // @ts-expect-error table is absent for some profiles
234
+ await serverClient["${privateTableName}"].find();
235
+ await serverClient["${privateTableName}"]?.find();
236
+ await unrestricted.dbo.tx(async (tx) => {
237
+ await tx["${tableName}"].update({}, { internal: "private" });
238
+ });
239
+
240
+ await client["${tableName}"].update({}, { body: "changed" });
241
+ await guest.update({}, { body: 123 });
242
+ // @ts-expect-error table is absent for guests and viewers
243
+ await client["${privateTableName}"].find();
244
+ await client["${privateTableName}"]?.find();
245
+ await adminClient["${privateTableName}"].find();
246
+ const updated = await guest.update({}, { body: "changed", note: null }, { returning: "*" });
247
+ updated?.[0]?.created_by satisfies string | undefined;
248
+ await guest.updateBatch([[{}, { body: "changed" }]]);
249
+ // @ts-expect-error excluded update field
250
+ await guest.update({}, { internal: "private" });
251
+ const forcedUpdate = { body: "changed", created_by: "other" };
252
+ // @ts-expect-error forced update field through a variable
253
+ await guest.update({}, forcedUpdate);
254
+ // @ts-expect-error casts must not bypass excluded fields
255
+ await guest.update({}, { created_by: { $merge: [] } });
256
+ // @ts-expect-error updateBatch respects allowed fields
257
+ await guest.updateBatch([[{}, { internal: "private" }]]);
258
+ // @ts-expect-error read-only profiles cannot update
259
+ await viewer.update({}, {});
260
+ await admin.update({}, { internal: "private" });
261
+ await combined.update({}, { internal: "private" });
262
+ await server.update({}, { internal: "private" });
263
+ const row = await guest.insert({ body: "hello", note: null }, { returning: "*" });
264
+ row.created_by satisfies string;
265
+ row.synced satisfies string;
266
+ await guest.insertMany([{ body: "hello" }]);
267
+ // @ts-expect-error excluded fields cannot be inserted
268
+ await guest.insert({ body: "hello", internal: "private" });
269
+ // @ts-expect-error a read-only profile cannot insert
270
+ await viewer.insert({ body: "hello" });
271
+ // @ts-expect-error body remains required
272
+ await guest.insert({});
273
+ // @ts-expect-error forced values are forbidden
274
+ await guest.insert({ body: "hello", created_by: "other" });
275
+ const supplied = { body: "hello", created_by: "other" };
276
+ // @ts-expect-error also reject forced fields through variables
277
+ await guest.insert(supplied);
278
+ await admin.insert({ body: "hello", created_by: "admin" });
279
+ // @ts-expect-error admin must supply created_by
280
+ await admin.insert({ body: "hello" });
281
+ await combined.insert({ body: "hello" });
282
+ await combined.insert({ body: "hello", created_by: "admin" });
283
+ // @ts-expect-error read-only profiles do not relax required insert fields
284
+ await combined.insert({});
285
+ // @ts-expect-error server insert defaults are unchanged
286
+ await server.insert({ body: "hello", created_by: "admin" });
287
+ await server.insert({ body: "hello", created_by: "admin", synced: 1 });
288
+ await files.insert({ original_name: "hello.txt", data: new Uint8Array() });
289
+ await files.insert({ id: "file-id", original_name: "hello.txt", original_last_modified: null, data: new Uint8Array() });
290
+ // @ts-expect-error file metadata is generated by the server
291
+ await files.insert({ original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" });
292
+ const fileWithGeneratedMetadata = { original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" };
293
+ // @ts-expect-error file metadata is also rejected through variables
294
+ await files.insert(fileWithGeneratedMetadata);
295
+ // @ts-expect-error original_name is required
296
+ await files.insert({ data: new Uint8Array() });
297
+ await files.update({}, { original_name: "updated.txt", data: new Uint8Array() });
298
+ // @ts-expect-error file metadata cannot be updated directly
299
+ await files.update({}, { content_type: "text/plain" });
300
+ // @ts-expect-error file metadata updates are also rejected through variables
301
+ await files.update({}, fileWithGeneratedMetadata);
302
+ // @ts-expect-error unpublished tables are absent
303
+ type Private = GuestDBSchema["${privateTableName}"];
304
+ type Nested = InsertDataWithNested<{}, GuestDBSchema, "parent">;
305
+ const nested: Nested = { "${tableName}": [{ body: "hello" }] };
306
+ // @ts-expect-error nested inputs also exclude forced fields
307
+ const badNested: Nested = { "${tableName}": [{ created_by: "other" }] };
308
+ };
309
+ `,
310
+ );
311
+ const originalPublish = instance.options.publish;
312
+ // await instance.update({ publish: originalPublish });
313
+ const profiles: Record<string, ClientSchema> = {};
314
+ for (const [role, name] of [
315
+ ["guest", "GuestDBSchema"],
316
+ ["admin", "AdminDBSchema"],
317
+ ["viewer", "ViewerDBSchema"],
318
+ ["member", "MemberDBSchema"],
319
+ ["unknown", "UnknownDBSchema"],
320
+ ["", "AnonymousDBSchema"],
321
+ ]) {
322
+ const socket = createClient(`http://127.0.0.1:${address.port}`, {
323
+ transports: ["websocket"],
324
+ reconnection: false,
325
+ query: role ? { token: role } : {},
326
+ });
327
+ sockets.push(socket);
328
+ profiles[name] = await new Promise<ClientSchema>((resolve, reject) => {
329
+ socket.once("connect_error", reject);
330
+ socket.once(CHANNELS.SCHEMA, resolve);
331
+ });
332
+ }
333
+ const guestSchema = profiles.GuestDBSchema!;
334
+ assert.equal(guestSchema.err, undefined);
335
+ const table = guestSchema.tableSchema.find((t) => t.name === tableName);
336
+ assert(table);
337
+ assert.equal(table.columns.find((c) => c.name === "created_by")?.insert, false);
338
+ assert.deepEqual(profiles.MemberDBSchema!.tableSchema, guestSchema.tableSchema);
339
+ assert.deepEqual(profiles.UnknownDBSchema!.tableSchema, []);
340
+ assert.deepEqual(profiles.AnonymousDBSchema!.tableSchema, []);
341
+ const serverSocket = io.sockets.sockets.get(sockets[0]!.id!);
342
+ assert(serverSocket);
343
+ const handlers = await instance.getClientDBHandlers({ socket: serverSocket }, undefined);
344
+ assert.equal(privateTableName in handlers.clientDb, false);
345
+ assert.equal("delete" in handlers.clientDb[tableName]!, false);
346
+ assert.equal(typeof handlers.clientDb[tableName]!.upsert, "function");
347
+ assert.equal(typeof handlers.clientDb[tableName]!.insertMany, "function");
348
+ assert.equal("updateBatch" in handlers.clientDb[tableName]!, false);
349
+ const viewerSocket = io.sockets.sockets.get(sockets[2]!.id!);
350
+ assert(viewerSocket);
351
+ const viewerHandlers = await instance.getClientDBHandlers(
352
+ { socket: viewerSocket },
353
+ undefined,
354
+ );
355
+ assert.deepEqual(Object.keys(viewerHandlers.clientDb).sort(), [tableName, "tx"]);
356
+ assert.deepEqual(Object.keys(viewerHandlers.clientDb[tableName]!).sort(), [
357
+ "count",
358
+ "find",
359
+ "findOne",
360
+ "getColumns",
361
+ "getInfo",
362
+ "size",
363
+ "subscribe",
364
+ "subscribeOne",
365
+ ]);
366
+ const inserted = await (handlers.clientDb as DBHandlerServer)[tableName]!.insert!(
367
+ { body: "hello" },
368
+ { returning: "*" },
369
+ );
370
+ assert.equal(inserted.created_by, "user-guest");
371
+ assert(Number(inserted.synced) > 0);
372
+ const updated = await handlers.clientDb[tableName]!.update!(
373
+ { id: inserted.id },
374
+ { body: "updated" },
375
+ { returning: "*" },
376
+ );
377
+ assert.equal(updated![0].body, "updated");
378
+ assert.equal(updated![0].created_by, "user-guest");
379
+ assert.equal(
380
+ (
381
+ await (handlers.clientDb as DBHandlerServer)[tableName]!.findOne!({
382
+ id: inserted.id,
383
+ })
384
+ ).created_by,
385
+ "user-guest",
386
+ );
387
+ await assert.rejects(
388
+ (handlers.clientDb as DBHandlerServer)[tableName]!.update!(
389
+ { id: inserted.id },
390
+ { internal: "forbidden" },
391
+ ),
392
+ );
393
+ const transactionRow = await handlers.withClientDbTx(async (...args) => {
394
+ assert.equal(args.length, 1);
395
+ const [tx] = args;
396
+ assert.equal("sql" in tx, false);
397
+ assert.throws(
398
+ () => (tx as DBHandlerServer).tx!(() => assert.fail("Nested transaction callback ran")),
399
+ /Transactions are not allowed in client handlers/,
400
+ );
401
+ const row = await tx[tableName]!.insert!(
402
+ { body: "transaction", created_by: "other" },
403
+ { returning: "*" },
404
+ );
405
+ assert.equal(row.created_by, "user-guest");
406
+ assert.equal(await tx[tableName]!.count!({ id: row.id }), 1);
407
+ assert.equal(await handlers.clientDb[tableName]!.count!({ id: row.id }), 0);
408
+ await tx[tableName]!.update!({ id: row.id }, { body: "committed" });
409
+ return tx[tableName]!.findOne!({ id: row.id });
410
+ });
411
+ assert.equal(transactionRow!.body, "committed");
412
+ assert.equal(await handlers.clientDb[tableName]!.count!({ id: transactionRow!.id }), 1);
413
+ await instance.db[tableName]!.insert!({ body: "hidden", created_by: "other" });
414
+ await handlers.withClientDbTx(async (tx) => {
415
+ assert.equal(await tx[tableName]!.count!({ body: "hidden" }), 0);
416
+ assert.equal(privateTableName in tx, false);
417
+ assert.equal("delete" in tx[tableName]!, false);
418
+ await assert.rejects(tx[tableName]!.subscribe!({}, {}, undefined));
419
+ assert.equal("db" in tx[tableName]!, false);
420
+ assert.equal("tx" in tx[tableName]!, false);
421
+ });
422
+ for (const failure of ["throw", "permission", "database"] as const) {
423
+ const body = `rollback-${failure}`;
424
+ await assert.rejects(
425
+ handlers.withClientDbTx(async (tx) => {
426
+ await tx[tableName]!.insert!({ body });
427
+ if (failure === "permission") {
428
+ await tx[tableName]!.update!({}, { internal: "forbidden" });
429
+ } else if (failure === "database") {
430
+ await tx[tableName]!.insert!({ id: transactionRow!.id, body });
431
+ } else {
432
+ throw new Error("rollback");
433
+ }
434
+ }),
435
+ );
436
+ assert.equal(await handlers.clientDb[tableName]!.count!({ body }), 0);
437
+ }
438
+ const scopedHandlers = await instance.getClientDBHandlers(
439
+ { socket: serverSocket },
440
+ { tables: { [tableName]: { select: { forcedFilter: { body: "committed" } } } } },
441
+ );
442
+ assert.deepEqual(Object.keys(scopedHandlers.clientDb).sort(), [tableName, "tx"]);
443
+ assert.equal("insert" in scopedHandlers.clientDb[tableName]!, false);
444
+ assert.equal("update" in scopedHandlers.clientDb[tableName]!, false);
445
+ assert.equal("upsert" in scopedHandlers.clientDb[tableName]!, false);
446
+ await scopedHandlers.withClientDbTx(async (tx) => {
447
+ assert.equal(await tx[tableName]!.count!(), 1);
448
+ assert.deepEqual(Object.keys(tx).sort(), Object.keys(scopedHandlers.clientDb).sort());
449
+ assert.deepEqual(
450
+ Object.keys(tx[tableName]!),
451
+ Object.keys(scopedHandlers.clientDb[tableName]!),
452
+ );
453
+ });
454
+ const functionRow = await handlers.clientMethods.transactionTest!.run({ fail: false });
455
+ const committedRow = await handlers.clientDb[tableName]!.findOne!({
456
+ body: "function-commit",
457
+ });
458
+ assert.equal(committedRow!.created_by, "user-guest");
459
+ assert.deepEqual(functionRow, committedRow);
460
+ await assert.rejects(async () =>
461
+ handlers.clientMethods.transactionTest!.run({ fail: true }),
462
+ );
463
+ assert.equal(await handlers.clientDb[tableName]!.count!({ body: "function-rollback" }), 0);
464
+ await assert.rejects(handlers.clientDb[tableName]!.insert!({ body: "review-denied" }));
465
+ assert.equal(await handlers.clientDb[tableName]!.count!({ body: "review-denied" }), 0);
466
+ await handlers.withClientDbTx(async (tx) => {
467
+ await assert.rejects(tx[tableName]!.insert!({ body: "review-denied" }));
468
+ });
469
+ const rejectedRowsCommitted = await handlers.clientDb[tableName]!.count!({
470
+ body: "review-denied",
471
+ });
472
+ readAuthFromDatabase = true;
473
+ await handlers.clientDb[tableName]!.count!();
474
+ let started = 0;
475
+ let release!: () => void;
476
+ const bothStarted = new Promise<void>((resolve) => {
477
+ release = resolve;
478
+ });
479
+ const concurrentResults = await Promise.allSettled(
480
+ [0, 1].map(() =>
481
+ handlers.withClientDbTx(async (tx) => {
482
+ if (++started === 2) release();
483
+ await bothStarted;
484
+ return tx[tableName]!.count!();
485
+ }),
486
+ ),
487
+ );
488
+ readAuthFromDatabase = false;
489
+ assert.deepEqual(
490
+ {
491
+ rejectedRowsCommitted,
492
+ concurrentResults: concurrentResults.map((r) =>
493
+ r.status === "fulfilled" ? r.status : r.reason,
494
+ ),
495
+ },
496
+ { rejectedRowsCommitted: 0, concurrentResults: ["fulfilled", "fulfilled"] },
497
+ "Rejected rows must roll back and concurrent authenticated transactions must complete",
498
+ );
499
+ sockets.forEach((socket) => socket.disconnect());
500
+ io.disconnectSockets(true);
501
+ await instance.update({ publish: originalPublish });
502
+ await instance.update({
503
+ publish: [{ name: "EmptyDBSchema", userTypes: ["empty"], publish: null }],
504
+ });
505
+ assert(
506
+ (await instance.getTSSchema()).tsSchema.includes(
507
+ "export type EmptyDBSchema = Record<string, never>;",
508
+ ),
509
+ );
510
+ await instance.update({ publish: originalPublish });
511
+ for (const [publish, error] of [
512
+ [[{ userTypes: [], publish: null }], /userTypes must not be empty/],
513
+ [
514
+ [
515
+ { userTypes: ["guest"], publish: null },
516
+ { userTypes: ["guest"], publish: null },
517
+ ],
518
+ /Duplicate publish user type/,
519
+ ],
520
+ [
521
+ [{ name: "ClientDBSchema", userTypes: ["guest"], publish: null }],
522
+ /Invalid client schema name/,
523
+ ],
524
+ [
525
+ [{ name: "ClientSchemas", userTypes: ["guest"], publish: null }],
526
+ /Invalid client schema name/,
527
+ ],
528
+ [
529
+ [
530
+ { name: "SameSchema", userTypes: ["a"], publish: null },
531
+ { name: "SameSchema", userTypes: ["b"], publish: null },
532
+ ],
533
+ /Duplicate publish schema name/,
534
+ ],
535
+ ] as const) {
536
+ // await instance.update({ publish: publish });
537
+ // await assert.rejects((handlers.clientDb as DBHandlerServer)[tableName]!.find!(), error);
538
+ // await instance.update({ publish: originalPublish });
539
+ await assert.rejects(instance.update({ publish }), error);
540
+ assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
541
+ }
542
+ await instance.update({
543
+ publish: [{ userTypes: ["guest"], publish: { [tableName]: { select: "*" } } }],
544
+ });
545
+ const updatedSchema = (await instance.getTSSchema()).tsSchema;
546
+ assert(updatedSchema.includes("export type Publish1Schema"));
547
+ assert(!updatedSchema.includes("GuestDBSchema"));
548
+ assert.equal(
549
+ readFileSync(path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`), "utf8"),
550
+ updatedSchema,
551
+ );
552
+ await instance.update({ publish: originalPublish });
553
+ assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
554
+ } finally {
555
+ sockets.forEach((socket) => socket.disconnect());
556
+ await instance?.destroy();
557
+ await new Promise<void>((resolve) => io.close(() => resolve()));
558
+ await db.none(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
559
+ }
560
+ },
561
+ );
562
+ };
563
+
564
+ const checkTypes = (schema: string, checks: string) => {
565
+ const filename = path.resolve(__dirname, "../../../client/client-schema-typecheck.ts");
566
+ const options: ts.CompilerOptions = {
567
+ strict: true,
568
+ noEmit: true,
569
+ skipLibCheck: true,
570
+ target: ts.ScriptTarget.ES2022,
571
+ module: ts.ModuleKind.CommonJS,
572
+ moduleResolution: ts.ModuleResolutionKind.Node10,
573
+ };
574
+ const host = ts.createCompilerHost(options);
575
+ const getSourceFile = host.getSourceFile;
576
+ host.getSourceFile = (name, languageVersion, onError, shouldCreateNewSourceFile) =>
577
+ name === filename ?
578
+ ts.createSourceFile(name, schema + checks, languageVersion, true)
579
+ : getSourceFile(name, languageVersion, onError, shouldCreateNewSourceFile);
580
+ const program = ts.createProgram([filename], options, host);
581
+ const diagnostics = ts.getPreEmitDiagnostics(program);
582
+ assert.equal(
583
+ diagnostics.length,
584
+ 0,
585
+ ts.formatDiagnosticsWithColorAndContext(diagnostics, {
586
+ getCanonicalFileName: (name) => name,
587
+ getCurrentDirectory: () => process.cwd(),
588
+ getNewLine: () => "\n",
589
+ }),
590
+ );
591
+ };
File without changes