@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.es.js CHANGED
@@ -2,6 +2,7 @@ import { Pool, Client } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { sql, inArray, eq as eq$3, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
4
4
  import { pgSchema, timestamp, varchar, boolean, uuid, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
5
+ import { createHash, randomUUID } from "crypto";
5
6
  import * as fs from "fs";
6
7
  import { promises } from "fs";
7
8
  import path from "path";
@@ -9,67 +10,120 @@ import { pathToFileURL } from "url";
9
10
  import chokidar from "chokidar";
10
11
  import { WebSocket, WebSocketServer } from "ws";
11
12
  import { EventEmitter } from "events";
12
- import { randomUUID } from "crypto";
13
13
  import { inspect } from "util";
14
- import { extractUserFromToken, configureGoogleOAuth, createEmailService } from "@rebasepro/server-core";
15
- function createPostgresDatabaseConnection(connectionString, schema) {
16
- const pool = new Pool({
14
+ import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
15
+ const DEFAULT_POOL = {
16
+ max: 20,
17
+ idleTimeoutMillis: 3e4,
18
+ connectionTimeoutMillis: 1e4,
19
+ queryTimeout: 3e4,
20
+ statementTimeout: 3e4,
21
+ keepAlive: true
22
+ };
23
+ function createPostgresDatabaseConnection(connectionString, schema, poolConfig) {
24
+ const opts = {
25
+ ...DEFAULT_POOL,
26
+ ...poolConfig
27
+ };
28
+ const pgPoolConfig = {
17
29
  connectionString,
18
- // Connection pool settings for resilience
19
- max: 20,
20
- // Maximum number of connections in the pool
21
- idleTimeoutMillis: 3e4,
22
- // Close idle connections after 30 seconds
23
- connectionTimeoutMillis: 1e4,
24
- // Timeout for new connections
25
- // Retry configuration
26
- query_timeout: 3e4,
27
- // Query timeout
28
- statement_timeout: 3e4,
29
- // Statement timeout
30
- // Keep connections alive
31
- keepAlive: true,
30
+ max: opts.max,
31
+ idleTimeoutMillis: opts.idleTimeoutMillis,
32
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
33
+ query_timeout: opts.queryTimeout,
34
+ statement_timeout: opts.statementTimeout,
35
+ keepAlive: opts.keepAlive,
32
36
  keepAliveInitialDelayMillis: 0
33
- });
37
+ };
38
+ const pool = new Pool(pgPoolConfig);
34
39
  pool.on("error", (err) => {
35
- console.error("Database connection error:", err);
40
+ console.error("[pg-pool] Unexpected pool error:", err.message);
36
41
  if (err.message.includes("ETIMEDOUT")) {
37
- console.warn("Connection timeout detected, pool will automatically retry...");
42
+ console.warn("[pg-pool] Connection timeout detected pool will auto-retry");
38
43
  }
39
44
  });
40
- pool.on("connect", (client) => {
41
- console.debug("Database client connected");
42
- client.on("error", (err) => {
43
- console.error("Database client error:", err);
44
- });
45
- });
46
- pool.on("remove", (client) => {
47
- console.debug("Database client removed from pool");
48
- });
49
45
  const db = schema ? drizzle(pool, {
50
46
  schema
51
47
  }) : drizzle(pool);
52
- process.on("SIGINT", async () => {
53
- console.log("SIGINT: Closing database pool...");
54
- await pool.end();
55
- process.exit(0);
56
- });
57
- process.on("SIGTERM", async () => {
58
- console.log("SIGTERM: Closing database pool...");
59
- await pool.end();
60
- process.exit(0);
61
- });
62
48
  return {
63
49
  db,
50
+ pool,
64
51
  connectionString
65
52
  };
66
53
  }
67
54
  function isPostgresCollection(collection) {
68
55
  return !collection.driver || collection.driver === "postgres";
69
56
  }
70
- function isFirebaseCollection(collection) {
71
- return collection.driver === "firestore";
57
+ function isSQLAdmin(admin) {
58
+ return !!admin && typeof admin.executeSql === "function";
59
+ }
60
+ function isSchemaAdmin(admin) {
61
+ return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
62
+ }
63
+ const POSTGRES_CAPABILITIES = {
64
+ key: "postgres",
65
+ label: "PostgreSQL",
66
+ supportsRelations: true,
67
+ supportsSubcollections: false,
68
+ supportsRLS: true,
69
+ supportsReferences: false,
70
+ supportsColumnTypes: true,
71
+ supportsRealtime: true,
72
+ supportsSQLAdmin: true,
73
+ supportsDocumentAdmin: false,
74
+ supportsSchemaAdmin: true
75
+ };
76
+ const FIREBASE_CAPABILITIES = {
77
+ key: "firestore",
78
+ label: "Firebase / Firestore",
79
+ supportsRelations: false,
80
+ supportsSubcollections: true,
81
+ supportsRLS: false,
82
+ supportsReferences: true,
83
+ supportsColumnTypes: false,
84
+ supportsRealtime: true,
85
+ supportsSQLAdmin: false,
86
+ supportsDocumentAdmin: false,
87
+ supportsSchemaAdmin: false
88
+ };
89
+ const MONGODB_CAPABILITIES = {
90
+ key: "mongodb",
91
+ label: "MongoDB",
92
+ supportsRelations: false,
93
+ supportsSubcollections: true,
94
+ supportsRLS: false,
95
+ supportsReferences: true,
96
+ supportsColumnTypes: false,
97
+ supportsRealtime: false,
98
+ supportsSQLAdmin: false,
99
+ supportsDocumentAdmin: true,
100
+ supportsSchemaAdmin: true
101
+ };
102
+ const DEFAULT_CAPABILITIES = {
103
+ key: "(default)",
104
+ label: "Default",
105
+ supportsRelations: true,
106
+ supportsSubcollections: true,
107
+ supportsRLS: true,
108
+ supportsReferences: true,
109
+ supportsColumnTypes: true,
110
+ supportsRealtime: true,
111
+ supportsSQLAdmin: true,
112
+ supportsDocumentAdmin: true,
113
+ supportsSchemaAdmin: true
114
+ };
115
+ const CAPABILITIES_REGISTRY = {
116
+ postgres: POSTGRES_CAPABILITIES,
117
+ firestore: FIREBASE_CAPABILITIES,
118
+ mongodb: MONGODB_CAPABILITIES,
119
+ "(default)": DEFAULT_CAPABILITIES
120
+ };
121
+ function getDataSourceCapabilities(driver) {
122
+ if (!driver) return POSTGRES_CAPABILITIES;
123
+ return CAPABILITIES_REGISTRY[driver] ?? DEFAULT_CAPABILITIES;
72
124
  }
125
+ const DEFAULT_ONE_OF_TYPE = "type";
126
+ const DEFAULT_ONE_OF_VALUE = "value";
73
127
  const snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g;
74
128
  const toSnakeCase = (str) => {
75
129
  const regExpMatchArray = str.match(snakeCaseRegex);
@@ -967,6 +1021,21 @@ function generateForeignKeyName(name) {
967
1021
  const singularName = snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName;
968
1022
  return `${singularName}_id`;
969
1023
  }
1024
+ function createRelationRef(id, path2) {
1025
+ return {
1026
+ id,
1027
+ path: path2,
1028
+ __type: "relation"
1029
+ };
1030
+ }
1031
+ function createRelationRefWithData(id, path2, data) {
1032
+ return {
1033
+ id,
1034
+ path: path2,
1035
+ __type: "relation",
1036
+ data
1037
+ };
1038
+ }
970
1039
  function enumToObjectEntries(enumValues) {
971
1040
  if (Array.isArray(enumValues)) {
972
1041
  return enumValues;
@@ -990,15 +1059,35 @@ function getSubcollections(collection) {
990
1059
  if (collection.childCollections) {
991
1060
  return collection.childCollections() ?? [];
992
1061
  }
993
- if (isFirebaseCollection(collection) && collection.subcollections) {
1062
+ if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
994
1063
  return collection.subcollections() ?? [];
995
1064
  }
996
- if (isPostgresCollection(collection) && collection.relations) {
1065
+ if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
997
1066
  const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
998
1067
  return manyRelations.map((r) => {
999
1068
  const target = r.target();
1000
- return r.overrides ? mergeDeep(target, r.overrides) : target;
1001
- });
1069
+ if (!target) return void 0;
1070
+ const relationKey = r.relationName || target.slug;
1071
+ let customName;
1072
+ if (collection.properties) {
1073
+ const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1074
+ if (prop && prop[1].name) {
1075
+ customName = prop[1].name;
1076
+ }
1077
+ }
1078
+ const baseOverrides = {
1079
+ slug: relationKey
1080
+ };
1081
+ if (customName) {
1082
+ baseOverrides.name = customName;
1083
+ baseOverrides.singularName = customName;
1084
+ }
1085
+ const targetWithOverrides = {
1086
+ ...target,
1087
+ ...baseOverrides
1088
+ };
1089
+ return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1090
+ }).filter((c) => Boolean(c));
1002
1091
  }
1003
1092
  return [];
1004
1093
  }
@@ -1105,7 +1194,7 @@ function sanitizeRelation(relation, sourceCollection) {
1105
1194
  if (!newRelation.foreignKeyOnTarget) {
1106
1195
  let foundForeignKey = false;
1107
1196
  try {
1108
- const targetRelations = targetCollection.relations || [];
1197
+ const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
1109
1198
  for (const targetRel of targetRelations) {
1110
1199
  if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) {
1111
1200
  try {
@@ -1131,7 +1220,7 @@ function sanitizeRelation(relation, sourceCollection) {
1131
1220
  let isManyToManyInverse = false;
1132
1221
  if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
1133
1222
  try {
1134
- const targetRelations = targetCollection.relations || [];
1223
+ const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
1135
1224
  for (const targetRel of targetRelations) {
1136
1225
  if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
1137
1226
  isManyToManyInverse = true;
@@ -1165,14 +1254,21 @@ function sanitizeRelation(relation, sourceCollection) {
1165
1254
  }
1166
1255
  return newRelation;
1167
1256
  }
1257
+ const _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
1168
1258
  function resolveCollectionRelations(collection) {
1259
+ const cached = _resolvedRelationsCache.get(collection);
1260
+ if (cached) return cached;
1261
+ if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
1262
+ const relCollection = collection;
1169
1263
  const relations2 = {};
1170
- if (collection.relations) {
1171
- collection.relations.forEach((relation) => {
1264
+ const registeredRelationNames = /* @__PURE__ */ new Set();
1265
+ if (relCollection.relations) {
1266
+ relCollection.relations.forEach((relation) => {
1172
1267
  const normalizedRelation = sanitizeRelation(relation, collection);
1173
1268
  const relationKey = normalizedRelation.relationName;
1174
1269
  if (relationKey) {
1175
1270
  relations2[relationKey] = normalizedRelation;
1271
+ registeredRelationNames.add(relationKey);
1176
1272
  }
1177
1273
  });
1178
1274
  }
@@ -1184,15 +1280,17 @@ function resolveCollectionRelations(collection) {
1184
1280
  sourceCollection: collection
1185
1281
  });
1186
1282
  if (relation) {
1187
- if (!relations2[propKey]) {
1188
- if (!relation.relationName) {
1189
- relation.relationName = propKey;
1190
- }
1191
- relations2[propKey] = sanitizeRelation(relation, collection);
1283
+ if (relations2[propKey]) return;
1284
+ if (!relation.relationName) {
1285
+ relation.relationName = propKey;
1192
1286
  }
1287
+ const normalizedRelation = sanitizeRelation(relation, collection);
1288
+ relations2[propKey] = normalizedRelation;
1289
+ registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
1193
1290
  }
1194
1291
  });
1195
1292
  }
1293
+ _resolvedRelationsCache.set(collection, relations2);
1196
1294
  return relations2;
1197
1295
  }
1198
1296
  function resolvePropertyRelation({
@@ -1201,7 +1299,24 @@ function resolvePropertyRelation({
1201
1299
  sourceCollection
1202
1300
  }) {
1203
1301
  if (property.type !== "relation") return void 0;
1204
- const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === property.relationName);
1302
+ const relProp = property;
1303
+ if (relProp.target) {
1304
+ return {
1305
+ relationName: relProp.relationName || propertyKey,
1306
+ target: relProp.target,
1307
+ cardinality: relProp.cardinality || "one",
1308
+ direction: relProp.direction || "owning",
1309
+ inverseRelationName: relProp.inverseRelationName,
1310
+ localKey: relProp.localKey,
1311
+ foreignKeyOnTarget: relProp.foreignKeyOnTarget,
1312
+ through: relProp.through,
1313
+ joinPath: relProp.joinPath,
1314
+ onUpdate: relProp.onUpdate,
1315
+ onDelete: relProp.onDelete,
1316
+ overrides: relProp.overrides
1317
+ };
1318
+ }
1319
+ const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
1205
1320
  if (!relation) {
1206
1321
  console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1207
1322
  return void 0;
@@ -1209,7 +1324,7 @@ function resolvePropertyRelation({
1209
1324
  return relation;
1210
1325
  }
1211
1326
  function getTableName(collection) {
1212
- if (isPostgresCollection(collection)) {
1327
+ if (getDataSourceCapabilities(collection.driver).supportsRelations) {
1213
1328
  return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1214
1329
  }
1215
1330
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
@@ -1225,6 +1340,14 @@ function getEnumVarName(tableName, propName) {
1225
1340
  function getColumnName(fullColumn) {
1226
1341
  return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
1227
1342
  }
1343
+ function findRelation(resolvedRelations, key) {
1344
+ if (resolvedRelations[key]) return resolvedRelations[key];
1345
+ const slugKey = key.replace(/_/g, "-");
1346
+ if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
1347
+ const snakeKey = key.replace(/-/g, "_");
1348
+ if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
1349
+ return void 0;
1350
+ }
1228
1351
  var logic = { exports: {} };
1229
1352
  (function(module, exports$1) {
1230
1353
  (function(root2, factory) {
@@ -2997,10 +3120,12 @@ class CollectionRegistry {
2997
3120
  collectionsByTableName = /* @__PURE__ */ new Map();
2998
3121
  collectionsBySlug = /* @__PURE__ */ new Map();
2999
3122
  rootCollections = [];
3123
+ cachedCollectionsList = null;
3000
3124
  // Raw configuration layer (used by Collection Editor AST generator)
3001
3125
  rawCollectionsByTableName = /* @__PURE__ */ new Map();
3002
3126
  rawCollectionsBySlug = /* @__PURE__ */ new Map();
3003
3127
  rawRootCollections = [];
3128
+ cachedRawCollectionsList = null;
3004
3129
  // Snapshot of raw input for idempotency check — compared BEFORE normalization
3005
3130
  // to avoid the issue where normalization creates new objects that always fail equality.
3006
3131
  lastRawInputSnapshot = null;
@@ -3013,9 +3138,11 @@ class CollectionRegistry {
3013
3138
  this.collectionsByTableName.clear();
3014
3139
  this.collectionsBySlug.clear();
3015
3140
  this.rootCollections = [];
3141
+ this.cachedCollectionsList = null;
3016
3142
  this.rawCollectionsByTableName.clear();
3017
3143
  this.rawCollectionsBySlug.clear();
3018
3144
  this.rawRootCollections = [];
3145
+ this.cachedRawCollectionsList = null;
3019
3146
  }
3020
3147
  /**
3021
3148
  * Registers a collection and its subcollections recursively.
@@ -3048,12 +3175,14 @@ class CollectionRegistry {
3048
3175
  this.rawCollectionsBySlug.set(raw.slug, raw);
3049
3176
  }
3050
3177
  });
3051
- normalizedCollections.forEach((c, index) => {
3178
+ normalizedCollections.forEach((c) => {
3052
3179
  const subcollections = getSubcollections(c);
3053
- const rawSubcollections = getSubcollections(collections[index]);
3054
- if (subcollections && rawSubcollections) {
3055
- subcollections.forEach((subCollection, subIndex) => {
3056
- this._registerRecursively(this.normalizeCollection(subCollection), cloneDeep$1(rawSubcollections[subIndex]));
3180
+ if (subcollections && subcollections.length > 0) {
3181
+ subcollections.forEach((subCollection) => {
3182
+ if (!subCollection) return;
3183
+ this._registerRecursively(this.normalizeCollection({
3184
+ ...subCollection
3185
+ }), cloneDeep$1(subCollection));
3057
3186
  });
3058
3187
  }
3059
3188
  });
@@ -3079,41 +3208,100 @@ class CollectionRegistry {
3079
3208
  if (rawCollection.slug) {
3080
3209
  this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
3081
3210
  }
3082
- const subcollections = getSubcollections(collection);
3083
- const rawSubcollections = getSubcollections(rawCollection);
3084
- if (subcollections && rawSubcollections) {
3085
- subcollections.forEach((subCollection, index) => {
3086
- this._registerRecursively(this.normalizeCollection(subCollection), cloneDeep$1(rawSubcollections[index]));
3211
+ const subcollections = getSubcollections(normalizedCollection);
3212
+ if (subcollections && subcollections.length > 0) {
3213
+ subcollections.forEach((subCollection) => {
3214
+ if (!subCollection) return;
3215
+ this._registerRecursively(this.normalizeCollection({
3216
+ ...subCollection
3217
+ }), cloneDeep$1(subCollection));
3087
3218
  });
3088
3219
  }
3089
3220
  }
3090
3221
  normalizeCollection(collection) {
3091
- const relations2 = isPostgresCollection(collection) ? collection.relations ?? [] : [];
3092
- const properties = this.normalizeProperties(collection.properties, relations2);
3093
- collection.properties = properties;
3094
- if (!collection.childCollections) {
3095
- if (isFirebaseCollection(collection) && collection.subcollections) {
3096
- collection.childCollections = collection.subcollections;
3097
- } else if (isPostgresCollection(collection) && collection.relations) {
3098
- const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
3222
+ const result = {
3223
+ ...collection
3224
+ };
3225
+ const extractedRelations = this.extractRelationsFromProperties(result.properties);
3226
+ const relResult = result;
3227
+ const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
3228
+ const mergedRelationsRaw = [...extractedRelations];
3229
+ for (const manual of manualRelations) {
3230
+ const name = manual.relationName;
3231
+ if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
3232
+ mergedRelationsRaw.push(manual);
3233
+ }
3234
+ }
3235
+ let mergedRelations = mergedRelationsRaw;
3236
+ if (getDataSourceCapabilities(result.driver).supportsRelations) {
3237
+ mergedRelations = mergedRelationsRaw.map((r) => {
3238
+ try {
3239
+ return sanitizeRelation(r, result);
3240
+ } catch {
3241
+ return r;
3242
+ }
3243
+ });
3244
+ relResult.relations = mergedRelations;
3245
+ }
3246
+ const properties = this.normalizeProperties(result.properties, mergedRelations);
3247
+ result.properties = properties;
3248
+ if (!result.childCollections) {
3249
+ if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) {
3250
+ result.childCollections = result.subcollections;
3251
+ } else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
3252
+ const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
3099
3253
  if (manyRelations.length > 0) {
3100
- collection.childCollections = () => manyRelations.map((r) => {
3254
+ result.childCollections = () => manyRelations.map((r) => {
3101
3255
  const target = r.target();
3102
3256
  return r.overrides ? mergeDeep(target, r.overrides) : target;
3103
3257
  });
3104
3258
  }
3105
3259
  }
3106
3260
  }
3107
- return collection;
3261
+ return result;
3262
+ }
3263
+ /**
3264
+ * Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
3265
+ * This allows developers to define relations directly on properties without a separate
3266
+ * `relations[]` entry on the collection.
3267
+ */
3268
+ extractRelationsFromProperties(properties) {
3269
+ const relations2 = [];
3270
+ for (const [key, property] of Object.entries(properties)) {
3271
+ if (property.type === "relation") {
3272
+ const relProp = property;
3273
+ const target = relProp.target ?? relProp.relation?.target;
3274
+ if (target) {
3275
+ const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
3276
+ relations2.push({
3277
+ relationName,
3278
+ target,
3279
+ cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
3280
+ direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
3281
+ inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
3282
+ localKey: relProp.localKey ?? relProp.relation?.localKey,
3283
+ foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
3284
+ through: relProp.through ?? relProp.relation?.through,
3285
+ joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
3286
+ onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
3287
+ onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
3288
+ overrides: relProp.overrides ?? relProp.relation?.overrides
3289
+ });
3290
+ }
3291
+ } else if (property.type === "map" && property.properties) {
3292
+ relations2.push(...this.extractRelationsFromProperties(property.properties));
3293
+ }
3294
+ }
3295
+ return relations2;
3108
3296
  }
3109
3297
  normalizeProperties(properties, relations2) {
3110
3298
  const newProperties = {};
3111
3299
  for (const key in properties) {
3112
- newProperties[key] = this.normalizeProperty(properties[key], relations2);
3300
+ newProperties[key] = this.normalizeProperty(key, properties[key], relations2);
3113
3301
  }
3114
3302
  return newProperties;
3115
3303
  }
3116
- normalizeProperty(property, relations2) {
3304
+ normalizeProperty(key, property, relations2) {
3117
3305
  const newProperty = {
3118
3306
  ...property
3119
3307
  };
@@ -3123,9 +3311,9 @@ class CollectionRegistry {
3123
3311
  const arrayProp = newProperty;
3124
3312
  if (arrayProp.of) {
3125
3313
  if (Array.isArray(arrayProp.of)) {
3126
- arrayProp.of = arrayProp.of.map((p) => this.normalizeProperty(p, relations2));
3314
+ arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations2));
3127
3315
  } else {
3128
- arrayProp.of = this.normalizeProperty(arrayProp.of, relations2);
3316
+ arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations2);
3129
3317
  }
3130
3318
  } else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
3131
3319
  arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations2);
@@ -3137,11 +3325,12 @@ class CollectionRegistry {
3137
3325
  }
3138
3326
  } else if (newProperty.type === "relation") {
3139
3327
  const relationProperty = newProperty;
3140
- const relation = relations2.find((r) => r.relationName === relationProperty.relationName);
3328
+ const name = relationProperty.relationName || key;
3329
+ const relation = relations2.find((r) => r.relationName === name);
3141
3330
  if (relation) {
3142
3331
  relationProperty.relation = relation;
3143
3332
  } else {
3144
- console.warn(`Could not find relation for property with relationName: ${relationProperty.relationName}`);
3333
+ console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
3145
3334
  }
3146
3335
  }
3147
3336
  return newProperty;
@@ -3149,6 +3338,11 @@ class CollectionRegistry {
3149
3338
  get(path2) {
3150
3339
  const bySlug = this.collectionsBySlug.get(path2);
3151
3340
  if (bySlug) return bySlug;
3341
+ if (path2.includes("-")) {
3342
+ const normalized = path2.replace(/-/g, "_");
3343
+ const byNormalized = this.collectionsBySlug.get(normalized);
3344
+ if (byNormalized) return byNormalized;
3345
+ }
3152
3346
  return this.collectionsByTableName.get(path2);
3153
3347
  }
3154
3348
  /**
@@ -3158,6 +3352,11 @@ class CollectionRegistry {
3158
3352
  getRaw(path2) {
3159
3353
  const bySlug = this.rawCollectionsBySlug.get(path2);
3160
3354
  if (bySlug) return bySlug;
3355
+ if (path2.includes("-")) {
3356
+ const normalized = path2.replace(/-/g, "_");
3357
+ const byNormalized = this.rawCollectionsBySlug.get(normalized);
3358
+ if (byNormalized) return byNormalized;
3359
+ }
3161
3360
  return this.rawCollectionsByTableName.get(path2);
3162
3361
  }
3163
3362
  /**
@@ -3179,11 +3378,11 @@ class CollectionRegistry {
3179
3378
  }
3180
3379
  for (let i = 2; i < pathSegments.length; i += 2) {
3181
3380
  const relationKey = pathSegments[i];
3182
- if (!isPostgresCollection(currentCollection)) {
3183
- throw new Error(`Relation path navigation requires a PostgreSQL collection, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
3381
+ if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) {
3382
+ throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
3184
3383
  }
3185
3384
  const resolvedRelations = resolveCollectionRelations(currentCollection);
3186
- const relation = resolvedRelations[relationKey];
3385
+ const relation = findRelation(resolvedRelations, relationKey);
3187
3386
  if (!relation) {
3188
3387
  throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
3189
3388
  }
@@ -3193,10 +3392,16 @@ class CollectionRegistry {
3193
3392
  return currentCollection;
3194
3393
  }
3195
3394
  getCollections() {
3196
- return Array.from(this.collectionsByTableName.values());
3395
+ if (!this.cachedCollectionsList) {
3396
+ this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
3397
+ }
3398
+ return this.cachedCollectionsList;
3197
3399
  }
3198
3400
  getRawCollections() {
3199
- return Array.from(this.rawCollectionsByTableName.values());
3401
+ if (!this.cachedRawCollectionsList) {
3402
+ this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
3403
+ }
3404
+ return this.cachedRawCollectionsList;
3200
3405
  }
3201
3406
  /**
3202
3407
  * Resolves a multi-segment path like "products/123/locales" and returns
@@ -3252,24 +3457,62 @@ function convertWhereToFilter(where) {
3252
3457
  "lte": "<=",
3253
3458
  "in": "in",
3254
3459
  "nin": "not-in",
3460
+ "not-in": "not-in",
3255
3461
  "cs": "array-contains",
3256
- "csa": "array-contains-any"
3462
+ "csa": "array-contains-any",
3463
+ "==": "==",
3464
+ "!=": "!=",
3465
+ ">": ">",
3466
+ ">=": ">=",
3467
+ "<": "<",
3468
+ "<=": "<=",
3469
+ "array-contains": "array-contains",
3470
+ "array-contains-any": "array-contains-any"
3257
3471
  };
3258
3472
  const filter = {};
3259
3473
  for (const [field, rawValue] of Object.entries(where)) {
3260
- const dotIndex = rawValue.indexOf(".");
3261
- if (dotIndex === -1) continue;
3262
- const op = rawValue.substring(0, dotIndex);
3263
- let value = rawValue.substring(dotIndex + 1);
3264
- if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
3265
- value = value.slice(1, -1).split(",").map((v) => v.trim());
3474
+ if (rawValue === null) {
3475
+ filter[field] = ["==", null];
3476
+ continue;
3477
+ }
3478
+ if (typeof rawValue === "boolean") {
3479
+ filter[field] = ["==", rawValue];
3480
+ continue;
3481
+ }
3482
+ if (typeof rawValue === "number") {
3483
+ filter[field] = ["==", rawValue];
3484
+ continue;
3266
3485
  }
3267
- if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
3268
- value = Number(value);
3486
+ if (Array.isArray(rawValue) && rawValue.length === 2) {
3487
+ const [rawOp, val] = rawValue;
3488
+ const mappedOp = operatorMap[rawOp] ?? "==";
3489
+ filter[field] = [mappedOp, val];
3490
+ continue;
3269
3491
  }
3270
- const mappedOp = operatorMap[op];
3271
- if (mappedOp) {
3272
- filter[field] = [mappedOp, value];
3492
+ if (typeof rawValue === "string") {
3493
+ const dotIndex = rawValue.indexOf(".");
3494
+ if (dotIndex === -1) {
3495
+ filter[field] = ["==", rawValue];
3496
+ continue;
3497
+ }
3498
+ const op = rawValue.substring(0, dotIndex);
3499
+ let value = rawValue.substring(dotIndex + 1);
3500
+ if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
3501
+ value = value.slice(1, -1).split(",").map((v) => v.trim());
3502
+ }
3503
+ if (value === "null") {
3504
+ value = null;
3505
+ } else if (value === "true") {
3506
+ value = true;
3507
+ } else if (value === "false") {
3508
+ value = false;
3509
+ } else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
3510
+ value = Number(value);
3511
+ }
3512
+ const mappedOp = operatorMap[op];
3513
+ if (mappedOp) {
3514
+ filter[field] = [mappedOp, value];
3515
+ }
3273
3516
  }
3274
3517
  }
3275
3518
  return Object.keys(filter).length > 0 ? filter : void 0;
@@ -3288,7 +3531,7 @@ function createDriverAccessor(driver, slug) {
3288
3531
  const entities = await driver.fetchCollection({
3289
3532
  path: slug,
3290
3533
  limit: params?.limit,
3291
- startAfter: params?.offset,
3534
+ offset: params?.offset,
3292
3535
  filter: convertWhereToFilter(params?.where),
3293
3536
  orderBy: orderParsed?.[0],
3294
3537
  order: orderParsed?.[1],
@@ -3350,7 +3593,7 @@ function createDriverAccessor(driver, slug) {
3350
3593
  return driver.listenCollection({
3351
3594
  path: slug,
3352
3595
  limit: params?.limit,
3353
- startAfter: params?.offset,
3596
+ offset: params?.offset,
3354
3597
  filter: convertWhereToFilter(params?.where),
3355
3598
  orderBy: orderParsed?.[0],
3356
3599
  order: orderParsed?.[1],
@@ -3397,7 +3640,8 @@ function buildRebaseData(driver) {
3397
3640
  if (prop === "collection") return getAccessor;
3398
3641
  if (typeof prop === "symbol") return void 0;
3399
3642
  if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
3400
- return getAccessor(prop);
3643
+ const slug = toSnakeCase(prop);
3644
+ return getAccessor(slug);
3401
3645
  }
3402
3646
  });
3403
3647
  }
@@ -3455,7 +3699,7 @@ class DrizzleConditionBuilder {
3455
3699
  * Build relation-based conditions for different relation types
3456
3700
  */
3457
3701
  static buildRelationConditions(relation, parentEntityId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry) {
3458
- console.debug(`🔍 [buildRelationConditions] Building conditions for relation:`, {
3702
+ console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
3459
3703
  relationName: relation.relationName,
3460
3704
  cardinality: relation.cardinality,
3461
3705
  direction: relation.direction,
@@ -3467,7 +3711,7 @@ class DrizzleConditionBuilder {
3467
3711
  const joinConditions = [];
3468
3712
  const whereConditions = [];
3469
3713
  if (relation.joinPath && relation.joinPath.length > 0) {
3470
- console.debug(`🔍 [buildRelationConditions] Using joinPath logic`);
3714
+ console.debug("🔍 [buildRelationConditions] Using joinPath logic");
3471
3715
  const {
3472
3716
  joins,
3473
3717
  finalCondition
@@ -3475,37 +3719,37 @@ class DrizzleConditionBuilder {
3475
3719
  joinConditions.push(...joins);
3476
3720
  whereConditions.push(finalCondition);
3477
3721
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
3478
- console.debug(`🔍 [buildRelationConditions] Using owning many-to-many with explicit through`);
3722
+ console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
3479
3723
  const junctionResult = this.buildJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
3480
3724
  joinConditions.push(junctionResult.join);
3481
3725
  whereConditions.push(junctionResult.condition);
3482
3726
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
3483
- console.debug(`🔍 [buildRelationConditions] Using inverse many-to-many with explicit through`);
3727
+ console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
3484
3728
  const junctionResult = this.buildInverseJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
3485
3729
  joinConditions.push(junctionResult.join);
3486
3730
  whereConditions.push(junctionResult.condition);
3487
3731
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
3488
- console.debug(`🔍 [buildRelationConditions] Handling inverse many relationship without explicit through`);
3732
+ console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
3489
3733
  const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
3490
3734
  if (junctionInfo) {
3491
- console.debug(`🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions`);
3735
+ console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
3492
3736
  const junctionResult = this.buildInverseJunctionTableConditions(junctionInfo, targetIdColumn, parentEntityId, registry);
3493
3737
  joinConditions.push(junctionResult.join);
3494
3738
  whereConditions.push(junctionResult.condition);
3495
3739
  } else if (relation.foreignKeyOnTarget) {
3496
- console.debug(`🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target`);
3740
+ console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
3497
3741
  const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
3498
3742
  whereConditions.push(simpleCondition);
3499
3743
  } else {
3500
- console.error(`🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified`);
3744
+ console.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
3501
3745
  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.`);
3502
3746
  }
3503
3747
  } else {
3504
- console.debug(`🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR`);
3748
+ console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
3505
3749
  const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
3506
3750
  whereConditions.push(simpleCondition);
3507
3751
  }
3508
- console.debug(`🔍 [buildRelationConditions] Final result:`, {
3752
+ console.debug("🔍 [buildRelationConditions] Final result:", {
3509
3753
  joinConditionsCount: joinConditions.length,
3510
3754
  whereConditionsCount: whereConditions.length
3511
3755
  });
@@ -3756,7 +4000,8 @@ class DrizzleConditionBuilder {
3756
4000
  static buildSearchConditions(searchString, properties, table) {
3757
4001
  const searchConditions = [];
3758
4002
  for (const [key, prop] of Object.entries(properties)) {
3759
- if (prop.type === "string" && !prop.enum && prop.isId !== "uuid") {
4003
+ const p = prop;
4004
+ if (p.type === "string" && !p.enum && p.isId !== "uuid") {
3760
4005
  const fieldColumn = table[key];
3761
4006
  if (fieldColumn) {
3762
4007
  searchConditions.push(ilike(fieldColumn, `%${searchString}%`));
@@ -3919,29 +4164,29 @@ class DrizzleConditionBuilder {
3919
4164
  try {
3920
4165
  console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
3921
4166
  if (!relation.inverseRelationName) {
3922
- console.debug(`🔍 [findCorrespondingJunctionTable] No inverseRelationName specified`);
4167
+ console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
3923
4168
  return null;
3924
4169
  }
3925
4170
  const targetCollection = relation.target();
3926
4171
  console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
3927
4172
  const targetCollectionRelations = resolveCollectionRelations(targetCollection);
3928
- console.debug(`🔍 [findCorrespondingJunctionTable] Target collection relations:`, Object.keys(targetCollectionRelations));
4173
+ console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
3929
4174
  const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
3930
4175
  if (!correspondingRelation) {
3931
4176
  console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
3932
4177
  return null;
3933
4178
  }
3934
- console.debug(`🔍 [findCorrespondingJunctionTable] Found relation:`, {
4179
+ console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
3935
4180
  relationName: correspondingRelation.relationName,
3936
4181
  cardinality: correspondingRelation.cardinality,
3937
4182
  direction: correspondingRelation.direction,
3938
4183
  hasThrough: !!correspondingRelation.through
3939
4184
  });
3940
4185
  if (correspondingRelation.cardinality !== "many" || correspondingRelation.direction !== "owning" || !correspondingRelation.through) {
3941
- console.debug(`🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table`);
4186
+ console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
3942
4187
  return null;
3943
4188
  }
3944
- console.debug(`🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!`);
4189
+ console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
3945
4190
  const through = correspondingRelation.through;
3946
4191
  const result = {
3947
4192
  table: through.table,
@@ -3950,7 +4195,7 @@ class DrizzleConditionBuilder {
3950
4195
  targetColumn: through.sourceColumn
3951
4196
  // Swapped for inverse relation
3952
4197
  };
3953
- console.debug(`🔍 [findCorrespondingJunctionTable] Returning junction info:`, result);
4198
+ console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
3954
4199
  return result;
3955
4200
  } catch (error) {
3956
4201
  console.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}':`, error);
@@ -3959,10 +4204,19 @@ class DrizzleConditionBuilder {
3959
4204
  }
3960
4205
  }
3961
4206
  const PostgresConditionBuilder = DrizzleConditionBuilder;
4207
+ function getColumnMeta(col) {
4208
+ const raw = col;
4209
+ return {
4210
+ columnType: typeof raw.columnType === "string" ? raw.columnType : void 0,
4211
+ dataType: typeof raw.dataType === "string" ? raw.dataType : void 0,
4212
+ primary: typeof raw.primary === "boolean" ? raw.primary : void 0
4213
+ };
4214
+ }
3962
4215
  function getCollectionByPath(collectionPath, registry) {
3963
4216
  const collection = registry.getCollectionByPath(collectionPath);
3964
4217
  if (!collection) {
3965
- throw new Error(`Collection not found: ${collectionPath}`);
4218
+ const registered = registry.getCollections().map((c) => c.slug).join(", ");
4219
+ throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
3966
4220
  }
3967
4221
  return collection;
3968
4222
  }
@@ -3979,7 +4233,8 @@ function getPrimaryKeys(collection, registry) {
3979
4233
  if (collection.properties) {
3980
4234
  const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
3981
4235
  fieldName: key,
3982
- type: prop.type === "number" ? "number" : "string"
4236
+ type: prop.type === "number" ? "number" : "string",
4237
+ isUUID: prop.isId === "uuid"
3983
4238
  }));
3984
4239
  if (idProps.length > 0) {
3985
4240
  return idProps;
@@ -3989,19 +4244,25 @@ function getPrimaryKeys(collection, registry) {
3989
4244
  for (const [key, colRaw] of Object.entries(table)) {
3990
4245
  const col = colRaw;
3991
4246
  if (col && typeof col === "object" && "primary" in col && col.primary) {
3992
- const type = col.dataType === "number" || col.columnType === "PgSerial" || col.columnType === "PgInteger" ? "number" : "string";
4247
+ const meta = getColumnMeta(col);
4248
+ const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
4249
+ const isUUID = meta.columnType === "PgUUID";
3993
4250
  keys2.push({
3994
4251
  fieldName: key,
3995
- type
4252
+ type,
4253
+ isUUID
3996
4254
  });
3997
4255
  }
3998
4256
  }
3999
4257
  if (keys2.length === 0 && "id" in table) {
4000
4258
  const idCol = table["id"];
4001
- const type = idCol.dataType === "number" || idCol.columnType === "PgSerial" || idCol.columnType === "PgInteger" ? "number" : "string";
4259
+ const idMeta = getColumnMeta(idCol);
4260
+ const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
4261
+ const isUUID = idMeta.columnType === "PgUUID";
4002
4262
  keys2.push({
4003
4263
  fieldName: "id",
4004
- type
4264
+ type,
4265
+ isUUID
4005
4266
  });
4006
4267
  }
4007
4268
  return keys2;
@@ -4013,7 +4274,7 @@ function parseIdValues(idValue, primaryKeys) {
4013
4274
  }
4014
4275
  if (primaryKeys.length === 1) {
4015
4276
  const pk = primaryKeys[0];
4016
- if (pk.type === "number") {
4277
+ if (pk.type === "number" && !pk.isUUID) {
4017
4278
  const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
4018
4279
  if (isNaN(parsed)) {
4019
4280
  throw new Error(`Invalid numeric ID: ${idValue}`);
@@ -4031,7 +4292,7 @@ function parseIdValues(idValue, primaryKeys) {
4031
4292
  for (let i = 0; i < primaryKeys.length; i++) {
4032
4293
  const pk = primaryKeys[i];
4033
4294
  const val = parts[i];
4034
- if (pk.type === "number") {
4295
+ if (pk.type === "number" && !pk.isUUID) {
4035
4296
  const parsed = parseInt(val, 10);
4036
4297
  if (isNaN(parsed)) {
4037
4298
  throw new Error(`Invalid numeric ID component: ${val}`);
@@ -4090,28 +4351,37 @@ function sanitizeAndConvertDates(obj) {
4090
4351
  return obj;
4091
4352
  }
4092
4353
  function serializeDataToServer(entity, properties, collection, registry) {
4093
- if (!entity || !properties) return entity;
4354
+ if (!entity || !properties) return {
4355
+ scalarData: entity ?? {},
4356
+ inverseRelationUpdates: [],
4357
+ joinPathRelationUpdates: []
4358
+ };
4094
4359
  const result = {};
4095
4360
  const resolvedRelations = collection ? resolveCollectionRelations(collection) : {};
4096
4361
  const inverseRelationUpdates = [];
4097
4362
  const joinPathRelationUpdates = [];
4363
+ const foreignKeys = /* @__PURE__ */ new Set();
4364
+ Object.values(resolvedRelations).forEach((relation) => {
4365
+ if (relation.localKey) foreignKeys.add(relation.localKey);
4366
+ });
4098
4367
  for (const [key, value] of Object.entries(entity)) {
4099
4368
  const property = properties[key];
4369
+ const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
4100
4370
  if (!property) {
4101
- result[key] = value;
4371
+ result[key] = effectiveValue;
4102
4372
  continue;
4103
4373
  }
4104
4374
  if (property.type === "relation" && collection) {
4105
- const relation = resolvedRelations[key];
4375
+ const relation = findRelation(resolvedRelations, key);
4106
4376
  if (relation) {
4107
4377
  if (relation.direction === "owning" && relation.localKey) {
4108
- const serializedValue = serializePropertyToServer(value, property);
4109
- if (serializedValue !== null && serializedValue !== void 0) {
4378
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4379
+ if (serializedValue !== void 0) {
4110
4380
  result[relation.localKey] = serializedValue;
4111
4381
  }
4112
4382
  continue;
4113
4383
  } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
4114
- const serializedValue = serializePropertyToServer(value, property);
4384
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4115
4385
  const pks = getPrimaryKeys(collection, registry);
4116
4386
  inverseRelationUpdates.push({
4117
4387
  relationKey: key,
@@ -4121,7 +4391,7 @@ function serializeDataToServer(entity, properties, collection, registry) {
4121
4391
  });
4122
4392
  continue;
4123
4393
  } else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
4124
- const serializedValue = serializePropertyToServer(value, property);
4394
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4125
4395
  if (relation.cardinality === "one") {
4126
4396
  joinPathRelationUpdates.push({
4127
4397
  relationKey: key,
@@ -4139,7 +4409,7 @@ function serializeDataToServer(entity, properties, collection, registry) {
4139
4409
  }
4140
4410
  continue;
4141
4411
  } else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
4142
- const serializedValue = serializePropertyToServer(value, property);
4412
+ const serializedValue = serializePropertyToServer(effectiveValue, property);
4143
4413
  joinPathRelationUpdates.push({
4144
4414
  relationKey: key,
4145
4415
  relation,
@@ -4149,15 +4419,13 @@ function serializeDataToServer(entity, properties, collection, registry) {
4149
4419
  }
4150
4420
  }
4151
4421
  }
4152
- result[key] = serializePropertyToServer(value, property);
4422
+ result[key] = serializePropertyToServer(effectiveValue, property);
4153
4423
  }
4154
- if (inverseRelationUpdates.length > 0) {
4155
- result.__inverseRelationUpdates = inverseRelationUpdates;
4156
- }
4157
- if (joinPathRelationUpdates.length > 0) {
4158
- result.__joinPathRelationUpdates = joinPathRelationUpdates;
4159
- }
4160
- return result;
4424
+ return {
4425
+ scalarData: result,
4426
+ inverseRelationUpdates,
4427
+ joinPathRelationUpdates
4428
+ };
4161
4429
  }
4162
4430
  function serializePropertyToServer(value, property) {
4163
4431
  if (value === null || value === void 0) {
@@ -4171,10 +4439,28 @@ function serializePropertyToServer(value, property) {
4171
4439
  } else if (typeof value === "object" && value !== null && "id" in value) {
4172
4440
  return value.id;
4173
4441
  }
4442
+ if (value === "") return null;
4174
4443
  return value;
4175
4444
  case "array":
4176
- if (Array.isArray(value) && property.of) {
4177
- return value.map((item) => serializePropertyToServer(item, property.of));
4445
+ if (Array.isArray(value)) {
4446
+ if (property.of) {
4447
+ return value.map((item) => serializePropertyToServer(item, property.of));
4448
+ } else if (property.oneOf) {
4449
+ const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
4450
+ const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
4451
+ return value.map((e) => {
4452
+ if (e === null) return null;
4453
+ if (typeof e !== "object") return e;
4454
+ const rec = e;
4455
+ const type = rec[typeField];
4456
+ const childProperty = property.oneOf?.properties[type];
4457
+ if (!type || !childProperty) return e;
4458
+ return {
4459
+ [typeField]: type,
4460
+ [valueField]: serializePropertyToServer(rec[valueField], childProperty)
4461
+ };
4462
+ });
4463
+ }
4178
4464
  }
4179
4465
  return value;
4180
4466
  case "map":
@@ -4198,38 +4484,20 @@ function serializePropertyToServer(value, property) {
4198
4484
  async function parseDataFromServer(data, collection, db, registry) {
4199
4485
  const properties = collection.properties;
4200
4486
  if (!data || !properties) return data;
4201
- const result = {};
4202
4487
  const resolvedRelations = resolveCollectionRelations(collection);
4203
- const internalFKColumns = /* @__PURE__ */ new Set();
4204
- Object.values(resolvedRelations).forEach((relation) => {
4205
- if (relation.localKey && !properties[relation.localKey]) {
4206
- internalFKColumns.add(relation.localKey);
4207
- }
4488
+ const result = normalizeScalarValues(data, properties, collection, resolvedRelations, {
4489
+ skipRelations: false
4208
4490
  });
4209
- for (const [key, value] of Object.entries(data)) {
4210
- if (internalFKColumns.has(key)) {
4211
- continue;
4212
- }
4213
- const property = properties[key];
4214
- if (!property) {
4215
- continue;
4216
- }
4217
- result[key] = parsePropertyFromServer(value, property, collection, key);
4218
- }
4219
4491
  for (const [propKey, property] of Object.entries(properties)) {
4220
4492
  if (property.type === "relation" && !(propKey in result)) {
4221
- const relation = resolvedRelations[propKey];
4493
+ const relation = findRelation(resolvedRelations, propKey);
4222
4494
  if (relation) {
4223
4495
  if (relation.direction === "owning" && relation.localKey && relation.localKey in data) {
4224
4496
  const fkValue = data[relation.localKey];
4225
4497
  if (fkValue !== null && fkValue !== void 0) {
4226
4498
  try {
4227
4499
  const targetCollection = relation.target();
4228
- result[propKey] = {
4229
- id: fkValue.toString(),
4230
- path: targetCollection.slug,
4231
- __type: "relation"
4232
- };
4500
+ result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
4233
4501
  } catch (e) {
4234
4502
  console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
4235
4503
  }
@@ -4248,18 +4516,10 @@ async function parseDataFromServer(data, collection, db, registry) {
4248
4516
  if (relation.cardinality === "one") {
4249
4517
  const targetPks = getPrimaryKeys(targetCollection, registry);
4250
4518
  const relatedEntity = relatedEntities[0];
4251
- result[propKey] = {
4252
- id: buildCompositeId(relatedEntity, targetPks),
4253
- path: targetCollection.slug,
4254
- __type: "relation"
4255
- };
4519
+ result[propKey] = createRelationRef(buildCompositeId(relatedEntity, targetPks), targetCollection.slug);
4256
4520
  } else {
4257
4521
  const targetPks = getPrimaryKeys(targetCollection, registry);
4258
- result[propKey] = relatedEntities.map((entity) => ({
4259
- id: buildCompositeId(entity, targetPks),
4260
- path: targetCollection.slug,
4261
- __type: "relation"
4262
- }));
4522
+ result[propKey] = relatedEntities.map((entity) => createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug));
4263
4523
  }
4264
4524
  }
4265
4525
  }
@@ -4320,19 +4580,11 @@ async function parseDataFromServer(data, collection, db, registry) {
4320
4580
  if (relation.cardinality === "one") {
4321
4581
  const joinResult = joinResults[0];
4322
4582
  const targetEntity = joinResult[targetTableName] || joinResult;
4323
- result[propKey] = {
4324
- id: buildCompositeId(targetEntity, targetPks),
4325
- path: targetCollection.slug,
4326
- __type: "relation"
4327
- };
4583
+ result[propKey] = createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
4328
4584
  } else {
4329
4585
  result[propKey] = joinResults.map((joinResult) => {
4330
4586
  const targetEntity = joinResult[targetTableName] || joinResult;
4331
- return {
4332
- id: buildCompositeId(targetEntity, targetPks),
4333
- path: targetCollection.slug,
4334
- __type: "relation"
4335
- };
4587
+ return createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
4336
4588
  });
4337
4589
  }
4338
4590
  }
@@ -4356,7 +4608,7 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4356
4608
  let relationDef = property.relation;
4357
4609
  if (!relationDef && propertyKey) {
4358
4610
  const resolvedRelations = resolveCollectionRelations(collection);
4359
- relationDef = resolvedRelations[propertyKey];
4611
+ relationDef = findRelation(resolvedRelations, propertyKey);
4360
4612
  }
4361
4613
  if (!relationDef) {
4362
4614
  relationDef = collection.relations?.find((rel) => rel.relationName === property.relationName);
@@ -4367,11 +4619,7 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4367
4619
  }
4368
4620
  try {
4369
4621
  const targetCollection = relationDef.target();
4370
- return {
4371
- id: value.toString(),
4372
- path: targetCollection.slug,
4373
- __type: "relation"
4374
- };
4622
+ return createRelationRef(value.toString(), targetCollection.slug);
4375
4623
  } catch (e) {
4376
4624
  console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
4377
4625
  return value;
@@ -4379,8 +4627,25 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4379
4627
  }
4380
4628
  return value;
4381
4629
  case "array":
4382
- if (Array.isArray(value) && property.of) {
4383
- return value.map((item) => parsePropertyFromServer(item, property.of, collection));
4630
+ if (Array.isArray(value)) {
4631
+ if (property.of) {
4632
+ return value.map((item) => parsePropertyFromServer(item, property.of, collection));
4633
+ } else if (property.oneOf) {
4634
+ const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
4635
+ const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
4636
+ return value.map((e) => {
4637
+ if (e === null) return null;
4638
+ if (typeof e !== "object") return e;
4639
+ const rec = e;
4640
+ const type = rec[typeField];
4641
+ const childProperty = property.oneOf?.properties[type];
4642
+ if (!type || !childProperty) return e;
4643
+ return {
4644
+ [typeField]: type,
4645
+ [valueField]: parsePropertyFromServer(rec[valueField], childProperty, collection)
4646
+ };
4647
+ });
4648
+ }
4384
4649
  }
4385
4650
  return value;
4386
4651
  case "map":
@@ -4425,11 +4690,8 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4425
4690
  return value;
4426
4691
  }
4427
4692
  }
4428
- function normalizeDbValues(data, collection) {
4429
- const properties = collection.properties;
4430
- if (!data || !properties) return data;
4693
+ function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
4431
4694
  const result = {};
4432
- const resolvedRelations = resolveCollectionRelations(collection);
4433
4695
  const internalFKColumns = /* @__PURE__ */ new Set();
4434
4696
  Object.values(resolvedRelations).forEach((relation) => {
4435
4697
  if (relation.localKey && !properties[relation.localKey]) {
@@ -4437,14 +4699,25 @@ function normalizeDbValues(data, collection) {
4437
4699
  }
4438
4700
  });
4439
4701
  for (const [key, value] of Object.entries(data)) {
4440
- if (internalFKColumns.has(key)) continue;
4702
+ if (internalFKColumns.has(key)) {
4703
+ result[key] = value === null ? null : typeof value === "number" ? value : String(value);
4704
+ continue;
4705
+ }
4441
4706
  const property = properties[key];
4442
4707
  if (!property) continue;
4443
- if (property.type === "relation") continue;
4708
+ if (options.skipRelations && property.type === "relation") continue;
4444
4709
  result[key] = parsePropertyFromServer(value, property, collection, key);
4445
4710
  }
4446
4711
  return result;
4447
4712
  }
4713
+ function normalizeDbValues(data, collection) {
4714
+ const properties = collection.properties;
4715
+ if (!data || !properties) return data;
4716
+ const resolvedRelations = resolveCollectionRelations(collection);
4717
+ return normalizeScalarValues(data, properties, collection, resolvedRelations, {
4718
+ skipRelations: true
4719
+ });
4720
+ }
4448
4721
  class RelationService {
4449
4722
  constructor(db, registry) {
4450
4723
  this.db = db;
@@ -4456,9 +4729,10 @@ class RelationService {
4456
4729
  async fetchRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
4457
4730
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
4458
4731
  const resolvedRelations = resolveCollectionRelations(parentCollection);
4459
- const relation = resolvedRelations[relationKey];
4732
+ const relation = findRelation(resolvedRelations, relationKey);
4460
4733
  if (!relation) {
4461
- throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
4734
+ const available = Object.keys(resolvedRelations).join(", ") || "(none)";
4735
+ throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
4462
4736
  }
4463
4737
  return this.fetchEntitiesUsingJoins(parentCollection, parentEntityId, relation, options);
4464
4738
  }
@@ -4558,8 +4832,11 @@ class RelationService {
4558
4832
  async countRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
4559
4833
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
4560
4834
  const resolvedRelations = resolveCollectionRelations(parentCollection);
4561
- const relation = resolvedRelations[relationKey];
4562
- if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
4835
+ const relation = findRelation(resolvedRelations, relationKey);
4836
+ if (!relation) {
4837
+ const available = Object.keys(resolvedRelations).join(", ") || "(none)";
4838
+ throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
4839
+ }
4563
4840
  const targetCollection = relation.target();
4564
4841
  const targetTable = getTableForCollection(targetCollection, this.registry);
4565
4842
  const targetPks = getPrimaryKeys(targetCollection, this.registry);
@@ -4624,16 +4901,59 @@ class RelationService {
4624
4901
  const results2 = await query2;
4625
4902
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
4626
4903
  const resultMap2 = /* @__PURE__ */ new Map();
4627
- results2.forEach((row) => {
4904
+ for (const row of results2) {
4628
4905
  const parentEntity = row[getTableName(parentCollection)] || row;
4629
4906
  const targetEntity = row[targetTableName] || row;
4630
4907
  const parentId = parentEntity[parentIdInfo.fieldName];
4631
- resultMap2.set(parentId, {
4908
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4909
+ resultMap2.set(String(parentId), {
4632
4910
  id: String(targetEntity[targetIdInfo.fieldName]),
4633
4911
  path: targetCollection.slug,
4634
- values: targetEntity
4912
+ values: parsedValues
4635
4913
  });
4636
- });
4914
+ }
4915
+ return resultMap2;
4916
+ }
4917
+ if (relation.direction === "owning" && relation.localKey) {
4918
+ const localKeyCol = parentTable[relation.localKey];
4919
+ if (!localKeyCol) {
4920
+ throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
4921
+ }
4922
+ const fkRows = await this.db.select({
4923
+ parentId: parentIdCol,
4924
+ fkValue: localKeyCol
4925
+ }).from(parentTable).where(inArray(parentIdCol, parsedParentIds));
4926
+ const parentToFk = /* @__PURE__ */ new Map();
4927
+ const uniqueFkValues = [];
4928
+ const seenFks = /* @__PURE__ */ new Set();
4929
+ for (const row of fkRows) {
4930
+ if (row.fkValue == null) continue;
4931
+ parentToFk.set(String(row.parentId), row.fkValue);
4932
+ const fkStr = String(row.fkValue);
4933
+ if (!seenFks.has(fkStr)) {
4934
+ seenFks.add(fkStr);
4935
+ uniqueFkValues.push(row.fkValue);
4936
+ }
4937
+ }
4938
+ if (uniqueFkValues.length === 0) return /* @__PURE__ */ new Map();
4939
+ const targetResults = await this.db.select().from(targetTable).where(inArray(targetIdField, uniqueFkValues));
4940
+ const targetById = /* @__PURE__ */ new Map();
4941
+ for (const row of targetResults) {
4942
+ const tid = String(row[targetIdInfo.fieldName]);
4943
+ targetById.set(tid, row);
4944
+ }
4945
+ const resultMap2 = /* @__PURE__ */ new Map();
4946
+ for (const [parentIdStr, fkValue] of parentToFk) {
4947
+ const targetEntity = targetById.get(String(fkValue));
4948
+ if (targetEntity) {
4949
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4950
+ resultMap2.set(parentIdStr, {
4951
+ id: String(targetEntity[targetIdInfo.fieldName]),
4952
+ path: targetCollection.slug,
4953
+ values: parsedValues
4954
+ });
4955
+ }
4956
+ }
4637
4957
  return resultMap2;
4638
4958
  }
4639
4959
  let query = this.db.select().from(targetTable).$dynamic();
@@ -4651,7 +4971,8 @@ class RelationService {
4651
4971
  );
4652
4972
  const results = await query;
4653
4973
  const resultMap = /* @__PURE__ */ new Map();
4654
- results.forEach((row) => {
4974
+ const parentIdSet = new Set(parsedParentIds.map(String));
4975
+ for (const row of results) {
4655
4976
  const targetEntity = row[getTableName(targetCollection)] || row;
4656
4977
  let parentId;
4657
4978
  if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
@@ -4659,22 +4980,133 @@ class RelationService {
4659
4980
  } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
4660
4981
  const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
4661
4982
  parentId = targetEntity[inferredForeignKeyName];
4662
- } else if (relation.direction === "owning" && relation.localKey) {
4663
- for (const parsedParentId of parsedParentIds) {
4664
- if (!resultMap.has(parsedParentId)) {
4665
- parentId = parsedParentId;
4666
- break;
4667
- }
4668
- }
4669
4983
  }
4670
- if (parentId !== void 0 && parsedParentIds.includes(parentId)) {
4671
- resultMap.set(parentId, {
4984
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
4985
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
4986
+ resultMap.set(String(parentId), {
4672
4987
  id: String(targetEntity[targetIdInfo.fieldName]),
4673
4988
  path: targetCollection.slug,
4674
- values: targetEntity
4989
+ values: parsedValues
4675
4990
  });
4676
4991
  }
4677
- });
4992
+ }
4993
+ return resultMap;
4994
+ }
4995
+ /**
4996
+ * Batch fetch many-cardinality related entities for multiple parent entities.
4997
+ * Returns a Map<parentId, Entity[]> instead of Map<parentId, Entity>.
4998
+ * Uses a single SQL query with IN clause to avoid N+1.
4999
+ */
5000
+ async batchFetchRelatedEntitiesMany(parentCollectionPath, parentEntityIds, _relationKey, relation) {
5001
+ if (parentEntityIds.length === 0) return /* @__PURE__ */ new Map();
5002
+ const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
5003
+ const targetCollection = relation.target();
5004
+ const targetTable = getTableForCollection(targetCollection, this.registry);
5005
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
5006
+ const targetIdInfo = targetPks[0];
5007
+ const targetIdField = targetTable[targetIdInfo.fieldName];
5008
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5009
+ const parentIdInfo = parentPks[0];
5010
+ const parentTable = this.registry.getTable(getTableName(parentCollection));
5011
+ if (!parentTable) throw new Error("Parent table not found");
5012
+ const parentIdCol = parentTable[parentIdInfo.fieldName];
5013
+ const parsedParentIds = parentEntityIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
5014
+ if (relation.joinPath && relation.joinPath.length > 0) {
5015
+ let query2 = this.db.select().from(parentTable).$dynamic();
5016
+ let currentTable = parentTable;
5017
+ for (const join of relation.joinPath) {
5018
+ const joinTable = this.registry.getTable(join.table);
5019
+ if (!joinTable) throw new Error(`Join table not found: ${join.table}`);
5020
+ const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
5021
+ const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
5022
+ const fromColName = fromColumn.split(".").pop();
5023
+ const toColName = toColumn.split(".").pop();
5024
+ const fromCol = currentTable[fromColName];
5025
+ const toCol = joinTable[toColName];
5026
+ if (!fromCol || !toCol) throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
5027
+ query2 = query2.innerJoin(joinTable, eq$3(fromCol, toCol));
5028
+ currentTable = joinTable;
5029
+ }
5030
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5031
+ query2 = query2.where(inArray(parentIdField, parsedParentIds));
5032
+ const results2 = await query2;
5033
+ const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
5034
+ const resultMap2 = /* @__PURE__ */ new Map();
5035
+ for (const row of results2) {
5036
+ const parentEntity = row[getTableName(parentCollection)] || row;
5037
+ const targetEntity = row[targetTableName] || row;
5038
+ const parentId = String(parentEntity[parentIdInfo.fieldName]);
5039
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
5040
+ const arr = resultMap2.get(parentId) || [];
5041
+ arr.push({
5042
+ id: String(targetEntity[targetIdInfo.fieldName]),
5043
+ path: targetCollection.slug,
5044
+ values: parsedValues
5045
+ });
5046
+ resultMap2.set(parentId, arr);
5047
+ }
5048
+ return resultMap2;
5049
+ }
5050
+ if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
5051
+ const junctionTable = this.registry.getTable(relation.through.table);
5052
+ if (!junctionTable) {
5053
+ console.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
5054
+ return /* @__PURE__ */ new Map();
5055
+ }
5056
+ const sourceJunctionCol = junctionTable[relation.through.sourceColumn];
5057
+ const targetJunctionCol = junctionTable[relation.through.targetColumn];
5058
+ if (!sourceJunctionCol || !targetJunctionCol) {
5059
+ console.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
5060
+ return /* @__PURE__ */ new Map();
5061
+ }
5062
+ const query2 = this.db.select().from(junctionTable).innerJoin(targetTable, eq$3(targetJunctionCol, targetIdField)).where(inArray(sourceJunctionCol, parsedParentIds));
5063
+ const results2 = await query2;
5064
+ const resultMap2 = /* @__PURE__ */ new Map();
5065
+ const targetTableName = getTableName(targetCollection);
5066
+ for (const row of results2) {
5067
+ const junctionData = row[relation.through.table] || row;
5068
+ const targetData = row[targetTableName] || row;
5069
+ const parentId = String(junctionData[relation.through.sourceColumn]);
5070
+ const parsedValues = await parseDataFromServer(targetData, targetCollection);
5071
+ const arr = resultMap2.get(parentId) || [];
5072
+ arr.push({
5073
+ id: String(targetData[targetIdInfo.fieldName]),
5074
+ path: targetCollection.slug,
5075
+ values: parsedValues
5076
+ });
5077
+ resultMap2.set(parentId, arr);
5078
+ }
5079
+ return resultMap2;
5080
+ }
5081
+ let query = this.db.select().from(targetTable).$dynamic();
5082
+ query = DrizzleConditionBuilder.buildRelationQuery(query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
5083
+ const results = await query;
5084
+ const resultMap = /* @__PURE__ */ new Map();
5085
+ const parentIdSet = new Set(parsedParentIds.map(String));
5086
+ for (const row of results) {
5087
+ const targetEntity = row[getTableName(targetCollection)] || row;
5088
+ let parentId;
5089
+ if (relation.through && relation.direction === "inverse") {
5090
+ const junctionData = row[relation.through.table] || row;
5091
+ parentId = junctionData[relation.through.targetColumn];
5092
+ } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
5093
+ parentId = targetEntity[relation.foreignKeyOnTarget];
5094
+ } else if (relation.direction === "inverse" && relation.inverseRelationName) {
5095
+ const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
5096
+ parentId = targetEntity[inferredForeignKeyName];
5097
+ }
5098
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
5099
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
5100
+ const key = String(parentId);
5101
+ const arr = resultMap.get(key) || [];
5102
+ arr.push({
5103
+ id: String(targetEntity[targetIdInfo.fieldName]),
5104
+ path: targetCollection.slug,
5105
+ values: parsedValues
5106
+ });
5107
+ resultMap.set(key, arr);
5108
+ }
5109
+ }
4678
5110
  return resultMap;
4679
5111
  }
4680
5112
  /**
@@ -4683,7 +5115,7 @@ class RelationService {
4683
5115
  async updateRelationsUsingJoins(tx, collection, entityId, relationValues) {
4684
5116
  const resolvedRelations = resolveCollectionRelations(collection);
4685
5117
  for (const [key, value] of Object.entries(relationValues)) {
4686
- const relation = resolvedRelations[key];
5118
+ const relation = findRelation(resolvedRelations, key);
4687
5119
  if (!relation || relation.cardinality !== "many") continue;
4688
5120
  const targetEntityIds = value && Array.isArray(value) ? value.map((rel) => rel.id) : [];
4689
5121
  const targetCollection = relation.target();
@@ -4767,6 +5199,8 @@ class RelationService {
4767
5199
  await tx.insert(junctionTable).values(newLinks);
4768
5200
  }
4769
5201
  }
5202
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
5203
+ console.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
4770
5204
  } else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
4771
5205
  const targetTable = getTableForCollection(targetCollection, this.registry);
4772
5206
  const targetPks = getPrimaryKeys(targetCollection, this.registry);
@@ -5144,6 +5578,20 @@ class EntityFetchService {
5144
5578
  // =============================================================
5145
5579
  // DRIZZLE QUERY HELPERS
5146
5580
  // =============================================================
5581
+ /**
5582
+ * Resolves the correct Drizzle column for sorting.
5583
+ * Automatically maps owning relation property keys to their underlying foreign key column.
5584
+ */
5585
+ resolveOrderByField(table, orderBy, collection) {
5586
+ let orderByField = table[orderBy];
5587
+ if (!orderByField && collection) {
5588
+ const property = collection.properties[orderBy];
5589
+ if (property && property.type === "relation" && "relation" in property && property.relation?.direction === "owning") {
5590
+ orderByField = table[`${orderBy}_id`];
5591
+ }
5592
+ }
5593
+ return orderByField;
5594
+ }
5147
5595
  /**
5148
5596
  * Build the `with` config for Drizzle's relational query API.
5149
5597
  * Converts collection relations to a Drizzle-compatible `with` object.
@@ -5156,12 +5604,10 @@ class EntityFetchService {
5156
5604
  */
5157
5605
  buildWithConfig(collection, include) {
5158
5606
  const resolvedRelations = resolveCollectionRelations(collection);
5159
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5160
5607
  const withConfig = {};
5161
5608
  const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
5162
5609
  for (const [key, relation] of Object.entries(resolvedRelations)) {
5163
5610
  if (!shouldInclude(key)) continue;
5164
- if (!include && !propertyKeys.has(key)) continue;
5165
5611
  const drizzleRelName = relation.relationName || key;
5166
5612
  if (relation.joinPath && relation.joinPath.length > 0) {
5167
5613
  continue;
@@ -5211,10 +5657,8 @@ class EntityFetchService {
5211
5657
  */
5212
5658
  drizzleResultToEntity(row, collection, collectionPath, idInfo, databaseId, idInfoArray) {
5213
5659
  const resolvedRelations = resolveCollectionRelations(collection);
5214
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5215
5660
  const normalizedValues = normalizeDbValues(row, collection);
5216
5661
  for (const [key, relation] of Object.entries(resolvedRelations)) {
5217
- if (!propertyKeys.has(key)) continue;
5218
5662
  const drizzleRelName = relation.relationName || key;
5219
5663
  const relData = row[drizzleRelName];
5220
5664
  if (relData === void 0 || relData === null) continue;
@@ -5233,17 +5677,12 @@ class EntityFetchService {
5233
5677
  }
5234
5678
  const relId = String(targetEntity[targetIdField] ?? targetEntity.id ?? targetEntity[Object.keys(targetEntity)[0]]);
5235
5679
  const targetValues = normalizeDbValues(targetEntity, targetCollection);
5236
- return {
5680
+ return createRelationRefWithData(relId, targetPath, {
5237
5681
  id: relId,
5238
5682
  path: targetPath,
5239
- __type: "relation",
5240
- data: {
5241
- id: relId,
5242
- path: targetPath,
5243
- values: targetValues,
5244
- databaseId
5245
- }
5246
- };
5683
+ values: targetValues,
5684
+ databaseId
5685
+ });
5247
5686
  });
5248
5687
  } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
5249
5688
  const targetCollection = relation.target();
@@ -5253,17 +5692,12 @@ class EntityFetchService {
5253
5692
  const relObj = relData;
5254
5693
  const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
5255
5694
  const targetValues = normalizeDbValues(relObj, targetCollection);
5256
- normalizedValues[key] = {
5695
+ normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
5257
5696
  id: relId,
5258
5697
  path: targetPath,
5259
- __type: "relation",
5260
- data: {
5261
- id: relId,
5262
- path: targetPath,
5263
- values: targetValues,
5264
- databaseId
5265
- }
5266
- };
5698
+ values: targetValues,
5699
+ databaseId
5700
+ });
5267
5701
  }
5268
5702
  }
5269
5703
  return {
@@ -5280,27 +5714,16 @@ class EntityFetchService {
5280
5714
  */
5281
5715
  async resolveJoinPathRelations(entity, collection, collectionPath, parsedId, databaseId) {
5282
5716
  const resolvedRelations = resolveCollectionRelations(collection);
5283
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5284
- const promises2 = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
5717
+ const promises2 = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
5285
5718
  try {
5286
5719
  const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {
5287
5720
  limit: relation.cardinality === "one" ? 1 : void 0
5288
5721
  });
5289
5722
  if (relation.cardinality === "one" && relatedEntities.length > 0) {
5290
5723
  const e = relatedEntities[0];
5291
- entity.values[key] = {
5292
- id: e.id,
5293
- path: e.path,
5294
- __type: "relation",
5295
- data: e
5296
- };
5724
+ entity.values[key] = createRelationRefWithData(e.id, e.path, e);
5297
5725
  } else if (relation.cardinality === "many") {
5298
- entity.values[key] = relatedEntities.map((e) => ({
5299
- id: e.id,
5300
- path: e.path,
5301
- __type: "relation",
5302
- data: e
5303
- }));
5726
+ entity.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
5304
5727
  }
5305
5728
  } catch (e) {
5306
5729
  console.warn(`Could not resolve joinPath relation '${key}':`, e);
@@ -5315,8 +5738,7 @@ class EntityFetchService {
5315
5738
  async resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, databaseId) {
5316
5739
  if (entities.length === 0) return;
5317
5740
  const resolvedRelations = resolveCollectionRelations(collection);
5318
- const propertyKeys = new Set(Object.keys(collection.properties || {}));
5319
- const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0);
5741
+ const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
5320
5742
  if (joinPathRelations.length === 0) return;
5321
5743
  for (const [key, relation] of joinPathRelations) {
5322
5744
  try {
@@ -5328,15 +5750,10 @@ class EntityFetchService {
5328
5750
  for (const entity of entities) {
5329
5751
  const parsed = parseIdValues(entity.id, [idInfo]);
5330
5752
  const entityId = parsed[idInfo.fieldName];
5331
- const relatedEntity = resultMap.get(entityId);
5753
+ const relatedEntity = resultMap.get(String(entityId));
5332
5754
  if (relatedEntity) {
5333
5755
  if (relation.cardinality === "one") {
5334
- entity.values[key] = {
5335
- id: relatedEntity.id,
5336
- path: relatedEntity.path,
5337
- __type: "relation",
5338
- data: relatedEntity
5339
- };
5756
+ entity.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
5340
5757
  }
5341
5758
  }
5342
5759
  }
@@ -5346,22 +5763,72 @@ class EntityFetchService {
5346
5763
  }
5347
5764
  }
5348
5765
  /**
5349
- * Convert a db.query result row to a flat REST-style object with populated relations.
5766
+ * Resolves joinPath relations for raw REST rows and directly injects them.
5767
+ * Uses RelationService to query the database and maps results back to the flattened objects.
5350
5768
  */
5351
- drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
5352
- const flat = {
5353
- id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
5354
- };
5769
+ async resolveJoinPathRelationsBatchRest(rows, collection, collectionPath, idInfoArray, include) {
5770
+ if (rows.length === 0) return;
5355
5771
  const resolvedRelations = resolveCollectionRelations(collection);
5356
- for (const [k, v] of Object.entries(row)) {
5357
- if (k === idInfo.fieldName) continue;
5358
- const relation = resolvedRelations[k];
5359
- if (Array.isArray(v) && relation) {
5360
- flat[k] = v.map((item) => {
5361
- if (this.isJunctionRelation(relation, collection)) {
5362
- const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
5363
- if (nestedKey) {
5364
- const nested = item[nestedKey];
5772
+ const propertyKeys = new Set(Object.keys(collection.properties || {}));
5773
+ const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
5774
+ const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
5775
+ if (joinPathRelations.length === 0) return;
5776
+ const idInfo = idInfoArray[0];
5777
+ for (const [key, relation] of joinPathRelations) {
5778
+ try {
5779
+ const entityIds = rows.map((r) => {
5780
+ const parsed = parseIdValues(String(r.id), idInfoArray);
5781
+ return parsed[idInfo.fieldName];
5782
+ });
5783
+ if (relation.cardinality === "one") {
5784
+ const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5785
+ for (const row of rows) {
5786
+ const parsed = parseIdValues(String(row.id), idInfoArray);
5787
+ const entityId = parsed[idInfo.fieldName];
5788
+ const relatedEntity = resultMap.get(String(entityId));
5789
+ if (relatedEntity) {
5790
+ row[key] = {
5791
+ id: relatedEntity.id,
5792
+ ...relatedEntity.values
5793
+ };
5794
+ } else {
5795
+ row[key] = null;
5796
+ }
5797
+ }
5798
+ } else if (relation.cardinality === "many") {
5799
+ const resultMap = await this.batchFetchManyRelatedEntities(collectionPath, entityIds, key);
5800
+ for (const row of rows) {
5801
+ const parsed = parseIdValues(String(row.id), idInfoArray);
5802
+ const entityId = parsed[idInfo.fieldName];
5803
+ const relatedList = resultMap.get(String(entityId)) || [];
5804
+ row[key] = relatedList.map((e) => ({
5805
+ id: e.id,
5806
+ ...e.values
5807
+ }));
5808
+ }
5809
+ }
5810
+ } catch (e) {
5811
+ console.warn(`Could not batch resolve joinPath relation '${key}' for REST:`, e);
5812
+ }
5813
+ }
5814
+ }
5815
+ /**
5816
+ * Convert a db.query result row to a flat REST-style object with populated relations.
5817
+ */
5818
+ drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
5819
+ const flat = {
5820
+ id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
5821
+ };
5822
+ const resolvedRelations = resolveCollectionRelations(collection);
5823
+ for (const [k, v] of Object.entries(row)) {
5824
+ if (k === idInfo.fieldName) continue;
5825
+ const relation = findRelation(resolvedRelations, k);
5826
+ if (Array.isArray(v) && relation) {
5827
+ flat[k] = v.map((item) => {
5828
+ if (this.isJunctionRelation(relation, collection)) {
5829
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
5830
+ if (nestedKey) {
5831
+ const nested = item[nestedKey];
5365
5832
  return {
5366
5833
  id: String(nested.id ?? nested[Object.keys(nested)[0]]),
5367
5834
  ...nested
@@ -5407,7 +5874,7 @@ class EntityFetchService {
5407
5874
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5408
5875
  }
5409
5876
  if (options.startAfter) {
5410
- const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
5877
+ const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
5411
5878
  if (cursorConditions.length > 0) allConditions.push(...cursorConditions);
5412
5879
  }
5413
5880
  if (allConditions.length > 0) {
@@ -5415,7 +5882,8 @@ class EntityFetchService {
5415
5882
  }
5416
5883
  const orderExpressions = [];
5417
5884
  if (options.orderBy) {
5418
- const orderByField = table[options.orderBy];
5885
+ const collection = getCollectionByPath(collectionPath, this.registry);
5886
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5419
5887
  if (orderByField) {
5420
5888
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
5421
5889
  }
@@ -5426,16 +5894,18 @@ class EntityFetchService {
5426
5894
  }
5427
5895
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5428
5896
  if (limitValue) queryOpts.limit = limitValue;
5897
+ if (options.offset && options.offset > 0) queryOpts.offset = options.offset;
5429
5898
  return queryOpts;
5430
5899
  }
5431
5900
  /**
5432
5901
  * Extract cursor pagination conditions from startAfter options.
5433
5902
  */
5434
- buildCursorConditions(table, idField, idInfo, options) {
5903
+ buildCursorConditions(table, idField, idInfo, options, collectionPath) {
5435
5904
  if (!options.startAfter) return [];
5436
5905
  const cursor = options.startAfter;
5437
5906
  if (options.orderBy) {
5438
- const orderByField = table[options.orderBy];
5907
+ const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
5908
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5439
5909
  if (orderByField) {
5440
5910
  const startAfterOrderValue = cursor.values?.[options.orderBy] ?? cursor[options.orderBy];
5441
5911
  const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
@@ -5497,11 +5967,7 @@ class EntityFetchService {
5497
5967
  const relationPromises = Object.entries(resolvedRelations).filter(([key]) => propertyKeys.has(key)).map(async ([key, relation]) => {
5498
5968
  if (relation.cardinality === "many") {
5499
5969
  const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {});
5500
- values[key] = relatedEntities.map((e) => ({
5501
- id: e.id,
5502
- path: e.path,
5503
- __type: "relation"
5504
- }));
5970
+ values[key] = relatedEntities.map((e) => createRelationRef(e.id, e.path));
5505
5971
  } else if (relation.cardinality === "one") {
5506
5972
  if (values[key] == null) {
5507
5973
  try {
@@ -5510,11 +5976,7 @@ class EntityFetchService {
5510
5976
  });
5511
5977
  if (relatedEntities.length > 0) {
5512
5978
  const e = relatedEntities[0];
5513
- values[key] = {
5514
- id: e.id,
5515
- path: e.path,
5516
- __type: "relation"
5517
- };
5979
+ values[key] = createRelationRef(e.id, e.path);
5518
5980
  }
5519
5981
  } catch (e) {
5520
5982
  console.warn(`Could not resolve one-to-one relation property: ${key}`, e);
@@ -5544,13 +6006,13 @@ class EntityFetchService {
5544
6006
  }
5545
6007
  const tableName = getTableName$1(table);
5546
6008
  const qb = this.getQueryBuilder(tableName);
5547
- if (qb) {
6009
+ const withConfig = this.buildWithConfig(collection);
6010
+ const hasRelations = withConfig && Object.keys(withConfig).length > 0;
6011
+ if (qb && !options.searchString && !hasRelations) {
5548
6012
  try {
5549
- const withConfig = this.buildWithConfig(collection);
5550
- const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
6013
+ const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5551
6014
  const results2 = await qb.findMany(queryOpts);
5552
6015
  const entities = results2.map((row) => this.drizzleResultToEntity(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray));
5553
- await this.resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, options.databaseId);
5554
6016
  return entities;
5555
6017
  } catch (e) {
5556
6018
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
@@ -5573,7 +6035,7 @@ class EntityFetchService {
5573
6035
  }
5574
6036
  const orderExpressions = [];
5575
6037
  if (options.orderBy) {
5576
- const orderByField = table[options.orderBy];
6038
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5577
6039
  if (orderByField) {
5578
6040
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
5579
6041
  }
@@ -5581,7 +6043,7 @@ class EntityFetchService {
5581
6043
  orderExpressions.push(desc(idField));
5582
6044
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5583
6045
  if (options.startAfter) {
5584
- const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
6046
+ const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
5585
6047
  if (cursorConditions.length > 0) {
5586
6048
  allConditions.push(...cursorConditions);
5587
6049
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
@@ -5590,6 +6052,7 @@ class EntityFetchService {
5590
6052
  }
5591
6053
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5592
6054
  if (limitValue) query = query.limit(limitValue);
6055
+ if (options.offset && options.offset > 0) query = query.offset(options.offset);
5593
6056
  const results = await query;
5594
6057
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5595
6058
  }
@@ -5603,7 +6066,7 @@ class EntityFetchService {
5603
6066
  async processEntityResults(results, collection, collectionPath, idInfo, databaseId, skipRelations = false, idInfoArray) {
5604
6067
  if (results.length === 0) return [];
5605
6068
  const entitiesWithValues = await Promise.all(results.map(async (entity) => {
5606
- const values = await parseDataFromServer(entity, collection, this.db, this.registry);
6069
+ const values = await parseDataFromServer(entity, collection);
5607
6070
  return {
5608
6071
  entity,
5609
6072
  values,
@@ -5628,37 +6091,29 @@ class EntityFetchService {
5628
6091
  const relationResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5629
6092
  entitiesMissingRelation.forEach((item) => {
5630
6093
  const entityId = item.entity[idInfo.fieldName];
5631
- const relatedEntity = relationResults.get(entityId);
6094
+ const relatedEntity = relationResults.get(String(entityId));
5632
6095
  if (relatedEntity) {
5633
- item.values[key] = {
5634
- id: relatedEntity.id,
5635
- path: relatedEntity.path,
5636
- __type: "relation",
5637
- data: relatedEntity
5638
- };
6096
+ item.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
5639
6097
  }
5640
6098
  });
5641
6099
  } catch (e) {
5642
6100
  console.warn(`Could not batch load one-to-one relation property: ${key}`, e);
5643
6101
  }
5644
6102
  }
5645
- const manyRelationPromises = entitiesWithValues.map(async (item) => {
5646
- const manyRelationQueries = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many").map(async ([key]) => {
5647
- try {
5648
- const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, item.entity[idInfo.fieldName], key, {});
5649
- item.values[key] = relatedEntities.map((e) => ({
5650
- id: e.id,
5651
- path: e.path,
5652
- __type: "relation",
5653
- data: e
5654
- }));
5655
- } catch (e) {
5656
- console.warn(`Could not resolve many relation property: ${key}`, e);
5657
- }
5658
- });
5659
- await Promise.all(manyRelationQueries);
5660
- });
5661
- await Promise.all(manyRelationPromises);
6103
+ const manyRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many");
6104
+ for (const [key, relation] of manyRelations) {
6105
+ try {
6106
+ const entityIds = entitiesWithValues.map((item) => item.entity[idInfo.fieldName]);
6107
+ const relationResults = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, entityIds, key, relation);
6108
+ entitiesWithValues.forEach((item) => {
6109
+ const entityId = String(item.entity[idInfo.fieldName]);
6110
+ const relatedEntities = relationResults.get(entityId) || [];
6111
+ item.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
6112
+ });
6113
+ } catch (e) {
6114
+ console.warn(`Could not batch load many relation property: ${key}`, e);
6115
+ }
6116
+ }
5662
6117
  }
5663
6118
  return entitiesWithValues.map((item) => ({
5664
6119
  id: item.id,
@@ -5699,12 +6154,16 @@ class EntityFetchService {
5699
6154
  for (let i = 2; i < pathSegments.length; i += 2) {
5700
6155
  const relationKey = pathSegments[i];
5701
6156
  const resolvedRelations = resolveCollectionRelations(currentCollection);
5702
- const relation = resolvedRelations[relationKey];
6157
+ const relation = findRelation(resolvedRelations, relationKey);
5703
6158
  if (!relation) {
5704
6159
  throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
5705
6160
  }
5706
6161
  if (i === pathSegments.length - 1) {
5707
- return this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
6162
+ const entities = await this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
6163
+ for (const entity of entities) {
6164
+ entity.path = path2;
6165
+ }
6166
+ return entities;
5708
6167
  }
5709
6168
  if (i + 1 < pathSegments.length) {
5710
6169
  const nextEntityId = pathSegments[i + 1];
@@ -5726,11 +6185,19 @@ class EntityFetchService {
5726
6185
  let query = this.db.select({
5727
6186
  count: count()
5728
6187
  }).from(table).$dynamic();
6188
+ const allConditions = [];
6189
+ if (options.searchString) {
6190
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
6191
+ if (searchConditions.length === 0) return 0;
6192
+ allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
6193
+ }
5729
6194
  if (options.filter) {
5730
6195
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5731
- if (filterConditions.length > 0) {
5732
- query = query.where(and(...filterConditions));
5733
- }
6196
+ if (filterConditions.length > 0) allConditions.push(...filterConditions);
6197
+ }
6198
+ if (allConditions.length > 0) {
6199
+ const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
6200
+ if (finalCondition) query = query.where(finalCondition);
5734
6201
  }
5735
6202
  const result = await query;
5736
6203
  return Number(result[0]?.count || 0);
@@ -5749,7 +6216,7 @@ class EntityFetchService {
5749
6216
  for (let i = 2; i < pathSegments.length; i += 2) {
5750
6217
  const relationKey = pathSegments[i];
5751
6218
  const resolvedRelations = resolveCollectionRelations(currentCollection);
5752
- const relation = resolvedRelations[relationKey];
6219
+ const relation = findRelation(resolvedRelations, relationKey);
5753
6220
  if (!relation) {
5754
6221
  throw new Error(`Relation '${relationKey}' not found`);
5755
6222
  }
@@ -5808,12 +6275,14 @@ class EntityFetchService {
5808
6275
  const idField = table[idInfo.fieldName];
5809
6276
  const tableName = getTableName$1(table);
5810
6277
  const qb = this.getQueryBuilder(tableName);
5811
- if (qb) {
6278
+ if (qb && !options.searchString) {
5812
6279
  try {
5813
6280
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5814
6281
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
5815
6282
  const results = await qb.findMany(queryOpts);
5816
- return results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
6283
+ const restRows = results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
6284
+ await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
6285
+ return restRows;
5817
6286
  } catch (e) {
5818
6287
  console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
5819
6288
  }
@@ -5835,7 +6304,7 @@ class EntityFetchService {
5835
6304
  const batchResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
5836
6305
  for (const entity of entities) {
5837
6306
  const eid = entity[idInfo.fieldName];
5838
- const related = batchResults.get(eid);
6307
+ const related = batchResults.get(String(eid));
5839
6308
  if (related) {
5840
6309
  entity[key] = {
5841
6310
  id: related.id,
@@ -5891,7 +6360,9 @@ class EntityFetchService {
5891
6360
  } : {}
5892
6361
  });
5893
6362
  if (!row) return null;
5894
- return this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
6363
+ const restRow = this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
6364
+ await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
6365
+ return restRow;
5895
6366
  } catch (e) {
5896
6367
  console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
5897
6368
  }
@@ -5959,7 +6430,7 @@ class EntityFetchService {
5959
6430
  }
5960
6431
  const orderExpressions = [];
5961
6432
  if (options.orderBy) {
5962
- const orderByField = table[options.orderBy];
6433
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5963
6434
  if (orderByField) {
5964
6435
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
5965
6436
  }
@@ -5968,6 +6439,7 @@ class EntityFetchService {
5968
6439
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5969
6440
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
5970
6441
  if (limitValue) query = query.limit(limitValue);
6442
+ if (options.offset && options.offset > 0) query = query.offset(options.offset);
5971
6443
  return await query;
5972
6444
  }
5973
6445
  /**
@@ -6016,7 +6488,7 @@ class EntityFetchService {
6016
6488
  }
6017
6489
  }
6018
6490
  if (options.orderBy) {
6019
- const orderByField = table[options.orderBy];
6491
+ const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
6020
6492
  if (orderByField) {
6021
6493
  queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
6022
6494
  }
@@ -6070,17 +6542,15 @@ class EntityFetchService {
6070
6542
  * Groups results by parent ID to avoid N+1.
6071
6543
  */
6072
6544
  async batchFetchManyRelatedEntities(parentCollectionPath, parentIds, relationKey) {
6073
- const resultMap = /* @__PURE__ */ new Map();
6074
- const batchPromises = parentIds.map(async (parentId) => {
6075
- try {
6076
- const related = await this.relationService.fetchRelatedEntities(parentCollectionPath, parentId, relationKey, {});
6077
- resultMap.set(String(parentId), related);
6078
- } catch (e) {
6079
- resultMap.set(String(parentId), []);
6080
- }
6081
- });
6082
- await Promise.all(batchPromises);
6083
- return resultMap;
6545
+ if (parentIds.length === 0) return /* @__PURE__ */ new Map();
6546
+ const collection = getCollectionByPath(parentCollectionPath, this.registry);
6547
+ const resolvedRelations = resolveCollectionRelations(collection);
6548
+ const relation = resolvedRelations[relationKey];
6549
+ if (!relation) {
6550
+ console.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
6551
+ return /* @__PURE__ */ new Map();
6552
+ }
6553
+ return this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
6084
6554
  }
6085
6555
  }
6086
6556
  class EntityPersistService {
@@ -6116,6 +6586,7 @@ class EntityPersistService {
6116
6586
  const effectiveValues = {
6117
6587
  ...values
6118
6588
  };
6589
+ let junctionTableInfo;
6119
6590
  if (collectionPath.includes("/")) {
6120
6591
  const segments = collectionPath.split("/").filter(Boolean);
6121
6592
  if (segments.length >= 3 && segments.length % 2 === 1) {
@@ -6125,9 +6596,10 @@ class EntityPersistService {
6125
6596
  for (let i = 2; i < segments.length; i += 2) {
6126
6597
  const relationKey = segments[i];
6127
6598
  const resolvedRelations2 = resolveCollectionRelations(currentCollection);
6128
- const relation = resolvedRelations2[relationKey];
6599
+ const relation = findRelation(resolvedRelations2, relationKey);
6129
6600
  if (!relation) {
6130
- throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
6601
+ const available = Object.keys(resolvedRelations2).join(", ") || "(none)";
6602
+ throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
6131
6603
  }
6132
6604
  if (i === segments.length - 1) {
6133
6605
  const targetCollection = relation.target();
@@ -6137,7 +6609,7 @@ class EntityPersistService {
6137
6609
  const parentIdInfo2 = parentIdInfoArray2[0];
6138
6610
  const parsedParentIdObj2 = parseIdValues(currentEntityId, parentIdInfoArray2);
6139
6611
  const parsedParentId2 = parsedParentIdObj2[parentIdInfo2.fieldName];
6140
- effectiveValues.__junction_table_info = {
6612
+ junctionTableInfo = {
6141
6613
  parentCollection: currentCollection,
6142
6614
  parentId: parsedParentId2,
6143
6615
  relation,
@@ -6206,14 +6678,10 @@ class EntityPersistService {
6206
6678
  }
6207
6679
  }
6208
6680
  }
6209
- const processedData = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
6210
- const inverseRelationUpdates = processedData.__inverseRelationUpdates || [];
6211
- const joinPathRelationUpdates = processedData.__joinPathRelationUpdates || [];
6212
- const junctionTableInfo = processedData.__junction_table_info;
6213
- delete processedData.__inverseRelationUpdates;
6214
- delete processedData.__joinPathRelationUpdates;
6215
- delete processedData.__junction_table_info;
6216
- const entityData = sanitizeAndConvertDates(processedData);
6681
+ const serializedResult = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
6682
+ const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
6683
+ const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
6684
+ const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
6217
6685
  let savedId;
6218
6686
  try {
6219
6687
  savedId = await this.db.transaction(async (tx) => {
@@ -6226,7 +6694,7 @@ class EntityPersistService {
6226
6694
  }
6227
6695
  const scalarKeys = Object.keys(entityData);
6228
6696
  if (scalarKeys.length > 0) {
6229
- let updateQuery = tx.update(table).set(entityData);
6697
+ const updateQuery = tx.update(table).set(entityData);
6230
6698
  const conditions = [];
6231
6699
  for (const info of idInfoArray) {
6232
6700
  const field = table[info.fieldName];
@@ -6590,21 +7058,6 @@ class PostgresBackendDriver {
6590
7058
  if (poolManager) {
6591
7059
  this.branchService = new BranchService(db, poolManager);
6592
7060
  }
6593
- this.admin = {
6594
- executeSql: this.executeSql.bind(this),
6595
- fetchAvailableDatabases: this.fetchAvailableDatabases.bind(this),
6596
- fetchAvailableRoles: this.fetchAvailableRoles.bind(this),
6597
- fetchCurrentDatabase: this.fetchCurrentDatabase.bind(this),
6598
- fetchUnmappedTables: this.fetchUnmappedTables.bind(this),
6599
- fetchTableMetadata: this.fetchTableMetadata.bind(this),
6600
- // Branch operations (only available when poolManager is configured)
6601
- ...this.branchService ? {
6602
- createBranch: this.branchService.createBranch.bind(this.branchService),
6603
- deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
6604
- listBranches: this.branchService.listBranches.bind(this.branchService),
6605
- getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
6606
- } : {}
6607
- };
6608
7061
  }
6609
7062
  key = "postgres";
6610
7063
  initialised = true;
@@ -6622,8 +7075,26 @@ class PostgresBackendDriver {
6622
7075
  _pendingNotifications = [];
6623
7076
  /**
6624
7077
  * Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin).
7078
+ * Implemented as a getter so method references are resolved at call-time,
7079
+ * allowing test spies applied after construction to take effect.
6625
7080
  */
6626
- admin;
7081
+ get admin() {
7082
+ return {
7083
+ executeSql: (...args) => this.executeSql(...args),
7084
+ fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
7085
+ fetchAvailableRoles: () => this.fetchAvailableRoles(),
7086
+ fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
7087
+ fetchUnmappedTables: (...args) => this.fetchUnmappedTables(...args),
7088
+ fetchTableMetadata: (...args) => this.fetchTableMetadata(...args),
7089
+ // Branch operations (only available when poolManager is configured)
7090
+ ...this.branchService ? {
7091
+ createBranch: this.branchService.createBranch.bind(this.branchService),
7092
+ deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
7093
+ listBranches: this.branchService.listBranches.bind(this.branchService),
7094
+ getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
7095
+ } : {}
7096
+ };
7097
+ }
6627
7098
  resolveCollectionCallbacks(collection, path2) {
6628
7099
  if (!collection && !path2) return {
6629
7100
  collection: void 0,
@@ -6652,6 +7123,7 @@ class PostgresBackendDriver {
6652
7123
  collection,
6653
7124
  filter,
6654
7125
  limit,
7126
+ offset,
6655
7127
  startAfter,
6656
7128
  orderBy,
6657
7129
  searchString,
@@ -6662,6 +7134,7 @@ class PostgresBackendDriver {
6662
7134
  orderBy,
6663
7135
  order,
6664
7136
  limit,
7137
+ offset,
6665
7138
  startAfter,
6666
7139
  databaseId: collection?.databaseId,
6667
7140
  searchString
@@ -6705,6 +7178,7 @@ class PostgresBackendDriver {
6705
7178
  collection,
6706
7179
  filter,
6707
7180
  limit,
7181
+ offset,
6708
7182
  startAfter,
6709
7183
  orderBy,
6710
7184
  searchString,
@@ -6725,6 +7199,7 @@ class PostgresBackendDriver {
6725
7199
  orderBy,
6726
7200
  order,
6727
7201
  limit,
7202
+ offset,
6728
7203
  startAfter,
6729
7204
  databaseId: collection?.databaseId,
6730
7205
  searchString
@@ -6736,6 +7211,7 @@ class PostgresBackendDriver {
6736
7211
  collection,
6737
7212
  filter,
6738
7213
  limit,
7214
+ offset,
6739
7215
  startAfter,
6740
7216
  orderBy,
6741
7217
  searchString,
@@ -6896,7 +7372,7 @@ class PostgresBackendDriver {
6896
7372
  collection: resolvedCollection,
6897
7373
  path: path2,
6898
7374
  entityId: savedEntity.id,
6899
- values: updatedValues,
7375
+ values: savedEntity.values,
6900
7376
  previousValues: previousValuesForHistory,
6901
7377
  status,
6902
7378
  context: contextForCallback
@@ -6907,7 +7383,7 @@ class PostgresBackendDriver {
6907
7383
  collection: resolvedCollection,
6908
7384
  path: path2,
6909
7385
  entityId: savedEntity.id,
6910
- values: updatedValues,
7386
+ values: savedEntity.values,
6911
7387
  previousValues: previousValuesForHistory,
6912
7388
  status,
6913
7389
  context: contextForCallback
@@ -7044,10 +7520,12 @@ class PostgresBackendDriver {
7044
7520
  async countEntities({
7045
7521
  path: path2,
7046
7522
  collection,
7047
- filter
7523
+ filter,
7524
+ searchString
7048
7525
  }) {
7049
7526
  return this.entityService.countEntities(path2, {
7050
- filter
7527
+ filter,
7528
+ searchString
7051
7529
  });
7052
7530
  }
7053
7531
  getTargetDb(databaseName) {
@@ -7103,7 +7581,7 @@ class PostgresBackendDriver {
7103
7581
  return databases;
7104
7582
  }
7105
7583
  async fetchAvailableRoles() {
7106
- const result = await this.executeSql(`SELECT rolname FROM pg_roles;`);
7584
+ const result = await this.executeSql("SELECT rolname FROM pg_roles;");
7107
7585
  return result.map((r) => r.rolname);
7108
7586
  }
7109
7587
  async fetchCurrentDatabase() {
@@ -7277,12 +7755,12 @@ class AuthenticatedPostgresBackendDriver {
7277
7755
  const result = await this.delegate.db.transaction(async (tx) => {
7278
7756
  let userId = this.user?.uid;
7279
7757
  if (!userId) {
7280
- console.warn(`[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:`, this.user);
7758
+ console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
7281
7759
  userId = "anonymous";
7282
7760
  }
7283
- let userRoles2 = this.user?.roles ?? [];
7761
+ const userRoles2 = this.user?.roles ?? [];
7284
7762
  if (!this.user?.roles) {
7285
- console.warn(`[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:`, this.user);
7763
+ console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
7286
7764
  }
7287
7765
  const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7288
7766
  const rolesString = normalizedRoles.join(",");
@@ -7352,29 +7830,6 @@ class AuthenticatedPostgresBackendDriver {
7352
7830
  async countEntities(props) {
7353
7831
  return this.withTransaction((delegate) => delegate.countEntities(props));
7354
7832
  }
7355
- /**
7356
- * Intentionally delegates to the base delegate WITHOUT RLS wrapping.
7357
- * executeSql is an admin-only feature; access control should be enforced
7358
- * at the API route level, not via database-level RLS.
7359
- */
7360
- async executeSql(sqlText, options) {
7361
- return this.delegate.executeSql(sqlText, options);
7362
- }
7363
- async fetchAvailableDatabases() {
7364
- return this.delegate.fetchAvailableDatabases();
7365
- }
7366
- async fetchAvailableRoles() {
7367
- return this.delegate.fetchAvailableRoles();
7368
- }
7369
- async fetchCurrentDatabase() {
7370
- return this.delegate.fetchCurrentDatabase();
7371
- }
7372
- async fetchUnmappedTables(mappedPaths) {
7373
- return this.delegate.fetchUnmappedTables(mappedPaths);
7374
- }
7375
- async fetchTableMetadata(tableName) {
7376
- return this.delegate.fetchTableMetadata(tableName);
7377
- }
7378
7833
  }
7379
7834
  class DatabasePoolManager {
7380
7835
  pools = /* @__PURE__ */ new Map();
@@ -7410,7 +7865,10 @@ class DatabasePoolManager {
7410
7865
  connectionString: url.toString(),
7411
7866
  max: 10,
7412
7867
  // Default sensible limit, can be tuned later
7413
- idleTimeoutMillis: 3e4
7868
+ idleTimeoutMillis: 1e4,
7869
+ // Reduced from 30000 for aggressive cleanup
7870
+ allowExitOnIdle: true
7871
+ // Prevent idle clients from hanging the Node.js process
7414
7872
  });
7415
7873
  pool.on("error", (err) => {
7416
7874
  console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
@@ -7461,13 +7919,6 @@ const users = rebaseSchema.table("users", {
7461
7919
  photoUrl: varchar("photo_url", {
7462
7920
  length: 500
7463
7921
  }),
7464
- provider: varchar("provider", {
7465
- length: 50
7466
- }).notNull().default("email"),
7467
- // 'email' | 'google'
7468
- googleId: varchar("google_id", {
7469
- length: 255
7470
- }).unique(),
7471
7922
  emailVerified: boolean("email_verified").default(false).notNull(),
7472
7923
  emailVerificationToken: varchar("email_verification_token", {
7473
7924
  length: 255
@@ -7541,12 +7992,31 @@ const appConfig = rebaseSchema.table("app_config", {
7541
7992
  value: jsonb("value").notNull(),
7542
7993
  updatedAt: timestamp("updated_at").defaultNow().notNull()
7543
7994
  });
7995
+ const userIdentities = rebaseSchema.table("user_identities", {
7996
+ id: uuid("id").defaultRandom().primaryKey(),
7997
+ userId: uuid("user_id").notNull().references(() => users.id, {
7998
+ onDelete: "cascade"
7999
+ }),
8000
+ provider: varchar("provider", {
8001
+ length: 50
8002
+ }).notNull(),
8003
+ // e.g. 'google', 'linkedin'
8004
+ providerId: varchar("provider_id", {
8005
+ length: 255
8006
+ }).notNull(),
8007
+ profileData: jsonb("profile_data"),
8008
+ createdAt: timestamp("created_at").defaultNow().notNull(),
8009
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
8010
+ }, (table) => ({
8011
+ uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
8012
+ }));
7544
8013
  const usersRelations = relations(users, ({
7545
8014
  many
7546
8015
  }) => ({
7547
8016
  userRoles: many(userRoles),
7548
8017
  refreshTokens: many(refreshTokens),
7549
- passwordResetTokens: many(passwordResetTokens)
8018
+ passwordResetTokens: many(passwordResetTokens),
8019
+ userIdentities: many(userIdentities)
7550
8020
  }));
7551
8021
  const rolesRelations = relations(roles, ({
7552
8022
  many
@@ -7581,13 +8051,24 @@ const passwordResetTokensRelations = relations(passwordResetTokens, ({
7581
8051
  references: [users.id]
7582
8052
  })
7583
8053
  }));
8054
+ const userIdentitiesRelations = relations(userIdentities, ({
8055
+ one
8056
+ }) => ({
8057
+ user: one(users, {
8058
+ fields: [userIdentities.userId],
8059
+ references: [users.id]
8060
+ })
8061
+ }));
7584
8062
  const getPrimaryKeyProp = (collection) => {
7585
8063
  if (collection.properties) {
7586
8064
  const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
7587
8065
  if (idPropEntry) {
8066
+ const prop = idPropEntry[1];
8067
+ const isUuid2 = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
7588
8068
  return {
7589
8069
  name: idPropEntry[0],
7590
- type: idPropEntry[1].type === "number" ? "number" : "string"
8070
+ type: prop.type === "number" ? "number" : "string",
8071
+ isUuid: isUuid2
7591
8072
  };
7592
8073
  }
7593
8074
  }
@@ -7595,12 +8076,15 @@ const getPrimaryKeyProp = (collection) => {
7595
8076
  if (idProp?.type === "number") {
7596
8077
  return {
7597
8078
  name: "id",
7598
- type: "number"
8079
+ type: "number",
8080
+ isUuid: false
7599
8081
  };
7600
8082
  }
8083
+ const isUuid = idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid";
7601
8084
  return {
7602
8085
  name: "id",
7603
- type: "string"
8086
+ type: "string",
8087
+ isUuid: isUuid ?? false
7604
8088
  };
7605
8089
  };
7606
8090
  const isNumericId = (collection) => {
@@ -7614,7 +8098,7 @@ const isIdProperty = (propName, prop, collection) => {
7614
8098
  const hasExplicitId = Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId));
7615
8099
  return !hasExplicitId && propName === "id";
7616
8100
  };
7617
- const getDrizzleColumn = (propName, prop, collection) => {
8101
+ const getDrizzleColumn = (propName, prop, collection, collections) => {
7618
8102
  const colName = toSnakeCase(propName);
7619
8103
  let columnDefinition;
7620
8104
  switch (prop.type) {
@@ -7633,20 +8117,20 @@ const getDrizzleColumn = (propName, prop, collection) => {
7633
8117
  columnDefinition = `varchar("${colName}")`;
7634
8118
  }
7635
8119
  if (isIdProperty(propName, prop, collection)) {
7636
- columnDefinition += `.primaryKey()`;
8120
+ columnDefinition += ".primaryKey()";
7637
8121
  }
7638
8122
  if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
7639
8123
  if (stringProp.isId === "uuid") {
7640
- columnDefinition += `.defaultRandom()`;
8124
+ columnDefinition += ".defaultRandom()";
7641
8125
  } else if (stringProp.isId === "cuid") {
7642
- columnDefinition += `.default(sql\`cuid()\`)`;
8126
+ columnDefinition += ".default(sql`cuid()`)";
7643
8127
  } else if (typeof stringProp.isId === "string") {
7644
8128
  const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`") ? stringProp.isId.substring(4, stringProp.isId.length - 1) : stringProp.isId;
7645
8129
  columnDefinition += `.default(sql\`${sqlContent}\`)`;
7646
8130
  }
7647
8131
  }
7648
8132
  if (stringProp.validation?.unique) {
7649
- columnDefinition += `.unique()`;
8133
+ columnDefinition += ".unique()";
7650
8134
  }
7651
8135
  break;
7652
8136
  }
@@ -7668,10 +8152,10 @@ const getDrizzleColumn = (propName, prop, collection) => {
7668
8152
  columnDefinition = baseType;
7669
8153
  }
7670
8154
  if (isId) {
7671
- columnDefinition += `.primaryKey()`;
8155
+ columnDefinition += ".primaryKey()";
7672
8156
  }
7673
8157
  if (numProp.validation?.unique) {
7674
- columnDefinition += `.unique()`;
8158
+ columnDefinition += ".unique()";
7675
8159
  }
7676
8160
  break;
7677
8161
  }
@@ -7702,7 +8186,7 @@ const getDrizzleColumn = (propName, prop, collection) => {
7702
8186
  case "relation": {
7703
8187
  const refProp = prop;
7704
8188
  const resolvedRelations = resolveCollectionRelations(collection);
7705
- const relation = resolvedRelations[refProp.relationName ?? propName];
8189
+ const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
7706
8190
  if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
7707
8191
  return null;
7708
8192
  }
@@ -7721,8 +8205,9 @@ const getDrizzleColumn = (propName, prop, collection) => {
7721
8205
  }
7722
8206
  const fkColumnName = toSnakeCase(relation.localKey);
7723
8207
  const targetTableVar = getTableVarName(getTableName(targetCollection));
7724
- const targetIdField = getPrimaryKeyName(targetCollection);
7725
- const baseColumn = isNumericId(targetCollection) ? `integer("${fkColumnName}")` : `varchar("${fkColumnName}")`;
8208
+ const pkProp = getPrimaryKeyProp(targetCollection);
8209
+ const targetIdField = pkProp.name;
8210
+ const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `varchar("${fkColumnName}")`;
7726
8211
  const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
7727
8212
  const required = prop.validation?.required;
7728
8213
  const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
@@ -7735,6 +8220,26 @@ const getDrizzleColumn = (propName, prop, collection) => {
7735
8220
  }
7736
8221
  return ` ${relation.localKey}: ${columnDef}`;
7737
8222
  }
8223
+ case "reference": {
8224
+ const refProp = prop;
8225
+ const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
8226
+ if (!targetCollection) {
8227
+ columnDefinition = `varchar("${colName}")`;
8228
+ break;
8229
+ }
8230
+ const pkProp = getPrimaryKeyProp(targetCollection);
8231
+ const targetTableVar = getTableVarName(getTableName(targetCollection));
8232
+ const targetIdField = pkProp.name;
8233
+ const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `varchar("${colName}")`;
8234
+ const required = prop.validation?.required;
8235
+ const onDelete = required ? "cascade" : "set null";
8236
+ const refOptions = `{ onDelete: "${onDelete}" }`;
8237
+ columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${refOptions})`;
8238
+ if (required) {
8239
+ columnDefinition += ".notNull()";
8240
+ }
8241
+ return ` ${propName}: ${columnDefinition}`;
8242
+ }
7738
8243
  default:
7739
8244
  return null;
7740
8245
  }
@@ -7761,7 +8266,7 @@ const buildUsingClause = (rule) => {
7761
8266
  return resolveRawSql(rule.using);
7762
8267
  }
7763
8268
  if (rule.access === "public") {
7764
- return `sql\`true\``;
8269
+ return "sql`true`";
7765
8270
  }
7766
8271
  if (rule.ownerField) {
7767
8272
  return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
@@ -7774,16 +8279,31 @@ const buildWithCheckClause = (rule) => {
7774
8279
  }
7775
8280
  return buildUsingClause(rule);
7776
8281
  };
8282
+ const getPolicyNameHash = (rule) => {
8283
+ const data = JSON.stringify({
8284
+ a: rule.access,
8285
+ m: rule.mode,
8286
+ op: rule.operation,
8287
+ ops: rule.operations?.slice().sort(),
8288
+ own: rule.ownerField,
8289
+ rol: rule.roles?.slice().sort(),
8290
+ pg: rule.pgRoles?.slice().sort(),
8291
+ u: rule.using,
8292
+ w: rule.withCheck
8293
+ });
8294
+ return createHash("sha1").update(data).digest("hex").substring(0, 7);
8295
+ };
7777
8296
  const generatePolicyCode = (tableName, rule, index) => {
7778
8297
  const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
8298
+ const ruleHash = getPolicyNameHash(rule);
7779
8299
  return ops.map((op, opIdx) => {
7780
- const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_policy_${index}${ops.length > 1 ? `_${opIdx}` : ""}`;
8300
+ const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
7781
8301
  return generateSinglePolicyCode(tableName, rule, op, policyName);
7782
8302
  }).join("");
7783
8303
  };
7784
8304
  const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
7785
8305
  const mode = rule.mode ?? "permissive";
7786
- const roles2 = rule.roles;
8306
+ const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
7787
8307
  const needsUsing = operation !== "insert";
7788
8308
  const needsWithCheck = operation !== "select" && operation !== "delete";
7789
8309
  let usingClause = needsUsing ? buildUsingClause(rule) : null;
@@ -7803,22 +8323,57 @@ const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
7803
8323
  }
7804
8324
  }
7805
8325
  if (!usingClause && needsUsing) {
7806
- usingClause = `sql\`false\``;
8326
+ usingClause = "sql`false`";
7807
8327
  }
7808
8328
  if (!withCheckClause && needsWithCheck) {
7809
- withCheckClause = `sql\`false\``;
8329
+ withCheckClause = "sql`false`";
7810
8330
  }
7811
8331
  const parts = [];
7812
8332
  parts.push(`as: "${mode}"`);
7813
8333
  parts.push(`for: "${operation}"`);
7814
- const toRoles = rule.pgRoles ?? ["public"];
8334
+ const toRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
7815
8335
  parts.push(`to: [${toRoles.map((r) => `"${r}"`).join(", ")}]`);
7816
8336
  if (usingClause) parts.push(`using: ${usingClause}`);
7817
8337
  if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
7818
8338
  return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),
7819
8339
  `;
7820
8340
  };
7821
- const generateSchema = async (collections) => {
8341
+ const computeSharedRelationName = (rel, sourceCollection, _collections) => {
8342
+ const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
8343
+ if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
8344
+ return `${getTableName(sourceCollection)}_${rel.localKey}`;
8345
+ }
8346
+ if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
8347
+ try {
8348
+ const targetCollection = rel.target();
8349
+ return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
8350
+ } catch {
8351
+ return fallback;
8352
+ }
8353
+ }
8354
+ if (rel.direction === "inverse" && rel.cardinality === "one") {
8355
+ if (rel.foreignKeyOnTarget) {
8356
+ try {
8357
+ const targetCollection = rel.target();
8358
+ return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
8359
+ } catch {
8360
+ return fallback;
8361
+ }
8362
+ }
8363
+ try {
8364
+ const targetCollection = rel.target();
8365
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8366
+ const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey && targetRel.target().slug === sourceCollection.slug);
8367
+ if (correspondingRelation && correspondingRelation.localKey) {
8368
+ return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
8369
+ }
8370
+ } catch {
8371
+ }
8372
+ return fallback;
8373
+ }
8374
+ return fallback;
8375
+ };
8376
+ const generateSchema = async (collections, stripPolicies = false) => {
7822
8377
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
7823
8378
  const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
7824
8379
  collections.some((c) => c.properties && Object.values(c.properties).some((p) => (p.type === "map" || p.type === "array") && p.columnType === "json"));
@@ -7826,9 +8381,7 @@ const generateSchema = async (collections) => {
7826
8381
  if (hasUuid) pgCoreImports.push("uuid");
7827
8382
  schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';
7828
8383
  `;
7829
- schemaContent += `import { relations as drizzleRelations, sql } from 'drizzle-orm';
7830
-
7831
- `;
8384
+ schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
7832
8385
  const exportedTableVars = [];
7833
8386
  const exportedEnumVars = [];
7834
8387
  const exportedRelationVars = [];
@@ -7889,8 +8442,8 @@ const generateSchema = async (collections) => {
7889
8442
  } = relation.through;
7890
8443
  const onDelete = relation.onDelete ?? "cascade";
7891
8444
  const refOptions = `{ onDelete: "${onDelete}" }`;
7892
- const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
7893
- const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
8445
+ const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "varchar";
8446
+ const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar";
7894
8447
  const sourceId = getPrimaryKeyName(sourceCollection);
7895
8448
  const targetId = getPrimaryKeyName(targetCollection);
7896
8449
  schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
@@ -7899,31 +8452,28 @@ const generateSchema = async (collections) => {
7899
8452
  `;
7900
8453
  schemaContent += ` ${targetColumn}: ${targetColType}("${toSnakeCase(targetColumn)}").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),
7901
8454
  `;
7902
- schemaContent += `}, (table) => ({
7903
- `;
8455
+ schemaContent += "}, (table) => ({\n";
7904
8456
  schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
7905
8457
  `;
7906
- schemaContent += `}));
7907
-
7908
- `;
8458
+ schemaContent += "}));\n\n";
7909
8459
  } else if (!isJunction) {
7910
8460
  schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
7911
8461
  `;
7912
8462
  const columns = /* @__PURE__ */ new Set();
7913
8463
  Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
7914
- const columnString = getDrizzleColumn(propName, prop, collection);
8464
+ const columnString = getDrizzleColumn(propName, prop, collection, collections);
7915
8465
  if (columnString) columns.add(columnString);
7916
8466
  });
7917
8467
  const hasIdColumn = Array.from(columns).some((col) => col.includes(".primaryKey()"));
7918
8468
  if (!hasIdColumn) {
7919
- columns.add(` id: varchar("id").primaryKey()`);
8469
+ columns.add(' id: varchar("id").primaryKey()');
7920
8470
  }
7921
8471
  schemaContent += `${Array.from(columns).join(",\n")}`;
7922
- const securityRules = collection.securityRules;
7923
- if (securityRules && securityRules.length > 0) {
8472
+ const securityRules = isPostgresCollection(collection) ? collection.securityRules : void 0;
8473
+ if (!stripPolicies && securityRules && securityRules.length > 0) {
7924
8474
  schemaContent += "\n}, (table) => ([\n";
7925
8475
  securityRules.forEach((rule, idx) => {
7926
- schemaContent += generatePolicyCode(tableName, rule, idx);
8476
+ schemaContent += generatePolicyCode(tableName, rule);
7927
8477
  });
7928
8478
  schemaContent += "])).enableRLS();\n\n";
7929
8479
  } else {
@@ -7963,13 +8513,13 @@ const generateSchema = async (collections) => {
7963
8513
  }
7964
8514
  } catch {
7965
8515
  }
7966
- tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {
8516
+ tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {
7967
8517
  fields: [${tableVarName}.${relation.through.sourceColumn}],
7968
8518
  references: [${sourceTableVar}.${sourceId}],
7969
8519
  relationName: "${owningRelationName}"
7970
8520
  })`);
7971
8521
  const targetRelName = inverseRelationName ?? owningRelationName;
7972
- tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {
8522
+ tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {
7973
8523
  fields: [${tableVarName}.${relation.through.targetColumn}],
7974
8524
  references: [${targetTableVar}.${targetId}],
7975
8525
  relationName: "${targetRelName}"
@@ -7977,22 +8527,25 @@ const generateSchema = async (collections) => {
7977
8527
  }
7978
8528
  } else {
7979
8529
  const resolvedRelations = resolveCollectionRelations(collection);
8530
+ const emittedRelationNames = /* @__PURE__ */ new Set();
7980
8531
  for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
7981
8532
  try {
7982
8533
  const target = rel.target();
7983
8534
  const targetTableVar = getTableVarName(getTableName(target));
7984
- const relationName = rel.relationName ?? relationKey;
7985
- const drizzleRelationName = relationName;
8535
+ const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
8536
+ const deduplicationKey = `${drizzleRelationName}::${rel.direction}`;
8537
+ if (emittedRelationNames.has(deduplicationKey)) continue;
8538
+ emittedRelationNames.add(deduplicationKey);
7986
8539
  if (rel.cardinality === "one") {
7987
8540
  if (rel.direction === "owning" && rel.localKey) {
7988
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8541
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
7989
8542
  fields: [${tableVarName}.${rel.localKey}],
7990
8543
  references: [${targetTableVar}.${getPrimaryKeyName(target)}],
7991
8544
  relationName: "${drizzleRelationName}"
7992
8545
  })`);
7993
8546
  } else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
7994
8547
  const sourceIdField = getPrimaryKeyName(collection);
7995
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8548
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
7996
8549
  fields: [${tableVarName}.${sourceIdField}],
7997
8550
  references: [${targetTableVar}.${rel.foreignKeyOnTarget}],
7998
8551
  relationName: "${drizzleRelationName}"
@@ -8004,7 +8557,7 @@ const generateSchema = async (collections) => {
8004
8557
  const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.target().slug === collection.slug);
8005
8558
  if (correspondingRelation && correspondingRelation.localKey) {
8006
8559
  const sourceIdField = getPrimaryKeyName(collection);
8007
- tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
8560
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8008
8561
  fields: [${tableVarName}.${sourceIdField}],
8009
8562
  references: [${targetTableVar}.${correspondingRelation.localKey}],
8010
8563
  relationName: "${drizzleRelationName}"
@@ -8016,10 +8569,10 @@ const generateSchema = async (collections) => {
8016
8569
  }
8017
8570
  } else if (rel.cardinality === "many") {
8018
8571
  if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8019
- tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8572
+ tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8020
8573
  } else if (rel.through) {
8021
8574
  const junctionTableVar = getTableVarName(rel.through.table);
8022
- tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8575
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8023
8576
  } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8024
8577
  try {
8025
8578
  const targetCollection = rel.target();
@@ -8027,7 +8580,7 @@ const generateSchema = async (collections) => {
8027
8580
  const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8028
8581
  if (correspondingRelation && correspondingRelation.through) {
8029
8582
  const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8030
- tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8583
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8031
8584
  } else {
8032
8585
  console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8033
8586
  }
@@ -8331,6 +8884,14 @@ class RealtimeService extends EventEmitter {
8331
8884
  async handleCollectionSubscription(clientId, request, authContext) {
8332
8885
  const subscriptionId = request.subscriptionId;
8333
8886
  try {
8887
+ const collection = this.registry.getCollectionByPath(request.path);
8888
+ if (!collection) {
8889
+ const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
8890
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
8891
+ console.error(`[RealtimeService] ${msg}`);
8892
+ this.sendError(clientId, msg, subscriptionId);
8893
+ return;
8894
+ }
8334
8895
  this._subscriptions.set(subscriptionId, {
8335
8896
  clientId,
8336
8897
  type: "collection",
@@ -8348,7 +8909,6 @@ class RealtimeService extends EventEmitter {
8348
8909
  });
8349
8910
  let entities;
8350
8911
  if (this.driver) {
8351
- const collection = this.registry.getCollectionByPath(request.path);
8352
8912
  entities = await this.driver.fetchCollection({
8353
8913
  path: request.path,
8354
8914
  collection,
@@ -8378,6 +8938,14 @@ class RealtimeService extends EventEmitter {
8378
8938
  async handleEntitySubscription(clientId, request, authContext) {
8379
8939
  const subscriptionId = request.subscriptionId;
8380
8940
  try {
8941
+ const collection = this.registry.getCollectionByPath(request.path);
8942
+ if (!collection) {
8943
+ const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
8944
+ const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
8945
+ console.error(`[RealtimeService] ${msg}`);
8946
+ this.sendError(clientId, msg, subscriptionId);
8947
+ return;
8948
+ }
8381
8949
  this._subscriptions.set(subscriptionId, {
8382
8950
  clientId,
8383
8951
  type: "entity",
@@ -8387,7 +8955,6 @@ class RealtimeService extends EventEmitter {
8387
8955
  });
8388
8956
  let entity;
8389
8957
  if (this.driver) {
8390
- const collection = this.registry.getCollectionByPath(request.path);
8391
8958
  entity = await this.driver.fetchEntity({
8392
8959
  path: request.path,
8393
8960
  entityId: request.entityId,
@@ -8460,13 +9027,13 @@ class RealtimeService extends EventEmitter {
8460
9027
  for (const [subscriptionId, subscription] of webSocketSubscriptions) {
8461
9028
  try {
8462
9029
  if (subscription.type === "entity" && notifyPath === originalPath) {
8463
- if (entity && entity.values?._rebase_invalidated) {
9030
+ if (entity && entity.values && entity.values?._rebase_invalidated) {
8464
9031
  this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
8465
9032
  } else {
8466
9033
  this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
8467
9034
  }
8468
9035
  } else if (subscription.type === "collection" && subscription.collectionRequest) {
8469
- if (!entity || !entity.values?._rebase_invalidated) {
9036
+ if (!entity || !(entity.values && entity.values?._rebase_invalidated)) {
8470
9037
  this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
8471
9038
  }
8472
9039
  this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
@@ -8481,7 +9048,7 @@ class RealtimeService extends EventEmitter {
8481
9048
  const callback = this.subscriptionCallbacks.get(subscriptionId);
8482
9049
  if (!callback) continue;
8483
9050
  if (subscription.type === "entity" && notifyPath === originalPath) {
8484
- if (entity && entity.values?._rebase_invalidated) {
9051
+ if (entity && entity.values && entity.values?._rebase_invalidated) {
8485
9052
  this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
8486
9053
  } else {
8487
9054
  callback(entity);
@@ -8547,6 +9114,7 @@ class RealtimeService extends EventEmitter {
8547
9114
  orderBy: collectionRequest.orderBy,
8548
9115
  order: collectionRequest.order,
8549
9116
  limit: collectionRequest.limit,
9117
+ offset: collectionRequest.offset,
8550
9118
  startAfter: collectionRequest.startAfter,
8551
9119
  searchString: collectionRequest.searchString
8552
9120
  });
@@ -8574,6 +9142,7 @@ class RealtimeService extends EventEmitter {
8574
9142
  orderBy: collectionRequest.orderBy,
8575
9143
  order: collectionRequest.order,
8576
9144
  limit: collectionRequest.limit,
9145
+ offset: collectionRequest.offset,
8577
9146
  startAfter: collectionRequest.startAfter,
8578
9147
  databaseId: collectionRequest.databaseId
8579
9148
  });
@@ -8634,6 +9203,7 @@ class RealtimeService extends EventEmitter {
8634
9203
  orderBy: collectionRequest.orderBy,
8635
9204
  order: collectionRequest.order,
8636
9205
  limit: collectionRequest.limit,
9206
+ offset: collectionRequest.offset,
8637
9207
  startAfter: collectionRequest.startAfter,
8638
9208
  databaseId: collectionRequest.databaseId
8639
9209
  });
@@ -8939,7 +9509,7 @@ class RealtimeService extends EventEmitter {
8939
9509
  }
8940
9510
  const PostgresRealtimeProvider = RealtimeService;
8941
9511
  const clientSessions = /* @__PURE__ */ new Map();
8942
- const WS_RATE_LIMIT = 200;
9512
+ const WS_RATE_LIMIT = 2e3;
8943
9513
  const WS_RATE_WINDOW_MS = 6e4;
8944
9514
  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"]);
8945
9515
  function isAdminSession(session) {
@@ -8959,6 +9529,12 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
8959
9529
  const wss = new WebSocketServer({
8960
9530
  server
8961
9531
  });
9532
+ wss.on("error", (err) => {
9533
+ if (err.code === "EADDRINUSE") {
9534
+ return;
9535
+ }
9536
+ console.error("❌ [WebSocket Server] Error:", err);
9537
+ });
8962
9538
  const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
8963
9539
  wss.on("connection", (ws) => {
8964
9540
  const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -9199,7 +9775,12 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9199
9775
  options
9200
9776
  } = payload;
9201
9777
  const delegate = await getScopedDelegate();
9202
- const result = await delegate.executeSql(sql2, options);
9778
+ const admin = delegate.admin;
9779
+ if (!isSQLAdmin(admin)) {
9780
+ sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
9781
+ break;
9782
+ }
9783
+ const result = await admin.executeSql(sql2, options);
9203
9784
  if (process.env.NODE_ENV !== "production") {
9204
9785
  wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
9205
9786
  }
@@ -9217,9 +9798,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9217
9798
  {
9218
9799
  wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
9219
9800
  const delegate = await getScopedDelegate();
9801
+ const admin = delegate.admin;
9220
9802
  let databases = [];
9221
- if (delegate.fetchAvailableDatabases) {
9222
- databases = await delegate.fetchAvailableDatabases();
9803
+ if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {
9804
+ databases = await admin.fetchAvailableDatabases();
9223
9805
  }
9224
9806
  wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
9225
9807
  const response = {
@@ -9236,9 +9818,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9236
9818
  {
9237
9819
  wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
9238
9820
  const delegate = await getScopedDelegate();
9821
+ const admin = delegate.admin;
9239
9822
  let roles2 = [];
9240
- if (delegate.fetchAvailableRoles) {
9241
- roles2 = await delegate.fetchAvailableRoles();
9823
+ if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
9824
+ roles2 = await admin.fetchAvailableRoles();
9242
9825
  }
9243
9826
  wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
9244
9827
  const response = {
@@ -9255,9 +9838,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9255
9838
  {
9256
9839
  wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
9257
9840
  const delegate = await getScopedDelegate();
9841
+ const admin = delegate.admin;
9258
9842
  let database = void 0;
9259
- if (delegate.fetchCurrentDatabase) {
9260
- database = await delegate.fetchCurrentDatabase();
9843
+ if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {
9844
+ database = await admin.fetchCurrentDatabase();
9261
9845
  }
9262
9846
  const response = {
9263
9847
  type: "FETCH_CURRENT_DATABASE_SUCCESS",
@@ -9273,9 +9857,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9273
9857
  {
9274
9858
  wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
9275
9859
  const delegate = await getScopedDelegate();
9860
+ const admin = delegate.admin;
9276
9861
  let tables = [];
9277
- if (delegate.fetchUnmappedTables) {
9278
- tables = await delegate.fetchUnmappedTables(payload?.mappedPaths);
9862
+ if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {
9863
+ tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
9279
9864
  }
9280
9865
  wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
9281
9866
  const response = {
@@ -9295,9 +9880,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
9295
9880
  tableName
9296
9881
  } = payload;
9297
9882
  const delegate = await getScopedDelegate();
9883
+ const admin = delegate.admin;
9298
9884
  let metadata;
9299
- if (delegate.fetchTableMetadata) {
9300
- metadata = await delegate.fetchTableMetadata(tableName);
9885
+ if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {
9886
+ metadata = await admin.fetchTableMetadata(tableName);
9301
9887
  }
9302
9888
  wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
9303
9889
  const response = {
@@ -9482,7 +10068,7 @@ class PostgresCollectionRegistry extends CollectionRegistry {
9482
10068
  */
9483
10069
  getRelationKeysForCollection(collectionPath) {
9484
10070
  const collection = this.getCollectionByPath(collectionPath);
9485
- if (!collection?.relations) return [];
10071
+ if (!collection || !getDataSourceCapabilities(collection.driver).supportsRelations || !collection.relations) return [];
9486
10072
  return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
9487
10073
  }
9488
10074
  }
@@ -9539,8 +10125,6 @@ async function ensureAuthTablesExist(db) {
9539
10125
  password_hash TEXT,
9540
10126
  display_name TEXT,
9541
10127
  photo_url TEXT,
9542
- provider TEXT DEFAULT 'email',
9543
- google_id TEXT UNIQUE,
9544
10128
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
9545
10129
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9546
10130
  )
@@ -9550,8 +10134,20 @@ async function ensureAuthTablesExist(db) {
9550
10134
  ON rebase.users(email)
9551
10135
  `);
9552
10136
  await db.execute(sql`
9553
- CREATE INDEX IF NOT EXISTS idx_users_google_id
9554
- ON rebase.users(google_id)
10137
+ CREATE TABLE IF NOT EXISTS rebase.user_identities (
10138
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10139
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
10140
+ provider TEXT NOT NULL,
10141
+ provider_id TEXT NOT NULL,
10142
+ profile_data JSONB,
10143
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10144
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10145
+ UNIQUE(provider, provider_id)
10146
+ )
10147
+ `);
10148
+ await db.execute(sql`
10149
+ CREATE INDEX IF NOT EXISTS idx_user_identities_user
10150
+ ON rebase.user_identities(user_id)
9555
10151
  `);
9556
10152
  await db.execute(sql`
9557
10153
  CREATE TABLE IF NOT EXISTS rebase.roles (
@@ -9564,10 +10160,6 @@ async function ensureAuthTablesExist(db) {
9564
10160
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9565
10161
  )
9566
10162
  `);
9567
- await db.execute(sql`
9568
- ALTER TABLE rebase.roles
9569
- ADD COLUMN IF NOT EXISTS collection_permissions JSONB
9570
- `);
9571
10163
  await db.execute(sql`
9572
10164
  CREATE TABLE IF NOT EXISTS rebase.user_roles (
9573
10165
  user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
@@ -9599,51 +10191,6 @@ async function ensureAuthTablesExist(db) {
9599
10191
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
9600
10192
  ON rebase.refresh_tokens(user_id)
9601
10193
  `);
9602
- await db.execute(sql`
9603
- ALTER TABLE rebase.refresh_tokens
9604
- ADD COLUMN IF NOT EXISTS user_agent TEXT
9605
- `);
9606
- await db.execute(sql`
9607
- ALTER TABLE rebase.refresh_tokens
9608
- ADD COLUMN IF NOT EXISTS ip_address TEXT
9609
- `);
9610
- const constraintCheck = await db.execute(sql`
9611
- SELECT 1 FROM information_schema.table_constraints
9612
- WHERE constraint_name = 'unique_device_session'
9613
- AND table_schema = 'rebase'
9614
- AND table_name = 'refresh_tokens'
9615
- `);
9616
- if (constraintCheck.rows.length === 0) {
9617
- try {
9618
- await db.execute(sql`
9619
- ALTER TABLE rebase.refresh_tokens
9620
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
9621
- `);
9622
- console.log("✅ Added unique_device_session constraint");
9623
- } catch (e) {
9624
- const errorMessage = e instanceof Error ? e.message : String(e);
9625
- if (errorMessage.includes("could not create unique index")) {
9626
- console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
9627
- await db.execute(sql`
9628
- DELETE FROM rebase.refresh_tokens a
9629
- USING rebase.refresh_tokens b
9630
- WHERE a.user_id = b.user_id
9631
- AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
9632
- AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
9633
- AND a.created_at < b.created_at
9634
- `);
9635
- await db.execute(sql`
9636
- ALTER TABLE rebase.refresh_tokens
9637
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
9638
- `).catch((retryErr) => {
9639
- const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
9640
- console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
9641
- });
9642
- } else {
9643
- console.error("Constraint migration issue:", errorMessage);
9644
- }
9645
- }
9646
- }
9647
10194
  await db.execute(sql`
9648
10195
  CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
9649
10196
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -9669,18 +10216,7 @@ async function ensureAuthTablesExist(db) {
9669
10216
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9670
10217
  )
9671
10218
  `);
9672
- await db.execute(sql`
9673
- ALTER TABLE rebase.users
9674
- ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE
9675
- `);
9676
- await db.execute(sql`
9677
- ALTER TABLE rebase.users
9678
- ADD COLUMN IF NOT EXISTS email_verification_token TEXT
9679
- `);
9680
- await db.execute(sql`
9681
- ALTER TABLE rebase.users
9682
- ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
9683
- `);
10219
+ await applyInternalMigrations(db);
9684
10220
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
9685
10221
  await db.transaction(async (tx) => {
9686
10222
  await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
@@ -9733,6 +10269,99 @@ async function seedDefaultRoles(db) {
9733
10269
  }
9734
10270
  console.log("✅ Default roles created: admin, editor, viewer");
9735
10271
  }
10272
+ async function applyInternalMigrations(db) {
10273
+ try {
10274
+ await db.execute(sql`
10275
+ ALTER TABLE rebase.users
10276
+ ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
10277
+ ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
10278
+ ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
10279
+ `);
10280
+ const columnsCheck = await db.execute(sql`
10281
+ SELECT column_name
10282
+ FROM information_schema.columns
10283
+ WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
10284
+ `);
10285
+ const existingColumns = columnsCheck.rows.map((r) => r.column_name);
10286
+ if (existingColumns.includes("google_id")) {
10287
+ await db.execute(sql`
10288
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
10289
+ SELECT id, 'google', google_id
10290
+ FROM rebase.users
10291
+ WHERE google_id IS NOT NULL
10292
+ ON CONFLICT (provider, provider_id) DO NOTHING
10293
+ `);
10294
+ }
10295
+ if (existingColumns.includes("linkedin_id")) {
10296
+ await db.execute(sql`
10297
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
10298
+ SELECT id, 'linkedin', linkedin_id
10299
+ FROM rebase.users
10300
+ WHERE linkedin_id IS NOT NULL
10301
+ ON CONFLICT (provider, provider_id) DO NOTHING
10302
+ `);
10303
+ }
10304
+ if (existingColumns.length > 0) {
10305
+ await db.execute(sql`
10306
+ ALTER TABLE rebase.users
10307
+ DROP COLUMN IF EXISTS provider,
10308
+ DROP COLUMN IF EXISTS google_id,
10309
+ DROP COLUMN IF EXISTS linkedin_id
10310
+ `);
10311
+ await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
10312
+ await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
10313
+ console.log("✅ Migrated to user_identities and dropped legacy columns.");
10314
+ }
10315
+ await db.execute(sql`
10316
+ ALTER TABLE rebase.roles
10317
+ ADD COLUMN IF NOT EXISTS collection_permissions JSONB
10318
+ `);
10319
+ await db.execute(sql`
10320
+ ALTER TABLE rebase.refresh_tokens
10321
+ ADD COLUMN IF NOT EXISTS user_agent TEXT,
10322
+ ADD COLUMN IF NOT EXISTS ip_address TEXT
10323
+ `);
10324
+ const constraintCheck = await db.execute(sql`
10325
+ SELECT 1 FROM information_schema.table_constraints
10326
+ WHERE constraint_name = 'unique_device_session'
10327
+ AND table_schema = 'rebase'
10328
+ AND table_name = 'refresh_tokens'
10329
+ `);
10330
+ if (constraintCheck.rows.length === 0) {
10331
+ try {
10332
+ await db.execute(sql`
10333
+ ALTER TABLE rebase.refresh_tokens
10334
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
10335
+ `);
10336
+ console.log("✅ Added unique_device_session constraint");
10337
+ } catch (e) {
10338
+ const errorMessage = e instanceof Error ? e.message : String(e);
10339
+ if (errorMessage.includes("could not create unique index")) {
10340
+ console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
10341
+ await db.execute(sql`
10342
+ DELETE FROM rebase.refresh_tokens a
10343
+ USING rebase.refresh_tokens b
10344
+ WHERE a.user_id = b.user_id
10345
+ AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
10346
+ AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
10347
+ AND a.created_at < b.created_at
10348
+ `);
10349
+ await db.execute(sql`
10350
+ ALTER TABLE rebase.refresh_tokens
10351
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
10352
+ `).catch((retryErr) => {
10353
+ const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
10354
+ console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
10355
+ });
10356
+ } else {
10357
+ console.error("Constraint migration issue:", errorMessage);
10358
+ }
10359
+ }
10360
+ }
10361
+ } catch (error) {
10362
+ console.error("❌ Failed to run internal migrations:", error);
10363
+ }
10364
+ }
9736
10365
  class UserService {
9737
10366
  constructor(db) {
9738
10367
  this.db = db;
@@ -9749,9 +10378,54 @@ class UserService {
9749
10378
  const [user] = await this.db.select().from(users).where(eq$3(users.email, email.toLowerCase()));
9750
10379
  return user || null;
9751
10380
  }
9752
- async getUserByGoogleId(googleId) {
9753
- const [user] = await this.db.select().from(users).where(eq$3(users.googleId, googleId));
9754
- return user || null;
10381
+ async getUserByIdentity(provider, providerId) {
10382
+ const result = await this.db.execute(sql`
10383
+ SELECT u.*
10384
+ FROM rebase.users u
10385
+ INNER JOIN rebase.user_identities ui ON u.id = ui.user_id
10386
+ WHERE ui.provider = ${provider} AND ui.provider_id = ${providerId}
10387
+ LIMIT 1
10388
+ `);
10389
+ if (result.rows.length === 0) return null;
10390
+ const row = result.rows[0];
10391
+ return {
10392
+ id: row.id,
10393
+ email: row.email,
10394
+ passwordHash: row.password_hash ?? null,
10395
+ displayName: row.display_name ?? null,
10396
+ photoUrl: row.photo_url ?? null,
10397
+ emailVerified: row.email_verified ?? false,
10398
+ emailVerificationToken: row.email_verification_token ?? null,
10399
+ emailVerificationSentAt: row.email_verification_sent_at ?? null,
10400
+ createdAt: row.created_at,
10401
+ updatedAt: row.updated_at
10402
+ };
10403
+ }
10404
+ async getUserIdentities(userId) {
10405
+ const result = await this.db.execute(sql`
10406
+ SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
10407
+ FROM rebase.user_identities
10408
+ WHERE user_id = ${userId}
10409
+ `);
10410
+ return result.rows.map((row) => ({
10411
+ id: row.id,
10412
+ userId: row.user_id,
10413
+ provider: row.provider,
10414
+ providerId: row.provider_id,
10415
+ profileData: row.profile_data ?? null,
10416
+ createdAt: row.created_at,
10417
+ updatedAt: row.updated_at
10418
+ }));
10419
+ }
10420
+ async linkUserIdentity(userId, provider, providerId, profileData) {
10421
+ await this.db.insert(userIdentities).values({
10422
+ userId,
10423
+ provider,
10424
+ providerId,
10425
+ profileData: profileData || null
10426
+ }).onConflictDoNothing({
10427
+ target: [userIdentities.provider, userIdentities.providerId]
10428
+ });
9755
10429
  }
9756
10430
  async updateUser(id, data) {
9757
10431
  const [user] = await this.db.update(users).set({
@@ -9772,6 +10446,7 @@ class UserService {
9772
10446
  const search = options?.search?.trim() || "";
9773
10447
  const orderBy = options?.orderBy || "createdAt";
9774
10448
  const orderDir = options?.orderDir || "desc";
10449
+ const roleId = options?.roleId;
9775
10450
  const columnMap = {
9776
10451
  email: "email",
9777
10452
  displayName: "display_name",
@@ -9781,42 +10456,34 @@ class UserService {
9781
10456
  };
9782
10457
  const orderColumn = columnMap[orderBy] || "created_at";
9783
10458
  const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
9784
- let rows;
9785
- let total;
10459
+ const conditions = [];
10460
+ if (roleId) {
10461
+ conditions.push(sql`EXISTS (SELECT 1 FROM rebase.user_roles ur WHERE ur.user_id = users.id AND ur.role_id = ${roleId})`);
10462
+ }
9786
10463
  if (search) {
9787
10464
  const pattern = `%${search}%`;
9788
- const countResult = await this.db.execute(sql`
9789
- SELECT count(*)::int as total FROM rebase.users
9790
- WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
9791
- `);
9792
- total = countResult.rows[0].total;
9793
- const dataResult = await this.db.execute(sql`
9794
- SELECT * FROM rebase.users
9795
- WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
9796
- ORDER BY ${sql.raw(orderColumn)} ${direction}
9797
- LIMIT ${limit} OFFSET ${offset}
9798
- `);
9799
- rows = dataResult.rows;
9800
- } else {
9801
- const countResult = await this.db.execute(sql`
9802
- SELECT count(*)::int as total FROM rebase.users
9803
- `);
9804
- total = countResult.rows[0].total;
9805
- const dataResult = await this.db.execute(sql`
9806
- SELECT * FROM rebase.users
9807
- ORDER BY ${sql.raw(orderColumn)} ${direction}
9808
- LIMIT ${limit} OFFSET ${offset}
9809
- `);
9810
- rows = dataResult.rows;
10465
+ conditions.push(sql`(email ILIKE ${pattern} OR display_name ILIKE ${pattern})`);
9811
10466
  }
10467
+ const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
10468
+ const orderByClause = roleId ? sql`ORDER BY ${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (SELECT count(*) FROM rebase.user_roles ur WHERE ur.user_id = users.id) DESC, ${sql.raw(orderColumn)} ${direction}`;
10469
+ const countResult = await this.db.execute(sql`
10470
+ SELECT count(*)::int as total FROM rebase.users
10471
+ ${whereClause}
10472
+ `);
10473
+ const total = countResult.rows[0].total;
10474
+ const dataResult = await this.db.execute(sql`
10475
+ SELECT * FROM rebase.users
10476
+ ${whereClause}
10477
+ ${orderByClause}
10478
+ LIMIT ${limit} OFFSET ${offset}
10479
+ `);
10480
+ const rows = dataResult.rows;
9812
10481
  const mappedUsers = rows.map((row) => ({
9813
10482
  id: row.id,
9814
10483
  email: row.email,
9815
10484
  passwordHash: row.password_hash ?? row.passwordHash ?? null,
9816
10485
  displayName: row.display_name ?? row.displayName ?? null,
9817
10486
  photoUrl: row.photo_url ?? row.photoUrl ?? null,
9818
- provider: row.provider,
9819
- googleId: row.google_id ?? row.googleId ?? null,
9820
10487
  emailVerified: row.email_verified ?? row.emailVerified ?? false,
9821
10488
  emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
9822
10489
  emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
@@ -10190,8 +10857,14 @@ class PostgresAuthRepository {
10190
10857
  async getUserByEmail(email) {
10191
10858
  return this.userService.getUserByEmail(email);
10192
10859
  }
10193
- async getUserByGoogleId(googleId) {
10194
- return this.userService.getUserByGoogleId(googleId);
10860
+ async getUserByIdentity(provider, providerId) {
10861
+ return this.userService.getUserByIdentity(provider, providerId);
10862
+ }
10863
+ async getUserIdentities(userId) {
10864
+ return this.userService.getUserIdentities(userId);
10865
+ }
10866
+ async linkUserIdentity(userId, provider, providerId, profileData) {
10867
+ return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
10195
10868
  }
10196
10869
  async updateUser(id, data) {
10197
10870
  return this.userService.updateUser(id, data);
@@ -10319,16 +10992,6 @@ class HistoryService {
10319
10992
  updatedBy
10320
10993
  } = params;
10321
10994
  const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
10322
- try {
10323
- require("fs").appendFileSync("/Users/francesco/rebase/packages/backend/history_diff.log", `[recordHistory: ${tableName}/${entityId} - ${action}]
10324
- CHANGED FIELDS: ${JSON.stringify(changedFields)}
10325
- PREVIOUS: ${JSON.stringify(previousValues, null, 2)}
10326
- NEW: ${JSON.stringify(values, null, 2)}
10327
-
10328
- `);
10329
- } catch (e) {
10330
- console.error("DEBUG FILE WRITE ERROR:", e);
10331
- }
10332
10995
  if (action === "update" && (!changedFields || changedFields.length === 0)) {
10333
10996
  return;
10334
10997
  }
@@ -10490,6 +11153,7 @@ function createPostgresBootstrapper(pgConfig) {
10490
11153
  const registry = new PostgresCollectionRegistry();
10491
11154
  if (collections) {
10492
11155
  registry.registerMultiple(collections);
11156
+ console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
10493
11157
  }
10494
11158
  if (pgConfig.schema?.tables) {
10495
11159
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -10556,9 +11220,6 @@ function createPostgresBootstrapper(pgConfig) {
10556
11220
  const internals = driverResult.internals;
10557
11221
  const db = internals.db;
10558
11222
  await ensureAuthTablesExist(db);
10559
- if (authConfig.google?.clientId) {
10560
- configureGoogleOAuth(authConfig.google.clientId);
10561
- }
10562
11223
  let emailService;
10563
11224
  if (authConfig.email) {
10564
11225
  emailService = createEmailService(authConfig.email);
@@ -10627,6 +11288,8 @@ export {
10627
11288
  refreshTokensRelations,
10628
11289
  roles,
10629
11290
  rolesRelations,
11291
+ userIdentities,
11292
+ userIdentitiesRelations,
10630
11293
  userRoles,
10631
11294
  userRolesRelations,
10632
11295
  users,