@rebasepro/server-postgresql 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +844 -160
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +842 -158
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +4 -2
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- package/vite.config.ts +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -59,6 +59,70 @@
|
|
|
59
59
|
connectionString
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
63
|
+
const opts = {
|
|
64
|
+
...DEFAULT_POOL,
|
|
65
|
+
max: 5,
|
|
66
|
+
...poolConfig
|
|
67
|
+
};
|
|
68
|
+
const pgPoolConfig = {
|
|
69
|
+
connectionString,
|
|
70
|
+
max: opts.max,
|
|
71
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
72
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
73
|
+
query_timeout: opts.queryTimeout,
|
|
74
|
+
statement_timeout: opts.statementTimeout,
|
|
75
|
+
keepAlive: opts.keepAlive,
|
|
76
|
+
keepAliveInitialDelayMillis: 0
|
|
77
|
+
};
|
|
78
|
+
const pool = new pg.Pool(pgPoolConfig);
|
|
79
|
+
pool.on("error", (err) => {
|
|
80
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
81
|
+
});
|
|
82
|
+
const db = schema ? nodePostgres.drizzle(pool, {
|
|
83
|
+
schema
|
|
84
|
+
}) : nodePostgres.drizzle(pool);
|
|
85
|
+
return {
|
|
86
|
+
db,
|
|
87
|
+
pool,
|
|
88
|
+
connectionString
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
92
|
+
const opts = {
|
|
93
|
+
...DEFAULT_POOL,
|
|
94
|
+
max: 10,
|
|
95
|
+
...poolConfig
|
|
96
|
+
};
|
|
97
|
+
const pgPoolConfig = {
|
|
98
|
+
connectionString,
|
|
99
|
+
max: opts.max,
|
|
100
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
101
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
102
|
+
query_timeout: opts.queryTimeout,
|
|
103
|
+
statement_timeout: opts.statementTimeout,
|
|
104
|
+
keepAlive: opts.keepAlive,
|
|
105
|
+
keepAliveInitialDelayMillis: 0
|
|
106
|
+
};
|
|
107
|
+
const pool = new pg.Pool(pgPoolConfig);
|
|
108
|
+
pool.on("error", (err) => {
|
|
109
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
110
|
+
});
|
|
111
|
+
const db = schema ? nodePostgres.drizzle(pool, {
|
|
112
|
+
schema
|
|
113
|
+
}) : nodePostgres.drizzle(pool);
|
|
114
|
+
return {
|
|
115
|
+
db,
|
|
116
|
+
pool,
|
|
117
|
+
connectionString
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
121
|
+
__proto__: null,
|
|
122
|
+
createDirectDatabaseConnection,
|
|
123
|
+
createPostgresDatabaseConnection,
|
|
124
|
+
createReadReplicaConnection
|
|
125
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
62
126
|
class Vector {
|
|
63
127
|
value;
|
|
64
128
|
constructor(value) {
|
|
@@ -978,6 +1042,9 @@
|
|
|
978
1042
|
return output;
|
|
979
1043
|
}
|
|
980
1044
|
for (const key in source) {
|
|
1045
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
981
1048
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
982
1049
|
const sourceValue = source[key];
|
|
983
1050
|
const outputValue = output[key];
|
|
@@ -2652,6 +2719,90 @@
|
|
|
2652
2719
|
};
|
|
2653
2720
|
}
|
|
2654
2721
|
}
|
|
2722
|
+
const defaultUsersCollection = {
|
|
2723
|
+
name: "Users",
|
|
2724
|
+
singularName: "User",
|
|
2725
|
+
slug: "users",
|
|
2726
|
+
table: "users",
|
|
2727
|
+
schema: "rebase",
|
|
2728
|
+
icon: "Users",
|
|
2729
|
+
group: "Settings",
|
|
2730
|
+
properties: {
|
|
2731
|
+
id: {
|
|
2732
|
+
name: "ID",
|
|
2733
|
+
type: "string",
|
|
2734
|
+
isId: "uuid"
|
|
2735
|
+
},
|
|
2736
|
+
email: {
|
|
2737
|
+
name: "Email",
|
|
2738
|
+
type: "string",
|
|
2739
|
+
validation: {
|
|
2740
|
+
required: true,
|
|
2741
|
+
unique: true
|
|
2742
|
+
}
|
|
2743
|
+
},
|
|
2744
|
+
password_hash: {
|
|
2745
|
+
name: "Password Hash",
|
|
2746
|
+
type: "string",
|
|
2747
|
+
ui: {
|
|
2748
|
+
hideFromCollection: true
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2751
|
+
display_name: {
|
|
2752
|
+
name: "Display Name",
|
|
2753
|
+
type: "string"
|
|
2754
|
+
},
|
|
2755
|
+
photo_url: {
|
|
2756
|
+
name: "Photo URL",
|
|
2757
|
+
type: "string"
|
|
2758
|
+
},
|
|
2759
|
+
email_verified: {
|
|
2760
|
+
name: "Email Verified",
|
|
2761
|
+
type: "boolean",
|
|
2762
|
+
defaultValue: false
|
|
2763
|
+
},
|
|
2764
|
+
email_verification_token: {
|
|
2765
|
+
name: "Email Verification Token",
|
|
2766
|
+
type: "string",
|
|
2767
|
+
ui: {
|
|
2768
|
+
hideFromCollection: true
|
|
2769
|
+
}
|
|
2770
|
+
},
|
|
2771
|
+
email_verification_sent_at: {
|
|
2772
|
+
name: "Email Verification Sent At",
|
|
2773
|
+
type: "date",
|
|
2774
|
+
ui: {
|
|
2775
|
+
hideFromCollection: true
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
metadata: {
|
|
2779
|
+
name: "Metadata",
|
|
2780
|
+
type: "map",
|
|
2781
|
+
defaultValue: {},
|
|
2782
|
+
ui: {
|
|
2783
|
+
hideFromCollection: true
|
|
2784
|
+
}
|
|
2785
|
+
},
|
|
2786
|
+
created_at: {
|
|
2787
|
+
name: "Created At",
|
|
2788
|
+
type: "date",
|
|
2789
|
+
autoValue: "on_create",
|
|
2790
|
+
ui: {
|
|
2791
|
+
readOnly: true,
|
|
2792
|
+
hideFromCollection: true
|
|
2793
|
+
}
|
|
2794
|
+
},
|
|
2795
|
+
updated_at: {
|
|
2796
|
+
name: "Updated At",
|
|
2797
|
+
type: "date",
|
|
2798
|
+
autoValue: "on_update",
|
|
2799
|
+
ui: {
|
|
2800
|
+
readOnly: true,
|
|
2801
|
+
hideFromCollection: true
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2655
2806
|
function mapOperator(op) {
|
|
2656
2807
|
switch (op) {
|
|
2657
2808
|
case "==":
|
|
@@ -2992,7 +3143,13 @@
|
|
|
2992
3143
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
2993
3144
|
if (!filterParam) continue;
|
|
2994
3145
|
const [op, value] = filterParam;
|
|
2995
|
-
|
|
3146
|
+
let fieldColumn = table[field];
|
|
3147
|
+
if (!fieldColumn) {
|
|
3148
|
+
const relationKey = `${field}_id`;
|
|
3149
|
+
if (relationKey in table) {
|
|
3150
|
+
fieldColumn = table[relationKey];
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
2996
3153
|
if (!fieldColumn) {
|
|
2997
3154
|
console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
2998
3155
|
continue;
|
|
@@ -3034,6 +3191,17 @@
|
|
|
3034
3191
|
return null;
|
|
3035
3192
|
case "array-contains":
|
|
3036
3193
|
return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
|
|
3194
|
+
case "array-contains-any":
|
|
3195
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3196
|
+
const textValues = value.map((v) => String(v));
|
|
3197
|
+
return drizzleOrm.sql`${column} ?| array[${drizzleOrm.sql.join(textValues.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)}]`;
|
|
3198
|
+
}
|
|
3199
|
+
return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
|
|
3200
|
+
case "not-in":
|
|
3201
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3202
|
+
return drizzleOrm.sql`${column} NOT IN (${drizzleOrm.sql.join(value.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)})`;
|
|
3203
|
+
}
|
|
3204
|
+
return null;
|
|
3037
3205
|
default:
|
|
3038
3206
|
console.warn(`Unsupported filter operation: ${op}`);
|
|
3039
3207
|
return null;
|
|
@@ -3549,6 +3717,40 @@
|
|
|
3549
3717
|
return null;
|
|
3550
3718
|
}
|
|
3551
3719
|
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Build vector similarity search expressions for pgvector.
|
|
3722
|
+
*
|
|
3723
|
+
* Returns:
|
|
3724
|
+
* - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
|
|
3725
|
+
* - `filter`: optional WHERE clause for distance threshold
|
|
3726
|
+
* - `distanceSelect`: SQL expression for selecting the distance as `_distance`
|
|
3727
|
+
*/
|
|
3728
|
+
static buildVectorSearchConditions(table, vectorSearch) {
|
|
3729
|
+
const column = table[vectorSearch.property];
|
|
3730
|
+
if (!column) {
|
|
3731
|
+
throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
3732
|
+
}
|
|
3733
|
+
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
3734
|
+
const distanceFn = vectorSearch.distance || "cosine";
|
|
3735
|
+
let operator;
|
|
3736
|
+
switch (distanceFn) {
|
|
3737
|
+
case "cosine":
|
|
3738
|
+
operator = "<=>";
|
|
3739
|
+
break;
|
|
3740
|
+
case "l2":
|
|
3741
|
+
operator = "<->";
|
|
3742
|
+
break;
|
|
3743
|
+
case "inner_product":
|
|
3744
|
+
operator = "<#>";
|
|
3745
|
+
break;
|
|
3746
|
+
}
|
|
3747
|
+
const distanceExpr = drizzleOrm.sql`${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}`;
|
|
3748
|
+
return {
|
|
3749
|
+
orderBy: distanceExpr,
|
|
3750
|
+
filter: vectorSearch.threshold != null ? drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
|
|
3751
|
+
distanceSelect: drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)})`
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3552
3754
|
}
|
|
3553
3755
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
3554
3756
|
function getColumnMeta(col) {
|
|
@@ -5494,7 +5696,7 @@
|
|
|
5494
5696
|
const qb = this.getQueryBuilder(tableName);
|
|
5495
5697
|
const withConfig = this.buildWithConfig(collection);
|
|
5496
5698
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
5497
|
-
if (qb && !options.searchString && !hasRelations) {
|
|
5699
|
+
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
|
|
5498
5700
|
try {
|
|
5499
5701
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5500
5702
|
const results2 = await qb.findMany(queryOpts);
|
|
@@ -5508,7 +5710,14 @@
|
|
|
5508
5710
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
5509
5711
|
}
|
|
5510
5712
|
}
|
|
5511
|
-
let
|
|
5713
|
+
let vectorMeta;
|
|
5714
|
+
if (options.vectorSearch) {
|
|
5715
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
5716
|
+
}
|
|
5717
|
+
let query = vectorMeta ? this.db.select({
|
|
5718
|
+
table_row: table,
|
|
5719
|
+
_distance: vectorMeta.distanceSelect
|
|
5720
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5512
5721
|
const allConditions = [];
|
|
5513
5722
|
if (options.searchString) {
|
|
5514
5723
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5519,12 +5728,17 @@
|
|
|
5519
5728
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5520
5729
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5521
5730
|
}
|
|
5731
|
+
if (vectorMeta?.filter) {
|
|
5732
|
+
allConditions.push(vectorMeta.filter);
|
|
5733
|
+
}
|
|
5522
5734
|
if (allConditions.length > 0) {
|
|
5523
5735
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5524
5736
|
if (finalCondition) query = query.where(finalCondition);
|
|
5525
5737
|
}
|
|
5526
5738
|
const orderExpressions = [];
|
|
5527
|
-
if (
|
|
5739
|
+
if (vectorMeta) {
|
|
5740
|
+
orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
|
|
5741
|
+
} else if (options.orderBy) {
|
|
5528
5742
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5529
5743
|
if (orderByField) {
|
|
5530
5744
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
@@ -5540,10 +5754,14 @@
|
|
|
5540
5754
|
if (finalCondition) query = query.where(finalCondition);
|
|
5541
5755
|
}
|
|
5542
5756
|
}
|
|
5543
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5757
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5544
5758
|
if (limitValue) query = query.limit(limitValue);
|
|
5545
5759
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5546
|
-
const
|
|
5760
|
+
const rawResults = await query;
|
|
5761
|
+
const results = vectorMeta ? rawResults.map((r) => ({
|
|
5762
|
+
...r.table_row,
|
|
5763
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
5764
|
+
})) : rawResults;
|
|
5547
5765
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5548
5766
|
}
|
|
5549
5767
|
/**
|
|
@@ -5765,7 +5983,7 @@
|
|
|
5765
5983
|
const idField = table[idInfo.fieldName];
|
|
5766
5984
|
const tableName = drizzleOrm.getTableName(table);
|
|
5767
5985
|
const qb = this.getQueryBuilder(tableName);
|
|
5768
|
-
if (qb && !options.searchString) {
|
|
5986
|
+
if (qb && !options.searchString && !options.vectorSearch) {
|
|
5769
5987
|
try {
|
|
5770
5988
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5771
5989
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
@@ -5911,7 +6129,14 @@
|
|
|
5911
6129
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
5912
6130
|
const idInfo = idInfoArray[0];
|
|
5913
6131
|
const idField = table[idInfo.fieldName];
|
|
5914
|
-
let
|
|
6132
|
+
let vectorMeta;
|
|
6133
|
+
if (options.vectorSearch) {
|
|
6134
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
6135
|
+
}
|
|
6136
|
+
let query = vectorMeta ? this.db.select({
|
|
6137
|
+
table_row: table,
|
|
6138
|
+
_distance: vectorMeta.distanceSelect
|
|
6139
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5915
6140
|
const allConditions = [];
|
|
5916
6141
|
if (options.searchString) {
|
|
5917
6142
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5922,12 +6147,17 @@
|
|
|
5922
6147
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5923
6148
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5924
6149
|
}
|
|
6150
|
+
if (vectorMeta?.filter) {
|
|
6151
|
+
allConditions.push(vectorMeta.filter);
|
|
6152
|
+
}
|
|
5925
6153
|
if (allConditions.length > 0) {
|
|
5926
6154
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5927
6155
|
if (finalCondition) query = query.where(finalCondition);
|
|
5928
6156
|
}
|
|
5929
6157
|
const orderExpressions = [];
|
|
5930
|
-
if (
|
|
6158
|
+
if (vectorMeta) {
|
|
6159
|
+
orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
|
|
6160
|
+
} else if (options.orderBy) {
|
|
5931
6161
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5932
6162
|
if (orderByField) {
|
|
5933
6163
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
@@ -5935,10 +6165,17 @@
|
|
|
5935
6165
|
}
|
|
5936
6166
|
orderExpressions.push(drizzleOrm.desc(idField));
|
|
5937
6167
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5938
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
6168
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5939
6169
|
if (limitValue) query = query.limit(limitValue);
|
|
5940
6170
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5941
|
-
|
|
6171
|
+
const rawResults = await query;
|
|
6172
|
+
if (vectorMeta) {
|
|
6173
|
+
return rawResults.map((r) => ({
|
|
6174
|
+
...r.table_row,
|
|
6175
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
6176
|
+
}));
|
|
6177
|
+
}
|
|
6178
|
+
return rawResults;
|
|
5942
6179
|
}
|
|
5943
6180
|
/**
|
|
5944
6181
|
* Check if the Drizzle instance has the relational query API available
|
|
@@ -6692,7 +6929,8 @@
|
|
|
6692
6929
|
startAfter,
|
|
6693
6930
|
orderBy,
|
|
6694
6931
|
searchString,
|
|
6695
|
-
order
|
|
6932
|
+
order,
|
|
6933
|
+
vectorSearch
|
|
6696
6934
|
}) {
|
|
6697
6935
|
const entities = await this.entityService.fetchCollection(path2, {
|
|
6698
6936
|
filter,
|
|
@@ -6702,7 +6940,8 @@
|
|
|
6702
6940
|
offset,
|
|
6703
6941
|
startAfter,
|
|
6704
6942
|
databaseId: collection?.databaseId,
|
|
6705
|
-
searchString
|
|
6943
|
+
searchString,
|
|
6944
|
+
vectorSearch
|
|
6706
6945
|
});
|
|
6707
6946
|
const {
|
|
6708
6947
|
collection: resolvedCollection,
|
|
@@ -7545,6 +7784,7 @@
|
|
|
7545
7784
|
length: 255
|
|
7546
7785
|
}),
|
|
7547
7786
|
emailVerificationSentAt: pgCore.timestamp("email_verification_sent_at"),
|
|
7787
|
+
isAnonymous: pgCore.boolean("is_anonymous").default(false).notNull(),
|
|
7548
7788
|
metadata: pgCore.jsonb("metadata").$type().default({}).notNull(),
|
|
7549
7789
|
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7550
7790
|
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
@@ -7559,8 +7799,7 @@
|
|
|
7559
7799
|
}).notNull(),
|
|
7560
7800
|
isAdmin: pgCore.boolean("is_admin").default(false).notNull(),
|
|
7561
7801
|
defaultPermissions: pgCore.jsonb("default_permissions").$type(),
|
|
7562
|
-
collectionPermissions: pgCore.jsonb("collection_permissions").$type()
|
|
7563
|
-
config: pgCore.jsonb("config").$type()
|
|
7802
|
+
collectionPermissions: pgCore.jsonb("collection_permissions").$type()
|
|
7564
7803
|
});
|
|
7565
7804
|
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7566
7805
|
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
@@ -7632,6 +7871,48 @@
|
|
|
7632
7871
|
}, (table) => ({
|
|
7633
7872
|
uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7634
7873
|
}));
|
|
7874
|
+
const mfaFactors2 = rolesTableCreator("mfa_factors", {
|
|
7875
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7876
|
+
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
7877
|
+
onDelete: "cascade"
|
|
7878
|
+
}),
|
|
7879
|
+
factorType: pgCore.varchar("factor_type", {
|
|
7880
|
+
length: 20
|
|
7881
|
+
}).notNull(),
|
|
7882
|
+
// 'totp'
|
|
7883
|
+
secretEncrypted: pgCore.varchar("secret_encrypted", {
|
|
7884
|
+
length: 500
|
|
7885
|
+
}).notNull(),
|
|
7886
|
+
friendlyName: pgCore.varchar("friendly_name", {
|
|
7887
|
+
length: 255
|
|
7888
|
+
}),
|
|
7889
|
+
verified: pgCore.boolean("verified").default(false).notNull(),
|
|
7890
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7891
|
+
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
7892
|
+
});
|
|
7893
|
+
const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
|
|
7894
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7895
|
+
factorId: pgCore.uuid("factor_id").notNull().references(() => mfaFactors2.id, {
|
|
7896
|
+
onDelete: "cascade"
|
|
7897
|
+
}),
|
|
7898
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7899
|
+
verifiedAt: pgCore.timestamp("verified_at"),
|
|
7900
|
+
ipAddress: pgCore.varchar("ip_address", {
|
|
7901
|
+
length: 45
|
|
7902
|
+
}),
|
|
7903
|
+
expiresAt: pgCore.timestamp("expires_at").notNull()
|
|
7904
|
+
});
|
|
7905
|
+
const recoveryCodes2 = rolesTableCreator("recovery_codes", {
|
|
7906
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7907
|
+
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
7908
|
+
onDelete: "cascade"
|
|
7909
|
+
}),
|
|
7910
|
+
codeHash: pgCore.varchar("code_hash", {
|
|
7911
|
+
length: 255
|
|
7912
|
+
}).notNull(),
|
|
7913
|
+
usedAt: pgCore.timestamp("used_at"),
|
|
7914
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
|
|
7915
|
+
});
|
|
7635
7916
|
return {
|
|
7636
7917
|
rolesSchema,
|
|
7637
7918
|
usersSchema: usersSchema2,
|
|
@@ -7641,7 +7922,10 @@
|
|
|
7641
7922
|
refreshTokens: refreshTokens2,
|
|
7642
7923
|
passwordResetTokens: passwordResetTokens2,
|
|
7643
7924
|
appConfig: appConfig2,
|
|
7644
|
-
userIdentities: userIdentities2
|
|
7925
|
+
userIdentities: userIdentities2,
|
|
7926
|
+
mfaFactors: mfaFactors2,
|
|
7927
|
+
mfaChallenges: mfaChallenges2,
|
|
7928
|
+
recoveryCodes: recoveryCodes2
|
|
7645
7929
|
};
|
|
7646
7930
|
}
|
|
7647
7931
|
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
@@ -7654,13 +7938,18 @@
|
|
|
7654
7938
|
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7655
7939
|
const appConfig = defaultAuthSchema.appConfig;
|
|
7656
7940
|
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7941
|
+
const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
7942
|
+
const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
7943
|
+
const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
7657
7944
|
const usersRelations = drizzleOrm.relations(users, ({
|
|
7658
7945
|
many
|
|
7659
7946
|
}) => ({
|
|
7660
7947
|
userRoles: many(userRoles),
|
|
7661
7948
|
refreshTokens: many(refreshTokens),
|
|
7662
7949
|
passwordResetTokens: many(passwordResetTokens),
|
|
7663
|
-
userIdentities: many(userIdentities)
|
|
7950
|
+
userIdentities: many(userIdentities),
|
|
7951
|
+
mfaFactors: many(mfaFactors),
|
|
7952
|
+
recoveryCodes: many(recoveryCodes)
|
|
7664
7953
|
}));
|
|
7665
7954
|
const rolesRelations = drizzleOrm.relations(roles, ({
|
|
7666
7955
|
many
|
|
@@ -7703,6 +7992,32 @@
|
|
|
7703
7992
|
references: [users.id]
|
|
7704
7993
|
})
|
|
7705
7994
|
}));
|
|
7995
|
+
const mfaFactorsRelations = drizzleOrm.relations(mfaFactors, ({
|
|
7996
|
+
one,
|
|
7997
|
+
many
|
|
7998
|
+
}) => ({
|
|
7999
|
+
user: one(users, {
|
|
8000
|
+
fields: [mfaFactors.userId],
|
|
8001
|
+
references: [users.id]
|
|
8002
|
+
}),
|
|
8003
|
+
challenges: many(mfaChallenges)
|
|
8004
|
+
}));
|
|
8005
|
+
const mfaChallengesRelations = drizzleOrm.relations(mfaChallenges, ({
|
|
8006
|
+
one
|
|
8007
|
+
}) => ({
|
|
8008
|
+
factor: one(mfaFactors, {
|
|
8009
|
+
fields: [mfaChallenges.factorId],
|
|
8010
|
+
references: [mfaFactors.id]
|
|
8011
|
+
})
|
|
8012
|
+
}));
|
|
8013
|
+
const recoveryCodesRelations = drizzleOrm.relations(recoveryCodes, ({
|
|
8014
|
+
one
|
|
8015
|
+
}) => ({
|
|
8016
|
+
user: one(users, {
|
|
8017
|
+
fields: [recoveryCodes.userId],
|
|
8018
|
+
references: [users.id]
|
|
8019
|
+
})
|
|
8020
|
+
}));
|
|
7706
8021
|
const resolveColumnName = (propName, prop) => {
|
|
7707
8022
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
7708
8023
|
return prop.columnName;
|
|
@@ -8306,90 +8621,6 @@ ${tableRelations.join(",\n")}
|
|
|
8306
8621
|
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8307
8622
|
return schemaContent;
|
|
8308
8623
|
};
|
|
8309
|
-
const defaultUsersCollection = {
|
|
8310
|
-
name: "Users",
|
|
8311
|
-
singularName: "User",
|
|
8312
|
-
slug: "users",
|
|
8313
|
-
table: "users",
|
|
8314
|
-
schema: "rebase",
|
|
8315
|
-
icon: "Users",
|
|
8316
|
-
group: "Settings",
|
|
8317
|
-
properties: {
|
|
8318
|
-
id: {
|
|
8319
|
-
name: "ID",
|
|
8320
|
-
type: "string",
|
|
8321
|
-
isId: "uuid"
|
|
8322
|
-
},
|
|
8323
|
-
email: {
|
|
8324
|
-
name: "Email",
|
|
8325
|
-
type: "string",
|
|
8326
|
-
validation: {
|
|
8327
|
-
required: true,
|
|
8328
|
-
unique: true
|
|
8329
|
-
}
|
|
8330
|
-
},
|
|
8331
|
-
password_hash: {
|
|
8332
|
-
name: "Password Hash",
|
|
8333
|
-
type: "string",
|
|
8334
|
-
ui: {
|
|
8335
|
-
hideFromCollection: true
|
|
8336
|
-
}
|
|
8337
|
-
},
|
|
8338
|
-
display_name: {
|
|
8339
|
-
name: "Display Name",
|
|
8340
|
-
type: "string"
|
|
8341
|
-
},
|
|
8342
|
-
photo_url: {
|
|
8343
|
-
name: "Photo URL",
|
|
8344
|
-
type: "string"
|
|
8345
|
-
},
|
|
8346
|
-
email_verified: {
|
|
8347
|
-
name: "Email Verified",
|
|
8348
|
-
type: "boolean",
|
|
8349
|
-
defaultValue: false
|
|
8350
|
-
},
|
|
8351
|
-
email_verification_token: {
|
|
8352
|
-
name: "Email Verification Token",
|
|
8353
|
-
type: "string",
|
|
8354
|
-
ui: {
|
|
8355
|
-
hideFromCollection: true
|
|
8356
|
-
}
|
|
8357
|
-
},
|
|
8358
|
-
email_verification_sent_at: {
|
|
8359
|
-
name: "Email Verification Sent At",
|
|
8360
|
-
type: "date",
|
|
8361
|
-
ui: {
|
|
8362
|
-
hideFromCollection: true
|
|
8363
|
-
}
|
|
8364
|
-
},
|
|
8365
|
-
metadata: {
|
|
8366
|
-
name: "Metadata",
|
|
8367
|
-
type: "map",
|
|
8368
|
-
defaultValue: {},
|
|
8369
|
-
ui: {
|
|
8370
|
-
hideFromCollection: true
|
|
8371
|
-
}
|
|
8372
|
-
},
|
|
8373
|
-
created_at: {
|
|
8374
|
-
name: "Created At",
|
|
8375
|
-
type: "date",
|
|
8376
|
-
autoValue: "on_create",
|
|
8377
|
-
ui: {
|
|
8378
|
-
readOnly: true,
|
|
8379
|
-
hideFromCollection: true
|
|
8380
|
-
}
|
|
8381
|
-
},
|
|
8382
|
-
updated_at: {
|
|
8383
|
-
name: "Updated At",
|
|
8384
|
-
type: "date",
|
|
8385
|
-
autoValue: "on_update",
|
|
8386
|
-
ui: {
|
|
8387
|
-
readOnly: true,
|
|
8388
|
-
hideFromCollection: true
|
|
8389
|
-
}
|
|
8390
|
-
}
|
|
8391
|
-
}
|
|
8392
|
-
};
|
|
8393
8624
|
const formatTerminalText = (text, options = {}) => {
|
|
8394
8625
|
let codes = "";
|
|
8395
8626
|
if (options.bold) codes += "\x1B[1m";
|
|
@@ -8455,10 +8686,7 @@ ${tableRelations.join(",\n")}
|
|
|
8455
8686
|
if (!collections || !Array.isArray(collections)) {
|
|
8456
8687
|
collections = [];
|
|
8457
8688
|
}
|
|
8458
|
-
|
|
8459
|
-
if (!hasUsersCollection) {
|
|
8460
|
-
collections.push(defaultUsersCollection);
|
|
8461
|
-
}
|
|
8689
|
+
collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
|
|
8462
8690
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
8463
8691
|
const schemaContent = await generateSchema(collections);
|
|
8464
8692
|
if (outputPath) {
|
|
@@ -8519,6 +8747,13 @@ ${tableRelations.join(",\n")}
|
|
|
8519
8747
|
this.entityService = new EntityService(db, registry);
|
|
8520
8748
|
}
|
|
8521
8749
|
clients = /* @__PURE__ */ new Map();
|
|
8750
|
+
// Broadcast channels: channel name → set of client IDs
|
|
8751
|
+
channels = /* @__PURE__ */ new Map();
|
|
8752
|
+
// Presence: channel → Map<clientId, { state, lastSeen }>
|
|
8753
|
+
presence = /* @__PURE__ */ new Map();
|
|
8754
|
+
presenceInterval;
|
|
8755
|
+
static PRESENCE_TIMEOUT_MS = 3e4;
|
|
8756
|
+
// 30s
|
|
8522
8757
|
entityService;
|
|
8523
8758
|
// Enhanced subscriptions storage with full request parameters
|
|
8524
8759
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -8645,8 +8880,19 @@ ${tableRelations.join(",\n")}
|
|
|
8645
8880
|
}
|
|
8646
8881
|
}
|
|
8647
8882
|
}
|
|
8883
|
+
for (const [channel, members] of this.channels.entries()) {
|
|
8884
|
+
if (members.has(clientId)) {
|
|
8885
|
+
members.delete(clientId);
|
|
8886
|
+
this.removePresence(clientId, channel);
|
|
8887
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
for (const [channel] of this.presence) {
|
|
8891
|
+
this.removePresence(clientId, channel);
|
|
8892
|
+
}
|
|
8648
8893
|
}
|
|
8649
8894
|
async handleMessage(clientId, message, authContext) {
|
|
8895
|
+
const payload = message.payload;
|
|
8650
8896
|
switch (message.type) {
|
|
8651
8897
|
case "subscribe_collection":
|
|
8652
8898
|
await this.handleCollectionSubscription(clientId, message.payload, authContext);
|
|
@@ -8657,6 +8903,25 @@ ${tableRelations.join(",\n")}
|
|
|
8657
8903
|
case "unsubscribe":
|
|
8658
8904
|
await this.handleUnsubscribe(clientId, message.subscriptionId);
|
|
8659
8905
|
break;
|
|
8906
|
+
case "join_channel":
|
|
8907
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8908
|
+
break;
|
|
8909
|
+
case "leave_channel":
|
|
8910
|
+
this.leaveChannel(clientId, payload?.channel);
|
|
8911
|
+
break;
|
|
8912
|
+
case "broadcast":
|
|
8913
|
+
this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
|
|
8914
|
+
break;
|
|
8915
|
+
case "presence_track":
|
|
8916
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8917
|
+
this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
|
|
8918
|
+
break;
|
|
8919
|
+
case "presence_untrack":
|
|
8920
|
+
this.removePresence(clientId, payload?.channel);
|
|
8921
|
+
break;
|
|
8922
|
+
case "presence_state":
|
|
8923
|
+
this.sendPresenceState(clientId, payload?.channel);
|
|
8924
|
+
break;
|
|
8660
8925
|
default:
|
|
8661
8926
|
this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
|
|
8662
8927
|
}
|
|
@@ -9114,6 +9379,132 @@ ${tableRelations.join(",\n")}
|
|
|
9114
9379
|
return parentPaths;
|
|
9115
9380
|
}
|
|
9116
9381
|
// =============================================================================
|
|
9382
|
+
// Broadcast Channels
|
|
9383
|
+
// =============================================================================
|
|
9384
|
+
/** Join a broadcast channel */
|
|
9385
|
+
joinChannel(clientId, channel) {
|
|
9386
|
+
if (!this.channels.has(channel)) {
|
|
9387
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
9388
|
+
}
|
|
9389
|
+
this.channels.get(channel).add(clientId);
|
|
9390
|
+
this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
|
|
9391
|
+
}
|
|
9392
|
+
/** Leave a broadcast channel */
|
|
9393
|
+
leaveChannel(clientId, channel) {
|
|
9394
|
+
const members = this.channels.get(channel);
|
|
9395
|
+
if (members) {
|
|
9396
|
+
members.delete(clientId);
|
|
9397
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
9398
|
+
}
|
|
9399
|
+
this.removePresence(clientId, channel);
|
|
9400
|
+
}
|
|
9401
|
+
/** Broadcast a message to all clients in a channel except sender */
|
|
9402
|
+
broadcastToChannel(clientId, channel, event, payload) {
|
|
9403
|
+
const members = this.channels.get(channel);
|
|
9404
|
+
if (!members) return;
|
|
9405
|
+
const message = JSON.stringify({
|
|
9406
|
+
type: "broadcast",
|
|
9407
|
+
channel,
|
|
9408
|
+
event,
|
|
9409
|
+
payload
|
|
9410
|
+
});
|
|
9411
|
+
for (const memberId of members) {
|
|
9412
|
+
if (memberId === clientId) continue;
|
|
9413
|
+
const ws$1 = this.clients.get(memberId);
|
|
9414
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9415
|
+
ws$1.send(message);
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
// =============================================================================
|
|
9420
|
+
// Presence
|
|
9421
|
+
// =============================================================================
|
|
9422
|
+
/** Track presence in a channel */
|
|
9423
|
+
trackPresence(clientId, channel, state) {
|
|
9424
|
+
if (!this.presence.has(channel)) {
|
|
9425
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
9426
|
+
}
|
|
9427
|
+
const channelPresence = this.presence.get(channel);
|
|
9428
|
+
channelPresence.set(clientId, {
|
|
9429
|
+
state,
|
|
9430
|
+
lastSeen: Date.now()
|
|
9431
|
+
});
|
|
9432
|
+
this.broadcastPresenceDiff(channel, {
|
|
9433
|
+
[clientId]: state
|
|
9434
|
+
}, {});
|
|
9435
|
+
this.ensurePresenceCleanup();
|
|
9436
|
+
}
|
|
9437
|
+
/** Remove presence from a channel */
|
|
9438
|
+
removePresence(clientId, channel) {
|
|
9439
|
+
const channelPresence = this.presence.get(channel);
|
|
9440
|
+
if (!channelPresence) return;
|
|
9441
|
+
const entry = channelPresence.get(clientId);
|
|
9442
|
+
if (entry) {
|
|
9443
|
+
channelPresence.delete(clientId);
|
|
9444
|
+
this.broadcastPresenceDiff(channel, {}, {
|
|
9445
|
+
[clientId]: entry.state
|
|
9446
|
+
});
|
|
9447
|
+
}
|
|
9448
|
+
if (channelPresence.size === 0) {
|
|
9449
|
+
this.presence.delete(channel);
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
/** Send full presence state to a specific client */
|
|
9453
|
+
sendPresenceState(clientId, channel) {
|
|
9454
|
+
const channelPresence = this.presence.get(channel);
|
|
9455
|
+
const presences = {};
|
|
9456
|
+
if (channelPresence) {
|
|
9457
|
+
for (const [id, {
|
|
9458
|
+
state
|
|
9459
|
+
}] of channelPresence) {
|
|
9460
|
+
presences[id] = state;
|
|
9461
|
+
}
|
|
9462
|
+
}
|
|
9463
|
+
const ws$1 = this.clients.get(clientId);
|
|
9464
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9465
|
+
ws$1.send(JSON.stringify({
|
|
9466
|
+
type: "presence_state",
|
|
9467
|
+
channel,
|
|
9468
|
+
presences
|
|
9469
|
+
}));
|
|
9470
|
+
}
|
|
9471
|
+
}
|
|
9472
|
+
/** Broadcast presence diff (joins/leaves) to channel */
|
|
9473
|
+
broadcastPresenceDiff(channel, joins, leaves) {
|
|
9474
|
+
const members = this.channels.get(channel);
|
|
9475
|
+
if (!members) return;
|
|
9476
|
+
const message = JSON.stringify({
|
|
9477
|
+
type: "presence_diff",
|
|
9478
|
+
channel,
|
|
9479
|
+
joins,
|
|
9480
|
+
leaves
|
|
9481
|
+
});
|
|
9482
|
+
for (const memberId of members) {
|
|
9483
|
+
const ws$1 = this.clients.get(memberId);
|
|
9484
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9485
|
+
ws$1.send(message);
|
|
9486
|
+
}
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
/** Periodic cleanup for stale presences */
|
|
9490
|
+
ensurePresenceCleanup() {
|
|
9491
|
+
if (this.presenceInterval) return;
|
|
9492
|
+
this.presenceInterval = setInterval(() => {
|
|
9493
|
+
const now = Date.now();
|
|
9494
|
+
for (const [channel, channelPresence] of this.presence) {
|
|
9495
|
+
for (const [clientId, entry] of channelPresence) {
|
|
9496
|
+
if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
|
|
9497
|
+
this.removePresence(clientId, channel);
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
if (this.presence.size === 0 && this.presenceInterval) {
|
|
9502
|
+
clearInterval(this.presenceInterval);
|
|
9503
|
+
this.presenceInterval = void 0;
|
|
9504
|
+
}
|
|
9505
|
+
}, 1e4);
|
|
9506
|
+
}
|
|
9507
|
+
// =============================================================================
|
|
9117
9508
|
// Lifecycle / Cleanup
|
|
9118
9509
|
// =============================================================================
|
|
9119
9510
|
/**
|
|
@@ -9134,6 +9525,12 @@ ${tableRelations.join(",\n")}
|
|
|
9134
9525
|
}
|
|
9135
9526
|
this._subscriptions.clear();
|
|
9136
9527
|
this.subscriptionCallbacks.clear();
|
|
9528
|
+
this.channels.clear();
|
|
9529
|
+
this.presence.clear();
|
|
9530
|
+
if (this.presenceInterval) {
|
|
9531
|
+
clearInterval(this.presenceInterval);
|
|
9532
|
+
this.presenceInterval = void 0;
|
|
9533
|
+
}
|
|
9137
9534
|
await this.stopListening();
|
|
9138
9535
|
this.clients.clear();
|
|
9139
9536
|
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
@@ -9780,8 +10177,14 @@ ${tableRelations.join(",\n")}
|
|
|
9780
10177
|
break;
|
|
9781
10178
|
case "subscribe_collection":
|
|
9782
10179
|
case "subscribe_entity":
|
|
9783
|
-
case "unsubscribe":
|
|
9784
|
-
|
|
10180
|
+
case "unsubscribe":
|
|
10181
|
+
case "join_channel":
|
|
10182
|
+
case "leave_channel":
|
|
10183
|
+
case "broadcast":
|
|
10184
|
+
case "presence_track":
|
|
10185
|
+
case "presence_untrack":
|
|
10186
|
+
case "presence_state": {
|
|
10187
|
+
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
9785
10188
|
const session = clientSessions.get(clientId);
|
|
9786
10189
|
const authContext = session?.user ? {
|
|
9787
10190
|
userId: session.user.userId,
|
|
@@ -9900,11 +10303,6 @@ ${tableRelations.join(",\n")}
|
|
|
9900
10303
|
create: true,
|
|
9901
10304
|
edit: true,
|
|
9902
10305
|
delete: true
|
|
9903
|
-
},
|
|
9904
|
-
config: {
|
|
9905
|
-
createCollections: true,
|
|
9906
|
-
editCollections: "all",
|
|
9907
|
-
deleteCollections: "all"
|
|
9908
10306
|
}
|
|
9909
10307
|
}, {
|
|
9910
10308
|
id: "editor",
|
|
@@ -9915,11 +10313,6 @@ ${tableRelations.join(",\n")}
|
|
|
9915
10313
|
create: true,
|
|
9916
10314
|
edit: true,
|
|
9917
10315
|
delete: true
|
|
9918
|
-
},
|
|
9919
|
-
config: {
|
|
9920
|
-
createCollections: true,
|
|
9921
|
-
editCollections: "own",
|
|
9922
|
-
deleteCollections: "own"
|
|
9923
10316
|
}
|
|
9924
10317
|
}, {
|
|
9925
10318
|
id: "viewer",
|
|
@@ -9930,11 +10323,10 @@ ${tableRelations.join(",\n")}
|
|
|
9930
10323
|
create: false,
|
|
9931
10324
|
edit: false,
|
|
9932
10325
|
delete: false
|
|
9933
|
-
}
|
|
9934
|
-
config: null
|
|
10326
|
+
}
|
|
9935
10327
|
}];
|
|
9936
10328
|
async function ensureAuthTablesExist(db, registry) {
|
|
9937
|
-
|
|
10329
|
+
serverCore.logger.info("🔍 Checking auth tables...");
|
|
9938
10330
|
try {
|
|
9939
10331
|
let usersTableName = '"users"';
|
|
9940
10332
|
let userIdType = "TEXT";
|
|
@@ -10004,7 +10396,6 @@ ${tableRelations.join(",\n")}
|
|
|
10004
10396
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
10005
10397
|
default_permissions JSONB,
|
|
10006
10398
|
collection_permissions JSONB,
|
|
10007
|
-
config JSONB,
|
|
10008
10399
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10009
10400
|
)
|
|
10010
10401
|
`);
|
|
@@ -10087,34 +10478,85 @@ ${tableRelations.join(",\n")}
|
|
|
10087
10478
|
`);
|
|
10088
10479
|
});
|
|
10089
10480
|
await seedDefaultRoles(db, rolesTableName);
|
|
10090
|
-
|
|
10481
|
+
await db.execute(drizzleOrm.sql`
|
|
10482
|
+
ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
|
|
10483
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
10484
|
+
`);
|
|
10485
|
+
const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
|
|
10486
|
+
const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
|
|
10487
|
+
const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
|
|
10488
|
+
await db.execute(drizzleOrm.sql`
|
|
10489
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaFactorsTableName)} (
|
|
10490
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10491
|
+
user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10492
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
10493
|
+
secret_encrypted TEXT NOT NULL,
|
|
10494
|
+
friendly_name TEXT,
|
|
10495
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
10496
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10497
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10498
|
+
)
|
|
10499
|
+
`);
|
|
10500
|
+
await db.execute(drizzleOrm.sql`
|
|
10501
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
10502
|
+
ON ${drizzleOrm.sql.raw(mfaFactorsTableName)}(user_id)
|
|
10503
|
+
`);
|
|
10504
|
+
await db.execute(drizzleOrm.sql`
|
|
10505
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaChallengesTableName)} (
|
|
10506
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10507
|
+
factor_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
10508
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10509
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
10510
|
+
ip_address TEXT,
|
|
10511
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
10512
|
+
)
|
|
10513
|
+
`);
|
|
10514
|
+
await db.execute(drizzleOrm.sql`
|
|
10515
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
10516
|
+
ON ${drizzleOrm.sql.raw(mfaChallengesTableName)}(factor_id)
|
|
10517
|
+
`);
|
|
10518
|
+
await db.execute(drizzleOrm.sql`
|
|
10519
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(recoveryCodesTableName)} (
|
|
10520
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10521
|
+
user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10522
|
+
code_hash TEXT NOT NULL,
|
|
10523
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
10524
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10525
|
+
)
|
|
10526
|
+
`);
|
|
10527
|
+
await db.execute(drizzleOrm.sql`
|
|
10528
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
10529
|
+
ON ${drizzleOrm.sql.raw(recoveryCodesTableName)}(user_id)
|
|
10530
|
+
`);
|
|
10531
|
+
serverCore.logger.info("✅ Auth tables ready");
|
|
10091
10532
|
} catch (error) {
|
|
10092
|
-
|
|
10093
|
-
|
|
10533
|
+
serverCore.logger.error("❌ Failed to create auth tables", {
|
|
10534
|
+
error
|
|
10535
|
+
});
|
|
10536
|
+
serverCore.logger.warn("⚠️ Continuing without creating auth tables.");
|
|
10094
10537
|
}
|
|
10095
10538
|
}
|
|
10096
10539
|
async function seedDefaultRoles(db, rolesTableName) {
|
|
10097
10540
|
const result = await db.execute(drizzleOrm.sql`SELECT COUNT(*) as count FROM ${drizzleOrm.sql.raw(rolesTableName)}`);
|
|
10098
10541
|
const count = parseInt(result.rows[0]?.count || "0", 10);
|
|
10099
10542
|
if (count > 0) {
|
|
10100
|
-
|
|
10543
|
+
serverCore.logger.info(`📋 Found ${count} existing roles`);
|
|
10101
10544
|
return;
|
|
10102
10545
|
}
|
|
10103
|
-
|
|
10546
|
+
serverCore.logger.info("🌱 Seeding default roles...");
|
|
10104
10547
|
for (const role of DEFAULT_ROLES) {
|
|
10105
10548
|
await db.execute(drizzleOrm.sql`
|
|
10106
|
-
INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions
|
|
10549
|
+
INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
10107
10550
|
VALUES (
|
|
10108
10551
|
${role.id},
|
|
10109
10552
|
${role.name},
|
|
10110
10553
|
${role.is_admin},
|
|
10111
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
10112
|
-
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
10554
|
+
${JSON.stringify(role.default_permissions)}::jsonb
|
|
10113
10555
|
)
|
|
10114
10556
|
ON CONFLICT (id) DO NOTHING
|
|
10115
10557
|
`);
|
|
10116
10558
|
}
|
|
10117
|
-
|
|
10559
|
+
serverCore.logger.info("✅ Default roles created: admin, editor, viewer");
|
|
10118
10560
|
}
|
|
10119
10561
|
function getColumnKey(table, ...keys2) {
|
|
10120
10562
|
if (!table) return void 0;
|
|
@@ -10168,12 +10610,13 @@ ${tableRelations.join(",\n")}
|
|
|
10168
10610
|
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10169
10611
|
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10170
10612
|
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10613
|
+
const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
|
|
10171
10614
|
const createdAt = row.created_at ?? row.createdAt;
|
|
10172
10615
|
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10173
10616
|
const metadata = {
|
|
10174
10617
|
...row.metadata || {}
|
|
10175
10618
|
};
|
|
10176
|
-
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10619
|
+
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10177
10620
|
for (const [key, val] of Object.entries(row)) {
|
|
10178
10621
|
if (!knownKeys.has(key)) {
|
|
10179
10622
|
const camelKey = camelCase(key);
|
|
@@ -10189,6 +10632,7 @@ ${tableRelations.join(",\n")}
|
|
|
10189
10632
|
emailVerified,
|
|
10190
10633
|
emailVerificationToken,
|
|
10191
10634
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10635
|
+
isAnonymous,
|
|
10192
10636
|
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10193
10637
|
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10194
10638
|
metadata
|
|
@@ -10205,6 +10649,7 @@ ${tableRelations.join(",\n")}
|
|
|
10205
10649
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10206
10650
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10207
10651
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10652
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
10208
10653
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10209
10654
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10210
10655
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -10216,6 +10661,7 @@ ${tableRelations.join(",\n")}
|
|
|
10216
10661
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10217
10662
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10218
10663
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10664
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
10219
10665
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10220
10666
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10221
10667
|
const metadata = {
|
|
@@ -10224,7 +10670,7 @@ ${tableRelations.join(",\n")}
|
|
|
10224
10670
|
const remainingMetadata = {};
|
|
10225
10671
|
for (const [key, val] of Object.entries(metadata)) {
|
|
10226
10672
|
const tableColKey = getColumnKey(this.usersTable, key);
|
|
10227
|
-
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10673
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10228
10674
|
payload[tableColKey] = val;
|
|
10229
10675
|
} else {
|
|
10230
10676
|
remainingMetadata[key] = val;
|
|
@@ -10412,7 +10858,7 @@ ${tableRelations.join(",\n")}
|
|
|
10412
10858
|
async getUserRoles(userId) {
|
|
10413
10859
|
const rolesSchema = pgCore.getTableConfig(this.rolesTable).schema || "public";
|
|
10414
10860
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10415
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10861
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10416
10862
|
FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10417
10863
|
INNER JOIN ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
10418
10864
|
WHERE ur.user_id = ${userId}
|
|
@@ -10422,8 +10868,7 @@ ${tableRelations.join(",\n")}
|
|
|
10422
10868
|
name: row.name,
|
|
10423
10869
|
isAdmin: row.is_admin,
|
|
10424
10870
|
defaultPermissions: row.default_permissions,
|
|
10425
|
-
collectionPermissions: row.collection_permissions
|
|
10426
|
-
config: row.config
|
|
10871
|
+
collectionPermissions: row.collection_permissions
|
|
10427
10872
|
}));
|
|
10428
10873
|
}
|
|
10429
10874
|
/**
|
|
@@ -10489,7 +10934,7 @@ ${tableRelations.join(",\n")}
|
|
|
10489
10934
|
async getRoleById(id) {
|
|
10490
10935
|
const tableName = this.getQualifiedRolesTableName();
|
|
10491
10936
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10492
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10937
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10493
10938
|
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
10494
10939
|
WHERE id = ${id}
|
|
10495
10940
|
`);
|
|
@@ -10500,14 +10945,13 @@ ${tableRelations.join(",\n")}
|
|
|
10500
10945
|
name: row.name,
|
|
10501
10946
|
isAdmin: row.is_admin,
|
|
10502
10947
|
defaultPermissions: row.default_permissions,
|
|
10503
|
-
collectionPermissions: row.collection_permissions
|
|
10504
|
-
config: row.config
|
|
10948
|
+
collectionPermissions: row.collection_permissions
|
|
10505
10949
|
};
|
|
10506
10950
|
}
|
|
10507
10951
|
async listRoles() {
|
|
10508
10952
|
const tableName = this.getQualifiedRolesTableName();
|
|
10509
10953
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10510
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10954
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10511
10955
|
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
10512
10956
|
ORDER BY name
|
|
10513
10957
|
`);
|
|
@@ -10516,23 +10960,21 @@ ${tableRelations.join(",\n")}
|
|
|
10516
10960
|
name: row.name,
|
|
10517
10961
|
isAdmin: row.is_admin,
|
|
10518
10962
|
defaultPermissions: row.default_permissions,
|
|
10519
|
-
collectionPermissions: row.collection_permissions
|
|
10520
|
-
config: row.config
|
|
10963
|
+
collectionPermissions: row.collection_permissions
|
|
10521
10964
|
}));
|
|
10522
10965
|
}
|
|
10523
10966
|
async createRole(data) {
|
|
10524
10967
|
const tableName = this.getQualifiedRolesTableName();
|
|
10525
10968
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10526
|
-
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
10969
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
10527
10970
|
VALUES (
|
|
10528
10971
|
${data.id},
|
|
10529
10972
|
${data.name},
|
|
10530
10973
|
${data.isAdmin ?? false},
|
|
10531
10974
|
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
10532
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10533
|
-
${data.config ? JSON.stringify(data.config) : null}::jsonb
|
|
10975
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10534
10976
|
)
|
|
10535
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10977
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10536
10978
|
`);
|
|
10537
10979
|
const row = result.rows[0];
|
|
10538
10980
|
return {
|
|
@@ -10540,8 +10982,7 @@ ${tableRelations.join(",\n")}
|
|
|
10540
10982
|
name: row.name,
|
|
10541
10983
|
isAdmin: row.is_admin,
|
|
10542
10984
|
defaultPermissions: row.default_permissions,
|
|
10543
|
-
collectionPermissions: row.collection_permissions
|
|
10544
|
-
config: row.config
|
|
10985
|
+
collectionPermissions: row.collection_permissions
|
|
10545
10986
|
};
|
|
10546
10987
|
}
|
|
10547
10988
|
async updateRole(id, data) {
|
|
@@ -10554,8 +10995,7 @@ ${tableRelations.join(",\n")}
|
|
|
10554
10995
|
name = ${data.name ?? existing.name},
|
|
10555
10996
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
10556
10997
|
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
10557
|
-
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10558
|
-
config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
|
|
10998
|
+
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10559
10999
|
WHERE id = ${id}
|
|
10560
11000
|
`);
|
|
10561
11001
|
return this.getRoleById(id);
|
|
@@ -10834,8 +11274,7 @@ ${tableRelations.join(",\n")}
|
|
|
10834
11274
|
return this.roleService.createRole({
|
|
10835
11275
|
...data,
|
|
10836
11276
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
10837
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
10838
|
-
config: data.config ?? null
|
|
11277
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
10839
11278
|
});
|
|
10840
11279
|
}
|
|
10841
11280
|
async updateRole(id, data) {
|
|
@@ -10878,6 +11317,219 @@ ${tableRelations.join(",\n")}
|
|
|
10878
11317
|
async deleteExpiredTokens() {
|
|
10879
11318
|
await this.tokenRepository.deleteExpiredTokens();
|
|
10880
11319
|
}
|
|
11320
|
+
// MFA operations (delegate to MfaService)
|
|
11321
|
+
_mfaService = null;
|
|
11322
|
+
getMfaService() {
|
|
11323
|
+
if (!this._mfaService) {
|
|
11324
|
+
this._mfaService = new MfaService(this.db);
|
|
11325
|
+
}
|
|
11326
|
+
return this._mfaService;
|
|
11327
|
+
}
|
|
11328
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11329
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
11330
|
+
}
|
|
11331
|
+
async getMfaFactors(userId) {
|
|
11332
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
11333
|
+
}
|
|
11334
|
+
async getMfaFactorById(factorId) {
|
|
11335
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
11336
|
+
}
|
|
11337
|
+
async verifyMfaFactor(factorId) {
|
|
11338
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
11339
|
+
}
|
|
11340
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11341
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
11342
|
+
}
|
|
11343
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11344
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
11345
|
+
}
|
|
11346
|
+
async getMfaChallengeById(challengeId) {
|
|
11347
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
11348
|
+
}
|
|
11349
|
+
async verifyMfaChallenge(challengeId) {
|
|
11350
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
11351
|
+
}
|
|
11352
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11353
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
11354
|
+
}
|
|
11355
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11356
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
11357
|
+
}
|
|
11358
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11359
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
11360
|
+
}
|
|
11361
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11362
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
11363
|
+
}
|
|
11364
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11365
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
class MfaService {
|
|
11369
|
+
constructor(db, schemaName = "rebase") {
|
|
11370
|
+
this.db = db;
|
|
11371
|
+
this.schemaName = schemaName;
|
|
11372
|
+
}
|
|
11373
|
+
qualify(tableName) {
|
|
11374
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
11375
|
+
}
|
|
11376
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11377
|
+
const tableName = this.qualify("mfa_factors");
|
|
11378
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11379
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
11380
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
11381
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11382
|
+
`);
|
|
11383
|
+
const row = result.rows[0];
|
|
11384
|
+
return {
|
|
11385
|
+
id: row.id,
|
|
11386
|
+
userId: row.user_id,
|
|
11387
|
+
factorType: row.factor_type,
|
|
11388
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11389
|
+
verified: row.verified,
|
|
11390
|
+
createdAt: new Date(row.created_at),
|
|
11391
|
+
updatedAt: new Date(row.updated_at)
|
|
11392
|
+
};
|
|
11393
|
+
}
|
|
11394
|
+
async getMfaFactors(userId) {
|
|
11395
|
+
const tableName = this.qualify("mfa_factors");
|
|
11396
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11397
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11398
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11399
|
+
WHERE user_id = ${userId}
|
|
11400
|
+
ORDER BY created_at
|
|
11401
|
+
`);
|
|
11402
|
+
return result.rows.map((row) => ({
|
|
11403
|
+
id: row.id,
|
|
11404
|
+
userId: row.user_id,
|
|
11405
|
+
factorType: row.factor_type,
|
|
11406
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11407
|
+
verified: row.verified,
|
|
11408
|
+
createdAt: new Date(row.created_at),
|
|
11409
|
+
updatedAt: new Date(row.updated_at)
|
|
11410
|
+
}));
|
|
11411
|
+
}
|
|
11412
|
+
async getMfaFactorById(factorId) {
|
|
11413
|
+
const tableName = this.qualify("mfa_factors");
|
|
11414
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11415
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
11416
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11417
|
+
WHERE id = ${factorId}
|
|
11418
|
+
`);
|
|
11419
|
+
if (result.rows.length === 0) return null;
|
|
11420
|
+
const row = result.rows[0];
|
|
11421
|
+
return {
|
|
11422
|
+
id: row.id,
|
|
11423
|
+
userId: row.user_id,
|
|
11424
|
+
factorType: row.factor_type,
|
|
11425
|
+
secretEncrypted: row.secret_encrypted,
|
|
11426
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11427
|
+
verified: row.verified,
|
|
11428
|
+
createdAt: new Date(row.created_at),
|
|
11429
|
+
updatedAt: new Date(row.updated_at)
|
|
11430
|
+
};
|
|
11431
|
+
}
|
|
11432
|
+
async verifyMfaFactor(factorId) {
|
|
11433
|
+
const tableName = this.qualify("mfa_factors");
|
|
11434
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11435
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11436
|
+
SET verified = TRUE, updated_at = NOW()
|
|
11437
|
+
WHERE id = ${factorId}
|
|
11438
|
+
`);
|
|
11439
|
+
}
|
|
11440
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11441
|
+
const tableName = this.qualify("mfa_factors");
|
|
11442
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11443
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11444
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
11445
|
+
`);
|
|
11446
|
+
}
|
|
11447
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11448
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11449
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
|
|
11450
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11451
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
11452
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
11453
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
11454
|
+
`);
|
|
11455
|
+
const row = result.rows[0];
|
|
11456
|
+
return {
|
|
11457
|
+
id: row.id,
|
|
11458
|
+
factorId: row.factor_id,
|
|
11459
|
+
createdAt: new Date(row.created_at),
|
|
11460
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11461
|
+
ipAddress: row.ip_address ?? void 0
|
|
11462
|
+
};
|
|
11463
|
+
}
|
|
11464
|
+
async getMfaChallengeById(challengeId) {
|
|
11465
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11466
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11467
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
11468
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11469
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
11470
|
+
`);
|
|
11471
|
+
if (result.rows.length === 0) return null;
|
|
11472
|
+
const row = result.rows[0];
|
|
11473
|
+
return {
|
|
11474
|
+
id: row.id,
|
|
11475
|
+
factorId: row.factor_id,
|
|
11476
|
+
createdAt: new Date(row.created_at),
|
|
11477
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11478
|
+
ipAddress: row.ip_address ?? void 0
|
|
11479
|
+
};
|
|
11480
|
+
}
|
|
11481
|
+
async verifyMfaChallenge(challengeId) {
|
|
11482
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11483
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11484
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11485
|
+
SET verified_at = NOW()
|
|
11486
|
+
WHERE id = ${challengeId}
|
|
11487
|
+
`);
|
|
11488
|
+
}
|
|
11489
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11490
|
+
const tableName = this.qualify("recovery_codes");
|
|
11491
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11492
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11493
|
+
`);
|
|
11494
|
+
for (const hash of codeHashes) {
|
|
11495
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11496
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, code_hash)
|
|
11497
|
+
VALUES (${userId}, ${hash})
|
|
11498
|
+
`);
|
|
11499
|
+
}
|
|
11500
|
+
}
|
|
11501
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11502
|
+
const tableName = this.qualify("recovery_codes");
|
|
11503
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11504
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11505
|
+
SET used_at = NOW()
|
|
11506
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
11507
|
+
RETURNING id
|
|
11508
|
+
`);
|
|
11509
|
+
return result.rows.length > 0;
|
|
11510
|
+
}
|
|
11511
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11512
|
+
const tableName = this.qualify("recovery_codes");
|
|
11513
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11514
|
+
SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11515
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
11516
|
+
`);
|
|
11517
|
+
return result.rows[0].count;
|
|
11518
|
+
}
|
|
11519
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11520
|
+
const tableName = this.qualify("recovery_codes");
|
|
11521
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11522
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11523
|
+
`);
|
|
11524
|
+
}
|
|
11525
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11526
|
+
const tableName = this.qualify("mfa_factors");
|
|
11527
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11528
|
+
SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11529
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
11530
|
+
`);
|
|
11531
|
+
return result.rows[0].count > 0;
|
|
11532
|
+
}
|
|
10881
11533
|
}
|
|
10882
11534
|
const DEFAULT_RETENTION = {
|
|
10883
11535
|
maxEntries: 200,
|
|
@@ -11088,7 +11740,7 @@ ${tableRelations.join(",\n")}
|
|
|
11088
11740
|
const registry = new PostgresCollectionRegistry();
|
|
11089
11741
|
if (collections) {
|
|
11090
11742
|
registry.registerMultiple(collections);
|
|
11091
|
-
|
|
11743
|
+
serverCore.logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
11092
11744
|
}
|
|
11093
11745
|
if (pgConfig.schema?.tables) {
|
|
11094
11746
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -11114,10 +11766,28 @@ ${tableRelations.join(",\n")}
|
|
|
11114
11766
|
try {
|
|
11115
11767
|
await schemaAwareDb.execute(drizzleOrm.sql`SELECT 1`);
|
|
11116
11768
|
} catch (err) {
|
|
11117
|
-
|
|
11118
|
-
|
|
11769
|
+
serverCore.logger.error("❌ Failed to connect to PostgreSQL", {
|
|
11770
|
+
error: err
|
|
11771
|
+
});
|
|
11772
|
+
serverCore.logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
11119
11773
|
}
|
|
11120
11774
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11775
|
+
let readDb;
|
|
11776
|
+
const readUrl = process.env.DATABASE_READ_URL;
|
|
11777
|
+
if (readUrl && readUrl !== pgConfig.connectionString) {
|
|
11778
|
+
try {
|
|
11779
|
+
const {
|
|
11780
|
+
createReadReplicaConnection: createReadReplicaConnection2
|
|
11781
|
+
} = await Promise.resolve().then(() => connection);
|
|
11782
|
+
const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
|
|
11783
|
+
readDb = readResources.db;
|
|
11784
|
+
serverCore.logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11785
|
+
} catch (err) {
|
|
11786
|
+
serverCore.logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
|
|
11787
|
+
error: err
|
|
11788
|
+
});
|
|
11789
|
+
}
|
|
11790
|
+
}
|
|
11121
11791
|
const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
|
|
11122
11792
|
const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
|
|
11123
11793
|
realtimeService.setDataDriver(driver);
|
|
@@ -11125,18 +11795,24 @@ ${tableRelations.join(",\n")}
|
|
|
11125
11795
|
try {
|
|
11126
11796
|
await driver.branchService.ensureBranchMetadataTable();
|
|
11127
11797
|
} catch (err) {
|
|
11128
|
-
|
|
11798
|
+
serverCore.logger.warn("⚠️ Could not initialize branch metadata table", {
|
|
11799
|
+
error: err
|
|
11800
|
+
});
|
|
11129
11801
|
}
|
|
11130
11802
|
}
|
|
11131
|
-
|
|
11803
|
+
const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
|
|
11804
|
+
if (directUrl) {
|
|
11132
11805
|
try {
|
|
11133
|
-
await realtimeService.startListening(
|
|
11806
|
+
await realtimeService.startListening(directUrl);
|
|
11134
11807
|
} catch (err) {
|
|
11135
|
-
|
|
11808
|
+
serverCore.logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
11809
|
+
error: err
|
|
11810
|
+
});
|
|
11136
11811
|
}
|
|
11137
11812
|
}
|
|
11138
11813
|
const internals = {
|
|
11139
11814
|
db: schemaAwareDb,
|
|
11815
|
+
readDb,
|
|
11140
11816
|
registry,
|
|
11141
11817
|
realtimeService,
|
|
11142
11818
|
driver,
|
|
@@ -11273,14 +11949,22 @@ ${tableRelations.join(",\n")}
|
|
|
11273
11949
|
exports2.RealtimeService = RealtimeService;
|
|
11274
11950
|
exports2.appConfig = appConfig;
|
|
11275
11951
|
exports2.createAuthSchema = createAuthSchema;
|
|
11952
|
+
exports2.createDirectDatabaseConnection = createDirectDatabaseConnection;
|
|
11276
11953
|
exports2.createPostgresAdapter = createPostgresAdapter;
|
|
11277
11954
|
exports2.createPostgresBootstrapper = createPostgresBootstrapper;
|
|
11278
11955
|
exports2.createPostgresDatabaseConnection = createPostgresDatabaseConnection;
|
|
11279
11956
|
exports2.createPostgresWebSocket = createPostgresWebSocket;
|
|
11957
|
+
exports2.createReadReplicaConnection = createReadReplicaConnection;
|
|
11280
11958
|
exports2.generateSchema = generateSchema;
|
|
11959
|
+
exports2.mfaChallenges = mfaChallenges;
|
|
11960
|
+
exports2.mfaChallengesRelations = mfaChallengesRelations;
|
|
11961
|
+
exports2.mfaFactors = mfaFactors;
|
|
11962
|
+
exports2.mfaFactorsRelations = mfaFactorsRelations;
|
|
11281
11963
|
exports2.passwordResetTokens = passwordResetTokens;
|
|
11282
11964
|
exports2.passwordResetTokensRelations = passwordResetTokensRelations;
|
|
11283
11965
|
exports2.rebaseSchema = rebaseSchema;
|
|
11966
|
+
exports2.recoveryCodes = recoveryCodes;
|
|
11967
|
+
exports2.recoveryCodesRelations = recoveryCodesRelations;
|
|
11284
11968
|
exports2.refreshTokens = refreshTokens;
|
|
11285
11969
|
exports2.refreshTokensRelations = refreshTokensRelations;
|
|
11286
11970
|
exports2.roles = roles;
|