@rebasepro/server-postgresql 0.6.0 → 0.7.0

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 (82) hide show
  1. package/package.json +24 -18
  2. package/src/PostgresBackendDriver.ts +65 -44
  3. package/src/PostgresBootstrapper.ts +49 -20
  4. package/src/auth/ensure-tables.ts +56 -1
  5. package/src/auth/services.ts +94 -1
  6. package/src/cli-errors.ts +162 -0
  7. package/src/cli-helpers.ts +183 -0
  8. package/src/cli.ts +198 -251
  9. package/src/data-transformer.ts +9 -1
  10. package/src/schema/auth-default-policies.ts +90 -0
  11. package/src/schema/auth-schema.ts +25 -2
  12. package/src/schema/doctor.ts +2 -4
  13. package/src/schema/generate-drizzle-schema-logic.ts +4 -3
  14. package/src/schema/generate-drizzle-schema.ts +3 -5
  15. package/src/schema/generate-postgres-ddl-logic.ts +524 -0
  16. package/src/schema/generate-postgres-ddl.ts +116 -0
  17. package/src/services/EntityPersistService.ts +18 -16
  18. package/src/services/entityService.ts +28 -3
  19. package/src/utils/pg-array-null-patch.ts +42 -0
  20. package/src/utils/pg-error-utils.ts +16 -0
  21. package/src/utils/table-classification.ts +16 -0
  22. package/src/websocket.ts +9 -0
  23. package/test/array-null-safety.test.ts +335 -0
  24. package/test/auth-default-policies.test.ts +89 -0
  25. package/test/cli-helpers-extended.test.ts +324 -0
  26. package/test/cli-helpers.test.ts +59 -0
  27. package/test/connection.test.ts +292 -0
  28. package/test/data-transformer.test.ts +53 -2
  29. package/test/databasePoolManager.test.ts +289 -0
  30. package/test/doctor-extended.test.ts +443 -0
  31. package/test/e2e/db-e2e.test.ts +293 -0
  32. package/test/e2e/pg-setup.ts +79 -0
  33. package/test/entity-persist-composite-keys.test.ts +451 -0
  34. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  35. package/test/generate-postgres-ddl.test.ts +300 -0
  36. package/test/mfa-service.test.ts +544 -0
  37. package/test/pg-array-null-patch.test.ts +65 -0
  38. package/test/pg-error-utils.test.ts +50 -1
  39. package/test/realtimeService-channels.test.ts +696 -0
  40. package/test/unmapped-tables-safety.test.ts +55 -342
  41. package/vite.config.ts +8 -6
  42. package/vitest.e2e.config.ts +10 -0
  43. package/build-errors.txt +0 -37
  44. package/dist/PostgresAdapter.d.ts +0 -6
  45. package/dist/PostgresBackendDriver.d.ts +0 -110
  46. package/dist/PostgresBootstrapper.d.ts +0 -46
  47. package/dist/auth/ensure-tables.d.ts +0 -10
  48. package/dist/auth/services.d.ts +0 -231
  49. package/dist/cli.d.ts +0 -1
  50. package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
  51. package/dist/connection.d.ts +0 -65
  52. package/dist/data-transformer.d.ts +0 -55
  53. package/dist/databasePoolManager.d.ts +0 -20
  54. package/dist/history/HistoryService.d.ts +0 -71
  55. package/dist/history/ensure-history-table.d.ts +0 -7
  56. package/dist/index.d.ts +0 -14
  57. package/dist/index.es.js +0 -10764
  58. package/dist/index.es.js.map +0 -1
  59. package/dist/index.umd.js +0 -11055
  60. package/dist/index.umd.js.map +0 -1
  61. package/dist/interfaces.d.ts +0 -18
  62. package/dist/schema/auth-schema.d.ts +0 -2149
  63. package/dist/schema/doctor-cli.d.ts +0 -2
  64. package/dist/schema/doctor.d.ts +0 -52
  65. package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
  66. package/dist/schema/generate-drizzle-schema.d.ts +0 -1
  67. package/dist/schema/introspect-db-inference.d.ts +0 -5
  68. package/dist/schema/introspect-db-logic.d.ts +0 -118
  69. package/dist/schema/introspect-db.d.ts +0 -1
  70. package/dist/schema/test-schema.d.ts +0 -24
  71. package/dist/services/BranchService.d.ts +0 -47
  72. package/dist/services/EntityFetchService.d.ts +0 -214
  73. package/dist/services/EntityPersistService.d.ts +0 -40
  74. package/dist/services/RelationService.d.ts +0 -98
  75. package/dist/services/entity-helpers.d.ts +0 -38
  76. package/dist/services/entityService.d.ts +0 -110
  77. package/dist/services/index.d.ts +0 -4
  78. package/dist/services/realtimeService.d.ts +0 -220
  79. package/dist/types.d.ts +0 -3
  80. package/dist/utils/drizzle-conditions.d.ts +0 -138
  81. package/dist/utils/pg-error-utils.d.ts +0 -55
  82. package/dist/websocket.d.ts +0 -11
