@rebasepro/server-postgresql 0.0.1-canary.eae7889 → 0.1.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 (70) hide show
  1. package/dist/index.es.js +458 -201
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +458 -201
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +8 -1
  6. package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
  7. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +117 -0
  8. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  9. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
  10. package/dist/types/src/controllers/auth.d.ts +8 -2
  11. package/dist/types/src/controllers/client.d.ts +13 -0
  12. package/dist/types/src/controllers/collection_registry.d.ts +2 -1
  13. package/dist/types/src/controllers/data_driver.d.ts +36 -1
  14. package/dist/types/src/controllers/navigation.d.ts +18 -6
  15. package/dist/types/src/controllers/registry.d.ts +9 -1
  16. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
  17. package/dist/types/src/rebase_context.d.ts +17 -0
  18. package/dist/types/src/types/backend_hooks.d.ts +187 -0
  19. package/dist/types/src/types/collections.d.ts +31 -11
  20. package/dist/types/src/types/component_ref.d.ts +47 -0
  21. package/dist/types/src/types/cron.d.ts +1 -1
  22. package/dist/types/src/types/entity_views.d.ts +6 -7
  23. package/dist/types/src/types/formex.d.ts +40 -0
  24. package/dist/types/src/types/index.d.ts +3 -0
  25. package/dist/types/src/types/plugins.d.ts +6 -3
  26. package/dist/types/src/types/properties.d.ts +72 -88
  27. package/dist/types/src/types/slots.d.ts +20 -10
  28. package/dist/types/src/types/translations.d.ts +6 -0
  29. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +21 -0
  30. package/examples/sdk-demo/node_modules/esbuild/README.md +3 -0
  31. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +223 -0
  32. package/examples/sdk-demo/node_modules/esbuild/install.js +289 -0
  33. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +716 -0
  34. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +2242 -0
  35. package/examples/sdk-demo/node_modules/esbuild/package.json +49 -0
  36. package/package.json +6 -5
  37. package/src/PostgresBackendDriver.ts +32 -6
  38. package/src/cli.ts +68 -2
  39. package/src/data-transformer.ts +84 -1
  40. package/src/schema/doctor.ts +14 -2
  41. package/src/schema/generate-drizzle-schema-logic.ts +59 -30
  42. package/src/schema/introspect-db-inference.ts +238 -0
  43. package/src/schema/introspect-db-logic.ts +896 -0
  44. package/src/schema/introspect-db.ts +254 -0
  45. package/src/services/EntityFetchService.ts +16 -0
  46. package/src/services/EntityPersistService.ts +95 -13
  47. package/test/generate-drizzle-schema.test.ts +342 -0
  48. package/test/introspect-db-generation.test.ts +458 -0
  49. package/test/introspect-db-utils.test.ts +392 -0
  50. package/test/property-ordering.test.ts +395 -0
  51. package/test/relations.test.ts +4 -4
  52. package/test/unmapped-tables-safety.test.ts +345 -0
  53. package/jest-all.log +0 -3128
  54. package/jest.log +0 -49
  55. package/scratch.ts +0 -41
  56. package/test-drizzle-bug.ts +0 -18
  57. package/test-drizzle-out/0000_cultured_freak.sql +0 -7
  58. package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
  59. package/test-drizzle-out/meta/0000_snapshot.json +0 -55
  60. package/test-drizzle-out/meta/0001_snapshot.json +0 -63
  61. package/test-drizzle-out/meta/_journal.json +0 -20
  62. package/test-drizzle-prompt.sh +0 -2
  63. package/test-policy-prompt.sh +0 -3
  64. package/test-programmatic.ts +0 -30
  65. package/test-programmatic2.ts +0 -59
  66. package/test-schema-no-policies.ts +0 -12
  67. package/test_drizzle_mock.js +0 -3
  68. package/test_find_changed.mjs +0 -32
  69. package/test_hash.js +0 -14
  70. package/test_output.txt +0 -3145
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "esbuild",
3
+ "version": "0.27.3",
4
+ "description": "An extremely fast JavaScript and CSS bundler and minifier.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/evanw/esbuild.git"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node install.js"
11
+ },
12
+ "main": "lib/main.js",
13
+ "types": "lib/main.d.ts",
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "bin": {
18
+ "esbuild": "bin/esbuild"
19
+ },
20
+ "optionalDependencies": {
21
+ "@esbuild/aix-ppc64": "0.27.3",
22
+ "@esbuild/android-arm": "0.27.3",
23
+ "@esbuild/android-arm64": "0.27.3",
24
+ "@esbuild/android-x64": "0.27.3",
25
+ "@esbuild/darwin-arm64": "0.27.3",
26
+ "@esbuild/darwin-x64": "0.27.3",
27
+ "@esbuild/freebsd-arm64": "0.27.3",
28
+ "@esbuild/freebsd-x64": "0.27.3",
29
+ "@esbuild/linux-arm": "0.27.3",
30
+ "@esbuild/linux-arm64": "0.27.3",
31
+ "@esbuild/linux-ia32": "0.27.3",
32
+ "@esbuild/linux-loong64": "0.27.3",
33
+ "@esbuild/linux-mips64el": "0.27.3",
34
+ "@esbuild/linux-ppc64": "0.27.3",
35
+ "@esbuild/linux-riscv64": "0.27.3",
36
+ "@esbuild/linux-s390x": "0.27.3",
37
+ "@esbuild/linux-x64": "0.27.3",
38
+ "@esbuild/netbsd-arm64": "0.27.3",
39
+ "@esbuild/netbsd-x64": "0.27.3",
40
+ "@esbuild/openbsd-arm64": "0.27.3",
41
+ "@esbuild/openbsd-x64": "0.27.3",
42
+ "@esbuild/openharmony-arm64": "0.27.3",
43
+ "@esbuild/sunos-x64": "0.27.3",
44
+ "@esbuild/win32-arm64": "0.27.3",
45
+ "@esbuild/win32-ia32": "0.27.3",
46
+ "@esbuild/win32-x64": "0.27.3"
47
+ },
48
+ "license": "MIT"
49
+ }
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.eae7889",
4
+ "version": "0.1.0",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -60,13 +60,14 @@
60
60
  "dependencies": {
61
61
  "arg": "^5.0.2",
62
62
  "chalk": "^4.1.2",
63
+ "chokidar": "5.0.0",
63
64
  "drizzle-orm": "^0.44.4",
64
65
  "execa": "^4.1.0",
65
66
  "pg": "^8.11.3",
66
- "@rebasepro/common": "0.0.1-canary.eae7889",
67
- "@rebasepro/server-core": "0.0.1-canary.eae7889",
68
- "@rebasepro/utils": "0.0.1-canary.eae7889",
69
- "@rebasepro/types": "0.0.1-canary.eae7889"
67
+ "@rebasepro/server-core": "0.1.0",
68
+ "@rebasepro/common": "0.1.0",
69
+ "@rebasepro/utils": "0.1.0",
70
+ "@rebasepro/types": "0.1.0"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@types/jest": "^29.5.14",
@@ -4,9 +4,9 @@ import { BranchService } from "./services/BranchService";
4
4
  import { RealtimeService } from "./services/realtimeService";
5
5
  import { DatabasePoolManager } from "./databasePoolManager";
6
6
  import { DrizzleClient } from "./interfaces";
7
- import { User } from "@rebasepro/types";
7
+ import { User, RebaseClient } from "@rebasepro/types";
8
8
  import { sql as drizzleSql } from "drizzle-orm";
9
- import { buildPropertyCallbacks } from "@rebasepro/common";
9
+ import { buildPropertyCallbacks, updateDateAutoValues } from "@rebasepro/common";
10
10
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
11
11
  import {
12
12
  DataDriver,
@@ -44,6 +44,7 @@ export class PostgresBackendDriver implements DataDriver {
44
44
  public branchService?: BranchService;
45
45
  public user?: User;
46
46
  public data: RebaseData;
47
+ public client?: RebaseClient;
47
48
 
48
49
  /**
49
50
  * When true, realtime notifications are deferred until after the
@@ -101,6 +102,15 @@ export class PostgresBackendDriver implements DataDriver {
101
102
  };
102
103
  }
103
104
 
105
+ /**
106
+ * REST-optimised fetch service (include-aware eager-loading).
107
+ * Delegates to the underlying EntityFetchService which already
108
+ * implements the matching method signatures.
109
+ */
110
+ get restFetchService() {
111
+ return this.entityService.getFetchService();
112
+ }
113
+
104
114
 
105
115
  private resolveCollectionCallbacks<M extends Record<string, unknown>>(collection: EntityCollection<M> | undefined, path: string) {
106
116
  if (!collection && !path) return { collection: undefined,
@@ -154,7 +164,8 @@ propertyCallbacks: undefined };
154
164
  const contextForCallback = {
155
165
  user: this.user,
156
166
  driver: this,
157
- data: this.data
167
+ data: this.data,
168
+ client: this.client
158
169
  } as unknown as RebaseCallContext; // Backend context
159
170
  return Promise.all(entities.map(async (entity) => {
160
171
  let fetched = entity;
@@ -263,7 +274,8 @@ propertyCallbacks: undefined };
263
274
  const contextForCallback = {
264
275
  user: this.user,
265
276
  driver: this,
266
- data: this.data
277
+ data: this.data,
278
+ client: this.client
267
279
  } as unknown as RebaseCallContext; // Backend context
268
280
  if (callbacks?.afterRead) {
269
281
  entity = await callbacks.afterRead({
@@ -345,7 +357,8 @@ propertyCallbacks: undefined };
345
357
  const contextForCallback = {
346
358
  user: this.user,
347
359
  driver: this,
348
- data: this.data
360
+ data: this.data,
361
+ client: this.client
349
362
  } as unknown as RebaseCallContext;
350
363
 
351
364
  // Fetch previous values for callbacks AND history recording
@@ -386,6 +399,17 @@ propertyCallbacks: undefined };
386
399
 
387
400
  }
388
401
 
402
+ // Apply autoValue timestamps (on_create / on_update) at the application layer.
403
+ // This handles updated_at fields for all writes that flow through the Rebase backend.
404
+ if (resolvedCollection?.properties) {
405
+ updatedValues = updateDateAutoValues({
406
+ inputValues: updatedValues,
407
+ properties: resolvedCollection.properties,
408
+ status: status ?? "new",
409
+ timestampNowValue: new Date()
410
+ });
411
+ }
412
+
389
413
  try {
390
414
  let savedEntity = await this.entityService.saveEntity<M>(
391
415
  path,
@@ -508,7 +532,8 @@ propertyCallbacks: undefined };
508
532
  const contextForCallback = {
509
533
  user: this.user,
510
534
  driver: this,
511
- data: this.data
535
+ data: this.data,
536
+ client: this.client
512
537
  } as unknown as RebaseCallContext;
513
538
 
514
539
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
@@ -931,6 +956,7 @@ roles: userRoles })}, true)
931
956
  txDelegate.entityService = txEntityService;
932
957
  txDelegate._deferNotifications = true;
933
958
  txDelegate._pendingNotifications = pendingNotifications;
959
+ txDelegate.client = this.delegate.client;
934
960
 
935
961
  return await operation(txDelegate);
936
962
  });
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);
@@ -68,6 +68,12 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
68
68
  console.log("");
