@rebasepro/server-postgresql 0.4.0 → 0.6.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 (168) hide show
  1. package/README.md +69 -89
  2. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  3. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  4. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  5. package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
  6. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  7. package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
  8. package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
  9. package/dist/index.es.js +10174 -11184
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +10735 -11462
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  14. package/dist/types.d.ts +3 -0
  15. package/dist/utils/pg-error-utils.d.ts +55 -0
  16. package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
  17. package/package.json +24 -21
  18. package/src/PostgresAdapter.ts +9 -10
  19. package/src/PostgresBackendDriver.ts +135 -122
  20. package/src/PostgresBootstrapper.ts +90 -16
  21. package/src/auth/ensure-tables.ts +28 -5
  22. package/src/auth/services.ts +56 -45
  23. package/src/cli.ts +140 -110
  24. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  25. package/src/connection.ts +11 -6
  26. package/src/data-transformer.ts +73 -109
  27. package/src/databasePoolManager.ts +5 -3
  28. package/src/history/HistoryService.ts +3 -2
  29. package/src/history/ensure-history-table.ts +5 -4
  30. package/src/schema/auth-schema.ts +1 -2
  31. package/src/schema/doctor-cli.ts +2 -1
  32. package/src/schema/doctor.ts +40 -37
  33. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  34. package/src/schema/generate-drizzle-schema.ts +11 -11
  35. package/src/schema/introspect-db-inference.ts +25 -25
  36. package/src/schema/introspect-db-logic.ts +38 -38
  37. package/src/schema/introspect-db.ts +28 -27
  38. package/src/services/BranchService.ts +14 -0
  39. package/src/services/EntityFetchService.ts +28 -25
  40. package/src/services/EntityPersistService.ts +11 -124
  41. package/src/services/RelationService.ts +57 -37
  42. package/src/services/entity-helpers.ts +6 -2
  43. package/src/services/realtimeService.ts +45 -32
  44. package/src/types.ts +4 -0
  45. package/src/utils/drizzle-conditions.ts +31 -15
  46. package/src/utils/pg-error-utils.ts +211 -0
  47. package/src/websocket.ts +51 -33
  48. package/test/auth-services.test.ts +36 -19
  49. package/test/batch-many-to-many-regression.test.ts +119 -39
  50. package/test/data-transformer-hardening.test.ts +67 -33
  51. package/test/data-transformer.test.ts +4 -2
  52. package/test/doctor.test.ts +10 -5
  53. package/test/drizzle-conditions.test.ts +59 -6
  54. package/test/generate-drizzle-schema.test.ts +65 -40
  55. package/test/introspect-db-generation.test.ts +179 -81
  56. package/test/introspect-db-utils.test.ts +92 -37
  57. package/test/mocks/chalk.cjs +7 -0
  58. package/test/pg-error-utils.test.ts +221 -0
  59. package/test/postgresDataDriver.test.ts +14 -5
  60. package/test/property-ordering.test.ts +126 -79
  61. package/test/realtimeService.test.ts +6 -2
  62. package/test/relation-pipeline-gaps.test.ts +84 -36
  63. package/test/relations.test.ts +247 -0
  64. package/test/unmapped-tables-safety.test.ts +14 -6
  65. package/test/websocket.test.ts +1 -1
  66. package/tsconfig.json +5 -0
  67. package/tsconfig.prod.json +3 -0
  68. package/vite.config.ts +5 -5
  69. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  70. package/dist/common/src/collections/default-collections.d.ts +0 -9
  71. package/dist/common/src/collections/index.d.ts +0 -2
  72. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  73. package/dist/common/src/data/query_builder.d.ts +0 -55
  74. package/dist/common/src/index.d.ts +0 -4
  75. package/dist/common/src/util/builders.d.ts +0 -57
  76. package/dist/common/src/util/callbacks.d.ts +0 -6
  77. package/dist/common/src/util/collections.d.ts +0 -11
  78. package/dist/common/src/util/common.d.ts +0 -2
  79. package/dist/common/src/util/conditions.d.ts +0 -26
  80. package/dist/common/src/util/entities.d.ts +0 -58
  81. package/dist/common/src/util/enums.d.ts +0 -3
  82. package/dist/common/src/util/index.d.ts +0 -16
  83. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  84. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  85. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  86. package/dist/common/src/util/paths.d.ts +0 -14
  87. package/dist/common/src/util/permissions.d.ts +0 -6
  88. package/dist/common/src/util/references.d.ts +0 -2
  89. package/dist/common/src/util/relations.d.ts +0 -22
  90. package/dist/common/src/util/resolutions.d.ts +0 -72
  91. package/dist/common/src/util/storage.d.ts +0 -24
  92. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  93. package/dist/types/src/controllers/auth.d.ts +0 -104
  94. package/dist/types/src/controllers/client.d.ts +0 -168
  95. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  96. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  97. package/dist/types/src/controllers/data.d.ts +0 -207
  98. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  99. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  100. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  101. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  102. package/dist/types/src/controllers/email.d.ts +0 -36
  103. package/dist/types/src/controllers/index.d.ts +0 -18
  104. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  105. package/dist/types/src/controllers/navigation.d.ts +0 -225
  106. package/dist/types/src/controllers/registry.d.ts +0 -63
  107. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  108. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  109. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  110. package/dist/types/src/controllers/storage.d.ts +0 -171
  111. package/dist/types/src/index.d.ts +0 -4
  112. package/dist/types/src/rebase_context.d.ts +0 -122
  113. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  114. package/dist/types/src/types/backend.d.ts +0 -536
  115. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  116. package/dist/types/src/types/builders.d.ts +0 -15
  117. package/dist/types/src/types/chips.d.ts +0 -5
  118. package/dist/types/src/types/collections.d.ts +0 -941
  119. package/dist/types/src/types/component_ref.d.ts +0 -47
  120. package/dist/types/src/types/cron.d.ts +0 -102
  121. package/dist/types/src/types/data_source.d.ts +0 -64
  122. package/dist/types/src/types/database_adapter.d.ts +0 -94
  123. package/dist/types/src/types/entities.d.ts +0 -145
  124. package/dist/types/src/types/entity_actions.d.ts +0 -104
  125. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  126. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  127. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  128. package/dist/types/src/types/entity_views.d.ts +0 -87
  129. package/dist/types/src/types/export_import.d.ts +0 -21
  130. package/dist/types/src/types/formex.d.ts +0 -40
  131. package/dist/types/src/types/index.d.ts +0 -28
  132. package/dist/types/src/types/locales.d.ts +0 -4
  133. package/dist/types/src/types/modify_collections.d.ts +0 -5
  134. package/dist/types/src/types/plugins.d.ts +0 -282
  135. package/dist/types/src/types/properties.d.ts +0 -1181
  136. package/dist/types/src/types/property_config.d.ts +0 -74
  137. package/dist/types/src/types/relations.d.ts +0 -336
  138. package/dist/types/src/types/slots.d.ts +0 -262
  139. package/dist/types/src/types/translations.d.ts +0 -900
  140. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  141. package/dist/types/src/types/websockets.d.ts +0 -78
  142. package/dist/types/src/users/index.d.ts +0 -1
  143. package/dist/types/src/users/user.d.ts +0 -50
  144. package/drizzle.test.config.ts +0 -10
  145. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  146. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  148. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  151. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  164. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  166. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  167. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  168. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
