@rebasepro/server-postgresql 0.2.1 → 0.2.4

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 (57) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/data/query_builder.d.ts +51 -0
  4. package/dist/common/src/index.d.ts +1 -0
  5. package/dist/common/src/util/permissions.d.ts +1 -0
  6. package/dist/index.es.js +1202 -369
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +1200 -367
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
  11. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  12. package/dist/server-postgresql/src/auth/services.d.ts +43 -1
  13. package/dist/server-postgresql/src/connection.d.ts +25 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
  15. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  16. package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
  17. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  18. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  19. package/dist/types/src/controllers/auth.d.ts +2 -24
  20. package/dist/types/src/controllers/client.d.ts +0 -3
  21. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  22. package/dist/types/src/controllers/data.d.ts +21 -0
  23. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  24. package/dist/types/src/controllers/registry.d.ts +5 -4
  25. package/dist/types/src/rebase_context.d.ts +1 -1
  26. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  27. package/dist/types/src/types/collections.d.ts +0 -4
  28. package/dist/types/src/types/component_ref.d.ts +1 -1
  29. package/dist/types/src/types/cron.d.ts +1 -1
  30. package/dist/types/src/types/entity_views.d.ts +1 -0
  31. package/dist/types/src/types/export_import.d.ts +1 -1
  32. package/dist/types/src/types/formex.d.ts +2 -2
  33. package/dist/types/src/types/properties.d.ts +2 -2
  34. package/dist/types/src/types/translations.d.ts +28 -12
  35. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  36. package/dist/types/src/users/roles.d.ts +0 -8
  37. package/package.json +7 -6
  38. package/src/PostgresBackendDriver.ts +13 -7
  39. package/src/PostgresBootstrapper.ts +27 -8
  40. package/src/auth/ensure-tables.ts +79 -17
  41. package/src/auth/services.ts +292 -23
  42. package/src/cli.ts +5 -0
  43. package/src/connection.ts +77 -0
  44. package/src/data-transformer.ts +2 -2
  45. package/src/schema/auth-schema.ts +80 -14
  46. package/src/schema/default-collections.ts +1 -0
  47. package/src/schema/doctor.ts +82 -41
  48. package/src/schema/generate-drizzle-schema.ts +6 -6
  49. package/src/services/EntityFetchService.ts +69 -10
  50. package/src/services/entityService.ts +2 -0
  51. package/src/services/realtimeService.ts +214 -2
  52. package/src/utils/drizzle-conditions.ts +74 -2
  53. package/src/websocket.ts +10 -2
  54. package/test/auth-services.test.ts +15 -28
  55. package/test/drizzle-conditions.test.ts +168 -0
  56. package/test/postgresDataDriver.test.ts +130 -1
  57. package/vite.config.ts +1 -1
package/dist/index.umd.js CHANGED
@@ -59,6 +59,70 @@
59
59
  connectionString
60
60
  };
61
61
  }
62
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
63
+ const opts = {
64
+ ...DEFAULT_POOL,
65
+ max: 5,
66
+ ...poolConfig
67
+ };
68
+ const pgPoolConfig = {
69
+ connectionString,
70
+ max: opts.max,
71
+ idleTimeoutMillis: opts.idleTimeoutMillis,
72
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
73
+ query_timeout: opts.queryTimeout,
74
+ statement_timeout: opts.statementTimeout,
75
+ keepAlive: opts.keepAlive,
76
+ keepAliveInitialDelayMillis: 0
77
+ };
78
+ const pool = new pg.Pool(pgPoolConfig);
79
+ pool.on("error", (err) => {
80
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
81
+ });
82
+ const db = schema ? nodePostgres.drizzle(pool, {
83
+ schema
84
+ }) : nodePostgres.drizzle(pool);
85
+ return {
86
+ db,
87
+ pool,
88
+ connectionString
89
+ };
90
+ }
91
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
92
+ const opts = {
93
+ ...DEFAULT_POOL,
94
+ max: 10,
95
+ ...poolConfig
96
+ };
97
+ const pgPoolConfig = {
98
+ connectionString,
99
+ max: opts.max,
100
+ idleTimeoutMillis: opts.idleTimeoutMillis,
101
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
102
+ query_timeout: opts.queryTimeout,
103
+ statement_timeout: opts.statementTimeout,
104
+ keepAlive: opts.keepAlive,
105
+ keepAliveInitialDelayMillis: 0
106
+ };
107
+ const pool = new pg.Pool(pgPoolConfig);
108
+ pool.on("error", (err) => {
109
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
110
+ });
111
+ const db = schema ? nodePostgres.drizzle(pool, {
112
+ schema
113
+ }) : nodePostgres.drizzle(pool);
114
+ return {
115
+ db,
116
+ pool,
117
+ connectionString
118
+ };
119
+ }
120
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
121
+ __proto__: null,
122
+ createDirectDatabaseConnection,
123
+ createPostgresDatabaseConnection,
124
+ createReadReplicaConnection
125
+ }, Symbol.toStringTag, { value: "Module" }));
62
126
  class Vector {
63
127
  value;
64
128
  constructor(value) {
@@ -978,6 +1042,9 @@
978
1042
  return output;
979
1043
  }
980
1044
  for (const key in source) {
1045
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1046
+ continue;
1047
+ }
981
1048
  if (Object.prototype.hasOwnProperty.call(source, key)) {
982
1049
  const sourceValue = source[key];
983
1050
  const outputValue = output[key];
@@ -1166,118 +1233,6 @@
1166
1233
  });
1167
1234
  }
1168
1235
  }
1169
- function getSubcollections(collection) {
1170
- if (collection.childCollections) {
1171
- return collection.childCollections() ?? [];
1172
- }
1173
- if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
1174
- return collection.subcollections() ?? [];
1175
- }
1176
- if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
1177
- const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
1178
- return manyRelations.map((r) => {
1179
- const target = r.target();
1180
- if (!target) return void 0;
1181
- const relationKey = r.relationName || target.slug;
1182
- let customName;
1183
- if (collection.properties) {
1184
- const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1185
- if (prop && prop[1].name) {
1186
- customName = prop[1].name;
1187
- }
1188
- }
1189
- const baseOverrides = {
1190
- slug: relationKey
1191
- };
1192
- if (customName) {
1193
- baseOverrides.name = customName;
1194
- baseOverrides.singularName = customName;
1195
- }
1196
- const targetWithOverrides = {
1197
- ...target,
1198
- ...baseOverrides
1199
- };
1200
- return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1201
- }).filter((c) => Boolean(c));
1202
- }
1203
- return [];
1204
- }
1205
- function hasPropertyCallbacks(properties, callbackName) {
1206
- if (!properties) return false;
1207
- for (const property of Object.values(properties)) {
1208
- if (property.callbacks?.[callbackName]) return true;
1209
- if (property.type === "map" && property.properties) {
1210
- if (hasPropertyCallbacks(property.properties, callbackName)) return true;
1211
- } else if (property.type === "array" && property.of) {
1212
- const ofs = Array.isArray(property.of) ? property.of : [property.of];
1213
- for (const of of ofs) {
1214
- if (of.callbacks?.[callbackName]) return true;
1215
- if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
1216
- }
1217
- }
1218
- }
1219
- return false;
1220
- }
1221
- async function processProperties(properties, values, previousValues, propsContext, callbackName) {
1222
- if (!values || typeof values !== "object") return values;
1223
- const result = {
1224
- ...values
1225
- };
1226
- for (const [key, property] of Object.entries(properties)) {
1227
- if (result[key] === void 0) continue;
1228
- let currentValue = result[key];
1229
- const previousValue = previousValues?.[key];
1230
- if (property.type === "array" && Array.isArray(currentValue)) {
1231
- if (property.of && !Array.isArray(property.of)) {
1232
- currentValue = await Promise.all(currentValue.map(async (item, index) => {
1233
- const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
1234
- const singlePropData = {
1235
- "_tmp": property.of
1236
- };
1237
- const res = await processProperties(singlePropData, {
1238
- "_tmp": item
1239
- }, {
1240
- "_tmp": prevItem
1241
- }, propsContext, callbackName);
1242
- return res["_tmp"];
1243
- }));
1244
- }
1245
- } else if (property.type === "map" && property.properties && typeof currentValue === "object") {
1246
- currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
1247
- }
1248
- if (property.callbacks?.[callbackName]) {
1249
- const cbRes = await Promise.resolve(property.callbacks[callbackName]({
1250
- ...propsContext,
1251
- value: currentValue,
1252
- previousValue
1253
- }));
1254
- if (cbRes !== void 0) {
1255
- currentValue = cbRes;
1256
- }
1257
- }
1258
- result[key] = currentValue;
1259
- }
1260
- return result;
1261
- }
1262
- const buildPropertyCallbacks = (properties) => {
1263
- if (!properties) return void 0;
1264
- const propertyCallbacks = {};
1265
- if (hasPropertyCallbacks(properties, "afterRead")) {
1266
- propertyCallbacks.afterRead = async (props) => {
1267
- const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1268
- return {
1269
- ...props.entity,
1270
- values: processedValues
1271
- };
1272
- };
1273
- }
1274
- if (hasPropertyCallbacks(properties, "beforeSave")) {
1275
- propertyCallbacks.beforeSave = async (props) => {
1276
- return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
1277
- };
1278
- }
1279
- return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
1280
- };
1281
1236
  function sanitizeRelation(relation, sourceCollection, resolveCollection) {
1282
1237
  if (!relation.target) {
1283
1238
  throw new Error("Relation is missing a `target` collection.");
@@ -1309,6 +1264,8 @@
1309
1264
  } else {
1310
1265
  targetCollection = evaluated;
1311
1266
  }
1267
+ } else if (rawTarget && typeof rawTarget === "object") {
1268
+ targetCollection = rawTarget;
1312
1269
  }
