@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.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Pool, Client } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
4
- import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, varchar, boolean, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
4
+ import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, boolean, varchar, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
5
5
  import { createHash, randomUUID } from "crypto";
6
6
  import * as fs from "fs";
7
7
  import { promises } from "fs";
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
11
11
  import { WebSocket, WebSocketServer } from "ws";
12
12
  import { EventEmitter } from "events";
13
13
  import { inspect } from "util";
14
- import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
14
+ import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
15
15
  const DEFAULT_POOL = {
16
16
  max: 20,
17
17
  idleTimeoutMillis: 3e4,
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
51
51
  connectionString
52
52
  };
53
53
  }
54
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
55
+ const opts = {
56
+ ...DEFAULT_POOL,
57
+ max: 5,
58
+ ...poolConfig
59
+ };
60
+ const pgPoolConfig = {
61
+ connectionString,
62
+ max: opts.max,
63
+ idleTimeoutMillis: opts.idleTimeoutMillis,
64
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
65
+ query_timeout: opts.queryTimeout,
66
+ statement_timeout: opts.statementTimeout,
67
+ keepAlive: opts.keepAlive,
68
+ keepAliveInitialDelayMillis: 0
69
+ };
70
+ const pool = new Pool(pgPoolConfig);
71
+ pool.on("error", (err) => {
72
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
73
+ });
74
+ const db = schema ? drizzle(pool, {
75
+ schema
76
+ }) : drizzle(pool);
77
+ return {
78
+ db,
79
+ pool,
80
+ connectionString
81
+ };
82
+ }
83
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
84
+ const opts = {
85
+ ...DEFAULT_POOL,
86
+ max: 10,
87
+ ...poolConfig
88
+ };
89
+ const pgPoolConfig = {
90
+ connectionString,
91
+ max: opts.max,
92
+ idleTimeoutMillis: opts.idleTimeoutMillis,
93
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
94
+ query_timeout: opts.queryTimeout,
95
+ statement_timeout: opts.statementTimeout,
96
+ keepAlive: opts.keepAlive,
97
+ keepAliveInitialDelayMillis: 0
98
+ };
99
+ const pool = new Pool(pgPoolConfig);
100
+ pool.on("error", (err) => {
101
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
102
+ });
103
+ const db = schema ? drizzle(pool, {
104
+ schema
105
+ }) : drizzle(pool);
106
+ return {
107
+ db,
108
+ pool,
109
+ connectionString
110
+ };
111
+ }
112
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
113
+ __proto__: null,
114
+ createDirectDatabaseConnection,
115
+ createPostgresDatabaseConnection,
116
+ createReadReplicaConnection
117
+ }, Symbol.toStringTag, { value: "Module" }));
54
118
  class Vector {
55
119
  value;
56
120
  constructor(value) {
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
970
1034
  return output;
971
1035
  }
972
1036
  for (const key in source) {
1037
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1038
+ continue;
1039
+ }
973
1040
  if (Object.prototype.hasOwnProperty.call(source, key)) {
974
1041
  const sourceValue = source[key];
975
1042
  const outputValue = output[key];
@@ -1158,118 +1225,6 @@ function enumToObjectEntries(enumValues) {
1158
1225
  });
1159
1226
  }
1160
1227
  }
1161
- function getSubcollections(collection) {
1162
- if (collection.childCollections) {
1163
- return collection.childCollections() ?? [];
1164
- }
1165
- if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
1166
- return collection.subcollections() ?? [];
1167
- }
1168
- if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
1169
- const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
1170
- return manyRelations.map((r) => {
1171
- const target = r.target();
1172
- if (!target) return void 0;
1173
- const relationKey = r.relationName || target.slug;
1174
- let customName;
1175
- if (collection.properties) {
1176
- const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1177
- if (prop && prop[1].name) {
1178
- customName = prop[1].name;
1179
- }
1180
- }
1181
- const baseOverrides = {
1182
- slug: relationKey
1183
- };
1184
- if (customName) {
1185
- baseOverrides.name = customName;
1186
- baseOverrides.singularName = customName;
1187
- }
1188
- const targetWithOverrides = {
1189
- ...target,
1190
- ...baseOverrides
1191
- };
1192
- return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1193
- }).filter((c) => Boolean(c));
1194
- }
1195
- return [];
1196
- }
1197
- function hasPropertyCallbacks(properties, callbackName) {
1198
- if (!properties) return false;
1199
- for (const property of Object.values(properties)) {
1200
- if (property.callbacks?.[callbackName]) return true;
1201
- if (property.type === "map" && property.properties) {
1202
- if (hasPropertyCallbacks(property.properties, callbackName)) return true;
1203
- } else if (property.type === "array" && property.of) {
1204
- const ofs = Array.isArray(property.of) ? property.of : [property.of];
1205
- for (const of of ofs) {
1206
- if (of.callbacks?.[callbackName]) return true;
1207
- if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
1208
- }
1209
- }
1210
- }
1211
- return false;
1212
- }
1213
- async function processProperties(properties, values, previousValues, propsContext, callbackName) {
1214
- if (!values || typeof values !== "object") return values;
1215
- const result = {
1216
- ...values
1217
- };
1218
- for (const [key, property] of Object.entries(properties)) {
1219
- if (result[key] === void 0) continue;
1220
- let currentValue = result[key];
1221
- const previousValue = previousValues?.[key];
1222
- if (property.type === "array" && Array.isArray(currentValue)) {
1223
- if (property.of && !Array.isArray(property.of)) {
1224
- currentValue = await Promise.all(currentValue.map(async (item, index) => {
1225
- const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
1226
- const singlePropData = {
1227
- "_tmp": property.of
1228
- };
1229
- const res = await processProperties(singlePropData, {
1230
- "_tmp": item
1231
- }, {
1232
- "_tmp": prevItem
1233
- }, propsContext, callbackName);
1234
- return res["_tmp"];
1235
- }));
1236
- }
1237
- } else if (property.type === "map" && property.properties && typeof currentValue === "object") {
1238
- currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
1239
- }
1240
- if (property.callbacks?.[callbackName]) {
1241
- const cbRes = await Promise.resolve(property.callbacks[callbackName]({
1242
- ...propsContext,
1243
- value: currentValue,
1244
- previousValue
1245
- }));
1246
- if (cbRes !== void 0) {
1247
- currentValue = cbRes;
1248
- }
1249
- }
1250
- result[key] = currentValue;
1251
- }
1252
- return result;
1253
- }
1254
- const buildPropertyCallbacks = (properties) => {
1255
- if (!properties) return void 0;
1256
- const propertyCallbacks = {};
1257
- if (hasPropertyCallbacks(properties, "afterRead")) {
1258
- propertyCallbacks.afterRead = async (props) => {
1259
- const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1260
- return {
1261
- ...props.entity,
1262
- values: processedValues
1263
- };
1264
- };
1265
- }
1266
- if (hasPropertyCallbacks(properties, "beforeSave")) {
1267
- propertyCallbacks.beforeSave = async (props) => {
1268
- return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
1269
- };
1270
- }
1271
- return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
1272
- };
1273
1228
  function sanitizeRelation(relation, sourceCollection, resolveCollection) {
1274
1229
  if (!relation.target) {
1275
1230
  throw new Error("Relation is missing a `target` collection.");
@@ -1301,6 +1256,8 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
1301
1256
  } else {
1302
1257
  targetCollection = evaluated;
1303
1258
  }
1259
+ } else if (rawTarget && typeof rawTarget === "object") {
1260
+ targetCollection = rawTarget;
1304
1261
  }