@@ -3,6 +3,7 @@ import { getPrimaryKeys } from "../services/entity-helpers";
3
3
  import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
4
4
  import { toSnakeCase } from "@rebasepro/utils";
5
5
  import { createHash } from "crypto";
6
+ import { logger } from "@rebasepro/server-core";
6
7
  // --- Helper Functions ---
7
8
 
8
9
  /**
@@ -41,6 +42,31 @@ type: "string",
41
42
  isUuid: isUuid ?? false };
42
43
  };
43
44
 
45
+ /**
46
+ * Given a raw DB column name (e.g. "client_id"), find the Drizzle property key
47
+ * on the collection that maps to that column. A property matches if:
48
+ * (a) it has an explicit `columnName` equal to the given column, OR
49
+ * (b) its snake_case form equals the given column.
50
+ *
51
+ * Returns the property key (the Drizzle object key) if found, or the original
52
+ * column name as a fallback.
53
+ */
54
+ const resolvePropertyKeyForColumn = (collection: EntityCollection, column: string): string => {
55
+ if (!collection.properties) return column;
56
+ for (const [propKey, prop] of Object.entries(collection.properties)) {
57
+ const p = prop as Property;
58
+ // Explicit columnName match
59
+ if ("columnName" in p && typeof (p as unknown as Record<string, unknown>).columnName === "string") {
60
+ if ((p as unknown as Record<string, unknown>).columnName === column) return propKey;
61
+ }
62
+ // Convention match: snake_case(propKey) === column
63
+ if (toSnakeCase(propKey) === column) return propKey;
64
+ // Exact match (propKey is already the column name)
65
+ if (propKey === column) return propKey;
66
+ }
67
+ return column;
68
+ };
69
+
44
70
  const isNumericId = (collection: EntityCollection): boolean => {
45
71
  return getPrimaryKeyProp(collection).type === "number";
46
72
  };