1313
1270
  if (!targetCollection) {
1314
1271
  throw new Error("Relation is missing a valid `target` collection.");
@@ -1428,11 +1385,14 @@
1428
1385
  const registeredRelationNames = /* @__PURE__ */ new Set();
1429
1386
  if (relCollection.relations) {
1430
1387
  relCollection.relations.forEach((relation) => {
1431
- const normalizedRelation = sanitizeRelation(relation, collection);
1432
- const relationKey = normalizedRelation.relationName;
1433
- if (relationKey) {
1434
- relations[relationKey] = normalizedRelation;
1435
- registeredRelationNames.add(relationKey);
1388
+ try {
1389
+ const normalizedRelation = sanitizeRelation(relation, collection);
1390
+ const relationKey = normalizedRelation.relationName;
1391
+ if (relationKey) {
1392
+ relations[relationKey] = normalizedRelation;
1393
+ registeredRelationNames.add(relationKey);
1394
+ }
1395
+ } catch (e) {
1436
1396
  }
1437
1397
  });
1438
1398
  }
@@ -1480,12 +1440,8 @@
1480
1440
  overrides: relProp.overrides
1481
1441
  };
1482
1442
  }
1483
- const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
1484
- if (!relation) {
1485
- console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1486
- return void 0;
1487
- }
1488
- return relation;
1443
+ console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1444
+ return void 0;
1489
1445
  }
1490
1446
  function getTableName(collection) {
1491
1447
  if (getDataSourceCapabilities(collection.driver).supportsRelations) {
@@ -1512,6 +1468,119 @@
1512
1468
  if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
1513
1469
  return void 0;
1514
1470
  }
1471
+ function getSubcollections(collection) {
1472
+ if (collection.childCollections) {
1473
+ return collection.childCollections() ?? [];
1474
+ }
1475
+ if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
1476
+ return collection.subcollections() ?? [];
1477
+ }
1478
+ if (getDataSourceCapabilities(collection.driver).supportsRelations) {
1479
+ const resolvedRelations = resolveCollectionRelations(collection);
1480
+ const manyRelations = Object.values(resolvedRelations).filter((r) => r.cardinality === "many");
1481
+ return manyRelations.map((r) => {
1482
+ const target = r.target();
1483
+ if (!target) return void 0;
1484
+ const relationKey = r.relationName || target.slug;
1485
+ let customName;
1486
+ if (collection.properties) {
1487
+ const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1488
+ if (prop && prop[1].name) {
1489
+ customName = prop[1].name;
1490
+ }
1491
+ }
1492
+ const baseOverrides = {
1493
+ slug: relationKey
1494
+ };
1495
+ if (customName) {
1496
+ baseOverrides.name = customName;
1497
+ baseOverrides.singularName = customName;
1498
+ }
1499
+ const targetWithOverrides = {
1500
+ ...target,
1501
+ ...baseOverrides
1502
+ };
1503
+ return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1504
+ }).filter((c) => Boolean(c));
1505
+ }
1506
+ return [];
1507
+ }
1508
+ function hasPropertyCallbacks(properties, callbackName) {
1509
+ if (!properties) return false;
1510
+ for (const property of Object.values(properties)) {
1511
+ if (property.callbacks?.[callbackName]) return true;
1512
+ if (property.type === "map" && property.properties) {
1513
+ if (hasPropertyCallbacks(property.properties, callbackName)) return true;
1514
+ } else if (property.type === "array" && property.of) {
1515
+ const ofs = Array.isArray(property.of) ? property.of : [property.of];
1516
+ for (const of of ofs) {
1517
+ if (of.callbacks?.[callbackName]) return true;
1518
+ if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
1519
+ }
1520
+ }
1521
+ }
1522
+ return false;
1523
+ }
1524
+ async function processProperties(properties, values, previousValues, propsContext, callbackName) {
1525
+ if (!values || typeof values !== "object") return values;
1526
+ const result = {
1527
+ ...values
1528
+ };
1529
+ for (const [key, property] of Object.entries(properties)) {
1530
+ if (result[key] === void 0) continue;
1531
+ let currentValue = result[key];
1532
+ const previousValue = previousValues?.[key];
1533
+ if (property.type === "array" && Array.isArray(currentValue)) {
1534
+ if (property.of && !Array.isArray(property.of)) {
1535
+ currentValue = await Promise.all(currentValue.map(async (item, index) => {
1536
+ const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
1537
+ const singlePropData = {
1538
+ "_tmp": property.of
1539
+ };
1540
+ const res = await processProperties(singlePropData, {
1541
+ "_tmp": item
1542
+ }, {
1543
+ "_tmp": prevItem
1544
+ }, propsContext, callbackName);
1545
+ return res["_tmp"];
1546
+ }));
1547
+ }
1548
+ } else if (property.type === "map" && property.properties && typeof currentValue === "object") {
1549
+ currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
1550
+ }
1551
+ if (property.callbacks?.[callbackName]) {
1552
+ const cbRes = await Promise.resolve(property.callbacks[callbackName]({
1553
+ ...propsContext,
1554
+ value: currentValue,
1555
+ previousValue
1556
+ }));
1557
+ if (cbRes !== void 0) {
1558
+ currentValue = cbRes;
1559
+ }
1560
+ }
1561
+ result[key] = currentValue;
1562
+ }
1563
+ return result;
1564
+ }
1565
+ const buildPropertyCallbacks = (properties) => {
1566
+ if (!properties) return void 0;
1567
+ const propertyCallbacks = {};
1568
+ if (hasPropertyCallbacks(properties, "afterRead")) {
1569
+ propertyCallbacks.afterRead = async (props) => {
1570
+ const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1571
+ return {
1572
+ ...props.entity,
1573
+ values: processedValues
1574
+ };
1575
+ };
1576
+ }
1577
+ if (hasPropertyCallbacks(properties, "beforeSave")) {
1578
+ propertyCallbacks.beforeSave = async (props) => {
1579
+ return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
1580
+ };
1581
+ }
1582
+ return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
1583
+ };
1515
1584
  var logic = { exports: {} };
1516
1585
  (function(module2, exports3) {
1517
1586
  (function(root, factory) {
@@ -2419,8 +2488,18 @@
2419
2488
  const mergedRelationsRaw = [...extractedRelations];
2420
2489
  for (const manual of manualRelations) {
2421
2490
  const name = manual.relationName;
2422
- if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
2491
+ if (!name) {
2423
2492
  mergedRelationsRaw.push(manual);
2493
+ } else {
2494
+ const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
2495
+ if (existingIndex === -1) {
2496
+ mergedRelationsRaw.push(manual);
2497
+ } else {
2498
+ mergedRelationsRaw[existingIndex] = {
2499
+ ...manual,
2500
+ ...mergedRelationsRaw[existingIndex]
2501
+ };
2502
+ }
2424
2503
  }
2425
2504
  }
2426
2505
  let mergedRelations = mergedRelationsRaw;
@@ -2633,11 +2712,207 @@
2633
2712
  collections.push(currentCollection);
2634
2713
  }
2635
2714
  }
2636
- return {
2637
- collections,
2638
- entityIds,
2639
- finalCollection: currentCollection
2640
- };
2715
+ return {
2716
+ collections,
2717
+ entityIds,
2718
+ finalCollection: currentCollection
2719
+ };
2720
+ }
2721
+ }
2722
+ const defaultUsersCollection = {
2723
+ name: "Users",
2724
+ singularName: "User",
2725
+ slug: "users",
2726
+ table: "users",
2727
+ schema: "rebase",
2728
+ icon: "Users",
2729
+ group: "Settings",
2730
+ properties: {
2731
+ id: {
2732
+ name: "ID",
2733
+ type: "string",
2734
+ isId: "uuid"
2735
+ },
2736
+ email: {
2737
+ name: "Email",
2738
+ type: "string",
2739
+ validation: {
2740
+ required: true,
2741
+ unique: true
2742
+ }
2743
+ },
2744
+ password_hash: {
2745
+ name: "Password Hash",
2746
+ type: "string",
2747
+ ui: {
2748
+ hideFromCollection: true
2749
+ }
2750
+ },
2751
+ display_name: {
2752
+ name: "Display Name",
2753
+ type: "string"
2754
+ },
2755
+ photo_url: {
2756
+ name: "Photo URL",
2757
+ type: "string"
2758
+ },
2759
+ email_verified: {
2760
+ name: "Email Verified",
2761
+ type: "boolean",
2762
+ defaultValue: false
2763
+ },
2764
+ email_verification_token: {
2765
+ name: "Email Verification Token",
2766
+ type: "string",
2767
+ ui: {
2768
+ hideFromCollection: true
2769
+ }
2770
+ },
2771
+ email_verification_sent_at: {
2772
+ name: "Email Verification Sent At",
2773
+ type: "date",
2774
+ ui: {
2775
+ hideFromCollection: true
2776
+ }
2777
+ },
2778
+ metadata: {
2779
+ name: "Metadata",
2780
+ type: "map",
2781
+ defaultValue: {},
2782
+ ui: {
2783
+ hideFromCollection: true
2784
+ }
2785
+ },
2786
+ created_at: {
2787
+ name: "Created At",
2788
+ type: "date",
2789
+ autoValue: "on_create",
2790
+ ui: {
2791
+ readOnly: true,
2792
+ hideFromCollection: true
2793
+ }
2794
+ },
2795
+ updated_at: {
2796
+ name: "Updated At",
2797
+ type: "date",
2798
+ autoValue: "on_update",
2799
+ ui: {
2800
+ readOnly: true,
2801
+ hideFromCollection: true
2802
+ }
2803
+ }
2804
+ }
2805
+ };
2806
+ function mapOperator(op) {
2807
+ switch (op) {
2808
+ case "==":
2809
+ return "eq";
2810
+ case "!=":
2811
+ return "neq";
2812
+ case ">":
2813
+ return "gt";
2814
+ case ">=":
2815
+ return "gte";
2816
+ case "<":
2817
+ return "lt";
2818
+ case "<=":
2819
+ return "lte";
2820
+ case "array-contains":
2821
+ return "cs";
2822
+ case "array-contains-any":
2823
+ return "csa";
2824
+ case "not-in":
2825
+ return "nin";
2826
+ default:
2827
+ return op;
2828
+ }
2829
+ }
2830
+ class QueryBuilder {
2831
+ constructor(collection) {
2832
+ this.collection = collection;
2833
+ }
2834
+ params = {
2835
+ where: {}
2836
+ };
2837
+ /**
2838
+ * Add a filter condition to your query.
2839
+ * @example
2840
+ * client.collection('users').where('age', '>=', 18).find()
2841
+ */
2842
+ where(column, operator, value) {
2843
+ if (!this.params.where) {
2844
+ this.params.where = {};
2845
+ }
2846
+ const mappedOp = mapOperator(operator);
2847
+ let formattedValue = value;
2848
+ if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
2849
+ formattedValue = `(${value.join(",")})`;
2850
+ } else if (value === null) {
2851
+ formattedValue = "null";
2852
+ }
2853
+ this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
2854
+ return this;
2855
+ }
2856
+ /**
2857
+ * Order the results by a specific column.
2858
+ * @example
2859
+ * client.collection('users').orderBy('createdAt', 'desc').find()
2860
+ */
2861
+ orderBy(column, ascending = "asc") {
2862
+ this.params.orderBy = `${column}:${ascending}`;
2863
+ return this;
2864
+ }
2865
+ /**
2866
+ * Limit the number of results returned.
2867
+ */
2868
+ limit(count) {
2869
+ this.params.limit = count;
2870
+ return this;
2871
+ }
2872
+ /**
2873
+ * Skip the first N results.
2874
+ */
2875
+ offset(count) {
2876
+ this.params.offset = count;
2877
+ return this;
2878
+ }
2879
+ /**
2880
+ * Set a free-text search string if supported by the backend.
2881
+ */
2882
+ search(searchString) {
2883
+ this.params.searchString = searchString;
2884
+ return this;
2885
+ }
2886
+ /**
2887
+ * Include related entities in the response.
2888
+ * Relations will be populated with full entity data instead of just IDs.
2889
+ *
2890
+ * @param relations - Relation names to include, or "*" for all.
2891
+ * @example
2892
+ * // Include specific relations
2893
+ * client.data.posts.include("tags", "author").find()
2894
+ *
2895
+ * // Include all relations
2896
+ * client.data.posts.include("*").find()
2897
+ */
2898
+ include(...relations) {
2899
+ this.params.include = relations;
2900
+ return this;
2901
+ }
2902
+ /**
2903
+ * Execute the find query and return the results.
2904
+ */
2905
+ async find() {
2906
+ return this.collection.find(this.params);
2907
+ }
2908
+ /**
2909
+ * Listen to realtime updates matching this query.
2910
+ */
2911
+ listen(onUpdate, onError) {
2912
+ if (!this.collection.listen) {
2913
+ throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
2914
+ }
2915
+ return this.collection.listen(this.params, onUpdate, onError);
2641
2916
  }
