@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (136) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index.es.js +1254 -591
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +1254 -591
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
  10. package/dist/server-postgresql/src/auth/services.d.ts +7 -3
  11. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
  12. package/dist/server-postgresql/src/connection.d.ts +34 -1
  13. package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
  14. package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
  15. package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
  16. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  17. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  18. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
  19. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  20. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
  21. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
  22. package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
  23. package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
  24. package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
  25. package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
  26. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
  27. package/dist/types/src/controllers/auth.d.ts +2 -0
  28. package/dist/types/src/controllers/client.d.ts +119 -7
  29. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  30. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  31. package/dist/types/src/controllers/data.d.ts +34 -7
  32. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  33. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  34. package/dist/types/src/controllers/email.d.ts +34 -0
  35. package/dist/types/src/controllers/index.d.ts +1 -0
  36. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  37. package/dist/types/src/controllers/navigation.d.ts +5 -5
  38. package/dist/types/src/controllers/registry.d.ts +6 -3
  39. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  40. package/dist/types/src/controllers/storage.d.ts +24 -26
  41. package/dist/types/src/rebase_context.d.ts +8 -4
  42. package/dist/types/src/types/backend.d.ts +4 -1
  43. package/dist/types/src/types/builders.d.ts +5 -4
  44. package/dist/types/src/types/chips.d.ts +1 -1
  45. package/dist/types/src/types/collections.d.ts +169 -125
  46. package/dist/types/src/types/cron.d.ts +102 -0
  47. package/dist/types/src/types/data_source.d.ts +1 -1
  48. package/dist/types/src/types/entity_actions.d.ts +8 -8
  49. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  50. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  51. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  52. package/dist/types/src/types/entity_views.d.ts +8 -8
  53. package/dist/types/src/types/export_import.d.ts +3 -3
  54. package/dist/types/src/types/index.d.ts +1 -0
  55. package/dist/types/src/types/plugins.d.ts +72 -18
  56. package/dist/types/src/types/properties.d.ts +118 -33
  57. package/dist/types/src/types/relations.d.ts +1 -1
  58. package/dist/types/src/types/slots.d.ts +30 -6
  59. package/dist/types/src/types/translations.d.ts +44 -0
  60. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  61. package/drizzle-test/0000_woozy_junta.sql +6 -0
  62. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  63. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  64. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  65. package/drizzle-test/meta/0000_snapshot.json +47 -0
  66. package/drizzle-test/meta/0001_snapshot.json +48 -0
  67. package/drizzle-test/meta/0002_snapshot.json +38 -0
  68. package/drizzle-test/meta/0003_snapshot.json +48 -0
  69. package/drizzle-test/meta/_journal.json +34 -0
  70. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  71. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  72. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  73. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  74. package/drizzle-test-out/meta/_journal.json +20 -0
  75. package/drizzle.test.config.ts +10 -0
  76. package/package.json +88 -89
  77. package/scratch.ts +41 -0
  78. package/src/PostgresBackendDriver.ts +63 -79
  79. package/src/PostgresBootstrapper.ts +7 -8
  80. package/src/auth/ensure-tables.ts +158 -86
  81. package/src/auth/services.ts +109 -50
  82. package/src/cli.ts +259 -16
  83. package/src/collections/PostgresCollectionRegistry.ts +6 -6
  84. package/src/connection.ts +70 -48
  85. package/src/data-transformer.ts +155 -116
  86. package/src/databasePoolManager.ts +6 -5
  87. package/src/history/HistoryService.ts +3 -12
  88. package/src/interfaces.ts +3 -3
  89. package/src/schema/auth-schema.ts +26 -3
  90. package/src/schema/doctor-cli.ts +47 -0
  91. package/src/schema/doctor.ts +595 -0
  92. package/src/schema/generate-drizzle-schema-logic.ts +204 -57
  93. package/src/schema/generate-drizzle-schema.ts +6 -6
  94. package/src/schema/test-schema.ts +11 -0
  95. package/src/services/BranchService.ts +5 -5
  96. package/src/services/EntityFetchService.ts +317 -188
  97. package/src/services/EntityPersistService.ts +15 -17
  98. package/src/services/RelationService.ts +299 -37
  99. package/src/services/entity-helpers.ts +39 -13
  100. package/src/services/entityService.ts +11 -9
  101. package/src/services/realtimeService.ts +58 -29
  102. package/src/utils/drizzle-conditions.ts +25 -24
  103. package/src/websocket.ts +52 -21
  104. package/test/auth-services.test.ts +131 -39
  105. package/test/batch-many-to-many-regression.test.ts +573 -0
  106. package/test/branchService.test.ts +22 -12
  107. package/test/data-transformer-hardening.test.ts +417 -0
  108. package/test/data-transformer.test.ts +175 -0
  109. package/test/doctor.test.ts +182 -0
  110. package/test/entityService.errors.test.ts +31 -16
  111. package/test/entityService.relations.test.ts +155 -59
  112. package/test/entityService.subcollection-search.test.ts +107 -57
  113. package/test/entityService.test.ts +105 -47
  114. package/test/generate-drizzle-schema.test.ts +262 -69
  115. package/test/historyService.test.ts +31 -16
  116. package/test/n-plus-one-regression.test.ts +314 -0
  117. package/test/postgresDataDriver.test.ts +260 -168
  118. package/test/realtimeService.test.ts +70 -39
  119. package/test/relation-pipeline-gaps.test.ts +637 -0
  120. package/test/relations.test.ts +492 -39
  121. package/test-drizzle-bug.ts +18 -0
  122. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  123. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  124. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  125. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  126. package/test-drizzle-out/meta/_journal.json +20 -0
  127. package/test-drizzle-prompt.sh +2 -0
  128. package/test-policy-prompt.sh +3 -0
  129. package/test-programmatic.ts +30 -0
  130. package/test-programmatic2.ts +59 -0
  131. package/test-schema-no-policies.ts +12 -0
  132. package/test_drizzle_mock.js +2 -2
  133. package/test_find_changed.mjs +3 -1
  134. package/test_hash.js +14 -0
  135. package/tsconfig.json +1 -1
  136. package/vite.config.ts +5 -5