@@ -149,14 +149,65 @@ path: "authors" }
149
149
  of: { type: "string" } as Property
150
150
  } as unknown as Property;
151
151
 
152
+ const arrayOfNumbersProp: Property = {
153
+ type: "array",
154
+ of: { type: "number" } as Property
155
+ } as unknown as Property;
156
+
157
+ const arrayOfRelationsProp: Property = {
158
+ type: "array",
159
+ of: { type: "relation" } as Property
160
+ } as unknown as Property;
161
+
152
162
  it("should serialize array elements through their sub-property type", () => {
153
163
  expect(serializePropertyToServer(["a", "b", "c"], arrayOfStringsProp))
154
164
  .toEqual(["a", "b", "c"]);
155
165
  });
156
166
 
157
- it("should pass through non-array values", () => {
167
+ it("should coerce non-array values to empty array", () => {
158
168
  expect(serializePropertyToServer("not-an-array", arrayOfStringsProp))
159
- .toBe("not-an-array");
169
+ .toEqual([]);
170
+ });
171
+
172
+ // --- Null safety ---
173
+ it("should return null for null array value", () => {
174
+ expect(serializePropertyToServer(null, arrayOfStringsProp)).toBeNull();
175
+ });
176
+
177
+ it("should return undefined for undefined array value", () => {
178
+ expect(serializePropertyToServer(undefined, arrayOfStringsProp)).toBeUndefined();
179
+ });
180
+
181
+ it("should return empty array for empty array value", () => {
182
+ expect(serializePropertyToServer([], arrayOfStringsProp)).toEqual([]);
183
+ });
184
+
185
+ it("should return null for null value with array-of-numbers property", () => {
186
+ expect(serializePropertyToServer(null, arrayOfNumbersProp)).toBeNull();
187
+ });
188
+
189
+ it("should handle array with null elements", () => {
190
+ const result = serializePropertyToServer([null, "a", null, "b"], arrayOfStringsProp);
191
+ expect(result).toEqual([null, "a", null, "b"]);
192
+ });
193
+
194
+ it("should handle array of relation objects with null entries", () => {
195
+ const value = [{ id: "1", path: "tags" }, null, { id: "2", path: "tags" }];
196
+ const result = serializePropertyToServer(value, arrayOfRelationsProp);
197
+ expect(result).toEqual(["1", null, "2"]);
198
+ });
199
+
200
+ it("should coerce number value to empty array for array property", () => {
201
+ expect(serializePropertyToServer(42, arrayOfStringsProp)).toEqual([]);
202
+ });
203
+
204
+ it("should coerce boolean value to empty array for array property", () => {
205
+ expect(serializePropertyToServer(true, arrayOfStringsProp)).toEqual([]);
206
+ });
207
+
208
+ it("should coerce object value to empty array for array property", () => {
209
+ const obj = { a: 1 };
210
+ expect(serializePropertyToServer(obj, arrayOfStringsProp)).toEqual([]);
160
211
  });
161
212
  });
162
213
 