2642
2917
  }
2643
2918
  function convertWhereToFilter(where) {
@@ -2719,7 +2994,7 @@
2719
2994
  return [field, direction];
2720
2995
  }
2721
2996
  function createDriverAccessor(driver, slug) {
2722
- return {
2997
+ const accessor = {
2723
2998
  async find(params) {
2724
2999
  const orderParsed = parseOrderBy(params?.orderBy);
2725
3000
  const entities = await driver.fetchCollection({
@@ -2813,8 +3088,28 @@
2813
3088
  onUpdate: (entity) => onUpdate(entity ?? void 0),
2814
3089
  onError
2815
3090
  });
2816
- } : void 0
3091
+ } : void 0,
3092
+ // Fluent Query Builder
3093
+ where(column, operator, value) {
3094
+ return new QueryBuilder(accessor).where(column, operator, value);
3095
+ },
3096
+ orderBy(column, ascending) {
3097
+ return new QueryBuilder(accessor).orderBy(column, ascending);
3098
+ },
3099
+ limit(count) {
3100
+ return new QueryBuilder(accessor).limit(count);
3101
+ },
3102
+ offset(count) {
3103
+ return new QueryBuilder(accessor).offset(count);
3104
+ },
3105
+ search(searchString) {
3106
+ return new QueryBuilder(accessor).search(searchString);
3107
+ },
3108
+ include(...relations) {
3109
+ return new QueryBuilder(accessor).include(...relations);
3110
+ }
2817
3111
  };
3112
+ return accessor;
2818
3113
  }