@@ -143,7 +169,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
143
169
  }
144
170
  // autoValue: database-level default for initial value on INSERT
145
171
  if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
146
- columnDefinition += `.default(sql\`now()\`)`;
172
+ columnDefinition += ".default(sql`now()`)";
147
173
  }
148
174
  break;
149
175
  }
@@ -206,7 +232,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
206
232
 
207
233
  // The localKey property is the source of truth for the FK column name.
208
234
  if (!relation.localKey) {
209
- console.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
235
+ logger.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
210
236
  return null;
211
237
  }
212
238
 
@@ -296,7 +322,7 @@ const resolveRawSql = (expression: string): string => {
296
322
  */
297
323
  const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
298
324
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
299
- const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
325
+ const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
300
326
  return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
301
327
  };
302
328
 
@@ -410,13 +436,13 @@ const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRu
410
436
  } else if (needsUsing) {
411
437
  // Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
412
438
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
413
- usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
439
+ usingClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
414
440
  }
415
441
  if (withCheckClause) {
416
442
  withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
417
443
  } else if (needsWithCheck) {
418
444
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
419
- withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
445
+ withCheckClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
420
446
  }
421
447
  }
422
448
 
@@ -466,15 +492,22 @@ const computeSharedRelationName = (
466
492
 
467
493
  // --- owning one (belongs-to) ---
468
494
  if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
469
- return `${getTableName(sourceCollection)}_${rel.localKey}`;
495
+ // Normalise the localKey to the actual Drizzle property name so that
496
+ // the owning side produces the same relation name as the inverse side
497
+ // (which resolves foreignKeyOnTarget via the same helper).
498
+ const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
499
+ return `${getTableName(sourceCollection)}_${normalisedKey}`;
470
500
  }
471
501
 
472
502
  // --- inverse many (one-to-many has-many) ---
473
503
  if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
474
- // The owning table is the *target*, the FK column is foreignKeyOnTarget
504
+ // The owning table is the *target*, the FK column is foreignKeyOnTarget.
505
+ // Resolve to the Drizzle property key on the target so it matches the
506
+ // owning side's normalised localKey.
475
507
  try {
476
508
  const targetCollection = rel.target();
477
- return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
509
+ const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
510
+ return `${getTableName(targetCollection)}_${normalisedFK}`;
478
511
  } catch {
479
512
  return fallback;
480
513
  }
@@ -483,10 +516,11 @@ const computeSharedRelationName = (
483
516
  // --- inverse one (one-to-one inverse) ---
484
517
  if (rel.direction === "inverse" && rel.cardinality === "one") {
485
518
  if (rel.foreignKeyOnTarget) {
486
- // FK lives on the target table
519
+ // FK lives on the target table — resolve to Drizzle property key
487
520
  try {
488
521
  const targetCollection = rel.target();
489
- return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
522
+ const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
523
+ return `${getTableName(targetCollection)}_${normalisedFK}`;
490
524
  } catch {
491
525
  return fallback;
492
526
  }
@@ -521,7 +555,6 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
521
555
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
522
556
 
523
557
 
524
-
525
558
  const hasUuid = collections.some(c =>
526
559
  c.properties && Object.values(c.properties).some(
527
560
  (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
@@ -812,23 +845,23 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
812
845
  const junctionTableVar = getTableVarName(correspondingRelation.through.table);
813
846
  tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
814
847
  } else {
815
- console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
848
+ logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
816
849
  }
817
850
  } catch (e) {
818
- console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
851
+ logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
819
852
  }
820
853
  }
821
854
  // joinPath relations don't generate Drizzle relations - they use existing user tables
822
855
  }
823
856
  } catch (e) {
824
- console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
857
+ logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
825
858
  }
826
859
  }
827
860
 
828
861
  // Synthesize missing reciprocal relations
