@rebasepro/server-postgresql 0.0.1-canary.09e5ec5
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/LICENSE +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +11298 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +11306 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +192 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +40 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
- 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 +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -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/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +92 -0
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +1008 -0
- package/src/PostgresBootstrapper.ts +231 -0
- package/src/auth/ensure-tables.ts +381 -0
- package/src/auth/services.ts +799 -0
- package/src/cli.ts +648 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +84 -0
- package/src/data-transformer.ts +608 -0
- package/src/databasePoolManager.ts +85 -0
- package/src/history/HistoryService.ts +248 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +169 -0
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +765 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/schema/introspect-db-logic.ts +542 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1576 -0
- package/src/services/EntityPersistService.ts +349 -0
- package/src/services/RelationService.ts +1274 -0
- package/src/services/entity-helpers.ts +147 -0
- package/src/services/entityService.ts +211 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1034 -0
- package/src/utils/drizzle-conditions.ts +1000 -0
- package/src/websocket.ts +518 -0
- package/test/auth-services.test.ts +661 -0
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +367 -0
- 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/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +367 -0
- package/test/entityService.relations.test.ts +1008 -0
- package/test/entityService.subcollection-search.test.ts +566 -0
- package/test/entityService.test.ts +1035 -0
- package/test/generate-drizzle-schema.test.ts +988 -0
- package/test/historyService.test.ts +141 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +389 -0
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +648 -0
- package/test/realtimeService.test.ts +307 -0
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +1115 -0
- package/test/unmapped-tables-safety.test.ts +345 -0
- 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 +3 -0
- package/test_find_changed.mjs +32 -0
- package/test_hash.js +14 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +82 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { RealtimeService } from "../src/services/realtimeService";
|
|
2
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { PostgresCollectionRegistry } from "../src/collections/PostgresCollectionRegistry";
|
|
4
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
5
|
+
|
|
6
|
+
jest.mock("../src/services/entityService", () => ({
|
|
7
|
+
EntityService: jest.fn().mockImplementation(() => ({
|
|
8
|
+
fetchCollection: jest.fn().mockResolvedValue([{ id: 1,
|
|
9
|
+
_rebase_invalidated: false }]),
|
|
10
|
+
fetchEntity: jest.fn().mockResolvedValue({ id: 1,
|
|
11
|
+
_rebase_invalidated: false }),
|
|
12
|
+
searchEntities: jest.fn().mockResolvedValue([])
|
|
13
|
+
}))
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// --- Mock Classes ---
|
|
17
|
+
class MockWebSocket {
|
|
18
|
+
public readyState = 1;
|
|
19
|
+
public send = jest.fn();
|
|
20
|
+
public on = jest.fn();
|
|
21
|
+
constructor() {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const mockPostsCollection: EntityCollection = {
|
|
25
|
+
slug: "posts",
|
|
26
|
+
name: "Posts",
|
|
27
|
+
table: "posts",
|
|
28
|
+
properties: {
|
|
29
|
+
id: { type: "number" },
|
|
30
|
+
title: { type: "string" }
|
|
31
|
+
},
|
|
32
|
+
idField: "id"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("RealtimeService", () => {
|
|
36
|
+
let realtimeService: RealtimeService;
|
|
37
|
+
let db: jest.Mocked<NodePgDatabase<any>>;
|
|
38
|
+
let registry: PostgresCollectionRegistry;
|
|
39
|
+
let mockDriver: any;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.useFakeTimers();
|
|
43
|
+
|
|
44
|
+
db = {
|
|
45
|
+
execute: jest.fn().mockResolvedValue({ rows: [] }),
|
|
46
|
+
transaction: jest.fn((callback) => callback(db)),
|
|
47
|
+
select: jest.fn().mockReturnThis(),
|
|
48
|
+
from: jest.fn().mockReturnThis(),
|
|
49
|
+
where: jest.fn().mockReturnThis(),
|
|
50
|
+
limit: jest.fn().mockReturnThis()
|
|
51
|
+
} as unknown as jest.Mocked<NodePgDatabase<any>>;
|
|
52
|
+
(db as any).then = jest.fn((resolve) => resolve([]));
|
|
53
|
+
|
|
54
|
+
registry = new PostgresCollectionRegistry();
|
|
55
|
+
jest.spyOn(registry, "getCollectionByPath").mockReturnValue(mockPostsCollection);
|
|
56
|
+
|
|
57
|
+
mockDriver = {
|
|
58
|
+
fetchCollection: jest.fn().mockResolvedValue([{ id: 1,
|
|
59
|
+
path: "posts",
|
|
60
|
+
values: { title: "Refetched Title" } }]),
|
|
61
|
+
fetchEntity: jest.fn().mockResolvedValue({ id: 1,
|
|
62
|
+
path: "posts",
|
|
63
|
+
values: { title: "Refetched Entity Title" } })
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const mockPoolManager = {
|
|
67
|
+
defaultDatabaseName: "main"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mockAuthSettings = {
|
|
71
|
+
accessTokenSecret: "secret"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
realtimeService = new RealtimeService(db, registry, mockPoolManager as any, "test-instance", mockAuthSettings);
|
|
75
|
+
realtimeService.setDataDriver(mockDriver);
|
|
76
|
+
realtimeService.setDataDriver(mockDriver);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
jest.clearAllMocks();
|
|
81
|
+
jest.useRealTimers();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Client Management", () => {
|
|
85
|
+
it("adds and removes clients safely", () => {
|
|
86
|
+
const ws = new MockWebSocket() as any;
|
|
87
|
+
realtimeService.addClient("client-1", ws);
|
|
88
|
+
expect(realtimeService.clients.has("client-1")).toBe(true);
|
|
89
|
+
|
|
90
|
+
realtimeService.removeClient("client-1");
|
|
91
|
+
expect(realtimeService.clients.has("client-1")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("removes client subscriptions upon disconnect", async () => {
|
|
95
|
+
const ws = new MockWebSocket() as any;
|
|
96
|
+
realtimeService.addClient("client-1", ws);
|
|
97
|
+
await realtimeService.handleClientMessage("client-1", {
|
|
98
|
+
type: "subscribe_collection",
|
|
99
|
+
payload: { path: "posts",
|
|
100
|
+
subscriptionId: "sub-1" }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(realtimeService.subscriptions.has("sub-1")).toBe(true);
|
|
104
|
+
realtimeService.removeClient("client-1");
|
|
105
|
+
expect(realtimeService.subscriptions.has("sub-1")).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("Collection Synchronization", () => {
|
|
110
|
+
it("triggers debounced refetch and omits dummy entities on PG_NOTIFY invalidation", async () => {
|
|
111
|
+
const ws = new MockWebSocket() as any;
|
|
112
|
+
realtimeService.addClient("client-1", ws);
|
|
113
|
+
|
|
114
|
+
await realtimeService.handleClientMessage("client-1", {
|
|
115
|
+
type: "subscribe_collection",
|
|
116
|
+
payload: { path: "posts",
|
|
117
|
+
subscriptionId: "sub-1" }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Simulate PG_NOTIFY listener receiving cross-instance payload
|
|
121
|
+
const dummyEntity = { id: "1",
|
|
122
|
+
path: "posts",
|
|
123
|
+
values: { _rebase_invalidated: true } } as any;
|
|
124
|
+
await realtimeService.notifyEntityUpdate("posts", "1", dummyEntity, undefined, false);
|
|
125
|
+
|
|
126
|
+
// Phase 1: sendCollectionEntityPatch SHOULD NOT SEND for dummy
|
|
127
|
+
expect(ws.send).not.toHaveBeenCalledWith(expect.stringContaining("collection_entity_patch"));
|
|
128
|
+
|
|
129
|
+
// Phase 2: Debounced refetch should kick in after 300ms
|
|
130
|
+
jest.advanceTimersByTime(350);
|
|
131
|
+
|
|
132
|
+
// Wait for async promises to drain
|
|
133
|
+
await Promise.resolve();
|
|
134
|
+
await Promise.resolve();
|
|
135
|
+
|
|
136
|
+
// It should fetch the collection with auth
|
|
137
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalled();
|
|
138
|
+
|
|
139
|
+
// It should send the refetched data
|
|
140
|
+
expect(ws.send).toHaveBeenCalled();
|
|
141
|
+
const lastCall = ws.send.mock.calls[ws.send.mock.calls.length - 1][0];
|
|
142
|
+
const parsed = JSON.parse(lastCall);
|
|
143
|
+
|
|
144
|
+
expect(parsed.type).toBe("collection_update");
|
|
145
|
+
expect(parsed.subscriptionId).toBe("sub-1");
|
|
146
|
+
expect(parsed.entities[0].values.title).toBe("Refetched Title");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("sends instant entity patch for valid entity updates without _rebase_invalidated", async () => {
|
|
150
|
+
const ws = new MockWebSocket() as any;
|
|
151
|
+
realtimeService.addClient("client-1", ws);
|
|
152
|
+
|
|
153
|
+
await realtimeService.handleClientMessage("client-1", {
|
|
154
|
+
type: "subscribe_collection",
|
|
155
|
+
payload: { path: "posts",
|
|
156
|
+
subscriptionId: "sub-1" }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Simulated normal Local update
|
|
160
|
+
const freshEntity = { id: "1",
|
|
161
|
+
path: "posts",
|
|
162
|
+
values: { title: "Immediate Patch" } } as any;
|
|
163
|
+
await realtimeService.notifyEntityUpdate("posts", "1", freshEntity, undefined, false);
|
|
164
|
+
|
|
165
|
+
// Phase 1: It SHOULD send immediate patch
|
|
166
|
+
expect(ws.send).toHaveBeenCalled();
|
|
167
|
+
let lastCall = ws.send.mock.calls[ws.send.mock.calls.length - 1][0];
|
|
168
|
+
let parsed = JSON.parse(lastCall);
|
|
169
|
+
expect(parsed.type).toBe("collection_entity_patch");
|
|
170
|
+
expect(parsed.entity.values.title).toBe("Immediate Patch");
|
|
171
|
+
|
|
172
|
+
// Phase 2: Refetch
|
|
173
|
+
jest.advanceTimersByTime(350);
|
|
174
|
+
for (let i = 0; i < 10; i++) {
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
}
|
|
177
|
+
lastCall = ws.send.mock.calls[ws.send.mock.calls.length - 1][0];
|
|
178
|
+
parsed = JSON.parse(lastCall);
|
|
179
|
+
expect(parsed.type).toBe("collection_update");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("Entity Synchronization", () => {
|
|
184
|
+
it("triggers debounced refetch and omits dummy entity update for single entity subscriptions", async () => {
|
|
185
|
+
const ws = new MockWebSocket() as any;
|
|
186
|
+
realtimeService.addClient("client-2", ws);
|
|
187
|
+
|
|
188
|
+
await realtimeService.handleClientMessage("client-2", {
|
|
189
|
+
type: "subscribe_entity",
|
|
190
|
+
payload: { path: "posts",
|
|
191
|
+
entityId: "1",
|
|
192
|
+
subscriptionId: "sub-2" }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Need to mock sendEntityUpdate
|
|
196
|
+
const dummyEntity = { id: "1",
|
|
197
|
+
path: "posts",
|
|
198
|
+
values: { _rebase_invalidated: true } } as any;
|
|
199
|
+
await realtimeService.notifyEntityUpdate("posts", "1", dummyEntity, undefined, false);
|
|
200
|
+
|
|
201
|
+
// Important: we patched notifyPathUpdate to NOT send entity_update directly if invalidated
|
|
202
|
+
expect(ws.send).not.toHaveBeenCalledWith(expect.stringContaining("_rebase_invalidated"));
|
|
203
|
+
|
|
204
|
+
// Fast forward refetch timer
|
|
205
|
+
jest.advanceTimersByTime(350);
|
|
206
|
+
await Promise.resolve();
|
|
207
|
+
await Promise.resolve();
|
|
208
|
+
|
|
209
|
+
// It should fetch the single entity
|
|
210
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(expect.objectContaining({ path: "posts",
|
|
211
|
+
entityId: "1" }));
|
|
212
|
+
|
|
213
|
+
// It should send entity update
|
|
214
|
+
expect(ws.send).toHaveBeenCalled();
|
|
215
|
+
const lastCall = ws.send.mock.calls[ws.send.mock.calls.length - 1][0];
|
|
216
|
+
const parsed = JSON.parse(lastCall);
|
|
217
|
+
|
|
218
|
+
expect(parsed.type).toBe("entity_update");
|
|
219
|
+
expect(parsed.subscriptionId).toBe("sub-2");
|
|
220
|
+
expect(parsed.entity.values.title).toBe("Refetched Entity Title");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("sends instant entity update if valid payload (local mutation)", async () => {
|
|
224
|
+
const ws = new MockWebSocket() as any;
|
|
225
|
+
realtimeService.addClient("client-2", ws);
|
|
226
|
+
|
|
227
|
+
await realtimeService.handleClientMessage("client-2", {
|
|
228
|
+
type: "subscribe_entity",
|
|
229
|
+
payload: { path: "posts",
|
|
230
|
+
entityId: "1",
|
|
231
|
+
subscriptionId: "sub-2" }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const freshEntity = { id: "1",
|
|
235
|
+
path: "posts",
|
|
236
|
+
values: { title: "Pure Patch" } } as any;
|
|
237
|
+
await realtimeService.notifyEntityUpdate("posts", "1", freshEntity, undefined, false);
|
|
238
|
+
|
|
239
|
+
expect(ws.send).toHaveBeenCalled();
|
|
240
|
+
const lastCall = ws.send.mock.calls[ws.send.mock.calls.length - 1][0];
|
|
241
|
+
const parsed = JSON.parse(lastCall);
|
|
242
|
+
|
|
243
|
+
expect(parsed.type).toBe("entity_update");
|
|
244
|
+
expect(parsed.entity.values.title).toBe("Pure Patch");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("RLS (Row Level Security)", () => {
|
|
249
|
+
it("applies auth context correctly on debounced collection refetches", async () => {
|
|
250
|
+
const ws = new MockWebSocket() as any;
|
|
251
|
+
realtimeService.addClient("client-rls", ws);
|
|
252
|
+
|
|
253
|
+
await realtimeService.handleClientMessage("client-rls", {
|
|
254
|
+
type: "subscribe_collection",
|
|
255
|
+
payload: { path: "posts",
|
|
256
|
+
subscriptionId: "sub-rls" }
|
|
257
|
+
}, { userId: "user123",
|
|
258
|
+
roles: ["admin", "editor"] });
|
|
259
|
+
|
|
260
|
+
// Simulate PG_NOTIFY invalidation
|
|
261
|
+
const dummyEntity = { id: "1",
|
|
262
|
+
path: "posts",
|
|
263
|
+
values: { _rebase_invalidated: true } } as any;
|
|
264
|
+
await realtimeService.notifyEntityUpdate("posts", "1", dummyEntity, undefined, false);
|
|
265
|
+
|
|
266
|
+
jest.advanceTimersByTime(350);
|
|
267
|
+
await Promise.resolve();
|
|
268
|
+
await Promise.resolve();
|
|
269
|
+
|
|
270
|
+
expect(db.execute).toHaveBeenCalled();
|
|
271
|
+
const executeCalls = db.execute.mock.calls.map(c => JSON.stringify(c[0]));
|
|
272
|
+
|
|
273
|
+
expect(executeCalls.some(sql => sql.includes("set_config('app.user_id'"))).toBe(true);
|
|
274
|
+
expect(executeCalls.some(sql => sql.includes("set_config('app.user_roles'"))).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("applies auth context correctly on debounced entity refetches", async () => {
|
|
278
|
+
const ws = new MockWebSocket() as any;
|
|
279
|
+
realtimeService.addClient("client-rls-ent", ws);
|
|
280
|
+
|
|
281
|
+
await realtimeService.handleClientMessage("client-rls-ent", {
|
|
282
|
+
type: "subscribe_entity",
|
|
283
|
+
payload: { path: "posts",
|
|
284
|
+
entityId: "1",
|
|
285
|
+
subscriptionId: "sub-rls-ent" }
|
|
286
|
+
}, { userId: "user456",
|
|
287
|
+
roles: ["viewer"] });
|
|
288
|
+
|
|
289
|
+
const dummyEntity = { id: "1",
|
|
290
|
+
path: "posts",
|
|
291
|
+
values: { _rebase_invalidated: true } } as any;
|
|
292
|
+
await realtimeService.notifyEntityUpdate("posts", "1", dummyEntity, undefined, false);
|
|
293
|
+
|
|
294
|
+
jest.advanceTimersByTime(350);
|
|
295
|
+
await Promise.resolve();
|
|
296
|
+
await Promise.resolve();
|
|
297
|
+
|
|
298
|
+
expect(db.execute).toHaveBeenCalled();
|
|
299
|
+
const executeCalls = db.execute.mock.calls.map(c => JSON.stringify(c[0]));
|
|
300
|
+
|
|
301
|
+
expect(executeCalls.some(sql => sql.includes("set_config('app.user_id'"))).toBe(true);
|
|
302
|
+
expect(executeCalls.some(sql => sql.includes("set_config('app.user_roles'"))).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
});
|
|
307
|
+
|