@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgresql",
3
3
  "type": "module",
4
- "version": "0.0.1-canary.7f31c25",
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.7f31c25",
68
- "@rebasepro/types": "0.0.1-canary.7f31c25",
69
- "@rebasepro/server-core": "0.0.1-canary.7f31c25",
70
- "@rebasepro/utils": "0.0.1-canary.7f31c25"
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", "pull", "generate", "migrate", "studio", "branch"];
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 (meta.fks.some((fk) => fk.column_name === col.column_name)) continue;
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 enumValues from the PG enum
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 enumValues: [${enumEntries}],`;
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 hideFromCollection: true,`;
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 hideFromCollection: true,`;
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
- const relName = fk.column_name.replace(/_id$/, "");
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
- propsOutput += `
417
- ${sourceTableName}: {
418
- name: "${relHumanName}",
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
- propsOutput += `
454
- ${relPropName}: {
455
- name: "${relHumanName}",
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
- propsOutput += `
492
- ${targetTableName}: {
493
- name: "${relHumanName}",
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 for alphabetically-first table", () => {
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 for alphabetically-second table", () => {
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_ property name", () => {
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("users_via_friend");
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"');