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