@rebasepro/server-postgresql 0.2.3 → 0.2.5

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 (63) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/index.es.js +1075 -470
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1071 -466
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +3 -1
  9. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  10. package/dist/server-postgresql/src/auth/services.d.ts +48 -31
  11. package/dist/server-postgresql/src/connection.d.ts +25 -0
  12. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2135 -41
  13. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  14. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  15. package/dist/server-postgresql/src/services/entityService.d.ts +6 -0
  16. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  17. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  18. package/dist/types/src/controllers/auth.d.ts +4 -26
  19. package/dist/types/src/controllers/client.d.ts +25 -43
  20. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  21. package/dist/types/src/controllers/data.d.ts +4 -0
  22. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  23. package/dist/types/src/controllers/registry.d.ts +5 -4
  24. package/dist/types/src/rebase_context.d.ts +1 -1
  25. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  26. package/dist/types/src/types/backend.d.ts +2 -2
  27. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  28. package/dist/types/src/types/collections.d.ts +0 -4
  29. package/dist/types/src/types/component_ref.d.ts +1 -1
  30. package/dist/types/src/types/cron.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +1 -0
  32. package/dist/types/src/types/export_import.d.ts +1 -1
  33. package/dist/types/src/types/formex.d.ts +2 -2
  34. package/dist/types/src/types/properties.d.ts +9 -7
  35. package/dist/types/src/types/translations.d.ts +28 -12
  36. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  37. package/dist/types/src/users/index.d.ts +0 -1
  38. package/dist/types/src/users/user.d.ts +0 -1
  39. package/package.json +6 -6
  40. package/src/PostgresBackendDriver.ts +14 -2
  41. package/src/PostgresBootstrapper.ts +30 -20
  42. package/src/auth/ensure-tables.ts +116 -103
  43. package/src/auth/services.ts +347 -177
  44. package/src/connection.ts +77 -0
  45. package/src/data-transformer.ts +2 -2
  46. package/src/schema/auth-schema.ts +85 -75
  47. package/src/schema/doctor.ts +44 -3
  48. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  49. package/src/schema/generate-drizzle-schema.ts +6 -6
  50. package/src/schema/introspect-db-logic.ts +7 -0
  51. package/src/services/EntityFetchService.ts +69 -10
  52. package/src/services/EntityPersistService.ts +9 -0
  53. package/src/services/entityService.ts +9 -0
  54. package/src/services/realtimeService.ts +214 -2
  55. package/src/utils/drizzle-conditions.ts +74 -2
  56. package/src/websocket.ts +10 -2
  57. package/test/auth-services.test.ts +10 -166
  58. package/test/doctor.test.ts +6 -2
  59. package/test/drizzle-conditions.test.ts +168 -0
  60. package/vite.config.ts +1 -1
  61. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  62. package/dist/types/src/users/roles.d.ts +0 -22
  63. package/src/schema/default-collections.ts +0 -69
package/dist/index.umd.js CHANGED
@@ -59,6 +59,70 @@
59
59
  connectionString
60
60
  };
61
61
  }
62
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
63
+ const opts = {
64
+ ...DEFAULT_POOL,
65
+ max: 5,
66
+ ...poolConfig
67
+ };
68
+ const pgPoolConfig = {
69
+ connectionString,
70
+ max: opts.max,
71
+ idleTimeoutMillis: opts.idleTimeoutMillis,
72
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
73
+ query_timeout: opts.queryTimeout,
74
+ statement_timeout: opts.statementTimeout,
75
+ keepAlive: opts.keepAlive,
76
+ keepAliveInitialDelayMillis: 0
77
+ };
78
+ const pool = new pg.Pool(pgPoolConfig);
79
+ pool.on("error", (err) => {
80
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
81
+ });
82
+ const db = schema ? nodePostgres.drizzle(pool, {
83
+ schema
84
+ }) : nodePostgres.drizzle(pool);
85
+ return {
86
+ db,
87
+ pool,
88
+ connectionString
89
+ };
90
+ }
91
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
92
+ const opts = {
93
+ ...DEFAULT_POOL,
94
+ max: 10,
95
+ ...poolConfig
96
+ };
97
+ const pgPoolConfig = {
98
+ connectionString,
99
+ max: opts.max,
100
+ idleTimeoutMillis: opts.idleTimeoutMillis,
101
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
102
+ query_timeout: opts.queryTimeout,
103
+ statement_timeout: opts.statementTimeout,
104
+ keepAlive: opts.keepAlive,
105
+ keepAliveInitialDelayMillis: 0
106
+ };
107
+ const pool = new pg.Pool(pgPoolConfig);
108
+ pool.on("error", (err) => {
109
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
110
+ });
111
+ const db = schema ? nodePostgres.drizzle(pool, {
112
+ schema
113
+ }) : nodePostgres.drizzle(pool);
114
+ return {
115
+ db,
116
+ pool,
117
+ connectionString
118
+ };
119
+ }
120
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
121
+ __proto__: null,
122
+ createDirectDatabaseConnection,
123
+ createPostgresDatabaseConnection,
124
+ createReadReplicaConnection
125
+ }, Symbol.toStringTag, { value: "Module" }));
62
126
  class Vector {
63
127
  value;
64
128
  constructor(value) {
@@ -978,6 +1042,9 @@
978
1042
  return output;
979
1043
  }
980
1044
  for (const key in source) {
1045
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1046
+ continue;
1047
+ }
981
1048
  if (Object.prototype.hasOwnProperty.call(source, key)) {
982
1049
  const sourceValue = source[key];
983
1050
  const outputValue = output[key];
@@ -2652,6 +2719,142 @@
2652
2719
  };
2653
2720
  }
2654
2721
  }
2722
+ const defaultUsersCollection = {
2723
+ name: "Users",
2724
+ singularName: "User",
2725
+ slug: "users",
2726
+ table: "users",
2727
+ schema: "rebase",
2728
+ icon: "Users",
2729
+ group: "Settings",
2730
+ openEntityMode: "dialog",
2731
+ disableDefaultActions: ["copy"],
2732
+ sort: ["createdAt", "desc"],
2733
+ properties: {
2734
+ id: {
2735
+ name: "ID",
2736
+ type: "string",
2737
+ isId: "uuid",
2738
+ ui: {
2739
+ readOnly: true
2740
+ }
2741
+ },
2742
+ email: {
2743
+ name: "Email",
2744
+ type: "string",
2745
+ validation: {
2746
+ required: true,
2747
+ unique: true
2748
+ }
2749
+ },
2750
+ displayName: {
2751
+ name: "Name",
2752
+ type: "string",
2753
+ columnName: "display_name",
2754
+ validation: {
2755
+ required: true
2756
+ }
2757
+ },
2758
+ photoURL: {
2759
+ name: "Photo URL",
2760
+ type: "string",
2761
+ columnName: "photo_url",
2762
+ url: "image"
2763
+ },
2764
+ roles: {
2765
+ name: "Roles",
2766
+ type: "array",
2767
+ columnType: "text[]",
2768
+ of: {
2769
+ name: "Role",
2770
+ type: "string",
2771
+ enum: {
2772
+ admin: "Admin",
2773
+ editor: "Editor",
2774
+ viewer: "Viewer"
2775
+ }
2776
+ }
2777
+ },
2778
+ passwordHash: {
2779
+ name: "Password Hash",
2780
+ type: "string",
2781
+ columnName: "password_hash",
2782
+ ui: {
2783
+ hideFromCollection: true,
2784
+ disabled: {
2785
+ hidden: true
2786
+ }
2787
+ }
2788
+ },
2789
+ emailVerified: {
2790
+ name: "Email Verified",
2791
+ type: "boolean",
2792
+ columnName: "email_verified",
2793
+ defaultValue: false,
2794
+ ui: {
2795
+ hideFromCollection: true,
2796
+ disabled: {
2797
+ hidden: true
2798
+ }
2799
+ }
2800
+ },
2801
+ emailVerificationToken: {
2802
+ name: "Email Verification Token",
2803
+ type: "string",
2804
+ columnName: "email_verification_token",
2805
+ ui: {
2806
+ hideFromCollection: true,
2807
+ disabled: {
2808
+ hidden: true
2809
+ }
2810
+ }
2811
+ },
2812
+ emailVerificationSentAt: {
2813
+ name: "Email Verification Sent At",
2814
+ type: "date",
2815
+ columnName: "email_verification_sent_at",
2816
+ ui: {
2817
+ hideFromCollection: true,
2818
+ disabled: {
2819
+ hidden: true
2820
+ }
2821
+ }
2822
+ },
2823
+ metadata: {
2824
+ name: "Metadata",
2825
+ type: "map",
2826
+ defaultValue: {},
2827
+ ui: {
2828
+ hideFromCollection: true,
2829
+ disabled: {
2830
+ hidden: true
2831
+ }
2832
+ }
2833
+ },
2834
+ createdAt: {
2835
+ name: "Created At",
2836
+ type: "date",
2837
+ columnName: "created_at",
2838
+ ui: {
2839
+ readOnly: true
2840
+ }
2841
+ },
2842
+ updatedAt: {
2843
+ name: "Updated At",
2844
+ type: "date",
2845
+ columnName: "updated_at",
2846
+ autoValue: "on_update",
2847
+ ui: {
2848
+ hideFromCollection: true,
2849
+ disabled: {
2850
+ hidden: true
2851
+ }
2852
+ }
2853
+ }
2854
+ },
2855
+ listProperties: ["displayName", "email", "roles", "createdAt"],
2856
+ propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
2857
+ };
2655
2858
  function mapOperator(op) {
2656
2859
  switch (op) {
2657
2860
  case "==":
@@ -2898,6 +3101,9 @@
2898
3101
  }
2899
3102
  });
2900
3103
  },