829
862
  for (const otherCollection of collections) {
830
863
  if (otherCollection.slug === collection.slug) continue;
831
-
864
+
832
865
  const otherRelations = resolveCollectionRelations(otherCollection);
833
866
  for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
834
867
  if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
@@ -837,11 +870,16 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
837
870
  if (otherTarget.slug === collection.slug) {
838
871
  const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
839
872
  const deduplicationKey = `${drizzleRelationName}::owning`;
840
-
873
+
841
874
  if (!emittedRelationNames.has(deduplicationKey)) {
842
875
  const otherTableVar = getTableVarName(getTableName(otherCollection));
843
- const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
844
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
876
+ // Resolve foreignKeyOnTarget to the Drizzle property key
877
+ // on THIS collection (the owning table). The raw FK column
878
+ // name (e.g. "client_id") may differ from the property key
879
+ // (e.g. "clientId") when `columnName` is set.
880
+ const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
881
+ const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
882
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
845
883
  emittedRelationNames.add(deduplicationKey);
846
884
  }
847
885
  }
@@ -5,6 +5,7 @@ import { pathToFileURL } from "url";
5
5
  import chokidar from "chokidar";
6
6
  import { generateSchema } from "./generate-drizzle-schema-logic";
7
7
  import { EntityCollection } from "@rebasepro/types";
8
+ import { logger } from "@rebasepro/server-core";
8
9
 
9
10
 
10
11
  // --- Helper Functions ---
@@ -48,7 +49,7 @@ const formatTerminalText = (text: string, options: {
48
49
  const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
49
50
  try {
50
51
  if (!collectionsFilePath) {
51
- console.error("Error: No collections file path provided. Skipping schema generation.");
52
+ logger.error("Error: No collections file path provided. Skipping schema generation.");
52
53
  return;
53
54
  }
54
55
 
@@ -74,7 +75,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
74
75
  }
75
76
  } catch (err: unknown) {
76
77
  const message = err instanceof Error ? err.message : String(err);
77
- console.error(`Error loading ${file}:`, message);
78
+ logger.error(`Error loading ${file}`, { detail: message });
78
79
  }
79
80
  }
80
81
  }
@@ -91,7 +92,6 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
91
92
  }
92
93
 
93
94
 
94
-
95
95
  // Sort collections by slug alphabetically to ensure deterministic schema generation
96
96
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
97
97
 
@@ -101,20 +101,20 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
101
101
  const outputDir = path.dirname(outputPath);
102
102
  await fsPromises.mkdir(outputDir, { recursive: true });
103
103
  await fsPromises.writeFile(outputPath, schemaContent);
104
- console.log("✅ Drizzle schema generated successfully at", outputPath);
104
+ logger.info("✅ Drizzle schema generated successfully at", { detail: outputPath });
105
105
  } else {
106
- console.log("✅ Drizzle schema generated successfully.");
107
- console.log(schemaContent);
106
+ logger.info("✅ Drizzle schema generated successfully.");
107
+ logger.info(String(schemaContent));
108
108
  }
109
109
 
