@rebasepro/server-postgresql 0.6.1 → 0.8.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/dist/PostgresBackendDriver.d.ts +8 -0
- package/dist/auth/services.d.ts +21 -1
- package/dist/cli-errors.d.ts +29 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +2 -2
- package/dist/index.es.js +2987 -230
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-default-policies.d.ts +12 -0
- package/dist/schema/auth-schema.d.ts +227 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/services/entityService.d.ts +1 -1
- package/dist/utils/pg-error-utils.d.ts +10 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/package.json +15 -9
- package/src/PostgresBackendDriver.ts +200 -68
- package/src/PostgresBootstrapper.ts +34 -2
- 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 +264 -245
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-default-policies.ts +97 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +20 -82
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +487 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +10 -8
- package/src/services/entityService.ts +28 -3
- package/src/services/realtimeService.ts +26 -2
- 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/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/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-callbacks-redaction.test.ts +86 -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-error-utils.test.ts +50 -1
- package/test/postgresDataDriver.test.ts +2 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vitest.e2e.config.ts +10 -0
|
@@ -1,353 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* 2. `schemaFilter` restricts to the "public" PostgreSQL schema, so tables in
|
|
14
|
-
* other schemas (extensions, internal, etc.) are also invisible.
|
|
15
|
-
*
|
|
16
|
-
* 3. `entities.roles: false` prevents drizzle-kit from managing database roles.
|
|
17
|
-
*
|
|
18
|
-
* 4. `extensionsFilters: ["postgis"]` ignores extension-managed tables.
|
|
19
|
-
*
|
|
20
|
-
* 5. The CLI always passes `--strict --verbose` to `db push`, so destructive
|
|
21
|
-
* operations require explicit confirmation even if something slips through.
|
|
22
|
-
*
|
|
23
|
-
* These tests verify:
|
|
24
|
-
* - The schema generator outputs correct table/enum lists for tablesFilter
|
|
25
|
-
* - The tablesFilter scoping correctly excludes unmapped tables
|
|
26
|
-
* - The drizzle-kit diff engine produces no destructive SQL when the previous
|
|
27
|
-
* snapshot only contains managed tables (simulating what tablesFilter provides)
|
|
28
|
-
* - Unmapped tables in the raw snapshot DO produce DROP statements (proving
|
|
29
|
-
* the tablesFilter is the critical safety boundary, not the diff engine)
|
|
30
|
-
*/
|
|
31
|
-
import { generateDrizzleJson, generateMigration } from "drizzle-kit/api";
|
|
32
|
-
import { pgTable, varchar, text, integer, boolean, pgEnum } from "drizzle-orm/pg-core";
|
|
33
|
-
import { getTableName, Table } from "drizzle-orm";
|
|
34
|
-
import { EntityCollection } from "@rebasepro/types";
|
|
35
|
-
import { generateSchema } from "../src/schema/generate-drizzle-schema-logic";
|
|
36
|
-
|
|
37
|
-
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function buildPrevSnapshot(tables: Record<string, any>, enums: Record<string, any> = {}) {
|
|
40
|
-
return {
|
|
41
|
-
id: "prev-snapshot",
|
|
42
|
-
prevId: "prev-prev-snapshot",
|
|
43
|
-
version: "7",
|
|
44
|
-
dialect: "postgresql",
|
|
45
|
-
tables,
|
|
46
|
-
enums,
|
|
47
|
-
schemas: {},
|
|
48
|
-
sequences: {},
|
|
49
|
-
roles: {},
|
|
50
|
-
policies: {},
|
|
51
|
-
views: {},
|
|
52
|
-
_meta: { schemas: {},
|
|
53
|
-
tables: {},
|
|
54
|
-
columns: {} }
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function snapshotTable(name: string, columns: Record<string, any>) {
|
|
59
|
-
return {
|
|
60
|
-
name,
|
|
61
|
-
schema: "",
|
|
62
|
-
columns,
|
|
63
|
-
indexes: {},
|
|
64
|
-
foreignKeys: {},
|
|
65
|
-
compositePrimaryKeys: {},
|
|
66
|
-
uniqueConstraints: {},
|
|
67
|
-
policies: {},
|
|
68
|
-
checkConstraints: {},
|
|
69
|
-
isRLSEnabled: false
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function snapshotColumn(name: string, type: string, opts: {
|
|
74
|
-
primaryKey?: boolean;
|
|
75
|
-
notNull?: boolean;
|
|
76
|
-
} = {}) {
|
|
77
|
-
return {
|
|
78
|
-
name,
|
|
79
|
-
type,
|
|
80
|
-
primaryKey: opts.primaryKey ?? false,
|
|
81
|
-
notNull: opts.notNull ?? (opts.primaryKey ?? false),
|
|
82
|
-
default: undefined
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function snapshotEnum(name: string, schema: string, values: string[]) {
|
|
87
|
-
return { name,
|
|
88
|
-
schema,
|
|
89
|
-
values };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Drizzle schema objects (the "managed" schema) ───────────────────────
|
|
93
|
-
|
|
94
|
-
const managedUsers = pgTable("users", {
|
|
95
|
-
id: varchar("id").primaryKey(),
|
|
96
|
-
name: varchar("name"),
|
|
97
|
-
email: varchar("email")
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const managedPosts = pgTable("posts", {
|
|
101
|
-
id: varchar("id").primaryKey(),
|
|
102
|
-
title: varchar("title").notNull(),
|
|
103
|
-
user_id: varchar("user_id")
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ── Tests ───────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
describe("Unmapped tables safety", () => {
|
|
109
|
-
|
|
110
|
-
describe("tablesFilter scoping", () => {
|
|
111
|
-
|
|
112
|
-
it("should extract only managed table names from the tables export", () => {
|
|
113
|
-
const tables = { managedUsers,
|
|
114
|
-
managedPosts };
|
|
115
|
-
const tableNames = Object.values(tables).map(t => getTableName(t as Table));
|
|
116
|
-
|
|
117
|
-
expect(tableNames).toEqual(["users", "posts"]);
|
|
118
|
-
expect(tableNames).not.toContain("legacy_orders");
|
|
119
|
-
expect(tableNames).not.toContain("external_analytics");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("should produce a tablesFilter that excludes unmapped tables", () => {
|
|
123
|
-
const tables = { managedUsers,
|
|
124
|
-
managedPosts };
|
|
125
|
-
const tablesFilter = Object.values(tables).map(t => getTableName(t as Table));
|
|
126
|
-
|
|
127
|
-
const unmappedTables = [
|
|
128
|
-
"legacy_orders",
|
|
129
|
-
"external_analytics",
|
|
130
|
-
"stripe_webhooks",
|
|
131
|
-
"audit_log"
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
for (const unmapped of unmappedTables) {
|
|
135
|
-
expect(tablesFilter).not.toContain(unmapped);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("should produce stable, deterministic table names regardless of export key names", () => {
|
|
140
|
-
// The table name comes from pgTable("table_name", ...), not the JS variable name
|
|
141
|
-
const weirdVarName = pgTable("actual_table_name", {
|
|
142
|
-
id: varchar("id").primaryKey()
|
|
143
|
-
});
|
|
144
|
-
expect(getTableName(weirdVarName as Table)).toBe("actual_table_name");
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("tablesFilter as the safety boundary — proof by contradiction", () => {
|
|
149
|
-
|
|
150
|
-
it("without tablesFilter: drizzle-kit WOULD drop unmapped tables (proving tablesFilter is essential)", async () => {
|
|
151
|
-
// If unmapped tables leak into the prev snapshot (i.e. tablesFilter is NOT applied),
|
|
152
|
-
// drizzle-kit's diff engine WILL generate DROP TABLE statements.
|
|
153
|
-
// This test proves that tablesFilter is the critical safety layer.
|
|
154
|
-
const prevWithUnmapped = buildPrevSnapshot({
|
|
155
|
-
"public.users": snapshotTable("users", {
|
|
156
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
157
|
-
name: snapshotColumn("name", "varchar"),
|
|
158
|
-
email: snapshotColumn("email", "varchar")
|
|
159
|
-
}),
|
|
160
|
-
// An unmapped table that leaked into the snapshot
|
|
161
|
-
"public.legacy_orders": snapshotTable("legacy_orders", {
|
|
162
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
163
|
-
amount: snapshotColumn("amount", "integer")
|
|
164
|
-
})
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const curJson = generateDrizzleJson({ managedUsers });
|
|
168
|
-
const statements = await generateMigration(prevWithUnmapped as any, curJson as any);
|
|
169
|
-
const sql = statements.join("\n");
|
|
170
|
-
|
|
171
|
-
// WITHOUT tablesFilter, drizzle-kit WOULD drop the unmapped table.
|
|
172
|
-
// This is expected behavior — it proves tablesFilter is essential.
|
|
173
|
-
expect(sql).toContain("DROP TABLE");
|
|
174
|
-
expect(sql).toContain("legacy_orders");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("without tablesFilter: drizzle-kit WOULD drop unmapped enums (proving tablesFilter scope is essential)", async () => {
|
|
178
|
-
const prevWithUnmappedEnum = buildPrevSnapshot(
|
|
179
|
-
{
|
|
180
|
-
"public.users": snapshotTable("users", {
|
|
181
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
182
|
-
name: snapshotColumn("name", "varchar"),
|
|
183
|
-
email: snapshotColumn("email", "varchar")
|
|
184
|
-
})
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
"public.order_priority": snapshotEnum("order_priority", "public", ["low", "medium", "high"])
|
|
188
|
-
}
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const curJson = generateDrizzleJson({ managedUsers });
|
|
192
|
-
const statements = await generateMigration(prevWithUnmappedEnum as any, curJson as any);
|
|
193
|
-
const sql = statements.join("\n");
|
|
194
|
-
|
|
195
|
-
// Without filtering, drizzle-kit drops the unmapped enum
|
|
196
|
-
expect(sql).toContain("DROP TYPE");
|
|
197
|
-
expect(sql).toContain("order_priority");
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe("with tablesFilter applied: only managed tables enter the diff", () => {
|
|
202
|
-
|
|
203
|
-
it("should produce zero migration statements when managed schema is unchanged", async () => {
|
|
204
|
-
// Simulate: tablesFilter ensures only managed tables appear in the snapshot
|
|
205
|
-
const prevOnlyManaged = buildPrevSnapshot({
|
|
206
|
-
"public.users": snapshotTable("users", {
|
|
207
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
208
|
-
name: snapshotColumn("name", "varchar"),
|
|
209
|
-
email: snapshotColumn("email", "varchar")
|
|
210
|
-
}),
|
|
211
|
-
"public.posts": snapshotTable("posts", {
|
|
212
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
213
|
-
title: snapshotColumn("title", "varchar", { notNull: true }),
|
|
214
|
-
user_id: snapshotColumn("user_id", "varchar")
|
|
215
|
-
})
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const curJson = generateDrizzleJson({ managedUsers,
|
|
219
|
-
managedPosts });
|
|
220
|
-
const statements = await generateMigration(prevOnlyManaged as any, curJson as any);
|
|
221
|
-
|
|
222
|
-
// No changes needed — managed schema is identical
|
|
223
|
-
expect(statements.length).toBe(0);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("should correctly detect changes to managed tables without touching anything else", async () => {
|
|
227
|
-
// Prev: users has id + name (missing email)
|
|
228
|
-
const prevOnlyManaged = buildPrevSnapshot({
|
|
229
|
-
"public.users": snapshotTable("users", {
|
|
230
|
-
id: snapshotColumn("id", "varchar", { primaryKey: true }),
|
|
231
|
-
name: snapshotColumn("name", "varchar")
|
|
232
|
-
})
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const curJson = generateDrizzleJson({ managedUsers });
|
|
236
|
-
const statements = await generateMigration(prevOnlyManaged as any, curJson as any);
|
|
237
|
-
const sql = statements.join("\n");
|
|
238
|
-
|
|
239
|
-
// Should add the email column
|
|
240
|
-
expect(sql.toLowerCase()).toContain("alter table");
|
|
241
|
-
expect(sql).toContain("email");
|
|
242
|
-
|
|
243
|
-
// Should NOT contain any DROP TABLE
|
|
244
|
-
expect(sql).not.toContain("DROP TABLE");
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe("schema generation exports correct metadata for tablesFilter", () => {
|
|
249
|
-
|
|
250
|
-
it("should export a tables object containing all and only defined collection tables", async () => {
|
|
251
|
-
const collections: EntityCollection[] = [
|
|
252
|
-
{
|
|
253
|
-
slug: "products",
|
|
254
|
-
table: "products",
|
|
255
|
-
name: "Products",
|
|
256
|
-
properties: {
|
|
257
|
-
name: { type: "string" },
|
|
258
|
-
price: { type: "number" }
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
slug: "categories",
|
|
263
|
-
table: "categories",
|
|
264
|
-
name: "Categories",
|
|
265
|
-
properties: {
|
|
266
|
-
title: { type: "string" }
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
];
|
|
270
|
-
|
|
271
|
-
const result = await generateSchema(collections);
|
|
272
|
-
|
|
273
|
-
expect(result).toContain("export const tables = {");
|
|
274
|
-
expect(result).toContain("products");
|
|
275
|
-
expect(result).toContain("categories");
|
|
276
|
-
expect(result).not.toContain("legacy_orders");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("should include junction tables in the tables export for M2M relations", async () => {
|
|
280
|
-
const postsCollection: EntityCollection = {
|
|
1
|
+
import { getTableIncludesFromCollections } from "../src/cli-helpers";
|
|
2
|
+
|
|
3
|
+
describe("Unmapped tables safety with Atlas", () => {
|
|
4
|
+
it("should produce include filters only for managed tables and M2M junction tables", async () => {
|
|
5
|
+
const mockCollections = [
|
|
6
|
+
{
|
|
7
|
+
slug: "users",
|
|
8
|
+
table: "users",
|
|
9
|
+
properties: { name: { type: "string" } }
|
|
10
|
+
},
|
|
11
|
+
{
|
|
281
12
|
slug: "posts",
|
|
282
13
|
table: "posts",
|
|
283
|
-
name: "Posts",
|
|
284
14
|
properties: {
|
|
285
|
-
title: { type: "string" }
|
|
286
|
-
tags: { type: "relation",
|
|
287
|
-
relationName: "tags" }
|
|
15
|
+
title: { type: "string" }
|
|
288
16
|
},
|
|
289
|
-
relations: [
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
17
|
+
relations: [
|
|
18
|
+
{
|
|
19
|
+
relationName: "tags",
|
|
20
|
+
target: () => ({ table: "tags", slug: "tags" }),
|
|
21
|
+
cardinality: "many",
|
|
22
|
+
direction: "owning",
|
|
23
|
+
through: {
|
|
24
|
+
table: "posts_to_tags",
|
|
25
|
+
sourceColumn: "post_id",
|
|
26
|
+
targetColumn: "tag_id"
|
|
27
|
+
}
|
|
298
28
|
}
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const includes = await getTableIncludesFromCollections(mockCollections);
|
|
34
|
+
|
|
35
|
+
// Verify only managed tables and their junctions are included
|
|
36
|
+
expect(includes).toContain("public.users");
|
|
37
|
+
expect(includes).toContain("public.posts");
|
|
38
|
+
expect(includes).toContain("public.posts_to_tags");
|
|
39
|
+
|
|
40
|
+
// Verify unmapped tables are NOT included, ensuring they are ignored by Atlas --include filters
|
|
41
|
+
const unmappedTables = [
|
|
42
|
+
"public.legacy_orders",
|
|
43
|
+
"public.external_analytics",
|
|
44
|
+
"public.stripe_webhooks",
|
|
45
|
+
"public.audit_log"
|
|
46
|
+
];
|
|
47
|
+
for (const unmapped of unmappedTables) {
|
|
48
|
+
expect(includes).not.toContain(unmapped);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
314
51
|
|
|
315
|
-
|
|
316
|
-
|
|
52
|
+
it("should support custom schemas in table include paths", async () => {
|
|
53
|
+
const mockCollections = [
|
|
54
|
+
{
|
|
317
55
|
slug: "orders",
|
|
318
56
|
table: "orders",
|
|
319
|
-
|
|
320
|
-
properties: {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
enumValues: ["pending", "shipped", "delivered"]
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}];
|
|
327
|
-
|
|
328
|
-
const result = await generateSchema(collections);
|
|
329
|
-
|
|
330
|
-
expect(result).toContain("export const enums = {");
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
describe("drizzle.config.ts safety properties", () => {
|
|
335
|
-
|
|
336
|
-
it("schemaFilter should restrict to public schema only", () => {
|
|
337
|
-
const schemaFilter = ["public"];
|
|
338
|
-
expect(schemaFilter).toEqual(["public"]);
|
|
339
|
-
expect(schemaFilter).not.toContain("information_schema");
|
|
340
|
-
expect(schemaFilter).not.toContain("pg_catalog");
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("entities.roles should be false to prevent managing DB roles", () => {
|
|
344
|
-
const entities = { roles: false };
|
|
345
|
-
expect(entities.roles).toBe(false);
|
|
346
|
-
});
|
|
57
|
+
schema: "sales",
|
|
58
|
+
properties: { id: { type: "string" } }
|
|
59
|
+
}
|
|
60
|
+
];
|
|
347
61
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
62
|
+
const includes = await getTableIncludesFromCollections(mockCollections);
|
|
63
|
+
expect(includes).toContain("sales.orders");
|
|
64
|
+
expect(includes).not.toContain("public.orders");
|
|
352
65
|
});
|
|
353
66
|
});
|