3104
+ deleteAll: driver.deleteAll ? async () => {
3105
+ return driver.deleteAll(slug);
3106
+ } : void 0,
2901
3107
  count: driver.countEntities ? async (params) => {
2902
3108
  return driver.countEntities({
2903
3109
  path: slug,
@@ -2992,7 +3198,13 @@
2992
3198
  for (const [field, filterParam] of Object.entries(filter)) {
2993
3199
  if (!filterParam) continue;
2994
3200
  const [op, value] = filterParam;
2995
- const fieldColumn = table[field];
3201
+ let fieldColumn = table[field];
3202
+ if (!fieldColumn) {
3203
+ const relationKey = `${field}_id`;
3204
+ if (relationKey in table) {
3205
+ fieldColumn = table[relationKey];
3206
+ }
3207
+ }
2996
3208
  if (!fieldColumn) {
2997
3209
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2998
3210
  continue;
@@ -3034,6 +3246,17 @@
3034
3246
  return null;
3035
3247
  case "array-contains":
3036
3248
  return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3249
+ case "array-contains-any":
3250
+ if (Array.isArray(value) && value.length > 0) {
3251
+ const textValues = value.map((v) => String(v));
3252
+ return drizzleOrm.sql`${column} ?| array[${drizzleOrm.sql.join(textValues.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)}]`;
3253
+ }
3254
+ return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3255
+ case "not-in":
3256
+ if (Array.isArray(value) && value.length > 0) {
3257
+ return drizzleOrm.sql`${column} NOT IN (${drizzleOrm.sql.join(value.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)})`;
3258
+ }
3259
+ return null;
3037
3260
  default:
3038
3261
  console.warn(`Unsupported filter operation: ${op}`);
3039
3262
  return null;
@@ -3549,6 +3772,40 @@
3549
3772
  return null;
3550
3773
  }
3551
3774
  }
3775
+ /**
3776
+ * Build vector similarity search expressions for pgvector.
3777
+ *
3778
+ * Returns:
3779
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3780
+ * - `filter`: optional WHERE clause for distance threshold
3781
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3782
+ */
3783
+ static buildVectorSearchConditions(table, vectorSearch) {
3784
+ const column = table[vectorSearch.property];
3785
+ if (!column) {
3786
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3787
+ }
3788
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3789
+ const distanceFn = vectorSearch.distance || "cosine";
3790
+ let operator;
3791
+ switch (distanceFn) {
3792
+ case "cosine":
3793
+ operator = "<=>";
3794
+ break;
3795
+ case "l2":
3796
+ operator = "<->";
3797
+ break;
3798
+ case "inner_product":
3799
+ operator = "<#>";
3800
+ break;
3801
+ }
3802
+ const distanceExpr = drizzleOrm.sql`${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}`;
3803
+ return {
3804
+ orderBy: distanceExpr,
3805
+ filter: vectorSearch.threshold != null ? drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3806
+ distanceSelect: drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)})`
3807
+ };
3808
+ }
3552
3809
  }
3553
3810
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3554
3811
  function getColumnMeta(col) {
@@ -5494,7 +5751,7 @@
5494
5751
  const qb = this.getQueryBuilder(tableName);
5495
5752
  const withConfig = this.buildWithConfig(collection);
5496
5753
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5497
- if (qb && !options.searchString && !hasRelations) {
5754
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5498
5755
  try {
5499
5756
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5500
5757
  const results2 = await qb.findMany(queryOpts);
@@ -5508,7 +5765,14 @@
5508
5765
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5509
5766
  }
5510
5767
  }
5511
- let query = this.db.select().from(table).$dynamic();
5768
+ let vectorMeta;
5769
+ if (options.vectorSearch) {
5770
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5771
+ }
5772
+ let query = vectorMeta ? this.db.select({
5773
+ table_row: table,
5774
+ _distance: vectorMeta.distanceSelect
5775
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5512
5776
  const allConditions = [];
5513
5777
  if (options.searchString) {
5514
5778
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5519,12 +5783,17 @@
5519
5783
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5520
5784
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5521
5785
  }
5786
+ if (vectorMeta?.filter) {
5787
+ allConditions.push(vectorMeta.filter);
5788
+ }
5522
5789
  if (allConditions.length > 0) {
5523
5790
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5524
5791
  if (finalCondition) query = query.where(finalCondition);
5525
5792
  }
5526
5793
  const orderExpressions = [];
5527
- if (options.orderBy) {
5794
+ if (vectorMeta) {
5795
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
5796
+ } else if (options.orderBy) {
5528
5797
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5529
5798
  if (orderByField) {
5530
5799
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5540,10 +5809,14 @@
5540
5809
  if (finalCondition) query = query.where(finalCondition);
5541
5810
  }
5542
5811
  }
5543
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5812
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5544
5813
  if (limitValue) query = query.limit(limitValue);
5545
5814
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5546
- const results = await query;
5815
+ const rawResults = await query;
5816
+ const results = vectorMeta ? rawResults.map((r) => ({
5817
+ ...r.table_row,
5818
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5819
+ })) : rawResults;
5547
5820
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5548
5821
  }
5549
5822
  /**
@@ -5765,7 +6038,7 @@
5765
6038
  const idField = table[idInfo.fieldName];
5766
6039
  const tableName = drizzleOrm.getTableName(table);
5767
6040
  const qb = this.getQueryBuilder(tableName);
5768
- if (qb && !options.searchString) {
6041
+ if (qb && !options.searchString && !options.vectorSearch) {
5769
6042
  try {
5770
6043
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5771
6044
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5911,7 +6184,14 @@
5911
6184
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5912
6185
  const idInfo = idInfoArray[0];
5913
6186
  const idField = table[idInfo.fieldName];
5914
- let query = this.db.select().from(table).$dynamic();
6187
+ let vectorMeta;
6188
+ if (options.vectorSearch) {
6189
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6190
+ }
6191
+ let query = vectorMeta ? this.db.select({
6192
+ table_row: table,
6193
+ _distance: vectorMeta.distanceSelect
6194
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5915
6195
  const allConditions = [];
5916
6196
  if (options.searchString) {
5917
6197
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5922,12 +6202,17 @@
5922
6202
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5923
6203
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5924
6204
  }
6205
+ if (vectorMeta?.filter) {
6206
+ allConditions.push(vectorMeta.filter);
6207
+ }
5925
6208
  if (allConditions.length > 0) {
5926
6209
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5927
6210
  if (finalCondition) query = query.where(finalCondition);
5928
6211
  }
5929
6212
  const orderExpressions = [];
5930
- if (options.orderBy) {
6213
+ if (vectorMeta) {
6214
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
6215
+ } else if (options.orderBy) {
5931
6216
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5932
6217
  if (orderByField) {
5933
6218
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5935,10 +6220,17 @@
5935
6220
  }
5936
6221
  orderExpressions.push(drizzleOrm.desc(idField));
5937
6222
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5938
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6223
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5939
6224
  if (limitValue) query = query.limit(limitValue);
5940
6225
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5941
- return await query;
6226
+ const rawResults = await query;
6227
+ if (vectorMeta) {
6228
+ return rawResults.map((r) => ({
6229
+ ...r.table_row,
6230
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6231
+ }));
6232
+ }
6233
+ return rawResults;
5942
6234
  }
5943
6235
  /**
5944
6236
  * Check if the Drizzle instance has the relational query API available
@@ -6076,6 +6368,14 @@
6076
6368
  const parsedId = parsedIdObj[idInfo.fieldName];
6077
6369
  await this.db.delete(table).where(drizzleOrm.eq(idField, parsedId));
6078
6370
  }
6371
+ /**
6372
+ * Delete all entities from a collection
6373
+ */
6374
+ async deleteAll(collectionPath, _databaseId) {
6375
+ const collection = getCollectionByPath(collectionPath, this.registry);
6376
+ const table = getTableForCollection(collection, this.registry);
6377
+ await this.db.delete(table);
6378
+ }
6079
6379
  /**
6080
6380
  * Save an entity (create or update)
6081
6381
  */
@@ -6407,6 +6707,12 @@
6407
6707
  async deleteEntity(collectionPath, entityId, databaseId) {
6408
6708
  return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
6409
6709
  }
6710
+ /**
6711
+ * Delete all entities from a collection
6712
+ */
6713
+ async deleteAll(collectionPath, databaseId) {
6714
+ return this.persistService.deleteAll(collectionPath, databaseId);
6715
+ }
6410
6716
  /**
6411
6717
  * Execute raw SQL
6412
6718
  */
@@ -6692,7 +6998,8 @@
6692
6998
  startAfter,
6693
6999
  orderBy,
6694
7000
  searchString,
6695
- order
7001
+ order,
7002
+ vectorSearch
6696
7003
  }) {
6697
7004
  const entities = await this.entityService.fetchCollection(path2, {
6698
7005
  filter,
@@ -6702,7 +7009,8 @@
6702
7009
  offset,
6703
7010
  startAfter,
6704
7011
  databaseId: collection?.databaseId,
6705
- searchString
7012
+ searchString,
7013
+ vectorSearch
6706
7014
  });
6707
7015
  const {
6708
7016
  collection: resolvedCollection,
@@ -7105,6 +7413,10 @@
7105
7413
  await this.realtimeService.notifyEntityUpdate(entity.path, entity.id.toString(), null, entity.databaseId || resolvedCollection?.databaseId);
7106
7414
  }
7107
7415
  }
7416
+ async deleteAll(path2) {
7417
+ await this.entityService.deleteAll(path2);
7418
+ await this.realtimeService.notifyEntityUpdate(path2, "*", null);
7419
+ }
7108
7420
  async checkUniqueField(path2, name, value, entityId, collection) {
7109
7421
  return this.entityService.checkUniqueField(path2, name, value, entityId, collection?.databaseId);
7110
7422
  }
@@ -7374,11 +7686,11 @@
7374
7686
  console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
7375
7687
  userId = "anonymous";
7376
7688
  }
7377
- const userRoles2 = this.user?.roles ?? [];
7689
+ const userRoles = this.user?.roles ?? [];
7378
7690
  if (!this.user?.roles) {
7379
7691
  console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
7380
7692
  }
7381
- const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7693
+ const normalizedRoles = userRoles.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7382
7694
  const rolesString = normalizedRoles.join(",");
7383
7695
  await tx.execute(drizzleOrm.sql`
7384
7696
  SELECT
@@ -7386,7 +7698,7 @@
7386
7698
  set_config('app.user_roles', ${rolesString}, true),
7387
7699
  set_config('app.jwt', ${JSON.stringify({
7388
7700
  sub: userId,
7389
- roles: userRoles2
7701
+ roles: userRoles
7390
7702
  })}, true)
7391
7703
  `);
7392
7704
  const txEntityService = new EntityService(tx, this.delegate.registry);
@@ -7441,6 +7753,9 @@
7441
7753
  async deleteEntity(props) {
7442
7754
  return this.withTransaction((delegate) => delegate.deleteEntity(props));
7443
7755
  }
7756
+ async deleteAll(path2) {
7757
+ return this.delegate.deleteAll(path2);
7758
+ }
7444
7759
  async checkUniqueField(path2, name, value, entityId, collection) {
7445
7760
  return this.withTransaction((delegate) => delegate.checkUniqueField(path2, name, value, entityId, collection));
7446
7761
  }
@@ -7520,11 +7835,10 @@
7520
7835
  this.pools.clear();
7521
7836
  }
7522
7837
  }
7523
- function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase") {
7524
- const rolesSchema = rolesSchemaName === "public" ? null : pgCore.pgSchema(rolesSchemaName);
7838
+ function createAuthSchema(usersSchemaName = "rebase") {
7525
7839
  const usersSchema2 = usersSchemaName === "public" ? null : pgCore.pgSchema(usersSchemaName);
7526
- const rolesTableCreator = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgCore.pgTable;
7527
- const usersTableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgCore.pgTable;
7840
+ const tableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgCore.pgTable;
7841
+ const usersTableCreator = tableCreator;
7528
7842
  const users2 = usersTableCreator("users", {
7529
7843
  id: pgCore.uuid("id").defaultRandom().primaryKey(),
7530
7844
  email: pgCore.varchar("email", {
@@ -7545,38 +7859,13 @@
7545
7859
  length: 255
7546
7860
  }),
7547
7861
  emailVerificationSentAt: pgCore.timestamp("email_verification_sent_at"),
7862
+ isAnonymous: pgCore.boolean("is_anonymous").default(false).notNull(),
7863
+ roles: pgCore.text("roles").array().default([]).notNull(),
7548
7864
  metadata: pgCore.jsonb("metadata").$type().default({}).notNull(),
7549
7865
  createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7550
7866
  updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7551
7867
  });
7552
- const roles2 = rolesTableCreator("roles", {
7553
- id: pgCore.varchar("id", {
7554
- length: 50
7555
- }).primaryKey(),
7556
- // 'admin', 'editor', 'viewer'
7557
- name: pgCore.varchar("name", {
7558
- length: 100
7559
- }).notNull(),
7560
- isAdmin: pgCore.boolean("is_admin").default(false).notNull(),
7561
- defaultPermissions: pgCore.jsonb("default_permissions").$type(),
7562
- collectionPermissions: pgCore.jsonb("collection_permissions").$type(),
7563
- config: pgCore.jsonb("config").$type()
7564
- });
7565
- const userRoles2 = rolesTableCreator("user_roles", {
7566
- userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7567
- onDelete: "cascade"
7568
- }),
7569
- roleId: pgCore.varchar("role_id", {
7570
- length: 50
7571
- }).notNull().references(() => roles2.id, {
7572
- onDelete: "cascade"
7573
- })
7574
- }, (table) => ({
7575
- pk: pgCore.primaryKey({
7576
- columns: [table.userId, table.roleId]
7577
- })
7578
- }));
7579
- const refreshTokens2 = rolesTableCreator("refresh_tokens", {
7868
+ const refreshTokens2 = tableCreator("refresh_tokens", {
7580
7869
  id: pgCore.uuid("id").defaultRandom().primaryKey(),
7581
7870
  userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7582
7871
  onDelete: "cascade"
@@ -7595,7 +7884,7 @@
7595
7884
  }, (table) => ({
7596
7885
  uniqueDeviceSession: pgCore.unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
7597
7886
  }));
7598
- const passwordResetTokens2 = rolesTableCreator("password_reset_tokens", {
7887
+ const passwordResetTokens2 = tableCreator("password_reset_tokens", {
7599
7888
  id: pgCore.uuid("id").defaultRandom().primaryKey(),
7600
7889
  userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7601
7890
  onDelete: "cascade"
@@ -7607,14 +7896,14 @@
7607
7896
  usedAt: pgCore.timestamp("used_at"),
7608
7897
  createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
7609
7898
  });
7610
- const appConfig2 = rolesTableCreator("app_config", {
7899
+ const appConfig2 = tableCreator("app_config", {
7611
7900
  key: pgCore.varchar("key", {
7612
7901
  length: 100
7613
7902
  }).primaryKey(),
7614
7903
  value: pgCore.jsonb("value").notNull(),
7615
7904
  updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7616
7905
  });
7617
- const userIdentities2 = rolesTableCreator("user_identities", {
7906
+ const userIdentities2 = tableCreator("user_identities", {
7618
7907
  id: pgCore.uuid("id").defaultRandom().primaryKey(),
7619
7908
  userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7620
7909
  onDelete: "cascade"
@@ -7632,74 +7921,126 @@
7632
7921
  }, (table) => ({
7633
7922
  uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
7634
7923
  }));
7924
+ const mfaFactors2 = tableCreator("mfa_factors", {
7925
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7926
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7927
+ onDelete: "cascade"
7928
+ }),
7929
+ factorType: pgCore.varchar("factor_type", {
7930
+ length: 20
7931
+ }).notNull(),
7932
+ // 'totp'
7933
+ secretEncrypted: pgCore.varchar("secret_encrypted", {
7934
+ length: 500
7935
+ }).notNull(),
7936
+ friendlyName: pgCore.varchar("friendly_name", {
7937
+ length: 255
7938
+ }),
7939
+ verified: pgCore.boolean("verified").default(false).notNull(),
7940
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7941
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7942
+ });
7943
+ const mfaChallenges2 = tableCreator("mfa_challenges", {
7944
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7945
+ factorId: pgCore.uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7946
+ onDelete: "cascade"
7947
+ }),
7948
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7949
+ verifiedAt: pgCore.timestamp("verified_at"),
7950
+ ipAddress: pgCore.varchar("ip_address", {
7951
+ length: 45
7952
+ }),
7953
+ expiresAt: pgCore.timestamp("expires_at").notNull()
7954
+ });
7955
+ const recoveryCodes2 = tableCreator("recovery_codes", {
7956
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7957
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7958
+ onDelete: "cascade"
7959
+ }),
7960
+ codeHash: pgCore.varchar("code_hash", {
7961
+ length: 255
7962
+ }).notNull(),
7963
+ usedAt: pgCore.timestamp("used_at"),
7964
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
7965
+ });
7635
7966
  return {
7636
- rolesSchema,
7637
7967
  usersSchema: usersSchema2,
7638
7968
  users: users2,
7639
- roles: roles2,
7640
- userRoles: userRoles2,
7641
7969
  refreshTokens: refreshTokens2,
7642
7970
  passwordResetTokens: passwordResetTokens2,
7643
7971
  appConfig: appConfig2,
7644
- userIdentities: userIdentities2
7972
+ userIdentities: userIdentities2,
7973
+ mfaFactors: mfaFactors2,
7974
+ mfaChallenges: mfaChallenges2,
7975
+ recoveryCodes: recoveryCodes2
7645
7976
  };
7646
7977
  }
7647
- const defaultAuthSchema = createAuthSchema("rebase", "rebase");
7648
- const rebaseSchema = defaultAuthSchema.rolesSchema;
7978
+ const defaultAuthSchema = createAuthSchema("rebase");
7649
7979
  const usersSchema = defaultAuthSchema.usersSchema;
7650
7980
  const users = defaultAuthSchema.users;
7651
- const roles = defaultAuthSchema.roles;
7652
- const userRoles = defaultAuthSchema.userRoles;
7653
7981
  const refreshTokens = defaultAuthSchema.refreshTokens;
7654
7982
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7655
7983
  const appConfig = defaultAuthSchema.appConfig;
7656
7984
  const userIdentities = defaultAuthSchema.userIdentities;
7985
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7986
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7987
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7657
7988
  const usersRelations = drizzleOrm.relations(users, ({
7658
7989
  many
7659
7990
  }) => ({
7660
- userRoles: many(userRoles),
7661
7991
  refreshTokens: many(refreshTokens),
7662
7992
  passwordResetTokens: many(passwordResetTokens),
7663
- userIdentities: many(userIdentities)
7993
+ userIdentities: many(userIdentities),
7994
+ mfaFactors: many(mfaFactors),
7995
+ recoveryCodes: many(recoveryCodes)
7664
7996
  }));
7665
- const rolesRelations = drizzleOrm.relations(roles, ({
7666
- many
7997
+ const refreshTokensRelations = drizzleOrm.relations(refreshTokens, ({
7998
+ one
7667
7999
  }) => ({
7668
- userRoles: many(userRoles)
8000
+ user: one(users, {
8001
+ fields: [refreshTokens.userId],
8002
+ references: [users.id]
8003
+ })
7669
8004
  }));
7670
- const userRolesRelations = drizzleOrm.relations(userRoles, ({
8005
+ const passwordResetTokensRelations = drizzleOrm.relations(passwordResetTokens, ({
7671
8006
  one
7672
8007
  }) => ({
7673
8008
  user: one(users, {
7674
- fields: [userRoles.userId],
8009
+ fields: [passwordResetTokens.userId],
7675
8010
  references: [users.id]
7676
- }),
7677
- role: one(roles, {
7678
- fields: [userRoles.roleId],
7679
- references: [roles.id]
7680
8011
  })
7681
8012
  }));
7682
- const refreshTokensRelations = drizzleOrm.relations(refreshTokens, ({
8013
+ const userIdentitiesRelations = drizzleOrm.relations(userIdentities, ({
7683
8014
  one
7684
8015
  }) => ({
7685
8016
  user: one(users, {
7686
- fields: [refreshTokens.userId],
8017
+ fields: [userIdentities.userId],
7687
8018
  references: [users.id]
7688
8019
  })
7689
8020
  }));
7690
- const passwordResetTokensRelations = drizzleOrm.relations(passwordResetTokens, ({
7691
- one
8021
+ const mfaFactorsRelations = drizzleOrm.relations(mfaFactors, ({
8022
+ one,
8023
+ many
7692
8024
  }) => ({
7693
8025
  user: one(users, {
7694
- fields: [passwordResetTokens.userId],
8026
+ fields: [mfaFactors.userId],
7695
8027
  references: [users.id]
8028
+ }),
8029
+ challenges: many(mfaChallenges)
8030
+ }));
8031
+ const mfaChallengesRelations = drizzleOrm.relations(mfaChallenges, ({
8032
+ one
8033
+ }) => ({
8034
+ factor: one(mfaFactors, {
8035
+ fields: [mfaChallenges.factorId],
8036
+ references: [mfaFactors.id]
7696
8037
  })
7697
8038
  }));
7698
- const userIdentitiesRelations = drizzleOrm.relations(userIdentities, ({
8039
+ const recoveryCodesRelations = drizzleOrm.relations(recoveryCodes, ({
7699
8040
  one
7700
8041
  }) => ({
7701
8042
  user: one(users, {
7702
- fields: [userIdentities.userId],
8043
+ fields: [recoveryCodes.userId],
7703
8044
  references: [users.id]
7704
8045
  })
7705
8046
  }));
@@ -7759,6 +8100,8 @@
7759
8100
  columnDefinition = `${enumName}("${colName}")`;
7760
8101
  } else if ("isId" in stringProp && stringProp.isId === "uuid") {
7761
8102
  columnDefinition = `uuid("${colName}")`;
8103
+ } else if (stringProp.columnType === "uuid") {
8104
+ columnDefinition = `uuid("${colName}")`;
7762
8105
  } else if (stringProp.columnType === "text") {
7763
8106
  columnDefinition = `text("${colName}")`;
7764
8107
  } else if (stringProp.columnType === "char") {
@@ -7826,11 +8169,38 @@
7826
8169
  }
7827
8170
  break;
7828
8171
  }
7829
- case "map":
8172
+ case "map": {
8173
+ const mapProp = prop;
8174
+ if (mapProp.columnType === "json") {
8175
+ columnDefinition = `json("${colName}")`;
8176
+ } else {
8177
+ columnDefinition = `jsonb("${colName}")`;
8178
+ }
8179
+ break;
8180
+ }
7830
8181
  case "array": {
7831
- const arrayOrMapProp = prop;
7832
- if (arrayOrMapProp.columnType === "json") {
8182
+ const arrayProp = prop;
8183
+ let colType = arrayProp.columnType;
8184
+ if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
8185
+ const ofProp = arrayProp.of;
8186
+ if (ofProp.type === "string") {
8187
+ colType = "text[]";
8188
+ } else if (ofProp.type === "number") {
8189
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
8190
+ } else if (ofProp.type === "boolean") {
8191
+ colType = "boolean[]";
8192
+ }
8193
+ }
8194
+ if (colType === "json") {
7833
8195
  columnDefinition = `json("${colName}")`;
8196
+ } else if (colType === "text[]") {
8197
+ columnDefinition = `text("${colName}").array()`;
8198
+ } else if (colType === "integer[]") {
8199
+ columnDefinition = `integer("${colName}").array()`;
8200
+ } else if (colType === "boolean[]") {
8201
+ columnDefinition = `boolean("${colName}").array()`;
8202
+ } else if (colType === "numeric[]") {
8203
+ columnDefinition = `numeric("${colName}").array()`;
7834
8204
  } else {
7835
8205
  columnDefinition = `jsonb("${colName}")`;
7836
8206
  }
@@ -7914,8 +8284,8 @@
7914
8284
  const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
7915
8285
  return `sql\`${resolved}\``;
7916
8286
  };
7917
- const wrapWithRoleCheck = (clause, roles2) => {
7918
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8287
+ const wrapWithRoleCheck = (clause, roles) => {
8288
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7919
8289
  const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
7920
8290
  return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
7921
8291
  };
@@ -7968,22 +8338,22 @@
7968
8338
  };
7969
8339
  const generateSinglePolicyCode = (collection, rule, operation, policyName) => {
7970
8340
  const mode = rule.mode ?? "permissive";
7971
- const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
8341
+ const roles = rule.roles ? [...rule.roles].sort() : void 0;
7972
8342
  const needsUsing = operation !== "insert";
7973
8343
  const needsWithCheck = operation !== "select" && operation !== "delete";
7974
8344
  let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
7975
8345
  let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
7976
- if (roles2 && roles2.length > 0) {
8346
+ if (roles && roles.length > 0) {
7977
8347
  if (usingClause) {
7978
- usingClause = wrapWithRoleCheck(usingClause, roles2);
8348
+ usingClause = wrapWithRoleCheck(usingClause, roles);
7979
8349
  } else if (needsUsing) {
7980
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8350
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7981
8351
  usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
7982
8352
  }
7983
8353
  if (withCheckClause) {
7984
- withCheckClause = wrapWithRoleCheck(withCheckClause, roles2);
8354
+ withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
7985
8355
  } else if (needsWithCheck) {
7986
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8356
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7987
8357
  withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
7988
8358
  }
7989
8359
  }
@@ -8306,90 +8676,6 @@ ${tableRelations.join(",\n")}
8306
8676
  schemaContent += tablesExport + enumsExport + relationsExport;
8307
8677
  return schemaContent;
8308
8678
  };
8309
- const defaultUsersCollection = {
8310
- name: "Users",
8311
- singularName: "User",
8312
- slug: "users",
8313
- table: "users",
8314
- schema: "rebase",
8315
- icon: "Users",
8316
- group: "Settings",
8317
- properties: {
8318
- id: {
8319
- name: "ID",
8320
- type: "string",
8321
- isId: "uuid"
8322
- },
8323
- email: {
8324
- name: "Email",
8325
- type: "string",
8326
- validation: {
8327
- required: true,
8328
- unique: true
8329
- }
8330
- },
8331
- password_hash: {
8332
- name: "Password Hash",
8333
- type: "string",
8334
- ui: {
8335
- hideFromCollection: true
8336
- }
8337
- },
8338
- display_name: {
8339
- name: "Display Name",
8340
- type: "string"
8341
- },
8342
- photo_url: {
8343
- name: "Photo URL",
8344
- type: "string"
8345
- },
8346
- email_verified: {
8347
- name: "Email Verified",
8348
- type: "boolean",
8349
- defaultValue: false
8350
- },
8351
- email_verification_token: {
8352
- name: "Email Verification Token",
8353
- type: "string",
8354
- ui: {
8355
- hideFromCollection: true
8356
- }
8357
- },
8358
- email_verification_sent_at: {
8359
- name: "Email Verification Sent At",
8360
- type: "date",
8361
- ui: {
8362
- hideFromCollection: true
8363
- }
8364
- },
8365
- metadata: {
8366
- name: "Metadata",
8367
- type: "map",
8368
- defaultValue: {},
8369
- ui: {
8370
- hideFromCollection: true
8371
- }
8372
- },
8373
- created_at: {
8374
- name: "Created At",
8375
- type: "date",
8376
- autoValue: "on_create",
8377
- ui: {
8378
- readOnly: true,
8379
- hideFromCollection: true
8380
- }
8381
- },
8382
- updated_at: {
8383
- name: "Updated At",
8384
- type: "date",
8385
- autoValue: "on_update",
8386
- ui: {
8387
- readOnly: true,
8388
- hideFromCollection: true
8389
- }
8390
- }
8391
- }
8392
- };
8393
8679
  const formatTerminalText = (text, options = {}) => {
8394
8680
  let codes = "";
8395
8681
  if (options.bold) codes += "\x1B[1m";
@@ -8455,10 +8741,7 @@ ${tableRelations.join(",\n")}
8455
8741
  if (!collections || !Array.isArray(collections)) {
8456
8742
  collections = [];
8457
8743
  }
8458
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8459
- if (!hasUsersCollection) {
8460
- collections.push(defaultUsersCollection);
8461
- }
8744
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8462
8745
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8463
8746
  const schemaContent = await generateSchema(collections);
8464
8747
  if (outputPath) {
@@ -8519,6 +8802,13 @@ ${tableRelations.join(",\n")}
8519
8802
  this.entityService = new EntityService(db, registry);
8520
8803
  }
8521
8804
  clients = /* @__PURE__ */ new Map();
8805
+ // Broadcast channels: channel name → set of client IDs
8806
+ channels = /* @__PURE__ */ new Map();
8807
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8808
+ presence = /* @__PURE__ */ new Map();
8809
+ presenceInterval;
8810
+ static PRESENCE_TIMEOUT_MS = 3e4;
8811
+ // 30s
8522
8812
  entityService;
8523
8813
  // Enhanced subscriptions storage with full request parameters
8524
8814
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8645,8 +8935,19 @@ ${tableRelations.join(",\n")}
8645
8935
  }
8646
8936
  }
8647
8937
  }