110
- console.log(`You can now run ${formatTerminalText("rebase db generate", {
110
+ logger.info(`You can now run ${formatTerminalText("rebase db generate", {
111
111
  bold: true,
112
112
  backgroundColor: "blue",
113
113
  textColor: "black"
114
114
  })} to generate the SQL migration files.`);
115
115
 
116
116
  } catch (error) {
117
- console.error("Error generating schema:", error);
117
+ logger.error("Error generating schema", { error: error });
118
118
  }
119
119
  };
120
120
 
@@ -128,7 +128,7 @@ const main = () => {
128
128
  const watch = process.argv.includes("--watch");
129
129
 
130
130
  if (!collectionsFilePath) {
131
- console.log("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
131
+ logger.info("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
132
132
  return;
133
133
  }
134
134
 
@@ -136,14 +136,14 @@ const main = () => {
136
136
  const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
137
137
 
138
138
  if (watch) {
139
- console.log(`Watching for changes in ${resolvedPath}...`);
139
+ logger.info(`Watching for changes in ${resolvedPath}...`);
140
140
  const watcher = chokidar.watch(resolvedPath, {
141
141
  persistent: true,
142
142
  ignoreInitial: false
143
143
  });
144
144
 
145
145
  watcher.on("all", (event, filePath) => {
146
- console.log(`[${event}] ${filePath}. Regenerating schema...`);
146
+ logger.info(`[${event}] ${filePath}. Regenerating schema...`);
147
147
  runGeneration(resolvedPath, resolvedOutputPath);
148
148
  });
149
149
  } else {
@@ -45,15 +45,15 @@ export function inferPropertyFromData(
45
45
  const max = Math.max(...numValues);
46
46
  // Example heuristic: percentages
47
47
  if (min >= 0 && max <= 100 && (colNameLower.includes("percent") || colNameLower.includes("rate") || colNameLower.includes("score"))) {
48
- extraLines.push(` validation: {\n min: 0,\n max: 100\n }`);
48
+ extraLines.push(" validation: {\n min: 0,\n max: 100\n }");
49
49
  } else if (min >= 0 && (colNameLower.includes("count") || colNameLower.includes("total") || colNameLower.includes("amount"))) {
50
- extraLines.push(` validation: {\n min: 0\n }`);
50
+ extraLines.push(" validation: {\n min: 0\n }");
51
51
  }
52
52
  }
53
53
 
54
54
  // Currency
55
55
  if (colNameLower.includes("price") || colNameLower.includes("cost") || colNameLower.includes("amount") || colNameLower.includes("fee") || pgDataType === "money") {
56
- extraLines.push(` ui: {\n currency: true\n }`);
56
+ extraLines.push(" ui: {\n currency: true\n }");
57
57
  }
58
58
  }
59
59
 
@@ -94,7 +94,7 @@ export function inferPropertyFromData(
94
94
  extraLines.push(` of: { name: "${humanize(columnName)} Item", type: "${innerType}" }`);
95
95
  } else {
96
96
  result.propType = "map";
97
-
97
+
98
98
  // Infer inner schema
99
99
  if (allObjects && validValues.length > 0) {
100
100
  const schema: Record<string, string> = {};
@@ -115,7 +115,7 @@ export function inferPropertyFromData(
115
115
  }
116
116
  }
117
117
  }
118
-
118
+
119
119
  const keys = Object.keys(schema).filter(k => schema[k] !== "mixed");
120
120
  if (keys.length > 0) {
121
121
  const props = keys.map(k => {
@@ -123,10 +123,10 @@ export function inferPropertyFromData(
123
123
  }).join(",");
124
124
  extraLines.push(` properties: {${props}\n }`);
125
125
  } else {
126
- extraLines.push(` keyValue: true`);
126
+ extraLines.push(" keyValue: true");
127
127
  }
128
128
  } else {
129
- extraLines.push(` keyValue: true`);
129
+ extraLines.push(" keyValue: true");
130
130
  }
131
131
  }
132
132
  }
@@ -134,7 +134,7 @@ export function inferPropertyFromData(
134
134
  // ── String Analysis ──────────────────────────────────────────────────
135
135
  if (currentPropType === "string") {
136
136
  // Date/Time Strings
137
- if (validValues.every(v => typeof v === 'string' && ISO_8601_REGEX.test(v))) {
137
+ if (validValues.every(v => typeof v === "string" && ISO_8601_REGEX.test(v))) {
138
138
  result.propType = "date";
139
139
  return result;
140
140
  }
@@ -148,7 +148,7 @@ export function inferPropertyFromData(
148
148
  maxEnumLength = v.length;
149
149
  }
150
150
  }
151
-
151
+
152
152
  // Ensure no empty string, max length makes sense, and fewer unique values than total values (unless small total)
153
153
  if (uniqueValues.size > 0 && uniqueValues.size <= 5 && maxEnumLength <= 50 && validValues.length > uniqueValues.size && !uniqueValues.has("")) {
154
154
  const isLikelyId = isPk || colNameLower.endsWith("_id");
@@ -161,18 +161,18 @@ export function inferPropertyFromData(
161
161
  }
162
162
 
163
163
  // UUID / CUID Detection
164
- const allUuid = validValues.every(v => typeof v === 'string' && UUID_REGEX.test(v));
165
- const allCuid = validValues.every(v => typeof v === 'string' && CUID_REGEX.test(v));
164
+ const allUuid = validValues.every(v => typeof v === "string" && UUID_REGEX.test(v));
165
+ const allCuid = validValues.every(v => typeof v === "string" && CUID_REGEX.test(v));
166
166
  if (allUuid) {
167
- if (isPk) extraLines.push(` isId: "uuid"`);
167
+ if (isPk) extraLines.push(" isId: \"uuid\"");
168
168
  } else if (allCuid) {
169
- if (isPk) extraLines.push(` isId: "cuid"`);
169
+ if (isPk) extraLines.push(" isId: \"cuid\"");
170
170
  }
171
171
 
172
172
  // Color Codes
173
- const allColors = validValues.every(v => typeof v === 'string' && COLOR_HEX_REGEX.test(v));
173
+ const allColors = validValues.every(v => typeof v === "string" && COLOR_HEX_REGEX.test(v));
174
174
  if (allColors) {
175
- extraLines.push(` ui: {\n color: true\n }`);
175
+ extraLines.push(" ui: {\n color: true\n }");
176
176
  }
177
177
 
178
178
  // Text Lengths, Multiline & Markdown
@@ -190,9 +190,9 @@ export function inferPropertyFromData(
190
190
  }
191
191
 
192
192
  if (hasMarkdown) {
193
- extraLines.push(` multiline: true,\n markdown: true`);
193
+ extraLines.push(" multiline: true,\n markdown: true");
194
194
  } else if (hasNewlines || maxLength > 100) {
195
- extraLines.push(` multiline: true`);
195
+ extraLines.push(" multiline: true");
196
196
  }
197
197
 
198
198
  if (maxLength > 0 && maxLength < 10000) { // arbitrary cap to avoid huge limits
@@ -208,26 +208,26 @@ export function inferPropertyFromData(
208
208
  const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
209
209
  const isMedia = colNameLower.includes("image") || colNameLower.includes("avatar") || colNameLower.includes("photo") || colNameLower.includes("logo") || colNameLower.includes("cover");
210
210
 
211
- const allAbsoluteUrls = validValues.every(v => typeof v === 'string' && (v.startsWith("http://") || v.startsWith("https://")));
211
+ const allAbsoluteUrls = validValues.every(v => typeof v === "string" && (v.startsWith("http://") || v.startsWith("https://")));
212
212
  if (allAbsoluteUrls) {
213
- const isImage = validValues.some(v => typeof v === 'string' && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
213
+ const isImage = validValues.some(v => typeof v === "string" && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
214
214
  if (isImage || isMedia) {
215
- extraLines.push(` ui: {\n url: "image"\n }`);
215
+ extraLines.push(" ui: {\n url: \"image\"\n }");
216
216
  } else {
217
- extraLines.push(` ui: {\n url: true\n }`);
217
+ extraLines.push(" ui: {\n url: true\n }");
218
218
  }
219
219
  } else {
220
- const hasFileExtension = validValues.some(v => typeof v === 'string' && v.match(/\.[a-zA-Z0-9]+$/));
220
+ const hasFileExtension = validValues.some(v => typeof v === "string" && v.match(/\.[a-zA-Z0-9]+$/));
221
221
  if (hasFileExtension) {
222
222
  const firstVal = validValues[0] as string;
223
- const lastSlash = firstVal.lastIndexOf('/');
223
+ const lastSlash = firstVal.lastIndexOf("/");
224
224
  const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
225
225
  extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
226
226
  } else if (isUrl) {
227
227
  if (isMedia) {
228
- extraLines.push(` ui: {\n url: "image"\n }`);
228
+ extraLines.push(" ui: {\n url: \"image\"\n }");
229
229
  } else {
230
- extraLines.push(` ui: {\n url: true\n }`);
230
+ extraLines.push(" ui: {\n url: true\n }");
231
231
  }
232
232
  }
233
233
  }
@@ -63,7 +63,7 @@ const IRREGULAR_SINGULARS: Record<string, string> = {
63
63
  data: "datum",
64
64
  media: "medium",
65
65
  criteria: "criterion",
66
- phenomena: "phenomenon",
66
+ phenomena: "phenomenon"
67
67
  };
68
68
 
69
69
  /** Words ending in 's' that are already singular. */
@@ -73,7 +73,7 @@ const UNCOUNTABLE = new Set([
73
73
  "synopsis", "parenthesis", "hypothesis", "emphasis",
74
74
  "news", "series", "species", "means", "athletics",
75
75
  "economics", "electronics", "mathematics", "physics",
76
- "politics", "statistics",
76
+ "politics", "statistics"
77
77
  ]);
78
78
 
79
79
  export function singularize(word: string): string {
@@ -165,10 +165,10 @@ export function mapPgType(dataType: string): string {
165
165
 
166
166
  // Numeric types
167
167
  if (
168
- dt.includes("int") || // integer, smallint, bigint
168
+ dt.includes("int") || // integer, smallint, bigint
169
169
  dt.includes("numeric") ||
170
170
  dt.includes("decimal") ||
171
- dt.includes("serial") || // serial, bigserial
171
+ dt.includes("serial") || // serial, bigserial
172
172
  dt === "real" ||
173
173
  dt === "float4" ||
174
174
  dt === "float8" ||
@@ -281,7 +281,7 @@ export interface PropertyOrderingContext {
281
281
  const IDENTITY_EXACT: Record<string, number> = {
282
282
  id: 0,
283
283
  uuid: 1,
284
- _id: 2,
284
+ _id: 2
285
285
  };
286
286
 
287
287
  // — Tier 1: Title / Name — the "display column" (10–19) ———————————————
@@ -293,7 +293,7 @@ const TITLE_EXACT: Record<string, number> = {
293
293
  displayname: 13,
294
294
  headline: 14,
295
295
  subject: 15,
296
- heading: 16,
296
+ heading: 16
297
297
  };
298
298
 
299
299
  // — Tier 2: Human identity fields (20–29) —————————————————————————————
@@ -313,7 +313,7 @@ const HUMAN_IDENTITY_EXACT: Record<string, number> = {
313
313
  email_address: 26,
314
314
  phone: 27,
315
315
  phone_number: 27,
316
- mobile: 27,
316
+ mobile: 27
317
317
  };
318
318
 
319
319
  // — Tier 3: Core descriptors (30–39) ——————————————————————————————————
@@ -333,7 +333,7 @@ const DESCRIPTOR_EXACT: Record<string, number> = {
333
333
  priority: 39,
334
334
  order: 39,
335
335
  sort_order: 39,
336
- position: 39,
336
+ position: 39
337
337
  };
338
338
 
339
339
  // — Tier 12: System timestamps (120–129) ——————————————————————————————
@@ -348,7 +348,7 @@ const SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {
348
348
  last_modified: 122,
349
349
  deleted_at: 123,
350
350
  deletedat: 123,
351
- archived_at: 124,
351
+ archived_at: 124
352
352
  };
353
353
 
354
354
  // — Pattern-based rules for partial matches ———————————————————————————
@@ -370,7 +370,7 @@ const JSON_MAP_NAMES = new Set(["metadata", "meta", "config", "configuration", "
370
370
  */
371
371
  export function computePropertyPriority(
372
372
  columnName: string,
373
- ctx: PropertyOrderingContext,
373
+ ctx: PropertyOrderingContext
374
374
  ): number {
375
375
  // Normalize camelCase/PascalCase to snake_case, then lowercase
376
376
  const col = columnName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
@@ -521,7 +521,7 @@ export function generateCollectionFile(
521
521
  joinTables: Set<string>,
522
522
  tablesMap: Map<string, TableMeta>,
523
523
  enumMap: Map<string, string[]>,
524
- sampleData?: Record<string, unknown>[],
524
+ sampleData?: Record<string, unknown>[]
525
525
  ): string {
526
526
  const collectionName = humanize(tableName);
527
527
  const singular = singularize(collectionName);
@@ -529,8 +529,8 @@ export function generateCollectionFile(
529
529
 
530
530
  const imports = new Set<string>(['import { PostgresCollection } from "@rebasepro/types";']);
531
531
 
532
- let propsOutput = ``;
533
- let relationsOutput = ``;
532
+ let propsOutput = "";
533
+ let relationsOutput = "";
534
534
  const orderEntries: PropertyOrderEntry[] = [];
535
535
  const propertyBlocks = new Map<string, string>();
536
536
  let columnIndex = 0;
@@ -559,7 +559,7 @@ export function generateCollectionFile(
559
559
  // ── Data Inference Engine ────────────────────────────────────────────
560
560
  let finalPropType = propType;
561
561
  let inferenceExtra = "";
562
-
562
+
563
563
  if (!isEnumColumn && sampleData && sampleData.length > 0) {
564
564
  const values = sampleData.map(r => r[col.column_name]);
565
565
  const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name));
@@ -578,11 +578,11 @@ export function generateCollectionFile(
578
578
  // Date auto-value heuristics
579
579
  if (finalPropType === "date") {
580
580
  if (colNameLower === "created_at" || colNameLower === "createdat") {
581
- extra += `\n autoValue: "on_create",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
581
+ extra += "\n autoValue: \"on_create\",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },";
582
582
  } else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
