@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.
- package/package.json +24 -18
- package/src/PostgresBackendDriver.ts +65 -44
- package/src/PostgresBootstrapper.ts +49 -20
- package/src/auth/ensure-tables.ts +56 -1
- package/src/auth/services.ts +94 -1
- package/src/cli-errors.ts +162 -0
- package/src/cli-helpers.ts +183 -0
- package/src/cli.ts +198 -251
- package/src/data-transformer.ts +9 -1
- package/src/schema/auth-default-policies.ts +90 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +4 -3
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +524 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +18 -16
- package/src/services/entityService.ts +28 -3
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +16 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +9 -0
- package/test/array-null-safety.test.ts +335 -0
- package/test/auth-default-policies.test.ts +89 -0
- package/test/cli-helpers-extended.test.ts +324 -0
- package/test/cli-helpers.test.ts +59 -0
- package/test/connection.test.ts +292 -0
- package/test/data-transformer.test.ts +53 -2
- package/test/databasePoolManager.test.ts +289 -0
- package/test/doctor-extended.test.ts +443 -0
- package/test/e2e/db-e2e.test.ts +293 -0
- package/test/e2e/pg-setup.ts +79 -0
- package/test/entity-persist-composite-keys.test.ts +451 -0
- package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
- package/test/generate-postgres-ddl.test.ts +300 -0
- package/test/mfa-service.test.ts +544 -0
- package/test/pg-array-null-patch.test.ts +65 -0
- package/test/pg-error-utils.test.ts +50 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vite.config.ts +8 -6
- package/vitest.e2e.config.ts +10 -0
- package/build-errors.txt +0 -37
- package/dist/PostgresAdapter.d.ts +0 -6
- package/dist/PostgresBackendDriver.d.ts +0 -110
- package/dist/PostgresBootstrapper.d.ts +0 -46
- package/dist/auth/ensure-tables.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -231
- package/dist/cli.d.ts +0 -1
- package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
- package/dist/connection.d.ts +0 -65
- package/dist/data-transformer.d.ts +0 -55
- package/dist/databasePoolManager.d.ts +0 -20
- package/dist/history/HistoryService.d.ts +0 -71
- package/dist/history/ensure-history-table.d.ts +0 -7
- package/dist/index.d.ts +0 -14
- package/dist/index.es.js +0 -10764
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -11055
- package/dist/index.umd.js.map +0 -1
- package/dist/interfaces.d.ts +0 -18
- package/dist/schema/auth-schema.d.ts +0 -2149
- package/dist/schema/doctor-cli.d.ts +0 -2
- package/dist/schema/doctor.d.ts +0 -52
- package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
- package/dist/schema/generate-drizzle-schema.d.ts +0 -1
- package/dist/schema/introspect-db-inference.d.ts +0 -5
- package/dist/schema/introspect-db-logic.d.ts +0 -118
- package/dist/schema/introspect-db.d.ts +0 -1
- package/dist/schema/test-schema.d.ts +0 -24
- package/dist/services/BranchService.d.ts +0 -47
- package/dist/services/EntityFetchService.d.ts +0 -214
- package/dist/services/EntityPersistService.d.ts +0 -40
- package/dist/services/RelationService.d.ts +0 -98
- package/dist/services/entity-helpers.d.ts +0 -38
- package/dist/services/entityService.d.ts +0 -110
- package/dist/services/index.d.ts +0 -4
- package/dist/services/realtimeService.d.ts +0 -220
- package/dist/types.d.ts +0 -3
- package/dist/utils/drizzle-conditions.d.ts +0 -138
- package/dist/utils/pg-error-utils.d.ts +0 -55
- 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
|
|
167
|
+
it("should coerce non-array values to empty array", () => {
|
|
158
168
|
expect(serializePropertyToServer("not-an-array", arrayOfStringsProp))
|
|
159
|
-
.
|
|
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
|
+
});
|