8938
+ for (const [channel, members] of this.channels.entries()) {
8939
+ if (members.has(clientId)) {
8940
+ members.delete(clientId);
8941
+ this.removePresence(clientId, channel);
8942
+ if (members.size === 0) this.channels.delete(channel);
8943
+ }
8944
+ }
8945
+ for (const [channel] of this.presence) {
8946
+ this.removePresence(clientId, channel);
8947
+ }
8648
8948
  }
8649
8949
  async handleMessage(clientId, message, authContext) {
8950
+ const payload = message.payload;
8650
8951
  switch (message.type) {
8651
8952
  case "subscribe_collection":
8652
8953
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8657,6 +8958,25 @@ ${tableRelations.join(",\n")}
8657
8958
  case "unsubscribe":
8658
8959
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8659
8960
  break;
8961
+ case "join_channel":
8962
+ this.joinChannel(clientId, payload?.channel);
8963
+ break;
8964
+ case "leave_channel":
8965
+ this.leaveChannel(clientId, payload?.channel);
8966
+ break;
8967
+ case "broadcast":
8968
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8969
+ break;
8970
+ case "presence_track":
8971
+ this.joinChannel(clientId, payload?.channel);
8972
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8973
+ break;
8974
+ case "presence_untrack":
8975
+ this.removePresence(clientId, payload?.channel);
8976
+ break;
8977
+ case "presence_state":
8978
+ this.sendPresenceState(clientId, payload?.channel);
8979
+ break;
8660
8980
  default:
8661
8981
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8662
8982
  }
@@ -9114,6 +9434,132 @@ ${tableRelations.join(",\n")}
9114
9434
  return parentPaths;
9115
9435
  }