583
- extra += `\n autoValue: "on_update",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },`;
583
+ extra += "\n autoValue: \"on_update\",\n ui: {\n readOnly: true,\n hideFromCollection: true\n },";
584
584
  } else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
585
- extra += `\n autoValue: "on_create",\n ui: {\n readOnly: true\n },`;
585
+ extra += "\n autoValue: \"on_create\",\n ui: {\n readOnly: true\n },";
586
586
  }
587
587
  }
588
588
 
@@ -602,7 +602,7 @@ export function generateCollectionFile(
602
602
  }
603
603
  extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
604
604
  } else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
605
- extra += `\n keyValue: true,`;
605
+ extra += "\n keyValue: true,";
606
606
  }
607
607
 
608
608
  // String sub-type heuristics (Fallback if not handled by inference or enum)
@@ -613,16 +613,16 @@ export function generateCollectionFile(
613
613
  if (isMedia) {
614
614
  extra += `\n storage: {\n storagePath: "${tableName}/${col.column_name}"\n },`;
615
615
  } else if (isUrl) {
616
- extra += `\n ui: {\n url: true\n },`;
616
+ extra += "\n ui: {\n url: true\n },";
617
617
  } else if (colNameLower === "description" || colNameLower === "summary" || colNameLower === "excerpt") {
618
- extra += `\n multiline: true,`;
618
+ extra += "\n multiline: true,";
619
619
  } else if (colNameLower === "content" || colNameLower === "body") {
620
- extra += `\n multiline: true,\n markdown: true,`;
620
+ extra += "\n multiline: true,\n markdown: true,";
621
621
  } else if (col.data_type === "text") {
622
- extra += `\n multiline: true,`;
622
+ extra += "\n multiline: true,";
623
623
  }
624
624
  }
