@rebasepro/server-postgresql 0.0.1-canary.7f31c25 → 0.0.1-canary.892f711
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 +5 -5
- package/src/cli.ts +1 -1
- package/src/schema/introspect-db-logic.ts +32 -30
- package/test/introspect-db-generation.test.ts +27 -5
- package/jest-all.log +0 -3128
- package/jest.log +0 -49
- package/scratch.ts +0 -41
- package/test-drizzle-bug.ts +0 -18
- package/test-drizzle-out/0000_cultured_freak.sql +0 -7
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
- package/test-drizzle-out/meta/0000_snapshot.json +0 -55
- package/test-drizzle-out/meta/0001_snapshot.json +0 -63
- package/test-drizzle-out/meta/_journal.json +0 -20
- package/test-drizzle-prompt.sh +0 -2
- package/test-policy-prompt.sh +0 -3
- package/test-programmatic.ts +0 -30
- package/test-programmatic2.ts +0 -59
- package/test-schema-no-policies.ts +0 -12
- package/test_drizzle_mock.js +0 -3
- package/test_find_changed.mjs +0 -32
- package/test_hash.js +0 -14
- package/test_output.txt +0 -3145
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgresql",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.1-canary.
|
|
4
|
+
"version": "0.0.1-canary.892f711",
|
|
5
5
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"drizzle-orm": "^0.44.4",
|
|
65
65
|
"execa": "^4.1.0",
|
|
66
66
|
"pg": "^8.11.3",
|
|
67
|
-
"@rebasepro/common": "0.0.1-canary.
|
|
68
|
-
"@rebasepro/
|
|
69
|
-
"@rebasepro/
|
|
70
|
-
"@rebasepro/utils": "0.0.1-canary.
|
|
67
|
+
"@rebasepro/common": "0.0.1-canary.892f711",
|
|
68
|
+
"@rebasepro/server-core": "0.0.1-canary.892f711",
|
|
69
|
+
"@rebasepro/types": "0.0.1-canary.892f711",
|
|
70
|
+
"@rebasepro/utils": "0.0.1-canary.892f711"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/jest": "^29.5.14",
|
package/src/cli.ts
CHANGED
|
@@ -43,7 +43,7 @@ export async function runPluginCommand(args: string[]) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
46
|
-
const VALID_ACTIONS = ["push", "
|
|
46
|
+
const VALID_ACTIONS = ["push", "generate", "migrate", "studio", "branch"];
|
|
47
47
|
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
48
48
|
console.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
49
49
|
process.exit(1);
|
|
@@ -281,6 +281,7 @@ export function generateCollectionFile(
|
|
|
281
281
|
const imports = new Set<string>(['import { PostgresCollection } from "@rebasepro/types";']);
|
|
282
282
|
|
|
283
283
|
let propsOutput = ``;
|
|
284
|
+
let relationsOutput = ``;
|
|
284
285
|
const propertiesOrder: string[] = [];
|
|
285
286
|
|
|
286
287
|
// Detect composite primary keys
|
|
@@ -289,7 +290,8 @@ export function generateCollectionFile(
|
|
|
289
290
|
// Map columns
|
|
290
291
|
for (const col of meta.columns) {
|
|
291
292
|
// Skip foreign keys since we handle them as relations
|
|
292
|
-
if
|
|
293
|
+
// Exception: Do not skip if it's part of the primary key!
|
|
294
|
+
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;
|
|
293
295
|
|
|
294
296
|
propertiesOrder.push(col.column_name);
|
|
295
297
|
|
|
@@ -302,22 +304,22 @@ export function generateCollectionFile(
|
|
|
302
304
|
|
|
303
305
|
const colNameLower = col.column_name.toLowerCase();
|
|
304
306
|
|
|
305
|
-
// Enum values — generate real
|
|
307
|
+
// Enum values — generate real enum from the PG enum
|
|
306
308
|
if (isEnumColumn && colEnumValues) {
|
|
307
309
|
const enumEntries = colEnumValues
|
|
308
310
|
.map((v) => `{ id: "${v}", label: "${humanize(v)}" }`)
|
|
309
311
|
.join(", ");
|
|
310
|
-
extra = `\n
|
|
312
|
+
extra = `\n enum: [${enumEntries}],`;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
// Date auto-value heuristics
|
|
314
316
|
if (propType === "date") {
|
|
315
317
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
316
|
-
extra = `\n autoValue: "on_create",\n readOnly: true,\n
|
|
318
|
+
extra = `\n autoValue: "on_create",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
|
|
317
319
|
} else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
|
|
318
|
-
extra = `\n autoValue: "on_update",\n readOnly: true,\n
|
|
320
|
+
extra = `\n autoValue: "on_update",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
|
|
319
321
|
} else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
|
|
320
|
-
extra = `\n autoValue: "on_create",\n readOnly: true,`;
|
|
322
|
+
extra = `\n autoValue: "on_create",\n ui: {\n readOnly: true\n },`;
|
|
321
323
|
}
|
|
322
324
|
}
|
|
323
325
|
|
|
@@ -330,7 +332,7 @@ export function generateCollectionFile(
|
|
|
330
332
|
// We'll just call mapPgType on the baseType
|
|
331
333
|
innerType = mapPgType(baseType);
|
|
332
334
|
}
|
|
333
|
-
extra = `\n of: { type: "${innerType}" },`;
|
|
335
|
+
extra = `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
|
|
334
336
|
} else if (propType === "map") {
|
|
335
337
|
extra = `\n keyValue: true,`;
|
|
336
338
|
}
|
|
@@ -379,7 +381,12 @@ export function generateCollectionFile(
|
|
|
379
381
|
for (const fk of meta.fks) {
|
|
380
382
|
const targetTableName = fk.foreign_table_name;
|
|
381
383
|
if (!joinTables.has(targetTableName)) {
|
|
382
|
-
|
|
384
|
+
let relName = fk.column_name.replace(/_id$/, "");
|
|
385
|
+
if (meta.pks.includes(fk.column_name) && relName === fk.column_name) {
|
|
386
|
+
// If the FK is also the PK and its name doesn't imply a relation (like "id"),
|
|
387
|
+
// use the target table name to avoid conflicting with the PK property.
|
|
388
|
+
relName = targetTableName;
|
|
389
|
+
}
|
|
383
390
|
// Push the relation property key, not the FK column name
|
|
384
391
|
propertiesOrder.push(relName);
|
|
385
392
|
|
|
@@ -402,21 +409,19 @@ export function generateCollectionFile(
|
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
// Map Inverse Relations (1-to-many where OTHER table points to THIS table)
|
|
412
|
+
// These go into the `relations` array so they render as subcollection tabs.
|
|
405
413
|
const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name));
|
|
406
414
|
for (const fk of inverseFks) {
|
|
407
415
|
const sourceTableName = fk.table_name;
|
|
408
|
-
propertiesOrder.push(sourceTableName);
|
|
409
416
|
|
|
410
417
|
const targetCollectionCamel = toCollectionVarName(sourceTableName);
|
|
411
418
|
imports.add(`import ${targetCollectionCamel} from "./${sourceTableName}";`);
|
|
412
419
|
|
|
413
420
|
const inverseRelName = fk.column_name.replace(/_id$/, "");
|
|
414
|
-
const relHumanName = humanize(sourceTableName);
|
|
415
421
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
type: "relation",
|
|
422
|
+
relationsOutput += `
|
|
423
|
+
{
|
|
424
|
+
relationName: "${sourceTableName}",
|
|
420
425
|
target: () => ${targetCollectionCamel},
|
|
421
426
|
cardinality: "many",
|
|
422
427
|
direction: "inverse",
|
|
@@ -426,6 +431,7 @@ export function generateCollectionFile(
|
|
|
426
431
|
}
|
|
427
432
|
|
|
428
433
|
// Map Many-to-Many Relations (Join Tables)
|
|
434
|
+
// These also go into the `relations` array so they render as subcollection tabs.
|
|
429
435
|
const relatedJoinTables = Array.from(joinTables).filter((jt) => {
|
|
430
436
|
const jtMeta = tablesMap.get(jt);
|
|
431
437
|
return jtMeta ? jtMeta.fks.some((fk) => fk.foreign_table_name === tableName) : false;
|
|
@@ -445,15 +451,10 @@ export function generateCollectionFile(
|
|
|
445
451
|
const otherFk = selfRefFks[1];
|
|
446
452
|
|
|
447
453
|
const relPropName = `${tableName}_via_${otherFk.column_name.replace(/_id$/, "")}`;
|
|
448
|
-
propertiesOrder.push(relPropName);
|
|
449
|
-
|
|
450
|
-
// Self-ref: import is the same collection (use a lazy reference)
|
|
451
|
-
const relHumanName = humanize(otherFk.column_name.replace(/_id$/, ""));
|
|
452
454
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
type: "relation",
|
|
455
|
+
relationsOutput += `
|
|
456
|
+
{
|
|
457
|
+
relationName: "${relPropName}",
|
|
457
458
|
target: () => ${tableName}Collection,
|
|
458
459
|
cardinality: "many",
|
|
459
460
|
direction: "owning",
|
|
@@ -470,7 +471,6 @@ export function generateCollectionFile(
|
|
|
470
471
|
|
|
471
472
|
if (otherFk) {
|
|
472
473
|
const targetTableName = otherFk.foreign_table_name;
|
|
473
|
-
propertiesOrder.push(targetTableName);
|
|
474
474
|
|
|
475
475
|
const targetCollectionCamel = toCollectionVarName(targetTableName);
|
|
476
476
|
imports.add(`import ${targetCollectionCamel} from "./${targetTableName}";`);
|
|
@@ -479,19 +479,17 @@ export function generateCollectionFile(
|
|
|
479
479
|
const direction = tableName < targetTableName ? "owning" : "inverse";
|
|
480
480
|
|
|
481
481
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
482
|
-
const relHumanName = humanize(targetTableName);
|
|
483
482
|
|
|
484
483
|
let throughCode = "";
|
|
485
484
|
if (direction === "owning" && thisFk) {
|
|
486
|
-
throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n }
|
|
485
|
+
throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n },`;
|
|
487
486
|
} else if (direction === "inverse") {
|
|
488
487
|
throughCode = `\n // Make sure the target collection configures the 'through' property.`;
|
|
489
488
|
}
|
|
490
489
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
type: "relation",
|
|
490
|
+
relationsOutput += `
|
|
491
|
+
{
|
|
492
|
+
relationName: "${targetTableName}",
|
|
495
493
|
target: () => ${targetCollectionCamel},
|
|
496
494
|
cardinality: "many",
|
|
497
495
|
direction: "${direction}",${throughCode}
|
|
@@ -499,6 +497,10 @@ export function generateCollectionFile(
|
|
|
499
497
|
}
|
|
500
498
|
}
|
|
501
499
|
|
|
500
|
+
const relationsBlock = relationsOutput
|
|
501
|
+
? `\n relations: [${relationsOutput}\n ],`
|
|
502
|
+
: "";
|
|
503
|
+
|
|
502
504
|
const fileContent = `${Array.from(imports).join("\n")}
|
|
503
505
|
|
|
504
506
|
const ${tableName}Collection: PostgresCollection = {
|
|
@@ -509,7 +511,7 @@ const ${tableName}Collection: PostgresCollection = {
|
|
|
509
511
|
icon: "${icon}",
|
|
510
512
|
group: "App",
|
|
511
513
|
properties: {${propsOutput}
|
|
512
|
-
}
|
|
514
|
+
},${relationsBlock}
|
|
513
515
|
propertiesOrder: ${JSON.stringify(propertiesOrder, null, 8).replace(/]$/, " ]")}
|
|
514
516
|
};
|
|
515
517
|
|
|
@@ -285,22 +285,40 @@ describe("generateCollectionFile", () => {
|
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
describe("inverse relation (other table -> this table)", () => {
|
|
288
|
-
it("generates a one-to-many inverse relation", () => {
|
|
288
|
+
it("generates a one-to-many inverse relation in the relations array", () => {
|
|
289
289
|
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
290
290
|
const meta = makeSimpleTable("posts", [
|
|
291
291
|
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
292
292
|
]);
|
|
293
293
|
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
294
294
|
expect(result).toContain('import commentsCollection from "./comments"');
|
|
295
|
+
// Should be in the relations array, not as an inline property
|
|
296
|
+
expect(result).toContain('relations: [');
|
|
297
|
+
expect(result).toContain('relationName: "comments"');
|
|
295
298
|
expect(result).toContain('cardinality: "many"');
|
|
296
299
|
expect(result).toContain('direction: "inverse"');
|
|
297
300
|
expect(result).toContain('inverseRelationName: "post"');
|
|
298
301
|
expect(result).toContain('foreignKeyOnTarget: "post_id"');
|
|
302
|
+
// Should NOT appear as an inline property with type: "relation"
|
|
303
|
+
const propsSection = result.split('properties:')[1].split('relations:')[0];
|
|
304
|
+
expect(propsSection).not.toContain('"relation"');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("does NOT include inverse relations in propertiesOrder", () => {
|
|
308
|
+
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
309
|
+
const meta = makeSimpleTable("posts", [
|
|
310
|
+
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
311
|
+
]);
|
|
312
|
+
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
313
|
+
const orderMatch = result.match(/propertiesOrder:\s*(\[[\s\S]*?\])/);
|
|
314
|
+
expect(orderMatch).toBeTruthy();
|
|
315
|
+
const orderBlock = orderMatch![1];
|
|
316
|
+
expect(orderBlock).not.toContain('"comments"');
|
|
299
317
|
});
|
|
300
318
|
});
|
|
301
319
|
|
|
302
320
|
describe("many-to-many relations", () => {
|
|
303
|
-
it("generates owning M2M with through config
|
|
321
|
+
it("generates owning M2M with through config in relations array", () => {
|
|
304
322
|
const jtFks: ForeignKeyRow[] = [
|
|
305
323
|
mkFk("articles_tags", "article_id", "articles"),
|
|
306
324
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -317,13 +335,15 @@ describe("generateCollectionFile", () => {
|
|
|
317
335
|
const joinTables = new Set(["articles_tags"]);
|
|
318
336
|
|
|
319
337
|
const result = generateCollectionFile("articles", articlesMeta, [], joinTables, tablesMap, new Map());
|
|
338
|
+
expect(result).toContain('relations: [');
|
|
339
|
+
expect(result).toContain('relationName: "tags"');
|
|
320
340
|
expect(result).toContain('direction: "owning"');
|
|
321
341
|
expect(result).toContain('table: "articles_tags"');
|
|
322
342
|
expect(result).toContain('sourceColumn: "article_id"');
|
|
323
343
|
expect(result).toContain('targetColumn: "tag_id"');
|
|
324
344
|
});
|
|
325
345
|
|
|
326
|
-
it("generates inverse M2M
|
|
346
|
+
it("generates inverse M2M in relations array", () => {
|
|
327
347
|
const jtFks: ForeignKeyRow[] = [
|
|
328
348
|
mkFk("articles_tags", "article_id", "articles"),
|
|
329
349
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -340,12 +360,13 @@ describe("generateCollectionFile", () => {
|
|
|
340
360
|
const joinTables = new Set(["articles_tags"]);
|
|
341
361
|
|
|
342
362
|
const result = generateCollectionFile("tags", tagsMeta, [], joinTables, tablesMap, new Map());
|
|
363
|
+
expect(result).toContain('relations: [');
|
|
343
364
|
expect(result).toContain('direction: "inverse"');
|
|
344
365
|
});
|
|
345
366
|
});
|
|
346
367
|
|
|
347
368
|
describe("self-referencing M2M", () => {
|
|
348
|
-
it("generates self-ref M2M with _via_
|
|
369
|
+
it("generates self-ref M2M with _via_ relation name in relations array", () => {
|
|
349
370
|
const jtFks: ForeignKeyRow[] = [
|
|
350
371
|
mkFk("user_friends", "user_id", "users"),
|
|
351
372
|
mkFk("user_friends", "friend_id", "users"),
|
|
@@ -362,7 +383,8 @@ describe("generateCollectionFile", () => {
|
|
|
362
383
|
const joinTables = new Set(["user_friends"]);
|
|
363
384
|
|
|
364
385
|
const result = generateCollectionFile("users", usersMeta, [], joinTables, tablesMap, new Map());
|
|
365
|
-
expect(result).toContain(
|
|
386
|
+
expect(result).toContain('relations: [');
|
|
387
|
+
expect(result).toContain('relationName: "users_via_friend"');
|
|
366
388
|
expect(result).toContain('table: "user_friends"');
|
|
367
389
|
expect(result).toContain('sourceColumn: "user_id"');
|
|
368
390
|
expect(result).toContain('targetColumn: "friend_id"');
|