9116
9436
  // =============================================================================
9437
+ // Broadcast Channels
9438
+ // =============================================================================
9439
+ /** Join a broadcast channel */
9440
+ joinChannel(clientId, channel) {
9441
+ if (!this.channels.has(channel)) {
9442
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9443
+ }
9444
+ this.channels.get(channel).add(clientId);
9445
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9446
+ }
9447
+ /** Leave a broadcast channel */
9448
+ leaveChannel(clientId, channel) {
9449
+ const members = this.channels.get(channel);
9450
+ if (members) {
9451
+ members.delete(clientId);
9452
+ if (members.size === 0) this.channels.delete(channel);
9453
+ }
9454
+ this.removePresence(clientId, channel);
9455
+ }
9456
+ /** Broadcast a message to all clients in a channel except sender */
9457
+ broadcastToChannel(clientId, channel, event, payload) {
9458
+ const members = this.channels.get(channel);
9459
+ if (!members) return;
9460
+ const message = JSON.stringify({
9461
+ type: "broadcast",
9462
+ channel,
9463
+ event,
9464
+ payload
9465
+ });
9466
+ for (const memberId of members) {
9467
+ if (memberId === clientId) continue;
9468
+ const ws$1 = this.clients.get(memberId);
9469
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9470
+ ws$1.send(message);
9471
+ }
9472
+ }
9473
+ }
9474
+ // =============================================================================
9475
+ // Presence
9476
+ // =============================================================================
9477
+ /** Track presence in a channel */
9478
+ trackPresence(clientId, channel, state) {
9479
+ if (!this.presence.has(channel)) {
9480
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9481
+ }
9482
+ const channelPresence = this.presence.get(channel);
9483
+ channelPresence.set(clientId, {
9484
+ state,
9485
+ lastSeen: Date.now()
9486
+ });
9487
+ this.broadcastPresenceDiff(channel, {
9488
+ [clientId]: state
9489
+ }, {});
9490
+ this.ensurePresenceCleanup();
9491
+ }
9492
+ /** Remove presence from a channel */
9493
+ removePresence(clientId, channel) {
9494
+ const channelPresence = this.presence.get(channel);
9495
+ if (!channelPresence) return;
9496
+ const entry = channelPresence.get(clientId);
9497
+ if (entry) {
9498
+ channelPresence.delete(clientId);
9499
+ this.broadcastPresenceDiff(channel, {}, {
9500
+ [clientId]: entry.state
9501
+ });
9502
+ }
9503
+ if (channelPresence.size === 0) {
9504
+ this.presence.delete(channel);
9505
+ }
9506
+ }
9507
+ /** Send full presence state to a specific client */
9508
+ sendPresenceState(clientId, channel) {
9509
+ const channelPresence = this.presence.get(channel);
9510
+ const presences = {};
9511
+ if (channelPresence) {
9512
+ for (const [id, {
9513
+ state
9514
+ }] of channelPresence) {
9515
+ presences[id] = state;
9516
+ }
9517
+ }
9518
+ const ws$1 = this.clients.get(clientId);
9519
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9520
+ ws$1.send(JSON.stringify({
9521
+ type: "presence_state",
9522
+ channel,
9523
+ presences
9524
+ }));
9525
+ }
9526
+ }
9527
+ /** Broadcast presence diff (joins/leaves) to channel */
9528
+ broadcastPresenceDiff(channel, joins, leaves) {
9529
+ const members = this.channels.get(channel);
9530
+ if (!members) return;
9531
+ const message = JSON.stringify({
9532
+ type: "presence_diff",
9533
+ channel,
9534
+ joins,
9535
+ leaves
9536
+ });
9537
+ for (const memberId of members) {
9538
+ const ws$1 = this.clients.get(memberId);
9539
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9540
+ ws$1.send(message);
9541
+ }
9542
+ }
9543
+ }
9544
+ /** Periodic cleanup for stale presences */
9545
+ ensurePresenceCleanup() {
9546
+ if (this.presenceInterval) return;
9547
+ this.presenceInterval = setInterval(() => {
9548
+ const now = Date.now();
9549
+ for (const [channel, channelPresence] of this.presence) {
9550
+ for (const [clientId, entry] of channelPresence) {
9551
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9552
+ this.removePresence(clientId, channel);
9553
+ }
9554
+ }
9555
+ }
9556
+ if (this.presence.size === 0 && this.presenceInterval) {
9557
+ clearInterval(this.presenceInterval);
9558
+ this.presenceInterval = void 0;
9559
+ }
9560
+ }, 1e4);
9561
+ }
9562
+ // =============================================================================
9117
9563
  // Lifecycle / Cleanup