625
-
625
+
626
626
  // Append inference results
627
627
  if (inferenceExtra) {
628
628
  extra += inferenceExtra;
@@ -634,11 +634,11 @@ export function generateCollectionFile(
634
634
  if (isCompositePk) {
635
635
  extra += `\n // Part of composite primary key (${meta.pks.join(", ")})`;
636
636
  } else if (finalPropType === "number" && !inferenceExtra.includes("isId:")) {
637
- extra += `\n isId: "increment",`;
637
+ extra += "\n isId: \"increment\",";
638
638
  } else if (col.data_type.toLowerCase() === "uuid" && !inferenceExtra.includes("isId:")) {
639
- extra += `\n isId: "uuid",`;
639
+ extra += "\n isId: \"uuid\",";
640
640
  } else if (!inferenceExtra.includes("isId:")) {
641
- extra += `\n isId: "uuid", // Verify if this is a UUID or CUID`;
641
+ extra += "\n isId: \"uuid\", // Verify if this is a UUID or CUID";
642
642
  }
643
643
  }
644
644
 
@@ -651,7 +651,7 @@ export function generateCollectionFile(
651
651
  if (extra.includes("validation: {")) {
652
652
  extra = extra.replace("validation: {", "validation: {\n required: true,");
653
653
  } else {
654
- extra += `\n validation: {\n required: true\n },`;
654
+ extra += "\n validation: {\n required: true\n },";
655
655
  }
656
656
  }
657
657
 
@@ -665,8 +665,8 @@ export function generateCollectionFile(
665
665
  isEnum: isEnumColumn,
666
666
  isStorage: extra.includes("storage: {") || inferenceExtra.includes("storage: {"),
667
667
  pgDataType: col.data_type,
668
- originalIndex: currentIndex,
669
- },
668
+ originalIndex: currentIndex
669
+ }
670
670
  });