2819
3114
  function buildRebaseData(driver) {
2820
3115
  const cache = /* @__PURE__ */ new Map();
@@ -2848,7 +3143,13 @@
2848
3143
  for (const [field, filterParam] of Object.entries(filter)) {
2849
3144
  if (!filterParam) continue;
2850
3145
  const [op, value] = filterParam;
2851
- const fieldColumn = table[field];
3146
+ let fieldColumn = table[field];
3147
+ if (!fieldColumn) {
3148
+ const relationKey = `${field}_id`;
3149
+ if (relationKey in table) {
3150
+ fieldColumn = table[relationKey];
3151
+ }
3152
+ }
2852
3153
  if (!fieldColumn) {
2853
3154
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2854
3155
  continue;
@@ -2890,6 +3191,17 @@
2890
3191
  return null;
2891
3192
  case "array-contains":
2892
3193
  return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3194
+ case "array-contains-any":
3195
+ if (Array.isArray(value) && value.length > 0) {
3196
+ const textValues = value.map((v) => String(v));
3197
+ return drizzleOrm.sql`${column} ?| array[${drizzleOrm.sql.join(textValues.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)}]`;
3198
+ }
3199
+ return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3200
+ case "not-in":
3201
+ if (Array.isArray(value) && value.length > 0) {
3202
+ return drizzleOrm.sql`${column} NOT IN (${drizzleOrm.sql.join(value.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)})`;
3203
+ }
3204
+ return null;
2893
3205
  default:
2894
3206
  console.warn(`Unsupported filter operation: ${op}`);
2895
3207
  return null;
@@ -3405,6 +3717,40 @@
3405
3717
  return null;
3406
3718
  }
3407
3719
  }
3720
+ /**
3721
+ * Build vector similarity search expressions for pgvector.
3722
+ *
3723
+ * Returns:
3724
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3725
+ * - `filter`: optional WHERE clause for distance threshold
3726
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3727
+ */
3728
+ static buildVectorSearchConditions(table, vectorSearch) {
3729
+ const column = table[vectorSearch.property];
3730
+ if (!column) {
3731
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3732
+ }
3733
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3734
+ const distanceFn = vectorSearch.distance || "cosine";
3735
+ let operator;
3736
+ switch (distanceFn) {
3737
+ case "cosine":
3738
+ operator = "<=>";
3739
+ break;
3740
+ case "l2":
3741
+ operator = "<->";
3742
+ break;
3743
+ case "inner_product":
3744
+ operator = "<#>";
3745
+ break;
3746
+ }
3747
+ const distanceExpr = drizzleOrm.sql`${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}`;
3748
+ return {
3749
+ orderBy: distanceExpr,
3750
+ filter: vectorSearch.threshold != null ? drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3751
+ distanceSelect: drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)})`
3752
+ };
3753
+ }
3408
3754
  }
3409
3755
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3410
3756
  function getColumnMeta(col) {
@@ -5350,7 +5696,7 @@
5350
5696
  const qb = this.getQueryBuilder(tableName);
5351
5697
  const withConfig = this.buildWithConfig(collection);
5352
5698
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5353
- if (qb && !options.searchString && !hasRelations) {
5699
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5354
5700
  try {
5355
5701
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5356
5702
  const results2 = await qb.findMany(queryOpts);
@@ -5364,7 +5710,14 @@
5364
5710
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5365
5711
  }
5366
5712
  }
5367
- let query = this.db.select().from(table).$dynamic();
5713
+ let vectorMeta;
5714
+ if (options.vectorSearch) {
5715
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5716
+ }
5717
+ let query = vectorMeta ? this.db.select({
5718
+ table_row: table,
5719
+ _distance: vectorMeta.distanceSelect
5720
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5368
5721
  const allConditions = [];
5369
5722
  if (options.searchString) {
5370
5723
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5375,12 +5728,17 @@
5375
5728
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5376
5729
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5377
5730
  }
5731
+ if (vectorMeta?.filter) {
5732
+ allConditions.push(vectorMeta.filter);
5733
+ }
5378
5734
  if (allConditions.length > 0) {
5379
5735
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5380
5736
  if (finalCondition) query = query.where(finalCondition);
5381
5737
  }
5382
5738
  const orderExpressions = [];
5383
- if (options.orderBy) {
5739
+ if (vectorMeta) {
5740
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
5741
+ } else if (options.orderBy) {
5384
5742
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5385
5743
  if (orderByField) {
5386
5744
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5396,10 +5754,14 @@
5396
5754
  if (finalCondition) query = query.where(finalCondition);
5397
5755
  }
5398
5756
  }
5399
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5757
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5400
5758
  if (limitValue) query = query.limit(limitValue);
5401
5759
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5402
- const results = await query;
5760
+ const rawResults = await query;
5761
+ const results = vectorMeta ? rawResults.map((r) => ({
5762
+ ...r.table_row,
5763
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5764
+ })) : rawResults;
5403
5765
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5404
5766
  }
5405
5767
  /**
@@ -5621,7 +5983,7 @@
5621
5983
  const idField = table[idInfo.fieldName];
5622
5984
  const tableName = drizzleOrm.getTableName(table);
5623
5985
  const qb = this.getQueryBuilder(tableName);
5624
- if (qb && !options.searchString) {
5986
+ if (qb && !options.searchString && !options.vectorSearch) {
5625
5987
  try {
5626
5988
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5627
5989
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5767,7 +6129,14 @@
5767
6129
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5768
6130
  const idInfo = idInfoArray[0];
5769
6131
  const idField = table[idInfo.fieldName];
5770
- let query = this.db.select().from(table).$dynamic();
6132
+ let vectorMeta;
6133
+ if (options.vectorSearch) {
6134
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6135
+ }
6136
+ let query = vectorMeta ? this.db.select({
6137
+ table_row: table,
6138
+ _distance: vectorMeta.distanceSelect
6139
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5771
6140
  const allConditions = [];
5772
6141
  if (options.searchString) {
5773
6142
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5778,12 +6147,17 @@
5778
6147
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5779
6148
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5780
6149
  }
6150
+ if (vectorMeta?.filter) {
6151
+ allConditions.push(vectorMeta.filter);
6152
+ }
5781
6153
  if (allConditions.length > 0) {
5782
6154
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5783
6155
  if (finalCondition) query = query.where(finalCondition);
5784
6156
  }
5785
6157
  const orderExpressions = [];
5786
- if (options.orderBy) {
6158
+ if (vectorMeta) {
6159
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
6160
+ } else if (options.orderBy) {
5787
6161
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5788
6162
  if (orderByField) {
5789
6163
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5791,10 +6165,17 @@
5791
6165
  }
5792
6166
  orderExpressions.push(drizzleOrm.desc(idField));
5793
6167
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5794
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6168
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5795
6169
  if (limitValue) query = query.limit(limitValue);
5796
6170
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5797
- return await query;
6171
+ const rawResults = await query;
6172
+ if (vectorMeta) {
6173
+ return rawResults.map((r) => ({
6174
+ ...r.table_row,
6175
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6176
+ }));
6177
+ }
6178
+ return rawResults;
5798
6179
  }
5799
6180
  /**
5800
6181
  * Check if the Drizzle instance has the relational query API available
@@ -6522,7 +6903,7 @@
6522
6903
  callbacks: void 0,
6523
6904
  propertyCallbacks: void 0
6524
6905
  };
6525
- const registryCollection = this.registry.getCollectionByPath(path2);
6906
+ const registryCollection = this.registry?.getCollectionByPath(path2);
6526
6907
  const resolvedCollection = registryCollection ? {
6527
6908
  ...collection,
6528
6909
  ...registryCollection
@@ -6548,7 +6929,8 @@
6548
6929
  startAfter,
6549
6930
  orderBy,
6550
6931
  searchString,
6551
- order
6932
+ order,
6933
+ vectorSearch
6552
6934
  }) {
6553
6935
  const entities = await this.entityService.fetchCollection(path2, {
6554
6936
  filter,
@@ -6558,7 +6940,8 @@
6558
6940
  offset,
6559
6941
  startAfter,
6560
6942
  databaseId: collection?.databaseId,
6561
- searchString
6943
+ searchString,
6944
+ vectorSearch
6562
6945
  });
6563
6946
  const {
6564
6947
  collection: resolvedCollection,
@@ -6570,7 +6953,8 @@
6570
6953
  user: this.user,
6571
6954
  driver: this,
6572
6955
  data: this.data,
6573
- client: this.client
6956
+ client: this.client,
6957
+ storageSource: this.client?.storage
6574
6958
  };
6575
6959
  return Promise.all(entities.map(async (entity) => {
6576
6960
  let fetched = entity;
@@ -6665,7 +7049,8 @@
6665
7049
  user: this.user,
6666
7050
  driver: this,
6667
7051
  data: this.data,
6668
- client: this.client
7052
+ client: this.client,
7053
+ storageSource: this.client?.storage
6669
7054
  };
6670
7055
  if (callbacks?.afterRead) {
6671
7056
  entity = await callbacks.afterRead({
@@ -6735,7 +7120,8 @@
6735
7120
  user: this.user,
6736
7121
  driver: this,
6737
7122
  data: this.data,
6738
- client: this.client
7123
+ client: this.client,
7124
+ storageSource: this.client?.storage
6739
7125
  };
6740
7126
  let previousValuesForHistory;
6741
7127
  if (status === "existing" && entityId) {
@@ -6884,7 +7270,8 @@
6884
7270
  user: this.user,
6885
7271
  driver: this,
6886
7272
  data: this.data,
6887
- client: this.client
7273
+ client: this.client,
7274
+ storageSource: this.client?.storage
6888
7275
  };
6889
7276
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
6890
7277
  let preventDefault = false;
@@ -7397,6 +7784,7 @@
7397
7784
  length: 255
7398
7785
  }),
7399
7786
  emailVerificationSentAt: pgCore.timestamp("email_verification_sent_at"),
7787
+ isAnonymous: pgCore.boolean("is_anonymous").default(false).notNull(),
7400
7788
  metadata: pgCore.jsonb("metadata").$type().default({}).notNull(),
7401
7789
  createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7402
7790
  updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
@@ -7411,8 +7799,7 @@
7411
7799
  }).notNull(),
7412
7800
  isAdmin: pgCore.boolean("is_admin").default(false).notNull(),
7413
7801
  defaultPermissions: pgCore.jsonb("default_permissions").$type(),
7414
- collectionPermissions: pgCore.jsonb("collection_permissions").$type(),
7415
- config: pgCore.jsonb("config").$type()
7802
+ collectionPermissions: pgCore.jsonb("collection_permissions").$type()
7416
7803
  });
7417
7804
  const userRoles2 = rolesTableCreator("user_roles", {
7418
7805
  userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
@@ -7484,6 +7871,48 @@
7484
7871
  }, (table) => ({
7485
7872
  uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
7486
7873
  }));
7874
+ const mfaFactors2 = rolesTableCreator("mfa_factors", {
7875
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7876
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7877
+ onDelete: "cascade"
7878
+ }),
7879
+ factorType: pgCore.varchar("factor_type", {
7880
+ length: 20
7881
+ }).notNull(),
7882
+ // 'totp'
7883
+ secretEncrypted: pgCore.varchar("secret_encrypted", {
7884
+ length: 500
7885
+ }).notNull(),
7886
+ friendlyName: pgCore.varchar("friendly_name", {
7887
+ length: 255
7888
+ }),
7889
+ verified: pgCore.boolean("verified").default(false).notNull(),
7890
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7891
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7892
+ });
7893
+ const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
7894
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7895
+ factorId: pgCore.uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7896
+ onDelete: "cascade"
7897
+ }),
7898
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7899
+ verifiedAt: pgCore.timestamp("verified_at"),
7900
+ ipAddress: pgCore.varchar("ip_address", {
7901
+ length: 45
7902
+ }),
7903
+ expiresAt: pgCore.timestamp("expires_at").notNull()
7904
+ });
7905
+ const recoveryCodes2 = rolesTableCreator("recovery_codes", {
7906
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7907
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7908
+ onDelete: "cascade"
7909
+ }),
7910
+ codeHash: pgCore.varchar("code_hash", {
7911
+ length: 255
7912
+ }).notNull(),
7913
+ usedAt: pgCore.timestamp("used_at"),
7914
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
7915
+ });
7487
7916
  return {
7488
7917
  rolesSchema,
7489
7918
  usersSchema: usersSchema2,
@@ -7493,7 +7922,10 @@
7493
7922
  refreshTokens: refreshTokens2,
7494
7923
  passwordResetTokens: passwordResetTokens2,
7495
7924
  appConfig: appConfig2,
7496
- userIdentities: userIdentities2
7925
+ userIdentities: userIdentities2,
7926
+ mfaFactors: mfaFactors2,
7927
+ mfaChallenges: mfaChallenges2,
7928
+ recoveryCodes: recoveryCodes2
7497
7929
  };
7498
7930
  }
7499
7931
  const defaultAuthSchema = createAuthSchema("rebase", "rebase");
@@ -7506,13 +7938,18 @@
7506
7938
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7507
7939
  const appConfig = defaultAuthSchema.appConfig;
7508
7940
  const userIdentities = defaultAuthSchema.userIdentities;
7941
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7942
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7943
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7509
7944
  const usersRelations = drizzleOrm.relations(users, ({
7510
7945
  many
7511
7946
  }) => ({
7512
7947
  userRoles: many(userRoles),
7513
7948
  refreshTokens: many(refreshTokens),
7514
7949
  passwordResetTokens: many(passwordResetTokens),
7515
- userIdentities: many(userIdentities)
7950
+ userIdentities: many(userIdentities),
7951
+ mfaFactors: many(mfaFactors),
7952
+ recoveryCodes: many(recoveryCodes)
7516
7953
  }));
7517
7954
  const rolesRelations = drizzleOrm.relations(roles, ({
7518
7955
  many
@@ -7555,6 +7992,32 @@
7555
7992
  references: [users.id]
7556
7993
  })
7557
7994
  }));
7995
+ const mfaFactorsRelations = drizzleOrm.relations(mfaFactors, ({
7996
+ one,
7997
+ many
7998
+ }) => ({
7999
+ user: one(users, {
8000
+ fields: [mfaFactors.userId],
8001
+ references: [users.id]
8002
+ }),
8003
+ challenges: many(mfaChallenges)
8004
+ }));
8005
+ const mfaChallengesRelations = drizzleOrm.relations(mfaChallenges, ({
8006
+ one
8007
+ }) => ({
8008
+ factor: one(mfaFactors, {
8009
+ fields: [mfaChallenges.factorId],
8010
+ references: [mfaFactors.id]
8011
+ })
8012
+ }));
8013
+ const recoveryCodesRelations = drizzleOrm.relations(recoveryCodes, ({
8014
+ one
8015
+ }) => ({
8016
+ user: one(users, {
8017
+ fields: [recoveryCodes.userId],
8018
+ references: [users.id]
8019
+ })
8020
+ }));
7558
8021
  const resolveColumnName = (propName, prop) => {
7559
8022
  if (prop && "columnName" in prop && typeof prop.columnName === "string") {
7560
8023
  return prop.columnName;
@@ -8079,167 +8542,84 @@
8079
8542
  fields: [${tableVarName}.${rel.localKey}],
8080
8543
  references: [${targetTableVar}.${getPrimaryKeyName(target)}],
8081
8544
  relationName: "${drizzleRelationName}"
8082
- })`);
8083
- } else if (rel.direction === "inverse") {
8084
- tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8085
- relationName: "${drizzleRelationName}"
8086
- })`);
8087
- }
8088
- } else if (rel.cardinality === "many") {
8089
- if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8090
- tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8091
- } else if (rel.through) {
8092
- const junctionTableVar = getTableVarName(rel.through.table);
8093
- tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8094
- } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8095
- try {
8096
- const targetCollection = rel.target();
8097
- const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8098
- const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8099
- if (correspondingRelation && correspondingRelation.through) {
8100
- const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8101
- tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8102
- } else {
8103
- console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8104
- }
8105
- } catch (e) {
8106
- console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
8107
- }
8108
- }
8109
- }
8110
- } catch (e) {
8111
- console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
8112
- }
8113
- }
8114
- for (const otherCollection of collections) {
8115
- if (otherCollection.slug === collection.slug) continue;
8116
- const otherRelations = resolveCollectionRelations(otherCollection);
8117
- for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
8118
- if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
8119
- try {
8120
- const otherTarget = otherRel.target();
8121
- if (otherTarget.slug === collection.slug) {
8122
- const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
8123
- const deduplicationKey = `${drizzleRelationName}::owning`;
8124
- if (!emittedRelationNames.has(deduplicationKey)) {
8125
- const otherTableVar = getTableVarName(getTableName(otherCollection));
8126
- const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
8127
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
8128
- fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
8129
- references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
8130
- relationName: "${drizzleRelationName}"
8131
- })`);
8132
- emittedRelationNames.add(deduplicationKey);
8133
- }
8134
- }
8135
- } catch (e) {
8136
- }
8137
- }
8138
- }
8139
- }
8140
- }
8141
- if (tableRelations.length > 0) {
8142
- const relVarName = `${tableVarName}Relations`;
8143
- schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
8144
- ${tableRelations.join(",\n")}
8145
- }));
8146
-
8147
- `;
8148
- if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
8149
- }
8150
- }
8151
- const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
8152
- `;
8153
- const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
8154
- `;
8155
- const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
8156
-
8157
- `;
8158
- schemaContent += tablesExport + enumsExport + relationsExport;
8159
- return schemaContent;
8160
- };
8161
- const defaultUsersCollection = {
8162
- name: "Users",
8163
- singularName: "User",
8164
- slug: "users",
8165
- table: "users",
8166
- icon: "Users",
8167
- group: "Settings",
8168
- properties: {
8169
- id: {
8170
- name: "ID",
8171
- type: "string",
8172
- isId: "uuid"
8173
- },
8174
- email: {
8175
- name: "Email",
8176
- type: "string",
8177
- validation: {
8178
- required: true,
8179
- unique: true
8180
- }
8181
- },
8182
- password_hash: {
8183
- name: "Password Hash",
8184
- type: "string",
8185
- ui: {
8186
- hideFromCollection: true
8187
- }
8188
- },
8189
- display_name: {
8190
- name: "Display Name",
8191
- type: "string"
8192
- },
8193
- photo_url: {
8194
- name: "Photo URL",
8195
- type: "string"
8196
- },
8197
- email_verified: {
8198
- name: "Email Verified",
8199
- type: "boolean",
8200
- defaultValue: false
8201
- },
8202
- email_verification_token: {
8203
- name: "Email Verification Token",
8204
- type: "string",
8205
- ui: {
8206
- hideFromCollection: true
8207
- }
8208
- },
8209
- email_verification_sent_at: {
8210
- name: "Email Verification Sent At",
8211
- type: "date",
8212
- ui: {
8213
- hideFromCollection: true
8214
- }
8215
- },
8216
- metadata: {
8217
- name: "Metadata",
8218
- type: "map",
8219
- defaultValue: {},
8220
- ui: {
8221
- hideFromCollection: true
8222
- }
8223
- },
8224
- created_at: {
8225
- name: "Created At",
8226
- type: "date",
8227
- autoValue: "on_create",
8228
- ui: {
8229
- readOnly: true,
8230
- hideFromCollection: true
8545
+ })`);
8546
+ } else if (rel.direction === "inverse") {
8547
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8548
+ relationName: "${drizzleRelationName}"
8549
+ })`);
8550
+ }
8551
+ } else if (rel.cardinality === "many") {
8552
+ if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8553
+ tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8554
+ } else if (rel.through) {
8555
+ const junctionTableVar = getTableVarName(rel.through.table);
8556
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8557
+ } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8558
+ try {
8559
+ const targetCollection = rel.target();
8560
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8561
+ const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8562
+ if (correspondingRelation && correspondingRelation.through) {
8563
+ const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8564
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8565
+ } else {
8566
+ console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8567
+ }
8568
+ } catch (e) {
8569
+ console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
8570
+ }
8571
+ }
8572
+ }
8573
+ } catch (e) {
8574
+ console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
8575
+ }
8231
8576
  }