package/dist/index.umd.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function(global2, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("pg"), require("drizzle-orm/node-postgres"), require("drizzle-orm"), require("drizzle-orm/pg-core"), require("fs"), require("path"), require("url"), require("chokidar"), require("ws"), require("events"), require("crypto"), require("util"), require("@rebasepro/server-core")) : typeof define === "function" && define.amd ? define(["exports", "pg", "drizzle-orm/node-postgres", "drizzle-orm", "drizzle-orm/pg-core", "fs", "path", "url", "chokidar", "ws", "events", "crypto", "util", "@rebasepro/server-core"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2["Rebase Backend"] = {}, global2.pg, global2.nodePostgres, global2.drizzleOrm, global2.pgCore, global2.fs, global2.path, global2.url, global2.chokidar, global2.ws, global2.events, global2.crypto, global2.util, global2.serverCore));
3
- })(this, function(exports2, pg, nodePostgres, drizzleOrm, pgCore, fs, path, url, chokidar, ws, events, crypto, util, serverCore) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("pg"), require("drizzle-orm/node-postgres"), require("drizzle-orm"), require("drizzle-orm/pg-core"), require("crypto"), require("fs"), require("path"), require("url"), require("chokidar"), require("ws"), require("events"), require("util"), require("@rebasepro/server-core")) : typeof define === "function" && define.amd ? define(["exports", "pg", "drizzle-orm/node-postgres", "drizzle-orm", "drizzle-orm/pg-core", "crypto", "fs", "path", "url", "chokidar", "ws", "events", "util", "@rebasepro/server-core"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2["Rebase Backend"] = {}, global2.pg, global2.nodePostgres, global2.drizzleOrm, global2.pgCore, global2.crypto, global2.fs, global2.path, global2.url, global2.chokidar, global2.ws, global2.events, global2.util, global2.serverCore));
3
+ })(this, function(exports2, pg, nodePostgres, drizzleOrm, pgCore, crypto, fs, path, url, chokidar, ws, events, util, serverCore) {
4
4
  "use strict";
5
5
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
6
6
  function _interopNamespaceDefault(e) {
@@ -20,64 +20,118 @@
20
20
  return Object.freeze(n);
21
21
  }
22
22
  const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
23
- function createPostgresDatabaseConnection(connectionString, schema) {
24
- const pool = new pg.Pool({
23
+ const DEFAULT_POOL = {
24
+ max: 20,
25
+ idleTimeoutMillis: 3e4,
26
+ connectionTimeoutMillis: 1e4,
27
+ queryTimeout: 3e4,
28
+ statementTimeout: 3e4,
29
+ keepAlive: true
30
+ };
31
+ function createPostgresDatabaseConnection(connectionString, schema, poolConfig) {
32
+ const opts = {
33
+ ...DEFAULT_POOL,
34
+ ...poolConfig
35
+ };
36
+ const pgPoolConfig = {
25
37
  connectionString,
26
- // Connection pool settings for resilience
27
- max: 20,
28
- // Maximum number of connections in the pool
29
- idleTimeoutMillis: 3e4,
30
- // Close idle connections after 30 seconds
31
- connectionTimeoutMillis: 1e4,
32
- // Timeout for new connections
33
- // Retry configuration
34
- query_timeout: 3e4,
35
- // Query timeout
36
- statement_timeout: 3e4,
37
- // Statement timeout
38
- // Keep connections alive
39
- keepAlive: true,
38
+ max: opts.max,
39
+ idleTimeoutMillis: opts.idleTimeoutMillis,
40
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
41
+ query_timeout: opts.queryTimeout,
42
+ statement_timeout: opts.statementTimeout,
43
+ keepAlive: opts.keepAlive,
40
44
  keepAliveInitialDelayMillis: 0
41
- });
45
+ };
46
+ const pool = new pg.Pool(pgPoolConfig);
42
47
  pool.on("error", (err) => {
43
- console.error("Database connection error:", err);
48
+ console.error("[pg-pool] Unexpected pool error:", err.message);
44
49
  if (err.message.includes("ETIMEDOUT")) {
45
- console.warn("Connection timeout detected, pool will automatically retry...");
50
+ console.warn("[pg-pool] Connection timeout detected pool will auto-retry");
46
51
  }
47
52
  });
48
- pool.on("connect", (client) => {
49
- console.debug("Database client connected");
50
- client.on("error", (err) => {
51
- console.error("Database client error:", err);
52
- });
53
- });
54
- pool.on("remove", (client) => {
55
- console.debug("Database client removed from pool");
56
- });
57
53
  const db = schema ? nodePostgres.drizzle(pool, {
58
54
  schema
59
55
  }) : nodePostgres.drizzle(pool);
60
- process.on("SIGINT", async () => {
61
- console.log("SIGINT: Closing database pool...");
62
- await pool.end();
63
- process.exit(0);
64
- });
65
- process.on("SIGTERM", async () => {
66
- console.log("SIGTERM: Closing database pool...");
67
- await pool.end();
68
- process.exit(0);
69
- });
70
56
  return {
71
57
  db,
58
+ pool,
72
59
  connectionString
73
60
  };
74
61
  }
75
62
  function isPostgresCollection(collection) {
76
63
  return !collection.driver || collection.driver === "postgres";
77
64
  }
78
- function isFirebaseCollection(collection) {
79
- return collection.driver === "firestore";
65
+ function isSQLAdmin(admin) {
66
+ return !!admin && typeof admin.executeSql === "function";
67
+ }
68
+ function isSchemaAdmin(admin) {
69
+ return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
70
+ }
71
+ const POSTGRES_CAPABILITIES = {
72
+ key: "postgres",
73
+ label: "PostgreSQL",
74
+ supportsRelations: true,
75
+ supportsSubcollections: false,
76
+ supportsRLS: true,
77
+ supportsReferences: false,
78
+ supportsColumnTypes: true,
79
+ supportsRealtime: true,
80
+ supportsSQLAdmin: true,
81
+ supportsDocumentAdmin: false,
82
+ supportsSchemaAdmin: true
83
+ };
84
+ const FIREBASE_CAPABILITIES = {
85
+ key: "firestore",
86
+ label: "Firebase / Firestore",
87
+ supportsRelations: false,
88
+ supportsSubcollections: true,
89
+ supportsRLS: false,
90
+ supportsReferences: true,
91
+ supportsColumnTypes: false,
92
+ supportsRealtime: true,
93
+ supportsSQLAdmin: false,
94
+ supportsDocumentAdmin: false,
95
+ supportsSchemaAdmin: false
96
+ };
97
+ const MONGODB_CAPABILITIES = {
98
+ key: "mongodb",
99
+ label: "MongoDB",
100
+ supportsRelations: false,
101
+ supportsSubcollections: true,
102
+ supportsRLS: false,
103
+ supportsReferences: true,
104
+ supportsColumnTypes: false,
105
+ supportsRealtime: false,
106
+ supportsSQLAdmin: false,
107
+ supportsDocumentAdmin: true,
108
+ supportsSchemaAdmin: true
109
+ };
110
+ const DEFAULT_CAPABILITIES = {
111
+ key: "(default)",
112
+ label: "Default",
113
+ supportsRelations: true,
114
+ supportsSubcollections: true,
115
+ supportsRLS: true,
116
+ supportsReferences: true,
117
+ supportsColumnTypes: true,
118
+ supportsRealtime: true,
119
+ supportsSQLAdmin: true,
120
+ supportsDocumentAdmin: true,
121
+ supportsSchemaAdmin: true
122
+ };
123
+ const CAPABILITIES_REGISTRY = {
124
+ postgres: POSTGRES_CAPABILITIES,
125
+ firestore: FIREBASE_CAPABILITIES,
126
+ mongodb: MONGODB_CAPABILITIES,
127
+ "(default)": DEFAULT_CAPABILITIES
128
+ };
129
+ function getDataSourceCapabilities(driver) {
130
+ if (!driver) return POSTGRES_CAPABILITIES;
131
+ return CAPABILITIES_REGISTRY[driver] ?? DEFAULT_CAPABILITIES;
80
132
  }
133
+ const DEFAULT_ONE_OF_TYPE = "type";
134
+ const DEFAULT_ONE_OF_VALUE = "value";
81
135
  const snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g;
82
136
  const toSnakeCase = (str) => {
83
137
  const regExpMatchArray = str.match(snakeCaseRegex);
@@ -975,6 +1029,21 @@
975
1029
  const singularName = snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName;
976
1030
  return `${singularName}_id`;
977
1031
  }
1032
+ function createRelationRef(id, path2) {
1033
+ return {
1034
+ id,
1035
+ path: path2,
1036
+ __type: "relation"
1037
+ };
1038
+ }
1039
+ function createRelationRefWithData(id, path2, data) {
1040
+ return {
1041
+ id,
1042
+ path: path2,
1043
+ __type: "relation",
1044
+ data
1045
+ };
1046
+ }
978
1047
  function enumToObjectEntries(enumValues) {
979
1048
  if (Array.isArray(enumValues)) {
980
1049
  return enumValues;
@@ -998,15 +1067,35 @@
998
1067
  if (collection.childCollections) {
999
1068
  return collection.childCollections() ?? [];
1000
1069
  }
1001
- if (isFirebaseCollection(collection) && collection.subcollections) {
1070
+ if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
1002
1071
  return collection.subcollections() ?? [];
1003
1072
  }
1004
- if (isPostgresCollection(collection) && collection.relations) {
1073
+ if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
1005
1074
  const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
1006
1075
  return manyRelations.map((r) => {
1007
1076
  const target = r.target();
1008
- return r.overrides ? mergeDeep(target, r.overrides) : target;
1009
- });
1077
+ if (!target) return void 0;
1078
+ const relationKey = r.relationName || target.slug;
1079
+ let customName;
1080
+ if (collection.properties) {
1081
+ const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1082
+ if (prop && prop[1].name) {
1083
+ customName = prop[1].name;
1084
+ }
1085
+ }
1086
+ const baseOverrides = {
1087
+ slug: relationKey
1088
+ };
1089
+ if (customName) {
1090
+ baseOverrides.name = customName;
1091
+ baseOverrides.singularName = customName;
1092
+ }
1093
+ const targetWithOverrides = {
1094
+ ...target,
1095
+ ...baseOverrides
1096
+ };
1097
+ return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1098
+ }).filter((c) => Boolean(c));
1010
1099
  }
1011
1100
  return [];
1012
1101
  }
@@ -1113,7 +1202,7 @@
1113
1202
  if (!newRelation.foreignKeyOnTarget) {
1114
1203
  let foundForeignKey = false;
1115
1204
  try {
1116
- const targetRelations = targetCollection.relations || [];
1205
+ const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
1117
1206
  for (const targetRel of targetRelations) {
1118
1207
  if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) {
1119
1208
  try {
@@ -1139,7 +1228,7 @@
1139
1228
  let isManyToManyInverse = false;
1140
1229
  if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
1141
1230
  try {
1142
- const targetRelations = targetCollection.relations || [];
1231
+ const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
1143
1232
  for (const targetRel of targetRelations) {
1144
1233
  if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
1145
1234
  isManyToManyInverse = true;
@@ -1173,14 +1262,21 @@
1173
1262
  }
1174
1263
  return newRelation;
1175
1264
  }
1265
+ const _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
1176
1266
  function resolveCollectionRelations(collection) {
1267
+ const cached = _resolvedRelationsCache.get(collection);
1268
+ if (cached) return cached;
1269
+ if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
1270
+ const relCollection = collection;
1177
1271
  const relations = {};
1178
- if (collection.relations) {
1179
- collection.relations.forEach((relation) => {
1272
+ const registeredRelationNames = /* @__PURE__ */ new Set();
1273
+ if (relCollection.relations) {
1274
+ relCollection.relations.forEach((relation) => {
1180
1275
  const normalizedRelation = sanitizeRelation(relation, collection);
1181
1276
  const relationKey = normalizedRelation.relationName;
1182
1277
  if (relationKey) {
1183
1278
  relations[relationKey] = normalizedRelation;
1279
+ registeredRelationNames.add(relationKey);
1184
1280
  }
1185
1281
  });
1186
1282
  }
@@ -1192,15 +1288,17 @@
1192
1288
  sourceCollection: collection
1193
1289
  });
1194
1290
  if (relation) {
1195
- if (!relations[propKey]) {
1196
- if (!relation.relationName) {
1197
- relation.relationName = propKey;
1198
- }
1199
- relations[propKey] = sanitizeRelation(relation, collection);
1291
+ if (relations[propKey]) return;
1292
+ if (!relation.relationName) {
1293
+ relation.relationName = propKey;
1200
1294
  }
1295
+ const normalizedRelation = sanitizeRelation(relation, collection);
1296
+ relations[propKey] = normalizedRelation;
1297
+ registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
1201
1298
  }
1202
1299
  });
1203
1300
  }
1301
+ _resolvedRelationsCache.set(collection, relations);
1204
1302
  return relations;
1205
1303
  }
1206
1304
  function resolvePropertyRelation({
@@ -1209,7 +1307,24 @@
1209
1307
  sourceCollection
1210
1308
  }) {
1211
1309
  if (property.type !== "relation") return void 0;
1212
- const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === property.relationName);
1310
+ const relProp = property;
1311
+ if (relProp.target) {
1312
+ return {
1313
+ relationName: relProp.relationName || propertyKey,
1314
+ target: relProp.target,
1315
+ cardinality: relProp.cardinality || "one",
1316
+ direction: relProp.direction || "owning",
1317
+ inverseRelationName: relProp.inverseRelationName,
1318
+ localKey: relProp.localKey,
1319
+ foreignKeyOnTarget: relProp.foreignKeyOnTarget,
1320
+ through: relProp.through,
1321
+ joinPath: relProp.joinPath,
1322
+ onUpdate: relProp.onUpdate,
1323
+ onDelete: relProp.onDelete,
1324
+ overrides: relProp.overrides
1325
+ };
1326
+ }
1327
+ const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
1213
1328
  if (!relation) {
1214
1329
  console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1215
1330
  return void 0;
@@ -1217,7 +1332,7 @@
1217
1332
  return relation;
1218
1333
  }
1219
1334
  function getTableName(collection) {
1220
- if (isPostgresCollection(collection)) {
1335
+ if (getDataSourceCapabilities(collection.driver).supportsRelations) {
1221
1336
  return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1222
1337
  }
1223
1338
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
@@ -1233,6 +1348,14 @@
1233
1348
  function getColumnName(fullColumn) {
1234
1349
  return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
1235
1350
  }
1351
+ function findRelation(resolvedRelations, key) {
1352
+ if (resolvedRelations[key]) return resolvedRelations[key];
1353
+ const slugKey = key.replace(/_/g, "-");
1354
+ if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
1355
+ const snakeKey = key.replace(/-/g, "_");
1356
+ if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
1357
+ return void 0;
1358
+ }
1236
1359
  var logic = { exports: {} };
1237
1360
  (function(module2, exports$1) {
1238
1361
  (function(root2, factory) {
@@ -3005,10 +3128,12 @@
3005
3128
  collectionsByTableName = /* @__PURE__ */ new Map();
3006
3129
  collectionsBySlug = /* @__PURE__ */ new Map();
3007
3130
  rootCollections = [];
3131
+ cachedCollectionsList = null;
3008
3132
  // Raw configuration layer (used by Collection Editor AST generator)
3009
3133
  rawCollectionsByTableName = /* @__PURE__ */ new Map();
3010
3134
  rawCollectionsBySlug = /* @__PURE__ */ new Map();
3011
3135
  rawRootCollections = [];
3136
+ cachedRawCollectionsList = null;
3012
3137
  // Snapshot of raw input for idempotency check — compared BEFORE normalization
3013
3138
  // to avoid the issue where normalization creates new objects that always fail equality.
3014
3139
  lastRawInputSnapshot = null;
@@ -3021,9 +3146,11 @@
3021
3146
  this.collectionsByTableName.clear();
3022
3147
  this.collectionsBySlug.clear();
3023
3148
  this.rootCollections = [];
3149
+ this.cachedCollectionsList = null;
3024
3150
  this.rawCollectionsByTableName.clear();
3025
3151
  this.rawCollectionsBySlug.clear();
3026
3152
  this.rawRootCollections = [];
3153
+ this.cachedRawCollectionsList = null;
3027
3154
  }
3028
3155
  /**
3029
3156
  * Registers a collection and its subcollections recursively.
@@ -3056,12 +3183,14 @@
3056
3183
  this.rawCollectionsBySlug.set(raw.slug, raw);
3057
3184
  }
3058
3185
  });
3059
- normalizedCollections.forEach((c, index) => {
3186
+ normalizedCollections.forEach((c) => {
3060
3187
  const subcollections = getSubcollections(c);
3061
- const rawSubcollections = getSubcollections(collections[index]);
3062
- if (subcollections && rawSubcollections) {
3063
- subcollections.forEach((subCollection, subIndex) => {
3064
- this._registerRecursively(this.normalizeCollection(subCollection), cloneDeep(rawSubcollections[subIndex]));
3188
+ if (subcollections && subcollections.length > 0) {
3189
+ subcollections.forEach((subCollection) => {
3190
+ if (!subCollection) return;
3191
+ this._registerRecursively(this.normalizeCollection({
3192
+ ...subCollection
3193
+ }), cloneDeep(subCollection));
3065
3194
  });
3066
3195
  }
3067
3196
  });
@@ -3087,41 +3216,100 @@
3087
3216
  if (rawCollection.slug) {
3088
3217
  this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
3089
3218
  }
3090
- const subcollections = getSubcollections(collection);
3091
- const rawSubcollections = getSubcollections(rawCollection);
3092
- if (subcollections && rawSubcollections) {
3093
- subcollections.forEach((subCollection, index) => {
3094
- this._registerRecursively(this.normalizeCollection(subCollection), cloneDeep(rawSubcollections[index]));
3219
+ const subcollections = getSubcollections(normalizedCollection);
3220
+ if (subcollections && subcollections.length > 0) {
3221
+ subcollections.forEach((subCollection) => {
3222
+ if (!subCollection) return;
3223
+ this._registerRecursively(this.normalizeCollection({
3224
+ ...subCollection
3225
+ }), cloneDeep(subCollection));
3095
3226
  });
3096
3227
  }
3097
3228
  }
3098
3229
  normalizeCollection(collection) {
3099
- const relations = isPostgresCollection(collection) ? collection.relations ?? [] : [];
3100
- const properties = this.normalizeProperties(collection.properties, relations);
3101
- collection.properties = properties;
3102
- if (!collection.childCollections) {
3103
- if (isFirebaseCollection(collection) && collection.subcollections) {
3104
- collection.childCollections = collection.subcollections;
3105
- } else if (isPostgresCollection(collection) && collection.relations) {
3106
- const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
3230
+ const result = {
3231
+ ...collection
3232
+ };
3233
+ const extractedRelations = this.extractRelationsFromProperties(result.properties);
3234
+ const relResult = result;
3235
+ const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
3236
+ const mergedRelationsRaw = [...extractedRelations];
3237
+ for (const manual of manualRelations) {
3238
+ const name = manual.relationName;
3239
+ if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
3240
+ mergedRelationsRaw.push(manual);
3241
+ }
3242
+ }
3243
+ let mergedRelations = mergedRelationsRaw;
3244
+ if (getDataSourceCapabilities(result.driver).supportsRelations) {
3245
+ mergedRelations = mergedRelationsRaw.map((r) => {
3246
+ try {
3247
+ return sanitizeRelation(r, result);
3248
+ } catch {
3249
+ return r;
3250
+ }
3251
+ });
3252
+ relResult.relations = mergedRelations;
3253
+ }
3254
+ const properties = this.normalizeProperties(result.properties, mergedRelations);
3255
+ result.properties = properties;
3256
+ if (!result.childCollections) {
3257
+ if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) {
3258
+ result.childCollections = result.subcollections;
3259
+ } else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
3260
+ const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
3107
3261
  if (manyRelations.length > 0) {
3108
- collection.childCollections = () => manyRelations.map((r) => {
3262
+ result.childCollections = () => manyRelations.map((r) => {
3109
3263
  const target = r.target();
3110
3264
  return r.overrides ? mergeDeep(target, r.overrides) : target;
3111
3265
  });
3112
3266
  }
3113
3267
  }
3114
3268
  }
3115
- return collection;
3269
+ return result;
3270
+ }
3271
+ /**
3272
+ * Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
3273
+ * This allows developers to define relations directly on properties without a separate
3274
+ * `relations[]` entry on the collection.
3275
+ */
3276
+ extractRelationsFromProperties(properties) {
3277
+ const relations = [];
3278
+ for (const [key, property] of Object.entries(properties)) {
3279
+ if (property.type === "relation") {
3280
+ const relProp = property;
3281
+ const target = relProp.target ?? relProp.relation?.target;
3282
+ if (target) {
3283
+ const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
3284
+ relations.push({
3285
+ relationName,
3286
+ target,
3287
+ cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
3288
+ direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
3289
+ inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
3290
+ localKey: relProp.localKey ?? relProp.relation?.localKey,
3291
+ foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
3292
+ through: relProp.through ?? relProp.relation?.through,
3293
+ joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
3294
+ onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
3295
+ onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
3296
+ overrides: relProp.overrides ?? relProp.relation?.overrides
3297
+ });
3298
+ }
3299
+ } else if (property.type === "map" && property.properties) {
3300
+ relations.push(...this.extractRelationsFromProperties(property.properties));
3301
+ }
3302
+ }
3303
+ return relations;
3116
3304
  }
3117
3305
  normalizeProperties(properties, relations) {
3118
3306
  const newProperties = {};
3119
3307
  for (const key in properties) {
3120
- newProperties[key] = this.normalizeProperty(properties[key], relations);
3308
+ newProperties[key] = this.normalizeProperty(key, properties[key], relations);
3121
3309
  }
3122
3310
  return newProperties;
3123
3311
  }
3124
- normalizeProperty(property, relations) {
3312
+ normalizeProperty(key, property, relations) {
3125
3313
  const newProperty = {
3126
3314
  ...property
3127
3315
  };
@@ -3131,9 +3319,9 @@
3131
3319
  const arrayProp = newProperty;
3132
3320
  if (arrayProp.of) {
3133
3321
  if (Array.isArray(arrayProp.of)) {
3134
- arrayProp.of = arrayProp.of.map((p) => this.normalizeProperty(p, relations));
3322
+ arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations));
3135
3323
  } else {
3136
- arrayProp.of = this.normalizeProperty(arrayProp.of, relations);
3324
+ arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations);
3137
3325
  }
3138
3326
  } else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
3139
3327
  arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations);
@@ -3145,11 +3333,12 @@
3145
3333
  }
3146
3334
  } else if (newProperty.type === "relation") {
3147
3335
  const relationProperty = newProperty;
3148
- const relation = relations.find((r) => r.relationName === relationProperty.relationName);
3336
+ const name = relationProperty.relationName || key;
3337
+ const relation = relations.find((r) => r.relationName === name);
3149
3338
  if (relation) {
3150
3339
  relationProperty.relation = relation;
3151
3340
  } else {
3152
- console.warn(`Could not find relation for property with relationName: ${relationProperty.relationName}`);
3341
+ console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
3153
3342
  }
3154
3343
  }
3155
3344
  return newProperty;
@@ -3157,6 +3346,11 @@
3157
3346
  get(path2) {
3158
3347
  const bySlug = this.collectionsBySlug.get(path2);
3159
3348
  if (bySlug) return bySlug;
3349
+ if (path2.includes("-")) {
3350
+ const normalized = path2.replace(/-/g, "_");
3351
+ const byNormalized = this.collectionsBySlug.get(normalized);
3352
+ if (byNormalized) return byNormalized;
3353
+ }
3160
3354
  return this.collectionsByTableName.get(path2);
3161
3355
  }
3162
3356
  /**
@@ -3166,6 +3360,11 @@
3166
3360
  getRaw(path2) {
3167
3361
  const bySlug = this.rawCollectionsBySlug.get(path2);
3168
3362
  if (bySlug) return bySlug;
3363
+ if (path2.includes("-")) {
3364
+ const normalized = path2.replace(/-/g, "_");
3365
+ const byNormalized = this.rawCollectionsBySlug.get(normalized);
3366
+ if (byNormalized) return byNormalized;
3367
+ }
3169
3368
  return this.rawCollectionsByTableName.get(path2);
3170
3369
  }
3171
3370
  /**
@@ -3187,11 +3386,11 @@
3187
3386
  }
3188
3387
  for (let i = 2; i < pathSegments.length; i += 2) {
3189
3388
  const relationKey = pathSegments[i];
3190
- if (!isPostgresCollection(currentCollection)) {
3191
- throw new Error(`Relation path navigation requires a PostgreSQL collection, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
3389
+ if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) {
3390
+ throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
3192
3391
  }
3193
3392
  const resolvedRelations = resolveCollectionRelations(currentCollection);
3194
- const relation = resolvedRelations[relationKey];
3393
+ const relation = findRelation(resolvedRelations, relationKey);
3195
3394
  if (!relation) {
3196
3395
  throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
3197
3396
  }
@@ -3201,10 +3400,16 @@
3201
3400
  return currentCollection;
3202
3401
  }
3203
3402
  getCollections() {
3204
- return Array.from(this.collectionsByTableName.values());
3403
+ if (!this.cachedCollectionsList) {
3404
+ this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
3405
+ }
3406
+ return this.cachedCollectionsList;
3205
3407
  }
3206
3408
  getRawCollections() {
3207
- return Array.from(this.rawCollectionsByTableName.values());
3409
+ if (!this.cachedRawCollectionsList) {
3410
+ this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
3411
+ }
3412
+ return this.cachedRawCollectionsList;
3208
3413
  }
3209
3414
  /**
3210
3415
  * Resolves a multi-segment path like "products/123/locales" and returns
@@ -3260,24 +3465,62 @@
3260
3465
  "lte": "<=",
3261
3466
  "in": "in",
3262
3467
  "nin": "not-in",
3468
+ "not-in": "not-in",
3263
3469
  "cs": "array-contains",
3264
- "csa": "array-contains-any"
3470
+ "csa": "array-contains-any",
3471
+ "==": "==",
3472
+ "!=": "!=",
3473
+ ">": ">",
3474
+ ">=": ">=",
3475
+ "<": "<",
3476
+ "<=": "<=",
3477
+ "array-contains": "array-contains",
3478
+ "array-contains-any": "array-contains-any"
3265
3479
  };
3266
3480
  const filter = {};
3267
3481
  for (const [field, rawValue] of Object.entries(where)) {
3268
- const dotIndex = rawValue.indexOf(".");
3269
- if (dotIndex === -1) continue;
3270
- const op = rawValue.substring(0, dotIndex);
3271
- let value = rawValue.substring(dotIndex + 1);
3272
- if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
3273
- value = value.slice(1, -1).split(",").map((v) => v.trim());
3482
+ if (rawValue === null) {
3483
+ filter[field] = ["==", null];
3484
+ continue;
3485
+ }
3486
+ if (typeof rawValue === "boolean") {
3487
+ filter[field] = ["==", rawValue];
3488
+ continue;
3489
+ }
3490
+ if (typeof rawValue === "number") {
3491
+ filter[field] = ["==", rawValue];
3492
+ continue;
3274
3493
  }
3275
- if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
3276
- value = Number(value);
3494
+ if (Array.isArray(rawValue) && rawValue.length === 2) {
3495
+ const [rawOp, val] = rawValue;
3496
+ const mappedOp = operatorMap[rawOp] ?? "==";
3497
+ filter[field] = [mappedOp, val];
3498
+ continue;
3277
3499
  }
3278
- const mappedOp = operatorMap[op];
3279
- if (mappedOp) {
3280
- filter[field] = [mappedOp, value];
3500
+ if (typeof rawValue === "string") {
3501
+ const dotIndex = rawValue.indexOf(".");
3502
+ if (dotIndex === -1) {
3503
+ filter[field] = ["==", rawValue];
3504
+ continue;
3505
+ }
3506
+ const op = rawValue.substring(0, dotIndex);
3507
+ let value = rawValue.substring(dotIndex + 1);
3508
+ if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
3509
+ value = value.slice(1, -1).split(",").map((v) => v.trim());
3510
+ }
3511
+ if (value === "null") {
3512
+ value = null;
3513
+ } else if (value === "true") {
3514
+ value = true;
3515
+ } else if (value === "false") {
3516
+ value = false;
3517
+ } else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
3518
+ value = Number(value);
3519
+ }
3520
+ const mappedOp = operatorMap[op];
3521
+ if (mappedOp) {
3522
+ filter[field] = [mappedOp, value];
3523
+ }
3281
3524
  }
3282
3525
  }
3283
3526
  return Object.keys(filter).length > 0 ? filter : void 0;
@@ -3296,7 +3539,7 @@
3296
3539
  const entities = await driver.fetchCollection({
3297
3540
  path: slug,
3298
3541
  limit: params?.limit,
3299
- startAfter: params?.offset,
3542
+ offset: params?.offset,
3300
3543
  filter: convertWhereToFilter(params?.where),
3301
3544
  orderBy: orderParsed?.[0],
3302
3545
  order: orderParsed?.[1],
@@ -3358,7 +3601,7 @@
3358
3601
  return driver.listenCollection({
3359
3602
  path: slug,
3360
3603
  limit: params?.limit,
3361
- startAfter: params?.offset,
3604
+ offset: params?.offset,
3362
3605
  filter: convertWhereToFilter(params?.where),
3363
3606
  orderBy: orderParsed?.[0],
3364
3607
  order: orderParsed?.[1],
@@ -3405,7 +3648,8 @@
3405
3648
  if (prop === "collection") return getAccessor;
3406
3649
  if (typeof prop === "symbol") return void 0;
3407
3650
  if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
3408
- return getAccessor(prop);
3651
+ const slug = toSnakeCase(prop);
3652
+ return getAccessor(slug);
3409
3653
  }
3410
3654
  });
3411
3655
  }
@@ -3463,7 +3707,7 @@
3463
3707
  * Build relation-based conditions for different relation types
3464
3708
  */
3465
3709
  static buildRelationConditions(relation, parentEntityId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry) {
3466
- console.debug(`🔍 [buildRelationConditions] Building conditions for relation:`, {
3710
+ console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
3467
3711
  relationName: relation.relationName,
3468
3712
  cardinality: relation.cardinality,
3469
3713
  direction: relation.direction,
@@ -3475,7 +3719,7 @@
3475
3719
  const joinConditions = [];
3476
3720
  const whereConditions = [];
3477
3721
  if (relation.joinPath && relation.joinPath.length > 0) {
3478
- console.debug(`🔍 [buildRelationConditions] Using joinPath logic`);
3722
+ console.debug("🔍 [buildRelationConditions] Using joinPath logic");
3479
3723
  const {
3480
3724
  joins,
3481
3725
  finalCondition
@@ -3483,37 +3727,37 @@
3483
3727
  joinConditions.push(...joins);
3484
3728
  whereConditions.push(finalCondition);
3485
3729
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
3486
- console.debug(`🔍 [buildRelationConditions] Using owning many-to-many with explicit through`);
3730
+ console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
3487
3731
  const junctionResult = this.buildJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
3488
3732
  joinConditions.push(junctionResult.join);
3489
3733
  whereConditions.push(junctionResult.condition);
3490
3734
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
3491
- console.debug(`🔍 [buildRelationConditions] Using inverse many-to-many with explicit through`);
3735
+ console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
3492
3736
  const junctionResult = this.buildInverseJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
3493
3737
  joinConditions.push(junctionResult.join);
3494
3738
  whereConditions.push(junctionResult.condition);
3495
3739
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
3496
- console.debug(`🔍 [buildRelationConditions] Handling inverse many relationship without explicit through`);
3740
+ console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
3497
3741
  const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
3498
3742
  if (junctionInfo) {
3499
- console.debug(`🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions`);
3743
+ console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
3500
3744
  const junctionResult = this.buildInverseJunctionTableConditions(junctionInfo, targetIdColumn, parentEntityId, registry);
3501
3745
  joinConditions.push(junctionResult.join);
3502
3746
  whereConditions.push(junctionResult.condition);
3503
3747
  } else if (relation.foreignKeyOnTarget) {
3504
- console.debug(`🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target`);
3748
+ console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
3505
3749
  const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
3506
3750
  whereConditions.push(simpleCondition);
3507
3751
  } else {
3508
- console.error(`🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified`);
3752
+ console.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
3509
3753
  throw new Error(`Cannot resolve inverse many relation '${relation.relationName}'. Either specify 'through' property, ensure corresponding owning relation exists with junction table configuration, or specify 'foreignKeyOnTarget' for one-to-many relationships.`);
3510
3754
  }
3511
3755
  } else {
3512
- console.debug(`🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR`);
3756
+ console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
3513
3757
  const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
3514
3758
  whereConditions.push(simpleCondition);
3515
3759
  }
3516
- console.debug(`🔍 [buildRelationConditions] Final result:`, {
3760
+ console.debug("🔍 [buildRelationConditions] Final result:", {
3517
3761
  joinConditionsCount: joinConditions.length,
3518
3762
  whereConditionsCount: whereConditions.length
3519
3763
  });
@@ -3764,7 +4008,8 @@
3764
4008
  static buildSearchConditions(searchString, properties, table) {
3765
4009
  const searchConditions = [];
3766
4010
  for (const [key, prop] of Object.entries(properties)) {
3767
- if (prop.type === "string" && !prop.enum && prop.isId !== "uuid") {
4011
+ const p = prop;
4012
+ if (p.type === "string" && !p.enum && p.isId !== "uuid") {
3768
4013
  const fieldColumn = table[key];
3769
4014
  if (fieldColumn) {
3770
4015
  searchConditions.push(drizzleOrm.ilike(fieldColumn, `%${searchString}%`));
@@ -3927,29 +4172,29 @@
3927
4172
  try {
3928
4173
  console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
3929
4174
  if (!relation.inverseRelationName) {
3930
- console.debug(`🔍 [findCorrespondingJunctionTable] No inverseRelationName specified`);
4175
+ console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
3931
4176
  return null;
3932
4177
  }
3933
4178
  const targetCollection = relation.target();
3934
4179
  console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
3935
4180
  const targetCollectionRelations = resolveCollectionRelations(targetCollection);
3936
- console.debug(`🔍 [findCorrespondingJunctionTable] Target collection relations:`, Object.keys(targetCollectionRelations));
4181
+ console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
3937
4182
  const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
3938
4183
  if (!correspondingRelation) {
3939
4184
  console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
3940
4185
  return null;
3941
4186
  }
3942
- console.debug(`🔍 [findCorrespondingJunctionTable] Found relation:`, {
4187
+ console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
3943
4188
  relationName: correspondingRelation.relationName,
3944
4189
  cardinality: correspondingRelation.cardinality,
3945
4190
  direction: correspondingRelation.direction,
3946
4191
  hasThrough: !!correspondingRelation.through
3947
4192
  });
3948
4193
  if (correspondingRelation.cardinality !== "many" || correspondingRelation.direction !== "owning" || !correspondingRelation.through) {
3949
- console.debug(`🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table`);
4194
+ console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
3950
4195
  return null;
3951
4196
  }
3952
- console.debug(`🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!`);
4197
+ console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
3953
4198
  const through = correspondingRelation.through;
3954
4199
  const result = {
3955
4200
  table: through.table,
@@ -3958,7 +4203,7 @@
3958
4203
  targetColumn: through.sourceColumn
3959
4204
  // Swapped for inverse relation
3960
4205
  };
3961
- console.debug(`🔍 [findCorrespondingJunctionTable] Returning junction info:`, result);
4206
+ console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
3962
4207
  return result;
3963
4208
  } catch (error) {
3964
4209
  console.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}':`, error);
@@ -3967,10 +4212,19 @@
3967
4212
  }
3968
4213
  }
3969
4214
  const PostgresConditionBuilder = DrizzleConditionBuilder;
4215
+ function getColumnMeta(col) {
4216
+ const raw = col;
4217
+ return {
4218
+ columnType: typeof raw.columnType === "string" ? raw.columnType : void 0,
4219
+ dataType: typeof raw.dataType === "string" ? raw.dataType : void 0,
4220
+ primary: typeof raw.primary === "boolean" ? raw.primary : void 0
4221
+ };
4222
+ }
3970
4223
  function getCollectionByPath(collectionPath, registry) {
3971
4224
  const collection = registry.getCollectionByPath(collectionPath);
3972
4225
  if (!collection) {
3973
- throw new Error(`Collection not found: ${collectionPath}`);
4226
+ const registered = registry.getCollections().map((c) => c.slug).join(", ");
4227
+ throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
3974
4228
  }
3975
4229
  return collection;
3976
4230
  }
@@ -3987,7 +4241,8 @@
3987
4241
  if (collection.properties) {
3988
4242
  const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
3989
4243
  fieldName: key,
3990
- type: prop.type === "number" ? "number" : "string"
4244
+ type: prop.type === "number" ? "number" : "string",
4245
+ isUUID: prop.isId === "uuid"
3991
4246
  }));
3992
4247
  if (idProps.length > 0) {
3993
4248
  return idProps;
@@ -3997,19 +4252,25 @@
3997
4252
  for (const [key, colRaw] of Object.entries(table)) {
3998
4253
  const col = colRaw;
3999
4254
  if (col && typeof col === "object" && "primary" in col && col.primary) {
4000
- const type = col.dataType === "number" || col.columnType === "PgSerial" || col.columnType === "PgInteger" ? "number" : "string";
4255
+ const meta = getColumnMeta(col);
4256
+ const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
4257
+ const isUUID = meta.columnType === "PgUUID";
4001
4258
  keys2.push({
4002
4259
  fieldName: key,
4003
- type
4260
+ type,
4261
+ isUUID
4004
4262
  });
4005
4263
  }
4006
4264
  }
4007
4265
  if (keys2.length === 0 && "id" in table) {
4008
4266
  const idCol = table["id"];
4009
- const type = idCol.dataType === "number" || idCol.columnType === "PgSerial" || idCol.columnType === "PgInteger" ? "number" : "string";
4267
+ const idMeta = getColumnMeta(idCol);
4268
+ const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
4269
+ const isUUID = idMeta.columnType === "PgUUID";
4010
4270
  keys2.push({
4011
4271
  fieldName: "id",
4012
- type
4272
+ type,
4273
+ isUUID
4013
4274
  });
4014
4275
  }
4015
4276
  return keys2;
@@ -4021,7 +4282,7 @@
4021
4282
  }
4022
4283
  if (primaryKeys.length === 1) {
4023
4284
  const pk = primaryKeys[0];
4024
- if (pk.type === "number") {
4285
+ if (pk.type === "number" && !pk.isUUID) {
4025
4286
  const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
4026
4287
  if (isNaN(parsed)) {
4027
4288
  throw new Error(`Invalid numeric ID: ${idValue}`);
@@ -4039,7 +4300,7 @@
4039
4300
  for (let i = 0; i < primaryKeys.length; i++) {
4040
4301
  const pk = primaryKeys[i];
4041
4302
  const val = parts[i];
4042
- if (pk.type === "number") {
4303
+ if (pk.type === "number" && !pk.isUUID) {
4043
4304
  const parsed = parseInt(val, 10);
4044
4305
  if (isNaN(parsed)) {
4045
4306
  throw new Error(`Invalid numeric ID component: ${val}`);
@@ -4098,28 +4359,37 @@
4098
4359
  return obj;
4099
4360
  }
4100
4361
  function serializeDataToServer(entity, properties, collection, registry) {
4101
- if (!entity || !properties) return entity;
4362
+ if (!entity || !properties) return {
4363
+ scalarData: entity ?? {},
4364
+ inverseRelationUpdates: [],
4365
+ joinPathRelationUpdates: []
4366
+ };
4102
4367
  const result = {};
4103
4368
  const resolvedRelations = collection ? resolveCollectionRelations(collection) : {};
4104
4369
  const inverseRelationUpdates = [];
4105
4370
  const joinPathRelationUpdates = [];
4371
+ const foreignKeys = /* @__PURE__ */ new Set();
4372
+ Object.values(resolvedRelations).forEach((relation) => {
4373
+ if (relation.localKey) foreignKeys.add(relation.localKey);
4374
+ });
4106
4375
  for (const [key, value] of Object.entries(entity)) {
4107
4376
  const property = properties[key];
4377
+ const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
4108
4378
  if (!property) {
4109
- result[key] = value;
4379
+ result[key] = effectiveValue;
4110
4380
  continue;
4111
4381
  }
4112
4382
  if (property.type === "relation" && collection) {
4113
- const relation = resolvedRelations[key];
4383
+ const relation = findRelation(resolvedRelations, key);
4114
4384
  if (relation) {
4115
4385
  if (relation.direction === "owning" && relation.localKey) {
4116
- const serializedValue = serializePropertyToServer(value, property);
4117
- if (serializedValue !== null && serializedValue !== void 0) {
4386
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4387
+ if (serializedValue !== void 0) {
4118
4388
  result[relation.localKey] = serializedValue;
4119
4389
  }
4120
4390
  continue;
4121
4391
  } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
4122
- const serializedValue = serializePropertyToServer(value, property);
4392
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4123
4393
  const pks = getPrimaryKeys(collection, registry);
4124
4394
  inverseRelationUpdates.push({
4125
4395
  relationKey: key,
@@ -4129,7 +4399,7 @@
4129
4399
  });
4130
4400
  continue;
4131
4401
  } else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
4132
- const serializedValue = serializePropertyToServer(value, property);
4402
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4133
4403
  if (relation.cardinality === "one") {
4134
4404
  joinPathRelationUpdates.push({
4135
4405
  relationKey: key,
@@ -4147,7 +4417,7 @@
4147
4417
  }
4148
4418
  continue;
4149
4419
  } else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
4150
- const serializedValue = serializePropertyToServer(value, property);
4420
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4151
4421
  joinPathRelationUpdates.push({
4152
4422
  relationKey: key,
4153
4423
  relation,
@@ -4157,15 +4427,13 @@
4157
4427
  }
4158
4428
  }
4159
4429
  }
4160
- result[key] = serializePropertyToServer(value, property);
4430
+ result[key] = serializePropertyToServer(effectiveValue, property);
4161
4431
  }
4162
- if (inverseRelationUpdates.length > 0) {
4163
- result.__inverseRelationUpdates = inverseRelationUpdates;
4164
- }
4165
- if (joinPathRelationUpdates.length > 0) {
4166
- result.__joinPathRelationUpdates = joinPathRelationUpdates;
4167
- }
4168
- return result;
4432
+ return {
4433
+ scalarData: result,
4434
+ inverseRelationUpdates,
4435
+ joinPathRelationUpdates
4436
+ };
4169
4437
  }
4170
4438
  function serializePropertyToServer(value, property) {
4171
4439
  if (value === null || value === void 0) {
@@ -4179,10 +4447,28 @@
4179
4447
  } else if (typeof value === "object" && value !== null && "id" in value) {
4180
4448
  return value.id;
4181
4449
  }
4450
+ if (value === "") return null;
4182
4451
  return value;
4183
4452
  case "array":
4184
- if (Array.isArray(value) && property.of) {
4185
- return value.map((item) => serializePropertyToServer(item, property.of));
4453
+ if (Array.isArray(value)) {
4454
+ if (property.of) {
4455
+ return value.map((item) => serializePropertyToServer(item, property.of));
4456
+ } else if (property.oneOf) {
4457
+ const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
4458
+ const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
4459
+ return value.map((e) => {
4460
+ if (e === null) return null;
4461
+ if (typeof e !== "object") return e;
4462
+ const rec = e;
4463
+ const type = rec[typeField];
4464
+ const childProperty = property.oneOf?.properties[type];
4465
+ if (!type || !childProperty) return e;
4466
+ return {
4467
+ [typeField]: type,
4468
+ [valueField]: serializePropertyToServer(rec[valueField], childProperty)
4469
+ };
4470
+ });
4471
+ }
4186
4472
  }
4187
4473
  return value;
4188
4474
  case "map":
@@ -4206,38 +4492,20 @@
4206
4492
  async function parseDataFromServer(data, collection, db, registry) {
4207
4493
  const properties = collection.properties;
4208
4494
  if (!data || !properties) return data;
4209
- const result = {};
4210
4495
  const resolvedRelations = resolveCollectionRelations(collection);
4211
- const internalFKColumns = /* @__PURE__ */ new Set();
4212
- Object.values(resolvedRelations).forEach((relation) => {
4213
- if (relation.localKey && !properties[relation.localKey]) {
4214
- internalFKColumns.add(relation.localKey);
4215
- }
4496
+ const result = normalizeScalarValues(data, properties, collection, resolvedRelations, {
4497
+ skipRelations: false
4216
4498
  });
4217
- for (const [key, value] of Object.entries(data)) {
4218
- if (internalFKColumns.has(key)) {
4219
- continue;
4220
- }
4221
- const property = properties[key];
4222
- if (!property) {
4223
- continue;
4224
- }
4225
- result[key] = parsePropertyFromServer(value, property, collection, key);
4226
- }
4227
4499
  for (const [propKey, property] of Object.entries(properties)) {
4228
4500
  if (property.type === "relation" && !(propKey in result)) {
4229
- const relation = resolvedRelations[propKey];
4501
+ const relation = findRelation(resolvedRelations, propKey);
4230
4502
  if (relation) {
4231
4503
  if (relation.direction === "owning" && relation.localKey && relation.localKey in data) {
4232
4504
  const fkValue = data[relation.localKey];
4233
4505
  if (fkValue !== null && fkValue !== void 0) {
4234
4506
  try {
4235
4507
  const targetCollection = relation.target();
4236
- result[propKey] = {
4237
- id: fkValue.toString(),
4238
- path: targetCollection.slug,
4239
- __type: "relation"
4240
- };
4508
+ result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
4241
4509
  } catch (e) {
4242
4510
  console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
4243
4511
  }
@@ -4256,18 +4524,10 @@
4256
4524
  if (relation.cardinality === "one") {
4257
4525
  const targetPks = getPrimaryKeys(targetCollection, registry);
4258
4526
  const relatedEntity = relatedEntities[0];
4259
- result[propKey] = {
4260
- id: buildCompositeId(relatedEntity, targetPks),
4261
- path: targetCollection.slug,
4262
- __type: "relation"
4263
- };
4527
+ result[propKey] = createRelationRef(buildCompositeId(relatedEntity, targetPks), targetCollection.slug);
4264
4528
  } else {
4265
4529
  const targetPks = getPrimaryKeys(targetCollection, registry);
4266
- result[propKey] = relatedEntities.map((entity) => ({
4267
- id: buildCompositeId(entity, targetPks),
4268
- path: targetCollection.slug,
4269
- __type: "relation"
4270
- }));
4530
+ result[propKey] = relatedEntities.map((entity) => createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug));
4271
4531
  }
4272
4532
  }
4273
4533
  }
@@ -4328,19 +4588,11 @@
4328
4588
  if (relation.cardinality === "one") {
4329
4589
  const joinResult = joinResults[0];
4330
4590
  const targetEntity = joinResult[targetTableName] || joinResult;
4331
- result[propKey] = {
4332
- id: buildCompositeId(targetEntity, targetPks),
4333
- path: targetCollection.slug,
4334
- __type: "relation"
4335
- };
4591
+ result[propKey] = createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
4336
4592
  } else {
4337
4593
  result[propKey] = joinResults.map((joinResult) => {
4338
4594
  const targetEntity = joinResult[targetTableName] || joinResult;
4339
- return {
4340
- id: buildCompositeId(targetEntity, targetPks),
4341
- path: targetCollection.slug,
4342
- __type: "relation"
4343
- };
4595
+ return createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
4344
4596
  });
4345
4597
  }
4346
4598
  }
@@ -4364,7 +4616,7 @@
4364
4616
  let relationDef = property.relation;
4365
4617
  if (!relationDef && propertyKey) {
4366
4618
  const resolvedRelations = resolveCollectionRelations(collection);
4367
- relationDef = resolvedRelations[propertyKey];
4619
+ relationDef = findRelation(resolvedRelations, propertyKey);
4368
4620
  }
4369
4621
  if (!relationDef) {
4370
4622
  relationDef = collection.relations?.find((rel) => rel.relationName === property.relationName);
@@ -4375,11 +4627,7 @@
4375
4627
  }
4376
4628
  try {
4377
4629
  const targetCollection = relationDef.target();
4378
- return {
4379
- id: value.toString(),
4380
- path: targetCollection.slug,
4381
- __type: "relation"
4382
- };
4630
+ return createRelationRef(value.toString(), targetCollection.slug);
4383
4631
  } catch (e) {
4384
4632
  console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
4385
4633
  return value;
@@ -4387,8 +4635,25 @@
4387
4635
  }
4388
4636
  return value;
4389
4637
  case "array":
4390
- if (Array.isArray(value) && property.of) {
4391
- return value.map((item) => parsePropertyFromServer(item, property.of, collection));
4638
+ if (Array.isArray(value)) {
4639
+ if (property.of) {
4640
+ return value.map((item) => parsePropertyFromServer(item, property.of, collection));
4641
+ } else if (property.oneOf) {
4642
+ const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
4643
+ const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
4644
+ return value.map((e) => {
4645
+ if (e === null) return null;
4646
+ if (typeof e !== "object") return e;
4647
+ const rec = e;
4648
+ const type = rec[typeField];
4649
+ const childProperty = property.oneOf?.properties[type];
4650
+ if (!type || !childProperty) return e;
4651
+ return {
4652
+ [typeField]: type,
4653
+ [valueField]: parsePropertyFromServer(rec[valueField], childProperty, collection)
4654
+ };
4655
+ });
4656
+ }
4392
4657
  }
4393
4658
  return value;
4394
4659
  case "map":
@@ -4433,11 +4698,8 @@
4433
4698
  return value;
4434
4699
  }
4435
4700
  }
4436
- function normalizeDbValues(data, collection) {
4437
- const properties = collection.properties;
4438
- if (!data || !properties) return data;
4701
+ function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
4439
4702
  const result = {};
4440
- const resolvedRelations = resolveCollectionRelations(collection);
4441
4703
  const internalFKColumns = /* @__PURE__ */ new Set();
4442
4704
  Object.values(resolvedRelations).forEach((relation) => {
4443
4705
  if (relation.localKey && !properties[relation.localKey]) {
@@ -4445,14 +4707,25 @@
4445
4707
  }
4446
4708
  });
4447
4709
  for (const [key, value] of Object.entries(data)) {
4448
- if (internalFKColumns.has(key)) continue;
4710
+ if (internalFKColumns.has(key)) {
4711
+ result[key] = value === null ? null : typeof value === "number" ? value : String(value);
4712
+ continue;
4713
+ }
4449
4714
  const property = properties[key];
4450
4715
  if (!property) continue;
4451
- if (property.type === "relation") continue;
4716
+ if (options.skipRelations && property.type === "relation") continue;
4452
4717
  result[key] = parsePropertyFromServer(value, property, collection, key);
4453
4718
  }
4454
4719
  return result;
4455
4720
  }
4721
+ function normalizeDbValues(data, collection) {
4722
+ const properties = collection.properties;
4723
+ if (!data || !properties) return data;
4724
+ const resolvedRelations = resolveCollectionRelations(collection);
4725
+ return normalizeScalarValues(data, properties, collection, resolvedRelations, {
4726
+ skipRelations: true
4727
+ });
4728
+ }
4456
4729
  class RelationService {
4457
4730
  constructor(db, registry) {
4458
4731
  this.db = db;
@@ -4464,9 +4737,10 @@
4464
4737
  async fetchRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
4465
4738
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
4466
4739
  const resolvedRelations = resolveCollectionRelations(parentCollection);
4467
- const relation = resolvedRelations[relationKey];
4740
+ const relation = findRelation(resolvedRelations, relationKey);
4468
4741
  if (!relation) {
4469
- throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
4742
+ const available = Object.keys(resolvedRelations).join(", ") || "(none)";
4743
+ throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
4470
4744
  }
4471
4745
  return this.fetchEntitiesUsingJoins(parentCollection, parentEntityId, relation, options);
4472
4746
  }
@@ -4566,8 +4840,11 @@
4566
4840
  async countRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
4567
4841
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
4568
4842
  const resolvedRelations = resolveCollectionRelations(parentCollection);
4569
- const relation = resolvedRelations[relationKey];
4570
- if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
4843
+ const relation = findRelation(resolvedRelations, relationKey);
4844
+ if (!relation) {
4845
+ const available = Object.keys(resolvedRelations).join(", ") || "(none)";
4846
+ throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
4847
+ }
4571
4848
  const targetCollection = relation.target();
4572
4849
  const targetTable = getTableForCollection(targetCollection, this.registry);
4573
4850
  const targetPks = getPrimaryKeys(targetCollection, this.registry);
@@ -4632,16 +4909,59 @@
4632
4909
  const results2 = await query2;
4633
4910
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
4634
4911
  const resultMap2 = /* @__PURE__ */ new Map();
4635
- results2.forEach((row) => {
4912
+ for (const row of results2) {
4636
4913
  const parentEntity = row[getTableName(parentCollection)] || row;
4637
4914
  const targetEntity = row[targetTableName] || row;
4638
4915
  const parentId = parentEntity[parentIdInfo.fieldName];
4639
- resultMap2.set(parentId, {
4916
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4917
+ resultMap2.set(String(parentId), {
4640
4918
  id: String(targetEntity[targetIdInfo.fieldName]),
4641
4919
  path: targetCollection.slug,
4642
- values: targetEntity
4920
+ values: parsedValues
4643
4921
  });
4644
- });
4922
+ }
4923
+ return resultMap2;
4924
+ }
4925
+ if (relation.direction === "owning" && relation.localKey) {
4926
+ const localKeyCol = parentTable[relation.localKey];
4927
+ if (!localKeyCol) {
4928
+ throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
4929
+ }
4930
+ const fkRows = await this.db.select({
4931
+ parentId: parentIdCol,
4932
+ fkValue: localKeyCol
4933
+ }).from(parentTable).where(drizzleOrm.inArray(parentIdCol, parsedParentIds));
4934
+ const parentToFk = /* @__PURE__ */ new Map();
4935
+ const uniqueFkValues = [];
4936
+ const seenFks = /* @__PURE__ */ new Set();
4937
+ for (const row of fkRows) {
4938
+ if (row.fkValue == null) continue;
4939
+ parentToFk.set(String(row.parentId), row.fkValue);
4940
+ const fkStr = String(row.fkValue);
4941
+ if (!seenFks.has(fkStr)) {
4942
+ seenFks.add(fkStr);
4943
+ uniqueFkValues.push(row.fkValue);
4944
+ }
4945
+ }
4946
+ if (uniqueFkValues.length === 0) return /* @__PURE__ */ new Map();
4947
+ const targetResults = await this.db.select().from(targetTable).where(drizzleOrm.inArray(targetIdField, uniqueFkValues));
4948
+ const targetById = /* @__PURE__ */ new Map();
4949
+ for (const row of targetResults) {
4950
+ const tid = String(row[targetIdInfo.fieldName]);
4951
+ targetById.set(tid, row);
4952
+ }
4953
+ const resultMap2 = /* @__PURE__ */ new Map();
4954
+ for (const [parentIdStr, fkValue] of parentToFk) {
4955
+ const targetEntity = targetById.get(String(fkValue));
4956
+ if (targetEntity) {
4957
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4958
+ resultMap2.set(parentIdStr, {
4959
+ id: String(targetEntity[targetIdInfo.fieldName]),
4960
+ path: targetCollection.slug,
4961
+ values: parsedValues
4962
+ });
4963
+ }
4964
+ }
4645
4965
  return resultMap2;
4646
4966
  }
4647
4967
  let query = this.db.select().from(targetTable).$dynamic();
@@ -4659,7 +4979,8 @@
4659
4979
  );
4660
4980
  const results = await query;
4661
4981
  const resultMap = /* @__PURE__ */ new Map();
4662
- results.forEach((row) => {
4982
+ const parentIdSet = new Set(parsedParentIds.map(String));
4983
+ for (const row of results) {
4663
4984
  const targetEntity = row[getTableName(targetCollection)] || row;
4664
4985
  let parentId;
4665
4986
  if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
@@ -4667,22 +4988,133 @@
4667
4988
  } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
4668
4989
  const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
4669
4990
  parentId = targetEntity[inferredForeignKeyName];
4670
- } else if (relation.direction === "owning" && relation.localKey) {
4671
- for (const parsedParentId of parsedParentIds) {
4672
- if (!resultMap.has(parsedParentId)) {
4673
- parentId = parsedParentId;
4674
- break;
4675
- }
4676
- }
4677
4991
  }
4678
- if (parentId !== void 0 && parsedParentIds.includes(parentId)) {
4679
- resultMap.set(parentId, {
4992
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
4993
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4994
+ resultMap.set(String(parentId), {
4680
4995
  id: String(targetEntity[targetIdInfo.fieldName]),
4681
4996
  path: targetCollection.slug,
4682
- values: targetEntity
4997
+ values: parsedValues
4683
4998
  });
4684
4999
  }
4685
- });
5000
+ }
5001
+ return resultMap;
5002
+ }
5003
+ /**
5004
+ * Batch fetch many-cardinality related entities for multiple parent entities.
5005
+ * Returns a Map<parentId, Entity[]> instead of Map<parentId, Entity>.
5006
+ * Uses a single SQL query with IN clause to avoid N+1.
5007
+ */
5008
+ async batchFetchRelatedEntitiesMany(parentCollectionPath, parentEntityIds, _relationKey, relation) {
5009
+ if (parentEntityIds.length === 0) return /* @__PURE__ */ new Map();
5010
+ const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
5011
+ const targetCollection = relation.target();
5012
+ const targetTable = getTableForCollection(targetCollection, this.registry);
5013
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
5014
+ const targetIdInfo = targetPks[0];
5015
+ const targetIdField = targetTable[targetIdInfo.fieldName];
5016
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5017
+ const parentIdInfo = parentPks[0];
5018
+ const parentTable = this.registry.getTable(getTableName(parentCollection));
5019
+ if (!parentTable) throw new Error("Parent table not found");
5020
+ const parentIdCol = parentTable[parentIdInfo.fieldName];
5021
+ const parsedParentIds = parentEntityIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
5022
+ if (relation.joinPath && relation.joinPath.length > 0) {
5023
+ let query2 = this.db.select().from(parentTable).$dynamic();
5024
+ let currentTable = parentTable;
5025
+ for (const join of relation.joinPath) {
5026
+ const joinTable = this.registry.getTable(join.table);
5027
+ if (!joinTable) throw new Error(`Join table not found: ${join.table}`);
5028
+ const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
5029
+ const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
5030
+ const fromColName = fromColumn.split(".").pop();
5031
+ const toColName = toColumn.split(".").pop();
5032
+ const fromCol = currentTable[fromColName];
5033
+ const toCol = joinTable[toColName];
5034
+ if (!fromCol || !toCol) throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
5035
+ query2 = query2.innerJoin(joinTable, drizzleOrm.eq(fromCol, toCol));
5036
+ currentTable = joinTable;
5037
+ }
5038
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5039
+ query2 = query2.where(drizzleOrm.inArray(parentIdField, parsedParentIds));
5040
+ const results2 = await query2;
5041
+ const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
5042
+ const resultMap2 = /* @__PURE__ */ new Map();
5043
+ for (const row of results2) {
5044
+ const parentEntity = row[getTableName(parentCollection)] || row;
5045
+ const targetEntity = row[targetTableName] || row;
5046
+ const parentId = String(parentEntity[parentIdInfo.fieldName]);
5047
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
5048
+ const arr = resultMap2.get(parentId) || [];
5049
+ arr.push({
5050
+ id: String(targetEntity[targetIdInfo.fieldName]),
5051
+ path: targetCollection.slug,
5052
+ values: parsedValues
5053
+ });
5054
+ resultMap2.set(parentId, arr);
5055
+ }
5056
+ return resultMap2;
5057
+ }
5058
+ if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
5059
+ const junctionTable = this.registry.getTable(relation.through.table);
5060
+ if (!junctionTable) {
5061
+ console.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
5062
+ return /* @__PURE__ */ new Map();
5063
+ }
5064
+ const sourceJunctionCol = junctionTable[relation.through.sourceColumn];
5065
+ const targetJunctionCol = junctionTable[relation.through.targetColumn];
5066
+ if (!sourceJunctionCol || !targetJunctionCol) {
5067
+ console.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
5068
+ return /* @__PURE__ */ new Map();
5069
+ }
5070
+ const query2 = this.db.select().from(junctionTable).innerJoin(targetTable, drizzleOrm.eq(targetJunctionCol, targetIdField)).where(drizzleOrm.inArray(sourceJunctionCol, parsedParentIds));
5071
+ const results2 = await query2;
5072
+ const resultMap2 = /* @__PURE__ */ new Map();
5073
+ const targetTableName = getTableName(targetCollection);
5074
+ for (const row of results2) {
5075
+ const junctionData = row[relation.through.table] || row;
5076
+ const targetData = row[targetTableName] || row;
5077
+ const parentId = String(junctionData[relation.through.sourceColumn]);
5078
+ const parsedValues = await parseDataFromServer(targetData, targetCollection);
5079
+ const arr = resultMap2.get(parentId) || [];
5080
+ arr.push({
5081
+ id: String(targetData[targetIdInfo.fieldName]),
5082
+ path: targetCollection.slug,
5083
+ values: parsedValues
5084
+ });
5085
+ resultMap2.set(parentId, arr);
5086
+ }
5087
+ return resultMap2;
5088
+ }
5089
+ let query = this.db.select().from(targetTable).$dynamic();
5090
+ query = DrizzleConditionBuilder.buildRelationQuery(query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
5091
+ const results = await query;
5092
+ const resultMap = /* @__PURE__ */ new Map();
5093
+ const parentIdSet = new Set(parsedParentIds.map(String));
5094
+ for (const row of results) {
5095
+ const targetEntity = row[getTableName(targetCollection)] || row;
5096
+ let parentId;
5097
+ if (relation.through && relation.direction === "inverse") {
5098
+ const junctionData = row[relation.through.table] || row;
5099
+ parentId = junctionData[relation.through.targetColumn];
5100
+ } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
5101
+ parentId = targetEntity[relation.foreignKeyOnTarget];
5102
+ } else if (relation.direction === "inverse" && relation.inverseRelationName) {
5103
+ const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
5104
+ parentId = targetEntity[inferredForeignKeyName];
5105
+ }
5106
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
5107
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
5108
+ const key = String(parentId);
5109
+ const arr = resultMap.get(key) || [];
5110
+ arr.push({
5111
+ id: String(targetEntity[targetIdInfo.fieldName]),
5112
+ path: targetCollection.slug,
5113
+ values: parsedValues
5114
+ });
5115
+ resultMap.set(key, arr);
5116
+ }
5117
+ }
4686
5118
  return resultMap;
4687
5119
  }
4688
5120
  /**
@@ -4691,7 +5123,7 @@
4691
5123
  async updateRelationsUsingJoins(tx, collection, entityId, relationValues) {
4692
5124
  const resolvedRelations = resolveCollectionRelations(collection);
4693
5125
  for (const [key, value] of Object.entries(relationValues)) {
4694
- const relation = resolvedRelations[key];
5126
+ const relation = findRelation(resolvedRelations, key);
4695
5127
  if (!relation || relation.cardinality !== "many") continue;
4696
5128
  const targetEntityIds = value && Array.isArray(value) ? value.map((rel) => rel.id) : [];
4697
5129
  const targetCollection = relation.target();
@@ -4775,6 +5207,8 @@
4775
5207
  await tx.insert(junctionTable).values(newLinks);
4776
5208
  }
4777
5209
  }
5210
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
5211
+ console.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
4778
5212
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
4779
5213
  const targetTable = getTableForCollection(targetCollection, this.registry);
4780
5214
  const targetPks = getPrimaryKeys(targetCollection, this.registry);
@@ -5152,6 +5586,20 @@
5152
5586
  // =============================================================
5153
5587
  // DRIZZLE QUERY HELPERS
5154
5588
  // =============================================================
5589
+ /**
5590
+ * Resolves the correct Drizzle column for sorting.
5591
+ * Automatically maps owning relation property keys to their underlying foreign key column.
5592
+ */
5593
+ resolveOrderByField(table, orderBy, collection) {
5594
+ let orderByField = table[orderBy];
5595
+ if (!orderByField && collection) {
5596
+ const property = collection.properties[orderBy];
5597
+ if (property && property.type === "relation" && "relation" in property && property.relation?.direction === "owning") {
5598
+ orderByField = table[`${orderBy}_id`];
5599
+ }
5600
+ }
5601
+ return orderByField;
5602
+ }
5155
5603
  /**
5156
5604
  * Build the `with` config for Drizzle's relational query API.
5157
5605
  * Converts collection relations to a Drizzle-compatible `with` object.
@@ -5164,12 +5612,10 @@
5164
5612
  */
5165
5613
  buildWithConfig(collection, include) {
5166
5614
  const resolvedRelations = resolveCollectionRelations(collection);
5167
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5168
5615
  const withConfig = {};
5169
5616
  const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
5170
5617
  for (const [key, relation] of Object.entries(resolvedRelations)) {
5171
5618
  if (!shouldInclude(key)) continue;
5172
- if (!include && !propertyKeys.has(key)) continue;
5173
5619
  const drizzleRelName = relation.relationName || key;
5174
5620
  if (relation.joinPath && relation.joinPath.length > 0) {
5175
5621
  continue;
@@ -5219,10 +5665,8 @@
5219
5665
  */
5220
5666
  drizzleResultToEntity(row, collection, collectionPath, idInfo, databaseId, idInfoArray) {
5221
5667
  const resolvedRelations = resolveCollectionRelations(collection);
5222
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5223
5668
  const normalizedValues = normalizeDbValues(row, collection);
5224
5669
  for (const [key, relation] of Object.entries(resolvedRelations)) {
5225
- if (!propertyKeys.has(key)) continue;
5226
5670
  const drizzleRelName = relation.relationName || key;
5227
5671
  const relData = row[drizzleRelName];
5228
5672
  if (relData === void 0 || relData === null) continue;
@@ -5241,17 +5685,12 @@
5241
5685
  }
5242
5686
  const relId = String(targetEntity[targetIdField] ?? targetEntity.id ?? targetEntity[Object.keys(targetEntity)[0]]);
5243
5687
  const targetValues = normalizeDbValues(targetEntity, targetCollection);
5244
- return {
5688
+ return createRelationRefWithData(relId, targetPath, {
5245
5689
  id: relId,
5246
5690
  path: targetPath,
5247
- __type: "relation",
5248
- data: {
5249
- id: relId,
5250
- path: targetPath,
5251
- values: targetValues,
5252
- databaseId
5253
- }
5254
- };
5691
+ values: targetValues,
5692
+ databaseId
5693
+ });
5255
5694
  });
5256
5695
  } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
5257
5696
  const targetCollection = relation.target();
@@ -5261,17 +5700,12 @@
5261
5700
  const relObj = relData;
5262
5701
  const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
5263
5702
  const targetValues = normalizeDbValues(relObj, targetCollection);
5264
- normalizedValues[key] = {
5703
+ normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
5265
5704
  id: relId,
5266
5705
  path: targetPath,
5267
- __type: "relation",
5268
- data: {
5269
- id: relId,
5270
- path: targetPath,
5271
- values: targetValues,
5272
- databaseId
5273
- }
5274
- };
5706
+ values: targetValues,
5707
+ databaseId
5708
+ });
5275
5709
  }
5276
5710
  }
5277
5711
  return {
@@ -5288,27 +5722,16 @@
5288
5722
  */
5289
5723
  async resolveJoinPathRelations(entity, collection, collectionPath, parsedId, databaseId) {
5290
5724
  const resolvedRelations = resolveCollectionRelations(collection);
5291
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5292
- const promises = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
5725
+ const promises = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
5293
5726
  try {
5294
5727
  const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {
5295
5728
  limit: relation.cardinality === "one" ? 1 : void 0
5296
5729
  });
5297
5730
  if (relation.cardinality === "one" && relatedEntities.length > 0) {
5298
5731
  const e = relatedEntities[0];
5299
- entity.values[key] = {
5300
- id: e.id,
5301
- path: e.path,
5302
- __type: "relation",
5303
- data: e
5304
- };
5732
+ entity.values[key] = createRelationRefWithData(e.id, e.path, e);
5305
5733
  } else if (relation.cardinality === "many") {
5306
- entity.values[key] = relatedEntities.map((e) => ({
5307
- id: e.id,
5308
- path: e.path,
5309
- __type: "relation",
5310
- data: e
5311
- }));
5734
+ entity.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
5312
5735
  }
5313
5736
  } catch (e) {
5314
5737
  console.warn(`Could not resolve joinPath relation '${key}':`, e);
@@ -5323,8 +5746,7 @@
5323
5746
  async resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, databaseId) {
5324
5747
  if (entities.length === 0) return;
5325
5748
  const resolvedRelations = resolveCollectionRelations(collection);
5326
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5327
- const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0);
5749
+ const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
5328
5750
  if (joinPathRelations.length === 0) return;
5329
5751
  for (const [key, relation] of joinPathRelations) {
5330
5752
  try {
@@ -5336,15 +5758,10 @@
5336
5758
  for (const entity of entities) {
5337
5759
  const parsed = parseIdValues(entity.id, [idInfo]);
5338
5760
  const entityId = parsed[idInfo.fieldName];
5339
- const relatedEntity = resultMap.get(entityId);
5761
+ const relatedEntity = resultMap.get(String(entityId));
5340
5762
  if (relatedEntity) {
5341
5763
  if (relation.cardinality === "one") {
5342
- entity.values[key] = {
5343
- id: relatedEntity.id,
5344
- path: relatedEntity.path,
5345
- __type: "relation",
5346
- data: relatedEntity
5347
- };
5764
+ entity.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
5348
5765
  }
5349
5766
  }
5350
5767
  }
@@ -5354,22 +5771,72 @@
5354
5771
  }
5355
5772
  }
5356
5773
  /**
5357
- * Convert a db.query result row to a flat REST-style object with populated relations.
5774
+ * Resolves joinPath relations for raw REST rows and directly injects them.
5775
+ * Uses RelationService to query the database and maps results back to the flattened objects.
5358
5776
  */
5359
- drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
5360
- const flat = {
5361
- id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
5362
- };
5777
+ async resolveJoinPathRelationsBatchRest(rows, collection, collectionPath, idInfoArray, include) {
5778
+ if (rows.length === 0) return;
5363
5779
  const resolvedRelations = resolveCollectionRelations(collection);
5364
- for (const [k, v] of Object.entries(row)) {
5365
- if (k === idInfo.fieldName) continue;
5366
- const relation = resolvedRelations[k];
5367
- if (Array.isArray(v) && relation) {
5368
- flat[k] = v.map((item) => {
5369
- if (this.isJunctionRelation(relation, collection)) {
5370
- const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
5371
- if (nestedKey) {
5372
- const nested = item[nestedKey];
5780
+ const propertyKeys = new Set(Object.keys(collection.properties || {}));
5781
+ const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
5782
+ const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
5783
+ if (joinPathRelations.length === 0) return;
5784
+ const idInfo = idInfoArray[0];
5785
+ for (const [key, relation] of joinPathRelations) {
5786
+ try {
5787
+ const entityIds = rows.map((r) => {
5788
+ const parsed = parseIdValues(String(r.id), idInfoArray);
5789
+ return parsed[idInfo.fieldName];
5790
+ });
5791
+ if (relation.cardinality === "one") {
5792
+ const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5793
+ for (const row of rows) {
5794
+ const parsed = parseIdValues(String(row.id), idInfoArray);
5795
+ const entityId = parsed[idInfo.fieldName];
5796
+ const relatedEntity = resultMap.get(String(entityId));
5797
+ if (relatedEntity) {
5798
+ row[key] = {
5799
+ id: relatedEntity.id,
5800
+ ...relatedEntity.values
5801
+ };
5802
+ } else {
5803
+ row[key] = null;
5804
+ }
5805
+ }
5806
+ } else if (relation.cardinality === "many") {
5807
+ const resultMap = await this.batchFetchManyRelatedEntities(collectionPath, entityIds, key);
5808
+ for (const row of rows) {
5809
+ const parsed = parseIdValues(String(row.id), idInfoArray);
5810
+ const entityId = parsed[idInfo.fieldName];
5811
+ const relatedList = resultMap.get(String(entityId)) || [];
5812
+ row[key] = relatedList.map((e) => ({
5813
+ id: e.id,
5814
+ ...e.values
5815
+ }));
5816
+ }
5817
+ }
5818
+ } catch (e) {
5819
+ console.warn(`Could not batch resolve joinPath relation '${key}' for REST:`, e);
5820
+ }
5821
+ }
5822
+ }
5823
+ /**
5824
+ * Convert a db.query result row to a flat REST-style object with populated relations.
5825
+ */
5826
+ drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
5827
+ const flat = {
5828
+ id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
5829
+ };
5830
+ const resolvedRelations = resolveCollectionRelations(collection);
5831
+ for (const [k, v] of Object.entries(row)) {
5832
+ if (k === idInfo.fieldName) continue;
5833
+ const relation = findRelation(resolvedRelations, k);
5834
+ if (Array.isArray(v) && relation) {
5835
+ flat[k] = v.map((item) => {
5836
+ if (this.isJunctionRelation(relation, collection)) {
5837
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
5838
+ if (nestedKey) {
5839
+ const nested = item[nestedKey];
5373
5840
  return {
5374
5841
  id: String(nested.id ?? nested[Object.keys(nested)[0]]),
5375
5842
  ...nested
@@ -5415,7 +5882,7 @@
5415
5882
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5416
5883
  }
5417
5884
  if (options.startAfter) {
5418
- const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
5885
+ const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
5419
5886
  if (cursorConditions.length > 0) allConditions.push(...cursorConditions);
5420
5887
  }
5421
5888
  if (allConditions.length > 0) {
@@ -5423,7 +5890,8 @@
5423
5890
  }
5424
5891
  const orderExpressions = [];
5425
5892
  if (options.orderBy) {
5426
- const orderByField = table[options.orderBy];
5893
+ const collection = getCollectionByPath(collectionPath, this.registry);
5894
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5427
5895
  if (orderByField) {
5428
5896
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
5429
5897
  }
@@ -5434,16 +5902,18 @@
5434
5902
  }
5435
5903
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5436
5904
  if (limitValue) queryOpts.limit = limitValue;
5905
+ if (options.offset && options.offset > 0) queryOpts.offset = options.offset;
5437
5906
  return queryOpts;
5438
5907
  }
5439
5908
  /**
5440
5909
  * Extract cursor pagination conditions from startAfter options.
5441
5910
  */
5442
- buildCursorConditions(table, idField, idInfo, options) {
5911
+ buildCursorConditions(table, idField, idInfo, options, collectionPath) {
5443
5912
  if (!options.startAfter) return [];
5444
5913
  const cursor = options.startAfter;
5445
5914
  if (options.orderBy) {
5446
- const orderByField = table[options.orderBy];
5915
+ const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
5916
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5447
5917
  if (orderByField) {
5448
5918
  const startAfterOrderValue = cursor.values?.[options.orderBy] ?? cursor[options.orderBy];
5449
5919
  const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
@@ -5505,11 +5975,7 @@
5505
5975
  const relationPromises = Object.entries(resolvedRelations).filter(([key]) => propertyKeys.has(key)).map(async ([key, relation]) => {
5506
5976
  if (relation.cardinality === "many") {
5507
5977
  const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {});
5508
- values[key] = relatedEntities.map((e) => ({
5509
- id: e.id,
5510
- path: e.path,
5511
- __type: "relation"
5512
- }));
5978
+ values[key] = relatedEntities.map((e) => createRelationRef(e.id, e.path));
5513
5979
  } else if (relation.cardinality === "one") {
5514
5980
  if (values[key] == null) {
5515
5981
  try {
@@ -5518,11 +5984,7 @@
5518
5984
  });
5519
5985
  if (relatedEntities.length > 0) {
5520
5986
  const e = relatedEntities[0];
5521
- values[key] = {
5522
- id: e.id,
5523
- path: e.path,
5524
- __type: "relation"
5525
- };
5987
+ values[key] = createRelationRef(e.id, e.path);
5526
5988
  }
5527
5989
  } catch (e) {
5528
5990
  console.warn(`Could not resolve one-to-one relation property: ${key}`, e);
@@ -5552,13 +6014,13 @@
5552
6014
  }
5553
6015
  const tableName = drizzleOrm.getTableName(table);
5554
6016
  const qb = this.getQueryBuilder(tableName);
5555
- if (qb) {
6017
+ const withConfig = this.buildWithConfig(collection);
6018
+ const hasRelations = withConfig && Object.keys(withConfig).length > 0;
6019
+ if (qb && !options.searchString && !hasRelations) {
5556
6020
  try {
5557
- const withConfig = this.buildWithConfig(collection);
5558
- const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
6021
+ const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5559
6022
  const results2 = await qb.findMany(queryOpts);
5560
6023
  const entities = results2.map((row) => this.drizzleResultToEntity(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray));
5561
- await this.resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, options.databaseId);
5562
6024
  return entities;
5563
6025
  } catch (e) {
5564
6026
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
@@ -5581,7 +6043,7 @@
5581
6043
  }
5582
6044
  const orderExpressions = [];
5583
6045
  if (options.orderBy) {
5584
- const orderByField = table[options.orderBy];
6046
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5585
6047
  if (orderByField) {
5586
6048
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
5587
6049
  }
@@ -5589,7 +6051,7 @@
5589
6051
  orderExpressions.push(drizzleOrm.desc(idField));
5590
6052
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5591
6053
  if (options.startAfter) {
5592
- const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
6054
+ const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
5593
6055
  if (cursorConditions.length > 0) {
5594
6056
  allConditions.push(...cursorConditions);
5595
6057
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
@@ -5598,6 +6060,7 @@
5598
6060
  }
5599
6061
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5600
6062
  if (limitValue) query = query.limit(limitValue);
6063
+ if (options.offset && options.offset > 0) query = query.offset(options.offset);
5601
6064
  const results = await query;
5602
6065
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5603
6066
  }
@@ -5611,7 +6074,7 @@
5611
6074
  async processEntityResults(results, collection, collectionPath, idInfo, databaseId, skipRelations = false, idInfoArray) {
5612
6075
  if (results.length === 0) return [];
5613
6076
  const entitiesWithValues = await Promise.all(results.map(async (entity) => {
5614
- const values = await parseDataFromServer(entity, collection, this.db, this.registry);
6077
+ const values = await parseDataFromServer(entity, collection);
5615
6078
  return {
5616
6079
  entity,
5617
6080
  values,
@@ -5636,37 +6099,29 @@
5636
6099
  const relationResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5637
6100
  entitiesMissingRelation.forEach((item) => {
5638
6101
  const entityId = item.entity[idInfo.fieldName];
5639
- const relatedEntity = relationResults.get(entityId);
6102
+ const relatedEntity = relationResults.get(String(entityId));
5640
6103
  if (relatedEntity) {
5641
- item.values[key] = {
5642
- id: relatedEntity.id,
5643
- path: relatedEntity.path,
5644
- __type: "relation",
5645
- data: relatedEntity
5646
- };
6104
+ item.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
5647
6105
  }
5648
6106
  });
5649
6107
  } catch (e) {
5650
6108
  console.warn(`Could not batch load one-to-one relation property: ${key}`, e);
5651
6109
  }
5652
6110
  }
5653
- const manyRelationPromises = entitiesWithValues.map(async (item) => {
5654
- const manyRelationQueries = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many").map(async ([key]) => {
5655
- try {
5656
- const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, item.entity[idInfo.fieldName], key, {});
5657
- item.values[key] = relatedEntities.map((e) => ({
5658
- id: e.id,
5659
- path: e.path,
5660
- __type: "relation",
5661
- data: e
5662
- }));
5663
- } catch (e) {
5664
- console.warn(`Could not resolve many relation property: ${key}`, e);
5665
- }
5666
- });
5667
- await Promise.all(manyRelationQueries);
5668
- });
5669
- await Promise.all(manyRelationPromises);
6111
+ const manyRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many");
6112
+ for (const [key, relation] of manyRelations) {
6113
+ try {
6114
+ const entityIds = entitiesWithValues.map((item) => item.entity[idInfo.fieldName]);
6115
+ const relationResults = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, entityIds, key, relation);
6116
+ entitiesWithValues.forEach((item) => {
6117
+ const entityId = String(item.entity[idInfo.fieldName]);
6118
+ const relatedEntities = relationResults.get(entityId) || [];
6119
+ item.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
6120
+ });
6121
+ } catch (e) {
6122
+ console.warn(`Could not batch load many relation property: ${key}`, e);
6123
+ }
6124
+ }
5670
6125
  }
5671
6126
  return entitiesWithValues.map((item) => ({
5672
6127
  id: item.id,
@@ -5707,12 +6162,16 @@
5707
6162
  for (let i = 2; i < pathSegments.length; i += 2) {
5708
6163
  const relationKey = pathSegments[i];
5709
6164
  const resolvedRelations = resolveCollectionRelations(currentCollection);
5710
- const relation = resolvedRelations[relationKey];
6165
+ const relation = findRelation(resolvedRelations, relationKey);
5711
6166
  if (!relation) {
5712
6167
  throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
5713
6168
  }
5714
6169
  if (i === pathSegments.length - 1) {
5715
- return this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
6170
+ const entities = await this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
6171
+ for (const entity of entities) {
6172
+ entity.path = path2;
6173
+ }
6174
+ return entities;
5716
6175
  }
5717
6176
  if (i + 1 < pathSegments.length) {
5718
6177
  const nextEntityId = pathSegments[i + 1];
@@ -5734,11 +6193,19 @@
5734
6193
  let query = this.db.select({
5735
6194
  count: drizzleOrm.count()
5736
6195
  }).from(table).$dynamic();
6196
+ const allConditions = [];
6197
+ if (options.searchString) {
6198
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
6199
+ if (searchConditions.length === 0) return 0;
6200
+ allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
6201
+ }
5737
6202
  if (options.filter) {
5738
6203
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5739
- if (filterConditions.length > 0) {
5740
- query = query.where(drizzleOrm.and(...filterConditions));
5741
- }
6204
+ if (filterConditions.length > 0) allConditions.push(...filterConditions);
6205
+ }
6206
+ if (allConditions.length > 0) {
6207
+ const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
6208
+ if (finalCondition) query = query.where(finalCondition);
5742
6209
  }
5743
6210
  const result = await query;
5744
6211
  return Number(result[0]?.count || 0);
@@ -5757,7 +6224,7 @@
5757
6224
  for (let i = 2; i < pathSegments.length; i += 2) {
5758
6225
  const relationKey = pathSegments[i];
5759
6226
  const resolvedRelations = resolveCollectionRelations(currentCollection);
5760
- const relation = resolvedRelations[relationKey];
6227
+ const relation = findRelation(resolvedRelations, relationKey);
5761
6228
  if (!relation) {
5762
6229
  throw new Error(`Relation '${relationKey}' not found`);
5763
6230
  }
@@ -5816,12 +6283,14 @@
5816
6283
  const idField = table[idInfo.fieldName];
5817
6284
  const tableName = drizzleOrm.getTableName(table);
5818
6285
  const qb = this.getQueryBuilder(tableName);
5819
- if (qb) {
6286
+ if (qb && !options.searchString) {
5820
6287
  try {
5821
6288
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5822
6289
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
5823
6290
  const results = await qb.findMany(queryOpts);
5824
- return results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
6291
+ const restRows = results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
6292
+ await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
6293
+ return restRows;
5825
6294
  } catch (e) {
5826
6295
  console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
5827
6296
  }
@@ -5843,7 +6312,7 @@
5843
6312
  const batchResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5844
6313
  for (const entity of entities) {
5845
6314
  const eid = entity[idInfo.fieldName];
5846
- const related = batchResults.get(eid);
6315
+ const related = batchResults.get(String(eid));
5847
6316
  if (related) {
5848
6317
  entity[key] = {
5849
6318
  id: related.id,
@@ -5899,7 +6368,9 @@
5899
6368
  } : {}
5900
6369
  });
5901
6370
  if (!row) return null;
5902
- return this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
6371
+ const restRow = this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
6372
+ await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
6373
+ return restRow;
5903
6374
  } catch (e) {
5904
6375
  console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
5905
6376
  }
@@ -5967,7 +6438,7 @@
5967
6438
  }
5968
6439
  const orderExpressions = [];
5969
6440
  if (options.orderBy) {
5970
- const orderByField = table[options.orderBy];
6441
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5971
6442
  if (orderByField) {
5972
6443
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
5973
6444
  }
@@ -5976,6 +6447,7 @@
5976
6447
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5977
6448
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5978
6449
  if (limitValue) query = query.limit(limitValue);
6450
+ if (options.offset && options.offset > 0) query = query.offset(options.offset);
5979
6451
  return await query;
5980
6452
  }
5981
6453
  /**
@@ -6024,7 +6496,7 @@
6024
6496
  }
6025
6497
  }
6026
6498
  if (options.orderBy) {
6027
- const orderByField = table[options.orderBy];
6499
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
6028
6500
  if (orderByField) {
6029
6501
  queryOpts.orderBy = options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField);
6030
6502
  }
@@ -6078,17 +6550,15 @@
6078
6550
  * Groups results by parent ID to avoid N+1.
6079
6551
  */
6080
6552
  async batchFetchManyRelatedEntities(parentCollectionPath, parentIds, relationKey) {
6081
- const resultMap = /* @__PURE__ */ new Map();
6082
- const batchPromises = parentIds.map(async (parentId) => {
6083
- try {
6084
- const related = await this.relationService.fetchRelatedEntities(parentCollectionPath, parentId, relationKey, {});
6085
- resultMap.set(String(parentId), related);
6086
- } catch (e) {
6087
- resultMap.set(String(parentId), []);
6088
- }
6089
- });
6090
- await Promise.all(batchPromises);
6091
- return resultMap;
6553
+ if (parentIds.length === 0) return /* @__PURE__ */ new Map();
6554
+ const collection = getCollectionByPath(parentCollectionPath, this.registry);
6555
+ const resolvedRelations = resolveCollectionRelations(collection);
6556
+ const relation = resolvedRelations[relationKey];
6557
+ if (!relation) {
6558
+ console.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
6559
+ return /* @__PURE__ */ new Map();
6560
+ }
6561
+ return this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
6092
6562
  }
6093
6563
  }
6094
6564
  class EntityPersistService {
@@ -6124,6 +6594,7 @@
6124
6594
  const effectiveValues = {
6125
6595
  ...values
6126
6596
  };
6597
+ let junctionTableInfo;
6127
6598
  if (collectionPath.includes("/")) {
6128
6599
  const segments = collectionPath.split("/").filter(Boolean);
6129
6600
  if (segments.length >= 3 && segments.length % 2 === 1) {
@@ -6133,9 +6604,10 @@
6133
6604
  for (let i = 2; i < segments.length; i += 2) {
6134
6605
  const relationKey = segments[i];
6135
6606
  const resolvedRelations2 = resolveCollectionRelations(currentCollection);
6136
- const relation = resolvedRelations2[relationKey];
6607
+ const relation = findRelation(resolvedRelations2, relationKey);
6137
6608
  if (!relation) {
6138
- throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
6609
+ const available = Object.keys(resolvedRelations2).join(", ") || "(none)";
6610
+ throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
6139
6611
  }
6140
6612
  if (i === segments.length - 1) {
6141
6613
  const targetCollection = relation.target();
@@ -6145,7 +6617,7 @@
6145
6617
  const parentIdInfo2 = parentIdInfoArray2[0];
6146
6618
  const parsedParentIdObj2 = parseIdValues(currentEntityId, parentIdInfoArray2);
6147
6619
  const parsedParentId2 = parsedParentIdObj2[parentIdInfo2.fieldName];
6148
- effectiveValues.__junction_table_info = {
6620
+ junctionTableInfo = {
6149
6621
  parentCollection: currentCollection,
6150
6622
  parentId: parsedParentId2,
6151
6623
  relation,
@@ -6214,14 +6686,10 @@
6214
6686
  }
6215
6687
  }
6216
6688
  }
6217
- const processedData = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
6218
- const inverseRelationUpdates = processedData.__inverseRelationUpdates || [];
6219
- const joinPathRelationUpdates = processedData.__joinPathRelationUpdates || [];
6220
- const junctionTableInfo = processedData.__junction_table_info;
6221
- delete processedData.__inverseRelationUpdates;
6222
- delete processedData.__joinPathRelationUpdates;
6223
- delete processedData.__junction_table_info;
6224
- const entityData = sanitizeAndConvertDates(processedData);
6689
+ const serializedResult = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
6690
+ const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
6691
+ const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
6692
+ const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
6225
6693
  let savedId;
6226
6694
  try {
6227
6695
  savedId = await this.db.transaction(async (tx) => {
@@ -6234,7 +6702,7 @@
6234
6702
  }
6235
6703
  const scalarKeys = Object.keys(entityData);
6236
6704
  if (scalarKeys.length > 0) {
6237
- let updateQuery = tx.update(table).set(entityData);
6705
+ const updateQuery = tx.update(table).set(entityData);
6238
6706
  const conditions = [];
6239
6707
  for (const info of idInfoArray) {
6240
6708
  const field = table[info.fieldName];
@@ -6598,21 +7066,6 @@
6598
7066
  if (poolManager) {
6599
7067
  this.branchService = new BranchService(db, poolManager);
6600
7068
  }
6601
- this.admin = {
6602
- executeSql: this.executeSql.bind(this),
6603
- fetchAvailableDatabases: this.fetchAvailableDatabases.bind(this),
6604
- fetchAvailableRoles: this.fetchAvailableRoles.bind(this),
6605
- fetchCurrentDatabase: this.fetchCurrentDatabase.bind(this),
6606
- fetchUnmappedTables: this.fetchUnmappedTables.bind(this),
6607
- fetchTableMetadata: this.fetchTableMetadata.bind(this),
6608
- // Branch operations (only available when poolManager is configured)
6609
- ...this.branchService ? {
6610
- createBranch: this.branchService.createBranch.bind(this.branchService),
6611
- deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
6612
- listBranches: this.branchService.listBranches.bind(this.branchService),
6613
- getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
6614
- } : {}
6615
- };
6616
7069
  }
6617
7070
  key = "postgres";
6618
7071
  initialised = true;
@@ -6630,8 +7083,26 @@
6630
7083
  _pendingNotifications = [];
6631
7084
  /**
6632
7085
  * Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin).
7086
+ * Implemented as a getter so method references are resolved at call-time,
7087
+ * allowing test spies applied after construction to take effect.
6633
7088
  */
6634
- admin;
7089
+ get admin() {
7090
+ return {
7091
+ executeSql: (...args) => this.executeSql(...args),
7092
+ fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
7093
+ fetchAvailableRoles: () => this.fetchAvailableRoles(),
7094
+ fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
7095
+ fetchUnmappedTables: (...args) => this.fetchUnmappedTables(...args),
7096
+ fetchTableMetadata: (...args) => this.fetchTableMetadata(...args),
7097
+ // Branch operations (only available when poolManager is configured)
7098
+ ...this.branchService ? {
7099
+ createBranch: this.branchService.createBranch.bind(this.branchService),
7100
+ deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
7101
+ listBranches: this.branchService.listBranches.bind(this.branchService),
7102
+ getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
7103
+ } : {}
7104
+ };
7105
+ }
6635
7106
  resolveCollectionCallbacks(collection, path2) {
6636
7107
  if (!collection && !path2) return {
6637
7108
  collection: void 0,
@@ -6660,6 +7131,7 @@
6660
7131
  collection,
6661
7132
  filter,
6662
7133
  limit,
7134
+ offset,
6663
7135
  startAfter,
6664
7136
  orderBy,
6665
7137
  searchString,
@@ -6670,6 +7142,7 @@
6670
7142
  orderBy,
6671
7143
  order,
6672
7144
  limit,
7145
+ offset,
6673
7146
  startAfter,
6674
7147
  databaseId: collection?.databaseId,
6675
7148
  searchString
@@ -6713,6 +7186,7 @@
6713
7186
  collection,
6714
7187
  filter,
6715
7188
  limit,
7189
+ offset,
6716
7190
  startAfter,
6717
7191
  orderBy,
6718
7192
  searchString,
@@ -6733,6 +7207,7 @@
6733
7207
  orderBy,
6734
7208
  order,
6735
7209
  limit,
7210
+ offset,
6736
7211
  startAfter,
6737
7212
  databaseId: collection?.databaseId,
6738
7213
  searchString
@@ -6744,6 +7219,7 @@
6744
7219
  collection,
6745
7220
  filter,
6746
7221
  limit,
7222
+ offset,
6747
7223
  startAfter,
6748
7224
  orderBy,
6749
7225
  searchString,
@@ -6904,7 +7380,7 @@
6904
7380
  collection: resolvedCollection,
6905
7381
  path: path2,
6906
7382
  entityId: savedEntity.id,
6907
- values: updatedValues,
7383
+ values: savedEntity.values,
6908
7384
  previousValues: previousValuesForHistory,
6909
7385
  status,
6910
7386
  context: contextForCallback
@@ -6915,7 +7391,7 @@
6915
7391
  collection: resolvedCollection,
6916
7392
  path: path2,
6917
7393
  entityId: savedEntity.id,
6918
- values: updatedValues,
7394
+ values: savedEntity.values,
6919
7395
  previousValues: previousValuesForHistory,
6920
7396
  status,
6921
7397
  context: contextForCallback
@@ -7052,10 +7528,12 @@
7052
7528
  async countEntities({
7053
7529
  path: path2,
7054
7530
  collection,
7055
- filter
7531
+ filter,
7532
+ searchString
7056
7533
  }) {
7057
7534
  return this.entityService.countEntities(path2, {
7058
- filter
7535
+ filter,
7536
+ searchString
7059
7537
  });
7060
7538
  }
7061
7539
  getTargetDb(databaseName) {
@@ -7111,7 +7589,7 @@
7111
7589
  return databases;
7112
7590
  }
7113
7591
  async fetchAvailableRoles() {
7114
- const result = await this.executeSql(`SELECT rolname FROM pg_roles;`);
7592
+ const result = await this.executeSql("SELECT rolname FROM pg_roles;");
7115
7593
  return result.map((r) => r.rolname);
7116
7594
  }
7117
7595
  async fetchCurrentDatabase() {
@@ -7285,12 +7763,12 @@
7285
7763
  const result = await this.delegate.db.transaction(async (tx) => {
7286
7764
  let userId = this.user?.uid;
7287
7765
  if (!userId) {
7288
- console.warn(`[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:`, this.user);
7766
+ console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
7289
7767
  userId = "anonymous";
7290
7768
  }
7291
- let userRoles2 = this.user?.roles ?? [];
7769
+ const userRoles2 = this.user?.roles ?? [];
7292
7770
  if (!this.user?.roles) {
7293
- console.warn(`[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:`, this.user);
7771
+ console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
7294
7772
  }
7295
7773
  const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7296
7774
  const rolesString = normalizedRoles.join(",");
@@ -7360,29 +7838,6 @@
7360
7838
  async countEntities(props) {
7361
7839
  return this.withTransaction((delegate) => delegate.countEntities(props));
7362
7840
  }
7363
- /**
7364
- * Intentionally delegates to the base delegate WITHOUT RLS wrapping.
7365
- * executeSql is an admin-only feature; access control should be enforced
7366
- * at the API route level, not via database-level RLS.
7367
- */
7368
- async executeSql(sqlText, options) {
7369
- return this.delegate.executeSql(sqlText, options);
7370
- }
7371
- async fetchAvailableDatabases() {
7372
- return this.delegate.fetchAvailableDatabases();
7373
- }
7374
- async fetchAvailableRoles() {
7375
- return this.delegate.fetchAvailableRoles();
7376
- }
7377
- async fetchCurrentDatabase() {
7378
- return this.delegate.fetchCurrentDatabase();
7379
- }
7380
- async fetchUnmappedTables(mappedPaths) {
7381
- return this.delegate.fetchUnmappedTables(mappedPaths);
7382
- }
7383
- async fetchTableMetadata(tableName) {
7384
- return this.delegate.fetchTableMetadata(tableName);
7385
- }
7386
7841
  }
7387
7842
  class DatabasePoolManager {
7388
7843
  pools = /* @__PURE__ */ new Map();
@@ -7418,7 +7873,10 @@
7418
7873
  connectionString: url2.toString(),
7419
7874
  max: 10,
7420
7875
  // Default sensible limit, can be tuned later
7421
- idleTimeoutMillis: 3e4
7876
+ idleTimeoutMillis: 1e4,
7877
+ // Reduced from 30000 for aggressive cleanup
7878
+ allowExitOnIdle: true
7879
+ // Prevent idle clients from hanging the Node.js process
7422
7880
  });
7423
7881
  pool.on("error", (err) => {
7424
7882
  console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
@@ -7469,13 +7927,6 @@
7469
7927
  photoUrl: pgCore.varchar("photo_url", {
7470
7928
  length: 500
7471
7929
  }),
7472
- provider: pgCore.varchar("provider", {
7473
- length: 50
7474
- }).notNull().default("email"),
7475
- // 'email' | 'google'
7476
- googleId: pgCore.varchar("google_id", {
7477
- length: 255
7478
- }).unique(),
7479
7930
  emailVerified: pgCore.boolean("email_verified").default(false).notNull(),
7480
7931
  emailVerificationToken: pgCore.varchar("email_verification_token", {
7481
7932
  length: 255
@@ -7549,12 +8000,31 @@
7549
8000
  value: pgCore.jsonb("value").notNull(),
7550
8001
  updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7551
8002
  });
8003
+ const userIdentities = rebaseSchema.table("user_identities", {
8004
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
8005
+ userId: pgCore.uuid("user_id").notNull().references(() => users.id, {
8006
+ onDelete: "cascade"
8007
+ }),
8008
+ provider: pgCore.varchar("provider", {
8009
+ length: 50
8010
+ }).notNull(),
8011
+ // e.g. 'google', 'linkedin'
8012
+ providerId: pgCore.varchar("provider_id", {
8013
+ length: 255
8014
+ }).notNull(),
8015
+ profileData: pgCore.jsonb("profile_data"),
8016
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
8017
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
8018
+ }, (table) => ({
8019
+ uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
8020
+ }));
7552
8021
  const usersRelations = drizzleOrm.relations(users, ({
7553
8022
  many
7554
8023
  }) => ({
7555
8024
  userRoles: many(userRoles),
7556
8025
  refreshTokens: many(refreshTokens),
7557
- passwordResetTokens: many(passwordResetTokens)
8026
+ passwordResetTokens: many(passwordResetTokens),
8027
+ userIdentities: many(userIdentities)
7558
8028
  }));
7559
8029
  const rolesRelations = drizzleOrm.relations(roles, ({
7560
8030
  many
@@ -7589,13 +8059,24 @@
7589
8059
  references: [users.id]
7590
8060
  })
7591
8061
  }));
8062
+ const userIdentitiesRelations = drizzleOrm.relations(userIdentities, ({
8063
+ one
8064
+ }) => ({
8065
+ user: one(users, {
8066
+ fields: [userIdentities.userId],
8067
+ references: [users.id]
8068
+ })
8069
+ }));
7592
8070
  const getPrimaryKeyProp = (collection) => {
7593
8071
  if (collection.properties) {
7594
8072
  const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
7595
8073
  if (idPropEntry) {
8074
+ const prop = idPropEntry[1];
8075
+ const isUuid2 = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
7596
8076
  return {
7597
8077
  name: idPropEntry[0],
7598
- type: idPropEntry[1].type === "number" ? "number" : "string"
8078
+ type: prop.type === "number" ? "number" : "string",
8079
+ isUuid: isUuid2
7599
8080
  };
7600
8081
  }
7601
8082
  }
@@ -7603,12 +8084,15 @@
7603
8084
  if (idProp?.type === "number") {
7604
8085
  return {
7605
8086
  name: "id",
7606
- type: "number"
8087
+ type: "number",
8088
+ isUuid: false
7607
8089
  };
7608
8090
  }
8091
+ const isUuid = idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid";
7609
8092
  return {
7610
8093
  name: "id",
7611
- type: "string"
8094
+ type: "string",
8095
+ isUuid: isUuid ?? false
7612
8096
  };
7613
8097
  };
7614
8098
  const isNumericId = (collection) => {
@@ -7622,7 +8106,7 @@
7622
8106
  const hasExplicitId = Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId));
7623
8107
  return !hasExplicitId && propName === "id";
7624
8108
  };
7625
- const getDrizzleColumn = (propName, prop, collection) => {
8109
+ const getDrizzleColumn = (propName, prop, collection, collections) => {
7626
8110
  const colName = toSnakeCase(propName);
7627
8111
  let columnDefinition;
7628
8112
  switch (prop.type) {
@@ -7641,20 +8125,20 @@
7641
8125
  columnDefinition = `varchar("${colName}")`;
7642
8126
  }
7643
8127
  if (isIdProperty(propName, prop, collection)) {
7644
- columnDefinition += `.primaryKey()`;
8128
+ columnDefinition += ".primaryKey()";
7645
8129
  }
7646
8130
  if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
7647
8131
  if (stringProp.isId === "uuid") {
7648
- columnDefinition += `.defaultRandom()`;
8132
+ columnDefinition += ".defaultRandom()";
7649
8133
  } else if (stringProp.isId === "cuid") {
7650
- columnDefinition += `.default(sql\`cuid()\`)`;
8134
+ columnDefinition += ".default(sql`cuid()`)";
7651
8135
  } else if (typeof stringProp.isId === "string") {
7652
8136
  const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`") ? stringProp.isId.substring(4, stringProp.isId.length - 1) : stringProp.isId;
7653
8137
  columnDefinition += `.default(sql\`${sqlContent}\`)`;
7654
8138
  }
7655
8139
  }
7656
8140
  if (stringProp.validation?.unique) {
7657
- columnDefinition += `.unique()`;
8141
+ columnDefinition += ".unique()";
7658
8142
  }
7659
8143
  break;
7660
8144
  }
@@ -7676,10 +8160,10 @@
7676
8160
  columnDefinition = baseType;
7677
8161
  }
7678
8162
  if (isId) {
7679
- columnDefinition += `.primaryKey()`;
8163
+ columnDefinition += ".primaryKey()";
7680
8164
  }
7681
8165
  if (numProp.validation?.unique) {
7682
- columnDefinition += `.unique()`;
8166
+ columnDefinition += ".unique()";
7683
8167
  }
7684
8168
  break;
7685
8169
  }
@@ -7710,7 +8194,7 @@
7710
8194
  case "relation": {
7711
8195
  const refProp = prop;
7712
8196
  const resolvedRelations = resolveCollectionRelations(collection);
7713
- const relation = resolvedRelations[refProp.relationName ?? propName];
8197
+ const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
7714
8198
  if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
7715
8199
  return null;
7716
8200
  }
@@ -7729,8 +8213,9 @@
7729
8213
  }
7730
8214
  const fkColumnName = toSnakeCase(relation.localKey);
7731
8215
  const targetTableVar = getTableVarName(getTableName(targetCollection));
7732
- const targetIdField = getPrimaryKeyName(targetCollection);
7733
- const baseColumn = isNumericId(targetCollection) ? `integer("${fkColumnName}")` : `varchar("${fkColumnName}")`;
8216
+ const pkProp = getPrimaryKeyProp(targetCollection);
8217
+ const targetIdField = pkProp.name;
8218
+ const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `varchar("${fkColumnName}")`;
7734
8219
  const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
7735
8220
  const required = prop.validation?.required;
7736
8221
  const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
@@ -7743,6 +8228,26 @@
7743
8228
  }
7744
8229
  return ` ${relation.localKey}: ${columnDef}`;
7745
8230
  }
8231
+ case "reference": {
8232
+ const refProp = prop;
8233
+ const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
8234
+ if (!targetCollection) {
8235
+ columnDefinition = `varchar("${colName}")`;
8236
+ break;
8237
+ }
8238
+ const pkProp = getPrimaryKeyProp(targetCollection);
8239
+ const targetTableVar = getTableVarName(getTableName(targetCollection));
8240
+ const targetIdField = pkProp.name;
8241
+ const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `varchar("${colName}")`;
8242
+ const required = prop.validation?.required;
8243
+ const onDelete = required ? "cascade" : "set null";
8244
+ const refOptions = `{ onDelete: "${onDelete}" }`;
8245
+ columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${refOptions})`;
8246
+ if (required) {
8247
+ columnDefinition += ".notNull()";
8248
+ }
8249
+ return ` ${propName}: ${columnDefinition}`;
8250
+ }
7746
8251
  default:
7747
8252
  return null;
7748
8253
  }
@@ -7769,7 +8274,7 @@
7769
8274
  return resolveRawSql(rule.using);
7770
8275
  }
7771
8276
  if (rule.access === "public") {
7772
- return `sql\`true\``;
8277
+ return "sql`true`";
7773
8278
  }
7774
8279
  if (rule.ownerField) {
7775
8280
  return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
@@ -7782,16 +8287,31 @@
7782
8287
  }
7783
8288
  return buildUsingClause(rule);
7784
8289
  };
8290
+ const getPolicyNameHash = (rule) => {
8291
+ const data = JSON.stringify({
8292
+ a: rule.access,
8293
+ m: rule.mode,
8294
+ op: rule.operation,
8295
+ ops: rule.operations?.slice().sort(),
8296
+ own: rule.ownerField,
8297
+ rol: rule.roles?.slice().sort(),
8298
+ pg: rule.pgRoles?.slice().sort(),
8299
+ u: rule.using,
8300
+ w: rule.withCheck
8301
+ });
8302
+ return crypto.createHash("sha1").update(data).digest("hex").substring(0, 7);
8303
+ };
7785
8304
  const generatePolicyCode = (tableName, rule, index) => {
7786
8305
  const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
8306
+ const ruleHash = getPolicyNameHash(rule);
7787
8307
  return ops.map((op, opIdx) => {
7788
- const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_policy_${index}${ops.length > 1 ? `_${opIdx}` : ""}`;
8308
+ const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
7789
8309
  return generateSinglePolicyCode(tableName, rule, op, policyName);
7790
8310
  }).join("");
7791
8311
  };
7792
8312
  const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
7793
8313
  const mode = rule.mode ?? "permissive";
7794
- const roles2 = rule.roles;
8314
+ const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
7795
8315
  const needsUsing = operation !== "insert";
7796
8316
  const needsWithCheck = operation !== "select" && operation !== "delete";
7797
8317
  let usingClause = needsUsing ? buildUsingClause(rule) : null;
@@ -7811,22 +8331,57 @@
7811
8331
  }
7812
8332
  }
7813
8333
  if (!usingClause && needsUsing) {
7814
- usingClause = `sql\`false\``;
8334
+ usingClause = "sql`false`";
7815
8335
  }
7816
8336
  if (!withCheckClause && needsWithCheck) {
7817
- withCheckClause = `sql\`false\``;
8337
+ withCheckClause = "sql`false`";
7818
8338
  }
7819
8339
  const parts = [];
7820
8340
  parts.push(`as: "${mode}"`);
7821
8341
  parts.push(`for: "${operation}"`);
7822
- const toRoles = rule.pgRoles ?? ["public"];
8342
+ const toRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
7823
8343
  parts.push(`to: [${toRoles.map((r) => `"${r}"`).join(", ")}]`);
7824
8344
  if (usingClause) parts.push(`using: ${usingClause}`);
7825
8345
  if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
7826
8346
  return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),
7827
8347
  `;
7828
8348
  };
7829
- const generateSchema = async (collections) => {
8349
+ const computeSharedRelationName = (rel, sourceCollection, _collections) => {
8350
+ const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
8351
+ if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
8352
+ return `${getTableName(sourceCollection)}_${rel.localKey}`;
8353
+ }
8354
+ if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
8355
+ try {
8356
+ const targetCollection = rel.target();
8357
+ return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
8358
+ } catch {
8359
+ return fallback;
8360
+ }
8361
+ }
8362
+ if (rel.direction === "inverse" && rel.cardinality === "one") {
8363
+ if (rel.foreignKeyOnTarget) {
8364
+ try {
8365
+ const targetCollection = rel.target();
8366
+ return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
8367
+ } catch {
8368
+ return fallback;
8369
+ }
8370
+ }
8371
+ try {
8372
+ const targetCollection = rel.target();
8373
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8374
+ const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey && targetRel.target().slug === sourceCollection.slug);
8375
+ if (correspondingRelation && correspondingRelation.localKey) {
8376
+ return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
8377
+ }
8378
+ } catch {
8379
+ }
8380
+ return fallback;
8381
+ }
8382
+ return fallback;
8383
+ };
8384
+ const generateSchema = async (collections, stripPolicies = false) => {
7830
8385
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
7831
8386
  const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
7832
8387
  collections.some((c) => c.properties && Object.values(c.properties).some((p) => (p.type === "map" || p.type === "array") && p.columnType === "json"));
@@ -7834,9 +8389,7 @@
7834
8389
  if (hasUuid) pgCoreImports.push("uuid");
7835
8390
  schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';
7836
8391
  `;
7837
- schemaContent += `import { relations as drizzleRelations, sql } from 'drizzle-orm';
7838
-
7839
- `;
8392
+ schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
7840
8393
  const exportedTableVars = [];
7841
8394
  const exportedEnumVars = [];
7842
8395
  const exportedRelationVars = [];
@@ -7897,8 +8450,8 @@
7897
8450
  } = relation.through;
7898
8451
  const onDelete = relation.onDelete ?? "cascade";
7899
8452
  const refOptions = `{ onDelete: "${onDelete}" }`;
7900
- const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
7901
- const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
8453
+ const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "varchar";
8454
+ const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar";
7902
8455
  const sourceId = getPrimaryKeyName(sourceCollection);
7903
8456
  const targetId = getPrimaryKeyName(targetCollection);
7904
8457
  schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
@@ -7907,31 +8460,28 @@
7907
8460
  `;
7908
8461
  schemaContent += ` ${targetColumn}: ${targetColType}("${toSnakeCase(targetColumn)}").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),
7909
8462
  `;
7910
- schemaContent += `}, (table) => ({
7911
- `;
8463
+ schemaContent += "}, (table) => ({\n";
7912
8464
  schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
7913
8465
  `;
7914
- schemaContent += `}));
7915
-
7916
- `;
8466
+ schemaContent += "}));\n\n";
7917
8467
  } else if (!isJunction) {
7918
8468
  schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
7919
8469
  `;
7920
8470
  const columns = /* @__PURE__ */ new Set();
7921
8471
  Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
7922
- const columnString = getDrizzleColumn(propName, prop, collection);
8472
+ const columnString = getDrizzleColumn(propName, prop, collection, collections);
7923
8473
  if (columnString) columns.add(columnString);
7924
8474
  });
7925
8475
  const hasIdColumn = Array.from(columns).some((col) => col.includes(".primaryKey()"));
7926
8476
  if (!hasIdColumn) {
7927
- columns.add(` id: varchar("id").primaryKey()`);
8477
+ columns.add(' id: varchar("id").primaryKey()');
7928
8478
  }
7929
8479
  schemaContent += `${Array.from(columns).join(",\n")}`;
7930
- const securityRules = collection.securityRules;
7931
- if (securityRules && securityRules.length > 0) {
8480
+ const securityRules = isPostgresCollection(collection) ? collection.securityRules : void 0;
8481
+ if (!stripPolicies && securityRules && securityRules.length > 0) {
7932
8482
  schemaContent += "\n}, (table) => ([\n";
7933
8483
  securityRules.forEach((rule, idx) => {
7934
- schemaContent += generatePolicyCode(tableName, rule, idx);
8484
+ schemaContent += generatePolicyCode(tableName, rule);
7935
8485
  });
7936
8486
  schemaContent += "])).enableRLS();\n\n";
7937
8487
  } else {
@@ -7971,13 +8521,13 @@
7971
8521
  }
7972
8522
  } catch {
7973
8523
  }
7974
- tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {
8524
+ tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {
7975
8525
  fields: [${tableVarName}.${relation.through.sourceColumn}],
7976
8526
  references: [${sourceTableVar}.${sourceId}],
7977
8527
  relationName: "${owningRelationName}"
7978
8528
  })`);
7979
8529
  const targetRelName = inverseRelationName ?? owningRelationName;
7980
- tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {
8530
+ tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {
7981
8531
  fields: [${tableVarName}.${relation.through.targetColumn}],
7982
8532
  references: [${targetTableVar}.${targetId}],
7983
8533
  relationName: "${targetRelName}"
@@ -7985,22 +8535,25 @@
7985
8535
  }
7986
8536
  } else {
7987
8537
  const resolvedRelations = resolveCollectionRelations(collection);
8538
+ const emittedRelationNames = /* @__PURE__ */ new Set();
7988
8539
  for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
7989
8540
  try {
7990
8541
  const target = rel.target();
7991
8542
  const targetTableVar = getTableVarName(getTableName(target));
7992
- const relationName = rel.relationName ?? relationKey;
7993
- const drizzleRelationName = relationName;
8543
+ const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
8544
+ const deduplicationKey = `${drizzleRelationName}::${rel.direction}`;
8545
+ if (emittedRelationNames.has(deduplicationKey)) continue;
8546
+ emittedRelationNames.add(deduplicationKey);
7994
8547
  if (rel.cardinality === "one") {
7995
8548
  if (rel.direction === "owning" && rel.localKey) {
7996
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8549
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
7997
8550
  fields: [${tableVarName}.${rel.localKey}],
7998
8551
  references: [${targetTableVar}.${getPrimaryKeyName(target)}],
7999
8552
  relationName: "${drizzleRelationName}"
8000
8553
  })`);
8001
8554
  } else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8002
8555
  const sourceIdField = getPrimaryKeyName(collection);
8003
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8556
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8004
8557
  fields: [${tableVarName}.${sourceIdField}],
8005
8558
  references: [${targetTableVar}.${rel.foreignKeyOnTarget}],
8006
8559
  relationName: "${drizzleRelationName}"
@@ -8012,7 +8565,7 @@
8012
8565
  const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.target().slug === collection.slug);
8013
8566
  if (correspondingRelation && correspondingRelation.localKey) {
8014
8567
  const sourceIdField = getPrimaryKeyName(collection);
8015
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8568
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8016
8569
  fields: [${tableVarName}.${sourceIdField}],
8017
8570
  references: [${targetTableVar}.${correspondingRelation.localKey}],
8018
8571
  relationName: "${drizzleRelationName}"
@@ -8024,10 +8577,10 @@
8024
8577
  }
8025
8578
  } else if (rel.cardinality === "many") {
8026
8579
  if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8027
- tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8580
+ tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8028
8581
  } else if (rel.through) {
8029
8582
  const junctionTableVar = getTableVarName(rel.through.table);
8030
- tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8583
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8031
8584
  } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8032
8585
  try {
8033
8586
  const targetCollection = rel.target();
@@ -8035,7 +8588,7 @@
8035
8588
  const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8036
8589
  if (correspondingRelation && correspondingRelation.through) {
8037
8590
  const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8038
- tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8591
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8039
8592
  } else {
8040
8593
  console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8041
8594
  }
@@ -8339,6 +8892,14 @@ ${tableRelations.join(",\n")}
8339
8892
  async handleCollectionSubscription(clientId, request, authContext) {
8340
8893
  const subscriptionId = request.subscriptionId;
8341
8894
  try {
8895
+ const collection = this.registry.getCollectionByPath(request.path);
8896
+ if (!collection) {
8897
+ const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
8898
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
8899
+ console.error(`[RealtimeService] ${msg}`);
8900
+ this.sendError(clientId, msg, subscriptionId);
8901
+ return;
8902
+ }
8342
8903
  this._subscriptions.set(subscriptionId, {
8343
8904
  clientId,
8344
8905
  type: "collection",
@@ -8356,7 +8917,6 @@ ${tableRelations.join(",\n")}
8356
8917
  });
8357
8918
  let entities;
8358
8919
  if (this.driver) {
8359
- const collection = this.registry.getCollectionByPath(request.path);
8360
8920
  entities = await this.driver.fetchCollection({
8361
8921
  path: request.path,
8362
8922
  collection,
@@ -8386,6 +8946,14 @@ ${tableRelations.join(",\n")}
8386
8946
  async handleEntitySubscription(clientId, request, authContext) {
8387
8947
  const subscriptionId = request.subscriptionId;
8388
8948
  try {
8949
+ const collection = this.registry.getCollectionByPath(request.path);
8950
+ if (!collection) {
8951
+ const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
8952
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
8953
+ console.error(`[RealtimeService] ${msg}`);
8954
+ this.sendError(clientId, msg, subscriptionId);
8955
+ return;
8956
+ }
8389
8957
  this._subscriptions.set(subscriptionId, {
8390
8958
  clientId,
8391
8959
  type: "entity",
@@ -8395,7 +8963,6 @@ ${tableRelations.join(",\n")}
8395
8963
  });
8396
8964
  let entity;
8397
8965
  if (this.driver) {
8398
- const collection = this.registry.getCollectionByPath(request.path);
8399
8966
  entity = await this.driver.fetchEntity({
8400
8967
  path: request.path,
8401
8968
  entityId: request.entityId,
@@ -8468,13 +9035,13 @@ ${tableRelations.join(",\n")}
8468
9035
  for (const [subscriptionId, subscription] of webSocketSubscriptions) {
8469
9036
  try {
8470
9037
  if (subscription.type === "entity" && notifyPath === originalPath) {
8471
- if (entity && entity.values?._rebase_invalidated) {
9038
+ if (entity && entity.values && entity.values?._rebase_invalidated) {
8472
9039
  this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
8473
9040
  } else {
8474
9041
  this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
8475
9042
  }
8476
9043
  } else if (subscription.type === "collection" && subscription.collectionRequest) {
8477
- if (!entity || !entity.values?._rebase_invalidated) {
9044
+ if (!entity || !(entity.values && entity.values?._rebase_invalidated)) {
8478
9045
  this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
8479
9046
  }
8480
9047
  this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
@@ -8489,7 +9056,7 @@ ${tableRelations.join(",\n")}
8489
9056
  const callback = this.subscriptionCallbacks.get(subscriptionId);
8490
9057
  if (!callback) continue;
8491
9058
  if (subscription.type === "entity" && notifyPath === originalPath) {
8492
- if (entity && entity.values?._rebase_invalidated) {
9059
+ if (entity && entity.values && entity.values?._rebase_invalidated) {
8493
9060
  this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
8494
9061
  } else {
8495
9062
  callback(entity);
@@ -8555,6 +9122,7 @@ ${tableRelations.join(",\n")}
8555
9122
  orderBy: collectionRequest.orderBy,
8556
9123
  order: collectionRequest.order,
8557
9124
  limit: collectionRequest.limit,
9125
+ offset: collectionRequest.offset,
8558
9126
  startAfter: collectionRequest.startAfter,
8559
9127
  searchString: collectionRequest.searchString
8560
9128
  });
@@ -8582,6 +9150,7 @@ ${tableRelations.join(",\n")}
8582
9150
  orderBy: collectionRequest.orderBy,
8583
9151
  order: collectionRequest.order,
8584
9152
  limit: collectionRequest.limit,
9153
+ offset: collectionRequest.offset,
8585
9154
  startAfter: collectionRequest.startAfter,
8586
9155
  databaseId: collectionRequest.databaseId
8587
9156
  });
@@ -8642,6 +9211,7 @@ ${tableRelations.join(",\n")}
8642
9211
  orderBy: collectionRequest.orderBy,
8643
9212
  order: collectionRequest.order,
8644
9213
  limit: collectionRequest.limit,
9214
+ offset: collectionRequest.offset,
8645
9215
  startAfter: collectionRequest.startAfter,
8646
9216
  databaseId: collectionRequest.databaseId
8647
9217
  });
@@ -8947,7 +9517,7 @@ ${tableRelations.join(",\n")}
8947
9517
  }
8948
9518
  const PostgresRealtimeProvider = RealtimeService;
8949
9519
  const clientSessions = /* @__PURE__ */ new Map();
8950
- const WS_RATE_LIMIT = 200;
9520
+ const WS_RATE_LIMIT = 2e3;
8951
9521
  const WS_RATE_WINDOW_MS = 6e4;
8952
9522
  const ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set(["EXECUTE_SQL", "FETCH_DATABASES", "FETCH_ROLES", "FETCH_UNMAPPED_TABLES", "FETCH_TABLE_METADATA", "FETCH_CURRENT_DATABASE", "CREATE_BRANCH", "DELETE_BRANCH", "LIST_BRANCHES"]);
8953
9523
  function isAdminSession(session) {
@@ -8967,6 +9537,12 @@ ${tableRelations.join(",\n")}
8967
9537
  const wss = new ws.WebSocketServer({
8968
9538
  server
8969
9539
  });
9540
+ wss.on("error", (err) => {
9541
+ if (err.code === "EADDRINUSE") {
9542
+ return;
9543
+ }
9544
+ console.error("❌ [WebSocket Server] Error:", err);
9545
+ });
8970
9546
  const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
8971
9547
  wss.on("connection", (ws2) => {
8972
9548
  const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -9207,7 +9783,12 @@ ${tableRelations.join(",\n")}
9207
9783
  options
9208
9784
  } = payload;
9209
9785
  const delegate = await getScopedDelegate();
9210
- const result = await delegate.executeSql(sql, options);
9786
+ const admin = delegate.admin;
9787
+ if (!isSQLAdmin(admin)) {
9788
+ sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
9789
+ break;
9790
+ }
9791
+ const result = await admin.executeSql(sql, options);
9211
9792
  if (process.env.NODE_ENV !== "production") {
9212
9793
  wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
9213
9794
  }
@@ -9225,9 +9806,10 @@ ${tableRelations.join(",\n")}
9225
9806
  {
9226
9807
  wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
9227
9808
  const delegate = await getScopedDelegate();
9809
+ const admin = delegate.admin;
9228
9810
  let databases = [];
9229
- if (delegate.fetchAvailableDatabases) {
9230
- databases = await delegate.fetchAvailableDatabases();
9811
+ if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {
9812
+ databases = await admin.fetchAvailableDatabases();
9231
9813
  }
9232
9814
  wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
9233
9815
  const response = {
@@ -9244,9 +9826,10 @@ ${tableRelations.join(",\n")}
9244
9826
  {
9245
9827
  wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
9246
9828
  const delegate = await getScopedDelegate();
9829
+ const admin = delegate.admin;
9247
9830
  let roles2 = [];
9248
- if (delegate.fetchAvailableRoles) {
9249
- roles2 = await delegate.fetchAvailableRoles();
9831
+ if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
9832
+ roles2 = await admin.fetchAvailableRoles();
9250
9833
  }
9251
9834
  wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
9252
9835
  const response = {
@@ -9263,9 +9846,10 @@ ${tableRelations.join(",\n")}
9263
9846
  {
9264
9847
  wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
9265
9848
  const delegate = await getScopedDelegate();
9849
+ const admin = delegate.admin;
9266
9850
  let database = void 0;
9267
- if (delegate.fetchCurrentDatabase) {
9268
- database = await delegate.fetchCurrentDatabase();
9851
+ if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {
9852
+ database = await admin.fetchCurrentDatabase();
9269
9853
  }
9270
9854
  const response = {
9271
9855
  type: "FETCH_CURRENT_DATABASE_SUCCESS",
@@ -9281,9 +9865,10 @@ ${tableRelations.join(",\n")}
9281
9865
  {
9282
9866
  wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
9283
9867
  const delegate = await getScopedDelegate();
9868
+ const admin = delegate.admin;
9284
9869
  let tables = [];
9285
- if (delegate.fetchUnmappedTables) {
9286
- tables = await delegate.fetchUnmappedTables(payload?.mappedPaths);
9870
+ if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {
9871
+ tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
9287
9872
  }
9288
9873
  wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
9289
9874
  const response = {
@@ -9303,9 +9888,10 @@ ${tableRelations.join(",\n")}
9303
9888
  tableName
9304
9889
  } = payload;
9305
9890
  const delegate = await getScopedDelegate();
9891
+ const admin = delegate.admin;
9306
9892
  let metadata;
9307
- if (delegate.fetchTableMetadata) {
9308
- metadata = await delegate.fetchTableMetadata(tableName);
9893
+ if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {
9894
+ metadata = await admin.fetchTableMetadata(tableName);
9309
9895
  }
9310
9896
  wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
9311
9897
  const response = {
@@ -9490,7 +10076,7 @@ ${tableRelations.join(",\n")}
9490
10076
  */
9491
10077
  getRelationKeysForCollection(collectionPath) {
9492
10078
  const collection = this.getCollectionByPath(collectionPath);
9493
- if (!collection?.relations) return [];
10079
+ if (!collection || !getDataSourceCapabilities(collection.driver).supportsRelations || !collection.relations) return [];
9494
10080
  return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
9495
10081
  }
9496
10082
  }
@@ -9547,8 +10133,6 @@ ${tableRelations.join(",\n")}
9547
10133
  password_hash TEXT,
9548
10134
  display_name TEXT,
9549
10135
  photo_url TEXT,
9550
- provider TEXT DEFAULT 'email',
9551
- google_id TEXT UNIQUE,
9552
10136
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
9553
10137
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9554
10138
  )
@@ -9558,8 +10142,20 @@ ${tableRelations.join(",\n")}
9558
10142
  ON rebase.users(email)
9559
10143
  `);
9560
10144
  await db.execute(drizzleOrm.sql`
9561
- CREATE INDEX IF NOT EXISTS idx_users_google_id
9562
- ON rebase.users(google_id)
10145
+ CREATE TABLE IF NOT EXISTS rebase.user_identities (
10146
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10147
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
10148
+ provider TEXT NOT NULL,
10149
+ provider_id TEXT NOT NULL,
10150
+ profile_data JSONB,
10151
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10152
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10153
+ UNIQUE(provider, provider_id)
10154
+ )
10155
+ `);
10156
+ await db.execute(drizzleOrm.sql`
10157
+ CREATE INDEX IF NOT EXISTS idx_user_identities_user
10158
+ ON rebase.user_identities(user_id)
9563
10159
  `);
9564
10160
  await db.execute(drizzleOrm.sql`
9565
10161
  CREATE TABLE IF NOT EXISTS rebase.roles (
@@ -9572,10 +10168,6 @@ ${tableRelations.join(",\n")}
9572
10168
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9573
10169
  )
9574
10170
  `);
9575
- await db.execute(drizzleOrm.sql`
9576
- ALTER TABLE rebase.roles
9577
- ADD COLUMN IF NOT EXISTS collection_permissions JSONB
9578
- `);
9579
10171
  await db.execute(drizzleOrm.sql`
9580
10172
  CREATE TABLE IF NOT EXISTS rebase.user_roles (
9581
10173
  user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
@@ -9607,51 +10199,6 @@ ${tableRelations.join(",\n")}
9607
10199
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
9608
10200
  ON rebase.refresh_tokens(user_id)
9609
10201
  `);
9610
- await db.execute(drizzleOrm.sql`
9611
- ALTER TABLE rebase.refresh_tokens
9612
- ADD COLUMN IF NOT EXISTS user_agent TEXT
9613
- `);
9614
- await db.execute(drizzleOrm.sql`
9615
- ALTER TABLE rebase.refresh_tokens
9616
- ADD COLUMN IF NOT EXISTS ip_address TEXT
9617
- `);
9618
- const constraintCheck = await db.execute(drizzleOrm.sql`
9619
- SELECT 1 FROM information_schema.table_constraints
9620
- WHERE constraint_name = 'unique_device_session'
9621
- AND table_schema = 'rebase'
9622
- AND table_name = 'refresh_tokens'
9623
- `);
9624
- if (constraintCheck.rows.length === 0) {
9625
- try {
9626
- await db.execute(drizzleOrm.sql`
9627
- ALTER TABLE rebase.refresh_tokens
9628
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
9629
- `);
9630
- console.log("✅ Added unique_device_session constraint");
9631
- } catch (e) {
9632
- const errorMessage = e instanceof Error ? e.message : String(e);
9633
- if (errorMessage.includes("could not create unique index")) {
9634
- console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
9635
- await db.execute(drizzleOrm.sql`
9636
- DELETE FROM rebase.refresh_tokens a
9637
- USING rebase.refresh_tokens b
9638
- WHERE a.user_id = b.user_id
9639
- AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
9640
- AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
9641
- AND a.created_at < b.created_at
9642
- `);
9643
- await db.execute(drizzleOrm.sql`
9644
- ALTER TABLE rebase.refresh_tokens
9645
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
9646
- `).catch((retryErr) => {
9647
- const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
9648
- console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
9649
- });
9650
- } else {
9651
- console.error("Constraint migration issue:", errorMessage);
9652
- }
9653
- }
9654
- }
9655
10202
  await db.execute(drizzleOrm.sql`
9656
10203
  CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
9657
10204
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -9677,18 +10224,7 @@ ${tableRelations.join(",\n")}
9677
10224
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9678
10225
  )
9679
10226
  `);
9680
- await db.execute(drizzleOrm.sql`
9681
- ALTER TABLE rebase.users
9682
- ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE
9683
- `);
9684
- await db.execute(drizzleOrm.sql`
9685
- ALTER TABLE rebase.users
9686
- ADD COLUMN IF NOT EXISTS email_verification_token TEXT
9687
- `);
9688
- await db.execute(drizzleOrm.sql`
9689
- ALTER TABLE rebase.users
9690
- ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
9691
- `);
10227
+ await applyInternalMigrations(db);
9692
10228
  await db.execute(drizzleOrm.sql`CREATE SCHEMA IF NOT EXISTS auth`);
9693
10229
  await db.transaction(async (tx) => {
9694
10230
  await tx.execute(drizzleOrm.sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
@@ -9741,6 +10277,99 @@ ${tableRelations.join(",\n")}
9741
10277
  }
9742
10278
  console.log("✅ Default roles created: admin, editor, viewer");
9743
10279
  }
10280
+ async function applyInternalMigrations(db) {
10281
+ try {
10282
+ await db.execute(drizzleOrm.sql`
10283
+ ALTER TABLE rebase.users
10284
+ ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
10285
+ ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
10286
+ ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
10287
+ `);
10288
+ const columnsCheck = await db.execute(drizzleOrm.sql`
10289
+ SELECT column_name
10290
+ FROM information_schema.columns
10291
+ WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
10292
+ `);
10293
+ const existingColumns = columnsCheck.rows.map((r) => r.column_name);
10294
+ if (existingColumns.includes("google_id")) {
10295
+ await db.execute(drizzleOrm.sql`
10296
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
10297
+ SELECT id, 'google', google_id
10298
+ FROM rebase.users
10299
+ WHERE google_id IS NOT NULL
10300
+ ON CONFLICT (provider, provider_id) DO NOTHING
10301
+ `);
10302
+ }
10303
+ if (existingColumns.includes("linkedin_id")) {
10304
+ await db.execute(drizzleOrm.sql`
10305
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
10306
+ SELECT id, 'linkedin', linkedin_id
10307
+ FROM rebase.users
10308
+ WHERE linkedin_id IS NOT NULL
10309
+ ON CONFLICT (provider, provider_id) DO NOTHING
10310
+ `);
10311
+ }
10312
+ if (existingColumns.length > 0) {
10313
+ await db.execute(drizzleOrm.sql`
10314
+ ALTER TABLE rebase.users
10315
+ DROP COLUMN IF EXISTS provider,
10316
+ DROP COLUMN IF EXISTS google_id,
10317
+ DROP COLUMN IF EXISTS linkedin_id
10318
+ `);
10319
+ await db.execute(drizzleOrm.sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
10320
+ await db.execute(drizzleOrm.sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
10321
+ console.log("✅ Migrated to user_identities and dropped legacy columns.");
10322
+ }
10323
+ await db.execute(drizzleOrm.sql`
10324
+ ALTER TABLE rebase.roles
10325
+ ADD COLUMN IF NOT EXISTS collection_permissions JSONB
10326
+ `);
10327
+ await db.execute(drizzleOrm.sql`
10328
+ ALTER TABLE rebase.refresh_tokens
10329
+ ADD COLUMN IF NOT EXISTS user_agent TEXT,
10330
+ ADD COLUMN IF NOT EXISTS ip_address TEXT
10331
+ `);
10332
+ const constraintCheck = await db.execute(drizzleOrm.sql`
10333
+ SELECT 1 FROM information_schema.table_constraints
10334
+ WHERE constraint_name = 'unique_device_session'
10335
+ AND table_schema = 'rebase'
10336
+ AND table_name = 'refresh_tokens'
10337
+ `);
10338
+ if (constraintCheck.rows.length === 0) {
10339
+ try {
10340
+ await db.execute(drizzleOrm.sql`
10341
+ ALTER TABLE rebase.refresh_tokens
10342
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
10343
+ `);
10344
+ console.log("✅ Added unique_device_session constraint");
10345
+ } catch (e) {
10346
+ const errorMessage = e instanceof Error ? e.message : String(e);
10347
+ if (errorMessage.includes("could not create unique index")) {
10348
+ console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
10349
+ await db.execute(drizzleOrm.sql`
10350
+ DELETE FROM rebase.refresh_tokens a
10351
+ USING rebase.refresh_tokens b
10352
+ WHERE a.user_id = b.user_id
10353
+ AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
10354
+ AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
10355
+ AND a.created_at < b.created_at
10356
+ `);
10357
+ await db.execute(drizzleOrm.sql`
10358
+ ALTER TABLE rebase.refresh_tokens
10359
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
10360
+ `).catch((retryErr) => {
10361
+ const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
10362
+ console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
10363
+ });
10364
+ } else {
10365
+ console.error("Constraint migration issue:", errorMessage);
10366
+ }
10367
+ }
10368
+ }
10369
+ } catch (error) {
10370
+ console.error("❌ Failed to run internal migrations:", error);
10371
+ }
10372
+ }
9744
10373
  class UserService {
9745
10374
  constructor(db) {
9746
10375
  this.db = db;
@@ -9757,9 +10386,54 @@ ${tableRelations.join(",\n")}
9757
10386
  const [user] = await this.db.select().from(users).where(drizzleOrm.eq(users.email, email.toLowerCase()));
9758
10387
  return user || null;
9759
10388
  }
9760
- async getUserByGoogleId(googleId) {
9761
- const [user] = await this.db.select().from(users).where(drizzleOrm.eq(users.googleId, googleId));
9762
- return user || null;
10389
+ async getUserByIdentity(provider, providerId) {
10390
+ const result = await this.db.execute(drizzleOrm.sql`
10391
+ SELECT u.*
10392
+ FROM rebase.users u
10393
+ INNER JOIN rebase.user_identities ui ON u.id = ui.user_id
10394
+ WHERE ui.provider = ${provider} AND ui.provider_id = ${providerId}
10395
+ LIMIT 1
10396
+ `);
10397
+ if (result.rows.length === 0) return null;
10398
+ const row = result.rows[0];
10399
+ return {
10400
+ id: row.id,
10401
+ email: row.email,
10402
+ passwordHash: row.password_hash ?? null,
10403
+ displayName: row.display_name ?? null,
10404
+ photoUrl: row.photo_url ?? null,
10405
+ emailVerified: row.email_verified ?? false,
10406
+ emailVerificationToken: row.email_verification_token ?? null,
10407
+ emailVerificationSentAt: row.email_verification_sent_at ?? null,
10408
+ createdAt: row.created_at,
10409
+ updatedAt: row.updated_at
10410
+ };
10411
+ }
10412
+ async getUserIdentities(userId) {
10413
+ const result = await this.db.execute(drizzleOrm.sql`
10414
+ SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
10415
+ FROM rebase.user_identities
10416
+ WHERE user_id = ${userId}
10417
+ `);
10418
+ return result.rows.map((row) => ({
10419
+ id: row.id,
10420
+ userId: row.user_id,
10421
+ provider: row.provider,
10422
+ providerId: row.provider_id,
10423
+ profileData: row.profile_data ?? null,
10424
+ createdAt: row.created_at,
10425
+ updatedAt: row.updated_at
10426
+ }));
10427
+ }
10428
+ async linkUserIdentity(userId, provider, providerId, profileData) {
10429
+ await this.db.insert(userIdentities).values({
10430
+ userId,
10431
+ provider,
10432
+ providerId,
10433
+ profileData: profileData || null
10434
+ }).onConflictDoNothing({
10435
+ target: [userIdentities.provider, userIdentities.providerId]
10436
+ });
9763
10437
  }
9764
10438
  async updateUser(id, data) {
9765
10439
  const [user] = await this.db.update(users).set({
@@ -9780,6 +10454,7 @@ ${tableRelations.join(",\n")}
9780
10454
  const search = options?.search?.trim() || "";
9781
10455
  const orderBy = options?.orderBy || "createdAt";
9782
10456
  const orderDir = options?.orderDir || "desc";
10457
+ const roleId = options?.roleId;
9783
10458
  const columnMap = {
9784
10459
  email: "email",
9785
10460
  displayName: "display_name",
@@ -9789,42 +10464,34 @@ ${tableRelations.join(",\n")}
9789
10464
  };
9790
10465
  const orderColumn = columnMap[orderBy] || "created_at";
9791
10466
  const direction = orderDir === "asc" ? drizzleOrm.sql`ASC` : drizzleOrm.sql`DESC`;
9792
- let rows;
9793
- let total;
10467
+ const conditions = [];
10468
+ if (roleId) {
10469
+ conditions.push(drizzleOrm.sql`EXISTS (SELECT 1 FROM rebase.user_roles ur WHERE ur.user_id = users.id AND ur.role_id = ${roleId})`);
10470
+ }
9794
10471
  if (search) {
9795
10472
  const pattern = `%${search}%`;
9796
- const countResult = await this.db.execute(drizzleOrm.sql`
9797
- SELECT count(*)::int as total FROM rebase.users
9798
- WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
9799
- `);
9800
- total = countResult.rows[0].total;
9801
- const dataResult = await this.db.execute(drizzleOrm.sql`
9802
- SELECT * FROM rebase.users
9803
- WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
9804
- ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}
9805
- LIMIT ${limit} OFFSET ${offset}
9806
- `);
9807
- rows = dataResult.rows;
9808
- } else {
9809
- const countResult = await this.db.execute(drizzleOrm.sql`
9810
- SELECT count(*)::int as total FROM rebase.users
9811
- `);
9812
- total = countResult.rows[0].total;
9813
- const dataResult = await this.db.execute(drizzleOrm.sql`
9814
- SELECT * FROM rebase.users
9815
- ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}
9816
- LIMIT ${limit} OFFSET ${offset}
9817
- `);
9818
- rows = dataResult.rows;
10473
+ conditions.push(drizzleOrm.sql`(email ILIKE ${pattern} OR display_name ILIKE ${pattern})`);
9819
10474
  }
10475
+ const whereClause = conditions.length > 0 ? drizzleOrm.sql`WHERE ${drizzleOrm.sql.join(conditions, drizzleOrm.sql` AND `)}` : drizzleOrm.sql``;
10476
+ const orderByClause = roleId ? drizzleOrm.sql`ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}` : drizzleOrm.sql`ORDER BY (SELECT count(*) FROM rebase.user_roles ur WHERE ur.user_id = users.id) DESC, ${drizzleOrm.sql.raw(orderColumn)} ${direction}`;
10477
+ const countResult = await this.db.execute(drizzleOrm.sql`
10478
+ SELECT count(*)::int as total FROM rebase.users
10479
+ ${whereClause}
10480
+ `);
10481
+ const total = countResult.rows[0].total;
10482
+ const dataResult = await this.db.execute(drizzleOrm.sql`
10483
+ SELECT * FROM rebase.users
10484
+ ${whereClause}
10485
+ ${orderByClause}
10486
+ LIMIT ${limit} OFFSET ${offset}
10487
+ `);
10488
+ const rows = dataResult.rows;
9820
10489
  const mappedUsers = rows.map((row) => ({
9821
10490
  id: row.id,
9822
10491
  email: row.email,
9823
10492
  passwordHash: row.password_hash ?? row.passwordHash ?? null,
9824
10493
  displayName: row.display_name ?? row.displayName ?? null,
9825
10494
  photoUrl: row.photo_url ?? row.photoUrl ?? null,
9826
- provider: row.provider,
9827
- googleId: row.google_id ?? row.googleId ?? null,
9828
10495
  emailVerified: row.email_verified ?? row.emailVerified ?? false,
9829
10496
  emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
9830
10497
  emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
@@ -10198,8 +10865,14 @@ ${tableRelations.join(",\n")}
10198
10865
  async getUserByEmail(email) {
10199
10866
  return this.userService.getUserByEmail(email);
10200
10867
  }
10201
- async getUserByGoogleId(googleId) {
10202
- return this.userService.getUserByGoogleId(googleId);
10868
+ async getUserByIdentity(provider, providerId) {
10869
+ return this.userService.getUserByIdentity(provider, providerId);
10870
+ }
10871
+ async getUserIdentities(userId) {
10872
+ return this.userService.getUserIdentities(userId);
10873
+ }
10874
+ async linkUserIdentity(userId, provider, providerId, profileData) {
10875
+ return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
10203
10876
  }
10204
10877
  async updateUser(id, data) {
10205
10878
  return this.userService.updateUser(id, data);
@@ -10327,16 +11000,6 @@ ${tableRelations.join(",\n")}
10327
11000
  updatedBy
10328
11001
  } = params;
10329
11002
  const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
10330
- try {
10331
- require("fs").appendFileSync("/Users/francesco/rebase/packages/backend/history_diff.log", `[recordHistory: ${tableName}/${entityId} - ${action}]
10332
- CHANGED FIELDS: ${JSON.stringify(changedFields)}
10333
- PREVIOUS: ${JSON.stringify(previousValues, null, 2)}
10334
- NEW: ${JSON.stringify(values, null, 2)}
10335
-
10336
- `);
10337
- } catch (e) {
10338
- console.error("DEBUG FILE WRITE ERROR:", e);
10339
- }
10340
11003
  if (action === "update" && (!changedFields || changedFields.length === 0)) {
10341
11004
  return;
10342
11005
  }
@@ -10498,6 +11161,7 @@ NEW: ${JSON.stringify(values, null, 2)}
10498
11161
  const registry = new PostgresCollectionRegistry();
10499
11162
  if (collections) {
10500
11163
  registry.registerMultiple(collections);
11164
+ console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
10501
11165
  }
10502
11166
  if (pgConfig.schema?.tables) {
10503
11167
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -10564,9 +11228,6 @@ NEW: ${JSON.stringify(values, null, 2)}
10564
11228
  const internals = driverResult.internals;
10565
11229
  const db = internals.db;
10566
11230
  await ensureAuthTablesExist(db);
10567
- if (authConfig.google?.clientId) {
10568
- serverCore.configureGoogleOAuth(authConfig.google.clientId);
10569
- }
10570
11231
  let emailService;
10571
11232
  if (authConfig.email) {
10572
11233
  emailService = serverCore.createEmailService(authConfig.email);
@@ -10634,6 +11295,8 @@ NEW: ${JSON.stringify(values, null, 2)}
10634
11295
  exports2.refreshTokensRelations = refreshTokensRelations;
10635
11296
  exports2.roles = roles;
10636
11297
  exports2.rolesRelations = rolesRelations;
11298
+ exports2.userIdentities = userIdentities;
11299
+ exports2.userIdentitiesRelations = userIdentitiesRelations;
10637
11300
  exports2.userRoles = userRoles;
10638
11301
  exports2.userRolesRelations = userRolesRelations;
10639
11302
  exports2.users = users;