9118
9564
  // =============================================================================
9119
9565
  /**
@@ -9134,6 +9580,12 @@ ${tableRelations.join(",\n")}
9134
9580
  }
9135
9581
  this._subscriptions.clear();
9136
9582
  this.subscriptionCallbacks.clear();
9583
+ this.channels.clear();
9584
+ this.presence.clear();
9585
+ if (this.presenceInterval) {
9586
+ clearInterval(this.presenceInterval);
9587
+ this.presenceInterval = void 0;
9588
+ }
9137
9589
  await this.stopListening();
9138
9590
  this.clients.clear();
9139
9591
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9635,15 +10087,15 @@ ${tableRelations.join(",\n")}
9635
10087
  wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
9636
10088
  const delegate = await getScopedDelegate();
9637
10089
  const admin = delegate.admin;
9638
- let roles2 = [];
10090
+ let roles = [];
9639
10091
  if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
9640
- roles2 = await admin.fetchAvailableRoles();
10092
+ roles = await admin.fetchAvailableRoles();
9641
10093
  }
9642
- wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
10094
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);
9643
10095
  const response = {
9644
10096
  type: "FETCH_ROLES_SUCCESS",
9645
10097
  payload: {
9646
- roles: roles2
10098
+ roles
9647
10099
  },
9648
10100
  requestId
9649
10101
  };
@@ -9780,8 +10232,14 @@ ${tableRelations.join(",\n")}
9780
10232
  break;
9781
10233
  case "subscribe_collection":
9782
10234
  case "subscribe_entity":
9783
- case "unsubscribe": {
9784
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10235
+ case "unsubscribe":
10236
+ case "join_channel":
10237
+ case "leave_channel":
10238
+ case "broadcast":
10239
+ case "presence_track":
10240
+ case "presence_untrack":
10241
+ case "presence_state": {
10242
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9785
10243
  const session = clientSessions.get(clientId);
9786
10244
  const authContext = session?.user ? {
9787
10245
  userId: session.user.userId,
@@ -9891,50 +10349,8 @@ ${tableRelations.join(",\n")}
9891
10349
  return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
9892
10350
  }
9893
10351
  }
9894
- const DEFAULT_ROLES = [{
9895
- id: "admin",
9896
- name: "Admin",
9897
- is_admin: true,
9898
- default_permissions: {
9899
- read: true,
9900
- create: true,
9901
- edit: true,
9902
- delete: true
9903
- },
9904
- config: {
9905
- createCollections: true,
9906
- editCollections: "all",
9907
- deleteCollections: "all"
9908
- }
9909
- }, {
9910
- id: "editor",
9911
- name: "Editor",
9912
- is_admin: false,
9913
- default_permissions: {
9914
- read: true,
9915
- create: true,
9916
- edit: true,
9917
- delete: true
9918
- },
9919
- config: {
9920
- createCollections: true,
9921
- editCollections: "own",
9922
- deleteCollections: "own"
9923
- }
9924
- }, {
9925
- id: "viewer",
9926
- name: "Viewer",
9927
- is_admin: false,
9928
- default_permissions: {
9929
- read: true,
9930
- create: false,
9931
- edit: false,
9932
- delete: false
9933
- },
9934
- config: null
9935
- }];
9936
10352
  async function ensureAuthTablesExist(db, registry) {
9937
- console.log("🔍 Checking auth tables...");
10353
+ serverCore.logger.info("🔍 Checking auth tables...");
9938
10354
  try {
9939
10355
  let usersTableName = '"users"';
9940
10356
  let userIdType = "TEXT";
@@ -9961,26 +10377,15 @@ ${tableRelations.join(",\n")}
9961
10377
  }
9962
10378
  }
9963
10379
  }
9964
- let rolesSchema = "rebase";
9965
- if (registry) {
9966
- const rolesTable = registry.getTable("roles");
9967
- if (rolesTable) {
9968
- rolesSchema = pgCore.getTableConfig(rolesTable).schema || "public";
9969
- }
9970
- }
9971
10380
  if (usersSchema2 !== "public") {
9972
10381
  await db.execute(drizzleOrm.sql`CREATE SCHEMA IF NOT EXISTS ${drizzleOrm.sql.raw(usersSchema2)}`);
9973
10382
  }
9974
- if (rolesSchema !== "public" && rolesSchema !== usersSchema2) {
9975
- await db.execute(drizzleOrm.sql`CREATE SCHEMA IF NOT EXISTS ${drizzleOrm.sql.raw(rolesSchema)}`);
9976
- }
9977
10383
  await db.execute(drizzleOrm.sql`CREATE SCHEMA IF NOT EXISTS rebase`);
9978
- const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
9979
- const rolesTableName = `"${rolesSchema}"."roles"`;
9980
- const userRolesTableName = `"${rolesSchema}"."user_roles"`;
9981
- const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
9982
- const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
9983
- const appConfigTableName = `"${rolesSchema}"."app_config"`;
10384
+ const authSchema = usersSchema2 === "public" ? "rebase" : usersSchema2;
10385
+ const userIdentitiesTable = `"${authSchema}"."user_identities"`;
10386
+ const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
10387
+ const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
10388
+ const appConfigTableName = `"${authSchema}"."app_config"`;
9984
10389
  await db.execute(drizzleOrm.sql`
9985
10390
  CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(userIdentitiesTable)} (
9986
10391
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -9997,28 +10402,6 @@ ${tableRelations.join(",\n")}
9997
10402
  CREATE INDEX IF NOT EXISTS idx_user_identities_user
9998
10403
  ON ${drizzleOrm.sql.raw(userIdentitiesTable)}(user_id)
9999
10404
  `);
10000
- await db.execute(drizzleOrm.sql`
10001
- CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(rolesTableName)} (
10002
- id TEXT PRIMARY KEY,
10003
- name TEXT NOT NULL,
10004
- is_admin BOOLEAN DEFAULT FALSE,
10005
- default_permissions JSONB,
10006
- collection_permissions JSONB,
10007
- config JSONB,
10008
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10009
- )
10010
- `);
10011
- await db.execute(drizzleOrm.sql`
10012
- CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(userRolesTableName)} (
10013
- user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10014
- role_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
10015
- PRIMARY KEY (user_id, role_id)
10016
- )
10017
- `);
10018
- await db.execute(drizzleOrm.sql`
10019
- CREATE INDEX IF NOT EXISTS idx_user_roles_user
10020
- ON ${drizzleOrm.sql.raw(userRolesTableName)}(user_id)
10021
- `);
10022
10405
  await db.execute(drizzleOrm.sql`
10023
10406
  CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(refreshTokensTableName)} (
10024
10407
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -10086,35 +10469,93 @@ ${tableRelations.join(",\n")}
10086
10469
  $$ LANGUAGE sql STABLE
10087
10470
  `);
10088
10471
  });
10089
- await seedDefaultRoles(db, rolesTableName);
10090
- console.log("✅ Auth tables ready");
10091
- } catch (error) {
10092
- console.error("❌ Failed to create auth tables:", error);
10093
- console.warn("⚠️ Continuing without creating auth tables.");
10094
- }
10095
- }
10096
- async function seedDefaultRoles(db, rolesTableName) {
10097
- const result = await db.execute(drizzleOrm.sql`SELECT COUNT(*) as count FROM ${drizzleOrm.sql.raw(rolesTableName)}`);
10098
- const count = parseInt(result.rows[0]?.count || "0", 10);
10099
- if (count > 0) {
10100
- console.log(`📋 Found ${count} existing roles`);
10101
- return;
10102
- }
10103
- console.log("🌱 Seeding default roles...");
10104
- for (const role of DEFAULT_ROLES) {
10105
10472
  await db.execute(drizzleOrm.sql`
10106
- INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10107
- VALUES (
10108
- ${role.id},
10109
- ${role.name},
10110
- ${role.is_admin},
10111
- ${JSON.stringify(role.default_permissions)}::jsonb,
10112
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10473
+ ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
10474
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10475
+ `);
10476
+ await db.execute(drizzleOrm.sql`
10477
+ ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
10478
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
10479
+ `);
10480
+ try {
10481
+ const legacyCheck = await db.execute(drizzleOrm.sql`
10482
+ SELECT EXISTS (
10483
+ SELECT 1 FROM information_schema.tables
10484
+ WHERE table_schema = 'rebase' AND table_name = 'user_roles'
10485
+ ) AS has_user_roles
10486
+ `);
10487
+ const hasLegacyTables = legacyCheck.rows[0].has_user_roles;
10488
+ if (hasLegacyTables) {
10489
+ serverCore.logger.info("🔄 Migrating roles from legacy user_roles table...");
10490
+ await db.execute(drizzleOrm.sql`
10491
+ UPDATE ${drizzleOrm.sql.raw(usersTableName)} u
10492
+ SET roles = COALESCE((
10493
+ SELECT array_agg(ur.role_id)
10494
+ FROM "rebase"."user_roles" ur
10495
+ WHERE ur.user_id = u.id
10496
+ ), '{}')
10497
+ WHERE u.roles = '{}' OR u.roles IS NULL
10498
+ `);
10499
+ await db.execute(drizzleOrm.sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
10500
+ await db.execute(drizzleOrm.sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
10501
+ serverCore.logger.info("✅ Legacy roles tables migrated and dropped");
10502
+ }
10503
+ } catch (migrationError) {
10504
+ serverCore.logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
10505
+ }
10506
+ const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
10507
+ const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
10508
+ const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
10509
+ await db.execute(drizzleOrm.sql`
10510
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaFactorsTableName)} (
10511
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10512
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10513
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10514
+ secret_encrypted TEXT NOT NULL,
10515
+ friendly_name TEXT,
10516
+ verified BOOLEAN DEFAULT FALSE,
10517
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10518
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10519
+ )
10520
+ `);
10521
+ await db.execute(drizzleOrm.sql`
10522
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10523
+ ON ${drizzleOrm.sql.raw(mfaFactorsTableName)}(user_id)
10524
+ `);
10525
+ await db.execute(drizzleOrm.sql`
10526
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaChallengesTableName)} (
10527
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10528
+ factor_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10529
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10530
+ verified_at TIMESTAMP WITH TIME ZONE,
10531
+ ip_address TEXT,
10532
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10113
10533
  )
10114
- ON CONFLICT (id) DO NOTHING
10115
10534
  `);
10535
+ await db.execute(drizzleOrm.sql`
10536
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10537
+ ON ${drizzleOrm.sql.raw(mfaChallengesTableName)}(factor_id)
10538
+ `);
10539
+ await db.execute(drizzleOrm.sql`
10540
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(recoveryCodesTableName)} (
10541
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10542
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10543
+ code_hash TEXT NOT NULL,
10544
+ used_at TIMESTAMP WITH TIME ZONE,
10545
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10546
+ )
10547
+ `);
10548
+ await db.execute(drizzleOrm.sql`
10549
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10550
+ ON ${drizzleOrm.sql.raw(recoveryCodesTableName)}(user_id)
10551
+ `);
10552
+ serverCore.logger.info("✅ Auth tables ready");
10553
+ } catch (error) {
10554
+ serverCore.logger.error("❌ Failed to create auth tables", {
10555
+ error
10556
+ });
10557
+ serverCore.logger.warn("⚠️ Continuing without creating auth tables.");
10116
10558
  }
10117
- console.log("✅ Default roles created: admin, editor, viewer");
10118
10559
  }
10119
10560
  function getColumnKey(table, ...keys2) {
10120
10561
  if (!table) return void 0;
@@ -10135,24 +10576,18 @@ ${tableRelations.join(",\n")}
10135
10576
  class UserService {
10136
10577
  constructor(db, tableOrTables) {
10137
10578
  this.db = db;
10138
- if (tableOrTables && (tableOrTables.users || tableOrTables.roles)) {
10579
+ if (tableOrTables && tableOrTables.users) {
10139
10580
  const tables = tableOrTables;
10140
10581
  this.usersTable = tables.users || users;
10141
10582
  this.userIdentitiesTable = tables.userIdentities || userIdentities;
10142
- this.userRolesTable = tables.userRoles || userRoles;
10143
- this.rolesTable = tables.roles || roles;
10144
10583
  } else {
10145
10584
  const table = tableOrTables;
10146
10585
  this.usersTable = table || users;
10147
10586
  this.userIdentitiesTable = userIdentities;
10148
- this.userRolesTable = userRoles;
10149
- this.rolesTable = roles;
10150
10587
  }
10151
10588
  }
10152
10589
  usersTable;
10153
10590
  userIdentitiesTable;
10154
- userRolesTable;
10155
- rolesTable;
10156
10591
  getQualifiedUsersTableName() {
10157
10592
  const name = drizzleOrm.getTableName(this.usersTable);
10158
10593
  const schema = pgCore.getTableConfig(this.usersTable).schema || "public";
@@ -10168,12 +10603,13 @@ ${tableRelations.join(",\n")}
10168
10603
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10169
10604
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10170
10605
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10606
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10171
10607
  const createdAt = row.created_at ?? row.createdAt;
10172
10608
  const updatedAt = row.updated_at ?? row.updatedAt;
10173
10609
  const metadata = {
10174
10610
  ...row.metadata || {}
10175
10611
  };
10176
- 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"]);
10612
+ 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", "roles", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
10177
10613
  for (const [key, val] of Object.entries(row)) {
10178
10614
  if (!knownKeys.has(key)) {
10179
10615
  const camelKey = camelCase(key);
@@ -10189,6 +10625,7 @@ ${tableRelations.join(",\n")}
10189
10625
  emailVerified,
10190
10626
  emailVerificationToken,
10191
10627
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10628
+ isAnonymous,
10192
10629
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10193
10630
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10194
10631
  metadata
@@ -10205,6 +10642,7 @@ ${tableRelations.join(",\n")}
10205
10642
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10206
10643
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10207
10644
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10645
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10208
10646
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10209
10647
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10210
10648
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10216,6 +10654,7 @@ ${tableRelations.join(",\n")}
10216
10654
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10217
10655
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10218
10656
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10657
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10219
10658
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10220
10659
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10221
10660
  const metadata = {
@@ -10224,7 +10663,7 @@ ${tableRelations.join(",\n")}
10224
10663
  const remainingMetadata = {};
10225
10664
  for (const [key, val] of Object.entries(metadata)) {
10226
10665
  const tableColKey = getColumnKey(this.usersTable, key);
10227
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10666
+ 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) {
10228
10667
  payload[tableColKey] = val;
10229
10668
  } else {
10230
10669
  remainingMetadata[key] = val;
@@ -10321,19 +10760,18 @@ ${tableRelations.join(",\n")}
10321
10760
  const displayNameCol = getColumn(this.usersTable, "displayName", "display_name");
10322
10761
  const displayNameColumn = displayNameCol ? displayNameCol.name : "display_name";
10323
10762
  const idCol = getColumn(this.usersTable, "id");
10324
- const idColumn = idCol ? idCol.name : "id";
10763
+ idCol ? idCol.name : "id";
10325
10764
  const usersTableName = this.getQualifiedUsersTableName();
10326
- const rolesSchema = pgCore.getTableConfig(this.userRolesTable).schema || "public";
10327
10765
  const conditions = [];
10328
10766
  if (roleId) {
10329
- conditions.push(drizzleOrm.sql`EXISTS (SELECT 1 FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
10767
+ conditions.push(drizzleOrm.sql`${roleId} = ANY(${drizzleOrm.sql.raw(usersTableName)}.roles)`);
10330
10768
  }
10331
10769
  if (search) {
10332
10770
  const pattern = `%${search}%`;
10333
10771
  conditions.push(drizzleOrm.sql`(${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(emailColumn)} ILIKE ${pattern} OR ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(displayNameColumn)} ILIKE ${pattern})`);
10334
10772
  }
10335
10773
  const whereClause = conditions.length > 0 ? drizzleOrm.sql`WHERE ${drizzleOrm.sql.join(conditions, drizzleOrm.sql` AND `)}` : drizzleOrm.sql``;
10336
- const orderByClause = roleId ? drizzleOrm.sql`ORDER BY ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(orderColumn)} ${direction}` : drizzleOrm.sql`ORDER BY (SELECT count(*) FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(idColumn)}) DESC, ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(orderColumn)} ${direction}`;
10774
+ const orderByClause = roleId ? drizzleOrm.sql`ORDER BY ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(orderColumn)} ${direction}` : drizzleOrm.sql`ORDER BY array_length(${drizzleOrm.sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${drizzleOrm.sql.raw(usersTableName)}.${drizzleOrm.sql.raw(orderColumn)} ${direction}`;
10337
10775
  const countResult = await this.db.execute(drizzleOrm.sql`
10338
10776
  SELECT count(*)::int as total FROM ${drizzleOrm.sql.raw(usersTableName)}
10339
10777
  ${whereClause}
@@ -10407,55 +10845,57 @@ ${tableRelations.join(",\n")}
10407
10845
  return row ? this.mapRowToUser(row) : null;
10408
10846
  }
10409
10847
  /**
10410
- * Get roles for a user from database
10848
+ * Get roles for a user from database (inline TEXT[] column)
10411
10849
  */