8232
- },
8233
- updated_at: {
8234
- name: "Updated At",
8235
- type: "date",
8236
- autoValue: "on_update",
8237
- ui: {
8238
- readOnly: true,
8239
- hideFromCollection: true
8577
+ for (const otherCollection of collections) {
8578
+ if (otherCollection.slug === collection.slug) continue;
8579
+ const otherRelations = resolveCollectionRelations(otherCollection);
8580
+ for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
8581
+ if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
8582
+ try {
8583
+ const otherTarget = otherRel.target();
8584
+ if (otherTarget.slug === collection.slug) {
8585
+ const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
8586
+ const deduplicationKey = `${drizzleRelationName}::owning`;
8587
+ if (!emittedRelationNames.has(deduplicationKey)) {
8588
+ const otherTableVar = getTableVarName(getTableName(otherCollection));
8589
+ const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
8590
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
8591
+ fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
8592
+ references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
8593
+ relationName: "${drizzleRelationName}"
8594
+ })`);
8595
+ emittedRelationNames.add(deduplicationKey);
8596
+ }
8597
+ }
8598
+ } catch (e) {
8599
+ }
8600
+ }
8601
+ }
8240
8602
  }
8241
8603
  }
8604
+ if (tableRelations.length > 0) {
8605
+ const relVarName = `${tableVarName}Relations`;
8606
+ schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
8607
+ ${tableRelations.join(",\n")}
8608
+ }));
8609
+
8610
+ `;
8611
+ if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
8612
+ }
8242
8613
  }
8614
+ const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
8615
+ `;
8616
+ const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
8617
+ `;
8618
+ const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
8619
+
8620
+ `;
8621
+ schemaContent += tablesExport + enumsExport + relationsExport;
8622
+ return schemaContent;
8243
8623
  };
8244
8624
  const formatTerminalText = (text, options = {}) => {
8245
8625
  let codes = "";
@@ -8306,10 +8686,7 @@ ${tableRelations.join(",\n")}
8306
8686
  if (!collections || !Array.isArray(collections)) {
8307
8687
  collections = [];
8308
8688
  }
8309
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8310
- if (!hasUsersCollection) {
8311
- collections.push(defaultUsersCollection);
8312
- }
8689
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8313
8690
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8314
8691
  const schemaContent = await generateSchema(collections);
8315
8692
  if (outputPath) {
@@ -8370,6 +8747,13 @@ ${tableRelations.join(",\n")}
8370
8747
  this.entityService = new EntityService(db, registry);
8371
8748
  }
8372
8749
  clients = /* @__PURE__ */ new Map();
8750
+ // Broadcast channels: channel name → set of client IDs
8751
+ channels = /* @__PURE__ */ new Map();
8752
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8753
+ presence = /* @__PURE__ */ new Map();
8754
+ presenceInterval;
8755
+ static PRESENCE_TIMEOUT_MS = 3e4;
8756
+ // 30s
8373
8757
  entityService;
8374
8758
  // Enhanced subscriptions storage with full request parameters
8375
8759
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8496,8 +8880,19 @@ ${tableRelations.join(",\n")}
8496
8880
  }
8497
8881
  }
8498
8882
  }
8883
+ for (const [channel, members] of this.channels.entries()) {
8884
+ if (members.has(clientId)) {
8885
+ members.delete(clientId);
8886
+ this.removePresence(clientId, channel);
8887
+ if (members.size === 0) this.channels.delete(channel);
8888
+ }
8889
+ }
8890
+ for (const [channel] of this.presence) {
8891
+ this.removePresence(clientId, channel);
8892
+ }
8499
8893
  }
8500
8894
  async handleMessage(clientId, message, authContext) {
8895
+ const payload = message.payload;
8501
8896
  switch (message.type) {
8502
8897
  case "subscribe_collection":
8503
8898
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8508,6 +8903,25 @@ ${tableRelations.join(",\n")}
8508
8903
  case "unsubscribe":
8509
8904
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8510
8905
  break;
8906
+ case "join_channel":
8907
+ this.joinChannel(clientId, payload?.channel);
8908
+ break;
8909
+ case "leave_channel":
8910
+ this.leaveChannel(clientId, payload?.channel);
8911
+ break;
8912
+ case "broadcast":
8913
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8914
+ break;
8915
+ case "presence_track":
8916
+ this.joinChannel(clientId, payload?.channel);
8917
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8918
+ break;
8919
+ case "presence_untrack":
8920
+ this.removePresence(clientId, payload?.channel);
8921
+ break;
8922
+ case "presence_state":
8923
+ this.sendPresenceState(clientId, payload?.channel);
8924
+ break;
8511
8925
  default:
8512
8926
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8513
8927
  }
@@ -8965,6 +9379,132 @@ ${tableRelations.join(",\n")}
8965
9379
  return parentPaths;
8966
9380
  }
8967
9381
  // =============================================================================
9382
+ // Broadcast Channels
9383
+ // =============================================================================
9384
+ /** Join a broadcast channel */
9385
+ joinChannel(clientId, channel) {
9386
+ if (!this.channels.has(channel)) {
9387
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9388
+ }
9389
+ this.channels.get(channel).add(clientId);
9390
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9391
+ }
9392
+ /** Leave a broadcast channel */
9393
+ leaveChannel(clientId, channel) {
9394
+ const members = this.channels.get(channel);
9395
+ if (members) {
9396
+ members.delete(clientId);
9397
+ if (members.size === 0) this.channels.delete(channel);
9398
+ }
9399
+ this.removePresence(clientId, channel);
9400
+ }
9401
+ /** Broadcast a message to all clients in a channel except sender */
9402
+ broadcastToChannel(clientId, channel, event, payload) {
9403
+ const members = this.channels.get(channel);
9404
+ if (!members) return;
9405
+ const message = JSON.stringify({
9406
+ type: "broadcast",
9407
+ channel,
9408
+ event,
9409
+ payload
9410
+ });
9411
+ for (const memberId of members) {
9412
+ if (memberId === clientId) continue;
9413
+ const ws$1 = this.clients.get(memberId);
9414
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9415
+ ws$1.send(message);
9416
+ }
9417
+ }
9418
+ }
9419
+ // =============================================================================
9420
+ // Presence
9421
+ // =============================================================================
9422
+ /** Track presence in a channel */
9423
+ trackPresence(clientId, channel, state) {
9424
+ if (!this.presence.has(channel)) {
9425
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9426
+ }
9427
+ const channelPresence = this.presence.get(channel);
9428
+ channelPresence.set(clientId, {
9429
+ state,
9430
+ lastSeen: Date.now()
9431
+ });
9432
+ this.broadcastPresenceDiff(channel, {
9433
+ [clientId]: state
9434
+ }, {});
9435
+ this.ensurePresenceCleanup();
9436
+ }
9437
+ /** Remove presence from a channel */
9438
+ removePresence(clientId, channel) {
9439
+ const channelPresence = this.presence.get(channel);
9440
+ if (!channelPresence) return;
9441
+ const entry = channelPresence.get(clientId);
9442
+ if (entry) {
9443
+ channelPresence.delete(clientId);
9444
+ this.broadcastPresenceDiff(channel, {}, {
9445
+ [clientId]: entry.state
9446
+ });
9447
+ }
9448
+ if (channelPresence.size === 0) {
9449
+ this.presence.delete(channel);
9450
+ }
9451
+ }
9452
+ /** Send full presence state to a specific client */
9453
+ sendPresenceState(clientId, channel) {
9454
+ const channelPresence = this.presence.get(channel);
9455
+ const presences = {};
9456
+ if (channelPresence) {
9457
+ for (const [id, {
9458
+ state
9459
+ }] of channelPresence) {
9460
+ presences[id] = state;
9461
+ }
9462
+ }
9463
+ const ws$1 = this.clients.get(clientId);
9464
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9465
+ ws$1.send(JSON.stringify({
9466
+ type: "presence_state",
9467
+ channel,
9468
+ presences
9469
+ }));
9470
+ }
9471
+ }
9472
+ /** Broadcast presence diff (joins/leaves) to channel */
9473
+ broadcastPresenceDiff(channel, joins, leaves) {
9474
+ const members = this.channels.get(channel);
9475
+ if (!members) return;
9476
+ const message = JSON.stringify({
9477
+ type: "presence_diff",
9478
+ channel,
9479
+ joins,
9480
+ leaves
9481
+ });
9482
+ for (const memberId of members) {
9483
+ const ws$1 = this.clients.get(memberId);
9484
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9485
+ ws$1.send(message);
9486
+ }
9487
+ }
9488
+ }
9489
+ /** Periodic cleanup for stale presences */
9490
+ ensurePresenceCleanup() {
9491
+ if (this.presenceInterval) return;
9492
+ this.presenceInterval = setInterval(() => {
9493
+ const now = Date.now();
9494
+ for (const [channel, channelPresence] of this.presence) {
9495
+ for (const [clientId, entry] of channelPresence) {
9496
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9497
+ this.removePresence(clientId, channel);
9498
+ }
9499
+ }
9500
+ }
9501
+ if (this.presence.size === 0 && this.presenceInterval) {
9502
+ clearInterval(this.presenceInterval);
9503
+ this.presenceInterval = void 0;
9504
+ }
9505
+ }, 1e4);
9506
+ }
9507
+ // =============================================================================
8968
9508
  // Lifecycle / Cleanup
8969
9509
  // =============================================================================
8970
9510
  /**
@@ -8985,6 +9525,12 @@ ${tableRelations.join(",\n")}
8985
9525
  }
8986
9526
  this._subscriptions.clear();
8987
9527
  this.subscriptionCallbacks.clear();
9528
+ this.channels.clear();
9529
+ this.presence.clear();
9530
+ if (this.presenceInterval) {
9531
+ clearInterval(this.presenceInterval);
9532
+ this.presenceInterval = void 0;
9533
+ }
8988
9534
  await this.stopListening();
8989
9535
  this.clients.clear();
8990
9536
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9631,8 +10177,14 @@ ${tableRelations.join(",\n")}
9631
10177
  break;
9632
10178
  case "subscribe_collection":
9633
10179
  case "subscribe_entity":
9634
- case "unsubscribe": {
9635
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10180
+ case "unsubscribe":
10181
+ case "join_channel":
10182
+ case "leave_channel":
10183
+ case "broadcast":
10184
+ case "presence_track":
10185
+ case "presence_untrack":
10186
+ case "presence_state": {
10187
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9636
10188
  const session = clientSessions.get(clientId);
9637
10189
  const authContext = session?.user ? {
9638
10190
  userId: session.user.userId,
@@ -9751,11 +10303,6 @@ ${tableRelations.join(",\n")}
9751
10303
  create: true,
9752
10304
  edit: true,
9753
10305
  delete: true
9754
- },
9755
- config: {
9756
- createCollections: true,
9757
- editCollections: "all",
9758
- deleteCollections: "all"
9759
10306
  }
9760
10307
  }, {
9761
10308
  id: "editor",
@@ -9766,11 +10313,6 @@ ${tableRelations.join(",\n")}
9766
10313
  create: true,
9767
10314
  edit: true,
9768
10315
  delete: true
9769
- },
9770
- config: {
9771
- createCollections: true,
9772
- editCollections: "own",
9773
- deleteCollections: "own"
9774
10316
  }
9775
10317
  }, {
9776
10318
  id: "viewer",
@@ -9781,11 +10323,10 @@ ${tableRelations.join(",\n")}
9781
10323
  create: false,
9782
10324
  edit: false,
9783
10325
  delete: false
9784
- },
9785
- config: null
10326
+ }
9786
10327
  }];
9787
10328
  async function ensureAuthTablesExist(db, registry) {
9788
- console.log("🔍 Checking auth tables...");
10329
+ serverCore.logger.info("🔍 Checking auth tables...");
9789
10330
  try {
9790
10331
  let usersTableName = '"users"';
9791
10332
  let userIdType = "TEXT";
@@ -9855,7 +10396,6 @@ ${tableRelations.join(",\n")}
9855
10396
  is_admin BOOLEAN DEFAULT FALSE,
9856
10397
  default_permissions JSONB,
9857
10398
  collection_permissions JSONB,
9858
- config JSONB,
9859
10399
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9860
10400
  )
9861
10401
  `);
@@ -9938,34 +10478,85 @@ ${tableRelations.join(",\n")}
9938
10478
  `);
9939
10479
  });
9940
10480
  await seedDefaultRoles(db, rolesTableName);
9941
- console.log("✅ Auth tables ready");
10481
+ await db.execute(drizzleOrm.sql`
10482
+ ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
10483
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10484
+ `);
10485
+ const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
10486
+ const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
10487
+ const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
10488
+ await db.execute(drizzleOrm.sql`
10489
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaFactorsTableName)} (
10490
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10491
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10492
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10493
+ secret_encrypted TEXT NOT NULL,
10494
+ friendly_name TEXT,
10495
+ verified BOOLEAN DEFAULT FALSE,
10496
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10497
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10498
+ )
10499
+ `);
10500
+ await db.execute(drizzleOrm.sql`
10501
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10502
+ ON ${drizzleOrm.sql.raw(mfaFactorsTableName)}(user_id)
10503
+ `);
10504
+ await db.execute(drizzleOrm.sql`
10505
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaChallengesTableName)} (
10506
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10507
+ factor_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10508
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10509
+ verified_at TIMESTAMP WITH TIME ZONE,
10510
+ ip_address TEXT,
10511
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10512
+ )
10513
+ `);
10514
+ await db.execute(drizzleOrm.sql`
10515
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10516
+ ON ${drizzleOrm.sql.raw(mfaChallengesTableName)}(factor_id)
10517
+ `);
10518
+ await db.execute(drizzleOrm.sql`
10519
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(recoveryCodesTableName)} (
10520
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10521
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10522
+ code_hash TEXT NOT NULL,
10523
+ used_at TIMESTAMP WITH TIME ZONE,
10524
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10525
+ )
10526
+ `);
10527
+ await db.execute(drizzleOrm.sql`
10528
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10529
+ ON ${drizzleOrm.sql.raw(recoveryCodesTableName)}(user_id)
10530
+ `);
10531
+ serverCore.logger.info("✅ Auth tables ready");
9942
10532
  } catch (error) {
9943
- console.error("❌ Failed to create auth tables:", error);
9944
- console.warn("⚠️ Continuing without creating auth tables.");
10533
+ serverCore.logger.error("❌ Failed to create auth tables", {
10534
+ error
10535
+ });
10536
+ serverCore.logger.warn("⚠️ Continuing without creating auth tables.");
9945
10537
  }
9946
10538
  }
9947
10539
  async function seedDefaultRoles(db, rolesTableName) {
9948
10540
  const result = await db.execute(drizzleOrm.sql`SELECT COUNT(*) as count FROM ${drizzleOrm.sql.raw(rolesTableName)}`);
9949
10541
  const count = parseInt(result.rows[0]?.count || "0", 10);
9950
10542
  if (count > 0) {
9951
- console.log(`📋 Found ${count} existing roles`);
10543
+ serverCore.logger.info(`📋 Found ${count} existing roles`);
9952
10544
  return;
9953
10545
  }
9954
- console.log("🌱 Seeding default roles...");
10546
+ serverCore.logger.info("🌱 Seeding default roles...");
9955
10547
  for (const role of DEFAULT_ROLES) {
9956
10548
  await db.execute(drizzleOrm.sql`
