@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.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Pool, Client } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
4
- import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, varchar, boolean, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
4
+ import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, text, boolean, varchar, uuid, unique, getTableConfig } from "drizzle-orm/pg-core";
5
5
  import { createHash, randomUUID } from "crypto";
6
6
  import * as fs from "fs";
7
7
  import { promises } from "fs";
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
11
11
  import { WebSocket, WebSocketServer } from "ws";
12
12
  import { EventEmitter } from "events";
13
13
  import { inspect } from "util";
14
- import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
14
+ import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
15
15
  const DEFAULT_POOL = {
16
16
  max: 20,
17
17
  idleTimeoutMillis: 3e4,
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
51
51
  connectionString
52
52
  };
53
53
  }
54
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
55
+ const opts = {
56
+ ...DEFAULT_POOL,
57
+ max: 5,
58
+ ...poolConfig
59
+ };
60
+ const pgPoolConfig = {
61
+ connectionString,
62
+ max: opts.max,
63
+ idleTimeoutMillis: opts.idleTimeoutMillis,
64
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
65
+ query_timeout: opts.queryTimeout,
66
+ statement_timeout: opts.statementTimeout,
67
+ keepAlive: opts.keepAlive,
68
+ keepAliveInitialDelayMillis: 0
69
+ };
70
+ const pool = new Pool(pgPoolConfig);
71
+ pool.on("error", (err) => {
72
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
73
+ });
74
+ const db = schema ? drizzle(pool, {
75
+ schema
76
+ }) : drizzle(pool);
77
+ return {
78
+ db,
79
+ pool,
80
+ connectionString
81
+ };
82
+ }
83
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
84
+ const opts = {
85
+ ...DEFAULT_POOL,
86
+ max: 10,
87
+ ...poolConfig
88
+ };
89
+ const pgPoolConfig = {
90
+ connectionString,
91
+ max: opts.max,
92
+ idleTimeoutMillis: opts.idleTimeoutMillis,
93
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
94
+ query_timeout: opts.queryTimeout,
95
+ statement_timeout: opts.statementTimeout,
96
+ keepAlive: opts.keepAlive,
97
+ keepAliveInitialDelayMillis: 0
98
+ };
99
+ const pool = new Pool(pgPoolConfig);
100
+ pool.on("error", (err) => {
101
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
102
+ });
103
+ const db = schema ? drizzle(pool, {
104
+ schema
105
+ }) : drizzle(pool);
106
+ return {
107
+ db,
108
+ pool,
109
+ connectionString
110
+ };
111
+ }
112
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
113
+ __proto__: null,
114
+ createDirectDatabaseConnection,
115
+ createPostgresDatabaseConnection,
116
+ createReadReplicaConnection
117
+ }, Symbol.toStringTag, { value: "Module" }));
54
118
  class Vector {
55
119
  value;
56
120
  constructor(value) {
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
970
1034
  return output;
971
1035
  }
972
1036
  for (const key in source) {
1037
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1038
+ continue;
1039
+ }
973
1040
  if (Object.prototype.hasOwnProperty.call(source, key)) {
974
1041
  const sourceValue = source[key];
975
1042
  const outputValue = output[key];
@@ -2644,6 +2711,142 @@ class CollectionRegistry {
2644
2711
  };
2645
2712
  }
2646
2713
  }
2714
+ const defaultUsersCollection = {
2715
+ name: "Users",
2716
+ singularName: "User",
2717
+ slug: "users",
2718
+ table: "users",
2719
+ schema: "rebase",
2720
+ icon: "Users",
2721
+ group: "Settings",
2722
+ openEntityMode: "dialog",
2723
+ disableDefaultActions: ["copy"],
2724
+ sort: ["createdAt", "desc"],
2725
+ properties: {
2726
+ id: {
2727
+ name: "ID",
2728
+ type: "string",
2729
+ isId: "uuid",
2730
+ ui: {
2731
+ readOnly: true
2732
+ }
2733
+ },
2734
+ email: {
2735
+ name: "Email",
2736
+ type: "string",
2737
+ validation: {
2738
+ required: true,
2739
+ unique: true
2740
+ }
2741
+ },
2742
+ displayName: {
2743
+ name: "Name",
2744
+ type: "string",
2745
+ columnName: "display_name",
2746
+ validation: {
2747
+ required: true
2748
+ }
2749
+ },
2750
+ photoURL: {
2751
+ name: "Photo URL",
2752
+ type: "string",
2753
+ columnName: "photo_url",
2754
+ url: "image"
2755
+ },
2756
+ roles: {
2757
+ name: "Roles",
2758
+ type: "array",
2759
+ columnType: "text[]",
2760
+ of: {
2761
+ name: "Role",
2762
+ type: "string",
2763
+ enum: {
2764
+ admin: "Admin",
2765
+ editor: "Editor",
2766
+ viewer: "Viewer"
2767
+ }
2768
+ }
2769
+ },
2770
+ passwordHash: {
2771
+ name: "Password Hash",
2772
+ type: "string",
2773
+ columnName: "password_hash",
2774
+ ui: {
2775
+ hideFromCollection: true,
2776
+ disabled: {
2777
+ hidden: true
2778
+ }
2779
+ }
2780
+ },
2781
+ emailVerified: {
2782
+ name: "Email Verified",
2783
+ type: "boolean",
2784
+ columnName: "email_verified",
2785
+ defaultValue: false,
2786
+ ui: {
2787
+ hideFromCollection: true,
2788
+ disabled: {
2789
+ hidden: true
2790
+ }
2791
+ }
2792
+ },
2793
+ emailVerificationToken: {
2794
+ name: "Email Verification Token",
2795
+ type: "string",
2796
+ columnName: "email_verification_token",
2797
+ ui: {
2798
+ hideFromCollection: true,
2799
+ disabled: {
2800
+ hidden: true
2801
+ }
2802
+ }
2803
+ },
2804
+ emailVerificationSentAt: {
2805
+ name: "Email Verification Sent At",
2806
+ type: "date",
2807
+ columnName: "email_verification_sent_at",
2808
+ ui: {
2809
+ hideFromCollection: true,
2810
+ disabled: {
2811
+ hidden: true
2812
+ }
2813
+ }
2814
+ },
2815
+ metadata: {
2816
+ name: "Metadata",
2817
+ type: "map",
2818
+ defaultValue: {},
2819
+ ui: {
2820
+ hideFromCollection: true,
2821
+ disabled: {
2822
+ hidden: true
2823
+ }
2824
+ }
2825
+ },
2826
+ createdAt: {
2827
+ name: "Created At",
2828
+ type: "date",
2829
+ columnName: "created_at",
2830
+ ui: {
2831
+ readOnly: true
2832
+ }
2833
+ },
2834
+ updatedAt: {
2835
+ name: "Updated At",
2836
+ type: "date",
2837
+ columnName: "updated_at",
2838
+ autoValue: "on_update",
2839
+ ui: {
2840
+ hideFromCollection: true,
2841
+ disabled: {
2842
+ hidden: true
2843
+ }
2844
+ }
2845
+ }
2846
+ },
2847
+ listProperties: ["displayName", "email", "roles", "createdAt"],
2848
+ propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
2849
+ };
2647
2850
  function mapOperator(op) {
2648
2851
  switch (op) {
2649
2852
  case "==":
@@ -2890,6 +3093,9 @@ function createDriverAccessor(driver, slug) {
2890
3093
  }
2891
3094
  });
2892
3095
  },