1305
1262
  if (!targetCollection) {
1306
1263
  throw new Error("Relation is missing a valid `target` collection.");
@@ -1420,11 +1377,14 @@ function resolveCollectionRelations(collection) {
1420
1377
  const registeredRelationNames = /* @__PURE__ */ new Set();
1421
1378
  if (relCollection.relations) {
1422
1379
  relCollection.relations.forEach((relation) => {
1423
- const normalizedRelation = sanitizeRelation(relation, collection);
1424
- const relationKey = normalizedRelation.relationName;
1425
- if (relationKey) {
1426
- relations2[relationKey] = normalizedRelation;
1427
- registeredRelationNames.add(relationKey);
1380
+ try {
1381
+ const normalizedRelation = sanitizeRelation(relation, collection);
1382
+ const relationKey = normalizedRelation.relationName;
1383
+ if (relationKey) {
1384
+ relations2[relationKey] = normalizedRelation;
1385
+ registeredRelationNames.add(relationKey);
1386
+ }
1387
+ } catch (e) {
1428
1388
  }
1429
1389
  });
1430
1390
  }
@@ -1472,12 +1432,8 @@ function resolvePropertyRelation({
1472
1432
  overrides: relProp.overrides
1473
1433
  };
1474
1434
  }
1475
- const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
1476
- if (!relation) {
1477
- console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1478
- return void 0;
1479
- }
1480
- return relation;
1435
+ console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
1436
+ return void 0;
1481
1437
  }
1482
1438
  function getTableName(collection) {
1483
1439
  if (getDataSourceCapabilities(collection.driver).supportsRelations) {
@@ -1504,6 +1460,119 @@ function findRelation(resolvedRelations, key) {
1504
1460
  if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
1505
1461
  return void 0;
1506
1462
  }
1463
+ function getSubcollections(collection) {
1464
+ if (collection.childCollections) {
1465
+ return collection.childCollections() ?? [];
1466
+ }
1467
+ if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
1468
+ return collection.subcollections() ?? [];
1469
+ }
1470
+ if (getDataSourceCapabilities(collection.driver).supportsRelations) {
1471
+ const resolvedRelations = resolveCollectionRelations(collection);
1472
+ const manyRelations = Object.values(resolvedRelations).filter((r) => r.cardinality === "many");
1473
+ return manyRelations.map((r) => {
1474
+ const target = r.target();
1475
+ if (!target) return void 0;
1476
+ const relationKey = r.relationName || target.slug;
1477
+ let customName;
1478
+ if (collection.properties) {
1479
+ const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
1480
+ if (prop && prop[1].name) {
1481
+ customName = prop[1].name;
1482
+ }
1483
+ }
1484
+ const baseOverrides = {
1485
+ slug: relationKey
1486
+ };
1487
+ if (customName) {
1488
+ baseOverrides.name = customName;
1489
+ baseOverrides.singularName = customName;
1490
+ }
1491
+ const targetWithOverrides = {
1492
+ ...target,
1493
+ ...baseOverrides
1494
+ };
1495
+ return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
1496
+ }).filter((c) => Boolean(c));
1497
+ }
1498
+ return [];
1499
+ }
1500
+ function hasPropertyCallbacks(properties, callbackName) {
1501
+ if (!properties) return false;
1502
+ for (const property of Object.values(properties)) {
1503
+ if (property.callbacks?.[callbackName]) return true;
1504
+ if (property.type === "map" && property.properties) {
1505
+ if (hasPropertyCallbacks(property.properties, callbackName)) return true;
1506
+ } else if (property.type === "array" && property.of) {
1507
+ const ofs = Array.isArray(property.of) ? property.of : [property.of];
1508
+ for (const of of ofs) {
1509
+ if (of.callbacks?.[callbackName]) return true;
1510
+ if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
1511
+ }
1512
+ }
1513
+ }
1514
+ return false;
1515
+ }
1516
+ async function processProperties(properties, values, previousValues, propsContext, callbackName) {
1517
+ if (!values || typeof values !== "object") return values;
1518
+ const result = {
1519
+ ...values
1520
+ };
1521
+ for (const [key, property] of Object.entries(properties)) {
1522
+ if (result[key] === void 0) continue;
1523
+ let currentValue = result[key];
1524
+ const previousValue = previousValues?.[key];
1525
+ if (property.type === "array" && Array.isArray(currentValue)) {
1526
+ if (property.of && !Array.isArray(property.of)) {
1527
+ currentValue = await Promise.all(currentValue.map(async (item, index) => {
1528
+ const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
1529
+ const singlePropData = {
1530
+ "_tmp": property.of
1531
+ };
1532
+ const res = await processProperties(singlePropData, {
1533
+ "_tmp": item
1534
+ }, {
1535
+ "_tmp": prevItem
1536
+ }, propsContext, callbackName);
1537
+ return res["_tmp"];
1538
+ }));
1539
+ }
1540
+ } else if (property.type === "map" && property.properties && typeof currentValue === "object") {
1541
+ currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
1542
+ }
1543
+ if (property.callbacks?.[callbackName]) {
1544
+ const cbRes = await Promise.resolve(property.callbacks[callbackName]({
1545
+ ...propsContext,
1546
+ value: currentValue,
1547
+ previousValue
1548
+ }));
1549
+ if (cbRes !== void 0) {
1550
+ currentValue = cbRes;
1551
+ }
1552
+ }
1553
+ result[key] = currentValue;
1554
+ }
1555
+ return result;
1556
+ }
1557
+ const buildPropertyCallbacks = (properties) => {
1558
+ if (!properties) return void 0;
1559
+ const propertyCallbacks = {};
1560
+ if (hasPropertyCallbacks(properties, "afterRead")) {
1561
+ propertyCallbacks.afterRead = async (props) => {
1562
+ const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1563
+ return {
1564
+ ...props.entity,
1565
+ values: processedValues
1566
+ };
1567
+ };
1568
+ }
1569
+ if (hasPropertyCallbacks(properties, "beforeSave")) {
1570
+ propertyCallbacks.beforeSave = async (props) => {
1571
+ return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
1572
+ };
1573
+ }
1574
+ return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
1575
+ };
1507
1576
  var logic = { exports: {} };
1508
1577
  (function(module, exports) {
1509
1578
  (function(root, factory) {
@@ -2411,8 +2480,18 @@ class CollectionRegistry {
2411
2480
  const mergedRelationsRaw = [...extractedRelations];
2412
2481
  for (const manual of manualRelations) {
2413
2482
  const name = manual.relationName;
2414
- if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
2483
+ if (!name) {
2415
2484
  mergedRelationsRaw.push(manual);
2485
+ } else {
2486
+ const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
2487
+ if (existingIndex === -1) {
2488
+ mergedRelationsRaw.push(manual);
2489
+ } else {
2490
+ mergedRelationsRaw[existingIndex] = {
2491
+ ...manual,
2492
+ ...mergedRelationsRaw[existingIndex]
2493
+ };
2494
+ }
2416
2495
  }
2417
2496
  }
2418
2497
  let mergedRelations = mergedRelationsRaw;
@@ -2625,11 +2704,207 @@ class CollectionRegistry {
2625
2704
  collections.push(currentCollection);
2626
2705
  }
2627
2706
  }
2628
- return {
2629
- collections,
2630
- entityIds,
2631
- finalCollection: currentCollection
2632
- };
2707
+ return {
2708
+ collections,
2709
+ entityIds,
2710
+ finalCollection: currentCollection
2711
+ };
2712
+ }
2713
+ }
2714
+ const defaultUsersCollection = {
2715
+ name: "Users",
2716
+ singularName: "User",
2717
+ slug: "users",
2718
+ table: "users",
2719
+ schema: "rebase",
2720
+ icon: "Users",
2721
+ group: "Settings",
2722
+ properties: {
2723
+ id: {
2724
+ name: "ID",
2725
+ type: "string",
2726
+ isId: "uuid"
2727
+ },
2728
+ email: {
2729
+ name: "Email",
2730
+ type: "string",
2731
+ validation: {
2732
+ required: true,
2733
+ unique: true
2734
+ }
2735
+ },
2736
+ password_hash: {
2737
+ name: "Password Hash",
2738
+ type: "string",
2739
+ ui: {
2740
+ hideFromCollection: true
2741
+ }
2742
+ },
2743
+ display_name: {
2744
+ name: "Display Name",
2745
+ type: "string"
2746
+ },
2747
+ photo_url: {
2748
+ name: "Photo URL",
2749
+ type: "string"
2750
+ },
2751
+ email_verified: {
2752
+ name: "Email Verified",
2753
+ type: "boolean",
2754
+ defaultValue: false
2755
+ },
2756
+ email_verification_token: {
2757
+ name: "Email Verification Token",
2758
+ type: "string",
2759
+ ui: {
2760
+ hideFromCollection: true
2761
+ }
2762
+ },
2763
+ email_verification_sent_at: {
2764
+ name: "Email Verification Sent At",
2765
+ type: "date",
2766
+ ui: {
2767
+ hideFromCollection: true
2768
+ }
2769
+ },
2770
+ metadata: {
2771
+ name: "Metadata",
2772
+ type: "map",
2773
+ defaultValue: {},
2774
+ ui: {
2775
+ hideFromCollection: true
2776
+ }
2777
+ },
2778
+ created_at: {
2779
+ name: "Created At",
2780
+ type: "date",
2781
+ autoValue: "on_create",
2782
+ ui: {
2783
+ readOnly: true,
2784
+ hideFromCollection: true
2785
+ }
2786
+ },
2787
+ updated_at: {
2788
+ name: "Updated At",
2789
+ type: "date",
2790
+ autoValue: "on_update",
2791
+ ui: {
2792
+ readOnly: true,
2793
+ hideFromCollection: true
2794
+ }
2795
+ }
2796
+ }
2797
+ };
2798
+ function mapOperator(op) {
2799
+ switch (op) {
2800
+ case "==":
2801
+ return "eq";
2802
+ case "!=":
2803
+ return "neq";
2804
+ case ">":
2805
+ return "gt";
2806
+ case ">=":
2807
+ return "gte";
2808
+ case "<":
2809
+ return "lt";
2810
+ case "<=":
2811
+ return "lte";
2812
+ case "array-contains":
2813
+ return "cs";
2814
+ case "array-contains-any":
2815
+ return "csa";
2816
+ case "not-in":
2817
+ return "nin";
2818
+ default:
2819
+ return op;
2820
+ }
2821
+ }
2822
+ class QueryBuilder {
2823
+ constructor(collection) {
2824
+ this.collection = collection;
2825
+ }
2826
+ params = {
2827
+ where: {}
2828
+ };
2829
+ /**
2830
+ * Add a filter condition to your query.
2831
+ * @example
2832
+ * client.collection('users').where('age', '>=', 18).find()
2833
+ */
2834
+ where(column, operator, value) {
2835
+ if (!this.params.where) {
2836
+ this.params.where = {};
2837
+ }
2838
+ const mappedOp = mapOperator(operator);
2839
+ let formattedValue = value;
2840
+ if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
2841
+ formattedValue = `(${value.join(",")})`;
2842
+ } else if (value === null) {
2843
+ formattedValue = "null";
2844
+ }
2845
+ this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
2846
+ return this;
2847
+ }
2848
+ /**
2849
+ * Order the results by a specific column.
2850
+ * @example
2851
+ * client.collection('users').orderBy('createdAt', 'desc').find()
2852
+ */
2853
+ orderBy(column, ascending = "asc") {
2854
+ this.params.orderBy = `${column}:${ascending}`;
2855
+ return this;
2856
+ }
2857
+ /**
2858
+ * Limit the number of results returned.
2859
+ */
2860
+ limit(count2) {
2861
+ this.params.limit = count2;
2862
+ return this;
2863
+ }
2864
+ /**
2865
+ * Skip the first N results.
2866
+ */
2867
+ offset(count2) {
2868
+ this.params.offset = count2;
2869
+ return this;
2870
+ }
2871
+ /**
2872
+ * Set a free-text search string if supported by the backend.
2873
+ */
2874
+ search(searchString) {
2875
+ this.params.searchString = searchString;
2876
+ return this;
2877
+ }
2878
+ /**
2879
+ * Include related entities in the response.
2880
+ * Relations will be populated with full entity data instead of just IDs.
2881
+ *
2882
+ * @param relations - Relation names to include, or "*" for all.
2883
+ * @example
2884
+ * // Include specific relations
2885
+ * client.data.posts.include("tags", "author").find()
2886
+ *
2887
+ * // Include all relations
2888
+ * client.data.posts.include("*").find()
2889
+ */
2890
+ include(...relations2) {
2891
+ this.params.include = relations2;
2892
+ return this;
2893
+ }
2894
+ /**
2895
+ * Execute the find query and return the results.
2896
+ */
2897
+ async find() {
2898
+ return this.collection.find(this.params);
2899
+ }
2900
+ /**
2901
+ * Listen to realtime updates matching this query.
2902
+ */
2903
+ listen(onUpdate, onError) {
2904
+ if (!this.collection.listen) {
2905
+ throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
2906
+ }
2907
+ return this.collection.listen(this.params, onUpdate, onError);
2633
2908
  }
2634
2909
  }
2635
2910
  function convertWhereToFilter(where) {
@@ -2711,7 +2986,7 @@ function parseOrderBy(orderBy) {
2711
2986
  return [field, direction];
2712
2987
  }
2713
2988
  function createDriverAccessor(driver, slug) {
2714
- return {
2989
+ const accessor = {
2715
2990
  async find(params) {
2716
2991
  const orderParsed = parseOrderBy(params?.orderBy);
2717
2992
  const entities = await driver.fetchCollection({
@@ -2805,8 +3080,28 @@ function createDriverAccessor(driver, slug) {
2805
3080
  onUpdate: (entity) => onUpdate(entity ?? void 0),
2806
3081
  onError
2807
3082
  });
2808
- } : void 0
3083
+ } : void 0,
3084
+ // Fluent Query Builder
3085
+ where(column, operator, value) {
3086
+ return new QueryBuilder(accessor).where(column, operator, value);
3087
+ },
3088
+ orderBy(column, ascending) {
3089
+ return new QueryBuilder(accessor).orderBy(column, ascending);
3090
+ },
3091
+ limit(count2) {
3092
+ return new QueryBuilder(accessor).limit(count2);
3093
+ },
3094
+ offset(count2) {
3095
+ return new QueryBuilder(accessor).offset(count2);
3096
+ },
3097
+ search(searchString) {
3098
+ return new QueryBuilder(accessor).search(searchString);
3099
+ },
3100
+ include(...relations2) {
3101
+ return new QueryBuilder(accessor).include(...relations2);
3102
+ }
2809
3103
  };
3104
+ return accessor;
2810
3105
  }
2811
3106
  function buildRebaseData(driver) {
2812
3107
  const cache = /* @__PURE__ */ new Map();
@@ -2840,7 +3135,13 @@ class DrizzleConditionBuilder {
2840
3135
  for (const [field, filterParam] of Object.entries(filter)) {
2841
3136
  if (!filterParam) continue;
2842
3137
  const [op, value] = filterParam;
2843
- const fieldColumn = table[field];
3138
+ let fieldColumn = table[field];
3139
+ if (!fieldColumn) {
3140
+ const relationKey = `${field}_id`;
3141
+ if (relationKey in table) {
3142
+ fieldColumn = table[relationKey];
3143
+ }
3144
+ }
2844
3145
  if (!fieldColumn) {
2845
3146
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2846
3147
  continue;
@@ -2882,6 +3183,17 @@ class DrizzleConditionBuilder {
2882
3183
  return null;
2883
3184
  case "array-contains":
2884
3185
  return sql`${column} @> ${JSON.stringify([value])}`;
3186
+ case "array-contains-any":
3187
+ if (Array.isArray(value) && value.length > 0) {
3188
+ const textValues = value.map((v) => String(v));
3189
+ return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
3190
+ }
3191
+ return sql`${column} @> ${JSON.stringify([value])}`;
3192
+ case "not-in":
3193
+ if (Array.isArray(value) && value.length > 0) {
3194
+ return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
3195
+ }
3196
+ return null;
2885
3197
  default:
2886
3198
  console.warn(`Unsupported filter operation: ${op}`);
2887
3199
  return null;
@@ -3397,6 +3709,40 @@ class DrizzleConditionBuilder {
3397
3709
  return null;
3398
3710
  }
3399
3711
  }
3712
+ /**
3713
+ * Build vector similarity search expressions for pgvector.
3714
+ *
3715
+ * Returns:
3716
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3717
+ * - `filter`: optional WHERE clause for distance threshold
3718
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3719
+ */
3720
+ static buildVectorSearchConditions(table, vectorSearch) {
3721
+ const column = table[vectorSearch.property];
3722
+ if (!column) {
3723
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3724
+ }
3725
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3726
+ const distanceFn = vectorSearch.distance || "cosine";
3727
+ let operator;
3728
+ switch (distanceFn) {
3729
+ case "cosine":
3730
+ operator = "<=>";
3731
+ break;
3732
+ case "l2":
3733
+ operator = "<->";
3734
+ break;
3735
+ case "inner_product":
3736
+ operator = "<#>";
3737
+ break;
3738
+ }
3739
+ const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
3740
+ return {
3741
+ orderBy: distanceExpr,
3742
+ filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3743
+ distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
3744
+ };
3745
+ }
3400
3746
  }
3401
3747
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3402
3748
  function getColumnMeta(col) {
@@ -5342,7 +5688,7 @@ class EntityFetchService {
5342
5688
  const qb = this.getQueryBuilder(tableName);
5343
5689
  const withConfig = this.buildWithConfig(collection);
5344
5690
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5345
- if (qb && !options.searchString && !hasRelations) {
5691
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5346
5692
  try {
5347
5693
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5348
5694
  const results2 = await qb.findMany(queryOpts);
@@ -5356,7 +5702,14 @@ class EntityFetchService {
5356
5702
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5357
5703
  }
5358
5704
  }
5359
- let query = this.db.select().from(table).$dynamic();
5705
+ let vectorMeta;
5706
+ if (options.vectorSearch) {
5707
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5708
+ }
5709
+ let query = vectorMeta ? this.db.select({
5710
+ table_row: table,
5711
+ _distance: vectorMeta.distanceSelect
5712
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5360
5713
  const allConditions = [];
5361
5714
  if (options.searchString) {
5362
5715
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5367,12 +5720,17 @@ class EntityFetchService {
5367
5720
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5368
5721
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5369
5722
  }
5723
+ if (vectorMeta?.filter) {
5724
+ allConditions.push(vectorMeta.filter);
5725
+ }
5370
5726
  if (allConditions.length > 0) {
5371
5727
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5372
5728
  if (finalCondition) query = query.where(finalCondition);
5373
5729
  }
5374
5730
  const orderExpressions = [];
5375
- if (options.orderBy) {
5731
+ if (vectorMeta) {
5732
+ orderExpressions.push(asc(vectorMeta.orderBy));
5733
+ } else if (options.orderBy) {
5376
5734
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5377
5735
  if (orderByField) {
5378
5736
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5388,10 +5746,14 @@ class EntityFetchService {
5388
5746
  if (finalCondition) query = query.where(finalCondition);
5389
5747
  }
5390
5748
  }
5391
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5749
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5392
5750
  if (limitValue) query = query.limit(limitValue);
5393
5751
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5394
- const results = await query;
5752
+ const rawResults = await query;
5753
+ const results = vectorMeta ? rawResults.map((r) => ({
5754
+ ...r.table_row,
5755
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5756
+ })) : rawResults;
5395
5757
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5396
5758
  }
5397
5759
  /**
@@ -5613,7 +5975,7 @@ class EntityFetchService {
5613
5975
  const idField = table[idInfo.fieldName];
5614
5976
  const tableName = getTableName$1(table);
5615
5977
  const qb = this.getQueryBuilder(tableName);
5616
- if (qb && !options.searchString) {
5978
+ if (qb && !options.searchString && !options.vectorSearch) {
5617
5979
  try {
5618
5980
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5619
5981
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5759,7 +6121,14 @@ class EntityFetchService {
5759
6121
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5760
6122
  const idInfo = idInfoArray[0];
5761
6123
  const idField = table[idInfo.fieldName];
5762
- let query = this.db.select().from(table).$dynamic();
6124
+ let vectorMeta;
6125
+ if (options.vectorSearch) {
6126
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6127
+ }
6128
+ let query = vectorMeta ? this.db.select({
6129
+ table_row: table,
6130
+ _distance: vectorMeta.distanceSelect
6131
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5763
6132
  const allConditions = [];
5764
6133
  if (options.searchString) {
5765
6134
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5770,12 +6139,17 @@ class EntityFetchService {
5770
6139
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5771
6140
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5772
6141
  }
6142
+ if (vectorMeta?.filter) {
6143
+ allConditions.push(vectorMeta.filter);
6144
+ }
5773
6145
  if (allConditions.length > 0) {
5774
6146
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5775
6147
  if (finalCondition) query = query.where(finalCondition);
5776
6148
  }
5777
6149
  const orderExpressions = [];
5778
- if (options.orderBy) {
6150
+ if (vectorMeta) {
6151
+ orderExpressions.push(asc(vectorMeta.orderBy));
6152
+ } else if (options.orderBy) {
5779
6153
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5780
6154
  if (orderByField) {
5781
6155
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5783,10 +6157,17 @@ class EntityFetchService {
5783
6157
  }
5784
6158
  orderExpressions.push(desc(idField));
5785
6159
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5786
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6160
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5787
6161
  if (limitValue) query = query.limit(limitValue);
5788
6162
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5789
- return await query;
6163
+ const rawResults = await query;
6164
+ if (vectorMeta) {
6165
+ return rawResults.map((r) => ({
6166
+ ...r.table_row,
6167
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6168
+ }));
6169
+ }
6170
+ return rawResults;
5790
6171
  }
5791
6172
  /**
5792
6173
  * Check if the Drizzle instance has the relational query API available
@@ -6514,7 +6895,7 @@ class PostgresBackendDriver {
6514
6895
  callbacks: void 0,
6515
6896
  propertyCallbacks: void 0
6516
6897
  };
6517
- const registryCollection = this.registry.getCollectionByPath(path2);
6898
+ const registryCollection = this.registry?.getCollectionByPath(path2);
6518
6899
  const resolvedCollection = registryCollection ? {
6519
6900
  ...collection,
6520
6901
  ...registryCollection
@@ -6540,7 +6921,8 @@ class PostgresBackendDriver {
6540
6921
  startAfter,
6541
6922
  orderBy,
6542
6923
  searchString,
6543
- order
6924
+ order,
6925
+ vectorSearch
6544
6926
  }) {
6545
6927
  const entities = await this.entityService.fetchCollection(path2, {
6546
6928
  filter,
@@ -6550,7 +6932,8 @@ class PostgresBackendDriver {
6550
6932
  offset,
6551
6933
  startAfter,
6552
6934
  databaseId: collection?.databaseId,
6553
- searchString
6935
+ searchString,
6936
+ vectorSearch
6554
6937
  });
6555
6938
  const {
6556
6939
  collection: resolvedCollection,
@@ -6562,7 +6945,8 @@ class PostgresBackendDriver {
6562
6945
  user: this.user,
6563
6946
  driver: this,
6564
6947
  data: this.data,
6565
- client: this.client
6948
+ client: this.client,
6949
+ storageSource: this.client?.storage
6566
6950
  };
6567
6951
  return Promise.all(entities.map(async (entity) => {
6568
6952
  let fetched = entity;
@@ -6657,7 +7041,8 @@ class PostgresBackendDriver {
6657
7041
  user: this.user,
6658
7042
  driver: this,
6659
7043
  data: this.data,
6660
- client: this.client
7044
+ client: this.client,
7045
+ storageSource: this.client?.storage
6661
7046
  };
6662
7047
  if (callbacks?.afterRead) {
6663
7048
  entity = await callbacks.afterRead({
@@ -6727,7 +7112,8 @@ class PostgresBackendDriver {
6727
7112
  user: this.user,
6728
7113
  driver: this,
6729
7114
  data: this.data,
6730
- client: this.client
7115
+ client: this.client,
7116
+ storageSource: this.client?.storage
6731
7117
  };
6732
7118
  let previousValuesForHistory;
6733
7119
  if (status === "existing" && entityId) {
@@ -6876,7 +7262,8 @@ class PostgresBackendDriver {
6876
7262
  user: this.user,
6877
7263
  driver: this,
6878
7264
  data: this.data,
6879
- client: this.client
7265
+ client: this.client,
7266
+ storageSource: this.client?.storage
6880
7267
  };
6881
7268
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
6882
7269
  let preventDefault = false;
@@ -7389,6 +7776,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7389
7776
  length: 255
7390
7777
  }),
7391
7778
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
7779
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
7392
7780
  metadata: jsonb("metadata").$type().default({}).notNull(),
7393
7781
  createdAt: timestamp("created_at").defaultNow().notNull(),
7394
7782
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -7403,8 +7791,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7403
7791
  }).notNull(),
7404
7792
  isAdmin: boolean("is_admin").default(false).notNull(),
7405
7793
  defaultPermissions: jsonb("default_permissions").$type(),
7406
- collectionPermissions: jsonb("collection_permissions").$type(),
7407
- config: jsonb("config").$type()
7794
+ collectionPermissions: jsonb("collection_permissions").$type()
7408
7795
  });
7409
7796
  const userRoles2 = rolesTableCreator("user_roles", {
7410
7797
  userId: uuid("user_id").notNull().references(() => users2.id, {
@@ -7476,6 +7863,48 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7476
7863
  }, (table) => ({
7477
7864
  uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
7478
7865
  }));
7866
+ const mfaFactors2 = rolesTableCreator("mfa_factors", {
7867
+ id: uuid("id").defaultRandom().primaryKey(),
7868
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7869
+ onDelete: "cascade"
7870
+ }),
7871
+ factorType: varchar("factor_type", {
7872
+ length: 20
7873
+ }).notNull(),
7874
+ // 'totp'
7875
+ secretEncrypted: varchar("secret_encrypted", {
7876
+ length: 500
7877
+ }).notNull(),
7878
+ friendlyName: varchar("friendly_name", {
7879
+ length: 255
7880
+ }),
7881
+ verified: boolean("verified").default(false).notNull(),
7882
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7883
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
7884
+ });
7885
+ const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
7886
+ id: uuid("id").defaultRandom().primaryKey(),
7887
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7888
+ onDelete: "cascade"
7889
+ }),
7890
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7891
+ verifiedAt: timestamp("verified_at"),
7892
+ ipAddress: varchar("ip_address", {
7893
+ length: 45
7894
+ }),
7895
+ expiresAt: timestamp("expires_at").notNull()
7896
+ });
7897
+ const recoveryCodes2 = rolesTableCreator("recovery_codes", {
7898
+ id: uuid("id").defaultRandom().primaryKey(),
7899
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7900
+ onDelete: "cascade"
7901
+ }),
7902
+ codeHash: varchar("code_hash", {
7903
+ length: 255
7904
+ }).notNull(),
7905
+ usedAt: timestamp("used_at"),
7906
+ createdAt: timestamp("created_at").defaultNow().notNull()
7907
+ });
7479
7908
  return {
7480
7909
  rolesSchema,
7481
7910
  usersSchema: usersSchema2,
@@ -7485,7 +7914,10 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7485
7914
  refreshTokens: refreshTokens2,
7486
7915
  passwordResetTokens: passwordResetTokens2,
7487
7916
  appConfig: appConfig2,
7488
- userIdentities: userIdentities2
7917
+ userIdentities: userIdentities2,
7918
+ mfaFactors: mfaFactors2,
7919
+ mfaChallenges: mfaChallenges2,
7920
+ recoveryCodes: recoveryCodes2
7489
7921
  };
7490
7922
  }
7491
7923
  const defaultAuthSchema = createAuthSchema("rebase", "rebase");
@@ -7498,13 +7930,18 @@ const refreshTokens = defaultAuthSchema.refreshTokens;
7498
7930
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7499
7931
  const appConfig = defaultAuthSchema.appConfig;
7500
7932
  const userIdentities = defaultAuthSchema.userIdentities;
7933
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7934
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7935
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7501
7936
  const usersRelations = relations(users, ({
7502
7937
  many
7503
7938
  }) => ({
7504
7939
  userRoles: many(userRoles),
7505
7940
  refreshTokens: many(refreshTokens),
7506
7941
  passwordResetTokens: many(passwordResetTokens),
7507
- userIdentities: many(userIdentities)
7942
+ userIdentities: many(userIdentities),
7943
+ mfaFactors: many(mfaFactors),
7944
+ recoveryCodes: many(recoveryCodes)
7508
7945
  }));
7509
7946
  const rolesRelations = relations(roles, ({
7510
7947
  many
@@ -7547,6 +7984,32 @@ const userIdentitiesRelations = relations(userIdentities, ({
7547
7984
  references: [users.id]
7548
7985
  })
7549
7986
  }));
7987
+ const mfaFactorsRelations = relations(mfaFactors, ({
7988
+ one,
7989
+ many
7990
+ }) => ({
7991
+ user: one(users, {
7992
+ fields: [mfaFactors.userId],
7993
+ references: [users.id]
7994
+ }),
7995
+ challenges: many(mfaChallenges)
7996
+ }));
7997
+ const mfaChallengesRelations = relations(mfaChallenges, ({
7998
+ one
7999
+ }) => ({
8000
+ factor: one(mfaFactors, {
8001
+ fields: [mfaChallenges.factorId],
8002
+ references: [mfaFactors.id]
8003
+ })
8004
+ }));
8005
+ const recoveryCodesRelations = relations(recoveryCodes, ({
8006
+ one
8007
+ }) => ({
8008
+ user: one(users, {
8009
+ fields: [recoveryCodes.userId],
8010
+ references: [users.id]
8011
+ })
8012
+ }));
7550
8013
  const resolveColumnName = (propName, prop) => {
7551
8014
  if (prop && "columnName" in prop && typeof prop.columnName === "string") {
7552
8015
  return prop.columnName;
@@ -8071,167 +8534,84 @@ const generateSchema = async (collections, stripPolicies = false) => {
8071
8534
  fields: [${tableVarName}.${rel.localKey}],
8072
8535
  references: [${targetTableVar}.${getPrimaryKeyName(target)}],
8073
8536
  relationName: "${drizzleRelationName}"
8074
- })`);
8075
- } else if (rel.direction === "inverse") {
8076
- tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8077
- relationName: "${drizzleRelationName}"
8078
- })`);
8079
- }
8080
- } else if (rel.cardinality === "many") {
8081
- if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8082
- tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8083
- } else if (rel.through) {
8084
- const junctionTableVar = getTableVarName(rel.through.table);
8085
- tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8086
- } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8087
- try {
8088
- const targetCollection = rel.target();
8089
- const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8090
- const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8091
- if (correspondingRelation && correspondingRelation.through) {
8092
- const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8093
- tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8094
- } else {
8095
- console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8096
- }
8097
- } catch (e) {
8098
- console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
8099
- }
8100
- }
8101
- }
8102
- } catch (e) {
8103
- console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
8104
- }
8105
- }
8106
- for (const otherCollection of collections) {
8107
- if (otherCollection.slug === collection.slug) continue;
8108
- const otherRelations = resolveCollectionRelations(otherCollection);
8109
- for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
8110
- if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
8111
- try {
8112
- const otherTarget = otherRel.target();
8113
- if (otherTarget.slug === collection.slug) {
8114
- const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
8115
- const deduplicationKey = `${drizzleRelationName}::owning`;
8116
- if (!emittedRelationNames.has(deduplicationKey)) {
8117
- const otherTableVar = getTableVarName(getTableName(otherCollection));
8118
- const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
8119
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
8120
- fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
8121
- references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
8122
- relationName: "${drizzleRelationName}"
8123
- })`);
8124
- emittedRelationNames.add(deduplicationKey);
8125
- }
8126
- }
8127
- } catch (e) {
8128
- }
8129
- }
8130
- }
8131
- }
8132
- }
8133
- if (tableRelations.length > 0) {
8134
- const relVarName = `${tableVarName}Relations`;
8135
- schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
8136
- ${tableRelations.join(",\n")}
8137
- }));
8138
-
8139
- `;
8140
- if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
8141
- }
8142
- }
8143
- const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
8144
- `;
8145
- const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
8146
- `;
8147
- const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
8148
-
8149
- `;
8150
- schemaContent += tablesExport + enumsExport + relationsExport;
8151
- return schemaContent;
8152
- };
8153
- const defaultUsersCollection = {
8154
- name: "Users",
8155
- singularName: "User",
8156
- slug: "users",
8157
- table: "users",
8158
- icon: "Users",
8159
- group: "Settings",
8160
- properties: {
8161
- id: {
8162
- name: "ID",
8163
- type: "string",
8164
- isId: "uuid"
8165
- },
8166
- email: {
8167
- name: "Email",
8168
- type: "string",
8169
- validation: {
8170
- required: true,
8171
- unique: true
8172
- }
8173
- },
8174
- password_hash: {
8175
- name: "Password Hash",
8176
- type: "string",
8177
- ui: {
8178
- hideFromCollection: true
8179
- }
8180
- },
8181
- display_name: {
8182
- name: "Display Name",
8183
- type: "string"
8184
- },
8185
- photo_url: {
8186
- name: "Photo URL",
8187
- type: "string"
8188
- },
8189
- email_verified: {
8190
- name: "Email Verified",
8191
- type: "boolean",
8192
- defaultValue: false
8193
- },
8194
- email_verification_token: {
8195
- name: "Email Verification Token",
8196
- type: "string",
8197
- ui: {
8198
- hideFromCollection: true
8199
- }
8200
- },
8201
- email_verification_sent_at: {
8202
- name: "Email Verification Sent At",
8203
- type: "date",
8204
- ui: {
8205
- hideFromCollection: true
8206
- }
8207
- },
8208
- metadata: {
8209
- name: "Metadata",
8210
- type: "map",
8211
- defaultValue: {},
8212
- ui: {
8213
- hideFromCollection: true
8214
- }
8215
- },
8216
- created_at: {
8217
- name: "Created At",
8218
- type: "date",
8219
- autoValue: "on_create",
8220
- ui: {
8221
- readOnly: true,
8222
- hideFromCollection: true
8537
+ })`);
8538
+ } else if (rel.direction === "inverse") {
8539
+ tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
8540
+ relationName: "${drizzleRelationName}"
8541
+ })`);
8542
+ }
8543
+ } else if (rel.cardinality === "many") {
8544
+ if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
8545
+ tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
8546
+ } else if (rel.through) {
8547
+ const junctionTableVar = getTableVarName(rel.through.table);
8548
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8549
+ } else if (rel.direction === "inverse" && rel.inverseRelationName) {
8550
+ try {
8551
+ const targetCollection = rel.target();
8552
+ const targetResolvedRelations = resolveCollectionRelations(targetCollection);
8553
+ const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
8554
+ if (correspondingRelation && correspondingRelation.through) {
8555
+ const junctionTableVar = getTableVarName(correspondingRelation.through.table);
8556
+ tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
8557
+ } else {
8558
+ console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
8559
+ }
8560
+ } catch (e) {
8561
+ console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
8562
+ }
8563
+ }
8564
+ }
8565
+ } catch (e) {
8566
+ console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
8567
+ }
8223
8568
  }
8224
- },
8225
- updated_at: {
8226
- name: "Updated At",
8227
- type: "date",
8228
- autoValue: "on_update",
8229
- ui: {
8230
- readOnly: true,
8231
- hideFromCollection: true
8569
+ for (const otherCollection of collections) {
8570
+ if (otherCollection.slug === collection.slug) continue;
8571
+ const otherRelations = resolveCollectionRelations(otherCollection);
8572
+ for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
8573
+ if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
8574
+ try {
8575
+ const otherTarget = otherRel.target();
8576
+ if (otherTarget.slug === collection.slug) {
8577
+ const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
8578
+ const deduplicationKey = `${drizzleRelationName}::owning`;
8579
+ if (!emittedRelationNames.has(deduplicationKey)) {
8580
+ const otherTableVar = getTableVarName(getTableName(otherCollection));
8581
+ const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
8582
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
8583
+ fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
8584
+ references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
8585
+ relationName: "${drizzleRelationName}"
8586
+ })`);
8587
+ emittedRelationNames.add(deduplicationKey);
8588
+ }
8589
+ }
8590
+ } catch (e) {
8591
+ }
8592
+ }
8593
+ }
8232
8594
  }
8233
8595
  }
8596
+ if (tableRelations.length > 0) {
8597
+ const relVarName = `${tableVarName}Relations`;
8598
+ schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
8599
+ ${tableRelations.join(",\n")}
8600
+ }));
8601
+
8602
+ `;
8603
+ if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
8604
+ }
8234
8605
  }
8606
+ const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
8607
+ `;
8608
+ const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
8609
+ `;
8610
+ const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
8611
+
8612
+ `;
8613
+ schemaContent += tablesExport + enumsExport + relationsExport;
8614
+ return schemaContent;
8235
8615
  };
8236
8616
  const formatTerminalText = (text, options = {}) => {
8237
8617
  let codes = "";
@@ -8298,10 +8678,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
8298
8678
  if (!collections || !Array.isArray(collections)) {
8299
8679
  collections = [];
8300
8680
  }
8301
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8302
- if (!hasUsersCollection) {
8303
- collections.push(defaultUsersCollection);
8304
- }
8681
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8305
8682
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8306
8683
  const schemaContent = await generateSchema(collections);
8307
8684
  if (outputPath) {
@@ -8362,6 +8739,13 @@ class RealtimeService extends EventEmitter {
8362
8739
  this.entityService = new EntityService(db, registry);
8363
8740
  }
8364
8741
  clients = /* @__PURE__ */ new Map();
8742
+ // Broadcast channels: channel name → set of client IDs
8743
+ channels = /* @__PURE__ */ new Map();
8744
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8745
+ presence = /* @__PURE__ */ new Map();
8746
+ presenceInterval;
8747
+ static PRESENCE_TIMEOUT_MS = 3e4;
8748
+ // 30s
8365
8749
  entityService;
8366
8750
  // Enhanced subscriptions storage with full request parameters
8367
8751
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8488,8 +8872,19 @@ class RealtimeService extends EventEmitter {
8488
8872
  }
8489
8873
  }
8490
8874
  }
8875
+ for (const [channel, members] of this.channels.entries()) {
8876
+ if (members.has(clientId)) {
8877
+ members.delete(clientId);
8878
+ this.removePresence(clientId, channel);
8879
+ if (members.size === 0) this.channels.delete(channel);
8880
+ }
8881
+ }
8882
+ for (const [channel] of this.presence) {
8883
+ this.removePresence(clientId, channel);
8884
+ }
8491
8885
  }
8492
8886
  async handleMessage(clientId, message, authContext) {
8887
+ const payload = message.payload;
8493
8888
  switch (message.type) {
8494
8889
  case "subscribe_collection":
8495
8890
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8500,6 +8895,25 @@ class RealtimeService extends EventEmitter {
8500
8895
  case "unsubscribe":
8501
8896
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8502
8897
  break;
8898
+ case "join_channel":
8899
+ this.joinChannel(clientId, payload?.channel);
8900
+ break;
8901
+ case "leave_channel":
8902
+ this.leaveChannel(clientId, payload?.channel);
8903
+ break;
8904
+ case "broadcast":
8905
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8906
+ break;
8907
+ case "presence_track":
8908
+ this.joinChannel(clientId, payload?.channel);
8909
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8910
+ break;
8911
+ case "presence_untrack":
8912
+ this.removePresence(clientId, payload?.channel);
8913
+ break;
8914
+ case "presence_state":
8915
+ this.sendPresenceState(clientId, payload?.channel);
8916
+ break;
8503
8917
  default:
8504
8918
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8505
8919
  }
@@ -8957,6 +9371,132 @@ class RealtimeService extends EventEmitter {
8957
9371
  return parentPaths;
8958
9372
  }
8959
9373
  // =============================================================================
9374
+ // Broadcast Channels
9375
+ // =============================================================================
9376
+ /** Join a broadcast channel */
9377
+ joinChannel(clientId, channel) {
9378
+ if (!this.channels.has(channel)) {
9379
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9380
+ }
9381
+ this.channels.get(channel).add(clientId);
9382
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9383
+ }
9384
+ /** Leave a broadcast channel */
9385
+ leaveChannel(clientId, channel) {
9386
+ const members = this.channels.get(channel);
9387
+ if (members) {
9388
+ members.delete(clientId);
9389
+ if (members.size === 0) this.channels.delete(channel);
9390
+ }
9391
+ this.removePresence(clientId, channel);
9392
+ }
9393
+ /** Broadcast a message to all clients in a channel except sender */
9394
+ broadcastToChannel(clientId, channel, event, payload) {
9395
+ const members = this.channels.get(channel);
9396
+ if (!members) return;
9397
+ const message = JSON.stringify({
9398
+ type: "broadcast",
9399
+ channel,
9400
+ event,
9401
+ payload
9402
+ });
9403
+ for (const memberId of members) {
9404
+ if (memberId === clientId) continue;
9405
+ const ws = this.clients.get(memberId);
9406
+ if (ws && ws.readyState === WebSocket.OPEN) {
9407
+ ws.send(message);
9408
+ }
9409
+ }
9410
+ }
9411
+ // =============================================================================
9412
+ // Presence
9413
+ // =============================================================================
9414
+ /** Track presence in a channel */
9415
+ trackPresence(clientId, channel, state) {
9416
+ if (!this.presence.has(channel)) {
9417
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9418
+ }
9419
+ const channelPresence = this.presence.get(channel);
9420
+ channelPresence.set(clientId, {
9421
+ state,
9422
+ lastSeen: Date.now()
9423
+ });
9424
+ this.broadcastPresenceDiff(channel, {
9425
+ [clientId]: state
9426
+ }, {});
9427
+ this.ensurePresenceCleanup();
9428
+ }
9429
+ /** Remove presence from a channel */
9430
+ removePresence(clientId, channel) {
9431
+ const channelPresence = this.presence.get(channel);
9432
+ if (!channelPresence) return;
9433
+ const entry = channelPresence.get(clientId);
9434
+ if (entry) {
9435
+ channelPresence.delete(clientId);
9436
+ this.broadcastPresenceDiff(channel, {}, {
9437
+ [clientId]: entry.state
9438
+ });
9439
+ }
9440
+ if (channelPresence.size === 0) {
9441
+ this.presence.delete(channel);
9442
+ }
9443
+ }
9444
+ /** Send full presence state to a specific client */
9445
+ sendPresenceState(clientId, channel) {
9446
+ const channelPresence = this.presence.get(channel);
9447
+ const presences = {};
9448
+ if (channelPresence) {
9449
+ for (const [id, {
9450
+ state
9451
+ }] of channelPresence) {
9452
+ presences[id] = state;
9453
+ }
9454
+ }
9455
+ const ws = this.clients.get(clientId);
9456
+ if (ws && ws.readyState === WebSocket.OPEN) {
9457
+ ws.send(JSON.stringify({
9458
+ type: "presence_state",
9459
+ channel,
9460
+ presences
9461
+ }));
9462
+ }
9463
+ }
9464
+ /** Broadcast presence diff (joins/leaves) to channel */
9465
+ broadcastPresenceDiff(channel, joins, leaves) {
9466
+ const members = this.channels.get(channel);
9467
+ if (!members) return;
9468
+ const message = JSON.stringify({
9469
+ type: "presence_diff",
9470
+ channel,
9471
+ joins,
9472
+ leaves
9473
+ });
9474
+ for (const memberId of members) {
9475
+ const ws = this.clients.get(memberId);
9476
+ if (ws && ws.readyState === WebSocket.OPEN) {
9477
+ ws.send(message);
9478
+ }
9479
+ }
9480
+ }
9481
+ /** Periodic cleanup for stale presences */
9482
+ ensurePresenceCleanup() {
9483
+ if (this.presenceInterval) return;
9484
+ this.presenceInterval = setInterval(() => {
9485
+ const now = Date.now();
9486
+ for (const [channel, channelPresence] of this.presence) {
9487
+ for (const [clientId, entry] of channelPresence) {
9488
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9489
+ this.removePresence(clientId, channel);
9490
+ }
9491
+ }
9492
+ }
9493
+ if (this.presence.size === 0 && this.presenceInterval) {
9494
+ clearInterval(this.presenceInterval);
9495
+ this.presenceInterval = void 0;
9496
+ }
9497
+ }, 1e4);
9498
+ }
9499
+ // =============================================================================
8960
9500
  // Lifecycle / Cleanup
8961
9501
  // =============================================================================
8962
9502
  /**
@@ -8977,6 +9517,12 @@ class RealtimeService extends EventEmitter {
8977
9517
  }
8978
9518
  this._subscriptions.clear();
8979
9519
  this.subscriptionCallbacks.clear();
9520
+ this.channels.clear();
9521
+ this.presence.clear();
9522
+ if (this.presenceInterval) {
9523
+ clearInterval(this.presenceInterval);
9524
+ this.presenceInterval = void 0;
9525
+ }
8980
9526
  await this.stopListening();
8981
9527
  this.clients.clear();
8982
9528
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9623,8 +10169,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
9623
10169
  break;
9624
10170
  case "subscribe_collection":
9625
10171
  case "subscribe_entity":
9626
- case "unsubscribe": {
9627
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10172
+ case "unsubscribe":
10173
+ case "join_channel":
10174
+ case "leave_channel":
10175
+ case "broadcast":
10176
+ case "presence_track":
10177
+ case "presence_untrack":
10178
+ case "presence_state": {
10179
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9628
10180
  const session = clientSessions.get(clientId);
9629
10181
  const authContext = session?.user ? {
9630
10182
  userId: session.user.userId,
@@ -9743,11 +10295,6 @@ const DEFAULT_ROLES = [{
9743
10295
  create: true,
9744
10296
  edit: true,
9745
10297
  delete: true
9746
- },
9747
- config: {
9748
- createCollections: true,
9749
- editCollections: "all",
9750
- deleteCollections: "all"
9751
10298
  }
9752
10299
  }, {
9753
10300
  id: "editor",
@@ -9758,11 +10305,6 @@ const DEFAULT_ROLES = [{
9758
10305
  create: true,
9759
10306
  edit: true,
9760
10307
  delete: true
9761
- },
9762
- config: {
9763
- createCollections: true,
9764
- editCollections: "own",
9765
- deleteCollections: "own"
9766
10308
  }
9767
10309
  }, {
9768
10310
  id: "viewer",
@@ -9773,11 +10315,10 @@ const DEFAULT_ROLES = [{
9773
10315
  create: false,
9774
10316
  edit: false,
9775
10317
  delete: false
9776
- },
9777
- config: null
10318
+ }
9778
10319
  }];
9779
10320
  async function ensureAuthTablesExist(db, registry) {
9780
- console.log("🔍 Checking auth tables...");
10321
+ logger.info("🔍 Checking auth tables...");
9781
10322
  try {
9782
10323
  let usersTableName = '"users"';
9783
10324
  let userIdType = "TEXT";
@@ -9847,7 +10388,6 @@ async function ensureAuthTablesExist(db, registry) {
9847
10388
  is_admin BOOLEAN DEFAULT FALSE,
9848
10389
  default_permissions JSONB,
9849
10390
  collection_permissions JSONB,
9850
- config JSONB,
9851
10391
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9852
10392
  )
9853
10393
  `);
@@ -9930,34 +10470,85 @@ async function ensureAuthTablesExist(db, registry) {
9930
10470
  `);
9931
10471
  });
9932
10472
  await seedDefaultRoles(db, rolesTableName);
9933
- console.log("✅ Auth tables ready");
10473
+ await db.execute(sql`
10474
+ ALTER TABLE ${sql.raw(usersTableName)}
10475
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10476
+ `);
10477
+ const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
10478
+ const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
10479
+ const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
10480
+ await db.execute(sql`
10481
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
10482
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10483
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10484
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10485
+ secret_encrypted TEXT NOT NULL,
10486
+ friendly_name TEXT,
10487
+ verified BOOLEAN DEFAULT FALSE,
10488
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10489
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10490
+ )
10491
+ `);
10492
+ await db.execute(sql`
10493
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10494
+ ON ${sql.raw(mfaFactorsTableName)}(user_id)
10495
+ `);
10496
+ await db.execute(sql`
10497
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
10498
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10499
+ factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10500
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10501
+ verified_at TIMESTAMP WITH TIME ZONE,
10502
+ ip_address TEXT,
10503
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10504
+ )
10505
+ `);
10506
+ await db.execute(sql`
10507
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10508
+ ON ${sql.raw(mfaChallengesTableName)}(factor_id)
10509
+ `);
10510
+ await db.execute(sql`
10511
+ CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
10512
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10513
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10514
+ code_hash TEXT NOT NULL,
10515
+ used_at TIMESTAMP WITH TIME ZONE,
10516
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10517
+ )
10518
+ `);
10519
+ await db.execute(sql`
10520
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10521
+ ON ${sql.raw(recoveryCodesTableName)}(user_id)
10522
+ `);
10523
+ logger.info("✅ Auth tables ready");
9934
10524
  } catch (error) {
9935
- console.error("❌ Failed to create auth tables:", error);
9936
- console.warn("⚠️ Continuing without creating auth tables.");
10525
+ logger.error("❌ Failed to create auth tables", {
10526
+ error
10527
+ });
10528
+ logger.warn("⚠️ Continuing without creating auth tables.");
9937
10529
  }
9938
10530
  }
9939
10531
  async function seedDefaultRoles(db, rolesTableName) {
9940
10532
  const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
9941
10533
  const count2 = parseInt(result.rows[0]?.count || "0", 10);
9942
10534
  if (count2 > 0) {
9943
- console.log(`📋 Found ${count2} existing roles`);
10535
+ logger.info(`📋 Found ${count2} existing roles`);
9944
10536
  return;
9945
10537
  }
9946
- console.log("🌱 Seeding default roles...");
10538
+ logger.info("🌱 Seeding default roles...");
9947
10539
  for (const role of DEFAULT_ROLES) {
9948
10540
  await db.execute(sql`
9949
- INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10541
+ INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
9950
10542
  VALUES (
9951
10543
  ${role.id},
9952
10544
  ${role.name},
9953
10545
  ${role.is_admin},
9954
- ${JSON.stringify(role.default_permissions)}::jsonb,
9955
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10546
+ ${JSON.stringify(role.default_permissions)}::jsonb
9956
10547
  )
9957
10548
  ON CONFLICT (id) DO NOTHING
9958
10549
  `);
9959
10550
  }
9960
- console.log("✅ Default roles created: admin, editor, viewer");
10551
+ logger.info("✅ Default roles created: admin, editor, viewer");
9961
10552
  }
9962
10553
  function getColumnKey(table, ...keys2) {
9963
10554
  if (!table) return void 0;
@@ -10011,12 +10602,13 @@ class UserService {
10011
10602
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10012
10603
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10013
10604
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10605
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10014
10606
  const createdAt = row.created_at ?? row.createdAt;
10015
10607
  const updatedAt = row.updated_at ?? row.updatedAt;
10016
10608
  const metadata = {
10017
10609
  ...row.metadata || {}
10018
10610
  };
10019
- 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"]);
10611
+ 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"]);
10020
10612
  for (const [key, val] of Object.entries(row)) {
10021
10613
  if (!knownKeys.has(key)) {
10022
10614
  const camelKey = camelCase(key);
@@ -10032,6 +10624,7 @@ class UserService {
10032
10624
  emailVerified,
10033
10625
  emailVerificationToken,
10034
10626
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10627
+ isAnonymous,
10035
10628
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10036
10629
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10037
10630
  metadata
@@ -10048,6 +10641,7 @@ class UserService {
10048
10641
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10049
10642
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10050
10643
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10644
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10051
10645
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10052
10646
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10053
10647
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10059,6 +10653,7 @@ class UserService {
10059
10653
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10060
10654
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10061
10655
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10656
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10062
10657
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10063
10658
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10064
10659
  const metadata = {
@@ -10067,7 +10662,7 @@ class UserService {
10067
10662
  const remainingMetadata = {};
10068
10663
  for (const [key, val] of Object.entries(metadata)) {
10069
10664
  const tableColKey = getColumnKey(this.usersTable, key);
10070
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10665
+ 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) {
10071
10666
  payload[tableColKey] = val;
10072
10667
  } else {
10073
10668
  remainingMetadata[key] = val;
@@ -10255,7 +10850,7 @@ class UserService {
10255
10850
  async getUserRoles(userId) {
10256
10851
  const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
10257
10852
  const result = await this.db.execute(sql`
10258
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10853
+ SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
10259
10854
  FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
10260
10855
  INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10261
10856
  WHERE ur.user_id = ${userId}
@@ -10265,8 +10860,7 @@ class UserService {
10265
10860
  name: row.name,
10266
10861
  isAdmin: row.is_admin,
10267
10862
  defaultPermissions: row.default_permissions,
10268
- collectionPermissions: row.collection_permissions,
10269
- config: row.config
10863
+ collectionPermissions: row.collection_permissions
10270
10864
  }));
10271
10865
  }
10272
10866
  /**
@@ -10332,7 +10926,7 @@ class RoleService {
10332
10926
  async getRoleById(id) {
10333
10927
  const tableName = this.getQualifiedRolesTableName();
10334
10928
  const result = await this.db.execute(sql`
10335
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10929
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10336
10930
  FROM ${sql.raw(tableName)}
10337
10931
  WHERE id = ${id}
10338
10932
  `);
@@ -10343,14 +10937,13 @@ class RoleService {
10343
10937
  name: row.name,
10344
10938
  isAdmin: row.is_admin,
10345
10939
  defaultPermissions: row.default_permissions,
10346
- collectionPermissions: row.collection_permissions,
10347
- config: row.config
10940
+ collectionPermissions: row.collection_permissions
10348
10941
  };
10349
10942
  }
10350
10943
  async listRoles() {
10351
10944
  const tableName = this.getQualifiedRolesTableName();
10352
10945
  const result = await this.db.execute(sql`
10353
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10946
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10354
10947
  FROM ${sql.raw(tableName)}
10355
10948
  ORDER BY name
10356
10949
  `);
@@ -10359,23 +10952,21 @@ class RoleService {
10359
10952
  name: row.name,
10360
10953
  isAdmin: row.is_admin,
10361
10954
  defaultPermissions: row.default_permissions,
10362
- collectionPermissions: row.collection_permissions,
10363
- config: row.config
10955
+ collectionPermissions: row.collection_permissions
10364
10956
  }));
10365
10957
  }
10366
10958
  async createRole(data) {
10367
10959
  const tableName = this.getQualifiedRolesTableName();
10368
10960
  const result = await this.db.execute(sql`
10369
- INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10961
+ INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
10370
10962
  VALUES (
10371
10963
  ${data.id},
10372
10964
  ${data.name},
10373
10965
  ${data.isAdmin ?? false},
10374
10966
  ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10375
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10376
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10967
+ ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
10377
10968
  )
10378
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10969
+ RETURNING id, name, is_admin, default_permissions, collection_permissions
10379
10970
  `);
10380
10971
  const row = result.rows[0];
10381
10972
  return {
@@ -10383,8 +10974,7 @@ class RoleService {
10383
10974
  name: row.name,
10384
10975
  isAdmin: row.is_admin,
10385
10976
  defaultPermissions: row.default_permissions,
10386
- collectionPermissions: row.collection_permissions,
10387
- config: row.config
10977
+ collectionPermissions: row.collection_permissions
10388
10978
  };
10389
10979
  }
10390
10980
  async updateRole(id, data) {
@@ -10397,8 +10987,7 @@ class RoleService {
10397
10987
  name = ${data.name ?? existing.name},
10398
10988
  is_admin = ${data.isAdmin ?? existing.isAdmin},
10399
10989
  default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10400
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10401
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10990
+ collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
10402
10991
  WHERE id = ${id}
10403
10992
  `);
10404
10993
  return this.getRoleById(id);
@@ -10677,8 +11266,7 @@ class PostgresAuthRepository {
10677
11266
  return this.roleService.createRole({
10678
11267
  ...data,
10679
11268
  defaultPermissions: data.defaultPermissions ?? null,
10680
- collectionPermissions: data.collectionPermissions ?? null,
10681
- config: data.config ?? null
11269
+ collectionPermissions: data.collectionPermissions ?? null
10682
11270
  });
10683
11271
  }
10684
11272
  async updateRole(id, data) {
@@ -10721,6 +11309,219 @@ class PostgresAuthRepository {
10721
11309
  async deleteExpiredTokens() {
10722
11310
  await this.tokenRepository.deleteExpiredTokens();
10723
11311
  }
11312
+ // MFA operations (delegate to MfaService)
11313
+ _mfaService = null;
11314
+ getMfaService() {
11315
+ if (!this._mfaService) {
11316
+ this._mfaService = new MfaService(this.db);
11317
+ }
11318
+ return this._mfaService;
11319
+ }
11320
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11321
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11322
+ }
11323
+ async getMfaFactors(userId) {
11324
+ return this.getMfaService().getMfaFactors(userId);
11325
+ }
11326
+ async getMfaFactorById(factorId) {
11327
+ return this.getMfaService().getMfaFactorById(factorId);
11328
+ }
11329
+ async verifyMfaFactor(factorId) {
11330
+ return this.getMfaService().verifyMfaFactor(factorId);
11331
+ }
11332
+ async deleteMfaFactor(factorId, userId) {
11333
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11334
+ }
11335
+ async createMfaChallenge(factorId, ipAddress) {
11336
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11337
+ }
11338
+ async getMfaChallengeById(challengeId) {
11339
+ return this.getMfaService().getMfaChallengeById(challengeId);
11340
+ }
11341
+ async verifyMfaChallenge(challengeId) {
11342
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11343
+ }
11344
+ async createRecoveryCodes(userId, codeHashes) {
11345
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11346
+ }
11347
+ async useRecoveryCode(userId, codeHash) {
11348
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11349
+ }
11350
+ async getUnusedRecoveryCodeCount(userId) {
11351
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11352
+ }
11353
+ async deleteAllRecoveryCodes(userId) {
11354
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11355
+ }
11356
+ async hasVerifiedMfaFactors(userId) {
11357
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11358
+ }
11359
+ }
11360
+ class MfaService {
11361
+ constructor(db, schemaName = "rebase") {
11362
+ this.db = db;
11363
+ this.schemaName = schemaName;
11364
+ }
11365
+ qualify(tableName) {
11366
+ return `"${this.schemaName}"."${tableName}"`;
11367
+ }
11368
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11369
+ const tableName = this.qualify("mfa_factors");
11370
+ const result = await this.db.execute(sql`
11371
+ INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11372
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11373
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11374
+ `);
11375
+ const row = result.rows[0];
11376
+ return {
11377
+ id: row.id,
11378
+ userId: row.user_id,
11379
+ factorType: row.factor_type,
11380
+ friendlyName: row.friendly_name ?? void 0,
11381
+ verified: row.verified,
11382
+ createdAt: new Date(row.created_at),
11383
+ updatedAt: new Date(row.updated_at)
11384
+ };
11385
+ }
11386
+ async getMfaFactors(userId) {
11387
+ const tableName = this.qualify("mfa_factors");
11388
+ const result = await this.db.execute(sql`
11389
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11390
+ FROM ${sql.raw(tableName)}
11391
+ WHERE user_id = ${userId}
11392
+ ORDER BY created_at
11393
+ `);
11394
+ return result.rows.map((row) => ({
11395
+ id: row.id,
11396
+ userId: row.user_id,
11397
+ factorType: row.factor_type,
11398
+ friendlyName: row.friendly_name ?? void 0,
11399
+ verified: row.verified,
11400
+ createdAt: new Date(row.created_at),
11401
+ updatedAt: new Date(row.updated_at)
11402
+ }));
11403
+ }
11404
+ async getMfaFactorById(factorId) {
11405
+ const tableName = this.qualify("mfa_factors");
11406
+ const result = await this.db.execute(sql`
11407
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11408
+ FROM ${sql.raw(tableName)}
11409
+ WHERE id = ${factorId}
11410
+ `);
11411
+ if (result.rows.length === 0) return null;
11412
+ const row = result.rows[0];
11413
+ return {
11414
+ id: row.id,
11415
+ userId: row.user_id,
11416
+ factorType: row.factor_type,
11417
+ secretEncrypted: row.secret_encrypted,
11418
+ friendlyName: row.friendly_name ?? void 0,
11419
+ verified: row.verified,
11420
+ createdAt: new Date(row.created_at),
11421
+ updatedAt: new Date(row.updated_at)
11422
+ };
11423
+ }
11424
+ async verifyMfaFactor(factorId) {
11425
+ const tableName = this.qualify("mfa_factors");
11426
+ await this.db.execute(sql`
11427
+ UPDATE ${sql.raw(tableName)}
11428
+ SET verified = TRUE, updated_at = NOW()
11429
+ WHERE id = ${factorId}
11430
+ `);
11431
+ }
11432
+ async deleteMfaFactor(factorId, userId) {
11433
+ const tableName = this.qualify("mfa_factors");
11434
+ await this.db.execute(sql`
11435
+ DELETE FROM ${sql.raw(tableName)}
11436
+ WHERE id = ${factorId} AND user_id = ${userId}
11437
+ `);
11438
+ }
11439
+ async createMfaChallenge(factorId, ipAddress) {
11440
+ const tableName = this.qualify("mfa_challenges");
11441
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11442
+ const result = await this.db.execute(sql`
11443
+ INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
11444
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11445
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11446
+ `);
11447
+ const row = result.rows[0];
11448
+ return {
11449
+ id: row.id,
11450
+ factorId: row.factor_id,
11451
+ createdAt: new Date(row.created_at),
11452
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11453
+ ipAddress: row.ip_address ?? void 0
11454
+ };
11455
+ }
11456
+ async getMfaChallengeById(challengeId) {
11457
+ const tableName = this.qualify("mfa_challenges");
11458
+ const result = await this.db.execute(sql`
11459
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11460
+ FROM ${sql.raw(tableName)}
11461
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11462
+ `);
11463
+ if (result.rows.length === 0) return null;
11464
+ const row = result.rows[0];
11465
+ return {
11466
+ id: row.id,
11467
+ factorId: row.factor_id,
11468
+ createdAt: new Date(row.created_at),
11469
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11470
+ ipAddress: row.ip_address ?? void 0
11471
+ };
11472
+ }
11473
+ async verifyMfaChallenge(challengeId) {
11474
+ const tableName = this.qualify("mfa_challenges");
11475
+ await this.db.execute(sql`
11476
+ UPDATE ${sql.raw(tableName)}
11477
+ SET verified_at = NOW()
11478
+ WHERE id = ${challengeId}
11479
+ `);
11480
+ }
11481
+ async createRecoveryCodes(userId, codeHashes) {
11482
+ const tableName = this.qualify("recovery_codes");
11483
+ await this.db.execute(sql`
11484
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11485
+ `);
11486
+ for (const hash of codeHashes) {
11487
+ await this.db.execute(sql`
11488
+ INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
11489
+ VALUES (${userId}, ${hash})
11490
+ `);
11491
+ }
11492
+ }
11493
+ async useRecoveryCode(userId, codeHash) {
11494
+ const tableName = this.qualify("recovery_codes");
11495
+ const result = await this.db.execute(sql`
11496
+ UPDATE ${sql.raw(tableName)}
11497
+ SET used_at = NOW()
11498
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11499
+ RETURNING id
11500
+ `);
11501
+ return result.rows.length > 0;
11502
+ }
11503
+ async getUnusedRecoveryCodeCount(userId) {
11504
+ const tableName = this.qualify("recovery_codes");
11505
+ const result = await this.db.execute(sql`
11506
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11507
+ WHERE user_id = ${userId} AND used_at IS NULL
11508
+ `);
11509
+ return result.rows[0].count;
11510
+ }
11511
+ async deleteAllRecoveryCodes(userId) {
11512
+ const tableName = this.qualify("recovery_codes");
11513
+ await this.db.execute(sql`
11514
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11515
+ `);
11516
+ }
11517
+ async hasVerifiedMfaFactors(userId) {
11518
+ const tableName = this.qualify("mfa_factors");
11519
+ const result = await this.db.execute(sql`
11520
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11521
+ WHERE user_id = ${userId} AND verified = TRUE
11522
+ `);
11523
+ return result.rows[0].count > 0;
11524
+ }
10724
11525
  }
10725
11526
  const DEFAULT_RETENTION = {
10726
11527
  maxEntries: 200,
@@ -10931,7 +11732,7 @@ function createPostgresBootstrapper(pgConfig) {
10931
11732
  const registry = new PostgresCollectionRegistry();
10932
11733
  if (collections) {
10933
11734
  registry.registerMultiple(collections);
10934
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11735
+ logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
10935
11736
  }
10936
11737
  if (pgConfig.schema?.tables) {
10937
11738
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -10957,10 +11758,28 @@ function createPostgresBootstrapper(pgConfig) {
10957
11758
  try {
10958
11759
  await schemaAwareDb.execute(sql`SELECT 1`);
10959
11760
  } catch (err) {
10960
- console.error("❌ Failed to connect to PostgreSQL:", err);
10961
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11761
+ logger.error("❌ Failed to connect to PostgreSQL", {
11762
+ error: err
11763
+ });
11764
+ logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
10962
11765
  }
10963
11766
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11767
+ let readDb;
11768
+ const readUrl = process.env.DATABASE_READ_URL;
11769
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11770
+ try {
11771
+ const {
11772
+ createReadReplicaConnection: createReadReplicaConnection2
11773
+ } = await Promise.resolve().then(() => connection);
11774
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11775
+ readDb = readResources.db;
11776
+ logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11777
+ } catch (err) {
11778
+ logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11779
+ error: err
11780
+ });
11781
+ }
11782
+ }
10964
11783
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
10965
11784
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
10966
11785
  realtimeService.setDataDriver(driver);
@@ -10968,18 +11787,24 @@ function createPostgresBootstrapper(pgConfig) {
10968
11787
  try {
10969
11788
  await driver.branchService.ensureBranchMetadataTable();
10970
11789
  } catch (err) {
10971
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11790
+ logger.warn("⚠️ Could not initialize branch metadata table", {
11791
+ error: err
11792
+ });
10972
11793
  }
10973
11794
  }
10974
- if (pgConfig.connectionString) {
11795
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11796
+ if (directUrl) {
10975
11797
  try {
10976
- await realtimeService.startListening(pgConfig.connectionString);
11798
+ await realtimeService.startListening(directUrl);
10977
11799
  } catch (err) {
10978
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11800
+ logger.warn("⚠️ Cross-instance realtime could not be started", {
11801
+ error: err
11802
+ });
10979
11803
  }
10980
11804
  }
10981
11805
  const internals = {
10982
11806
  db: schemaAwareDb,
11807
+ readDb,
10983
11808
  registry,
10984
11809
  realtimeService,
10985
11810
  driver,
@@ -11117,14 +11942,22 @@ export {
11117
11942
  RealtimeService,
11118
11943
  appConfig,
11119
11944
  createAuthSchema,
11945
+ createDirectDatabaseConnection,
11120
11946
  createPostgresAdapter,
11121
11947
  createPostgresBootstrapper,
11122
11948
  createPostgresDatabaseConnection,
11123
11949
  createPostgresWebSocket,
11950
+ createReadReplicaConnection,
11124
11951
  generateSchema,
11952
+ mfaChallenges,
11953
+ mfaChallengesRelations,
11954
+ mfaFactors,
11955
+ mfaFactorsRelations,
11125
11956
  passwordResetTokens,
11126
11957
  passwordResetTokensRelations,
11127
11958
  rebaseSchema,
11959
+ recoveryCodes,
11960
+ recoveryCodesRelations,
11128
11961
  refreshTokens,
11129
11962
  refreshTokensRelations,
11130
11963
  roles,