9957
- INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10549
+ INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
9958
10550
  VALUES (
9959
10551
  ${role.id},
9960
10552
  ${role.name},
9961
10553
  ${role.is_admin},
9962
- ${JSON.stringify(role.default_permissions)}::jsonb,
9963
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10554
+ ${JSON.stringify(role.default_permissions)}::jsonb
9964
10555
  )
9965
10556
  ON CONFLICT (id) DO NOTHING
9966
10557
  `);
9967
10558
  }
9968
- console.log("✅ Default roles created: admin, editor, viewer");
10559
+ serverCore.logger.info("✅ Default roles created: admin, editor, viewer");
9969
10560
  }
9970
10561
  function getColumnKey(table, ...keys2) {
9971
10562
  if (!table) return void 0;
@@ -10019,12 +10610,13 @@ ${tableRelations.join(",\n")}
10019
10610
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10020
10611
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10021
10612
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10613
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10022
10614
  const createdAt = row.created_at ?? row.createdAt;
10023
10615
  const updatedAt = row.updated_at ?? row.updatedAt;
10024
10616
  const metadata = {
10025
10617
  ...row.metadata || {}
10026
10618
  };
10027
- const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
10619
+ const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
10028
10620
  for (const [key, val] of Object.entries(row)) {
10029
10621
  if (!knownKeys.has(key)) {
10030
10622
  const camelKey = camelCase(key);
@@ -10040,6 +10632,7 @@ ${tableRelations.join(",\n")}
10040
10632
  emailVerified,
10041
10633
  emailVerificationToken,
10042
10634
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10635
+ isAnonymous,
10043
10636
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10044
10637
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10045
10638
  metadata
@@ -10056,6 +10649,7 @@ ${tableRelations.join(",\n")}
10056
10649
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10057
10650
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10058
10651
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10652
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10059
10653
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10060
10654
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10061
10655
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10067,6 +10661,7 @@ ${tableRelations.join(",\n")}
10067
10661
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10068
10662
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10069
10663
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10664
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10070
10665
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10071
10666
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10072
10667
  const metadata = {
@@ -10075,7 +10670,7 @@ ${tableRelations.join(",\n")}
10075
10670
  const remainingMetadata = {};
10076
10671
  for (const [key, val] of Object.entries(metadata)) {
10077
10672
  const tableColKey = getColumnKey(this.usersTable, key);
10078
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10673
+ if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10079
10674
  payload[tableColKey] = val;
10080
10675
  } else {
10081
10676
  remainingMetadata[key] = val;
@@ -10263,7 +10858,7 @@ ${tableRelations.join(",\n")}
10263
10858
  async getUserRoles(userId) {
10264
10859
  const rolesSchema = pgCore.getTableConfig(this.rolesTable).schema || "public";
10265
10860
  const result = await this.db.execute(drizzleOrm.sql`