671
671
 
672
672
  propertyBlocks.set(col.column_name, `
@@ -696,8 +696,8 @@ export function generateCollectionFile(
696
696
  isEnum: false,
697
697
  isStorage: false,
698
698
  pgDataType: "",
699
- originalIndex: columnIndex++,
700
- },
699
+ originalIndex: columnIndex++
700
+ }
701
701
  });
702
702
 
703
703
  const targetCollectionCamel = toCollectionVarName(targetTableName);
@@ -794,7 +794,7 @@ export function generateCollectionFile(
794
794
  if (direction === "owning" && thisFk) {
795
795
  throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n },`;
796
796
  } else if (direction === "inverse") {
797
- throughCode = `\n // Make sure the target collection configures the 'through' property.`;
797
+ throughCode = "\n // Make sure the target collection configures the 'through' property.";
798
798
  }
799
799
 
800
800
  relationsOutput += `
@@ -860,10 +860,10 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
860
860
  [...existingContent.matchAll(/import\s+([a-zA-Z0-9_]+)\s+from\s+"\.\/([^"]+)"/g)].map((m) => m[2])
861
861
  );
862
862
  const sorted = [...newFileNames].sort();
863
-
863
+
864
864
  let newImports = "";
865
865
  let newElements = "";
866
-
866
+
867
867
  for (const f of sorted) {
868
868
  if (!existingImports.has(f)) {
869
869
  const varName = toCollectionVarName(f);
@@ -871,9 +871,9 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
871
871
  newElements += ` ${varName},\n`;
872
872
  }
873
873
  }
874
-
874
+
875
875
  if (!newImports) return existingContent;
876
-
876
+
877
877
  // Simple injection logic:
878
878
  // Add new imports below the last import or at the top
879
879
  const importRegex = /import\s+.*?;/g;
@@ -882,7 +882,7 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
882
882
  while ((match = importRegex.exec(existingContent)) !== null) {
883
883
  lastImportMatch = match;
884
884
  }
885
-
885
+
886
886
  let contentWithImports = existingContent;
887
887
  if (lastImportMatch) {
888
888
  const pos = lastImportMatch.index + lastImportMatch[0].length;
@@ -890,7 +890,7 @@ export function mergeIndexContent(existingContent: string, newFileNames: string[
890
890
  } else {
891
891
  contentWithImports = newImports + "\n" + existingContent;
892
892
  }
893
-
893
+
894
894
  // Inject into the `collections = [...]` array
895
895
  const arrayRegex = /export\s+const\s+collections\s*=\s*\[([\s\S]*?)\];/;
896
896
  return contentWithImports.replace(arrayRegex, (fullMatch, arrayContent) => {