3096
+ deleteAll: driver.deleteAll ? async () => {
3097
+ return driver.deleteAll(slug);
3098
+ } : void 0,
2893
3099
  count: driver.countEntities ? async (params) => {
2894
3100
  return driver.countEntities({
2895
3101
  path: slug,
@@ -2984,7 +3190,13 @@ class DrizzleConditionBuilder {
2984
3190
  for (const [field, filterParam] of Object.entries(filter)) {
2985
3191
  if (!filterParam) continue;
2986
3192
  const [op, value] = filterParam;
2987
- const fieldColumn = table[field];
3193
+ let fieldColumn = table[field];
3194
+ if (!fieldColumn) {
3195
+ const relationKey = `${field}_id`;
3196
+ if (relationKey in table) {
3197
+ fieldColumn = table[relationKey];
3198
+ }
3199
+ }
2988
3200
  if (!fieldColumn) {
2989
3201
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2990
3202
  continue;
@@ -3026,6 +3238,17 @@ class DrizzleConditionBuilder {
3026
3238
  return null;
3027
3239
  case "array-contains":
3028
3240
  return sql`${column} @> ${JSON.stringify([value])}`;
3241
+ case "array-contains-any":
3242
+ if (Array.isArray(value) && value.length > 0) {
3243
+ const textValues = value.map((v) => String(v));
3244
+ return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
3245
+ }
3246
+ return sql`${column} @> ${JSON.stringify([value])}`;
3247
+ case "not-in":
3248
+ if (Array.isArray(value) && value.length > 0) {
3249
+ return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
3250
+ }
3251
+ return null;
3029
3252
  default:
3030
3253
  console.warn(`Unsupported filter operation: ${op}`);
3031
3254
  return null;
@@ -3541,6 +3764,40 @@ class DrizzleConditionBuilder {
3541
3764
  return null;
3542
3765
  }
3543
3766
  }
3767
+ /**
3768
+ * Build vector similarity search expressions for pgvector.
3769
+ *
3770
+ * Returns:
3771
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3772
+ * - `filter`: optional WHERE clause for distance threshold
3773
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3774
+ */
3775
+ static buildVectorSearchConditions(table, vectorSearch) {
3776
+ const column = table[vectorSearch.property];
3777
+ if (!column) {
3778
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3779
+ }
3780
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3781
+ const distanceFn = vectorSearch.distance || "cosine";
3782
+ let operator;
3783
+ switch (distanceFn) {
3784
+ case "cosine":
3785
+ operator = "<=>";
3786
+ break;
3787
+ case "l2":
3788
+ operator = "<->";
3789
+ break;
3790
+ case "inner_product":
3791
+ operator = "<#>";
3792
+ break;
3793
+ }
3794
+ const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
3795
+ return {
3796
+ orderBy: distanceExpr,
3797
+ filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3798
+ distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
3799
+ };
3800
+ }
3544
3801
  }
3545
3802
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3546
3803
  function getColumnMeta(col) {
@@ -5486,7 +5743,7 @@ class EntityFetchService {
5486
5743
  const qb = this.getQueryBuilder(tableName);
5487
5744
  const withConfig = this.buildWithConfig(collection);
5488
5745
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5489
- if (qb && !options.searchString && !hasRelations) {
5746
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5490
5747
  try {
5491
5748
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5492
5749
  const results2 = await qb.findMany(queryOpts);
@@ -5500,7 +5757,14 @@ class EntityFetchService {
5500
5757
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5501
5758
  }
5502
5759
  }
5503
- let query = this.db.select().from(table).$dynamic();
5760
+ let vectorMeta;
5761
+ if (options.vectorSearch) {
5762
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5763
+ }
5764
+ let query = vectorMeta ? this.db.select({
5765
+ table_row: table,
5766
+ _distance: vectorMeta.distanceSelect
5767
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5504
5768
  const allConditions = [];
5505
5769
  if (options.searchString) {
5506
5770
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5511,12 +5775,17 @@ class EntityFetchService {
5511
5775
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5512
5776
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5513
5777
  }
5778
+ if (vectorMeta?.filter) {
5779
+ allConditions.push(vectorMeta.filter);
5780
+ }
5514
5781
  if (allConditions.length > 0) {
5515
5782
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5516
5783
  if (finalCondition) query = query.where(finalCondition);
5517
5784
  }
5518
5785
  const orderExpressions = [];
5519
- if (options.orderBy) {
5786
+ if (vectorMeta) {
5787
+ orderExpressions.push(asc(vectorMeta.orderBy));
5788
+ } else if (options.orderBy) {
5520
5789
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5521
5790
  if (orderByField) {
5522
5791
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5532,10 +5801,14 @@ class EntityFetchService {
5532
5801
  if (finalCondition) query = query.where(finalCondition);
5533
5802
  }
5534
5803
  }
5535
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5804
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5536
5805
  if (limitValue) query = query.limit(limitValue);
5537
5806
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5538
- const results = await query;
5807
+ const rawResults = await query;
5808
+ const results = vectorMeta ? rawResults.map((r) => ({
5809
+ ...r.table_row,
5810
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5811
+ })) : rawResults;
5539
5812
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5540
5813
  }
5541
5814
  /**
@@ -5757,7 +6030,7 @@ class EntityFetchService {
5757
6030
  const idField = table[idInfo.fieldName];
5758
6031
  const tableName = getTableName$1(table);
5759
6032
  const qb = this.getQueryBuilder(tableName);
5760
- if (qb && !options.searchString) {
6033
+ if (qb && !options.searchString && !options.vectorSearch) {
5761
6034
  try {
5762
6035
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5763
6036
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5903,7 +6176,14 @@ class EntityFetchService {
5903
6176
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5904
6177
  const idInfo = idInfoArray[0];
5905
6178
  const idField = table[idInfo.fieldName];
5906
- let query = this.db.select().from(table).$dynamic();
6179
+ let vectorMeta;
6180
+ if (options.vectorSearch) {
6181
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6182
+ }
6183
+ let query = vectorMeta ? this.db.select({
6184
+ table_row: table,
6185
+ _distance: vectorMeta.distanceSelect
6186
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5907
6187
  const allConditions = [];
5908
6188
  if (options.searchString) {
5909
6189
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5914,12 +6194,17 @@ class EntityFetchService {
5914
6194
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5915
6195
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5916
6196
  }
6197
+ if (vectorMeta?.filter) {
6198
+ allConditions.push(vectorMeta.filter);
6199
+ }
5917
6200
  if (allConditions.length > 0) {
5918
6201
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5919
6202
  if (finalCondition) query = query.where(finalCondition);
5920
6203
  }
5921
6204
  const orderExpressions = [];
5922
- if (options.orderBy) {
6205
+ if (vectorMeta) {
6206
+ orderExpressions.push(asc(vectorMeta.orderBy));
6207
+ } else if (options.orderBy) {
5923
6208
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5924
6209
  if (orderByField) {
5925
6210
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5927,10 +6212,17 @@ class EntityFetchService {
5927
6212
  }
5928
6213
  orderExpressions.push(desc(idField));
5929
6214
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5930
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6215
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5931
6216
  if (limitValue) query = query.limit(limitValue);
5932
6217
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5933
- return await query;
6218
+ const rawResults = await query;
6219
+ if (vectorMeta) {
6220
+ return rawResults.map((r) => ({
6221
+ ...r.table_row,
6222
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6223
+ }));
6224
+ }
6225
+ return rawResults;
5934
6226
  }
5935
6227
  /**
5936
6228
  * Check if the Drizzle instance has the relational query API available
@@ -6068,6 +6360,14 @@ class EntityPersistService {
6068
6360
  const parsedId = parsedIdObj[idInfo.fieldName];
6069
6361
  await this.db.delete(table).where(eq(idField, parsedId));
6070
6362
  }
6363
+ /**
6364
+ * Delete all entities from a collection
6365
+ */
6366
+ async deleteAll(collectionPath, _databaseId) {
6367
+ const collection = getCollectionByPath(collectionPath, this.registry);
6368
+ const table = getTableForCollection(collection, this.registry);
6369
+ await this.db.delete(table);
6370
+ }
6071
6371
  /**
6072
6372
  * Save an entity (create or update)
6073
6373
  */
@@ -6399,6 +6699,12 @@ class EntityService {
6399
6699
  async deleteEntity(collectionPath, entityId, databaseId) {
6400
6700
  return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
6401
6701
  }
6702
+ /**
6703
+ * Delete all entities from a collection
6704
+ */
6705
+ async deleteAll(collectionPath, databaseId) {
6706
+ return this.persistService.deleteAll(collectionPath, databaseId);
6707
+ }
6402
6708
  /**
6403
6709
  * Execute raw SQL
6404
6710
  */
@@ -6684,7 +6990,8 @@ class PostgresBackendDriver {
6684
6990
  startAfter,
6685
6991
  orderBy,
6686
6992
  searchString,
6687
- order
6993
+ order,
6994
+ vectorSearch
6688
6995
  }) {
6689
6996
  const entities = await this.entityService.fetchCollection(path2, {
6690
6997
  filter,
@@ -6694,7 +7001,8 @@ class PostgresBackendDriver {
6694
7001
  offset,
6695
7002
  startAfter,
6696
7003
  databaseId: collection?.databaseId,
6697
- searchString
7004
+ searchString,
7005
+ vectorSearch
6698
7006
  });
6699
7007
  const {
6700
7008
  collection: resolvedCollection,
@@ -7097,6 +7405,10 @@ class PostgresBackendDriver {
7097
7405
  await this.realtimeService.notifyEntityUpdate(entity.path, entity.id.toString(), null, entity.databaseId || resolvedCollection?.databaseId);
7098
7406
  }
7099
7407
  }
7408
+ async deleteAll(path2) {
7409
+ await this.entityService.deleteAll(path2);
7410
+ await this.realtimeService.notifyEntityUpdate(path2, "*", null);
7411
+ }
7100
7412
  async checkUniqueField(path2, name, value, entityId, collection) {
7101
7413
  return this.entityService.checkUniqueField(path2, name, value, entityId, collection?.databaseId);
7102
7414
  }
@@ -7366,11 +7678,11 @@ class AuthenticatedPostgresBackendDriver {
7366
7678
  console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
7367
7679
  userId = "anonymous";
7368
7680
  }
7369
- const userRoles2 = this.user?.roles ?? [];
7681
+ const userRoles = this.user?.roles ?? [];
7370
7682
  if (!this.user?.roles) {
7371
7683
  console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
7372
7684
  }
7373
- const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7685
+ const normalizedRoles = userRoles.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
7374
7686
  const rolesString = normalizedRoles.join(",");
7375
7687
  await tx.execute(sql`
7376
7688
  SELECT
@@ -7378,7 +7690,7 @@ class AuthenticatedPostgresBackendDriver {
7378
7690
  set_config('app.user_roles', ${rolesString}, true),
7379
7691
  set_config('app.jwt', ${JSON.stringify({
7380
7692
  sub: userId,
7381
- roles: userRoles2
7693
+ roles: userRoles
7382
7694
  })}, true)
7383
7695
  `);
7384
7696
  const txEntityService = new EntityService(tx, this.delegate.registry);
@@ -7433,6 +7745,9 @@ class AuthenticatedPostgresBackendDriver {
7433
7745
  async deleteEntity(props) {
7434
7746
  return this.withTransaction((delegate) => delegate.deleteEntity(props));
7435
7747
  }
7748
+ async deleteAll(path2) {
7749
+ return this.delegate.deleteAll(path2);
7750
+ }
7436
7751
  async checkUniqueField(path2, name, value, entityId, collection) {
7437
7752
  return this.withTransaction((delegate) => delegate.checkUniqueField(path2, name, value, entityId, collection));
7438
7753
  }
@@ -7512,11 +7827,10 @@ class DatabasePoolManager {
7512
7827
  this.pools.clear();
7513
7828
  }
7514
7829
  }
7515
- function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase") {
7516
- const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
7830
+ function createAuthSchema(usersSchemaName = "rebase") {
7517
7831
  const usersSchema2 = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
7518
- const rolesTableCreator = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable;
7519
- const usersTableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgTable;
7832
+ const tableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgTable;
7833
+ const usersTableCreator = tableCreator;
7520
7834
  const users2 = usersTableCreator("users", {
7521
7835
  id: uuid("id").defaultRandom().primaryKey(),
7522
7836
  email: varchar("email", {
@@ -7537,38 +7851,13 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7537
7851
  length: 255
7538
7852
  }),
7539
7853
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
7854
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
7855
+ roles: text("roles").array().default([]).notNull(),
7540
7856
  metadata: jsonb("metadata").$type().default({}).notNull(),
7541
7857
  createdAt: timestamp("created_at").defaultNow().notNull(),
7542
7858
  updatedAt: timestamp("updated_at").defaultNow().notNull()
7543
7859
  });
7544
- const roles2 = rolesTableCreator("roles", {
7545
- id: varchar("id", {
7546
- length: 50
7547
- }).primaryKey(),
7548
- // 'admin', 'editor', 'viewer'
7549
- name: varchar("name", {
7550
- length: 100
7551
- }).notNull(),
7552
- isAdmin: boolean("is_admin").default(false).notNull(),
7553
- defaultPermissions: jsonb("default_permissions").$type(),
7554
- collectionPermissions: jsonb("collection_permissions").$type(),
7555
- config: jsonb("config").$type()
7556
- });
7557
- const userRoles2 = rolesTableCreator("user_roles", {
7558
- userId: uuid("user_id").notNull().references(() => users2.id, {
7559
- onDelete: "cascade"
7560
- }),
7561
- roleId: varchar("role_id", {
7562
- length: 50
7563
- }).notNull().references(() => roles2.id, {
7564
- onDelete: "cascade"
7565
- })
7566
- }, (table) => ({
7567
- pk: primaryKey({
7568
- columns: [table.userId, table.roleId]
7569
- })
7570
- }));
7571
- const refreshTokens2 = rolesTableCreator("refresh_tokens", {
7860
+ const refreshTokens2 = tableCreator("refresh_tokens", {
7572
7861
  id: uuid("id").defaultRandom().primaryKey(),
7573
7862
  userId: uuid("user_id").notNull().references(() => users2.id, {
7574
7863
  onDelete: "cascade"
@@ -7587,7 +7876,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7587
7876
  }, (table) => ({
7588
7877
  uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
7589
7878
  }));
7590
- const passwordResetTokens2 = rolesTableCreator("password_reset_tokens", {
7879
+ const passwordResetTokens2 = tableCreator("password_reset_tokens", {
7591
7880
  id: uuid("id").defaultRandom().primaryKey(),
7592
7881
  userId: uuid("user_id").notNull().references(() => users2.id, {
7593
7882
  onDelete: "cascade"
@@ -7599,14 +7888,14 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7599
7888
  usedAt: timestamp("used_at"),
7600
7889
  createdAt: timestamp("created_at").defaultNow().notNull()
7601
7890
  });
7602
- const appConfig2 = rolesTableCreator("app_config", {
7891
+ const appConfig2 = tableCreator("app_config", {
7603
7892
  key: varchar("key", {
7604
7893
  length: 100
7605
7894
  }).primaryKey(),
7606
7895
  value: jsonb("value").notNull(),
7607
7896
  updatedAt: timestamp("updated_at").defaultNow().notNull()
7608
7897
  });
7609
- const userIdentities2 = rolesTableCreator("user_identities", {
7898
+ const userIdentities2 = tableCreator("user_identities", {
7610
7899
  id: uuid("id").defaultRandom().primaryKey(),
7611
7900
  userId: uuid("user_id").notNull().references(() => users2.id, {
7612
7901
  onDelete: "cascade"
@@ -7624,74 +7913,126 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7624
7913
  }, (table) => ({
7625
7914
  uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
7626
7915
  }));
7916
+ const mfaFactors2 = tableCreator("mfa_factors", {
7917
+ id: uuid("id").defaultRandom().primaryKey(),
7918
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7919
+ onDelete: "cascade"
7920
+ }),
7921
+ factorType: varchar("factor_type", {
7922
+ length: 20
7923
+ }).notNull(),
7924
+ // 'totp'
7925
+ secretEncrypted: varchar("secret_encrypted", {
7926
+ length: 500
7927
+ }).notNull(),
7928
+ friendlyName: varchar("friendly_name", {
7929
+ length: 255
7930
+ }),
7931
+ verified: boolean("verified").default(false).notNull(),
7932
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7933
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
7934
+ });
7935
+ const mfaChallenges2 = tableCreator("mfa_challenges", {
7936
+ id: uuid("id").defaultRandom().primaryKey(),
7937
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7938
+ onDelete: "cascade"
7939
+ }),
7940
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7941
+ verifiedAt: timestamp("verified_at"),
7942
+ ipAddress: varchar("ip_address", {
7943
+ length: 45
7944
+ }),
7945
+ expiresAt: timestamp("expires_at").notNull()
7946
+ });
7947
+ const recoveryCodes2 = tableCreator("recovery_codes", {
7948
+ id: uuid("id").defaultRandom().primaryKey(),
7949
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7950
+ onDelete: "cascade"
7951
+ }),
7952
+ codeHash: varchar("code_hash", {
7953
+ length: 255
7954
+ }).notNull(),
7955
+ usedAt: timestamp("used_at"),
7956
+ createdAt: timestamp("created_at").defaultNow().notNull()
7957
+ });
7627
7958
  return {
7628
- rolesSchema,
7629
7959
  usersSchema: usersSchema2,
7630
7960
  users: users2,
7631
- roles: roles2,
7632
- userRoles: userRoles2,
7633
7961
  refreshTokens: refreshTokens2,
7634
7962
  passwordResetTokens: passwordResetTokens2,
7635
7963
  appConfig: appConfig2,
7636
- userIdentities: userIdentities2
7964
+ userIdentities: userIdentities2,
7965
+ mfaFactors: mfaFactors2,
7966
+ mfaChallenges: mfaChallenges2,
7967
+ recoveryCodes: recoveryCodes2
7637
7968
  };
7638
7969
  }
7639
- const defaultAuthSchema = createAuthSchema("rebase", "rebase");
7640
- const rebaseSchema = defaultAuthSchema.rolesSchema;
7970
+ const defaultAuthSchema = createAuthSchema("rebase");
7641
7971
  const usersSchema = defaultAuthSchema.usersSchema;
7642
7972
  const users = defaultAuthSchema.users;
7643
- const roles = defaultAuthSchema.roles;
7644
- const userRoles = defaultAuthSchema.userRoles;
7645
7973
  const refreshTokens = defaultAuthSchema.refreshTokens;
7646
7974
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7647
7975
  const appConfig = defaultAuthSchema.appConfig;
7648
7976
  const userIdentities = defaultAuthSchema.userIdentities;
7977
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7978
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7979
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7649
7980
  const usersRelations = relations(users, ({
7650
7981
  many
7651
7982
  }) => ({
7652
- userRoles: many(userRoles),
7653
7983
  refreshTokens: many(refreshTokens),
7654
7984
  passwordResetTokens: many(passwordResetTokens),
7655
- userIdentities: many(userIdentities)
7985
+ userIdentities: many(userIdentities),
7986
+ mfaFactors: many(mfaFactors),
7987
+ recoveryCodes: many(recoveryCodes)
7656
7988
  }));
7657
- const rolesRelations = relations(roles, ({
7658
- many
7989
+ const refreshTokensRelations = relations(refreshTokens, ({
7990
+ one
7659
7991
  }) => ({
7660
- userRoles: many(userRoles)
7992
+ user: one(users, {
7993
+ fields: [refreshTokens.userId],
7994
+ references: [users.id]
7995
+ })
7661
7996
  }));
7662
- const userRolesRelations = relations(userRoles, ({
7997
+ const passwordResetTokensRelations = relations(passwordResetTokens, ({
7663
7998
  one
7664
7999
  }) => ({
7665
8000
  user: one(users, {
7666
- fields: [userRoles.userId],
8001
+ fields: [passwordResetTokens.userId],
7667
8002
  references: [users.id]
7668
- }),
7669
- role: one(roles, {
7670
- fields: [userRoles.roleId],
7671
- references: [roles.id]
7672
8003
  })
7673
8004
  }));
7674
- const refreshTokensRelations = relations(refreshTokens, ({
8005
+ const userIdentitiesRelations = relations(userIdentities, ({
7675
8006
  one
7676
8007
  }) => ({
7677
8008
  user: one(users, {
7678
- fields: [refreshTokens.userId],
8009
+ fields: [userIdentities.userId],
7679
8010
  references: [users.id]
7680
8011
  })
7681
8012
  }));
7682
- const passwordResetTokensRelations = relations(passwordResetTokens, ({
7683
- one
8013
+ const mfaFactorsRelations = relations(mfaFactors, ({
8014
+ one,
8015
+ many
7684
8016
  }) => ({
7685
8017
  user: one(users, {
7686
- fields: [passwordResetTokens.userId],
8018
+ fields: [mfaFactors.userId],
7687
8019
  references: [users.id]
8020
+ }),
8021
+ challenges: many(mfaChallenges)
8022
+ }));
8023
+ const mfaChallengesRelations = relations(mfaChallenges, ({
8024
+ one
8025
+ }) => ({
8026
+ factor: one(mfaFactors, {
8027
+ fields: [mfaChallenges.factorId],
8028
+ references: [mfaFactors.id]
7688
8029
  })
7689
8030
  }));
7690
- const userIdentitiesRelations = relations(userIdentities, ({
8031
+ const recoveryCodesRelations = relations(recoveryCodes, ({
7691
8032
  one
7692
8033
  }) => ({
7693
8034
  user: one(users, {
7694
- fields: [userIdentities.userId],
8035
+ fields: [recoveryCodes.userId],
7695
8036
  references: [users.id]
7696
8037
  })
7697
8038
  }));
@@ -7751,6 +8092,8 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
7751
8092
  columnDefinition = `${enumName}("${colName}")`;
7752
8093
  } else if ("isId" in stringProp && stringProp.isId === "uuid") {
7753
8094
  columnDefinition = `uuid("${colName}")`;
8095
+ } else if (stringProp.columnType === "uuid") {
8096
+ columnDefinition = `uuid("${colName}")`;
7754
8097
  } else if (stringProp.columnType === "text") {
7755
8098
  columnDefinition = `text("${colName}")`;
7756
8099
  } else if (stringProp.columnType === "char") {
@@ -7818,11 +8161,38 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
7818
8161
  }
7819
8162
  break;
7820
8163
  }
7821
- case "map":
8164
+ case "map": {
8165
+ const mapProp = prop;
8166
+ if (mapProp.columnType === "json") {
8167
+ columnDefinition = `json("${colName}")`;
8168
+ } else {
8169
+ columnDefinition = `jsonb("${colName}")`;
8170
+ }
8171
+ break;
8172
+ }
7822
8173
  case "array": {
7823
- const arrayOrMapProp = prop;
7824
- if (arrayOrMapProp.columnType === "json") {
8174
+ const arrayProp = prop;
8175
+ let colType = arrayProp.columnType;
8176
+ if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
8177
+ const ofProp = arrayProp.of;
8178
+ if (ofProp.type === "string") {
8179
+ colType = "text[]";
8180
+ } else if (ofProp.type === "number") {
8181
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
8182
+ } else if (ofProp.type === "boolean") {
8183
+ colType = "boolean[]";
8184
+ }
8185
+ }
8186
+ if (colType === "json") {
7825
8187
  columnDefinition = `json("${colName}")`;
8188
+ } else if (colType === "text[]") {
8189
+ columnDefinition = `text("${colName}").array()`;
8190
+ } else if (colType === "integer[]") {
8191
+ columnDefinition = `integer("${colName}").array()`;
8192
+ } else if (colType === "boolean[]") {
8193
+ columnDefinition = `boolean("${colName}").array()`;
8194
+ } else if (colType === "numeric[]") {
8195
+ columnDefinition = `numeric("${colName}").array()`;
7826
8196
  } else {
7827
8197
  columnDefinition = `jsonb("${colName}")`;
7828
8198
  }
@@ -7906,8 +8276,8 @@ const resolveRawSql = (expression) => {
7906
8276
  const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
7907
8277
  return `sql\`${resolved}\``;
7908
8278
  };
7909
- const wrapWithRoleCheck = (clause, roles2) => {
7910
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8279
+ const wrapWithRoleCheck = (clause, roles) => {
8280
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7911
8281
  const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
7912
8282
  return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
7913
8283
  };
@@ -7960,22 +8330,22 @@ const generatePolicyCode = (collection, rule, index) => {
7960
8330
  };
7961
8331
  const generateSinglePolicyCode = (collection, rule, operation, policyName) => {
7962
8332
  const mode = rule.mode ?? "permissive";
7963
- const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
8333
+ const roles = rule.roles ? [...rule.roles].sort() : void 0;
7964
8334
  const needsUsing = operation !== "insert";
7965
8335
  const needsWithCheck = operation !== "select" && operation !== "delete";
7966
8336
  let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
7967
8337
  let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
7968
- if (roles2 && roles2.length > 0) {
8338
+ if (roles && roles.length > 0) {
7969
8339
  if (usingClause) {
7970
- usingClause = wrapWithRoleCheck(usingClause, roles2);
8340
+ usingClause = wrapWithRoleCheck(usingClause, roles);
7971
8341
  } else if (needsUsing) {
7972
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8342
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7973
8343
  usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
7974
8344
  }
7975
8345
  if (withCheckClause) {
7976
- withCheckClause = wrapWithRoleCheck(withCheckClause, roles2);
8346
+ withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
7977
8347
  } else if (needsWithCheck) {
7978
- const rolesArrayString = `ARRAY[${roles2.map((r) => `'${r}'`).join(",")}]`;
8348
+ const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
7979
8349
  withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
7980
8350
  }
7981
8351
  }
@@ -8298,91 +8668,7 @@ ${tableRelations.join(",\n")}
8298
8668
  schemaContent += tablesExport + enumsExport + relationsExport;
8299
8669
  return schemaContent;
8300
8670
  };
8301
- const defaultUsersCollection = {
8302
- name: "Users",
8303
- singularName: "User",
8304
- slug: "users",
8305
- table: "users",
8306
- schema: "rebase",
8307
- icon: "Users",
8308
- group: "Settings",
8309
- properties: {
8310
- id: {
8311
- name: "ID",
8312
- type: "string",
8313
- isId: "uuid"
8314
- },
8315
- email: {
8316
- name: "Email",
8317
- type: "string",
8318
- validation: {
8319
- required: true,
8320
- unique: true
8321
- }
8322
- },
8323
- password_hash: {
8324
- name: "Password Hash",
8325
- type: "string",
8326
- ui: {
8327
- hideFromCollection: true
8328
- }
8329
- },
8330
- display_name: {
8331
- name: "Display Name",
8332
- type: "string"
8333
- },
8334
- photo_url: {
8335
- name: "Photo URL",
8336
- type: "string"
8337
- },
8338
- email_verified: {
8339
- name: "Email Verified",
8340
- type: "boolean",
8341
- defaultValue: false
8342
- },
8343
- email_verification_token: {
8344
- name: "Email Verification Token",
8345
- type: "string",
8346
- ui: {
8347
- hideFromCollection: true
8348
- }
8349
- },
8350
- email_verification_sent_at: {
8351
- name: "Email Verification Sent At",
8352
- type: "date",
8353
- ui: {
8354
- hideFromCollection: true
8355
- }
8356
- },
8357
- metadata: {
8358
- name: "Metadata",
8359
- type: "map",
8360
- defaultValue: {},
8361
- ui: {
8362
- hideFromCollection: true
8363
- }
8364
- },
8365
- created_at: {
8366
- name: "Created At",
8367
- type: "date",
8368
- autoValue: "on_create",
8369
- ui: {
8370
- readOnly: true,
8371
- hideFromCollection: true
8372
- }
8373
- },
8374
- updated_at: {
8375
- name: "Updated At",
8376
- type: "date",
8377
- autoValue: "on_update",
8378
- ui: {
8379
- readOnly: true,
8380
- hideFromCollection: true
8381
- }
8382
- }
8383
- }
8384
- };
8385
- const formatTerminalText = (text, options = {}) => {
8671
+ const formatTerminalText = (text2, options = {}) => {
8386
8672
  let codes = "";
8387
8673
  if (options.bold) codes += "\x1B[1m";
8388
8674
  if (options.backgroundColor) {
@@ -8409,7 +8695,7 @@ const formatTerminalText = (text, options = {}) => {
8409
8695
  };
8410
8696
  codes += textColors[options.textColor];
8411
8697
  }
8412
- return `${codes}${text}\x1B[0m`;
8698
+ return `${codes}${text2}\x1B[0m`;
8413
8699
  };
8414
8700
  const runGeneration = async (collectionsFilePath, outputPath) => {
8415
8701
  try {
@@ -8447,10 +8733,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
8447
8733
  if (!collections || !Array.isArray(collections)) {
8448
8734
  collections = [];
8449
8735
  }
8450
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8451
- if (!hasUsersCollection) {
8452
- collections.push(defaultUsersCollection);
8453
- }
8736
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8454
8737
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8455
8738
  const schemaContent = await generateSchema(collections);
8456
8739
  if (outputPath) {
@@ -8511,6 +8794,13 @@ class RealtimeService extends EventEmitter {
8511
8794
  this.entityService = new EntityService(db, registry);
8512
8795
  }
8513
8796
  clients = /* @__PURE__ */ new Map();
8797
+ // Broadcast channels: channel name → set of client IDs
8798
+ channels = /* @__PURE__ */ new Map();
8799
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8800
+ presence = /* @__PURE__ */ new Map();
8801
+ presenceInterval;
8802
+ static PRESENCE_TIMEOUT_MS = 3e4;
8803
+ // 30s
8514
8804
  entityService;
8515
8805
  // Enhanced subscriptions storage with full request parameters
8516
8806
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8637,8 +8927,19 @@ class RealtimeService extends EventEmitter {
8637
8927
  }
8638
8928
  }
8639
8929
  }
8930
+ for (const [channel, members] of this.channels.entries()) {
8931
+ if (members.has(clientId)) {
8932
+ members.delete(clientId);
8933
+ this.removePresence(clientId, channel);
8934
+ if (members.size === 0) this.channels.delete(channel);
8935
+ }
8936
+ }
8937
+ for (const [channel] of this.presence) {
8938
+ this.removePresence(clientId, channel);
8939
+ }
8640
8940
  }
8641
8941
  async handleMessage(clientId, message, authContext) {
8942
+ const payload = message.payload;
8642
8943
  switch (message.type) {
8643
8944
  case "subscribe_collection":
8644
8945
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8649,6 +8950,25 @@ class RealtimeService extends EventEmitter {
8649
8950
  case "unsubscribe":
8650
8951
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8651
8952
  break;
8953
+ case "join_channel":
8954
+ this.joinChannel(clientId, payload?.channel);
8955
+ break;
8956
+ case "leave_channel":
8957
+ this.leaveChannel(clientId, payload?.channel);
8958
+ break;
8959
+ case "broadcast":
8960
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8961
+ break;
8962
+ case "presence_track":
8963
+ this.joinChannel(clientId, payload?.channel);
8964
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8965
+ break;
8966
+ case "presence_untrack":
8967
+ this.removePresence(clientId, payload?.channel);
8968
+ break;
8969
+ case "presence_state":
8970
+ this.sendPresenceState(clientId, payload?.channel);
8971
+ break;
8652
8972
  default:
8653
8973
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8654
8974
  }
@@ -9106,6 +9426,132 @@ class RealtimeService extends EventEmitter {
9106
9426
  return parentPaths;
9107
9427
  }
9108
9428
  // =============================================================================
9429
+ // Broadcast Channels
9430
+ // =============================================================================
9431
+ /** Join a broadcast channel */
9432
+ joinChannel(clientId, channel) {
9433
+ if (!this.channels.has(channel)) {
9434
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9435
+ }
9436
+ this.channels.get(channel).add(clientId);
9437
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9438
+ }
9439
+ /** Leave a broadcast channel */
9440
+ leaveChannel(clientId, channel) {
9441
+ const members = this.channels.get(channel);
9442
+ if (members) {
9443
+ members.delete(clientId);
9444
+ if (members.size === 0) this.channels.delete(channel);
9445
+ }
9446
+ this.removePresence(clientId, channel);
9447
+ }
9448
+ /** Broadcast a message to all clients in a channel except sender */
9449
+ broadcastToChannel(clientId, channel, event, payload) {
9450
+ const members = this.channels.get(channel);
9451
+ if (!members) return;
9452
+ const message = JSON.stringify({
9453
+ type: "broadcast",
9454
+ channel,
9455
+ event,
9456
+ payload
9457
+ });
9458
+ for (const memberId of members) {
9459
+ if (memberId === clientId) continue;
9460
+ const ws = this.clients.get(memberId);
9461
+ if (ws && ws.readyState === WebSocket.OPEN) {
9462
+ ws.send(message);
9463
+ }
9464
+ }
9465
+ }
9466
+ // =============================================================================
9467
+ // Presence
9468
+ // =============================================================================
9469
+ /** Track presence in a channel */
9470
+ trackPresence(clientId, channel, state) {
9471
+ if (!this.presence.has(channel)) {
9472
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9473
+ }
9474
+ const channelPresence = this.presence.get(channel);
9475
+ channelPresence.set(clientId, {
9476
+ state,
9477
+ lastSeen: Date.now()
9478
+ });
9479
+ this.broadcastPresenceDiff(channel, {
9480
+ [clientId]: state
9481
+ }, {});
9482
+ this.ensurePresenceCleanup();
9483
+ }
9484
+ /** Remove presence from a channel */
9485
+ removePresence(clientId, channel) {
9486
+ const channelPresence = this.presence.get(channel);
9487
+ if (!channelPresence) return;
9488
+ const entry = channelPresence.get(clientId);
9489
+ if (entry) {
9490
+ channelPresence.delete(clientId);
9491
+ this.broadcastPresenceDiff(channel, {}, {
9492
+ [clientId]: entry.state
9493
+ });
9494
+ }
9495
+ if (channelPresence.size === 0) {
9496
+ this.presence.delete(channel);
9497
+ }
9498
+ }
9499
+ /** Send full presence state to a specific client */
9500
+ sendPresenceState(clientId, channel) {
9501
+ const channelPresence = this.presence.get(channel);
9502
+ const presences = {};
9503
+ if (channelPresence) {
9504
+ for (const [id, {
9505
+ state
9506
+ }] of channelPresence) {
9507
+ presences[id] = state;
9508
+ }
9509
+ }
9510
+ const ws = this.clients.get(clientId);
9511
+ if (ws && ws.readyState === WebSocket.OPEN) {
9512
+ ws.send(JSON.stringify({
9513
+ type: "presence_state",
9514
+ channel,
9515
+ presences
9516
+ }));
9517
+ }
9518
+ }
9519
+ /** Broadcast presence diff (joins/leaves) to channel */
9520
+ broadcastPresenceDiff(channel, joins, leaves) {
9521
+ const members = this.channels.get(channel);
9522
+ if (!members) return;
9523
+ const message = JSON.stringify({
9524
+ type: "presence_diff",
9525
+ channel,
9526
+ joins,
9527
+ leaves
9528
+ });
9529
+ for (const memberId of members) {
9530
+ const ws = this.clients.get(memberId);
9531
+ if (ws && ws.readyState === WebSocket.OPEN) {
9532
+ ws.send(message);
9533
+ }
9534
+ }
9535
+ }
9536
+ /** Periodic cleanup for stale presences */
9537
+ ensurePresenceCleanup() {
9538
+ if (this.presenceInterval) return;
9539
+ this.presenceInterval = setInterval(() => {
9540
+ const now = Date.now();
9541
+ for (const [channel, channelPresence] of this.presence) {
9542
+ for (const [clientId, entry] of channelPresence) {
9543
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9544
+ this.removePresence(clientId, channel);
9545
+ }
9546
+ }
9547
+ }
9548
+ if (this.presence.size === 0 && this.presenceInterval) {
9549
+ clearInterval(this.presenceInterval);
9550
+ this.presenceInterval = void 0;
9551
+ }
9552
+ }, 1e4);
9553
+ }
9554
+ // =============================================================================
9109
9555
  // Lifecycle / Cleanup
9110
9556
  // =============================================================================
9111
9557
  /**
@@ -9126,6 +9572,12 @@ class RealtimeService extends EventEmitter {
9126
9572
  }
9127
9573
  this._subscriptions.clear();
9128
9574
  this.subscriptionCallbacks.clear();
9575
+ this.channels.clear();
9576
+ this.presence.clear();
9577
+ if (this.presenceInterval) {
9578
+ clearInterval(this.presenceInterval);
9579
+ this.presenceInterval = void 0;
9580
+ }
9129
9581
  await this.stopListening();
9130
9582
  this.clients.clear();
9131
9583
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9627,15 +10079,15 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
9627
10079
  wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
9628
10080
  const delegate = await getScopedDelegate();
9629
10081
  const admin = delegate.admin;
9630
- let roles2 = [];
10082
+ let roles = [];
9631
10083
  if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
9632
- roles2 = await admin.fetchAvailableRoles();
10084
+ roles = await admin.fetchAvailableRoles();
9633
10085
  }
9634
- wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
10086
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);
9635
10087
  const response = {
9636
10088
  type: "FETCH_ROLES_SUCCESS",
9637
10089
  payload: {
9638
- roles: roles2
10090
+ roles
9639
10091
  },
9640
10092
  requestId
9641
10093
  };
@@ -9772,8 +10224,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
9772
10224
  break;
9773
10225
  case "subscribe_collection":
9774
10226
  case "subscribe_entity":
9775
- case "unsubscribe": {
9776
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10227
+ case "unsubscribe":
10228
+ case "join_channel":
10229
+ case "leave_channel":
10230
+ case "broadcast":
10231
+ case "presence_track":
10232
+ case "presence_untrack":
10233
+ case "presence_state": {
10234
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9777
10235
  const session = clientSessions.get(clientId);
9778
10236
  const authContext = session?.user ? {
9779
10237
  userId: session.user.userId,
@@ -9883,50 +10341,8 @@ class PostgresCollectionRegistry extends CollectionRegistry {
9883
10341
  return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
9884
10342
  }
9885
10343
  }
9886
- const DEFAULT_ROLES = [{
9887
- id: "admin",
9888
- name: "Admin",
9889
- is_admin: true,
9890
- default_permissions: {
9891
- read: true,
9892
- create: true,
9893
- edit: true,
9894
- delete: true
9895
- },
9896
- config: {
9897
- createCollections: true,
9898
- editCollections: "all",
9899
- deleteCollections: "all"
9900
- }
9901
- }, {
9902
- id: "editor",
9903
- name: "Editor",
9904
- is_admin: false,
9905
- default_permissions: {
9906
- read: true,
9907
- create: true,
9908
- edit: true,
9909
- delete: true
9910
- },
9911
- config: {
9912
- createCollections: true,
9913
- editCollections: "own",
9914
- deleteCollections: "own"
9915
- }
9916
- }, {
9917
- id: "viewer",
9918
- name: "Viewer",
9919
- is_admin: false,
9920
- default_permissions: {
9921
- read: true,
9922
- create: false,
9923
- edit: false,
9924
- delete: false
9925
- },
9926
- config: null
9927
- }];
9928
10344
  async function ensureAuthTablesExist(db, registry) {
9929
- console.log("🔍 Checking auth tables...");
10345
+ logger.info("🔍 Checking auth tables...");
9930
10346
  try {
9931
10347
  let usersTableName = '"users"';
9932
10348
  let userIdType = "TEXT";
@@ -9953,26 +10369,15 @@ async function ensureAuthTablesExist(db, registry) {
9953
10369
  }
9954
10370
  }
9955
10371
  }
9956
- let rolesSchema = "rebase";
9957
- if (registry) {
9958
- const rolesTable = registry.getTable("roles");
9959
- if (rolesTable) {
9960
- rolesSchema = getTableConfig(rolesTable).schema || "public";
9961
- }
9962
- }
9963
10372
  if (usersSchema2 !== "public") {
9964
10373
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema2)}`);
9965
10374
  }
9966
- if (rolesSchema !== "public" && rolesSchema !== usersSchema2) {
9967
- await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
9968
- }
9969
10375
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
9970
- const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
9971
- const rolesTableName = `"${rolesSchema}"."roles"`;
9972
- const userRolesTableName = `"${rolesSchema}"."user_roles"`;
9973
- const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
9974
- const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
9975
- const appConfigTableName = `"${rolesSchema}"."app_config"`;
10376
+ const authSchema = usersSchema2 === "public" ? "rebase" : usersSchema2;
10377
+ const userIdentitiesTable = `"${authSchema}"."user_identities"`;
10378
+ const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
10379
+ const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
10380
+ const appConfigTableName = `"${authSchema}"."app_config"`;
9976
10381
  await db.execute(sql`
9977
10382
  CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
9978
10383
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -9989,28 +10394,6 @@ async function ensureAuthTablesExist(db, registry) {
9989
10394
  CREATE INDEX IF NOT EXISTS idx_user_identities_user
9990
10395
  ON ${sql.raw(userIdentitiesTable)}(user_id)
9991
10396
  `);
9992
- await db.execute(sql`
9993
- CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
9994
- id TEXT PRIMARY KEY,
9995
- name TEXT NOT NULL,
9996
- is_admin BOOLEAN DEFAULT FALSE,
9997
- default_permissions JSONB,
9998
- collection_permissions JSONB,
9999
- config JSONB,
10000
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10001
- )
10002
- `);
10003
- await db.execute(sql`
10004
- CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
10005
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10006
- role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
10007
- PRIMARY KEY (user_id, role_id)
10008
- )
10009
- `);
10010
- await db.execute(sql`
10011
- CREATE INDEX IF NOT EXISTS idx_user_roles_user
10012
- ON ${sql.raw(userRolesTableName)}(user_id)
10013
- `);
10014
10397
  await db.execute(sql`
10015
10398
  CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
10016
10399
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
@@ -10078,35 +10461,93 @@ async function ensureAuthTablesExist(db, registry) {
10078
10461
  $$ LANGUAGE sql STABLE
10079
10462
  `);
10080
10463
  });
10081
- await seedDefaultRoles(db, rolesTableName);
10082
- console.log("✅ Auth tables ready");
10083
- } catch (error) {
10084
- console.error("❌ Failed to create auth tables:", error);
10085
- console.warn("⚠️ Continuing without creating auth tables.");
10086
- }
10087
- }
10088
- async function seedDefaultRoles(db, rolesTableName) {
10089
- const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
10090
- const count2 = parseInt(result.rows[0]?.count || "0", 10);
10091
- if (count2 > 0) {
10092
- console.log(`📋 Found ${count2} existing roles`);
10093
- return;
10094
- }
10095
- console.log("🌱 Seeding default roles...");
10096
- for (const role of DEFAULT_ROLES) {
10097
10464
  await db.execute(sql`
10098
- INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10099
- VALUES (
10100
- ${role.id},
10101
- ${role.name},
10102
- ${role.is_admin},
10103
- ${JSON.stringify(role.default_permissions)}::jsonb,
10104
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10465
+ ALTER TABLE ${sql.raw(usersTableName)}
10466
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10467
+ `);
10468
+ await db.execute(sql`
10469
+ ALTER TABLE ${sql.raw(usersTableName)}
10470
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
10471
+ `);
10472
+ try {
10473
+ const legacyCheck = await db.execute(sql`
10474
+ SELECT EXISTS (
10475
+ SELECT 1 FROM information_schema.tables
10476
+ WHERE table_schema = 'rebase' AND table_name = 'user_roles'
10477
+ ) AS has_user_roles
10478
+ `);
10479
+ const hasLegacyTables = legacyCheck.rows[0].has_user_roles;
10480
+ if (hasLegacyTables) {
10481
+ logger.info("🔄 Migrating roles from legacy user_roles table...");
10482
+ await db.execute(sql`
10483
+ UPDATE ${sql.raw(usersTableName)} u
10484
+ SET roles = COALESCE((
10485
+ SELECT array_agg(ur.role_id)
10486
+ FROM "rebase"."user_roles" ur
10487
+ WHERE ur.user_id = u.id
10488
+ ), '{}')
10489
+ WHERE u.roles = '{}' OR u.roles IS NULL
10490
+ `);
10491
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
10492
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
10493
+ logger.info("✅ Legacy roles tables migrated and dropped");
10494
+ }
10495
+ } catch (migrationError) {
10496
+ logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
10497
+ }
10498
+ const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
10499
+ const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
10500
+ const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
10501
+ await db.execute(sql`
10502
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
10503
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10504
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10505
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10506
+ secret_encrypted TEXT NOT NULL,
10507
+ friendly_name TEXT,
10508
+ verified BOOLEAN DEFAULT FALSE,
10509
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10510
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10511
+ )
10512
+ `);
10513
+ await db.execute(sql`
10514
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10515
+ ON ${sql.raw(mfaFactorsTableName)}(user_id)
10516
+ `);
10517
+ await db.execute(sql`
10518
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
10519
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10520
+ factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10521
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10522
+ verified_at TIMESTAMP WITH TIME ZONE,
10523
+ ip_address TEXT,
10524
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10105
10525
  )
10106
- ON CONFLICT (id) DO NOTHING
10107
10526
  `);
10527
+ await db.execute(sql`
10528
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10529
+ ON ${sql.raw(mfaChallengesTableName)}(factor_id)
10530
+ `);
10531
+ await db.execute(sql`
10532
+ CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
10533
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10534
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10535
+ code_hash TEXT NOT NULL,
10536
+ used_at TIMESTAMP WITH TIME ZONE,
10537
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10538
+ )
10539
+ `);
10540
+ await db.execute(sql`
10541
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10542
+ ON ${sql.raw(recoveryCodesTableName)}(user_id)
10543
+ `);
10544
+ logger.info("✅ Auth tables ready");
10545
+ } catch (error) {
10546
+ logger.error("❌ Failed to create auth tables", {
10547
+ error
10548
+ });
10549
+ logger.warn("⚠️ Continuing without creating auth tables.");
10108
10550
  }
10109
- console.log("✅ Default roles created: admin, editor, viewer");
10110
10551
  }
10111
10552
  function getColumnKey(table, ...keys2) {
10112
10553
  if (!table) return void 0;
@@ -10127,24 +10568,18 @@ function getColumn(table, ...keys2) {
10127
10568
  class UserService {
10128
10569
  constructor(db, tableOrTables) {
10129
10570
  this.db = db;
10130
- if (tableOrTables && (tableOrTables.users || tableOrTables.roles)) {
10571
+ if (tableOrTables && tableOrTables.users) {
10131
10572
  const tables = tableOrTables;
10132
10573
  this.usersTable = tables.users || users;
10133
10574
  this.userIdentitiesTable = tables.userIdentities || userIdentities;
10134
- this.userRolesTable = tables.userRoles || userRoles;
10135
- this.rolesTable = tables.roles || roles;
10136
10575
  } else {
10137
10576
  const table = tableOrTables;
10138
10577
  this.usersTable = table || users;
10139
10578
  this.userIdentitiesTable = userIdentities;
10140
- this.userRolesTable = userRoles;
10141
- this.rolesTable = roles;
10142
10579
  }
10143
10580
  }
10144
10581
  usersTable;
10145
10582
  userIdentitiesTable;
10146
- userRolesTable;
10147
- rolesTable;
10148
10583
  getQualifiedUsersTableName() {
10149
10584
  const name = getTableName$1(this.usersTable);
10150
10585
  const schema = getTableConfig(this.usersTable).schema || "public";
@@ -10160,12 +10595,13 @@ class UserService {
10160
10595
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10161
10596
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10162
10597
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10598
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10163
10599
  const createdAt = row.created_at ?? row.createdAt;
10164
10600
  const updatedAt = row.updated_at ?? row.updatedAt;
10165
10601
  const metadata = {
10166
10602
  ...row.metadata || {}
10167
10603
  };
10168
- 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"]);
10604
+ 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"]);
10169
10605
  for (const [key, val] of Object.entries(row)) {
10170
10606
  if (!knownKeys.has(key)) {
10171
10607
  const camelKey = camelCase(key);
@@ -10181,6 +10617,7 @@ class UserService {
10181
10617
  emailVerified,
10182
10618
  emailVerificationToken,
10183
10619
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10620
+ isAnonymous,
10184
10621
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10185
10622
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10186
10623
  metadata
@@ -10197,6 +10634,7 @@ class UserService {
10197
10634
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10198
10635
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10199
10636
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10637
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10200
10638
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10201
10639
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10202
10640
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10208,6 +10646,7 @@ class UserService {
10208
10646
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10209
10647
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10210
10648
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10649
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10211
10650
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10212
10651
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10213
10652
  const metadata = {
@@ -10216,7 +10655,7 @@ class UserService {
10216
10655
  const remainingMetadata = {};
10217
10656
  for (const [key, val] of Object.entries(metadata)) {
10218
10657
  const tableColKey = getColumnKey(this.usersTable, key);
10219
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10658
+ 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) {
10220
10659
  payload[tableColKey] = val;
10221
10660
  } else {
10222
10661
  remainingMetadata[key] = val;
@@ -10313,19 +10752,18 @@ class UserService {
10313
10752
  const displayNameCol = getColumn(this.usersTable, "displayName", "display_name");
10314
10753
  const displayNameColumn = displayNameCol ? displayNameCol.name : "display_name";
10315
10754
  const idCol = getColumn(this.usersTable, "id");
10316
- const idColumn = idCol ? idCol.name : "id";
10755
+ idCol ? idCol.name : "id";
10317
10756
  const usersTableName = this.getQualifiedUsersTableName();
10318
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
10319
10757
  const conditions = [];
10320
10758
  if (roleId) {
10321
- conditions.push(sql`EXISTS (SELECT 1 FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
10759
+ conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
10322
10760
  }
10323
10761
  if (search) {
10324
10762
  const pattern = `%${search}%`;
10325
10763
  conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);
10326
10764
  }
10327
10765
  const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
10328
- const orderByClause = roleId ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (SELECT count(*) FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)}) DESC, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
10766
+ const orderByClause = roleId ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
10329
10767
  const countResult = await this.db.execute(sql`
10330
10768
  SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
10331
10769
  ${whereClause}
@@ -10399,55 +10837,57 @@ class UserService {
10399
10837
  return row ? this.mapRowToUser(row) : null;
10400
10838
  }
10401
10839
  /**
10402
- * Get roles for a user from database
10840
+ * Get roles for a user from database (inline TEXT[] column)
10403
10841
  */
10404
10842
  async getUserRoles(userId) {
10405
- const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
10843
+ const usersTableName = this.getQualifiedUsersTableName();
10406
10844
  const result = await this.db.execute(sql`
10407
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10408
- FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
10409
- INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10410
- WHERE ur.user_id = ${userId}
10845
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
10411
10846
  `);
10412
- return result.rows.map((row) => ({
10413
- id: row.id,
10414
- name: row.name,
10415
- isAdmin: row.is_admin,
10416
- defaultPermissions: row.default_permissions,
10417
- collectionPermissions: row.collection_permissions,
10418
- config: row.config
10847
+ if (result.rows.length === 0) return [];
10848
+ const row = result.rows[0];
10849
+ const roleIds = row.roles ?? [];
10850
+ return roleIds.map((id) => ({
10851
+ id,
10852
+ name: id,
10853
+ isAdmin: id === "admin",
10854
+ defaultPermissions: null,
10855
+ collectionPermissions: null
10419
10856
  }));
10420
10857
  }
10421
10858
  /**
10422
10859
  * Get role IDs for a user
10423
10860
  */
10424
10861
  async getUserRoleIds(userId) {
10425
- const roles2 = await this.getUserRoles(userId);
10426
- return roles2.map((r) => r.id);
10862
+ const usersTableName = this.getQualifiedUsersTableName();
10863
+ const result = await this.db.execute(sql`
10864
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
10865
+ `);
10866
+ if (result.rows.length === 0) return [];
10867
+ const row = result.rows[0];
10868
+ return row.roles ?? [];
10427
10869
  }
10428
10870
  /**
10429
- * Set roles for a user
10871
+ * Set roles for a user (replaces existing roles)
10430
10872
  */
10431
10873
  async setUserRoles(userId, roleIds) {
10432
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
10433
- await this.db.execute(sql`DELETE FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
10434
- for (const roleId of roleIds) {
10435
- await this.db.execute(sql`
10436
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
10437
- VALUES (${userId}, ${roleId})
10438
- ON CONFLICT DO NOTHING
10439
- `);
10440
- }
10874
+ const usersTableName = this.getQualifiedUsersTableName();
10875
+ const rolesArray = `{${roleIds.join(",")}}`;
10876
+ await this.db.execute(sql`
10877
+ UPDATE ${sql.raw(usersTableName)}
10878
+ SET roles = ${rolesArray}::text[], updated_at = NOW()
10879
+ WHERE id = ${userId}
10880
+ `);
10441
10881
  }
10442
10882
  /**
10443
- * Assign a specific role to new user
10883
+ * Assign a specific role to new user (appends if not present)
10444
10884
  */
10445
10885
  async assignDefaultRole(userId, roleId) {
10446
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
10886
+ const usersTableName = this.getQualifiedUsersTableName();
10447
10887
  await this.db.execute(sql`
10448
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
10449
- VALUES (${userId}, ${roleId})
10450
- ON CONFLICT DO NOTHING
10888
+ UPDATE ${sql.raw(usersTableName)}
10889
+ SET roles = array_append(roles, ${roleId}), updated_at = NOW()
10890
+ WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
10451
10891
  `);
10452
10892
  }
10453
10893
  /**
@@ -10456,107 +10896,13 @@ class UserService {
10456
10896
  async getUserWithRoles(userId) {
10457
10897
  const user = await this.getUserById(userId);
10458
10898
  if (!user) return null;
10459
- const roles2 = await this.getUserRoles(userId);
10899
+ const roles = await this.getUserRoles(userId);
10460
10900
  return {
10461
10901
  user,
10462
- roles: roles2
10902
+ roles
10463
10903
  };
10464
10904
  }
10465
10905
  }
10466
- class RoleService {
10467
- constructor(db, tableOrTables) {
10468
- this.db = db;
10469
- if (tableOrTables && (tableOrTables.roles || tableOrTables.users)) {
10470
- this.rolesTable = tableOrTables.roles || roles;
10471
- } else {
10472
- this.rolesTable = tableOrTables || roles;
10473
- }
10474
- }
10475
- rolesTable;
10476
- getQualifiedRolesTableName() {
10477
- const name = getTableName$1(this.rolesTable);
10478
- const schema = getTableConfig(this.rolesTable).schema || "public";
10479
- return `"${schema}"."${name}"`;
10480
- }
10481
- async getRoleById(id) {
10482
- const tableName = this.getQualifiedRolesTableName();
10483
- const result = await this.db.execute(sql`
10484
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10485
- FROM ${sql.raw(tableName)}
10486
- WHERE id = ${id}
10487
- `);
10488
- if (result.rows.length === 0) return null;
10489
- const row = result.rows[0];
10490
- return {
10491
- id: row.id,
10492
- name: row.name,
10493
- isAdmin: row.is_admin,
10494
- defaultPermissions: row.default_permissions,
10495
- collectionPermissions: row.collection_permissions,
10496
- config: row.config
10497
- };
10498
- }
10499
- async listRoles() {
10500
- const tableName = this.getQualifiedRolesTableName();
10501
- const result = await this.db.execute(sql`
10502
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10503
- FROM ${sql.raw(tableName)}
10504
- ORDER BY name
10505
- `);
10506
- return result.rows.map((row) => ({
10507
- id: row.id,
10508
- name: row.name,
10509
- isAdmin: row.is_admin,
10510
- defaultPermissions: row.default_permissions,
10511
- collectionPermissions: row.collection_permissions,
10512
- config: row.config
10513
- }));
10514
- }
10515
- async createRole(data) {
10516
- const tableName = this.getQualifiedRolesTableName();
10517
- const result = await this.db.execute(sql`
10518
- INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10519
- VALUES (
10520
- ${data.id},
10521
- ${data.name},
10522
- ${data.isAdmin ?? false},
10523
- ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10524
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10525
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10526
- )
10527
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10528
- `);
10529
- const row = result.rows[0];
10530
- return {
10531
- id: row.id,
10532
- name: row.name,
10533
- isAdmin: row.is_admin,
10534
- defaultPermissions: row.default_permissions,
10535
- collectionPermissions: row.collection_permissions,
10536
- config: row.config
10537
- };
10538
- }
10539
- async updateRole(id, data) {
10540
- const existing = await this.getRoleById(id);
10541
- if (!existing) return null;
10542
- const tableName = this.getQualifiedRolesTableName();
10543
- await this.db.execute(sql`
10544
- UPDATE ${sql.raw(tableName)}
10545
- SET
10546
- name = ${data.name ?? existing.name},
10547
- is_admin = ${data.isAdmin ?? existing.isAdmin},
10548
- default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10549
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10550
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10551
- WHERE id = ${id}
10552
- `);
10553
- return this.getRoleById(id);
10554
- }
10555
- async deleteRole(id) {
10556
- const tableName = this.getQualifiedRolesTableName();
10557
- await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
10558
- }
10559
- }
10560
10906
  class RefreshTokenService {
10561
10907
  constructor(db, tableOrTables) {
10562
10908
  this.db = db;
@@ -10751,11 +11097,9 @@ class PostgresAuthRepository {
10751
11097
  constructor(db, tableOrTables) {
10752
11098
  this.db = db;
10753
11099
  this.userService = new UserService(db, tableOrTables);
10754
- this.roleService = new RoleService(db, tableOrTables);
10755
11100
  this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
10756
11101
  }
10757
11102
  userService;
10758
- roleService;
10759
11103
  tokenRepository;
10760
11104
  // User operations (delegate to UserService)
10761
11105
  async createUser(data) {
@@ -10815,26 +11159,56 @@ class PostgresAuthRepository {
10815
11159
  async getUserWithRoles(userId) {
10816
11160
  return this.userService.getUserWithRoles(userId);
10817
11161
  }
10818
- // Role operations (delegate to RoleService)
11162
+ // Role operations (roles are inline on users, synthesized from string IDs)
10819
11163
  async getRoleById(id) {
10820
- return this.roleService.getRoleById(id);
11164
+ return {
11165
+ id,
11166
+ name: id,
11167
+ isAdmin: id === "admin",
11168
+ defaultPermissions: null,
11169
+ collectionPermissions: null
11170
+ };
10821
11171
  }
10822
11172
  async listRoles() {
10823
- return this.roleService.listRoles();
10824
- }
10825
- async createRole(data) {
10826
- return this.roleService.createRole({
10827
- ...data,
10828
- defaultPermissions: data.defaultPermissions ?? null,
10829
- collectionPermissions: data.collectionPermissions ?? null,
10830
- config: data.config ?? null
10831
- });
11173
+ return [{
11174
+ id: "admin",
11175
+ name: "Admin",
11176
+ isAdmin: true,
11177
+ defaultPermissions: null,
11178
+ collectionPermissions: null
11179
+ }, {
11180
+ id: "editor",
11181
+ name: "Editor",
11182
+ isAdmin: false,
11183
+ defaultPermissions: null,
11184
+ collectionPermissions: null
11185
+ }, {
11186
+ id: "viewer",
11187
+ name: "Viewer",
11188
+ isAdmin: false,
11189
+ defaultPermissions: null,
11190
+ collectionPermissions: null
11191
+ }];
11192
+ }
11193
+ async createRole(_data) {
11194
+ return {
11195
+ id: _data.id,
11196
+ name: _data.name,
11197
+ isAdmin: _data.isAdmin ?? false,
11198
+ defaultPermissions: _data.defaultPermissions ?? null,
11199
+ collectionPermissions: _data.collectionPermissions ?? null
11200
+ };
10832
11201
  }
10833
11202
  async updateRole(id, data) {
10834
- return this.roleService.updateRole(id, data);
11203
+ return {
11204
+ id,
11205
+ name: data.name ?? id,
11206
+ isAdmin: data.isAdmin ?? id === "admin",
11207
+ defaultPermissions: data.defaultPermissions ?? null,
11208
+ collectionPermissions: data.collectionPermissions ?? null
11209
+ };
10835
11210
  }
10836
- async deleteRole(id) {
10837
- await this.roleService.deleteRole(id);
11211
+ async deleteRole(_id) {
10838
11212
  }
10839
11213
  // Token operations (delegate to PostgresTokenRepository)
10840
11214
  async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
@@ -10870,6 +11244,219 @@ class PostgresAuthRepository {
10870
11244
  async deleteExpiredTokens() {
10871
11245
  await this.tokenRepository.deleteExpiredTokens();
10872
11246
  }
11247
+ // MFA operations (delegate to MfaService)
11248
+ _mfaService = null;
11249
+ getMfaService() {
11250
+ if (!this._mfaService) {
11251
+ this._mfaService = new MfaService(this.db);
11252
+ }
11253
+ return this._mfaService;
11254
+ }
11255
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11256
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11257
+ }
11258
+ async getMfaFactors(userId) {
11259
+ return this.getMfaService().getMfaFactors(userId);
11260
+ }
11261
+ async getMfaFactorById(factorId) {
11262
+ return this.getMfaService().getMfaFactorById(factorId);
11263
+ }
11264
+ async verifyMfaFactor(factorId) {
11265
+ return this.getMfaService().verifyMfaFactor(factorId);
11266
+ }
11267
+ async deleteMfaFactor(factorId, userId) {
11268
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11269
+ }
11270
+ async createMfaChallenge(factorId, ipAddress) {
11271
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11272
+ }
11273
+ async getMfaChallengeById(challengeId) {
11274
+ return this.getMfaService().getMfaChallengeById(challengeId);
11275
+ }
11276
+ async verifyMfaChallenge(challengeId) {
11277
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11278
+ }
11279
+ async createRecoveryCodes(userId, codeHashes) {
11280
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11281
+ }
11282
+ async useRecoveryCode(userId, codeHash) {
11283
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11284
+ }
11285
+ async getUnusedRecoveryCodeCount(userId) {
11286
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11287
+ }
11288
+ async deleteAllRecoveryCodes(userId) {
11289
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11290
+ }
11291
+ async hasVerifiedMfaFactors(userId) {
11292
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11293
+ }
11294
+ }
11295
+ class MfaService {
11296
+ constructor(db, schemaName = "rebase") {
11297
+ this.db = db;
11298
+ this.schemaName = schemaName;
11299
+ }
11300
+ qualify(tableName) {
11301
+ return `"${this.schemaName}"."${tableName}"`;
11302
+ }
11303
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11304
+ const tableName = this.qualify("mfa_factors");
11305
+ const result = await this.db.execute(sql`
11306
+ INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11307
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11308
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11309
+ `);
11310
+ const row = result.rows[0];
11311
+ return {
11312
+ id: row.id,
11313
+ userId: row.user_id,
11314
+ factorType: row.factor_type,
11315
+ friendlyName: row.friendly_name ?? void 0,
11316
+ verified: row.verified,
11317
+ createdAt: new Date(row.created_at),
11318
+ updatedAt: new Date(row.updated_at)
11319
+ };
11320
+ }
11321
+ async getMfaFactors(userId) {
11322
+ const tableName = this.qualify("mfa_factors");
11323
+ const result = await this.db.execute(sql`
11324
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11325
+ FROM ${sql.raw(tableName)}
11326
+ WHERE user_id = ${userId}
11327
+ ORDER BY created_at
11328
+ `);
11329
+ return result.rows.map((row) => ({
11330
+ id: row.id,
11331
+ userId: row.user_id,
11332
+ factorType: row.factor_type,
11333
+ friendlyName: row.friendly_name ?? void 0,
11334
+ verified: row.verified,
11335
+ createdAt: new Date(row.created_at),
11336
+ updatedAt: new Date(row.updated_at)
11337
+ }));
11338
+ }
11339
+ async getMfaFactorById(factorId) {
11340
+ const tableName = this.qualify("mfa_factors");
11341
+ const result = await this.db.execute(sql`
11342
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11343
+ FROM ${sql.raw(tableName)}
11344
+ WHERE id = ${factorId}
11345
+ `);
11346
+ if (result.rows.length === 0) return null;
11347
+ const row = result.rows[0];
11348
+ return {
11349
+ id: row.id,
11350
+ userId: row.user_id,
11351
+ factorType: row.factor_type,
11352
+ secretEncrypted: row.secret_encrypted,
11353
+ friendlyName: row.friendly_name ?? void 0,
11354
+ verified: row.verified,
11355
+ createdAt: new Date(row.created_at),
11356
+ updatedAt: new Date(row.updated_at)
11357
+ };
11358
+ }
11359
+ async verifyMfaFactor(factorId) {
11360
+ const tableName = this.qualify("mfa_factors");
11361
+ await this.db.execute(sql`
11362
+ UPDATE ${sql.raw(tableName)}
11363
+ SET verified = TRUE, updated_at = NOW()
11364
+ WHERE id = ${factorId}
11365
+ `);
11366
+ }
11367
+ async deleteMfaFactor(factorId, userId) {
11368
+ const tableName = this.qualify("mfa_factors");
11369
+ await this.db.execute(sql`
11370
+ DELETE FROM ${sql.raw(tableName)}
11371
+ WHERE id = ${factorId} AND user_id = ${userId}
11372
+ `);
11373
+ }
11374
+ async createMfaChallenge(factorId, ipAddress) {
11375
+ const tableName = this.qualify("mfa_challenges");
11376
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11377
+ const result = await this.db.execute(sql`
11378
+ INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
11379
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11380
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11381
+ `);
11382
+ const row = result.rows[0];
11383
+ return {
11384
+ id: row.id,
11385
+ factorId: row.factor_id,
11386
+ createdAt: new Date(row.created_at),
11387
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11388
+ ipAddress: row.ip_address ?? void 0
11389
+ };
11390
+ }
11391
+ async getMfaChallengeById(challengeId) {
11392
+ const tableName = this.qualify("mfa_challenges");
11393
+ const result = await this.db.execute(sql`
11394
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11395
+ FROM ${sql.raw(tableName)}
11396
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11397
+ `);
11398
+ if (result.rows.length === 0) return null;
11399
+ const row = result.rows[0];
11400
+ return {
11401
+ id: row.id,
11402
+ factorId: row.factor_id,
11403
+ createdAt: new Date(row.created_at),
11404
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11405
+ ipAddress: row.ip_address ?? void 0
11406
+ };
11407
+ }
11408
+ async verifyMfaChallenge(challengeId) {
11409
+ const tableName = this.qualify("mfa_challenges");
11410
+ await this.db.execute(sql`
11411
+ UPDATE ${sql.raw(tableName)}
11412
+ SET verified_at = NOW()
11413
+ WHERE id = ${challengeId}
11414
+ `);
11415
+ }
11416
+ async createRecoveryCodes(userId, codeHashes) {
11417
+ const tableName = this.qualify("recovery_codes");
11418
+ await this.db.execute(sql`
11419
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11420
+ `);
11421
+ for (const hash of codeHashes) {
11422
+ await this.db.execute(sql`
11423
+ INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
11424
+ VALUES (${userId}, ${hash})
11425
+ `);
11426
+ }
11427
+ }
11428
+ async useRecoveryCode(userId, codeHash) {
11429
+ const tableName = this.qualify("recovery_codes");
11430
+ const result = await this.db.execute(sql`
11431
+ UPDATE ${sql.raw(tableName)}
11432
+ SET used_at = NOW()
11433
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11434
+ RETURNING id
11435
+ `);
11436
+ return result.rows.length > 0;
11437
+ }
11438
+ async getUnusedRecoveryCodeCount(userId) {
11439
+ const tableName = this.qualify("recovery_codes");
11440
+ const result = await this.db.execute(sql`
11441
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11442
+ WHERE user_id = ${userId} AND used_at IS NULL
11443
+ `);
11444
+ return result.rows[0].count;
11445
+ }
11446
+ async deleteAllRecoveryCodes(userId) {
11447
+ const tableName = this.qualify("recovery_codes");
11448
+ await this.db.execute(sql`
11449
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11450
+ `);
11451
+ }
11452
+ async hasVerifiedMfaFactors(userId) {
11453
+ const tableName = this.qualify("mfa_factors");
11454
+ const result = await this.db.execute(sql`
11455
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11456
+ WHERE user_id = ${userId} AND verified = TRUE
11457
+ `);
11458
+ return result.rows[0].count > 0;
11459
+ }
10873
11460
  }
10874
11461
  const DEFAULT_RETENTION = {
10875
11462
  maxEntries: 200,
@@ -11080,7 +11667,7 @@ function createPostgresBootstrapper(pgConfig) {
11080
11667
  const registry = new PostgresCollectionRegistry();
11081
11668
  if (collections) {
11082
11669
  registry.registerMultiple(collections);
11083
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11670
+ logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11084
11671
  }
11085
11672
  if (pgConfig.schema?.tables) {
11086
11673
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -11106,10 +11693,28 @@ function createPostgresBootstrapper(pgConfig) {
11106
11693
  try {
11107
11694
  await schemaAwareDb.execute(sql`SELECT 1`);
11108
11695
  } catch (err) {
11109
- console.error("❌ Failed to connect to PostgreSQL:", err);
11110
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11696
+ logger.error("❌ Failed to connect to PostgreSQL", {
11697
+ error: err
11698
+ });
11699
+ logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11111
11700
  }
11112
11701
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11702
+ let readDb;
11703
+ const readUrl = process.env.DATABASE_READ_URL;
11704
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11705
+ try {
11706
+ const {
11707
+ createReadReplicaConnection: createReadReplicaConnection2
11708
+ } = await Promise.resolve().then(() => connection);
11709
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11710
+ readDb = readResources.db;
11711
+ logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11712
+ } catch (err) {
11713
+ logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11714
+ error: err
11715
+ });
11716
+ }
11717
+ }
11113
11718
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
11114
11719
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
11115
11720
  realtimeService.setDataDriver(driver);
@@ -11117,18 +11722,24 @@ function createPostgresBootstrapper(pgConfig) {
11117
11722
  try {
11118
11723
  await driver.branchService.ensureBranchMetadataTable();
11119
11724
  } catch (err) {
11120
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11725
+ logger.warn("⚠️ Could not initialize branch metadata table", {
11726
+ error: err
11727
+ });
11121
11728
  }
11122
11729
  }
11123
- if (pgConfig.connectionString) {
11730
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11731
+ if (directUrl) {
11124
11732
  try {
11125
- await realtimeService.startListening(pgConfig.connectionString);
11733
+ await realtimeService.startListening(directUrl);
11126
11734
  } catch (err) {
11127
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11735
+ logger.warn("⚠️ Cross-instance realtime could not be started", {
11736
+ error: err
11737
+ });
11128
11738
  }
11129
11739
  }
11130
11740
  const internals = {
11131
11741
  db: schemaAwareDb,
11742
+ readDb,
11132
11743
  registry,
11133
11744
  realtimeService,
11134
11745
  driver,
@@ -11153,28 +11764,19 @@ function createPostgresBootstrapper(pgConfig) {
11153
11764
  emailService = createEmailService(authConfig.email);
11154
11765
  }
11155
11766
  const customUsersTable = registry?.getTable("users");
11156
- const customRolesTable = registry?.getTable("roles");
11157
11767
  let usersSchemaName = "rebase";
11158
- let rolesSchemaName = "rebase";
11159
11768
  if (customUsersTable) {
11160
11769
  usersSchemaName = getTableConfig(customUsersTable).schema || "public";
11161
11770
  }
11162
- if (customRolesTable) {
11163
- rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
11164
- }
11165
- const authTables = createAuthSchema(rolesSchemaName, usersSchemaName);
11771
+ const authTables = createAuthSchema(usersSchemaName);
11166
11772
  if (customUsersTable) {
11167
11773
  authTables.users = customUsersTable;
11168
11774
  }
11169
- if (customRolesTable) {
11170
- authTables.roles = customRolesTable;
11171
- }
11172
11775
  const userService = new UserService(db, authTables);
11173
- const roleService = new RoleService(db, authTables);
11174
11776
  const authRepository = new PostgresAuthRepository(db, authTables);
11175
11777
  return {
11176
11778
  userService,
11177
- roleService,
11779
+ roleService: userService,
11178
11780
  emailService,
11179
11781
  authRepository
11180
11782
  };
@@ -11266,22 +11868,25 @@ export {
11266
11868
  RealtimeService,
11267
11869
  appConfig,
11268
11870
  createAuthSchema,
11871
+ createDirectDatabaseConnection,
11269
11872
  createPostgresAdapter,
11270
11873
  createPostgresBootstrapper,
11271
11874
  createPostgresDatabaseConnection,
11272
11875
  createPostgresWebSocket,
11876
+ createReadReplicaConnection,
11273
11877
  generateSchema,
11878
+ mfaChallenges,
11879
+ mfaChallengesRelations,
11880
+ mfaFactors,
11881
+ mfaFactorsRelations,
11274
11882
  passwordResetTokens,
11275
11883
  passwordResetTokensRelations,
11276
- rebaseSchema,
11884
+ recoveryCodes,
11885
+ recoveryCodesRelations,
11277
11886
  refreshTokens,
11278
11887
  refreshTokensRelations,
11279
- roles,
11280
- rolesRelations,
11281
11888
  userIdentities,
11282
11889
  userIdentitiesRelations,
11283
- userRoles,
11284
- userRolesRelations,
11285
11890
  users,
11286
11891
  usersRelations,
11287
11892
  usersSchema