10266
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10861
+ SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
10267
10862
  FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."roles"`)} r
10268
10863
  INNER JOIN ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10269
10864
  WHERE ur.user_id = ${userId}
@@ -10273,8 +10868,7 @@ ${tableRelations.join(",\n")}
10273
10868
  name: row.name,
10274
10869
  isAdmin: row.is_admin,
10275
10870
  defaultPermissions: row.default_permissions,
10276
- collectionPermissions: row.collection_permissions,
10277
- config: row.config
10871
+ collectionPermissions: row.collection_permissions
10278
10872
  }));
10279
10873
  }
10280
10874
  /**
@@ -10340,7 +10934,7 @@ ${tableRelations.join(",\n")}
10340
10934
  async getRoleById(id) {
10341
10935
  const tableName = this.getQualifiedRolesTableName();
10342
10936
  const result = await this.db.execute(drizzleOrm.sql`
10343
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10937
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10344
10938
  FROM ${drizzleOrm.sql.raw(tableName)}
10345
10939
  WHERE id = ${id}
10346
10940
  `);
@@ -10351,14 +10945,13 @@ ${tableRelations.join(",\n")}
10351
10945
  name: row.name,
10352
10946
  isAdmin: row.is_admin,
10353
10947
  defaultPermissions: row.default_permissions,
10354
- collectionPermissions: row.collection_permissions,
10355
- config: row.config
10948
+ collectionPermissions: row.collection_permissions
10356
10949
  };
10357
10950
  }
10358
10951
  async listRoles() {
10359
10952
  const tableName = this.getQualifiedRolesTableName();
10360
10953
  const result = await this.db.execute(drizzleOrm.sql`
10361
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10954
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10362
10955
  FROM ${drizzleOrm.sql.raw(tableName)}
10363
10956
  ORDER BY name
10364
10957
  `);
@@ -10367,23 +10960,21 @@ ${tableRelations.join(",\n")}
10367
10960
  name: row.name,
10368
10961
  isAdmin: row.is_admin,
10369
10962
  defaultPermissions: row.default_permissions,
10370
- collectionPermissions: row.collection_permissions,
10371
- config: row.config
10963
+ collectionPermissions: row.collection_permissions
10372
10964
  }));
10373
10965
  }
10374
10966
  async createRole(data) {
10375
10967
  const tableName = this.getQualifiedRolesTableName();
10376
10968
  const result = await this.db.execute(drizzleOrm.sql`
10377
- INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10969
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
10378
10970
  VALUES (
10379
10971
  ${data.id},
10380
10972
  ${data.name},
10381
10973
  ${data.isAdmin ?? false},
10382
10974
  ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10383
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10384
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10975
+ ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
10385
10976
  )
10386
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10977
+ RETURNING id, name, is_admin, default_permissions, collection_permissions
10387
10978
  `);
10388
10979
  const row = result.rows[0];
10389
10980
  return {
@@ -10391,8 +10982,7 @@ ${tableRelations.join(",\n")}
10391
10982
  name: row.name,
10392
10983
  isAdmin: row.is_admin,
10393
10984
  defaultPermissions: row.default_permissions,
10394
- collectionPermissions: row.collection_permissions,
10395
- config: row.config
10985
+ collectionPermissions: row.collection_permissions
10396
10986
  };
10397
10987
  }
10398
10988
  async updateRole(id, data) {
@@ -10405,8 +10995,7 @@ ${tableRelations.join(",\n")}
10405
10995
  name = ${data.name ?? existing.name},
10406
10996
  is_admin = ${data.isAdmin ?? existing.isAdmin},
10407
10997
  default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10408
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10409
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10998
+ collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
10410
10999
  WHERE id = ${id}
10411
11000
  `);
10412
11001
  return this.getRoleById(id);
@@ -10685,8 +11274,7 @@ ${tableRelations.join(",\n")}
10685
11274
  return this.roleService.createRole({
10686
11275
  ...data,
10687
11276
  defaultPermissions: data.defaultPermissions ?? null,
10688
- collectionPermissions: data.collectionPermissions ?? null,
10689
- config: data.config ?? null
11277
+ collectionPermissions: data.collectionPermissions ?? null
10690
11278
  });
10691
11279
  }