69
69
  console.log(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
70
70
  console.log("");
71
+ } else if (subcommand === "pull") {
72
+ console.log("");
73
+ console.log(chalk.yellow(" ⚠ \"rebase db pull\" has been removed."));
74
+ console.log(chalk.gray(" Use \"rebase schema introspect\" instead."));
75
+ console.log("");
76
+ process.exit(1);
71
77
  } else {
72
78
  console.log("");
73
79
  console.log(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
@@ -408,9 +414,16 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
408
414
 
409
415
  const interactive = ["generate", "push"].includes(action);
410
416
 
417
+ // For push: always use --strict (prompts before destructive ops) and --verbose
418
+ // (shows all SQL). This ensures unmapped tables are never silently dropped.
419
+ const drizzleKitArgs = [action];
420
+ if (action === "push") {
421
+ drizzleKitArgs.push("--strict", "--verbose");
422
+ }
423
+
411
424
  try {
412
425
  if (interactive) {
413
- await execa(drizzleKitBin, [action], {
426
+ await execa(drizzleKitBin, drizzleKitArgs, {
414
427
  cwd: process.cwd(),
415
428
  stdio: "inherit",
416
429
  env
@@ -527,6 +540,59 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
527
540
  console.error(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
528
541
  process.exit(1);
529
542
  }
543
+ } else if (subcommand === "introspect") {
544
+ const argsList = arg(
545
+ {
546
+ "--output": String,
547
+ "--collections": String,
548
+ "--force": Boolean,
549
+ "--schema": String,
550
+ "-o": "--output",
551
+ "-c": "--collections",
552
+ "-f": "--force"
553
+ },
554
+ {
555
+ argv: rawArgs.slice(2),
556
+ permissive: true
557
+ }
558
+ );
559
+
560
+ const introspectScript = path.join(__dirname, "schema", "introspect-db.ts");
561
+ if (!fs.existsSync(introspectScript)) {
562
+ console.error(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
563
+ process.exit(1);
564
+ }
565
+
566
+ const tsxBin = resolveLocalBin("tsx");
567
+ if (!tsxBin) {
568
+ console.error(chalk.red("✗ Could not find tsx binary."));
569
+ process.exit(1);
570
+ }
571
+
572
+ const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
573
+
574
+ console.log("");
575
+ console.log(chalk.bold(" 🔍 Rebase Schema Introspector"));
576
+ console.log("");
577
+
578
+ const cmdParts = [
579
+ tsxBin,
580
+ introspectScript,
581
+ `--output=${outputPath}`,
582
+ ...(argsList["--force"] ? ["--force"] : []),
583
+ ...(argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : [])
584
+ ];
585
+
586
+ try {
587
+ await execa(cmdParts[0], cmdParts.slice(1), {
588
+ cwd: process.cwd(),
589
+ stdio: "inherit",
590
+ env: { ...process.env as Record<string, string> }
591
+ });
592
+ } catch (err: unknown) {
593
+ console.error(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
594
+ process.exit(1);
595
+ }
530
596
  } else {
531
597
  console.error(chalk.red("Unknown schema command."));
532
598
  process.exit(1);
@@ -258,7 +258,26 @@ export function serializePropertyToServer(value: unknown, property: Property): u
258
258
  }
259
259
  return value;
260
260
 
261
+ case "string":
262
+ if (typeof value === "string") {
263
+ if (value.startsWith("data:application/octet-stream;base64,")) {
264
+ const base64Data = value.split(",")[1];
265
+ if (base64Data) {
266
+ return Buffer.from(base64Data, "base64");
267
+ }
268
+ }
269
+ }
270
+ return value;
271
+
261
272
  default:
273
+ if (typeof value === "string") {
274
+ if (value.startsWith("data:application/octet-stream;base64,")) {
275
+ const base64Data = value.split(",")[1];
276
+ if (base64Data) {
277
+ return Buffer.from(base64Data, "base64");
278
+ }
279
+ }
280
+ }
262
281
  return value;
263
282
  }
264
283
  }
@@ -447,6 +466,45 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
447
466
  }
448
467
 
449
468
  switch (property.type) {
469
+ case "string": {
470
+ if (typeof value === "string") return value;
471
+
472
+ // Handle Buffer objects (e.g. from PostgreSQL bytea columns)
473
+ let isBuffer = false;
474
+ let buf: Buffer | null = null;
475
+
476
+ if (Buffer.isBuffer(value)) {
477
+ isBuffer = true;
478
+ buf = value;
479
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
480
+ isBuffer = true;
481
+ buf = Buffer.from((value as any).data);
482
+ }
483
+
484
+ if (isBuffer && buf) {
485
+ // Heuristic: if all bytes are printable ASCII, return utf8, else base64
486
+ let isPrintable = true;
487
+ for (let i = 0; i < buf.length; i++) {
488
+ const b = buf[i];
489
+ // Allow standard printable ASCII + common whitespace (\r, \n, \t)
490
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
491
+ isPrintable = false;
492
+ break;
493
+ }
494
+ }
495
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
496
+ }
497
+
498
+ if (typeof value === "object" && value !== null) {
499
+ try {
500
+ return JSON.stringify(value);
501
+ } catch {
502
+ return String(value);
503
+ }
504
+ }
505
+ return String(value);
506
+ }
507
+
450
508
  case "relation":
451
509
  // Transform ID back to relation object with type information
452
510
  if (typeof value === "string" || typeof value === "number") {
@@ -538,8 +596,33 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
538
596
  return null;
539
597
  }
540
598
 
541
- default:
599
+ default: {
600
+ // Fallback for buffers in case they are mapped to something other than string
601
+ let isBuffer = false;
602
+ let buf: Buffer | null = null;
603
+
604
+ if (Buffer.isBuffer(value)) {
605
+ isBuffer = true;
606
+ buf = value;
607
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
608
+ isBuffer = true;
609
+ buf = Buffer.from((value as any).data);
610
+ }
611
+
612
+ if (isBuffer && buf) {
613
+ let isPrintable = true;
614
+ for (let i = 0; i < buf.length; i++) {
615
+ const b = buf[i];
616
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
617
+ isPrintable = false;
618
+ break;
619
+ }
620
+ }
621
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
622
+ }
623
+
542
624
  return value;
625
+ }
543
626
  }
544
627
  }
545
628
 
@@ -17,6 +17,18 @@ import { generateSchema } from "./generate-drizzle-schema-logic";
17
17
  import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
18
18
  import { toSnakeCase } from "@rebasepro/utils";
19
19
 
20
+ /**
21
+ * Resolve the SQL column name for a property.
22
+ * Uses the explicit `columnName` when set (e.g. from introspection),
23
+ * falling back to `toSnakeCase(propName)` for manually-authored collections.
24
+ */
25
+ const resolveColumnName = (propName: string, prop?: Property | null): string => {
26
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") {
27
+ return prop.columnName;
28
+ }
29
+ return toSnakeCase(propName);
30
+ };
31
+
20
32
  // ── Types ────────────────────────────────────────────────────────────────
21
33
 
22
34
  export type IssueSeverity = "error" | "warning" | "info";
@@ -316,7 +328,7 @@ export async function checkCollectionsVsDatabase(
316
328
  const resolvedRelations = resolveCollectionRelations(collection);
317
329
  const relation = findRelation(resolvedRelations, (prop as RelationProperty).relationName ?? propName);
318
330
  if (relation?.direction === "owning" && relation.cardinality === "one" && relation.localKey) {
319
- const fkColName = toSnakeCase(relation.localKey);
331
+ const fkColName = relation.localKey;
320
332
  if (!dbColumnMap.has(fkColName)) {
321
333
  issues.push({
322
334
  severity: "error",
@@ -349,7 +361,7 @@ export async function checkCollectionsVsDatabase(
349
361
  continue;
350
362
  }
351
363
 
352
- const colName = toSnakeCase(propName);
364
+ const colName = resolveColumnName(propName, prop);
353
365
 
354
366
  // Skip system columns — they're handled automatically
355
367
  if (systemColumns.has(colName)) continue;
@@ -5,6 +5,18 @@ import { toSnakeCase } from "@rebasepro/utils";
5
5
  import { createHash } from "crypto";
6
6
  // --- Helper Functions ---
7
7
 
8
+ /**
9
+ * Resolve the SQL column name for a property.
10
+ * Uses the explicit `columnName` when set (e.g. from introspection),
11
+ * falling back to `toSnakeCase(propName)` for manually-authored collections.
12
+ */
13
+ const resolveColumnName = (propName: string, prop?: Property | null): string => {
14
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") {
15
+ return prop.columnName;
16
+ }
17
+ return toSnakeCase(propName);
18
+ };
19
+
8
20
  const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number", isUuid: boolean } => {
9
21
  if (collection.properties) {
10
22
  const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
@@ -46,7 +58,7 @@ const isIdProperty = (propName: string, prop: Property, collection: EntityCollec
46
58
  };
47
59
 
48
60
  const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCollection, collections: EntityCollection[]): string | null => {
49
- const colName = toSnakeCase(propName);
61
+ const colName = resolveColumnName(propName, prop);
50
62
  let columnDefinition: string;
51
63
 
52
64
  switch (prop.type) {
@@ -126,6 +138,10 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
126
138
  } else {
127
139
  columnDefinition = `timestamp("${colName}", { withTimezone: true, mode: 'string' })`;
128
140
  }
141
+ // autoValue: database-level default for initial value on INSERT
142
+ if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
143
+ columnDefinition += `.default(sql\`now()\`)`;
144
+ }
129
145
  break;
130
146
  }
131
147
  case "map":
@@ -167,7 +183,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
167
183
  return null; // Cannot resolve target
168
184
  }
169
185
 
170
- const fkColumnName = toSnakeCase(relation.localKey);
186
+ const fkColumnName = relation.localKey;
171
187
  const targetTableVar = getTableVarName(getTableName(targetCollection));
172
188
  const pkProp = getPrimaryKeyProp(targetCollection);
173
189
  const targetIdField = pkProp.name;
@@ -461,6 +477,8 @@ const computeSharedRelationName = (
461
477
  export const generateSchema = async (collections: EntityCollection[], stripPolicies = false): Promise<string> => {
462
478
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
463
479
 
480
+
481
+
464
482
  const hasUuid = collections.some(c =>
465
483
  c.properties && Object.values(c.properties).some(
466
484
  (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
@@ -496,7 +514,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
496
514
  Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
497
515
  if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
498
516
  const enumVarName = getEnumVarName(collectionPath, propName);
499
- const enumDbName = `${collectionPath}_${toSnakeCase(propName)}`;
517
+ const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
500
518
  const values = Array.isArray(prop.enum)
501
519
  ? prop.enum.map(v => String(v.id ?? v))
502
520
  : Object.keys(prop.enum);
@@ -560,8 +578,8 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
560
578
  const targetId = getPrimaryKeyName(targetCollection);
561
579
 
562
580
  schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
563
- schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${toSnakeCase(sourceColumn)}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
564
- schemaContent += ` ${targetColumn}: ${targetColType}(\"${toSnakeCase(targetColumn)}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
581
+ schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
582
+ schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
565
583
  schemaContent += "}, (table) => ({\n";
566
584
  schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
567
585
  schemaContent += "}));\n\n";
@@ -571,6 +589,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
571
589
  Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
572
590
  const columnString = getDrizzleColumn(propName, prop as Property, collection, collections);
573
591
  if (columnString) columns.add(columnString);
592
+
574
593
  });
575
594
 
576
595
  // Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
@@ -682,31 +701,13 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
682
701
  if (rel.cardinality === "one") {
683
702
  if (rel.direction === "owning" && rel.localKey) {
684
703
  tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
685
- } else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
686
- const sourceIdField = getPrimaryKeyName(collection);
687
- tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${rel.foreignKeyOnTarget}],\n relationName: \"${drizzleRelationName}\"\n })`);
688
- } else if (rel.direction === "inverse" && !rel.foreignKeyOnTarget) {
689
- // Handle inverse one-to-one relations where the FK is on the target table
690
- // but foreignKeyOnTarget is not explicitly specified
691
- // In this case, we need to find the corresponding owning relation on the target
692
- try {
693
- const targetCollection = rel.target();
694
- const targetResolvedRelations = resolveCollectionRelations(targetCollection);
695
-
696
- // Find the owning relation on the target that points back to this collection
697
- const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
698
- targetRel.direction === "owning" &&
699
- targetRel.cardinality === "one" &&
700
- targetRel.target().slug === collection.slug
701
- );
702
-
703
- if (correspondingRelation && correspondingRelation.localKey) {
704
- const sourceIdField = getPrimaryKeyName(collection);
705
- tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${sourceIdField}],\n references: [${targetTableVar}.${correspondingRelation.localKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
706
- }
707
- } catch (e) {
708
- console.warn(`Could not resolve inverse one-to-one relation '${relationKey}':`, e);
709
- }
704
+ } else if (rel.direction === "inverse") {
705
+ // Inverse one-to-one: the FK lives on the TARGET table, not here.
706
+ // Drizzle pairs inverse relations via `relationName` alone — specifying
707
+ // `fields`/`references` on the inverse side is invalid and causes
708
+ // `normalizeRelation` to crash with "Cannot read properties of
709
+ // undefined (reading 'referencedTable')".
710
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n relationName: \"${drizzleRelationName}\"\n })`);
710
711
  }
711
712
  } else if (rel.cardinality === "many") {
712
713
  if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
@@ -746,6 +747,33 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
746
747
  console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
747
748
  }
748
749
  }
750
+
751
+ // Synthesize missing reciprocal relations
752
+ for (const otherCollection of collections) {
753
+ if (otherCollection.slug === collection.slug) continue;
754
+
755
+ const otherRelations = resolveCollectionRelations(otherCollection);
756
+ for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
757
+ if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
758
+ try {
759
+ const otherTarget = otherRel.target();
760
+ if (otherTarget.slug === collection.slug) {
761
+ const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
762
+ const deduplicationKey = `${drizzleRelationName}::owning`;
763
+
764
+ if (!emittedRelationNames.has(deduplicationKey)) {
765
+ const otherTableVar = getTableVarName(getTableName(otherCollection));
766
+ const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
767
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
768
+ emittedRelationNames.add(deduplicationKey);
769
+ }
770
+ }
771
+ } catch (e) {
772
+ // ignore
773
+ }
774
+ }
775
+ }
776
+ }
749
777
  }
750
778
 
751
779
  if (tableRelations.length > 0) {
@@ -763,3 +791,4 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
763
791
 
764
792
  return schemaContent;
765
793
  };
794
+