10412
10850
  async getUserRoles(userId) {
10413
- const rolesSchema = pgCore.getTableConfig(this.rolesTable).schema || "public";
10851
+ const usersTableName = this.getQualifiedUsersTableName();
10414
10852
  const result = await this.db.execute(drizzleOrm.sql`
10415
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10416
- FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."roles"`)} r
10417
- INNER JOIN ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10418
- WHERE ur.user_id = ${userId}
10853
+ SELECT roles FROM ${drizzleOrm.sql.raw(usersTableName)} WHERE id = ${userId}
10419
10854
  `);
10420
- return result.rows.map((row) => ({
10421
- id: row.id,
10422
- name: row.name,
10423
- isAdmin: row.is_admin,
10424
- defaultPermissions: row.default_permissions,
10425
- collectionPermissions: row.collection_permissions,
10426
- config: row.config
10855
+ if (result.rows.length === 0) return [];
10856
+ const row = result.rows[0];
10857
+ const roleIds = row.roles ?? [];
10858
+ return roleIds.map((id) => ({
10859
+ id,
10860
+ name: id,
10861
+ isAdmin: id === "admin",
10862
+ defaultPermissions: null,
10863
+ collectionPermissions: null
10427
10864
  }));
10428
10865
  }
10429
10866
  /**
10430
10867
  * Get role IDs for a user
10431
10868
  */
10432
10869
  async getUserRoleIds(userId) {
10433
- const roles2 = await this.getUserRoles(userId);
10434
- return roles2.map((r) => r.id);
10870
+ const usersTableName = this.getQualifiedUsersTableName();
10871
+ const result = await this.db.execute(drizzleOrm.sql`
10872
+ SELECT roles FROM ${drizzleOrm.sql.raw(usersTableName)} WHERE id = ${userId}
10873
+ `);
10874
+ if (result.rows.length === 0) return [];
10875
+ const row = result.rows[0];
10876
+ return row.roles ?? [];
10435
10877
  }
10436
10878
  /**
10437
- * Set roles for a user
10879
+ * Set roles for a user (replaces existing roles)
10438
10880
  */
10439
10881
  async setUserRoles(userId, roleIds) {
10440
- const rolesSchema = pgCore.getTableConfig(this.userRolesTable).schema || "public";
10441
- await this.db.execute(drizzleOrm.sql`DELETE FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
10442
- for (const roleId of roleIds) {
10443
- await this.db.execute(drizzleOrm.sql`
10444
- INSERT INTO ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
10445
- VALUES (${userId}, ${roleId})
10446
- ON CONFLICT DO NOTHING
10447
- `);
10448
- }
10882
+ const usersTableName = this.getQualifiedUsersTableName();
10883
+ const rolesArray = `{${roleIds.join(",")}}`;
10884
+ await this.db.execute(drizzleOrm.sql`
10885
+ UPDATE ${drizzleOrm.sql.raw(usersTableName)}
10886
+ SET roles = ${rolesArray}::text[], updated_at = NOW()
10887
+ WHERE id = ${userId}
10888
+ `);
10449
10889
  }
10450
10890
  /**
10451
- * Assign a specific role to new user
10891
+ * Assign a specific role to new user (appends if not present)
10452
10892
  */
10453
10893
  async assignDefaultRole(userId, roleId) {
10454
- const rolesSchema = pgCore.getTableConfig(this.userRolesTable).schema || "public";
10894
+ const usersTableName = this.getQualifiedUsersTableName();
10455
10895
  await this.db.execute(drizzleOrm.sql`
10456
- INSERT INTO ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
10457
- VALUES (${userId}, ${roleId})
10458
- ON CONFLICT DO NOTHING
10896
+ UPDATE ${drizzleOrm.sql.raw(usersTableName)}
10897
+ SET roles = array_append(roles, ${roleId}), updated_at = NOW()
10898
+ WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
10459
10899
  `);
10460
10900
  }
