@rebasepro/server-postgresql 0.2.3 → 0.3.0
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.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,
|
|
4
|
+
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, boolean, varchar, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
|
|
5
5
|
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import { promises } from "fs";
|
|
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
|
|
|
11
11
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12
12
|
import { EventEmitter } from "events";
|
|
13
13
|
import { inspect } from "util";
|
|
14
|
-
import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
|
|
14
|
+
import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
|
|
15
15
|
const DEFAULT_POOL = {
|
|
16
16
|
max: 20,
|
|
17
17
|
idleTimeoutMillis: 3e4,
|
|
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
51
51
|
connectionString
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
55
|
+
const opts = {
|
|
56
|
+
...DEFAULT_POOL,
|
|
57
|
+
max: 5,
|
|
58
|
+
...poolConfig
|
|
59
|
+
};
|
|
60
|
+
const pgPoolConfig = {
|
|
61
|
+
connectionString,
|
|
62
|
+
max: opts.max,
|
|
63
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
64
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
65
|
+
query_timeout: opts.queryTimeout,
|
|
66
|
+
statement_timeout: opts.statementTimeout,
|
|
67
|
+
keepAlive: opts.keepAlive,
|
|
68
|
+
keepAliveInitialDelayMillis: 0
|
|
69
|
+
};
|
|
70
|
+
const pool = new Pool(pgPoolConfig);
|
|
71
|
+
pool.on("error", (err) => {
|
|
72
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
73
|
+
});
|
|
74
|
+
const db = schema ? drizzle(pool, {
|
|
75
|
+
schema
|
|
76
|
+
}) : drizzle(pool);
|
|
77
|
+
return {
|
|
78
|
+
db,
|
|
79
|
+
pool,
|
|
80
|
+
connectionString
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
84
|
+
const opts = {
|
|
85
|
+
...DEFAULT_POOL,
|
|
86
|
+
max: 10,
|
|
87
|
+
...poolConfig
|
|
88
|
+
};
|
|
89
|
+
const pgPoolConfig = {
|
|
90
|
+
connectionString,
|
|
91
|
+
max: opts.max,
|
|
92
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
93
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
94
|
+
query_timeout: opts.queryTimeout,
|
|
95
|
+
statement_timeout: opts.statementTimeout,
|
|
96
|
+
keepAlive: opts.keepAlive,
|
|
97
|
+
keepAliveInitialDelayMillis: 0
|
|
98
|
+
};
|
|
99
|
+
const pool = new Pool(pgPoolConfig);
|
|
100
|
+
pool.on("error", (err) => {
|
|
101
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
102
|
+
});
|
|
103
|
+
const db = schema ? drizzle(pool, {
|
|
104
|
+
schema
|
|
105
|
+
}) : drizzle(pool);
|
|
106
|
+
return {
|
|
107
|
+
db,
|
|
108
|
+
pool,
|
|
109
|
+
connectionString
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
113
|
+
__proto__: null,
|
|
114
|
+
createDirectDatabaseConnection,
|
|
115
|
+
createPostgresDatabaseConnection,
|
|
116
|
+
createReadReplicaConnection
|
|
117
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
54
118
|
class Vector {
|
|
55
119
|
value;
|
|
56
120
|
constructor(value) {
|
|
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
|
|
|
970
1034
|
return output;
|
|
971
1035
|
}
|
|
972
1036
|
for (const key in source) {
|
|
1037
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
973
1040
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
974
1041
|
const sourceValue = source[key];
|
|
975
1042
|
const outputValue = output[key];
|
|
@@ -2644,6 +2711,90 @@ 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
|
+
properties: {
|
|
2723
|
+
id: {
|
|
2724
|
+
name: "ID",
|
|
2725
|
+
type: "string",
|
|
2726
|
+
isId: "uuid"
|
|
2727
|
+
},
|
|
2728
|
+
email: {
|
|
2729
|
+
name: "Email",
|
|
2730
|
+
type: "string",
|
|
2731
|
+
validation: {
|
|
2732
|
+
required: true,
|
|
2733
|
+
unique: true
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
password_hash: {
|
|
2737
|
+
name: "Password Hash",
|
|
2738
|
+
type: "string",
|
|
2739
|
+
ui: {
|
|
2740
|
+
hideFromCollection: true
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
display_name: {
|
|
2744
|
+
name: "Display Name",
|
|
2745
|
+
type: "string"
|
|
2746
|
+
},
|
|
2747
|
+
photo_url: {
|
|
2748
|
+
name: "Photo URL",
|
|
2749
|
+
type: "string"
|
|
2750
|
+
},
|
|
2751
|
+
email_verified: {
|
|
2752
|
+
name: "Email Verified",
|
|
2753
|
+
type: "boolean",
|
|
2754
|
+
defaultValue: false
|
|
2755
|
+
},
|
|
2756
|
+
email_verification_token: {
|
|
2757
|
+
name: "Email Verification Token",
|
|
2758
|
+
type: "string",
|
|
2759
|
+
ui: {
|
|
2760
|
+
hideFromCollection: true
|
|
2761
|
+
}
|
|
2762
|
+
},
|
|
2763
|
+
email_verification_sent_at: {
|
|
2764
|
+
name: "Email Verification Sent At",
|
|
2765
|
+
type: "date",
|
|
2766
|
+
ui: {
|
|
2767
|
+
hideFromCollection: true
|
|
2768
|
+
}
|
|
2769
|
+
},
|
|
2770
|
+
metadata: {
|
|
2771
|
+
name: "Metadata",
|
|
2772
|
+
type: "map",
|
|
2773
|
+
defaultValue: {},
|
|
2774
|
+
ui: {
|
|
2775
|
+
hideFromCollection: true
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
created_at: {
|
|
2779
|
+
name: "Created At",
|
|
2780
|
+
type: "date",
|
|
2781
|
+
autoValue: "on_create",
|
|
2782
|
+
ui: {
|
|
2783
|
+
readOnly: true,
|
|
2784
|
+
hideFromCollection: true
|
|
2785
|
+
}
|
|
2786
|
+
},
|
|
2787
|
+
updated_at: {
|
|
2788
|
+
name: "Updated At",
|
|
2789
|
+
type: "date",
|
|
2790
|
+
autoValue: "on_update",
|
|
2791
|
+
ui: {
|
|
2792
|
+
readOnly: true,
|
|
2793
|
+
hideFromCollection: true
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2647
2798
|
function mapOperator(op) {
|
|
2648
2799
|
switch (op) {
|
|
2649
2800
|
case "==":
|
|
@@ -2984,7 +3135,13 @@ class DrizzleConditionBuilder {
|
|
|
2984
3135
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
2985
3136
|
if (!filterParam) continue;
|
|
2986
3137
|
const [op, value] = filterParam;
|
|
2987
|
-
|
|
3138
|
+
let fieldColumn = table[field];
|
|
3139
|
+
if (!fieldColumn) {
|
|
3140
|
+
const relationKey = `${field}_id`;
|
|
3141
|
+
if (relationKey in table) {
|
|
3142
|
+
fieldColumn = table[relationKey];
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2988
3145
|
if (!fieldColumn) {
|
|
2989
3146
|
console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
2990
3147
|
continue;
|
|
@@ -3026,6 +3183,17 @@ class DrizzleConditionBuilder {
|
|
|
3026
3183
|
return null;
|
|
3027
3184
|
case "array-contains":
|
|
3028
3185
|
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3186
|
+
case "array-contains-any":
|
|
3187
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3188
|
+
const textValues = value.map((v) => String(v));
|
|
3189
|
+
return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
|
|
3190
|
+
}
|
|
3191
|
+
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3192
|
+
case "not-in":
|
|
3193
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3194
|
+
return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
|
|
3195
|
+
}
|
|
3196
|
+
return null;
|
|
3029
3197
|
default:
|
|
3030
3198
|
console.warn(`Unsupported filter operation: ${op}`);
|
|
3031
3199
|
return null;
|
|
@@ -3541,6 +3709,40 @@ class DrizzleConditionBuilder {
|
|
|
3541
3709
|
return null;
|
|
3542
3710
|
}
|
|
3543
3711
|
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Build vector similarity search expressions for pgvector.
|
|
3714
|
+
*
|
|
3715
|
+
* Returns:
|
|
3716
|
+
* - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
|
|
3717
|
+
* - `filter`: optional WHERE clause for distance threshold
|
|
3718
|
+
* - `distanceSelect`: SQL expression for selecting the distance as `_distance`
|
|
3719
|
+
*/
|
|
3720
|
+
static buildVectorSearchConditions(table, vectorSearch) {
|
|
3721
|
+
const column = table[vectorSearch.property];
|
|
3722
|
+
if (!column) {
|
|
3723
|
+
throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
3724
|
+
}
|
|
3725
|
+
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
3726
|
+
const distanceFn = vectorSearch.distance || "cosine";
|
|
3727
|
+
let operator;
|
|
3728
|
+
switch (distanceFn) {
|
|
3729
|
+
case "cosine":
|
|
3730
|
+
operator = "<=>";
|
|
3731
|
+
break;
|
|
3732
|
+
case "l2":
|
|
3733
|
+
operator = "<->";
|
|
3734
|
+
break;
|
|
3735
|
+
case "inner_product":
|
|
3736
|
+
operator = "<#>";
|
|
3737
|
+
break;
|
|
3738
|
+
}
|
|
3739
|
+
const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
|
|
3740
|
+
return {
|
|
3741
|
+
orderBy: distanceExpr,
|
|
3742
|
+
filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
|
|
3743
|
+
distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3544
3746
|
}
|
|
3545
3747
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
3546
3748
|
function getColumnMeta(col) {
|
|
@@ -5486,7 +5688,7 @@ class EntityFetchService {
|
|
|
5486
5688
|
const qb = this.getQueryBuilder(tableName);
|
|
5487
5689
|
const withConfig = this.buildWithConfig(collection);
|
|
5488
5690
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
5489
|
-
if (qb && !options.searchString && !hasRelations) {
|
|
5691
|
+
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
|
|
5490
5692
|
try {
|
|
5491
5693
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5492
5694
|
const results2 = await qb.findMany(queryOpts);
|
|
@@ -5500,7 +5702,14 @@ class EntityFetchService {
|
|
|
5500
5702
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
5501
5703
|
}
|
|
5502
5704
|
}
|
|
5503
|
-
let
|
|
5705
|
+
let vectorMeta;
|
|
5706
|
+
if (options.vectorSearch) {
|
|
5707
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
5708
|
+
}
|
|
5709
|
+
let query = vectorMeta ? this.db.select({
|
|
5710
|
+
table_row: table,
|
|
5711
|
+
_distance: vectorMeta.distanceSelect
|
|
5712
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5504
5713
|
const allConditions = [];
|
|
5505
5714
|
if (options.searchString) {
|
|
5506
5715
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5511,12 +5720,17 @@ class EntityFetchService {
|
|
|
5511
5720
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5512
5721
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5513
5722
|
}
|
|
5723
|
+
if (vectorMeta?.filter) {
|
|
5724
|
+
allConditions.push(vectorMeta.filter);
|
|
5725
|
+
}
|
|
5514
5726
|
if (allConditions.length > 0) {
|
|
5515
5727
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5516
5728
|
if (finalCondition) query = query.where(finalCondition);
|
|
5517
5729
|
}
|
|
5518
5730
|
const orderExpressions = [];
|
|
5519
|
-
if (
|
|
5731
|
+
if (vectorMeta) {
|
|
5732
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
5733
|
+
} else if (options.orderBy) {
|
|
5520
5734
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5521
5735
|
if (orderByField) {
|
|
5522
5736
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5532,10 +5746,14 @@ class EntityFetchService {
|
|
|
5532
5746
|
if (finalCondition) query = query.where(finalCondition);
|
|
5533
5747
|
}
|
|
5534
5748
|
}
|
|
5535
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5749
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5536
5750
|
if (limitValue) query = query.limit(limitValue);
|
|
5537
5751
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5538
|
-
const
|
|
5752
|
+
const rawResults = await query;
|
|
5753
|
+
const results = vectorMeta ? rawResults.map((r) => ({
|
|
5754
|
+
...r.table_row,
|
|
5755
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
5756
|
+
})) : rawResults;
|
|
5539
5757
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5540
5758
|
}
|
|
5541
5759
|
/**
|
|
@@ -5757,7 +5975,7 @@ class EntityFetchService {
|
|
|
5757
5975
|
const idField = table[idInfo.fieldName];
|
|
5758
5976
|
const tableName = getTableName$1(table);
|
|
5759
5977
|
const qb = this.getQueryBuilder(tableName);
|
|
5760
|
-
if (qb && !options.searchString) {
|
|
5978
|
+
if (qb && !options.searchString && !options.vectorSearch) {
|
|
5761
5979
|
try {
|
|
5762
5980
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5763
5981
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
@@ -5903,7 +6121,14 @@ class EntityFetchService {
|
|
|
5903
6121
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
5904
6122
|
const idInfo = idInfoArray[0];
|
|
5905
6123
|
const idField = table[idInfo.fieldName];
|
|
5906
|
-
let
|
|
6124
|
+
let vectorMeta;
|
|
6125
|
+
if (options.vectorSearch) {
|
|
6126
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
6127
|
+
}
|
|
6128
|
+
let query = vectorMeta ? this.db.select({
|
|
6129
|
+
table_row: table,
|
|
6130
|
+
_distance: vectorMeta.distanceSelect
|
|
6131
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5907
6132
|
const allConditions = [];
|
|
5908
6133
|
if (options.searchString) {
|
|
5909
6134
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5914,12 +6139,17 @@ class EntityFetchService {
|
|
|
5914
6139
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5915
6140
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5916
6141
|
}
|
|
6142
|
+
if (vectorMeta?.filter) {
|
|
6143
|
+
allConditions.push(vectorMeta.filter);
|
|
6144
|
+
}
|
|
5917
6145
|
if (allConditions.length > 0) {
|
|
5918
6146
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5919
6147
|
if (finalCondition) query = query.where(finalCondition);
|
|
5920
6148
|
}
|
|
5921
6149
|
const orderExpressions = [];
|
|
5922
|
-
if (
|
|
6150
|
+
if (vectorMeta) {
|
|
6151
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
6152
|
+
} else if (options.orderBy) {
|
|
5923
6153
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5924
6154
|
if (orderByField) {
|
|
5925
6155
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5927,10 +6157,17 @@ class EntityFetchService {
|
|
|
5927
6157
|
}
|
|
5928
6158
|
orderExpressions.push(desc(idField));
|
|
5929
6159
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5930
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
6160
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5931
6161
|
if (limitValue) query = query.limit(limitValue);
|
|
5932
6162
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5933
|
-
|
|
6163
|
+
const rawResults = await query;
|
|
6164
|
+
if (vectorMeta) {
|
|
6165
|
+
return rawResults.map((r) => ({
|
|
6166
|
+
...r.table_row,
|
|
6167
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
6168
|
+
}));
|
|
6169
|
+
}
|
|
6170
|
+
return rawResults;
|
|
5934
6171
|
}
|
|
5935
6172
|
/**
|
|
5936
6173
|
* Check if the Drizzle instance has the relational query API available
|
|
@@ -6684,7 +6921,8 @@ class PostgresBackendDriver {
|
|
|
6684
6921
|
startAfter,
|
|
6685
6922
|
orderBy,
|
|
6686
6923
|
searchString,
|
|
6687
|
-
order
|
|
6924
|
+
order,
|
|
6925
|
+
vectorSearch
|
|
6688
6926
|
}) {
|
|
6689
6927
|
const entities = await this.entityService.fetchCollection(path2, {
|
|
6690
6928
|
filter,
|
|
@@ -6694,7 +6932,8 @@ class PostgresBackendDriver {
|
|
|
6694
6932
|
offset,
|
|
6695
6933
|
startAfter,
|
|
6696
6934
|
databaseId: collection?.databaseId,
|
|
6697
|
-
searchString
|
|
6935
|
+
searchString,
|
|
6936
|
+
vectorSearch
|
|
6698
6937
|
});
|
|
6699
6938
|
const {
|
|
6700
6939
|
collection: resolvedCollection,
|
|
@@ -7537,6 +7776,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7537
7776
|
length: 255
|
|
7538
7777
|
}),
|
|
7539
7778
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
7779
|
+
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
7540
7780
|
metadata: jsonb("metadata").$type().default({}).notNull(),
|
|
7541
7781
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7542
7782
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -7551,8 +7791,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7551
7791
|
}).notNull(),
|
|
7552
7792
|
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
7553
7793
|
defaultPermissions: jsonb("default_permissions").$type(),
|
|
7554
|
-
collectionPermissions: jsonb("collection_permissions").$type()
|
|
7555
|
-
config: jsonb("config").$type()
|
|
7794
|
+
collectionPermissions: jsonb("collection_permissions").$type()
|
|
7556
7795
|
});
|
|
7557
7796
|
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7558
7797
|
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
@@ -7624,6 +7863,48 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7624
7863
|
}, (table) => ({
|
|
7625
7864
|
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7626
7865
|
}));
|
|
7866
|
+
const mfaFactors2 = rolesTableCreator("mfa_factors", {
|
|
7867
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7868
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7869
|
+
onDelete: "cascade"
|
|
7870
|
+
}),
|
|
7871
|
+
factorType: varchar("factor_type", {
|
|
7872
|
+
length: 20
|
|
7873
|
+
}).notNull(),
|
|
7874
|
+
// 'totp'
|
|
7875
|
+
secretEncrypted: varchar("secret_encrypted", {
|
|
7876
|
+
length: 500
|
|
7877
|
+
}).notNull(),
|
|
7878
|
+
friendlyName: varchar("friendly_name", {
|
|
7879
|
+
length: 255
|
|
7880
|
+
}),
|
|
7881
|
+
verified: boolean("verified").default(false).notNull(),
|
|
7882
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7883
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7884
|
+
});
|
|
7885
|
+
const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
|
|
7886
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7887
|
+
factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
|
|
7888
|
+
onDelete: "cascade"
|
|
7889
|
+
}),
|
|
7890
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7891
|
+
verifiedAt: timestamp("verified_at"),
|
|
7892
|
+
ipAddress: varchar("ip_address", {
|
|
7893
|
+
length: 45
|
|
7894
|
+
}),
|
|
7895
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
7896
|
+
});
|
|
7897
|
+
const recoveryCodes2 = rolesTableCreator("recovery_codes", {
|
|
7898
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7899
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7900
|
+
onDelete: "cascade"
|
|
7901
|
+
}),
|
|
7902
|
+
codeHash: varchar("code_hash", {
|
|
7903
|
+
length: 255
|
|
7904
|
+
}).notNull(),
|
|
7905
|
+
usedAt: timestamp("used_at"),
|
|
7906
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7907
|
+
});
|
|
7627
7908
|
return {
|
|
7628
7909
|
rolesSchema,
|
|
7629
7910
|
usersSchema: usersSchema2,
|
|
@@ -7633,7 +7914,10 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7633
7914
|
refreshTokens: refreshTokens2,
|
|
7634
7915
|
passwordResetTokens: passwordResetTokens2,
|
|
7635
7916
|
appConfig: appConfig2,
|
|
7636
|
-
userIdentities: userIdentities2
|
|
7917
|
+
userIdentities: userIdentities2,
|
|
7918
|
+
mfaFactors: mfaFactors2,
|
|
7919
|
+
mfaChallenges: mfaChallenges2,
|
|
7920
|
+
recoveryCodes: recoveryCodes2
|
|
7637
7921
|
};
|
|
7638
7922
|
}
|
|
7639
7923
|
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
@@ -7646,13 +7930,18 @@ const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
|
7646
7930
|
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7647
7931
|
const appConfig = defaultAuthSchema.appConfig;
|
|
7648
7932
|
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7933
|
+
const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
7934
|
+
const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
7935
|
+
const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
7649
7936
|
const usersRelations = relations(users, ({
|
|
7650
7937
|
many
|
|
7651
7938
|
}) => ({
|
|
7652
7939
|
userRoles: many(userRoles),
|
|
7653
7940
|
refreshTokens: many(refreshTokens),
|
|
7654
7941
|
passwordResetTokens: many(passwordResetTokens),
|
|
7655
|
-
userIdentities: many(userIdentities)
|
|
7942
|
+
userIdentities: many(userIdentities),
|
|
7943
|
+
mfaFactors: many(mfaFactors),
|
|
7944
|
+
recoveryCodes: many(recoveryCodes)
|
|
7656
7945
|
}));
|
|
7657
7946
|
const rolesRelations = relations(roles, ({
|
|
7658
7947
|
many
|
|
@@ -7695,6 +7984,32 @@ const userIdentitiesRelations = relations(userIdentities, ({
|
|
|
7695
7984
|
references: [users.id]
|
|
7696
7985
|
})
|
|
7697
7986
|
}));
|
|
7987
|
+
const mfaFactorsRelations = relations(mfaFactors, ({
|
|
7988
|
+
one,
|
|
7989
|
+
many
|
|
7990
|
+
}) => ({
|
|
7991
|
+
user: one(users, {
|
|
7992
|
+
fields: [mfaFactors.userId],
|
|
7993
|
+
references: [users.id]
|
|
7994
|
+
}),
|
|
7995
|
+
challenges: many(mfaChallenges)
|
|
7996
|
+
}));
|
|
7997
|
+
const mfaChallengesRelations = relations(mfaChallenges, ({
|
|
7998
|
+
one
|
|
7999
|
+
}) => ({
|
|
8000
|
+
factor: one(mfaFactors, {
|
|
8001
|
+
fields: [mfaChallenges.factorId],
|
|
8002
|
+
references: [mfaFactors.id]
|
|
8003
|
+
})
|
|
8004
|
+
}));
|
|
8005
|
+
const recoveryCodesRelations = relations(recoveryCodes, ({
|
|
8006
|
+
one
|
|
8007
|
+
}) => ({
|
|
8008
|
+
user: one(users, {
|
|
8009
|
+
fields: [recoveryCodes.userId],
|
|
8010
|
+
references: [users.id]
|
|
8011
|
+
})
|
|
8012
|
+
}));
|
|
7698
8013
|
const resolveColumnName = (propName, prop) => {
|
|
7699
8014
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
7700
8015
|
return prop.columnName;
|
|
@@ -8298,90 +8613,6 @@ ${tableRelations.join(",\n")}
|
|
|
8298
8613
|
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8299
8614
|
return schemaContent;
|
|
8300
8615
|
};
|
|
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
8616
|
const formatTerminalText = (text, options = {}) => {
|
|
8386
8617
|
let codes = "";
|
|
8387
8618
|
if (options.bold) codes += "\x1B[1m";
|
|
@@ -8447,10 +8678,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
|
|
|
8447
8678
|
if (!collections || !Array.isArray(collections)) {
|
|
8448
8679
|
collections = [];
|
|
8449
8680
|
}
|
|
8450
|
-
|
|
8451
|
-
if (!hasUsersCollection) {
|
|
8452
|
-
collections.push(defaultUsersCollection);
|
|
8453
|
-
}
|
|
8681
|
+
collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
|
|
8454
8682
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
8455
8683
|
const schemaContent = await generateSchema(collections);
|
|
8456
8684
|
if (outputPath) {
|
|
@@ -8511,6 +8739,13 @@ class RealtimeService extends EventEmitter {
|
|
|
8511
8739
|
this.entityService = new EntityService(db, registry);
|
|
8512
8740
|
}
|
|
8513
8741
|
clients = /* @__PURE__ */ new Map();
|
|
8742
|
+
// Broadcast channels: channel name → set of client IDs
|
|
8743
|
+
channels = /* @__PURE__ */ new Map();
|
|
8744
|
+
// Presence: channel → Map<clientId, { state, lastSeen }>
|
|
8745
|
+
presence = /* @__PURE__ */ new Map();
|
|
8746
|
+
presenceInterval;
|
|
8747
|
+
static PRESENCE_TIMEOUT_MS = 3e4;
|
|
8748
|
+
// 30s
|
|
8514
8749
|
entityService;
|
|
8515
8750
|
// Enhanced subscriptions storage with full request parameters
|
|
8516
8751
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -8637,8 +8872,19 @@ class RealtimeService extends EventEmitter {
|
|
|
8637
8872
|
}
|
|
8638
8873
|
}
|
|
8639
8874
|
}
|
|
8875
|
+
for (const [channel, members] of this.channels.entries()) {
|
|
8876
|
+
if (members.has(clientId)) {
|
|
8877
|
+
members.delete(clientId);
|
|
8878
|
+
this.removePresence(clientId, channel);
|
|
8879
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
for (const [channel] of this.presence) {
|
|
8883
|
+
this.removePresence(clientId, channel);
|
|
8884
|
+
}
|
|
8640
8885
|
}
|
|
8641
8886
|
async handleMessage(clientId, message, authContext) {
|
|
8887
|
+
const payload = message.payload;
|
|
8642
8888
|
switch (message.type) {
|
|
8643
8889
|
case "subscribe_collection":
|
|
8644
8890
|
await this.handleCollectionSubscription(clientId, message.payload, authContext);
|
|
@@ -8649,6 +8895,25 @@ class RealtimeService extends EventEmitter {
|
|
|
8649
8895
|
case "unsubscribe":
|
|
8650
8896
|
await this.handleUnsubscribe(clientId, message.subscriptionId);
|
|
8651
8897
|
break;
|
|
8898
|
+
case "join_channel":
|
|
8899
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8900
|
+
break;
|
|
8901
|
+
case "leave_channel":
|
|
8902
|
+
this.leaveChannel(clientId, payload?.channel);
|
|
8903
|
+
break;
|
|
8904
|
+
case "broadcast":
|
|
8905
|
+
this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
|
|
8906
|
+
break;
|
|
8907
|
+
case "presence_track":
|
|
8908
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8909
|
+
this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
|
|
8910
|
+
break;
|
|
8911
|
+
case "presence_untrack":
|
|
8912
|
+
this.removePresence(clientId, payload?.channel);
|
|
8913
|
+
break;
|
|
8914
|
+
case "presence_state":
|
|
8915
|
+
this.sendPresenceState(clientId, payload?.channel);
|
|
8916
|
+
break;
|
|
8652
8917
|
default:
|
|
8653
8918
|
this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
|
|
8654
8919
|
}
|
|
@@ -9106,6 +9371,132 @@ class RealtimeService extends EventEmitter {
|
|
|
9106
9371
|
return parentPaths;
|
|
9107
9372
|
}
|
|
9108
9373
|
// =============================================================================
|
|
9374
|
+
// Broadcast Channels
|
|
9375
|
+
// =============================================================================
|
|
9376
|
+
/** Join a broadcast channel */
|
|
9377
|
+
joinChannel(clientId, channel) {
|
|
9378
|
+
if (!this.channels.has(channel)) {
|
|
9379
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
9380
|
+
}
|
|
9381
|
+
this.channels.get(channel).add(clientId);
|
|
9382
|
+
this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
|
|
9383
|
+
}
|
|
9384
|
+
/** Leave a broadcast channel */
|
|
9385
|
+
leaveChannel(clientId, channel) {
|
|
9386
|
+
const members = this.channels.get(channel);
|
|
9387
|
+
if (members) {
|
|
9388
|
+
members.delete(clientId);
|
|
9389
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
9390
|
+
}
|
|
9391
|
+
this.removePresence(clientId, channel);
|
|
9392
|
+
}
|
|
9393
|
+
/** Broadcast a message to all clients in a channel except sender */
|
|
9394
|
+
broadcastToChannel(clientId, channel, event, payload) {
|
|
9395
|
+
const members = this.channels.get(channel);
|
|
9396
|
+
if (!members) return;
|
|
9397
|
+
const message = JSON.stringify({
|
|
9398
|
+
type: "broadcast",
|
|
9399
|
+
channel,
|
|
9400
|
+
event,
|
|
9401
|
+
payload
|
|
9402
|
+
});
|
|
9403
|
+
for (const memberId of members) {
|
|
9404
|
+
if (memberId === clientId) continue;
|
|
9405
|
+
const ws = this.clients.get(memberId);
|
|
9406
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9407
|
+
ws.send(message);
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
}
|
|
9411
|
+
// =============================================================================
|
|
9412
|
+
// Presence
|
|
9413
|
+
// =============================================================================
|
|
9414
|
+
/** Track presence in a channel */
|
|
9415
|
+
trackPresence(clientId, channel, state) {
|
|
9416
|
+
if (!this.presence.has(channel)) {
|
|
9417
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
9418
|
+
}
|
|
9419
|
+
const channelPresence = this.presence.get(channel);
|
|
9420
|
+
channelPresence.set(clientId, {
|
|
9421
|
+
state,
|
|
9422
|
+
lastSeen: Date.now()
|
|
9423
|
+
});
|
|
9424
|
+
this.broadcastPresenceDiff(channel, {
|
|
9425
|
+
[clientId]: state
|
|
9426
|
+
}, {});
|
|
9427
|
+
this.ensurePresenceCleanup();
|
|
9428
|
+
}
|
|
9429
|
+
/** Remove presence from a channel */
|
|
9430
|
+
removePresence(clientId, channel) {
|
|
9431
|
+
const channelPresence = this.presence.get(channel);
|
|
9432
|
+
if (!channelPresence) return;
|
|
9433
|
+
const entry = channelPresence.get(clientId);
|
|
9434
|
+
if (entry) {
|
|
9435
|
+
channelPresence.delete(clientId);
|
|
9436
|
+
this.broadcastPresenceDiff(channel, {}, {
|
|
9437
|
+
[clientId]: entry.state
|
|
9438
|
+
});
|
|
9439
|
+
}
|
|
9440
|
+
if (channelPresence.size === 0) {
|
|
9441
|
+
this.presence.delete(channel);
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
/** Send full presence state to a specific client */
|
|
9445
|
+
sendPresenceState(clientId, channel) {
|
|
9446
|
+
const channelPresence = this.presence.get(channel);
|
|
9447
|
+
const presences = {};
|
|
9448
|
+
if (channelPresence) {
|
|
9449
|
+
for (const [id, {
|
|
9450
|
+
state
|
|
9451
|
+
}] of channelPresence) {
|
|
9452
|
+
presences[id] = state;
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
const ws = this.clients.get(clientId);
|
|
9456
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9457
|
+
ws.send(JSON.stringify({
|
|
9458
|
+
type: "presence_state",
|
|
9459
|
+
channel,
|
|
9460
|
+
presences
|
|
9461
|
+
}));
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
/** Broadcast presence diff (joins/leaves) to channel */
|
|
9465
|
+
broadcastPresenceDiff(channel, joins, leaves) {
|
|
9466
|
+
const members = this.channels.get(channel);
|
|
9467
|
+
if (!members) return;
|
|
9468
|
+
const message = JSON.stringify({
|
|
9469
|
+
type: "presence_diff",
|
|
9470
|
+
channel,
|
|
9471
|
+
joins,
|
|
9472
|
+
leaves
|
|
9473
|
+
});
|
|
9474
|
+
for (const memberId of members) {
|
|
9475
|
+
const ws = this.clients.get(memberId);
|
|
9476
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9477
|
+
ws.send(message);
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
/** Periodic cleanup for stale presences */
|
|
9482
|
+
ensurePresenceCleanup() {
|
|
9483
|
+
if (this.presenceInterval) return;
|
|
9484
|
+
this.presenceInterval = setInterval(() => {
|
|
9485
|
+
const now = Date.now();
|
|
9486
|
+
for (const [channel, channelPresence] of this.presence) {
|
|
9487
|
+
for (const [clientId, entry] of channelPresence) {
|
|
9488
|
+
if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
|
|
9489
|
+
this.removePresence(clientId, channel);
|
|
9490
|
+
}
|
|
9491
|
+
}
|
|
9492
|
+
}
|
|
9493
|
+
if (this.presence.size === 0 && this.presenceInterval) {
|
|
9494
|
+
clearInterval(this.presenceInterval);
|
|
9495
|
+
this.presenceInterval = void 0;
|
|
9496
|
+
}
|
|
9497
|
+
}, 1e4);
|
|
9498
|
+
}
|
|
9499
|
+
// =============================================================================
|
|
9109
9500
|
// Lifecycle / Cleanup
|
|
9110
9501
|
// =============================================================================
|
|
9111
9502
|
/**
|
|
@@ -9126,6 +9517,12 @@ class RealtimeService extends EventEmitter {
|
|
|
9126
9517
|
}
|
|
9127
9518
|
this._subscriptions.clear();
|
|
9128
9519
|
this.subscriptionCallbacks.clear();
|
|
9520
|
+
this.channels.clear();
|
|
9521
|
+
this.presence.clear();
|
|
9522
|
+
if (this.presenceInterval) {
|
|
9523
|
+
clearInterval(this.presenceInterval);
|
|
9524
|
+
this.presenceInterval = void 0;
|
|
9525
|
+
}
|
|
9129
9526
|
await this.stopListening();
|
|
9130
9527
|
this.clients.clear();
|
|
9131
9528
|
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
@@ -9772,8 +10169,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
9772
10169
|
break;
|
|
9773
10170
|
case "subscribe_collection":
|
|
9774
10171
|
case "subscribe_entity":
|
|
9775
|
-
case "unsubscribe":
|
|
9776
|
-
|
|
10172
|
+
case "unsubscribe":
|
|
10173
|
+
case "join_channel":
|
|
10174
|
+
case "leave_channel":
|
|
10175
|
+
case "broadcast":
|
|
10176
|
+
case "presence_track":
|
|
10177
|
+
case "presence_untrack":
|
|
10178
|
+
case "presence_state": {
|
|
10179
|
+
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
9777
10180
|
const session = clientSessions.get(clientId);
|
|
9778
10181
|
const authContext = session?.user ? {
|
|
9779
10182
|
userId: session.user.userId,
|
|
@@ -9892,11 +10295,6 @@ const DEFAULT_ROLES = [{
|
|
|
9892
10295
|
create: true,
|
|
9893
10296
|
edit: true,
|
|
9894
10297
|
delete: true
|
|
9895
|
-
},
|
|
9896
|
-
config: {
|
|
9897
|
-
createCollections: true,
|
|
9898
|
-
editCollections: "all",
|
|
9899
|
-
deleteCollections: "all"
|
|
9900
10298
|
}
|
|
9901
10299
|
}, {
|
|
9902
10300
|
id: "editor",
|
|
@@ -9907,11 +10305,6 @@ const DEFAULT_ROLES = [{
|
|
|
9907
10305
|
create: true,
|
|
9908
10306
|
edit: true,
|
|
9909
10307
|
delete: true
|
|
9910
|
-
},
|
|
9911
|
-
config: {
|
|
9912
|
-
createCollections: true,
|
|
9913
|
-
editCollections: "own",
|
|
9914
|
-
deleteCollections: "own"
|
|
9915
10308
|
}
|
|
9916
10309
|
}, {
|
|
9917
10310
|
id: "viewer",
|
|
@@ -9922,11 +10315,10 @@ const DEFAULT_ROLES = [{
|
|
|
9922
10315
|
create: false,
|
|
9923
10316
|
edit: false,
|
|
9924
10317
|
delete: false
|
|
9925
|
-
}
|
|
9926
|
-
config: null
|
|
10318
|
+
}
|
|
9927
10319
|
}];
|
|
9928
10320
|
async function ensureAuthTablesExist(db, registry) {
|
|
9929
|
-
|
|
10321
|
+
logger.info("🔍 Checking auth tables...");
|
|
9930
10322
|
try {
|
|
9931
10323
|
let usersTableName = '"users"';
|
|
9932
10324
|
let userIdType = "TEXT";
|
|
@@ -9996,7 +10388,6 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
9996
10388
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
9997
10389
|
default_permissions JSONB,
|
|
9998
10390
|
collection_permissions JSONB,
|
|
9999
|
-
config JSONB,
|
|
10000
10391
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10001
10392
|
)
|
|
10002
10393
|
`);
|
|
@@ -10079,34 +10470,85 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
10079
10470
|
`);
|
|
10080
10471
|
});
|
|
10081
10472
|
await seedDefaultRoles(db, rolesTableName);
|
|
10082
|
-
|
|
10473
|
+
await db.execute(sql`
|
|
10474
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10475
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
10476
|
+
`);
|
|
10477
|
+
const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
|
|
10478
|
+
const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
|
|
10479
|
+
const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
|
|
10480
|
+
await db.execute(sql`
|
|
10481
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
|
|
10482
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10483
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10484
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
10485
|
+
secret_encrypted TEXT NOT NULL,
|
|
10486
|
+
friendly_name TEXT,
|
|
10487
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
10488
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10489
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10490
|
+
)
|
|
10491
|
+
`);
|
|
10492
|
+
await db.execute(sql`
|
|
10493
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
10494
|
+
ON ${sql.raw(mfaFactorsTableName)}(user_id)
|
|
10495
|
+
`);
|
|
10496
|
+
await db.execute(sql`
|
|
10497
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
|
|
10498
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10499
|
+
factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
10500
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10501
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
10502
|
+
ip_address TEXT,
|
|
10503
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
10504
|
+
)
|
|
10505
|
+
`);
|
|
10506
|
+
await db.execute(sql`
|
|
10507
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
10508
|
+
ON ${sql.raw(mfaChallengesTableName)}(factor_id)
|
|
10509
|
+
`);
|
|
10510
|
+
await db.execute(sql`
|
|
10511
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
|
|
10512
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10513
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10514
|
+
code_hash TEXT NOT NULL,
|
|
10515
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
10516
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10517
|
+
)
|
|
10518
|
+
`);
|
|
10519
|
+
await db.execute(sql`
|
|
10520
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
10521
|
+
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
10522
|
+
`);
|
|
10523
|
+
logger.info("✅ Auth tables ready");
|
|
10083
10524
|
} catch (error) {
|
|
10084
|
-
|
|
10085
|
-
|
|
10525
|
+
logger.error("❌ Failed to create auth tables", {
|
|
10526
|
+
error
|
|
10527
|
+
});
|
|
10528
|
+
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
10086
10529
|
}
|
|
10087
10530
|
}
|
|
10088
10531
|
async function seedDefaultRoles(db, rolesTableName) {
|
|
10089
10532
|
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
10090
10533
|
const count2 = parseInt(result.rows[0]?.count || "0", 10);
|
|
10091
10534
|
if (count2 > 0) {
|
|
10092
|
-
|
|
10535
|
+
logger.info(`📋 Found ${count2} existing roles`);
|
|
10093
10536
|
return;
|
|
10094
10537
|
}
|
|
10095
|
-
|
|
10538
|
+
logger.info("🌱 Seeding default roles...");
|
|
10096
10539
|
for (const role of DEFAULT_ROLES) {
|
|
10097
10540
|
await db.execute(sql`
|
|
10098
|
-
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions
|
|
10541
|
+
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
10099
10542
|
VALUES (
|
|
10100
10543
|
${role.id},
|
|
10101
10544
|
${role.name},
|
|
10102
10545
|
${role.is_admin},
|
|
10103
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
10104
|
-
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
10546
|
+
${JSON.stringify(role.default_permissions)}::jsonb
|
|
10105
10547
|
)
|
|
10106
10548
|
ON CONFLICT (id) DO NOTHING
|
|
10107
10549
|
`);
|
|
10108
10550
|
}
|
|
10109
|
-
|
|
10551
|
+
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
10110
10552
|
}
|
|
10111
10553
|
function getColumnKey(table, ...keys2) {
|
|
10112
10554
|
if (!table) return void 0;
|
|
@@ -10160,12 +10602,13 @@ class UserService {
|
|
|
10160
10602
|
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10161
10603
|
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10162
10604
|
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10605
|
+
const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
|
|
10163
10606
|
const createdAt = row.created_at ?? row.createdAt;
|
|
10164
10607
|
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10165
10608
|
const metadata = {
|
|
10166
10609
|
...row.metadata || {}
|
|
10167
10610
|
};
|
|
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"]);
|
|
10611
|
+
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10169
10612
|
for (const [key, val] of Object.entries(row)) {
|
|
10170
10613
|
if (!knownKeys.has(key)) {
|
|
10171
10614
|
const camelKey = camelCase(key);
|
|
@@ -10181,6 +10624,7 @@ class UserService {
|
|
|
10181
10624
|
emailVerified,
|
|
10182
10625
|
emailVerificationToken,
|
|
10183
10626
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10627
|
+
isAnonymous,
|
|
10184
10628
|
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10185
10629
|
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10186
10630
|
metadata
|
|
@@ -10197,6 +10641,7 @@ class UserService {
|
|
|
10197
10641
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10198
10642
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10199
10643
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10644
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
10200
10645
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10201
10646
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10202
10647
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -10208,6 +10653,7 @@ class UserService {
|
|
|
10208
10653
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10209
10654
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10210
10655
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10656
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
10211
10657
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10212
10658
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10213
10659
|
const metadata = {
|
|
@@ -10216,7 +10662,7 @@ class UserService {
|
|
|
10216
10662
|
const remainingMetadata = {};
|
|
10217
10663
|
for (const [key, val] of Object.entries(metadata)) {
|
|
10218
10664
|
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) {
|
|
10665
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10220
10666
|
payload[tableColKey] = val;
|
|
10221
10667
|
} else {
|
|
10222
10668
|
remainingMetadata[key] = val;
|
|
@@ -10404,7 +10850,7 @@ class UserService {
|
|
|
10404
10850
|
async getUserRoles(userId) {
|
|
10405
10851
|
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
10406
10852
|
const result = await this.db.execute(sql`
|
|
10407
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10853
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10408
10854
|
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10409
10855
|
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
10410
10856
|
WHERE ur.user_id = ${userId}
|
|
@@ -10414,8 +10860,7 @@ class UserService {
|
|
|
10414
10860
|
name: row.name,
|
|
10415
10861
|
isAdmin: row.is_admin,
|
|
10416
10862
|
defaultPermissions: row.default_permissions,
|
|
10417
|
-
collectionPermissions: row.collection_permissions
|
|
10418
|
-
config: row.config
|
|
10863
|
+
collectionPermissions: row.collection_permissions
|
|
10419
10864
|
}));
|
|
10420
10865
|
}
|
|
10421
10866
|
/**
|
|
@@ -10481,7 +10926,7 @@ class RoleService {
|
|
|
10481
10926
|
async getRoleById(id) {
|
|
10482
10927
|
const tableName = this.getQualifiedRolesTableName();
|
|
10483
10928
|
const result = await this.db.execute(sql`
|
|
10484
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10929
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10485
10930
|
FROM ${sql.raw(tableName)}
|
|
10486
10931
|
WHERE id = ${id}
|
|
10487
10932
|
`);
|
|
@@ -10492,14 +10937,13 @@ class RoleService {
|
|
|
10492
10937
|
name: row.name,
|
|
10493
10938
|
isAdmin: row.is_admin,
|
|
10494
10939
|
defaultPermissions: row.default_permissions,
|
|
10495
|
-
collectionPermissions: row.collection_permissions
|
|
10496
|
-
config: row.config
|
|
10940
|
+
collectionPermissions: row.collection_permissions
|
|
10497
10941
|
};
|
|
10498
10942
|
}
|
|
10499
10943
|
async listRoles() {
|
|
10500
10944
|
const tableName = this.getQualifiedRolesTableName();
|
|
10501
10945
|
const result = await this.db.execute(sql`
|
|
10502
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10946
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10503
10947
|
FROM ${sql.raw(tableName)}
|
|
10504
10948
|
ORDER BY name
|
|
10505
10949
|
`);
|
|
@@ -10508,23 +10952,21 @@ class RoleService {
|
|
|
10508
10952
|
name: row.name,
|
|
10509
10953
|
isAdmin: row.is_admin,
|
|
10510
10954
|
defaultPermissions: row.default_permissions,
|
|
10511
|
-
collectionPermissions: row.collection_permissions
|
|
10512
|
-
config: row.config
|
|
10955
|
+
collectionPermissions: row.collection_permissions
|
|
10513
10956
|
}));
|
|
10514
10957
|
}
|
|
10515
10958
|
async createRole(data) {
|
|
10516
10959
|
const tableName = this.getQualifiedRolesTableName();
|
|
10517
10960
|
const result = await this.db.execute(sql`
|
|
10518
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
10961
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
10519
10962
|
VALUES (
|
|
10520
10963
|
${data.id},
|
|
10521
10964
|
${data.name},
|
|
10522
10965
|
${data.isAdmin ?? false},
|
|
10523
10966
|
${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
|
|
10967
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10526
10968
|
)
|
|
10527
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10969
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10528
10970
|
`);
|
|
10529
10971
|
const row = result.rows[0];
|
|
10530
10972
|
return {
|
|
@@ -10532,8 +10974,7 @@ class RoleService {
|
|
|
10532
10974
|
name: row.name,
|
|
10533
10975
|
isAdmin: row.is_admin,
|
|
10534
10976
|
defaultPermissions: row.default_permissions,
|
|
10535
|
-
collectionPermissions: row.collection_permissions
|
|
10536
|
-
config: row.config
|
|
10977
|
+
collectionPermissions: row.collection_permissions
|
|
10537
10978
|
};
|
|
10538
10979
|
}
|
|
10539
10980
|
async updateRole(id, data) {
|
|
@@ -10546,8 +10987,7 @@ class RoleService {
|
|
|
10546
10987
|
name = ${data.name ?? existing.name},
|
|
10547
10988
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
10548
10989
|
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
|
|
10990
|
+
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10551
10991
|
WHERE id = ${id}
|
|
10552
10992
|
`);
|
|
10553
10993
|
return this.getRoleById(id);
|
|
@@ -10826,8 +11266,7 @@ class PostgresAuthRepository {
|
|
|
10826
11266
|
return this.roleService.createRole({
|
|
10827
11267
|
...data,
|
|
10828
11268
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
10829
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
10830
|
-
config: data.config ?? null
|
|
11269
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
10831
11270
|
});
|
|
10832
11271
|
}
|
|
10833
11272
|
async updateRole(id, data) {
|
|
@@ -10870,6 +11309,219 @@ class PostgresAuthRepository {
|
|
|
10870
11309
|
async deleteExpiredTokens() {
|
|
10871
11310
|
await this.tokenRepository.deleteExpiredTokens();
|
|
10872
11311
|
}
|
|
11312
|
+
// MFA operations (delegate to MfaService)
|
|
11313
|
+
_mfaService = null;
|
|
11314
|
+
getMfaService() {
|
|
11315
|
+
if (!this._mfaService) {
|
|
11316
|
+
this._mfaService = new MfaService(this.db);
|
|
11317
|
+
}
|
|
11318
|
+
return this._mfaService;
|
|
11319
|
+
}
|
|
11320
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11321
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
11322
|
+
}
|
|
11323
|
+
async getMfaFactors(userId) {
|
|
11324
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
11325
|
+
}
|
|
11326
|
+
async getMfaFactorById(factorId) {
|
|
11327
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
11328
|
+
}
|
|
11329
|
+
async verifyMfaFactor(factorId) {
|
|
11330
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
11331
|
+
}
|
|
11332
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11333
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
11334
|
+
}
|
|
11335
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11336
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
11337
|
+
}
|
|
11338
|
+
async getMfaChallengeById(challengeId) {
|
|
11339
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
11340
|
+
}
|
|
11341
|
+
async verifyMfaChallenge(challengeId) {
|
|
11342
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
11343
|
+
}
|
|
11344
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11345
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
11346
|
+
}
|
|
11347
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11348
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
11349
|
+
}
|
|
11350
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11351
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
11352
|
+
}
|
|
11353
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11354
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
11355
|
+
}
|
|
11356
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11357
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
11358
|
+
}
|
|
11359
|
+
}
|
|
11360
|
+
class MfaService {
|
|
11361
|
+
constructor(db, schemaName = "rebase") {
|
|
11362
|
+
this.db = db;
|
|
11363
|
+
this.schemaName = schemaName;
|
|
11364
|
+
}
|
|
11365
|
+
qualify(tableName) {
|
|
11366
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
11367
|
+
}
|
|
11368
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11369
|
+
const tableName = this.qualify("mfa_factors");
|
|
11370
|
+
const result = await this.db.execute(sql`
|
|
11371
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
11372
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
11373
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11374
|
+
`);
|
|
11375
|
+
const row = result.rows[0];
|
|
11376
|
+
return {
|
|
11377
|
+
id: row.id,
|
|
11378
|
+
userId: row.user_id,
|
|
11379
|
+
factorType: row.factor_type,
|
|
11380
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11381
|
+
verified: row.verified,
|
|
11382
|
+
createdAt: new Date(row.created_at),
|
|
11383
|
+
updatedAt: new Date(row.updated_at)
|
|
11384
|
+
};
|
|
11385
|
+
}
|
|
11386
|
+
async getMfaFactors(userId) {
|
|
11387
|
+
const tableName = this.qualify("mfa_factors");
|
|
11388
|
+
const result = await this.db.execute(sql`
|
|
11389
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11390
|
+
FROM ${sql.raw(tableName)}
|
|
11391
|
+
WHERE user_id = ${userId}
|
|
11392
|
+
ORDER BY created_at
|
|
11393
|
+
`);
|
|
11394
|
+
return result.rows.map((row) => ({
|
|
11395
|
+
id: row.id,
|
|
11396
|
+
userId: row.user_id,
|
|
11397
|
+
factorType: row.factor_type,
|
|
11398
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11399
|
+
verified: row.verified,
|
|
11400
|
+
createdAt: new Date(row.created_at),
|
|
11401
|
+
updatedAt: new Date(row.updated_at)
|
|
11402
|
+
}));
|
|
11403
|
+
}
|
|
11404
|
+
async getMfaFactorById(factorId) {
|
|
11405
|
+
const tableName = this.qualify("mfa_factors");
|
|
11406
|
+
const result = await this.db.execute(sql`
|
|
11407
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
11408
|
+
FROM ${sql.raw(tableName)}
|
|
11409
|
+
WHERE id = ${factorId}
|
|
11410
|
+
`);
|
|
11411
|
+
if (result.rows.length === 0) return null;
|
|
11412
|
+
const row = result.rows[0];
|
|
11413
|
+
return {
|
|
11414
|
+
id: row.id,
|
|
11415
|
+
userId: row.user_id,
|
|
11416
|
+
factorType: row.factor_type,
|
|
11417
|
+
secretEncrypted: row.secret_encrypted,
|
|
11418
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11419
|
+
verified: row.verified,
|
|
11420
|
+
createdAt: new Date(row.created_at),
|
|
11421
|
+
updatedAt: new Date(row.updated_at)
|
|
11422
|
+
};
|
|
11423
|
+
}
|
|
11424
|
+
async verifyMfaFactor(factorId) {
|
|
11425
|
+
const tableName = this.qualify("mfa_factors");
|
|
11426
|
+
await this.db.execute(sql`
|
|
11427
|
+
UPDATE ${sql.raw(tableName)}
|
|
11428
|
+
SET verified = TRUE, updated_at = NOW()
|
|
11429
|
+
WHERE id = ${factorId}
|
|
11430
|
+
`);
|
|
11431
|
+
}
|
|
11432
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11433
|
+
const tableName = this.qualify("mfa_factors");
|
|
11434
|
+
await this.db.execute(sql`
|
|
11435
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
11436
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
11437
|
+
`);
|
|
11438
|
+
}
|
|
11439
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11440
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11441
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
|
|
11442
|
+
const result = await this.db.execute(sql`
|
|
11443
|
+
INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
11444
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
11445
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
11446
|
+
`);
|
|
11447
|
+
const row = result.rows[0];
|
|
11448
|
+
return {
|
|
11449
|
+
id: row.id,
|
|
11450
|
+
factorId: row.factor_id,
|
|
11451
|
+
createdAt: new Date(row.created_at),
|
|
11452
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11453
|
+
ipAddress: row.ip_address ?? void 0
|
|
11454
|
+
};
|
|
11455
|
+
}
|
|
11456
|
+
async getMfaChallengeById(challengeId) {
|
|
11457
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11458
|
+
const result = await this.db.execute(sql`
|
|
11459
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
11460
|
+
FROM ${sql.raw(tableName)}
|
|
11461
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
11462
|
+
`);
|
|
11463
|
+
if (result.rows.length === 0) return null;
|
|
11464
|
+
const row = result.rows[0];
|
|
11465
|
+
return {
|
|
11466
|
+
id: row.id,
|
|
11467
|
+
factorId: row.factor_id,
|
|
11468
|
+
createdAt: new Date(row.created_at),
|
|
11469
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11470
|
+
ipAddress: row.ip_address ?? void 0
|
|
11471
|
+
};
|
|
11472
|
+
}
|
|
11473
|
+
async verifyMfaChallenge(challengeId) {
|
|
11474
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11475
|
+
await this.db.execute(sql`
|
|
11476
|
+
UPDATE ${sql.raw(tableName)}
|
|
11477
|
+
SET verified_at = NOW()
|
|
11478
|
+
WHERE id = ${challengeId}
|
|
11479
|
+
`);
|
|
11480
|
+
}
|
|
11481
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11482
|
+
const tableName = this.qualify("recovery_codes");
|
|
11483
|
+
await this.db.execute(sql`
|
|
11484
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11485
|
+
`);
|
|
11486
|
+
for (const hash of codeHashes) {
|
|
11487
|
+
await this.db.execute(sql`
|
|
11488
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
|
|
11489
|
+
VALUES (${userId}, ${hash})
|
|
11490
|
+
`);
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
11493
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11494
|
+
const tableName = this.qualify("recovery_codes");
|
|
11495
|
+
const result = await this.db.execute(sql`
|
|
11496
|
+
UPDATE ${sql.raw(tableName)}
|
|
11497
|
+
SET used_at = NOW()
|
|
11498
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
11499
|
+
RETURNING id
|
|
11500
|
+
`);
|
|
11501
|
+
return result.rows.length > 0;
|
|
11502
|
+
}
|
|
11503
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11504
|
+
const tableName = this.qualify("recovery_codes");
|
|
11505
|
+
const result = await this.db.execute(sql`
|
|
11506
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11507
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
11508
|
+
`);
|
|
11509
|
+
return result.rows[0].count;
|
|
11510
|
+
}
|
|
11511
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11512
|
+
const tableName = this.qualify("recovery_codes");
|
|
11513
|
+
await this.db.execute(sql`
|
|
11514
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11515
|
+
`);
|
|
11516
|
+
}
|
|
11517
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11518
|
+
const tableName = this.qualify("mfa_factors");
|
|
11519
|
+
const result = await this.db.execute(sql`
|
|
11520
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11521
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
11522
|
+
`);
|
|
11523
|
+
return result.rows[0].count > 0;
|
|
11524
|
+
}
|
|
10873
11525
|
}
|
|
10874
11526
|
const DEFAULT_RETENTION = {
|
|
10875
11527
|
maxEntries: 200,
|
|
@@ -11080,7 +11732,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11080
11732
|
const registry = new PostgresCollectionRegistry();
|
|
11081
11733
|
if (collections) {
|
|
11082
11734
|
registry.registerMultiple(collections);
|
|
11083
|
-
|
|
11735
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
11084
11736
|
}
|
|
11085
11737
|
if (pgConfig.schema?.tables) {
|
|
11086
11738
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -11106,10 +11758,28 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11106
11758
|
try {
|
|
11107
11759
|
await schemaAwareDb.execute(sql`SELECT 1`);
|
|
11108
11760
|
} catch (err) {
|
|
11109
|
-
|
|
11110
|
-
|
|
11761
|
+
logger.error("❌ Failed to connect to PostgreSQL", {
|
|
11762
|
+
error: err
|
|
11763
|
+
});
|
|
11764
|
+
logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
11111
11765
|
}
|
|
11112
11766
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11767
|
+
let readDb;
|
|
11768
|
+
const readUrl = process.env.DATABASE_READ_URL;
|
|
11769
|
+
if (readUrl && readUrl !== pgConfig.connectionString) {
|
|
11770
|
+
try {
|
|
11771
|
+
const {
|
|
11772
|
+
createReadReplicaConnection: createReadReplicaConnection2
|
|
11773
|
+
} = await Promise.resolve().then(() => connection);
|
|
11774
|
+
const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
|
|
11775
|
+
readDb = readResources.db;
|
|
11776
|
+
logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11777
|
+
} catch (err) {
|
|
11778
|
+
logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
|
|
11779
|
+
error: err
|
|
11780
|
+
});
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
11113
11783
|
const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
|
|
11114
11784
|
const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
|
|
11115
11785
|
realtimeService.setDataDriver(driver);
|
|
@@ -11117,18 +11787,24 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11117
11787
|
try {
|
|
11118
11788
|
await driver.branchService.ensureBranchMetadataTable();
|
|
11119
11789
|
} catch (err) {
|
|
11120
|
-
|
|
11790
|
+
logger.warn("⚠️ Could not initialize branch metadata table", {
|
|
11791
|
+
error: err
|
|
11792
|
+
});
|
|
11121
11793
|
}
|
|
11122
11794
|
}
|
|
11123
|
-
|
|
11795
|
+
const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
|
|
11796
|
+
if (directUrl) {
|
|
11124
11797
|
try {
|
|
11125
|
-
await realtimeService.startListening(
|
|
11798
|
+
await realtimeService.startListening(directUrl);
|
|
11126
11799
|
} catch (err) {
|
|
11127
|
-
|
|
11800
|
+
logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
11801
|
+
error: err
|
|
11802
|
+
});
|
|
11128
11803
|
}
|
|
11129
11804
|
}
|
|
11130
11805
|
const internals = {
|
|
11131
11806
|
db: schemaAwareDb,
|
|
11807
|
+
readDb,
|
|
11132
11808
|
registry,
|
|
11133
11809
|
realtimeService,
|
|
11134
11810
|
driver,
|
|
@@ -11266,14 +11942,22 @@ export {
|
|
|
11266
11942
|
RealtimeService,
|
|
11267
11943
|
appConfig,
|
|
11268
11944
|
createAuthSchema,
|
|
11945
|
+
createDirectDatabaseConnection,
|
|
11269
11946
|
createPostgresAdapter,
|
|
11270
11947
|
createPostgresBootstrapper,
|
|
11271
11948
|
createPostgresDatabaseConnection,
|
|
11272
11949
|
createPostgresWebSocket,
|
|
11950
|
+
createReadReplicaConnection,
|
|
11273
11951
|
generateSchema,
|
|
11952
|
+
mfaChallenges,
|
|
11953
|
+
mfaChallengesRelations,
|
|
11954
|
+
mfaFactors,
|
|
11955
|
+
mfaFactorsRelations,
|
|
11274
11956
|
passwordResetTokens,
|
|
11275
11957
|
passwordResetTokensRelations,
|
|
11276
11958
|
rebaseSchema,
|
|
11959
|
+
recoveryCodes,
|
|
11960
|
+
recoveryCodesRelations,
|
|
11277
11961
|
refreshTokens,
|
|
11278
11962
|
refreshTokensRelations,
|
|
11279
11963
|
roles,
|