10692
11280
  async updateRole(id, data) {
@@ -10729,6 +11317,219 @@ ${tableRelations.join(",\n")}
10729
11317
  async deleteExpiredTokens() {
10730
11318
  await this.tokenRepository.deleteExpiredTokens();
10731
11319
  }
11320
+ // MFA operations (delegate to MfaService)
11321
+ _mfaService = null;
11322
+ getMfaService() {
11323
+ if (!this._mfaService) {
11324
+ this._mfaService = new MfaService(this.db);
11325
+ }
11326
+ return this._mfaService;
11327
+ }
11328
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11329
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11330
+ }
11331
+ async getMfaFactors(userId) {
11332
+ return this.getMfaService().getMfaFactors(userId);
11333
+ }
11334
+ async getMfaFactorById(factorId) {
11335
+ return this.getMfaService().getMfaFactorById(factorId);
11336
+ }
11337
+ async verifyMfaFactor(factorId) {
11338
+ return this.getMfaService().verifyMfaFactor(factorId);
11339
+ }
11340
+ async deleteMfaFactor(factorId, userId) {
11341
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11342
+ }
11343
+ async createMfaChallenge(factorId, ipAddress) {
11344
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11345
+ }
11346
+ async getMfaChallengeById(challengeId) {
11347
+ return this.getMfaService().getMfaChallengeById(challengeId);
11348
+ }
11349
+ async verifyMfaChallenge(challengeId) {
11350
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11351
+ }
11352
+ async createRecoveryCodes(userId, codeHashes) {
11353
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11354
+ }
11355
+ async useRecoveryCode(userId, codeHash) {
11356
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11357
+ }
11358
+ async getUnusedRecoveryCodeCount(userId) {
11359
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11360
+ }
11361
+ async deleteAllRecoveryCodes(userId) {
11362
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11363
+ }
11364
+ async hasVerifiedMfaFactors(userId) {
11365
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11366
+ }
11367
+ }
11368
+ class MfaService {
11369
+ constructor(db, schemaName = "rebase") {
11370
+ this.db = db;
11371
+ this.schemaName = schemaName;
11372
+ }
11373
+ qualify(tableName) {
11374
+ return `"${this.schemaName}"."${tableName}"`;
11375
+ }
11376
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11377
+ const tableName = this.qualify("mfa_factors");
11378
+ const result = await this.db.execute(drizzleOrm.sql`
11379
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11380
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11381
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11382
+ `);
11383
+ const row = result.rows[0];
11384
+ return {
11385
+ id: row.id,
11386
+ userId: row.user_id,
11387
+ factorType: row.factor_type,
11388
+ friendlyName: row.friendly_name ?? void 0,
11389
+ verified: row.verified,
11390
+ createdAt: new Date(row.created_at),
11391
+ updatedAt: new Date(row.updated_at)
11392
+ };
11393
+ }
11394
+ async getMfaFactors(userId) {
11395
+ const tableName = this.qualify("mfa_factors");
11396
+ const result = await this.db.execute(drizzleOrm.sql`
11397
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11398
+ FROM ${drizzleOrm.sql.raw(tableName)}
11399
+ WHERE user_id = ${userId}
11400
+ ORDER BY created_at
11401
+ `);
11402
+ return result.rows.map((row) => ({
11403
+ id: row.id,
11404
+ userId: row.user_id,
11405
+ factorType: row.factor_type,
11406
+ friendlyName: row.friendly_name ?? void 0,
11407
+ verified: row.verified,
11408
+ createdAt: new Date(row.created_at),
11409
+ updatedAt: new Date(row.updated_at)
11410
+ }));
11411
+ }
11412
+ async getMfaFactorById(factorId) {
11413
+ const tableName = this.qualify("mfa_factors");
11414
+ const result = await this.db.execute(drizzleOrm.sql`
11415
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11416
+ FROM ${drizzleOrm.sql.raw(tableName)}
11417
+ WHERE id = ${factorId}
11418
+ `);
11419
+ if (result.rows.length === 0) return null;
11420
+ const row = result.rows[0];
11421
+ return {
11422
+ id: row.id,
11423
+ userId: row.user_id,
11424
+ factorType: row.factor_type,
11425
+ secretEncrypted: row.secret_encrypted,
11426
+ friendlyName: row.friendly_name ?? void 0,
11427
+ verified: row.verified,
11428
+ createdAt: new Date(row.created_at),
11429
+ updatedAt: new Date(row.updated_at)
11430
+ };
11431
+ }
11432
+ async verifyMfaFactor(factorId) {
11433
+ const tableName = this.qualify("mfa_factors");
11434
+ await this.db.execute(drizzleOrm.sql`
11435
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11436
+ SET verified = TRUE, updated_at = NOW()
11437
+ WHERE id = ${factorId}
11438
+ `);
11439
+ }
11440
+ async deleteMfaFactor(factorId, userId) {
11441
+ const tableName = this.qualify("mfa_factors");
11442
+ await this.db.execute(drizzleOrm.sql`
11443
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)}
11444
+ WHERE id = ${factorId} AND user_id = ${userId}
11445
+ `);
11446
+ }
11447
+ async createMfaChallenge(factorId, ipAddress) {
11448
+ const tableName = this.qualify("mfa_challenges");
11449
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11450
+ const result = await this.db.execute(drizzleOrm.sql`
11451
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (factor_id, ip_address, expires_at)
11452
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11453
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11454
+ `);
11455
+ const row = result.rows[0];
11456
+ return {
11457
+ id: row.id,
11458
+ factorId: row.factor_id,
11459
+ createdAt: new Date(row.created_at),
11460
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11461
+ ipAddress: row.ip_address ?? void 0
11462
+ };
11463
+ }
11464
+ async getMfaChallengeById(challengeId) {
11465
+ const tableName = this.qualify("mfa_challenges");
11466
+ const result = await this.db.execute(drizzleOrm.sql`
11467
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11468
+ FROM ${drizzleOrm.sql.raw(tableName)}
11469
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11470
+ `);
11471
+ if (result.rows.length === 0) return null;
11472
+ const row = result.rows[0];
11473
+ return {
11474
+ id: row.id,
11475
+ factorId: row.factor_id,
11476
+ createdAt: new Date(row.created_at),
11477
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11478
+ ipAddress: row.ip_address ?? void 0
11479
+ };
11480
+ }
11481
+ async verifyMfaChallenge(challengeId) {
11482
+ const tableName = this.qualify("mfa_challenges");
11483
+ await this.db.execute(drizzleOrm.sql`
11484
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11485
+ SET verified_at = NOW()
11486
+ WHERE id = ${challengeId}
11487
+ `);
11488
+ }
11489
+ async createRecoveryCodes(userId, codeHashes) {
11490
+ const tableName = this.qualify("recovery_codes");
11491
+ await this.db.execute(drizzleOrm.sql`
11492
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11493
+ `);
11494
+ for (const hash of codeHashes) {
11495
+ await this.db.execute(drizzleOrm.sql`
11496
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, code_hash)
11497
+ VALUES (${userId}, ${hash})
11498
+ `);
11499
+ }
11500
+ }
11501
+ async useRecoveryCode(userId, codeHash) {
11502
+ const tableName = this.qualify("recovery_codes");
11503
+ const result = await this.db.execute(drizzleOrm.sql`
11504
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11505
+ SET used_at = NOW()
11506
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11507
+ RETURNING id
11508
+ `);
11509
+ return result.rows.length > 0;
11510
+ }
11511
+ async getUnusedRecoveryCodeCount(userId) {
11512
+ const tableName = this.qualify("recovery_codes");
11513
+ const result = await this.db.execute(drizzleOrm.sql`
11514
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11515
+ WHERE user_id = ${userId} AND used_at IS NULL
11516
+ `);
11517
+ return result.rows[0].count;
11518
+ }
11519
+ async deleteAllRecoveryCodes(userId) {
11520
+ const tableName = this.qualify("recovery_codes");
11521
+ await this.db.execute(drizzleOrm.sql`
11522
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11523
+ `);
11524
+ }
11525
+ async hasVerifiedMfaFactors(userId) {
11526
+ const tableName = this.qualify("mfa_factors");
11527
+ const result = await this.db.execute(drizzleOrm.sql`
11528
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11529
+ WHERE user_id = ${userId} AND verified = TRUE
11530
+ `);
11531
+ return result.rows[0].count > 0;
11532
+ }
10732
11533
  }
10733
11534
  const DEFAULT_RETENTION = {
10734
11535
  maxEntries: 200,
@@ -10939,7 +11740,7 @@ ${tableRelations.join(",\n")}
10939
11740
  const registry = new PostgresCollectionRegistry();
10940
11741
  if (collections) {
10941
11742
  registry.registerMultiple(collections);
10942
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11743
+ serverCore.logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
10943
11744
  }
10944
11745
  if (pgConfig.schema?.tables) {
10945
11746
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -10965,10 +11766,28 @@ ${tableRelations.join(",\n")}
10965
11766
  try {
10966
11767
  await schemaAwareDb.execute(drizzleOrm.sql`SELECT 1`);
10967
11768
  } catch (err) {
10968
- console.error("❌ Failed to connect to PostgreSQL:", err);
10969
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11769
+ serverCore.logger.error("❌ Failed to connect to PostgreSQL", {
11770
+ error: err
11771
+ });
11772
+ serverCore.logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
10970
11773
  }
10971
11774
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11775
+ let readDb;
11776
+ const readUrl = process.env.DATABASE_READ_URL;
11777
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11778
+ try {
11779
+ const {
11780
+ createReadReplicaConnection: createReadReplicaConnection2
11781
+ } = await Promise.resolve().then(() => connection);
11782
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11783
+ readDb = readResources.db;
11784
+ serverCore.logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11785
+ } catch (err) {
11786
+ serverCore.logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11787
+ error: err
11788
+ });
11789
+ }
11790
+ }
10972
11791
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
10973
11792
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
10974
11793
  realtimeService.setDataDriver(driver);
@@ -10976,18 +11795,24 @@ ${tableRelations.join(",\n")}
10976
11795
  try {
10977
11796
  await driver.branchService.ensureBranchMetadataTable();
10978
11797
  } catch (err) {
10979
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11798
+ serverCore.logger.warn("⚠️ Could not initialize branch metadata table", {
11799
+ error: err
11800
+ });
10980
11801
  }
10981
11802
  }
10982
- if (pgConfig.connectionString) {
11803
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11804
+ if (directUrl) {
10983
11805
  try {
10984
- await realtimeService.startListening(pgConfig.connectionString);
11806
+ await realtimeService.startListening(directUrl);
10985
11807
  } catch (err) {
10986
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11808
+ serverCore.logger.warn("⚠️ Cross-instance realtime could not be started", {
11809
+ error: err
11810
+ });
10987
11811
  }
10988
11812
  }
10989
11813
  const internals = {
10990
11814
  db: schemaAwareDb,
11815
+ readDb,
10991
11816
  registry,
10992
11817
  realtimeService,
10993
11818
  driver,
@@ -11124,14 +11949,22 @@ ${tableRelations.join(",\n")}
11124
11949
  exports2.RealtimeService = RealtimeService;
11125
11950
  exports2.appConfig = appConfig;
11126
11951
  exports2.createAuthSchema = createAuthSchema;
11952
+ exports2.createDirectDatabaseConnection = createDirectDatabaseConnection;
11127
11953
  exports2.createPostgresAdapter = createPostgresAdapter;
11128
11954
  exports2.createPostgresBootstrapper = createPostgresBootstrapper;
11129
11955
  exports2.createPostgresDatabaseConnection = createPostgresDatabaseConnection;
11130
11956
  exports2.createPostgresWebSocket = createPostgresWebSocket;
11957
+ exports2.createReadReplicaConnection = createReadReplicaConnection;
11131
11958
  exports2.generateSchema = generateSchema;
11959
+ exports2.mfaChallenges = mfaChallenges;
11960
+ exports2.mfaChallengesRelations = mfaChallengesRelations;
11961
+ exports2.mfaFactors = mfaFactors;
11962
+ exports2.mfaFactorsRelations = mfaFactorsRelations;
11132
11963
  exports2.passwordResetTokens = passwordResetTokens;
11133
11964
  exports2.passwordResetTokensRelations = passwordResetTokensRelations;
11134
11965
  exports2.rebaseSchema = rebaseSchema;
11966
+ exports2.recoveryCodes = recoveryCodes;
11967
+ exports2.recoveryCodesRelations = recoveryCodesRelations;
11135
11968
  exports2.refreshTokens = refreshTokens;
11136
11969
  exports2.refreshTokensRelations = refreshTokensRelations;
11137
11970
  exports2.roles = roles;