10461
10901
  /**
@@ -10464,107 +10904,13 @@ ${tableRelations.join(",\n")}
10464
10904
  async getUserWithRoles(userId) {
10465
10905
  const user = await this.getUserById(userId);
10466
10906
  if (!user) return null;
10467
- const roles2 = await this.getUserRoles(userId);
10907
+ const roles = await this.getUserRoles(userId);
10468
10908
  return {
10469
10909
  user,
10470
- roles: roles2
10910
+ roles
10471
10911
  };
10472
10912
  }
10473
10913
  }
10474
- class RoleService {
10475
- constructor(db, tableOrTables) {
10476
- this.db = db;
10477
- if (tableOrTables && (tableOrTables.roles || tableOrTables.users)) {
10478
- this.rolesTable = tableOrTables.roles || roles;
10479
- } else {
10480
- this.rolesTable = tableOrTables || roles;
10481
- }
10482
- }
10483
- rolesTable;
10484
- getQualifiedRolesTableName() {
10485
- const name = drizzleOrm.getTableName(this.rolesTable);
10486
- const schema = pgCore.getTableConfig(this.rolesTable).schema || "public";
10487
- return `"${schema}"."${name}"`;
10488
- }
10489
- async getRoleById(id) {
10490
- const tableName = this.getQualifiedRolesTableName();
10491
- const result = await this.db.execute(drizzleOrm.sql`
10492
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10493
- FROM ${drizzleOrm.sql.raw(tableName)}
10494
- WHERE id = ${id}
10495
- `);
10496
- if (result.rows.length === 0) return null;
10497
- const row = result.rows[0];
10498
- return {
10499
- id: row.id,
10500
- name: row.name,
10501
- isAdmin: row.is_admin,
10502
- defaultPermissions: row.default_permissions,
10503
- collectionPermissions: row.collection_permissions,
10504
- config: row.config
10505
- };
10506
- }
10507
- async listRoles() {
10508
- const tableName = this.getQualifiedRolesTableName();
10509
- const result = await this.db.execute(drizzleOrm.sql`
10510
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10511
- FROM ${drizzleOrm.sql.raw(tableName)}
10512
- ORDER BY name
10513
- `);
10514
- return result.rows.map((row) => ({
10515
- id: row.id,
10516
- name: row.name,
10517
- isAdmin: row.is_admin,
10518
- defaultPermissions: row.default_permissions,
10519
- collectionPermissions: row.collection_permissions,
10520
- config: row.config
10521
- }));
10522
- }
10523
- async createRole(data) {
10524
- const tableName = this.getQualifiedRolesTableName();
10525
- const result = await this.db.execute(drizzleOrm.sql`
10526
- INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10527
- VALUES (
10528
- ${data.id},
10529
- ${data.name},
10530
- ${data.isAdmin ?? false},
10531
- ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10532
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10533
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10534
- )
10535
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10536
- `);
10537
- const row = result.rows[0];
10538
- return {
10539
- id: row.id,
10540
- name: row.name,
10541
- isAdmin: row.is_admin,
10542
- defaultPermissions: row.default_permissions,
10543
- collectionPermissions: row.collection_permissions,
10544
- config: row.config
10545
- };
10546
- }
10547
- async updateRole(id, data) {
10548
- const existing = await this.getRoleById(id);
10549
- if (!existing) return null;
10550
- const tableName = this.getQualifiedRolesTableName();
10551
- await this.db.execute(drizzleOrm.sql`
10552
- UPDATE ${drizzleOrm.sql.raw(tableName)}
10553
- SET
10554
- name = ${data.name ?? existing.name},
10555
- is_admin = ${data.isAdmin ?? existing.isAdmin},
10556
- default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10557
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10558
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10559
- WHERE id = ${id}
10560
- `);
10561
- return this.getRoleById(id);
10562
- }
10563
- async deleteRole(id) {
10564
- const tableName = this.getQualifiedRolesTableName();
10565
- await this.db.execute(drizzleOrm.sql`DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE id = ${id}`);
10566
- }
10567
- }
10568
10914
  class RefreshTokenService {
10569
10915
  constructor(db, tableOrTables) {
10570
10916
  this.db = db;
@@ -10759,11 +11105,9 @@ ${tableRelations.join(",\n")}
10759
11105
  constructor(db, tableOrTables) {
10760
11106
  this.db = db;
10761
11107
  this.userService = new UserService(db, tableOrTables);
10762
- this.roleService = new RoleService(db, tableOrTables);
10763
11108
  this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
10764
11109
  }
10765
11110
  userService;
10766
- roleService;
10767
11111
  tokenRepository;
10768
11112
  // User operations (delegate to UserService)
10769
11113
  async createUser(data) {
@@ -10823,26 +11167,56 @@ ${tableRelations.join(",\n")}
10823
11167
  async getUserWithRoles(userId) {
10824
11168
  return this.userService.getUserWithRoles(userId);
10825
11169
  }
10826
- // Role operations (delegate to RoleService)
11170
+ // Role operations (roles are inline on users, synthesized from string IDs)
10827
11171
  async getRoleById(id) {
10828
- return this.roleService.getRoleById(id);
11172
+ return {
11173
+ id,
11174
+ name: id,
11175
+ isAdmin: id === "admin",
11176
+ defaultPermissions: null,
11177
+ collectionPermissions: null
11178
+ };
10829
11179
  }
10830
11180
  async listRoles() {
10831
- return this.roleService.listRoles();
10832
- }
10833
- async createRole(data) {
10834
- return this.roleService.createRole({
10835
- ...data,
10836
- defaultPermissions: data.defaultPermissions ?? null,
10837
- collectionPermissions: data.collectionPermissions ?? null,
10838
- config: data.config ?? null
10839
- });
11181
+ return [{
11182
+ id: "admin",
11183
+ name: "Admin",
11184
+ isAdmin: true,
11185
+ defaultPermissions: null,
11186
+ collectionPermissions: null
11187
+ }, {
11188
+ id: "editor",
11189
+ name: "Editor",
11190
+ isAdmin: false,
11191
+ defaultPermissions: null,
11192
+ collectionPermissions: null
11193
+ }, {
11194
+ id: "viewer",
11195
+ name: "Viewer",
11196
+ isAdmin: false,
11197
+ defaultPermissions: null,
11198
+ collectionPermissions: null
11199
+ }];
11200
+ }
11201
+ async createRole(_data) {
11202
+ return {
11203
+ id: _data.id,
11204
+ name: _data.name,
11205
+ isAdmin: _data.isAdmin ?? false,
11206
+ defaultPermissions: _data.defaultPermissions ?? null,
11207
+ collectionPermissions: _data.collectionPermissions ?? null
11208
+ };
10840
11209
  }
10841
11210
  async updateRole(id, data) {
10842
- return this.roleService.updateRole(id, data);
11211
+ return {
11212
+ id,
11213
+ name: data.name ?? id,
11214
+ isAdmin: data.isAdmin ?? id === "admin",
11215
+ defaultPermissions: data.defaultPermissions ?? null,
11216
+ collectionPermissions: data.collectionPermissions ?? null
11217
+ };
10843
11218
  }
10844
- async deleteRole(id) {
10845
- await this.roleService.deleteRole(id);
11219
+ async deleteRole(_id) {
10846
11220
  }
10847
11221
  // Token operations (delegate to PostgresTokenRepository)
10848
11222
  async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
@@ -10878,6 +11252,219 @@ ${tableRelations.join(",\n")}
10878
11252
  async deleteExpiredTokens() {
10879
11253
  await this.tokenRepository.deleteExpiredTokens();
10880
11254
  }
11255
+ // MFA operations (delegate to MfaService)
11256
+ _mfaService = null;
11257
+ getMfaService() {
11258
+ if (!this._mfaService) {
11259
+ this._mfaService = new MfaService(this.db);
11260
+ }
11261
+ return this._mfaService;
11262
+ }
11263
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11264
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11265
+ }
11266
+ async getMfaFactors(userId) {
11267
+ return this.getMfaService().getMfaFactors(userId);
11268
+ }
11269
+ async getMfaFactorById(factorId) {
11270
+ return this.getMfaService().getMfaFactorById(factorId);
11271
+ }
11272
+ async verifyMfaFactor(factorId) {
11273
+ return this.getMfaService().verifyMfaFactor(factorId);
11274
+ }
11275
+ async deleteMfaFactor(factorId, userId) {
11276
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11277
+ }
11278
+ async createMfaChallenge(factorId, ipAddress) {
11279
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11280
+ }
11281
+ async getMfaChallengeById(challengeId) {
11282
+ return this.getMfaService().getMfaChallengeById(challengeId);
11283
+ }
11284
+ async verifyMfaChallenge(challengeId) {
11285
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11286
+ }
11287
+ async createRecoveryCodes(userId, codeHashes) {
11288
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11289
+ }
11290
+ async useRecoveryCode(userId, codeHash) {
11291
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11292
+ }
11293
+ async getUnusedRecoveryCodeCount(userId) {
11294
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11295
+ }
11296
+ async deleteAllRecoveryCodes(userId) {
11297
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11298
+ }
11299
+ async hasVerifiedMfaFactors(userId) {
11300
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11301
+ }
11302
+ }
11303
+ class MfaService {
11304
+ constructor(db, schemaName = "rebase") {
11305
+ this.db = db;
11306
+ this.schemaName = schemaName;
11307
+ }
11308
+ qualify(tableName) {
11309
+ return `"${this.schemaName}"."${tableName}"`;
11310
+ }
11311
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11312
+ const tableName = this.qualify("mfa_factors");
11313
+ const result = await this.db.execute(drizzleOrm.sql`
11314
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11315
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11316
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11317
+ `);
11318
+ const row = result.rows[0];
11319
+ return {
11320
+ id: row.id,
11321
+ userId: row.user_id,
11322
+ factorType: row.factor_type,
11323
+ friendlyName: row.friendly_name ?? void 0,
11324
+ verified: row.verified,
11325
+ createdAt: new Date(row.created_at),
11326
+ updatedAt: new Date(row.updated_at)
11327
+ };
11328
+ }
11329
+ async getMfaFactors(userId) {
11330
+ const tableName = this.qualify("mfa_factors");
11331
+ const result = await this.db.execute(drizzleOrm.sql`
11332
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11333
+ FROM ${drizzleOrm.sql.raw(tableName)}
11334
+ WHERE user_id = ${userId}
11335
+ ORDER BY created_at
11336
+ `);
11337
+ return result.rows.map((row) => ({
11338
+ id: row.id,
11339
+ userId: row.user_id,
11340
+ factorType: row.factor_type,
11341
+ friendlyName: row.friendly_name ?? void 0,
11342
+ verified: row.verified,
11343
+ createdAt: new Date(row.created_at),
11344
+ updatedAt: new Date(row.updated_at)
11345
+ }));
11346
+ }
11347
+ async getMfaFactorById(factorId) {
11348
+ const tableName = this.qualify("mfa_factors");
11349
+ const result = await this.db.execute(drizzleOrm.sql`
11350
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11351
+ FROM ${drizzleOrm.sql.raw(tableName)}
11352
+ WHERE id = ${factorId}
11353
+ `);
11354
+ if (result.rows.length === 0) return null;
11355
+ const row = result.rows[0];
11356
+ return {
11357
+ id: row.id,
11358
+ userId: row.user_id,
11359
+ factorType: row.factor_type,
11360
+ secretEncrypted: row.secret_encrypted,
11361
+ friendlyName: row.friendly_name ?? void 0,
11362
+ verified: row.verified,
11363
+ createdAt: new Date(row.created_at),
11364
+ updatedAt: new Date(row.updated_at)
11365
+ };
11366
+ }
11367
+ async verifyMfaFactor(factorId) {
11368
+ const tableName = this.qualify("mfa_factors");
11369
+ await this.db.execute(drizzleOrm.sql`
11370
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11371
+ SET verified = TRUE, updated_at = NOW()
11372
+ WHERE id = ${factorId}
11373
+ `);
11374
+ }
11375
+ async deleteMfaFactor(factorId, userId) {
11376
+ const tableName = this.qualify("mfa_factors");
11377
+ await this.db.execute(drizzleOrm.sql`
11378
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)}
11379
+ WHERE id = ${factorId} AND user_id = ${userId}
11380
+ `);
11381
+ }
11382
+ async createMfaChallenge(factorId, ipAddress) {
11383
+ const tableName = this.qualify("mfa_challenges");
11384
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11385
+ const result = await this.db.execute(drizzleOrm.sql`
11386
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (factor_id, ip_address, expires_at)
11387
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11388
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11389
+ `);
11390
+ const row = result.rows[0];
11391
+ return {
11392
+ id: row.id,
11393
+ factorId: row.factor_id,
11394
+ createdAt: new Date(row.created_at),
11395
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11396
+ ipAddress: row.ip_address ?? void 0
11397
+ };
11398
+ }
11399
+ async getMfaChallengeById(challengeId) {
11400
+ const tableName = this.qualify("mfa_challenges");
11401
+ const result = await this.db.execute(drizzleOrm.sql`
11402
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11403
+ FROM ${drizzleOrm.sql.raw(tableName)}
11404
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11405
+ `);
11406
+ if (result.rows.length === 0) return null;
11407
+ const row = result.rows[0];
11408
+ return {
11409
+ id: row.id,
11410
+ factorId: row.factor_id,
11411
+ createdAt: new Date(row.created_at),
11412
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11413
+ ipAddress: row.ip_address ?? void 0
11414
+ };
11415
+ }
11416
+ async verifyMfaChallenge(challengeId) {
11417
+ const tableName = this.qualify("mfa_challenges");
11418
+ await this.db.execute(drizzleOrm.sql`
11419
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11420
+ SET verified_at = NOW()
11421
+ WHERE id = ${challengeId}
11422
+ `);
11423
+ }
11424
+ async createRecoveryCodes(userId, codeHashes) {
11425
+ const tableName = this.qualify("recovery_codes");
11426
+ await this.db.execute(drizzleOrm.sql`
11427
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11428
+ `);
11429
+ for (const hash of codeHashes) {
11430
+ await this.db.execute(drizzleOrm.sql`
11431
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, code_hash)
11432
+ VALUES (${userId}, ${hash})
11433
+ `);
11434
+ }
11435
+ }
11436
+ async useRecoveryCode(userId, codeHash) {
11437
+ const tableName = this.qualify("recovery_codes");
11438
+ const result = await this.db.execute(drizzleOrm.sql`
11439
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11440
+ SET used_at = NOW()
11441
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11442
+ RETURNING id
11443
+ `);
11444
+ return result.rows.length > 0;
11445
+ }
11446
+ async getUnusedRecoveryCodeCount(userId) {
11447
+ const tableName = this.qualify("recovery_codes");
11448
+ const result = await this.db.execute(drizzleOrm.sql`
11449
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11450
+ WHERE user_id = ${userId} AND used_at IS NULL
11451
+ `);
11452
+ return result.rows[0].count;
11453
+ }
11454
+ async deleteAllRecoveryCodes(userId) {
11455
+ const tableName = this.qualify("recovery_codes");
11456
+ await this.db.execute(drizzleOrm.sql`
11457
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11458
+ `);
11459
+ }
11460
+ async hasVerifiedMfaFactors(userId) {
11461
+ const tableName = this.qualify("mfa_factors");
11462
+ const result = await this.db.execute(drizzleOrm.sql`
11463
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11464
+ WHERE user_id = ${userId} AND verified = TRUE
11465
+ `);
11466
+ return result.rows[0].count > 0;
11467
+ }
10881
11468
  }
10882
11469
  const DEFAULT_RETENTION = {
10883
11470
  maxEntries: 200,
@@ -11088,7 +11675,7 @@ ${tableRelations.join(",\n")}
11088
11675
  const registry = new PostgresCollectionRegistry();
11089
11676
  if (collections) {
11090
11677
  registry.registerMultiple(collections);
11091
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11678
+ serverCore.logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11092
11679
  }
11093
11680
  if (pgConfig.schema?.tables) {
11094
11681
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -11114,10 +11701,28 @@ ${tableRelations.join(",\n")}
11114
11701
  try {
11115
11702
  await schemaAwareDb.execute(drizzleOrm.sql`SELECT 1`);
11116
11703
  } catch (err) {
11117
- console.error("❌ Failed to connect to PostgreSQL:", err);
11118
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11704
+ serverCore.logger.error("❌ Failed to connect to PostgreSQL", {
11705
+ error: err
11706
+ });
11707
+ serverCore.logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11119
11708
  }
11120
11709
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11710
+ let readDb;
11711
+ const readUrl = process.env.DATABASE_READ_URL;
11712
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11713
+ try {
11714
+ const {
11715
+ createReadReplicaConnection: createReadReplicaConnection2
11716
+ } = await Promise.resolve().then(() => connection);
11717
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11718
+ readDb = readResources.db;
11719
+ serverCore.logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11720
+ } catch (err) {
11721
+ serverCore.logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11722
+ error: err
11723
+ });
11724
+ }
11725
+ }
11121
11726
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
11122
11727
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
11123
11728
  realtimeService.setDataDriver(driver);
@@ -11125,18 +11730,24 @@ ${tableRelations.join(",\n")}
11125
11730
  try {
11126
11731
  await driver.branchService.ensureBranchMetadataTable();
11127
11732
  } catch (err) {
11128
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11733
+ serverCore.logger.warn("⚠️ Could not initialize branch metadata table", {
11734
+ error: err
11735
+ });
11129
11736
  }
11130
11737
  }
11131
- if (pgConfig.connectionString) {
11738
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11739
+ if (directUrl) {
11132
11740
  try {
11133
- await realtimeService.startListening(pgConfig.connectionString);
11741
+ await realtimeService.startListening(directUrl);
11134
11742
  } catch (err) {
11135
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11743
+ serverCore.logger.warn("⚠️ Cross-instance realtime could not be started", {
11744
+ error: err
11745
+ });
11136
11746
  }
11137
11747
  }
11138
11748
  const internals = {
11139
11749
  db: schemaAwareDb,
11750
+ readDb,
11140
11751
  registry,
11141
11752
  realtimeService,
11142
11753
  driver,
@@ -11161,28 +11772,19 @@ ${tableRelations.join(",\n")}
11161
11772
  emailService = serverCore.createEmailService(authConfig.email);
11162
11773
  }
11163
11774
  const customUsersTable = registry?.getTable("users");
11164
- const customRolesTable = registry?.getTable("roles");
11165
11775
  let usersSchemaName = "rebase";
11166
- let rolesSchemaName = "rebase";
11167
11776
  if (customUsersTable) {
11168
11777
  usersSchemaName = pgCore.getTableConfig(customUsersTable).schema || "public";
11169
11778
  }
11170
- if (customRolesTable) {
11171
- rolesSchemaName = pgCore.getTableConfig(customRolesTable).schema || "public";
11172
- }
11173
- const authTables = createAuthSchema(rolesSchemaName, usersSchemaName);
11779
+ const authTables = createAuthSchema(usersSchemaName);
11174
11780
  if (customUsersTable) {
11175
11781
  authTables.users = customUsersTable;
11176
11782
  }
11177
- if (customRolesTable) {
11178
- authTables.roles = customRolesTable;
11179
- }
11180
11783
  const userService = new UserService(db, authTables);
11181
- const roleService = new RoleService(db, authTables);
11182
11784
  const authRepository = new PostgresAuthRepository(db, authTables);
11183
11785
  return {
11184
11786
  userService,
11185
- roleService,
11787
+ roleService: userService,
11186
11788
  emailService,
11187
11789
  authRepository
11188
11790
  };
@@ -11273,22 +11875,25 @@ ${tableRelations.join(",\n")}
11273
11875
  exports2.RealtimeService = RealtimeService;
11274
11876
  exports2.appConfig = appConfig;
11275
11877
  exports2.createAuthSchema = createAuthSchema;
11878
+ exports2.createDirectDatabaseConnection = createDirectDatabaseConnection;
11276
11879
  exports2.createPostgresAdapter = createPostgresAdapter;
11277
11880
  exports2.createPostgresBootstrapper = createPostgresBootstrapper;
11278
11881
  exports2.createPostgresDatabaseConnection = createPostgresDatabaseConnection;
11279
11882
  exports2.createPostgresWebSocket = createPostgresWebSocket;
11883
+ exports2.createReadReplicaConnection = createReadReplicaConnection;
11280
11884
  exports2.generateSchema = generateSchema;
11885
+ exports2.mfaChallenges = mfaChallenges;
11886
+ exports2.mfaChallengesRelations = mfaChallengesRelations;
11887
+ exports2.mfaFactors = mfaFactors;
11888
+ exports2.mfaFactorsRelations = mfaFactorsRelations;
11281
11889
  exports2.passwordResetTokens = passwordResetTokens;
11282
11890
  exports2.passwordResetTokensRelations = passwordResetTokensRelations;
11283
- exports2.rebaseSchema = rebaseSchema;
11891
+ exports2.recoveryCodes = recoveryCodes;
11892
+ exports2.recoveryCodesRelations = recoveryCodesRelations;
11284
11893
  exports2.refreshTokens = refreshTokens;
11285
11894
  exports2.refreshTokensRelations = refreshTokensRelations;
11286
- exports2.roles = roles;
11287
- exports2.rolesRelations = rolesRelations;
11288
11895
  exports2.userIdentities = userIdentities;
11289
11896
  exports2.userIdentitiesRelations = userIdentitiesRelations;
11290
- exports2.userRoles = userRoles;
11291
- exports2.userRolesRelations = userRolesRelations;
11292
11897
  exports2.users = users;
11293
11898
  exports2.usersRelations = usersRelations;
11294
11899
  exports2.usersSchema = usersSchema;