@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +5 -5
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
import { describe, it, expect, beforeEach } from
|
|
3
|
-
import { PostgresBackendDriver } from
|
|
4
|
-
import { RealtimeService } from
|
|
5
|
-
import { EntityService } from
|
|
6
|
-
import { NodePgDatabase } from
|
|
7
|
-
import { sql } from
|
|
8
|
-
import type { EntityCollection, Entity } from
|
|
2
|
+
import { describe, it, expect, beforeEach } from "@jest/globals";
|
|
3
|
+
import { PostgresBackendDriver } from "../src/PostgresBackendDriver";
|
|
4
|
+
import { RealtimeService } from "../src/services/realtimeService";
|
|
5
|
+
import { EntityService } from "../src/services/entityService";
|
|
6
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
|
+
import { sql } from "drizzle-orm";
|
|
8
|
+
import type { EntityCollection, Entity } from "@rebasepro/types";
|
|
9
9
|
|
|
10
10
|
type MockTx = { execute: jest.Mock };
|
|
11
11
|
type MockUser = { uid: string; email?: string; roles?: unknown[] };
|
|
@@ -18,7 +18,7 @@ const mockDb = {
|
|
|
18
18
|
from: jest.fn().mockReturnThis(),
|
|
19
19
|
limit: jest.fn().mockReturnThis(),
|
|
20
20
|
where: jest.fn().mockReturnThis(),
|
|
21
|
-
orderBy: jest.fn().mockReturnThis()
|
|
21
|
+
orderBy: jest.fn().mockReturnThis()
|
|
22
22
|
} as unknown as NodePgDatabase;
|
|
23
23
|
|
|
24
24
|
const mockRealtimeService = {
|
|
@@ -26,11 +26,11 @@ const mockRealtimeService = {
|
|
|
26
26
|
addSubscriptionCallback: jest.fn(),
|
|
27
27
|
removeSubscriptionCallback: jest.fn(),
|
|
28
28
|
subscriptions: new Map(),
|
|
29
|
-
notifyEntityUpdate: jest.fn()
|
|
29
|
+
notifyEntityUpdate: jest.fn()
|
|
30
30
|
} as unknown as RealtimeService;
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
describe(
|
|
33
|
+
describe("PostgresBackendDriver", () => {
|
|
34
34
|
let delegate: PostgresBackendDriver;
|
|
35
35
|
|
|
36
36
|
beforeEach(() => {
|
|
@@ -38,22 +38,24 @@ describe('PostgresBackendDriver', () => {
|
|
|
38
38
|
delegate = new PostgresBackendDriver(mockDb, mockRealtimeService);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it(
|
|
41
|
+
it("should initialize correctly", () => {
|
|
42
42
|
expect(delegate).toBeDefined();
|
|
43
|
-
expect(delegate.key).toBe(
|
|
43
|
+
expect(delegate.key).toBe("postgres");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
describe(
|
|
47
|
-
it(
|
|
48
|
-
const user = { uid:
|
|
46
|
+
describe("withAuth", () => {
|
|
47
|
+
it("should return a new delegate instance", async () => {
|
|
48
|
+
const user = { uid: "test-user",
|
|
49
|
+
email: "test@example.com" };
|
|
49
50
|
const authDelegate = await delegate.withAuth(user);
|
|
50
51
|
|
|
51
52
|
expect(authDelegate).toBeDefined();
|
|
52
53
|
expect(authDelegate).not.toBe(delegate);
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
it(
|
|
56
|
-
const user = { uid:
|
|
56
|
+
it("should wrap methods in a transaction", async () => {
|
|
57
|
+
const user = { uid: "test-user",
|
|
58
|
+
email: "test@example.com" };
|
|
57
59
|
const authDelegate = await delegate.withAuth(user);
|
|
58
60
|
|
|
59
61
|
const mockTx = { execute: jest.fn() };
|
|
@@ -61,15 +63,18 @@ describe('PostgresBackendDriver', () => {
|
|
|
61
63
|
return await cb(mockTx);
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
66
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
65
67
|
|
|
66
|
-
await authDelegate.fetchCollection({ path:
|
|
68
|
+
await authDelegate.fetchCollection({ path: "test_collection",
|
|
69
|
+
collection: { slug: "test",
|
|
70
|
+
properties: {} } as unknown as EntityCollection });
|
|
67
71
|
|
|
68
72
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
69
73
|
});
|
|
70
74
|
|
|
71
|
-
it(
|
|
72
|
-
const user = { uid:
|
|
75
|
+
it("should set app.user_id in the transaction for RLS", async () => {
|
|
76
|
+
const user = { uid: "test-user-123",
|
|
77
|
+
email: "test@example.com" };
|
|
73
78
|
const authDelegate = await delegate.withAuth(user);
|
|
74
79
|
|
|
75
80
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -77,9 +82,11 @@ describe('PostgresBackendDriver', () => {
|
|
|
77
82
|
return await cb(mockTx);
|
|
78
83
|
});
|
|
79
84
|
|
|
80
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
85
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
81
86
|
|
|
82
|
-
await authDelegate.fetchCollection({ path:
|
|
87
|
+
await authDelegate.fetchCollection({ path: "test",
|
|
88
|
+
collection: { slug: "test",
|
|
89
|
+
properties: {} } as unknown as EntityCollection });
|
|
83
90
|
|
|
84
91
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
85
92
|
expect(mockTx.execute).toHaveBeenCalled();
|
|
@@ -89,8 +96,10 @@ describe('PostgresBackendDriver', () => {
|
|
|
89
96
|
expect(callString).toContain("test-user-123");
|
|
90
97
|
});
|
|
91
98
|
|
|
92
|
-
it(
|
|
93
|
-
const user: MockUser = { uid:
|
|
99
|
+
it("should set app.user_roles handling array of strings correctly", async () => {
|
|
100
|
+
const user: MockUser = { uid: "test-user-123",
|
|
101
|
+
email: "test@example.com",
|
|
102
|
+
roles: ["admin", "editor"] };
|
|
94
103
|
const authDelegate = await delegate.withAuth(user);
|
|
95
104
|
|
|
96
105
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -98,9 +107,11 @@ describe('PostgresBackendDriver', () => {
|
|
|
98
107
|
return await cb(mockTx);
|
|
99
108
|
});
|
|
100
109
|
|
|
101
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
110
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
102
111
|
|
|
103
|
-
await authDelegate.fetchCollection({ path:
|
|
112
|
+
await authDelegate.fetchCollection({ path: "test",
|
|
113
|
+
collection: { slug: "test",
|
|
114
|
+
properties: {} } as unknown as EntityCollection });
|
|
104
115
|
|
|
105
116
|
expect(mockTx.execute).toHaveBeenCalledTimes(1);
|
|
106
117
|
const sqlCall = mockTx.execute.mock.calls[0][0];
|
|
@@ -109,8 +120,10 @@ describe('PostgresBackendDriver', () => {
|
|
|
109
120
|
expect(callString).toContain("admin,editor");
|
|
110
121
|
});
|
|
111
122
|
|
|
112
|
-
it(
|
|
113
|
-
const user: MockUser = { uid:
|
|
123
|
+
it("should set app.user_roles handling array of objects correctly", async () => {
|
|
124
|
+
const user: MockUser = { uid: "test-user-123",
|
|
125
|
+
email: "test@example.com",
|
|
126
|
+
roles: [{ id: "admin" }, { id: "editor" }] };
|
|
114
127
|
const authDelegate = await delegate.withAuth(user);
|
|
115
128
|
|
|
116
129
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -118,9 +131,11 @@ describe('PostgresBackendDriver', () => {
|
|
|
118
131
|
return await cb(mockTx);
|
|
119
132
|
});
|
|
120
133
|
|
|
121
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
134
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
122
135
|
|
|
123
|
-
await authDelegate.fetchCollection({ path:
|
|
136
|
+
await authDelegate.fetchCollection({ path: "test",
|
|
137
|
+
collection: { slug: "test",
|
|
138
|
+
properties: {} } as unknown as EntityCollection });
|
|
124
139
|
|
|
125
140
|
expect(mockTx.execute).toHaveBeenCalledTimes(1);
|
|
126
141
|
const sqlCall = mockTx.execute.mock.calls[0][0];
|
|
@@ -129,7 +144,7 @@ describe('PostgresBackendDriver', () => {
|
|
|
129
144
|
expect(callString).toContain("admin,editor");
|
|
130
145
|
});
|
|
131
146
|
|
|
132
|
-
it(
|
|
147
|
+
it("should fallback to anonymous and empty roles when missing from user", async () => {
|
|
133
148
|
const user = {} as unknown as MockUser; // Empty user object
|
|
134
149
|
const authDelegate = await delegate.withAuth(user);
|
|
135
150
|
|
|
@@ -139,23 +154,25 @@ describe('PostgresBackendDriver', () => {
|
|
|
139
154
|
});
|
|
140
155
|
|
|
141
156
|
// We mock fetchCollection to just return something and not crash
|
|
142
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
157
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
143
158
|
|
|
144
|
-
await authDelegate.fetchCollection({ path:
|
|
159
|
+
await authDelegate.fetchCollection({ path: "test",
|
|
160
|
+
collection: { slug: "test",
|
|
161
|
+
properties: {} } as unknown as EntityCollection });
|
|
145
162
|
|
|
146
163
|
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
|
|
147
|
-
|
|
164
|
+
|
|
148
165
|
const transactionCallback = (mockDb.transaction as jest.Mock).mock.calls[0][0];
|
|
149
166
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
150
167
|
await transactionCallback(mockTx).catch(() => {});
|
|
151
|
-
|
|
168
|
+
|
|
152
169
|
const sqlCall = mockTx.execute.mock.calls[0][0];
|
|
153
170
|
const callString = JSON.stringify(sqlCall);
|
|
154
171
|
expect(callString).toContain("set_config");
|
|
155
172
|
expect(callString).toContain("anonymous");
|
|
156
173
|
});
|
|
157
174
|
|
|
158
|
-
it(
|
|
175
|
+
it("should gracefully handle completely null or undefined user objects", async () => {
|
|
159
176
|
const authDelegate = await delegate.withAuth(null as unknown as MockUser);
|
|
160
177
|
|
|
161
178
|
(mockDb.transaction as jest.Mock).mockImplementation(async (cb: (tx: MockTx) => Promise<unknown>) => {
|
|
@@ -163,14 +180,16 @@ describe('PostgresBackendDriver', () => {
|
|
|
163
180
|
return cb(mockTx);
|
|
164
181
|
});
|
|
165
182
|
|
|
166
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
183
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
167
184
|
|
|
168
|
-
await authDelegate.fetchCollection({ path:
|
|
185
|
+
await authDelegate.fetchCollection({ path: "test",
|
|
186
|
+
collection: { slug: "test",
|
|
187
|
+
properties: {} } as unknown as EntityCollection });
|
|
169
188
|
|
|
170
189
|
const transactionCallback = (mockDb.transaction as jest.Mock).mock.calls[0][0];
|
|
171
190
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
172
191
|
await transactionCallback(mockTx).catch(() => {});
|
|
173
|
-
|
|
192
|
+
|
|
174
193
|
const sqlCall = mockTx.execute.mock.calls[0][0];
|
|
175
194
|
const callString = JSON.stringify(sqlCall);
|
|
176
195
|
expect(callString).toContain("set_config");
|
|
@@ -178,14 +197,15 @@ describe('PostgresBackendDriver', () => {
|
|
|
178
197
|
});
|
|
179
198
|
});
|
|
180
199
|
|
|
181
|
-
describe(
|
|
200
|
+
describe("AuthenticatedPostgresBackendDriver Transactional Integrity", () => {
|
|
182
201
|
let authDelegate: Awaited<ReturnType<typeof delegate.withAuth>>;
|
|
183
202
|
|
|
184
203
|
beforeEach(async () => {
|
|
185
|
-
authDelegate = await delegate.withAuth({ uid:
|
|
204
|
+
authDelegate = await delegate.withAuth({ uid: "test-user",
|
|
205
|
+
email: "test@example.com" });
|
|
186
206
|
});
|
|
187
207
|
|
|
188
|
-
it(
|
|
208
|
+
it("should execute operation and flush notifications on success", async () => {
|
|
189
209
|
const mockTx = {
|
|
190
210
|
execute: jest.fn()
|
|
191
211
|
};
|
|
@@ -195,25 +215,29 @@ describe('PostgresBackendDriver', () => {
|
|
|
195
215
|
});
|
|
196
216
|
|
|
197
217
|
// Let's pretend the operation queues a notification
|
|
198
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
218
|
+
jest.spyOn(PostgresBackendDriver.prototype, "saveEntity").mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
199
219
|
this._pendingNotifications?.push({
|
|
200
|
-
path:
|
|
201
|
-
entityId:
|
|
202
|
-
entity: {} as unknown as Entity
|
|
220
|
+
path: "test",
|
|
221
|
+
entityId: "123",
|
|
222
|
+
entity: {} as unknown as Entity
|
|
203
223
|
});
|
|
204
224
|
return {} as unknown as Entity;
|
|
205
225
|
});
|
|
206
226
|
|
|
207
|
-
await authDelegate.saveEntity({ path:
|
|
227
|
+
await authDelegate.saveEntity({ path: "test",
|
|
228
|
+
entityId: "123",
|
|
229
|
+
values: {},
|
|
230
|
+
collection: {} as unknown as EntityCollection,
|
|
231
|
+
status: "new" });
|
|
208
232
|
|
|
209
233
|
// Ensure transaction was called
|
|
210
234
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
211
235
|
|
|
212
236
|
// Ensure notification was flushed after commit
|
|
213
|
-
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith(
|
|
237
|
+
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith("test", "123", {}, undefined);
|
|
214
238
|
});
|
|
215
239
|
|
|
216
|
-
it(
|
|
240
|
+
it("should NOT flush notifications if transaction throws an error", async () => {
|
|
217
241
|
const mockTx = {
|
|
218
242
|
execute: jest.fn()
|
|
219
243
|
};
|
|
@@ -222,22 +246,26 @@ describe('PostgresBackendDriver', () => {
|
|
|
222
246
|
return await cb(mockTx);
|
|
223
247
|
});
|
|
224
248
|
|
|
225
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
249
|
+
jest.spyOn(PostgresBackendDriver.prototype, "saveEntity").mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
226
250
|
this._pendingNotifications?.push({
|
|
227
|
-
path:
|
|
228
|
-
entityId:
|
|
229
|
-
entity: {} as unknown as Entity
|
|
251
|
+
path: "test",
|
|
252
|
+
entityId: "123",
|
|
253
|
+
entity: {} as unknown as Entity
|
|
230
254
|
});
|
|
231
255
|
throw new Error("Transaction failed");
|
|
232
256
|
});
|
|
233
257
|
|
|
234
|
-
await expect(authDelegate.saveEntity({ path:
|
|
258
|
+
await expect(authDelegate.saveEntity({ path: "test",
|
|
259
|
+
entityId: "123",
|
|
260
|
+
values: {},
|
|
261
|
+
collection: {} as unknown as EntityCollection,
|
|
262
|
+
status: "new" })).rejects.toThrow("Transaction failed");
|
|
235
263
|
|
|
236
264
|
// Ensure notification was NOT flushed
|
|
237
265
|
expect(mockRealtimeService.notifyEntityUpdate).not.toHaveBeenCalled();
|
|
238
266
|
});
|
|
239
267
|
|
|
240
|
-
it(
|
|
268
|
+
it("should return successfully even if a deferred notification throw an error (resilience)", async () => {
|
|
241
269
|
const mockTx = {
|
|
242
270
|
execute: jest.fn()
|
|
243
271
|
};
|
|
@@ -246,27 +274,31 @@ describe('PostgresBackendDriver', () => {
|
|
|
246
274
|
return await cb(mockTx);
|
|
247
275
|
});
|
|
248
276
|
|
|
249
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
277
|
+
jest.spyOn(PostgresBackendDriver.prototype, "saveEntity").mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
250
278
|
this._pendingNotifications?.push({
|
|
251
|
-
path:
|
|
252
|
-
entityId:
|
|
253
|
-
entity: {} as unknown as Entity
|
|
279
|
+
path: "test",
|
|
280
|
+
entityId: "buggy-123",
|
|
281
|
+
entity: {} as unknown as Entity
|
|
254
282
|
});
|
|
255
|
-
return { id:
|
|
283
|
+
return { id: "success" } as unknown as Entity;
|
|
256
284
|
});
|
|
257
285
|
|
|
258
286
|
// Mock the notification service intentionally crashing
|
|
259
287
|
const notifySpy = mockRealtimeService.notifyEntityUpdate as jest.Mock;
|
|
260
288
|
notifySpy.mockRejectedValueOnce(new Error("Network Failure on Notification"));
|
|
261
|
-
|
|
289
|
+
|
|
262
290
|
// Should still return the entity successfully despite the error
|
|
263
|
-
const result = await authDelegate.saveEntity({ path:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
const result = await authDelegate.saveEntity({ path: "test",
|
|
292
|
+
entityId: "buggy-123",
|
|
293
|
+
values: {},
|
|
294
|
+
collection: {} as unknown as EntityCollection,
|
|
295
|
+
status: "new" });
|
|
296
|
+
|
|
297
|
+
expect(result).toEqual({ id: "success" });
|
|
298
|
+
expect(notifySpy).toHaveBeenCalledWith("test", "buggy-123", {}, undefined);
|
|
267
299
|
});
|
|
268
300
|
|
|
269
|
-
it(
|
|
301
|
+
it("should safely isolate completely concurrent transaction notifications from leaking across scopes", async () => {
|
|
270
302
|
const mockTx = {
|
|
271
303
|
execute: jest.fn()
|
|
272
304
|
};
|
|
@@ -278,143 +310,173 @@ describe('PostgresBackendDriver', () => {
|
|
|
278
310
|
});
|
|
279
311
|
|
|
280
312
|
// Operation 1 flags a notification
|
|
281
|
-
const save1 = jest.spyOn(PostgresBackendDriver.prototype,
|
|
282
|
-
this._pendingNotifications?.push({ path:
|
|
313
|
+
const save1 = jest.spyOn(PostgresBackendDriver.prototype, "saveEntity").mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
314
|
+
this._pendingNotifications?.push({ path: "scope-1",
|
|
315
|
+
entityId: "1",
|
|
316
|
+
entity: {} as unknown as Entity });
|
|
283
317
|
return {} as unknown as Entity;
|
|
284
318
|
});
|
|
285
319
|
|
|
286
320
|
// Operation 2 flags a different notification
|
|
287
|
-
const save2 = jest.spyOn(PostgresBackendDriver.prototype,
|
|
288
|
-
this._pendingNotifications?.push({ path:
|
|
321
|
+
const save2 = jest.spyOn(PostgresBackendDriver.prototype, "saveEntity").mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
322
|
+
this._pendingNotifications?.push({ path: "scope-2",
|
|
323
|
+
entityId: "2",
|
|
324
|
+
entity: {} as unknown as Entity });
|
|
289
325
|
return {} as unknown as Entity;
|
|
290
326
|
});
|
|
291
327
|
|
|
292
328
|
// Fire simultaneously
|
|
293
329
|
await Promise.all([
|
|
294
|
-
authDelegate.saveEntity({ path:
|
|
295
|
-
|
|
330
|
+
authDelegate.saveEntity({ path: "scope-1",
|
|
331
|
+
entityId: "1",
|
|
332
|
+
values: {},
|
|
333
|
+
collection: {} as unknown as EntityCollection,
|
|
334
|
+
status: "new" }),
|
|
335
|
+
authDelegate.saveEntity({ path: "scope-2",
|
|
336
|
+
entityId: "2",
|
|
337
|
+
values: {},
|
|
338
|
+
collection: {} as unknown as EntityCollection,
|
|
339
|
+
status: "new" })
|
|
296
340
|
]);
|
|
297
341
|
|
|
298
342
|
// Ensure our notify was called with both exact combinations, but NOT cross-pollinated
|
|
299
|
-
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith(
|
|
300
|
-
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith(
|
|
301
|
-
|
|
343
|
+
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith("scope-1", "1", {}, undefined);
|
|
344
|
+
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledWith("scope-2", "2", {}, undefined);
|
|
345
|
+
|
|
302
346
|
// Check count
|
|
303
347
|
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledTimes(2);
|
|
304
348
|
});
|
|
305
349
|
});
|
|
306
350
|
|
|
307
|
-
describe(
|
|
351
|
+
describe("AuthenticatedPostgresBackendDriver Delegation", () => {
|
|
308
352
|
let authDelegate: Awaited<ReturnType<typeof delegate.withAuth>>;
|
|
309
353
|
|
|
310
354
|
beforeEach(async () => {
|
|
311
|
-
authDelegate = await delegate.withAuth({ uid:
|
|
355
|
+
authDelegate = await delegate.withAuth({ uid: "test-user",
|
|
356
|
+
email: "test@example.com" });
|
|
312
357
|
});
|
|
313
358
|
|
|
314
|
-
it(
|
|
315
|
-
jest.spyOn(delegate,
|
|
359
|
+
it("should delegate executeSql via admin without a transaction", async () => {
|
|
360
|
+
jest.spyOn(delegate, "executeSql").mockResolvedValueOnce([{ id: 1 }] as unknown as Record<string, unknown>[]);
|
|
316
361
|
|
|
317
|
-
const result = await authDelegate.executeSql("SELECT 1");
|
|
362
|
+
const result = await authDelegate.admin.executeSql("SELECT 1");
|
|
318
363
|
|
|
319
364
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
320
|
-
expect(delegate.executeSql).toHaveBeenCalledWith("SELECT 1"
|
|
365
|
+
expect(delegate.executeSql).toHaveBeenCalledWith("SELECT 1");
|
|
321
366
|
expect(result).toEqual([{ id: 1 }]);
|
|
322
367
|
});
|
|
323
368
|
|
|
324
|
-
it(
|
|
369
|
+
it("should override listenCollection to inject auth context", () => {
|
|
325
370
|
const mockUnsubscribe = jest.fn();
|
|
326
|
-
|
|
371
|
+
|
|
327
372
|
// Clear original map instead of reassigning
|
|
328
373
|
mockRealtimeService.subscriptions.clear();
|
|
329
|
-
|
|
374
|
+
|
|
330
375
|
// The act of calling the delegated method should update the last subscription
|
|
331
|
-
jest.spyOn(delegate,
|
|
332
|
-
mockRealtimeService.subscriptions.set(
|
|
376
|
+
jest.spyOn(delegate, "listenCollection").mockImplementationOnce(() => {
|
|
377
|
+
mockRealtimeService.subscriptions.set("sub1", { clientId: "driver",
|
|
378
|
+
authContext: undefined });
|
|
333
379
|
return mockUnsubscribe;
|
|
334
380
|
});
|
|
335
381
|
|
|
336
|
-
const unsub = authDelegate.listenCollection({ path:
|
|
382
|
+
const unsub = authDelegate.listenCollection({ path: "test",
|
|
383
|
+
collection: {} as unknown as EntityCollection,
|
|
384
|
+
callbacks: {} as unknown as Record<string, unknown> });
|
|
337
385
|
|
|
338
386
|
expect(unsub).toBe(mockUnsubscribe);
|
|
339
|
-
expect(mockRealtimeService.subscriptions.get(
|
|
387
|
+
expect(mockRealtimeService.subscriptions.get("sub1").authContext).toEqual({ userId: "test-user",
|
|
388
|
+
roles: [] });
|
|
340
389
|
});
|
|
341
390
|
|
|
342
|
-
it(
|
|
391
|
+
it("should override listenEntity to inject auth context", () => {
|
|
343
392
|
const mockUnsubscribe = jest.fn();
|
|
344
|
-
|
|
393
|
+
|
|
345
394
|
mockRealtimeService.subscriptions.clear();
|
|
346
|
-
|
|
347
|
-
jest.spyOn(delegate,
|
|
348
|
-
mockRealtimeService.subscriptions.set(
|
|
395
|
+
|
|
396
|
+
jest.spyOn(delegate, "listenEntity").mockImplementationOnce(() => {
|
|
397
|
+
mockRealtimeService.subscriptions.set("sub2", { clientId: "driver",
|
|
398
|
+
authContext: undefined });
|
|
349
399
|
return mockUnsubscribe;
|
|
350
400
|
});
|
|
351
401
|
|
|
352
|
-
const unsub = authDelegate.listenEntity({ path:
|
|
402
|
+
const unsub = authDelegate.listenEntity({ path: "test",
|
|
403
|
+
entityId: "123",
|
|
404
|
+
collection: {} as unknown as EntityCollection,
|
|
405
|
+
callbacks: {} as unknown as Record<string, unknown> });
|
|
353
406
|
|
|
354
407
|
expect(unsub).toBe(mockUnsubscribe);
|
|
355
|
-
expect(mockRealtimeService.subscriptions.get(
|
|
408
|
+
expect(mockRealtimeService.subscriptions.get("sub2").authContext).toEqual({ userId: "test-user",
|
|
409
|
+
roles: [] });
|
|
356
410
|
});
|
|
357
411
|
|
|
358
|
-
it(
|
|
412
|
+
it("should handle listenCollection gracefully if delegate fails to add a subscription", () => {
|
|
359
413
|
const mockUnsubscribe = jest.fn();
|
|
360
414
|
mockRealtimeService.subscriptions.clear();
|
|
361
|
-
jest.spyOn(delegate,
|
|
362
|
-
const unsub = authDelegate.listenCollection({ path:
|
|
415
|
+
jest.spyOn(delegate, "listenCollection").mockImplementationOnce(() => mockUnsubscribe);
|
|
416
|
+
const unsub = authDelegate.listenCollection({ path: "empty-test",
|
|
417
|
+
collection: {} as unknown as EntityCollection,
|
|
418
|
+
callbacks: {} as unknown as Record<string, unknown> });
|
|
363
419
|
expect(unsub).toBe(mockUnsubscribe);
|
|
364
420
|
});
|
|
365
421
|
|
|
366
|
-
it(
|
|
422
|
+
it("should NOT skip authContext injection if subscription has a non-driver clientId", () => {
|
|
367
423
|
const mockUnsubscribe = jest.fn();
|
|
368
424
|
mockRealtimeService.subscriptions.clear();
|
|
369
|
-
jest.spyOn(delegate,
|
|
370
|
-
mockRealtimeService.subscriptions.set(
|
|
425
|
+
jest.spyOn(delegate, "listenCollection").mockImplementationOnce(() => {
|
|
426
|
+
mockRealtimeService.subscriptions.set("sub-ext", { clientId: "external-client",
|
|
427
|
+
authContext: undefined });
|
|
371
428
|
return mockUnsubscribe;
|
|
372
429
|
});
|
|
373
|
-
authDelegate.listenCollection({ path:
|
|
430
|
+
authDelegate.listenCollection({ path: "test",
|
|
431
|
+
collection: {} as unknown as EntityCollection,
|
|
432
|
+
callbacks: {} as unknown as Record<string, unknown> });
|
|
374
433
|
// authContext should NOT be injected because clientId !== 'driver'
|
|
375
|
-
expect(mockRealtimeService.subscriptions.get(
|
|
434
|
+
expect(mockRealtimeService.subscriptions.get("sub-ext").authContext).toBeUndefined();
|
|
376
435
|
});
|
|
377
436
|
|
|
378
|
-
it(
|
|
379
|
-
jest.spyOn(delegate,
|
|
380
|
-
const result = await authDelegate.fetchAvailableDatabases();
|
|
437
|
+
it("should delegate fetchAvailableDatabases via admin without a transaction", async () => {
|
|
438
|
+
jest.spyOn(delegate, "fetchAvailableDatabases").mockResolvedValueOnce(["db1", "db2"]);
|
|
439
|
+
const result = await authDelegate.admin.fetchAvailableDatabases!();
|
|
381
440
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
382
|
-
expect(result).toEqual([
|
|
441
|
+
expect(result).toEqual(["db1", "db2"]);
|
|
383
442
|
});
|
|
384
443
|
|
|
385
|
-
it(
|
|
386
|
-
jest.spyOn(delegate,
|
|
387
|
-
const result = await authDelegate.fetchAvailableRoles();
|
|
444
|
+
it("should delegate fetchAvailableRoles via admin without a transaction", async () => {
|
|
445
|
+
jest.spyOn(delegate, "fetchAvailableRoles").mockResolvedValueOnce(["admin", "viewer"]);
|
|
446
|
+
const result = await authDelegate.admin.fetchAvailableRoles!();
|
|
388
447
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
389
|
-
expect(result).toEqual([
|
|
448
|
+
expect(result).toEqual(["admin", "viewer"]);
|
|
390
449
|
});
|
|
391
450
|
|
|
392
|
-
it(
|
|
393
|
-
jest.spyOn(delegate,
|
|
394
|
-
const result = await authDelegate.fetchCurrentDatabase();
|
|
451
|
+
it("should delegate fetchCurrentDatabase via admin without a transaction", async () => {
|
|
452
|
+
jest.spyOn(delegate, "fetchCurrentDatabase").mockResolvedValueOnce("my_db");
|
|
453
|
+
const result = await authDelegate.admin.fetchCurrentDatabase!();
|
|
395
454
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
396
|
-
expect(result).toBe(
|
|
455
|
+
expect(result).toBe("my_db");
|
|
397
456
|
});
|
|
398
457
|
|
|
399
|
-
it(
|
|
400
|
-
jest.spyOn(delegate,
|
|
401
|
-
const result = await authDelegate.fetchUnmappedTables([
|
|
458
|
+
it("should delegate fetchUnmappedTables via admin without a transaction", async () => {
|
|
459
|
+
jest.spyOn(delegate, "fetchUnmappedTables").mockResolvedValueOnce(["orphan_table"]);
|
|
460
|
+
const result = await authDelegate.admin.fetchUnmappedTables!(["mapped"]);
|
|
402
461
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
403
|
-
expect(result).toEqual([
|
|
462
|
+
expect(result).toEqual(["orphan_table"]);
|
|
404
463
|
});
|
|
405
464
|
|
|
406
|
-
it(
|
|
407
|
-
jest.spyOn(delegate,
|
|
408
|
-
|
|
465
|
+
it("should delegate fetchTableMetadata via admin without a transaction", async () => {
|
|
466
|
+
jest.spyOn(delegate, "fetchTableMetadata").mockResolvedValueOnce([{ name: "id",
|
|
467
|
+
type: "int4" }] as unknown as Record<string, unknown>[]);
|
|
468
|
+
const result = await authDelegate.admin.fetchTableMetadata!("users");
|
|
409
469
|
expect(mockDb.transaction).not.toHaveBeenCalled();
|
|
410
|
-
expect(result).toEqual([{ name:
|
|
470
|
+
expect(result).toEqual([{ name: "id",
|
|
471
|
+
type: "int4" }]);
|
|
411
472
|
});
|
|
412
473
|
});
|
|
413
474
|
|
|
414
|
-
describe(
|
|
415
|
-
it(
|
|
475
|
+
describe("AuthenticatedPostgresBackendDriver Security & Contract", () => {
|
|
476
|
+
it("should use parameterized queries (drizzle sql``) NOT string interpolation for set_config", async () => {
|
|
416
477
|
// A malicious uid should be passed as a parameter, not concatenated
|
|
417
|
-
const maliciousUser: MockUser = { uid: "admin'; DROP TABLE users; --",
|
|
478
|
+
const maliciousUser: MockUser = { uid: "admin'; DROP TABLE users; --",
|
|
479
|
+
email: "hacker@evil.com" };
|
|
418
480
|
const authDelegate = await delegate.withAuth(maliciousUser);
|
|
419
481
|
|
|
420
482
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -422,9 +484,11 @@ describe('PostgresBackendDriver', () => {
|
|
|
422
484
|
return await cb(mockTx);
|
|
423
485
|
});
|
|
424
486
|
|
|
425
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
487
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
426
488
|
|
|
427
|
-
await authDelegate.fetchCollection({ path:
|
|
489
|
+
await authDelegate.fetchCollection({ path: "x",
|
|
490
|
+
collection: { slug: "x",
|
|
491
|
+
properties: {} } as unknown as EntityCollection });
|
|
428
492
|
|
|
429
493
|
// The SQL template tag should have the userId as a parameter value, not embedded in the SQL string
|
|
430
494
|
const sqlObj = mockTx.execute.mock.calls[0][0];
|
|
@@ -433,11 +497,12 @@ describe('PostgresBackendDriver', () => {
|
|
|
433
497
|
// The malicious string should appear as a bound parameter, not as raw SQL
|
|
434
498
|
expect(serialized).toContain("admin'; DROP TABLE users; --");
|
|
435
499
|
// Verify it's using Drizzle's tagged template, which inherently parameterizes
|
|
436
|
-
expect(sqlObj).toHaveProperty(
|
|
500
|
+
expect(sqlObj).toHaveProperty("queryChunks");
|
|
437
501
|
});
|
|
438
502
|
|
|
439
|
-
it(
|
|
440
|
-
const user: MockUser = { uid:
|
|
503
|
+
it("should produce a valid JWT payload in set_config even with exotic roles", async () => {
|
|
504
|
+
const user: MockUser = { uid: "u1",
|
|
505
|
+
roles: ['role"with"quotes', "role,with,commas", "rôle-spécial"] };
|
|
441
506
|
const authDelegate = await delegate.withAuth(user);
|
|
442
507
|
|
|
443
508
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -445,18 +510,21 @@ describe('PostgresBackendDriver', () => {
|
|
|
445
510
|
return await cb(mockTx);
|
|
446
511
|
});
|
|
447
512
|
|
|
448
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
513
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
449
514
|
|
|
450
|
-
await authDelegate.fetchCollection({ path:
|
|
515
|
+
await authDelegate.fetchCollection({ path: "x",
|
|
516
|
+
collection: { slug: "x",
|
|
517
|
+
properties: {} } as unknown as EntityCollection });
|
|
451
518
|
|
|
452
519
|
const serialized = JSON.stringify(mockTx.execute.mock.calls[0][0]);
|
|
453
520
|
// The JWT should be valid JSON.stringify output containing the roles
|
|
454
521
|
expect(serialized).toContain('role\\"with\\"quotes');
|
|
455
|
-
expect(serialized).toContain(
|
|
522
|
+
expect(serialized).toContain("rôle-spécial");
|
|
456
523
|
});
|
|
457
524
|
|
|
458
|
-
it(
|
|
459
|
-
const user: MockUser = { uid:
|
|
525
|
+
it("should handle role objects missing the id field by falling back to String()", async () => {
|
|
526
|
+
const user: MockUser = { uid: "u1",
|
|
527
|
+
roles: [{ name: "viewer" }, 42, null] };
|
|
460
528
|
const authDelegate = await delegate.withAuth(user);
|
|
461
529
|
|
|
462
530
|
const mockTx: MockTx = { execute: jest.fn() };
|
|
@@ -464,92 +532,116 @@ describe('PostgresBackendDriver', () => {
|
|
|
464
532
|
return await cb(mockTx);
|
|
465
533
|
});
|
|
466
534
|
|
|
467
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
535
|
+
jest.spyOn(PostgresBackendDriver.prototype, "fetchCollection").mockResolvedValueOnce([]);
|
|
468
536
|
|
|
469
|
-
await authDelegate.fetchCollection({ path:
|
|
537
|
+
await authDelegate.fetchCollection({ path: "x",
|
|
538
|
+
collection: { slug: "x",
|
|
539
|
+
properties: {} } as unknown as EntityCollection });
|
|
470
540
|
|
|
471
541
|
const serialized = JSON.stringify(mockTx.execute.mock.calls[0][0]);
|
|
472
542
|
// Objects without id → String({name:'viewer'}) = "[object Object]", 42 → "42", null → "null"
|
|
473
|
-
expect(serialized).toContain(
|
|
543
|
+
expect(serialized).toContain("set_config");
|
|
474
544
|
});
|
|
475
545
|
|
|
476
|
-
it(
|
|
546
|
+
it("should wrap deleteEntity in a transaction with RLS", async () => {
|
|
477
547
|
(mockDb.transaction as jest.Mock).mockImplementation(async (cb) => {
|
|
478
548
|
return await cb({ execute: jest.fn() });
|
|
479
549
|
});
|
|
480
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
550
|
+
jest.spyOn(PostgresBackendDriver.prototype, "deleteEntity").mockResolvedValueOnce(undefined);
|
|
481
551
|
|
|
482
|
-
const authDelegate = await delegate.withAuth({ uid:
|
|
483
|
-
|
|
552
|
+
const authDelegate = await delegate.withAuth({ uid: "deleter",
|
|
553
|
+
email: "del@test.com" } as MockUser);
|
|
554
|
+
await authDelegate.deleteEntity({ entity: { id: "1",
|
|
555
|
+
path: "x",
|
|
556
|
+
values: {} } as unknown as Entity });
|
|
484
557
|
|
|
485
558
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
486
559
|
});
|
|
487
560
|
|
|
488
|
-
it(
|
|
561
|
+
it("should wrap checkUniqueField in a transaction with RLS", async () => {
|
|
489
562
|
(mockDb.transaction as jest.Mock).mockImplementation(async (cb) => {
|
|
490
563
|
return await cb({ execute: jest.fn() });
|
|
491
564
|
});
|
|
492
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
565
|
+
jest.spyOn(PostgresBackendDriver.prototype, "checkUniqueField").mockResolvedValueOnce(true);
|
|
493
566
|
|
|
494
|
-
const authDelegate = await delegate.withAuth({ uid:
|
|
495
|
-
|
|
567
|
+
const authDelegate = await delegate.withAuth({ uid: "checker",
|
|
568
|
+
email: "c@test.com" } as MockUser);
|
|
569
|
+
const result = await authDelegate.checkUniqueField("path", "email", "test@x.com", "1");
|
|
496
570
|
|
|
497
571
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
498
572
|
expect(result).toBe(true);
|
|
499
573
|
});
|
|
500
574
|
|
|
501
|
-
it(
|
|
575
|
+
it("should wrap countEntities in a transaction with RLS", async () => {
|
|
502
576
|
(mockDb.transaction as jest.Mock).mockImplementation(async (cb) => {
|
|
503
577
|
return await cb({ execute: jest.fn() });
|
|
504
578
|
});
|
|
505
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
579
|
+
jest.spyOn(PostgresBackendDriver.prototype, "countEntities").mockResolvedValueOnce(42);
|
|
506
580
|
|
|
507
|
-
const authDelegate = await delegate.withAuth({ uid:
|
|
508
|
-
|
|
581
|
+
const authDelegate = await delegate.withAuth({ uid: "counter",
|
|
582
|
+
email: "c@test.com" } as MockUser);
|
|
583
|
+
const result = await authDelegate.countEntities({ path: "items",
|
|
584
|
+
collection: {} as unknown as EntityCollection });
|
|
509
585
|
|
|
510
586
|
expect(mockDb.transaction).toHaveBeenCalled();
|
|
511
587
|
expect(result).toBe(42);
|
|
512
588
|
});
|
|
513
589
|
|
|
514
590
|
it('should expose key="postgres" and initialised=true on the authenticated wrapper', async () => {
|
|
515
|
-
const authDelegate = await delegate.withAuth({ uid:
|
|
516
|
-
|
|
591
|
+
const authDelegate = await delegate.withAuth({ uid: "u",
|
|
592
|
+
email: "u@t.com" } as MockUser);
|
|
593
|
+
expect(authDelegate.key).toBe("postgres");
|
|
517
594
|
expect(authDelegate.initialised).toBe(true);
|
|
518
595
|
});
|
|
519
596
|
|
|
520
|
-
it(
|
|
521
|
-
const auth1 = await delegate.withAuth({ uid:
|
|
522
|
-
|
|
597
|
+
it("should NOT share user state if withAuth is called with different users", async () => {
|
|
598
|
+
const auth1 = await delegate.withAuth({ uid: "alice",
|
|
599
|
+
email: "a@t.com" } as MockUser);
|
|
600
|
+
const auth2 = await delegate.withAuth({ uid: "bob",
|
|
601
|
+
email: "b@t.com" } as MockUser);
|
|
523
602
|
|
|
524
|
-
expect(auth1.user.uid).toBe(
|
|
525
|
-
expect(auth2.user.uid).toBe(
|
|
603
|
+
expect(auth1.user.uid).toBe("alice");
|
|
604
|
+
expect(auth2.user.uid).toBe("bob");
|
|
526
605
|
// They share the same underlying delegate
|
|
527
606
|
expect(auth1.delegate).toBe(auth2.delegate);
|
|
528
607
|
});
|
|
529
608
|
|
|
530
|
-
it(
|
|
531
|
-
const authDelegate = await delegate.withAuth({ uid:
|
|
609
|
+
it("should create a fresh pendingNotifications array per withTransaction call (no accumulation)", async () => {
|
|
610
|
+
const authDelegate = await delegate.withAuth({ uid: "u1",
|
|
611
|
+
email: "u@t.com" } as MockUser);
|
|
532
612
|
const mockTx = { execute: jest.fn() };
|
|
533
613
|
|
|
534
614
|
(mockDb.transaction as jest.Mock).mockImplementation(async (cb) => await cb(mockTx));
|
|
535
615
|
|
|
536
|
-
jest.spyOn(PostgresBackendDriver.prototype,
|
|
616
|
+
jest.spyOn(PostgresBackendDriver.prototype, "saveEntity")
|
|
537
617
|
.mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
538
|
-
this._pendingNotifications?.push({ path:
|
|
618
|
+
this._pendingNotifications?.push({ path: "call-1",
|
|
619
|
+
entityId: "1",
|
|
620
|
+
entity: {} as unknown as Entity });
|
|
539
621
|
return {} as unknown as Entity;
|
|
540
622
|
})
|
|
541
623
|
.mockImplementationOnce(async function(this: { _pendingNotifications?: Array<Record<string, unknown>> }) {
|
|
542
|
-
this._pendingNotifications?.push({ path:
|
|
624
|
+
this._pendingNotifications?.push({ path: "call-2",
|
|
625
|
+
entityId: "2",
|
|
626
|
+
entity: {} as unknown as Entity });
|
|
543
627
|
return {} as unknown as Entity;
|
|
544
628
|
});
|
|
545
629
|
|
|
546
|
-
await authDelegate.saveEntity({ path:
|
|
547
|
-
|
|
630
|
+
await authDelegate.saveEntity({ path: "call-1",
|
|
631
|
+
entityId: "1",
|
|
632
|
+
values: {},
|
|
633
|
+
collection: {} as unknown as EntityCollection,
|
|
634
|
+
status: "new" });
|
|
635
|
+
await authDelegate.saveEntity({ path: "call-2",
|
|
636
|
+
entityId: "2",
|
|
637
|
+
values: {},
|
|
638
|
+
collection: {} as unknown as EntityCollection,
|
|
639
|
+
status: "new" });
|
|
548
640
|
|
|
549
641
|
// Each call should have flushed exactly 1 notification, not accumulated
|
|
550
642
|
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenCalledTimes(2);
|
|
551
|
-
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenNthCalledWith(1,
|
|
552
|
-
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenNthCalledWith(2,
|
|
643
|
+
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenNthCalledWith(1, "call-1", "1", {}, undefined);
|
|
644
|
+
expect(mockRealtimeService.notifyEntityUpdate).toHaveBeenNthCalledWith(2, "call-2", "2", {}, undefined);
|
|
553
645
|
});
|
|
554
646
|
});
|
|
555
647
|
});
|