@@ -0,0 +1,289 @@
1
+ import { DatabasePoolManager } from "../src/databasePoolManager";
2
+
3
+ // ── Mock pg.Pool ──────────────────────────────────────────────────────────────
4
+ // Each `new Pool(...)` call returns a fresh mock with `.end()` and `.on()`.
5
+ const createMockPool = () => ({
6
+ end: jest.fn().mockResolvedValue(undefined),
7
+ on: jest.fn()
8
+ });
9
+
10
+ let mockPoolInstances: ReturnType<typeof createMockPool>[] = [];
11
+
12
+ jest.mock("pg", () => ({
13
+ Pool: jest.fn().mockImplementation(() => {
14
+ const pool = createMockPool();
15
+ mockPoolInstances.push(pool);
16
+ return pool;
17
+ })
18
+ }));
19
+
20
+ // ── Mock drizzle ──────────────────────────────────────────────────────────────
21
+ jest.mock("drizzle-orm/node-postgres", () => ({
22
+ drizzle: jest.fn().mockImplementation(() => ({
23
+ __drizzleMock: true
24
+ }))
25
+ }));
26
+
27
+ // ── Mock logger ───────────────────────────────────────────────────────────────
28
+ jest.mock("@rebasepro/server-core", () => ({
29
+ logger: {
30
+ info: jest.fn(),
31
+ warn: jest.fn(),
32
+ error: jest.fn()
33
+ }
34
+ }));
35
+
36
+ const VALID_CONNECTION_STRING = "postgresql://user:pass@localhost:5432/mydb";
37
+
38
+ describe("DatabasePoolManager", () => {
39
+ beforeEach(() => {
40
+ jest.clearAllMocks();
41
+ mockPoolInstances = [];
42
+ });
43
+
44
+ // ── Constructor ────────────────────────────────────────────────────────
45
+ describe("constructor", () => {
46
+ it("should extract the default database name from the connection string", () => {
47
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
48
+ expect(manager.defaultDatabaseName).toBe("mydb");
49
+ });
50
+
51
+ it("should handle connection strings with paths containing multiple segments", () => {
52
+ const manager = new DatabasePoolManager(
53
+ "postgresql://user:pass@localhost:5432/production_db"
54
+ );
55
+ expect(manager.defaultDatabaseName).toBe("production_db");
56
+ });
57
+
58
+ it("should throw on an invalid connection string", () => {
59
+ expect(() => new DatabasePoolManager("not-a-url")).toThrow(
60
+ /Invalid adminConnectionString/
61
+ );
62
+ });
63
+ });
64
+
65
+ // ── getPool – caching ──────────────────────────────────────────────────
66
+ describe("getPool", () => {
67
+ it("should return the same pool instance for the same database name", () => {
68
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
69
+
70
+ const pool1 = manager.getPool("testdb");
71
+ const pool2 = manager.getPool("testdb");
72
+
73
+ expect(pool1).toBe(pool2);
74
+ // Only one Pool should have been constructed
75
+ expect(mockPoolInstances).toHaveLength(1);
76
+ });
77
+
78
+ it("should create separate pool instances for different database names", () => {
79
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
80
+
81
+ const poolA = manager.getPool("db_alpha");
82
+ const poolB = manager.getPool("db_beta");
83
+
84
+ expect(poolA).not.toBe(poolB);
85
+ expect(mockPoolInstances).toHaveLength(2);
86
+ });
87
+
88
+ it("should register an error handler on newly created pools", () => {
89
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
90
+ manager.getPool("errdb");
91
+
92
+ const pool = mockPoolInstances[0];
93
+ expect(pool.on).toHaveBeenCalledWith("error", expect.any(Function));
94
+ });
95
+
96
+ it("should build the connection string using the requested database name", () => {
97
+ const { Pool: PoolCtor } = jest.requireMock("pg") as {
98
+ Pool: jest.Mock;
99
+ };
100
+
101
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
102
+ manager.getPool("custom_db");
103
+
104
+ const configArg = PoolCtor.mock.calls[0][0] as {
105
+ connectionString: string;
106
+ };
107
+ expect(configArg.connectionString).toContain("/custom_db");
108
+ });
109
+ });
110
+
111
+ // ── hasPool ────────────────────────────────────────────────────────────
112
+ describe("hasPool", () => {
113
+ it("should return false when no pool exists for the given name", () => {
114
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
115
+ expect(manager.hasPool("nonexistent")).toBe(false);
116
+ });
117
+
118
+ it("should return true after a pool has been created", () => {
119
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
120
+ manager.getPool("exists");
121
+ expect(manager.hasPool("exists")).toBe(true);
122
+ });
123
+ });
124
+
125
+ // ── getDrizzle ─────────────────────────────────────────────────────────
126
+ describe("getDrizzle", () => {
127
+ it("should return a drizzle instance for a given database name", () => {
128
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
129
+ const db = manager.getDrizzle("appdb");
130
+
131
+ expect(db).toBeDefined();
132
+ expect((db as unknown as { __drizzleMock: boolean }).__drizzleMock).toBe(true);
133
+ });
134
+
135
+ it("should cache and reuse the drizzle instance for the same database name", () => {
136
+ const { drizzle } = jest.requireMock("drizzle-orm/node-postgres") as {
137
+ drizzle: jest.Mock;
138
+ };
139
+
140
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
141
+ const db1 = manager.getDrizzle("cached");
142
+ const db2 = manager.getDrizzle("cached");
143
+
144
+ expect(db1).toBe(db2);
145
+ // drizzle() should only have been called once for this database
146
+ expect(drizzle).toHaveBeenCalledTimes(1);
147
+ });
148
+
149
+ it("should create different drizzle instances for different databases", () => {
150
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
151
+ const dbA = manager.getDrizzle("first");
152
+ const dbB = manager.getDrizzle("second");
153
+
154
+ expect(dbA).not.toBe(dbB);
155
+ });
156
+ });
157
+
158
+ // ── disconnectDatabase ─────────────────────────────────────────────────
159
+ describe("disconnectDatabase", () => {
160
+ it("should call pool.end() and remove the pool from cache", async () => {
161
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
162
+ manager.getPool("dropme");
163
+
164
+ expect(manager.hasPool("dropme")).toBe(true);
165
+ const pool = mockPoolInstances[0];
166
+
167
+ await manager.disconnectDatabase("dropme");
168
+
169
+ expect(pool.end).toHaveBeenCalledTimes(1);
170
+ expect(manager.hasPool("dropme")).toBe(false);
171
+ });
172
+
173
+ it("should also clear the cached drizzle instance", async () => {
174
+ const { drizzle } = jest.requireMock("drizzle-orm/node-postgres") as {
175
+ drizzle: jest.Mock;
176
+ };
177
+
178
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
179
+ manager.getDrizzle("dropme");
180
+ drizzle.mockClear();
181
+
182
+ await manager.disconnectDatabase("dropme");
183
+
184
+ // After disconnect, getDrizzle should create a fresh instance
185
+ manager.getDrizzle("dropme");
186
+ expect(drizzle).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it("should not throw when disconnecting a non-existent database", async () => {
190
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
191
+ // Should complete without error
192
+ await expect(
193
+ manager.disconnectDatabase("ghost")
194
+ ).resolves.toBeUndefined();
195
+ });
196
+ });
197
+
198
+ // ── shutdown ────────────────────────────────────────────────────────────
199
+ describe("shutdown", () => {
200
+ it("should call pool.end() on every cached pool", async () => {
201
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
202
+ manager.getPool("a");
203
+ manager.getPool("b");
204
+ manager.getPool("c");
205
+
206
+ await manager.shutdown();
207
+
208
+ for (const pool of mockPoolInstances) {
209
+ expect(pool.end).toHaveBeenCalledTimes(1);
210
+ }
211
+ });
212
+
213
+ it("should clear the pool cache after shutdown", async () => {
214
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
215
+ manager.getPool("x");
216
+ manager.getPool("y");
217
+
218
+ await manager.shutdown();
219
+
220
+ expect(manager.hasPool("x")).toBe(false);
221
+ expect(manager.hasPool("y")).toBe(false);
222
+ });
223
+
224
+ it("should clear drizzle instances after shutdown", async () => {
225
+ const { drizzle } = jest.requireMock("drizzle-orm/node-postgres") as {
226
+ drizzle: jest.Mock;
227
+ };
228
+
229
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
230
+ manager.getDrizzle("z");
231
+ drizzle.mockClear();
232
+
233
+ await manager.shutdown();
234
+
235
+ // A fresh drizzle call should be needed after shutdown
236
+ manager.getDrizzle("z");
237
+ expect(drizzle).toHaveBeenCalledTimes(1);
238
+ });
239
+
240
+ it("should not throw when called on a manager with no pools", async () => {
241
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
242
+ await expect(manager.shutdown()).resolves.toBeUndefined();
243
+ });
244
+
245
+ it("should not throw when called multiple times", async () => {
246
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
247
+ manager.getPool("once");
248
+
249
+ await manager.shutdown();
250
+ await expect(manager.shutdown()).resolves.toBeUndefined();
251
+ });
252
+
253
+ it("should handle a pool.end() rejection gracefully (Promise.all rejects)", async () => {
254
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
255
+ manager.getPool("bad");
256
+
257
+ const pool = mockPoolInstances[0];
258
+ pool.end.mockRejectedValue(new Error("connection already closed"));
259
+
260
+ await expect(manager.shutdown()).rejects.toThrow(
261
+ "connection already closed"
262
+ );
263
+ });
264
+ });
265
+
266
+ // ── Pool re-creation after disconnect ──────────────────────────────────
267
+ describe("pool re-creation after disconnect", () => {
268
+ it("should create a fresh pool after disconnecting a database", async () => {
269
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
270
+ const poolBefore = manager.getPool("recreate");
271
+
272
+ await manager.disconnectDatabase("recreate");
273
+ const poolAfter = manager.getPool("recreate");
274
+
275
+ expect(poolBefore).not.toBe(poolAfter);
276
+ expect(mockPoolInstances).toHaveLength(2);
277
+ });
278
+
279
+ it("should create fresh pools after shutdown", async () => {
280
+ const manager = new DatabasePoolManager(VALID_CONNECTION_STRING);
281
+ const poolBefore = manager.getPool("revive");
282
+
283
+ await manager.shutdown();
284
+ const poolAfter = manager.getPool("revive");
285
+
286
+ expect(poolBefore).not.toBe(poolAfter);
287
+ });
288
+ });
289
+ });