@rebasepro/server-postgresql 0.1.2 → 0.2.1
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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1160 -612
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1158 -610
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +21 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +57 -8
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +44 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +68 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +85 -8
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +17 -0
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
package/dist/index.es.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Pool, Client } from "pg";
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
|
|
4
|
-
import { pgSchema, timestamp, varchar, boolean, uuid,
|
|
4
|
+
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, varchar, boolean, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
|
|
5
5
|
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import { promises } from "fs";
|
|
@@ -51,6 +51,12 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
51
51
|
connectionString
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
class Vector {
|
|
55
|
+
value;
|
|
56
|
+
constructor(value) {
|
|
57
|
+
this.value = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
function isPostgresCollection(collection) {
|
|
55
61
|
return !collection.driver || collection.driver === "postgres";
|
|
56
62
|
}
|
|
@@ -127,16 +133,25 @@ const DEFAULT_ONE_OF_VALUE = "value";
|
|
|
127
133
|
const tokenizeRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;
|
|
128
134
|
const snakeCaseRegex = tokenizeRegex;
|
|
129
135
|
const toSnakeCase = (str) => {
|
|
136
|
+
if (!str || typeof str !== "string") return "";
|
|
130
137
|
const regExpMatchArray = str.match(snakeCaseRegex);
|
|
131
138
|
if (!regExpMatchArray) return "";
|
|
132
139
|
return regExpMatchArray.map((x) => x.toLowerCase()).join("_");
|
|
133
140
|
};
|
|
141
|
+
function camelCase(str) {
|
|
142
|
+
if (!str) return "";
|
|
143
|
+
if (str.length === 1) return str.toLowerCase();
|
|
144
|
+
const parts = str.split(/[-_ ]+/).filter(Boolean);
|
|
145
|
+
if (parts.length === 0) return "";
|
|
146
|
+
return parts[0].toLowerCase() + // Transform remaining parts to have first letter uppercase
|
|
147
|
+
parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.substring(1).toLowerCase()).join("");
|
|
148
|
+
}
|
|
134
149
|
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
135
150
|
function commonjsRequire(path2) {
|
|
136
151
|
throw new Error('Could not dynamically require "' + path2 + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
|
|
137
152
|
}
|
|
138
153
|
var object_hash = { exports: {} };
|
|
139
|
-
(function(module, exports
|
|
154
|
+
(function(module, exports) {
|
|
140
155
|
!function(e) {
|
|
141
156
|
module.exports = e();
|
|
142
157
|
}(function() {
|
|
@@ -1255,14 +1270,56 @@ const buildPropertyCallbacks = (properties) => {
|
|
|
1255
1270
|
}
|
|
1256
1271
|
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1257
1272
|
};
|
|
1258
|
-
function sanitizeRelation(relation, sourceCollection) {
|
|
1273
|
+
function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
1259
1274
|
if (!relation.target) {
|
|
1260
1275
|
throw new Error("Relation is missing a `target` collection.");
|
|
1261
1276
|
}
|
|
1262
|
-
const
|
|
1277
|
+
const rawTarget = relation.target;
|
|
1278
|
+
let targetCollection;
|
|
1279
|
+
if (typeof rawTarget === "string") {
|
|
1280
|
+
if (resolveCollection) {
|
|
1281
|
+
targetCollection = resolveCollection(rawTarget);
|
|
1282
|
+
}
|
|
1283
|
+
if (!targetCollection) {
|
|
1284
|
+
targetCollection = {
|
|
1285
|
+
slug: rawTarget,
|
|
1286
|
+
name: rawTarget
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
} else if (typeof rawTarget === "function") {
|
|
1290
|
+
const evaluated = rawTarget();
|
|
1291
|
+
if (typeof evaluated === "string") {
|
|
1292
|
+
if (resolveCollection) {
|
|
1293
|
+
targetCollection = resolveCollection(evaluated);
|
|
1294
|
+
}
|
|
1295
|
+
if (!targetCollection) {
|
|
1296
|
+
targetCollection = {
|
|
1297
|
+
slug: evaluated,
|
|
1298
|
+
name: evaluated
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
targetCollection = evaluated;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!targetCollection) {
|
|
1306
|
+
throw new Error("Relation is missing a valid `target` collection.");
|
|
1307
|
+
}
|
|
1263
1308
|
const newRelation = {
|
|
1264
1309
|
...relation
|
|
1265
1310
|
};
|
|
1311
|
+
newRelation.target = () => {
|
|
1312
|
+
if (typeof rawTarget === "string") {
|
|
1313
|
+
return resolveCollection && resolveCollection(rawTarget) || targetCollection;
|
|
1314
|
+
} else if (typeof rawTarget === "function") {
|
|
1315
|
+
const evaluated = rawTarget();
|
|
1316
|
+
if (typeof evaluated === "string") {
|
|
1317
|
+
return resolveCollection && resolveCollection(evaluated) || targetCollection;
|
|
1318
|
+
}
|
|
1319
|
+
return evaluated;
|
|
1320
|
+
}
|
|
1321
|
+
return targetCollection;
|
|
1322
|
+
};
|
|
1266
1323
|
if (!newRelation.relationName) {
|
|
1267
1324
|
newRelation.relationName = toSnakeCase(targetCollection.slug);
|
|
1268
1325
|
}
|
|
@@ -1315,6 +1372,17 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1315
1372
|
break;
|
|
1316
1373
|
}
|
|
1317
1374
|
}
|
|
1375
|
+
if (!isManyToManyInverse && targetCollection.properties) {
|
|
1376
|
+
for (const [propKey, prop] of Object.entries(targetCollection.properties)) {
|
|
1377
|
+
if (prop.type !== "relation") continue;
|
|
1378
|
+
const relProp = prop;
|
|
1379
|
+
const relName = relProp.relationName || propKey;
|
|
1380
|
+
if (relName === newRelation.inverseRelationName && relProp.cardinality === "many" && (relProp.direction === "owning" || !relProp.direction)) {
|
|
1381
|
+
isManyToManyInverse = true;
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1318
1386
|
} catch (e) {
|
|
1319
1387
|
}
|
|
1320
1388
|
}
|
|
@@ -1437,7 +1505,7 @@ function findRelation(resolvedRelations, key) {
|
|
|
1437
1505
|
return void 0;
|
|
1438
1506
|
}
|
|
1439
1507
|
var logic = { exports: {} };
|
|
1440
|
-
(function(module, exports
|
|
1508
|
+
(function(module, exports) {
|
|
1441
1509
|
(function(root, factory) {
|
|
1442
1510
|
{
|
|
1443
1511
|
module.exports = factory();
|
|
@@ -2194,7 +2262,7 @@ function createSupportedComparatorMap({ areArrayBuffersEqual: areArrayBuffersEqu
|
|
|
2194
2262
|
"[object Uint32Array]": areTypedArraysEqual2
|
|
2195
2263
|
};
|
|
2196
2264
|
}
|
|
2197
|
-
const deepEqual = createCustomEqual();
|
|
2265
|
+
const deepEqual$1 = createCustomEqual();
|
|
2198
2266
|
createCustomEqual({ strict: true });
|
|
2199
2267
|
createCustomEqual({ circular: true });
|
|
2200
2268
|
createCustomEqual({
|
|
@@ -2263,10 +2331,16 @@ class CollectionRegistry {
|
|
|
2263
2331
|
*/
|
|
2264
2332
|
registerMultiple(collections) {
|
|
2265
2333
|
const rawSnapshot = collections.map((c) => removeFunctions(c));
|
|
2266
|
-
if (this.lastRawInputSnapshot && deepEqual(this.lastRawInputSnapshot, rawSnapshot)) {
|
|
2334
|
+
if (this.lastRawInputSnapshot && deepEqual$1(this.lastRawInputSnapshot, rawSnapshot)) {
|
|
2267
2335
|
return false;
|
|
2268
2336
|
}
|
|
2269
2337
|
this.reset();
|
|
2338
|
+
collections.forEach((c) => {
|
|
2339
|
+
if (c.slug) {
|
|
2340
|
+
this.collectionsBySlug.set(c.slug, c);
|
|
2341
|
+
}
|
|
2342
|
+
this.collectionsByTableName.set(getTableName(c), c);
|
|
2343
|
+
});
|
|
2270
2344
|
const normalizedCollections = collections.map((c) => this.normalizeCollection({
|
|
2271
2345
|
...c
|
|
2272
2346
|
}));
|
|
@@ -2345,7 +2419,7 @@ class CollectionRegistry {
|
|
|
2345
2419
|
if (getDataSourceCapabilities(result.driver).supportsRelations) {
|
|
2346
2420
|
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
2347
2421
|
try {
|
|
2348
|
-
return sanitizeRelation(r, result);
|
|
2422
|
+
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
2349
2423
|
} catch {
|
|
2350
2424
|
return r;
|
|
2351
2425
|
}
|
|
@@ -2784,8 +2858,14 @@ class DrizzleConditionBuilder {
|
|
|
2784
2858
|
static buildSingleFilterCondition(column, op, value) {
|
|
2785
2859
|
switch (op) {
|
|
2786
2860
|
case "==":
|
|
2861
|
+
if (value === null || value === void 0) {
|
|
2862
|
+
return sql`${column} IS NULL`;
|
|
2863
|
+
}
|
|
2787
2864
|
return eq(column, value);
|
|
2788
2865
|
case "!=":
|
|
2866
|
+
if (value === null || value === void 0) {
|
|
2867
|
+
return sql`${column} IS NOT NULL`;
|
|
2868
|
+
}
|
|
2789
2869
|
return sql`${column} != ${value}`;
|
|
2790
2870
|
case ">":
|
|
2791
2871
|
return sql`${column} > ${value}`;
|
|
@@ -3116,7 +3196,10 @@ class DrizzleConditionBuilder {
|
|
|
3116
3196
|
if (p.type === "string" && !p.enum && p.isId !== "uuid") {
|
|
3117
3197
|
const fieldColumn = table[key];
|
|
3118
3198
|
if (fieldColumn) {
|
|
3119
|
-
|
|
3199
|
+
const supportsILike = fieldColumn instanceof PgVarchar || fieldColumn instanceof PgText || fieldColumn instanceof PgChar || fieldColumn && typeof fieldColumn === "object" && !("columnType" in fieldColumn);
|
|
3200
|
+
if (supportsILike) {
|
|
3201
|
+
searchConditions.push(ilike(fieldColumn, `%${searchString}%`));
|
|
3202
|
+
}
|
|
3120
3203
|
}
|
|
3121
3204
|
}
|
|
3122
3205
|
}
|
|
@@ -3589,6 +3672,31 @@ function serializePropertyToServer(value, property) {
|
|
|
3589
3672
|
return result;
|
|
3590
3673
|
}
|
|
3591
3674
|
return value;
|
|
3675
|
+
case "vector": {
|
|
3676
|
+
if (value instanceof Vector) {
|
|
3677
|
+
return value.value;
|
|
3678
|
+
}
|
|
3679
|
+
if (value && typeof value === "object" && "value" in value && Array.isArray(value.value)) {
|
|
3680
|
+
return value.value.map(Number);
|
|
3681
|
+
}
|
|
3682
|
+
if (Array.isArray(value)) {
|
|
3683
|
+
return value.map(Number);
|
|
3684
|
+
}
|
|
3685
|
+
return value;
|
|
3686
|
+
}
|
|
3687
|
+
case "binary":
|
|
3688
|
+
if (typeof value === "string") {
|
|
3689
|
+
if (value.startsWith("data:application/octet-stream;base64,")) {
|
|
3690
|
+
const base64Data = value.split(",")[1];
|
|
3691
|
+
if (base64Data) {
|
|
3692
|
+
return Buffer.from(base64Data, "base64");
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
if (Buffer.isBuffer(value)) {
|
|
3697
|
+
return value;
|
|
3698
|
+
}
|
|
3699
|
+
return value;
|
|
3592
3700
|
case "string":
|
|
3593
3701
|
if (typeof value === "string") {
|
|
3594
3702
|
if (value.startsWith("data:application/octet-stream;base64,")) {
|
|
@@ -3733,6 +3841,21 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
3733
3841
|
return value;
|
|
3734
3842
|
}
|
|
3735
3843
|
switch (property.type) {
|
|
3844
|
+
case "binary": {
|
|
3845
|
+
let buf = null;
|
|
3846
|
+
if (Buffer.isBuffer(value)) {
|
|
3847
|
+
buf = value;
|
|
3848
|
+
} else if (typeof value === "object" && value !== null) {
|
|
3849
|
+
const rawVal = value;
|
|
3850
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
3851
|
+
buf = Buffer.from(rawVal.data);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
if (buf) {
|
|
3855
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
3856
|
+
}
|
|
3857
|
+
return value;
|
|
3858
|
+
}
|
|
3736
3859
|
case "string": {
|
|
3737
3860
|
if (typeof value === "string") return value;
|
|
3738
3861
|
let isBuffer = false;
|
|
@@ -3740,9 +3863,12 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
3740
3863
|
if (Buffer.isBuffer(value)) {
|
|
3741
3864
|
isBuffer = true;
|
|
3742
3865
|
buf = value;
|
|
3743
|
-
} else if (typeof value === "object" && value !== null
|
|
3744
|
-
|
|
3745
|
-
|
|
3866
|
+
} else if (typeof value === "object" && value !== null) {
|
|
3867
|
+
const rawVal = value;
|
|
3868
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
3869
|
+
isBuffer = true;
|
|
3870
|
+
buf = Buffer.from(rawVal.data);
|
|
3871
|
+
}
|
|
3746
3872
|
}
|
|
3747
3873
|
if (isBuffer && buf) {
|
|
3748
3874
|
let isPrintable = true;
|
|
@@ -3829,6 +3955,25 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
3829
3955
|
return isNaN(parsed) ? null : parsed;
|
|
3830
3956
|
}
|
|
3831
3957
|
return value;
|
|
3958
|
+
case "vector": {
|
|
3959
|
+
let nums = [];
|
|
3960
|
+
if (typeof value === "string") {
|
|
3961
|
+
nums = value.slice(1, -1).split(",").map(Number);
|
|
3962
|
+
} else if (Array.isArray(value)) {
|
|
3963
|
+
nums = value.map(Number);
|
|
3964
|
+
} else if (value instanceof Vector) {
|
|
3965
|
+
nums = value.value;
|
|
3966
|
+
} else if (typeof value === "object" && value !== null && "value" in value) {
|
|
3967
|
+
const valObj = value;
|
|
3968
|
+
if (Array.isArray(valObj.value)) {
|
|
3969
|
+
nums = valObj.value.map(Number);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return {
|
|
3973
|
+
__type: "Vector",
|
|
3974
|
+
value: nums
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3832
3977
|
case "date": {
|
|
3833
3978
|
let date;
|
|
3834
3979
|
if (value instanceof Date) {
|
|
@@ -3853,9 +3998,12 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
3853
3998
|
if (Buffer.isBuffer(value)) {
|
|
3854
3999
|
isBuffer = true;
|
|
3855
4000
|
buf = value;
|
|
3856
|
-
} else if (typeof value === "object" && value !== null
|
|
3857
|
-
|
|
3858
|
-
|
|
4001
|
+
} else if (typeof value === "object" && value !== null) {
|
|
4002
|
+
const rawVal = value;
|
|
4003
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
4004
|
+
isBuffer = true;
|
|
4005
|
+
buf = Buffer.from(rawVal.data);
|
|
4006
|
+
}
|
|
3859
4007
|
}
|
|
3860
4008
|
if (isBuffer && buf) {
|
|
3861
4009
|
let isPrintable = true;
|
|
@@ -4645,7 +4793,7 @@ class RelationService {
|
|
|
4645
4793
|
if (parentFKValue !== null && parentFKValue !== void 0) {
|
|
4646
4794
|
await tx.update(targetTable).set({
|
|
4647
4795
|
[targetFKColName]: null
|
|
4648
|
-
}).where(eq(targetFKCol, parentFKValue));
|
|
4796
|
+
}).where(eq(targetFKCol, String(parentFKValue)));
|
|
4649
4797
|
}
|
|
4650
4798
|
continue;
|
|
4651
4799
|
}
|
|
@@ -4654,7 +4802,7 @@ class RelationService {
|
|
|
4654
4802
|
if (parentFKValue !== null && parentFKValue !== void 0) {
|
|
4655
4803
|
await tx.update(targetTable).set({
|
|
4656
4804
|
[targetFKColName]: null
|
|
4657
|
-
}).where(eq(targetFKCol, parentFKValue));
|
|
4805
|
+
}).where(eq(targetFKCol, String(parentFKValue)));
|
|
4658
4806
|
} else {
|
|
4659
4807
|
console.warn(`Cannot set joinPath relation '${relation.relationName}' because parent FK value is null/undefined`);
|
|
4660
4808
|
continue;
|
|
@@ -6731,23 +6879,33 @@ class PostgresBackendDriver {
|
|
|
6731
6879
|
client: this.client
|
|
6732
6880
|
};
|
|
6733
6881
|
if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
6882
|
+
let preventDefault = false;
|
|
6734
6883
|
if (callbacks?.beforeDelete) {
|
|
6735
|
-
await callbacks.beforeDelete({
|
|
6884
|
+
const result = await callbacks.beforeDelete({
|
|
6736
6885
|
collection: resolvedCollection,
|
|
6737
6886
|
path: entity.path,
|
|
6738
6887
|
entityId: entity.id,
|
|
6739
6888
|
entity,
|
|
6740
6889
|
context: contextForCallback
|
|
6741
6890
|
});
|
|
6891
|
+
if (result === false) {
|
|
6892
|
+
preventDefault = true;
|
|
6893
|
+
}
|
|
6742
6894
|
}
|
|
6743
6895
|
if (propertyCallbacks?.beforeDelete) {
|
|
6744
|
-
await propertyCallbacks.beforeDelete({
|
|
6896
|
+
const result = await propertyCallbacks.beforeDelete({
|
|
6745
6897
|
collection: resolvedCollection,
|
|
6746
6898
|
path: entity.path,
|
|
6747
6899
|
entityId: entity.id,
|
|
6748
6900
|
entity,
|
|
6749
6901
|
context: contextForCallback
|
|
6750
6902
|
});
|
|
6903
|
+
if (result === false) {
|
|
6904
|
+
preventDefault = true;
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
if (preventDefault) {
|
|
6908
|
+
return;
|
|
6751
6909
|
}
|
|
6752
6910
|
}
|
|
6753
6911
|
await this.entityService.deleteEntity(entity.path, entity.id, entity.databaseId || resolvedCollection?.databaseId);
|
|
@@ -6820,7 +6978,17 @@ class PostgresBackendDriver {
|
|
|
6820
6978
|
}
|
|
6821
6979
|
const targetDb = this.getTargetDb(options?.database);
|
|
6822
6980
|
try {
|
|
6823
|
-
|
|
6981
|
+
let needsRoleSwitch = false;
|
|
6982
|
+
if (options?.role && process.env.DISABLE_DB_ROLE_SWITCHING !== "true") {
|
|
6983
|
+
try {
|
|
6984
|
+
const currentRoleResult = await targetDb.execute(sql.raw("SELECT current_user AS role"));
|
|
6985
|
+
const currentRole = currentRoleResult.rows?.[0]?.role;
|
|
6986
|
+
needsRoleSwitch = !!currentRole && currentRole !== options.role;
|
|
6987
|
+
} catch {
|
|
6988
|
+
needsRoleSwitch = true;
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
if (needsRoleSwitch && options?.role) {
|
|
6824
6992
|
const safeRole = options.role.replace(/"/g, '""');
|
|
6825
6993
|
return await targetDb.transaction(async (tx) => {
|
|
6826
6994
|
await tx.execute(sql.raw(`SET LOCAL ROLE "${safeRole}"`));
|
|
@@ -6858,7 +7026,7 @@ class PostgresBackendDriver {
|
|
|
6858
7026
|
return databases;
|
|
6859
7027
|
}
|
|
6860
7028
|
async fetchAvailableRoles() {
|
|
6861
|
-
const result = await this.executeSql("SELECT rolname FROM pg_roles;");
|
|
7029
|
+
const result = await this.executeSql("SELECT rolname FROM pg_roles WHERE pg_has_role(current_user, rolname, 'member') ORDER BY rolname;");
|
|
6862
7030
|
return result.map((r) => r.rolname);
|
|
6863
7031
|
}
|
|
6864
7032
|
async fetchCurrentDatabase() {
|
|
@@ -7027,6 +7195,21 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7027
7195
|
* Typed admin capabilities — delegates to the base driver.
|
|
7028
7196
|
*/
|
|
7029
7197
|
admin;
|
|
7198
|
+
get restFetchService() {
|
|
7199
|
+
if (!this.delegate.restFetchService) return void 0;
|
|
7200
|
+
return {
|
|
7201
|
+
fetchCollectionForRest: async (collectionPath, options, include) => {
|
|
7202
|
+
return this.withTransaction(async (delegate) => {
|
|
7203
|
+
return delegate.restFetchService.fetchCollectionForRest(collectionPath, options, include);
|
|
7204
|
+
});
|
|
7205
|
+
},
|
|
7206
|
+
fetchEntityForRest: async (collectionPath, entityId, include, databaseId) => {
|
|
7207
|
+
return this.withTransaction(async (delegate) => {
|
|
7208
|
+
return delegate.restFetchService.fetchEntityForRest(collectionPath, entityId, include, databaseId);
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7030
7213
|
async withTransaction(operation) {
|
|
7031
7214
|
const pendingNotifications = [];
|
|
7032
7215
|
const result = await this.delegate.db.transaction(async (tx) => {
|
|
@@ -7181,113 +7364,140 @@ class DatabasePoolManager {
|
|
|
7181
7364
|
this.pools.clear();
|
|
7182
7365
|
}
|
|
7183
7366
|
}
|
|
7184
|
-
|
|
7185
|
-
const
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
})
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
})
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
})
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
})
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
},
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
})
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
})
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7367
|
+
function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase") {
|
|
7368
|
+
const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
|
|
7369
|
+
const usersSchema2 = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
7370
|
+
const rolesTableCreator = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable;
|
|
7371
|
+
const usersTableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgTable;
|
|
7372
|
+
const users2 = usersTableCreator("users", {
|
|
7373
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7374
|
+
email: varchar("email", {
|
|
7375
|
+
length: 255
|
|
7376
|
+
}).notNull().unique(),
|
|
7377
|
+
passwordHash: varchar("password_hash", {
|
|
7378
|
+
length: 255
|
|
7379
|
+
}),
|
|
7380
|
+
// NULL for OAuth-only users
|
|
7381
|
+
displayName: varchar("display_name", {
|
|
7382
|
+
length: 255
|
|
7383
|
+
}),
|
|
7384
|
+
photoUrl: varchar("photo_url", {
|
|
7385
|
+
length: 500
|
|
7386
|
+
}),
|
|
7387
|
+
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
7388
|
+
emailVerificationToken: varchar("email_verification_token", {
|
|
7389
|
+
length: 255
|
|
7390
|
+
}),
|
|
7391
|
+
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
7392
|
+
metadata: jsonb("metadata").$type().default({}).notNull(),
|
|
7393
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7394
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7395
|
+
});
|
|
7396
|
+
const roles2 = rolesTableCreator("roles", {
|
|
7397
|
+
id: varchar("id", {
|
|
7398
|
+
length: 50
|
|
7399
|
+
}).primaryKey(),
|
|
7400
|
+
// 'admin', 'editor', 'viewer'
|
|
7401
|
+
name: varchar("name", {
|
|
7402
|
+
length: 100
|
|
7403
|
+
}).notNull(),
|
|
7404
|
+
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
7405
|
+
defaultPermissions: jsonb("default_permissions").$type(),
|
|
7406
|
+
collectionPermissions: jsonb("collection_permissions").$type(),
|
|
7407
|
+
config: jsonb("config").$type()
|
|
7408
|
+
});
|
|
7409
|
+
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7410
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7411
|
+
onDelete: "cascade"
|
|
7412
|
+
}),
|
|
7413
|
+
roleId: varchar("role_id", {
|
|
7414
|
+
length: 50
|
|
7415
|
+
}).notNull().references(() => roles2.id, {
|
|
7416
|
+
onDelete: "cascade"
|
|
7417
|
+
})
|
|
7418
|
+
}, (table) => ({
|
|
7419
|
+
pk: primaryKey({
|
|
7420
|
+
columns: [table.userId, table.roleId]
|
|
7421
|
+
})
|
|
7422
|
+
}));
|
|
7423
|
+
const refreshTokens2 = rolesTableCreator("refresh_tokens", {
|
|
7424
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7425
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7426
|
+
onDelete: "cascade"
|
|
7427
|
+
}),
|
|
7428
|
+
tokenHash: varchar("token_hash", {
|
|
7429
|
+
length: 255
|
|
7430
|
+
}).notNull().unique(),
|
|
7431
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
7432
|
+
userAgent: varchar("user_agent", {
|
|
7433
|
+
length: 500
|
|
7434
|
+
}),
|
|
7435
|
+
ipAddress: varchar("ip_address", {
|
|
7436
|
+
length: 45
|
|
7437
|
+
}),
|
|
7438
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7439
|
+
}, (table) => ({
|
|
7440
|
+
uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
|
|
7441
|
+
}));
|
|
7442
|
+
const passwordResetTokens2 = rolesTableCreator("password_reset_tokens", {
|
|
7443
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7444
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7445
|
+
onDelete: "cascade"
|
|
7446
|
+
}),
|
|
7447
|
+
tokenHash: varchar("token_hash", {
|
|
7448
|
+
length: 255
|
|
7449
|
+
}).notNull().unique(),
|
|
7450
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
7451
|
+
usedAt: timestamp("used_at"),
|
|
7452
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7453
|
+
});
|
|
7454
|
+
const appConfig2 = rolesTableCreator("app_config", {
|
|
7455
|
+
key: varchar("key", {
|
|
7456
|
+
length: 100
|
|
7457
|
+
}).primaryKey(),
|
|
7458
|
+
value: jsonb("value").notNull(),
|
|
7459
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7460
|
+
});
|
|
7461
|
+
const userIdentities2 = rolesTableCreator("user_identities", {
|
|
7462
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7463
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7464
|
+
onDelete: "cascade"
|
|
7465
|
+
}),
|
|
7466
|
+
provider: varchar("provider", {
|
|
7467
|
+
length: 50
|
|
7468
|
+
}).notNull(),
|
|
7469
|
+
// e.g. 'google', 'linkedin'
|
|
7470
|
+
providerId: varchar("provider_id", {
|
|
7471
|
+
length: 255
|
|
7472
|
+
}).notNull(),
|
|
7473
|
+
profileData: jsonb("profile_data"),
|
|
7474
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7475
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7476
|
+
}, (table) => ({
|
|
7477
|
+
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7478
|
+
}));
|
|
7479
|
+
return {
|
|
7480
|
+
rolesSchema,
|
|
7481
|
+
usersSchema: usersSchema2,
|
|
7482
|
+
users: users2,
|
|
7483
|
+
roles: roles2,
|
|
7484
|
+
userRoles: userRoles2,
|
|
7485
|
+
refreshTokens: refreshTokens2,
|
|
7486
|
+
passwordResetTokens: passwordResetTokens2,
|
|
7487
|
+
appConfig: appConfig2,
|
|
7488
|
+
userIdentities: userIdentities2
|
|
7489
|
+
};
|
|
7490
|
+
}
|
|
7491
|
+
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
7492
|
+
const rebaseSchema = defaultAuthSchema.rolesSchema;
|
|
7493
|
+
const usersSchema = defaultAuthSchema.usersSchema;
|
|
7494
|
+
const users = defaultAuthSchema.users;
|
|
7495
|
+
const roles = defaultAuthSchema.roles;
|
|
7496
|
+
const userRoles = defaultAuthSchema.userRoles;
|
|
7497
|
+
const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
7498
|
+
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7499
|
+
const appConfig = defaultAuthSchema.appConfig;
|
|
7500
|
+
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7291
7501
|
const usersRelations = relations(users, ({
|
|
7292
7502
|
many
|
|
7293
7503
|
}) => ({
|
|
@@ -7470,6 +7680,15 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
7470
7680
|
}
|
|
7471
7681
|
break;
|
|
7472
7682
|
}
|
|
7683
|
+
case "vector": {
|
|
7684
|
+
const vp = prop;
|
|
7685
|
+
columnDefinition = `vector("${colName}", { dimensions: ${vp.dimensions} })`;
|
|
7686
|
+
break;
|
|
7687
|
+
}
|
|
7688
|
+
case "binary": {
|
|
7689
|
+
columnDefinition = `customType({ dataType() { return 'bytea'; } })("${colName}")`;
|
|
7690
|
+
break;
|
|
7691
|
+
}
|
|
7473
7692
|
case "relation": {
|
|
7474
7693
|
const refProp = prop;
|
|
7475
7694
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -7536,7 +7755,7 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
7536
7755
|
return ` ${propName}: ${columnDefinition}`;
|
|
7537
7756
|
};
|
|
7538
7757
|
const resolveRawSql = (expression) => {
|
|
7539
|
-
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) =>
|
|
7758
|
+
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
7540
7759
|
return `sql\`${resolved}\``;
|
|
7541
7760
|
};
|
|
7542
7761
|
const wrapWithRoleCheck = (clause, roles2) => {
|
|
@@ -7548,7 +7767,7 @@ const unwrapSql = (sqlExpr) => {
|
|
|
7548
7767
|
const match = sqlExpr.match(/^sql`(.*)`$/s);
|
|
7549
7768
|
return match ? match[1] : sqlExpr;
|
|
7550
7769
|
};
|
|
7551
|
-
const buildUsingClause = (rule) => {
|
|
7770
|
+
const buildUsingClause = (rule, collection) => {
|
|
7552
7771
|
if (rule.using) {
|
|
7553
7772
|
return resolveRawSql(rule.using);
|
|
7554
7773
|
}
|
|
@@ -7556,15 +7775,17 @@ const buildUsingClause = (rule) => {
|
|
|
7556
7775
|
return "sql`true`";
|
|
7557
7776
|
}
|
|
7558
7777
|
if (rule.ownerField) {
|
|
7559
|
-
|
|
7778
|
+
const prop = collection.properties?.[rule.ownerField];
|
|
7779
|
+
const colName = resolveColumnName(rule.ownerField, prop);
|
|
7780
|
+
return `sql\`${colName} = auth.uid()\``;
|
|
7560
7781
|
}
|
|
7561
7782
|
return null;
|
|
7562
7783
|
};
|
|
7563
|
-
const buildWithCheckClause = (rule) => {
|
|
7784
|
+
const buildWithCheckClause = (rule, collection) => {
|
|
7564
7785
|
if (rule.withCheck) {
|
|
7565
7786
|
return resolveRawSql(rule.withCheck);
|
|
7566
7787
|
}
|
|
7567
|
-
return buildUsingClause(rule);
|
|
7788
|
+
return buildUsingClause(rule, collection);
|
|
7568
7789
|
};
|
|
7569
7790
|
const getPolicyNameHash = (rule) => {
|
|
7570
7791
|
const data = JSON.stringify({
|
|
@@ -7580,21 +7801,22 @@ const getPolicyNameHash = (rule) => {
|
|
|
7580
7801
|
});
|
|
7581
7802
|
return createHash("sha1").update(data).digest("hex").substring(0, 7);
|
|
7582
7803
|
};
|
|
7583
|
-
const generatePolicyCode = (
|
|
7804
|
+
const generatePolicyCode = (collection, rule, index) => {
|
|
7805
|
+
const tableName = getTableName(collection);
|
|
7584
7806
|
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
7585
7807
|
const ruleHash = getPolicyNameHash(rule);
|
|
7586
7808
|
return ops.map((op, opIdx) => {
|
|
7587
7809
|
const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
7588
|
-
return generateSinglePolicyCode(
|
|
7810
|
+
return generateSinglePolicyCode(collection, rule, op, policyName);
|
|
7589
7811
|
}).join("");
|
|
7590
7812
|
};
|
|
7591
|
-
const generateSinglePolicyCode = (
|
|
7813
|
+
const generateSinglePolicyCode = (collection, rule, operation, policyName) => {
|
|
7592
7814
|
const mode = rule.mode ?? "permissive";
|
|
7593
7815
|
const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
|
|
7594
7816
|
const needsUsing = operation !== "insert";
|
|
7595
7817
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
7596
|
-
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
7597
|
-
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule) : null;
|
|
7818
|
+
let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
|
|
7819
|
+
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
|
|
7598
7820
|
if (roles2 && roles2.length > 0) {
|
|
7599
7821
|
if (usingClause) {
|
|
7600
7822
|
usingClause = wrapWithRoleCheck(usingClause, roles2);
|
|
@@ -7663,12 +7885,26 @@ const computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
|
7663
7885
|
const generateSchema = async (collections, stripPolicies = false) => {
|
|
7664
7886
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
7665
7887
|
const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
|
|
7666
|
-
collections.some((c) => c.properties && Object.values(c.properties).some((p) =>
|
|
7888
|
+
const hasVector = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "vector"));
|
|
7889
|
+
const hasBinary = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "binary"));
|
|
7667
7890
|
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
7668
7891
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
7892
|
+
if (hasVector) pgCoreImports.push("vector");
|
|
7893
|
+
if (hasBinary) pgCoreImports.push("customType");
|
|
7894
|
+
const uniqueSchemas = Array.from(new Set(collections.map((c) => isPostgresCollection(c) ? c.schema : void 0).filter(Boolean)));
|
|
7895
|
+
if (uniqueSchemas.length > 0) {
|
|
7896
|
+
pgCoreImports.push("pgSchema");
|
|
7897
|
+
}
|
|
7669
7898
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';
|
|
7670
7899
|
`;
|
|
7671
7900
|
schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
|
|
7901
|
+
uniqueSchemas.forEach((schema) => {
|
|
7902
|
+
schemaContent += `export const ${schema}Schema = pgSchema("${schema}");
|
|
7903
|
+
`;
|
|
7904
|
+
});
|
|
7905
|
+
if (uniqueSchemas.length > 0) {
|
|
7906
|
+
schemaContent += "\n";
|
|
7907
|
+
}
|
|
7672
7908
|
const exportedTableVars = [];
|
|
7673
7909
|
const exportedEnumVars = [];
|
|
7674
7910
|
const exportedRelationVars = [];
|
|
@@ -7723,6 +7959,9 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
7723
7959
|
const tableVarName = getTableVarName(tableName);
|
|
7724
7960
|
if (isJunction && relation && sourceCollection && relation.through) {
|
|
7725
7961
|
const targetCollection = relation.target();
|
|
7962
|
+
const schema = (isPostgresCollection(targetCollection) ? targetCollection.schema : void 0) || (isPostgresCollection(sourceCollection) ? sourceCollection.schema : void 0);
|
|
7963
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
7964
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
7726
7965
|
const {
|
|
7727
7966
|
sourceColumn,
|
|
7728
7967
|
targetColumn
|
|
@@ -7733,7 +7972,7 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
7733
7972
|
const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar";
|
|
7734
7973
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
7735
7974
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
7736
|
-
schemaContent += `export const ${tableVarName} =
|
|
7975
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}("${baseTableName}", {
|
|
7737
7976
|
`;
|
|
7738
7977
|
schemaContent += ` ${sourceColumn}: ${sourceColType}("${sourceColumn}").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),
|
|
7739
7978
|
`;
|
|
@@ -7744,7 +7983,10 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
7744
7983
|
`;
|
|
7745
7984
|
schemaContent += "}));\n\n";
|
|
7746
7985
|
} else if (!isJunction) {
|
|
7747
|
-
|
|
7986
|
+
const schema = isPostgresCollection(collection) ? collection.schema : void 0;
|
|
7987
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
7988
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
7989
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}("${baseTableName}", {
|
|
7748
7990
|
`;
|
|
7749
7991
|
const columns = /* @__PURE__ */ new Set();
|
|
7750
7992
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
@@ -7760,7 +8002,7 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
7760
8002
|
if (!stripPolicies && securityRules && securityRules.length > 0) {
|
|
7761
8003
|
schemaContent += "\n}, (table) => ([\n";
|
|
7762
8004
|
securityRules.forEach((rule, idx) => {
|
|
7763
|
-
schemaContent += generatePolicyCode(
|
|
8005
|
+
schemaContent += generatePolicyCode(collection, rule);
|
|
7764
8006
|
});
|
|
7765
8007
|
schemaContent += "])).enableRLS();\n\n";
|
|
7766
8008
|
} else {
|
|
@@ -7805,11 +8047,11 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
7805
8047
|
references: [${sourceTableVar}.${sourceId}],
|
|
7806
8048
|
relationName: "${owningRelationName}"
|
|
7807
8049
|
})`);
|
|
7808
|
-
const
|
|
8050
|
+
const targetRelationName = inverseRelationName ? inverseRelationName : `${tableName}_${relation.through.targetColumn}`;
|
|
7809
8051
|
tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {
|
|
7810
8052
|
fields: [${tableVarName}.${relation.through.targetColumn}],
|
|
7811
8053
|
references: [${targetTableVar}.${targetId}],
|
|
7812
|
-
relationName: "${
|
|
8054
|
+
relationName: "${targetRelationName}"
|
|
7813
8055
|
})`);
|
|
7814
8056
|
}
|
|
7815
8057
|
} else {
|
|
@@ -7908,6 +8150,89 @@ ${tableRelations.join(",\n")}
|
|
|
7908
8150
|
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
7909
8151
|
return schemaContent;
|
|
7910
8152
|
};
|
|
8153
|
+
const defaultUsersCollection = {
|
|
8154
|
+
name: "Users",
|
|
8155
|
+
singularName: "User",
|
|
8156
|
+
slug: "users",
|
|
8157
|
+
table: "users",
|
|
8158
|
+
icon: "Users",
|
|
8159
|
+
group: "Settings",
|
|
8160
|
+
properties: {
|
|
8161
|
+
id: {
|
|
8162
|
+
name: "ID",
|
|
8163
|
+
type: "string",
|
|
8164
|
+
isId: "uuid"
|
|
8165
|
+
},
|
|
8166
|
+
email: {
|
|
8167
|
+
name: "Email",
|
|
8168
|
+
type: "string",
|
|
8169
|
+
validation: {
|
|
8170
|
+
required: true,
|
|
8171
|
+
unique: true
|
|
8172
|
+
}
|
|
8173
|
+
},
|
|
8174
|
+
password_hash: {
|
|
8175
|
+
name: "Password Hash",
|
|
8176
|
+
type: "string",
|
|
8177
|
+
ui: {
|
|
8178
|
+
hideFromCollection: true
|
|
8179
|
+
}
|
|
8180
|
+
},
|
|
8181
|
+
display_name: {
|
|
8182
|
+
name: "Display Name",
|
|
8183
|
+
type: "string"
|
|
8184
|
+
},
|
|
8185
|
+
photo_url: {
|
|
8186
|
+
name: "Photo URL",
|
|
8187
|
+
type: "string"
|
|
8188
|
+
},
|
|
8189
|
+
email_verified: {
|
|
8190
|
+
name: "Email Verified",
|
|
8191
|
+
type: "boolean",
|
|
8192
|
+
defaultValue: false
|
|
8193
|
+
},
|
|
8194
|
+
email_verification_token: {
|
|
8195
|
+
name: "Email Verification Token",
|
|
8196
|
+
type: "string",
|
|
8197
|
+
ui: {
|
|
8198
|
+
hideFromCollection: true
|
|
8199
|
+
}
|
|
8200
|
+
},
|
|
8201
|
+
email_verification_sent_at: {
|
|
8202
|
+
name: "Email Verification Sent At",
|
|
8203
|
+
type: "date",
|
|
8204
|
+
ui: {
|
|
8205
|
+
hideFromCollection: true
|
|
8206
|
+
}
|
|
8207
|
+
},
|
|
8208
|
+
metadata: {
|
|
8209
|
+
name: "Metadata",
|
|
8210
|
+
type: "map",
|
|
8211
|
+
defaultValue: {},
|
|
8212
|
+
ui: {
|
|
8213
|
+
hideFromCollection: true
|
|
8214
|
+
}
|
|
8215
|
+
},
|
|
8216
|
+
created_at: {
|
|
8217
|
+
name: "Created At",
|
|
8218
|
+
type: "date",
|
|
8219
|
+
autoValue: "on_create",
|
|
8220
|
+
ui: {
|
|
8221
|
+
readOnly: true,
|
|
8222
|
+
hideFromCollection: true
|
|
8223
|
+
}
|
|
8224
|
+
},
|
|
8225
|
+
updated_at: {
|
|
8226
|
+
name: "Updated At",
|
|
8227
|
+
type: "date",
|
|
8228
|
+
autoValue: "on_update",
|
|
8229
|
+
ui: {
|
|
8230
|
+
readOnly: true,
|
|
8231
|
+
hideFromCollection: true
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
};
|
|
7911
8236
|
const formatTerminalText = (text, options = {}) => {
|
|
7912
8237
|
let codes = "";
|
|
7913
8238
|
if (options.bold) codes += "\x1B[1m";
|
|
@@ -7970,10 +8295,14 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
|
|
|
7970
8295
|
const imported = await dynamicImport(fileUrl);
|
|
7971
8296
|
collections = imported.backendCollections || imported.collections;
|
|
7972
8297
|
}
|
|
7973
|
-
if (!collections || !Array.isArray(collections)
|
|
7974
|
-
|
|
7975
|
-
|
|
8298
|
+
if (!collections || !Array.isArray(collections)) {
|
|
8299
|
+
collections = [];
|
|
8300
|
+
}
|
|
8301
|
+
const hasUsersCollection = collections.some((c) => c.slug === "users");
|
|
8302
|
+
if (!hasUsersCollection) {
|
|
8303
|
+
collections.push(defaultUsersCollection);
|
|
7976
8304
|
}
|
|
8305
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
7977
8306
|
const schemaContent = await generateSchema(collections);
|
|
7978
8307
|
if (outputPath) {
|
|
7979
8308
|
const outputDir = path.dirname(outputPath);
|
|
@@ -8201,29 +8530,14 @@ class RealtimeService extends EventEmitter {
|
|
|
8201
8530
|
},
|
|
8202
8531
|
authContext
|
|
8203
8532
|
});
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
limit: request.limit,
|
|
8213
|
-
startAfter: request.startAfter,
|
|
8214
|
-
searchString: request.searchString
|
|
8215
|
-
});
|
|
8216
|
-
} else {
|
|
8217
|
-
entities = await this.entityService.fetchCollection(request.path, {
|
|
8218
|
-
filter: request.filter,
|
|
8219
|
-
orderBy: request.orderBy,
|
|
8220
|
-
order: request.order,
|
|
8221
|
-
limit: request.limit,
|
|
8222
|
-
startAfter: request.startAfter,
|
|
8223
|
-
databaseId: request.collection?.databaseId,
|
|
8224
|
-
searchString: request.searchString
|
|
8225
|
-
});
|
|
8226
|
-
}
|
|
8533
|
+
const entities = await this.fetchCollectionWithAuth(request.path, {
|
|
8534
|
+
filter: request.filter,
|
|
8535
|
+
orderBy: request.orderBy,
|
|
8536
|
+
order: request.order,
|
|
8537
|
+
limit: request.limit,
|
|
8538
|
+
startAfter: request.startAfter,
|
|
8539
|
+
searchString: request.searchString
|
|
8540
|
+
}, authContext);
|
|
8227
8541
|
this.sendCollectionUpdate(clientId, subscriptionId, entities);
|
|
8228
8542
|
} catch (error) {
|
|
8229
8543
|
this.sendError(clientId, `Failed to subscribe to collection: ${error}`, subscriptionId);
|
|
@@ -8247,16 +8561,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8247
8561
|
entityId: request.entityId,
|
|
8248
8562
|
authContext
|
|
8249
8563
|
});
|
|
8250
|
-
|
|
8251
|
-
if (this.driver) {
|
|
8252
|
-
entity = await this.driver.fetchEntity({
|
|
8253
|
-
path: request.path,
|
|
8254
|
-
entityId: request.entityId,
|
|
8255
|
-
collection
|
|
8256
|
-
});
|
|
8257
|
-
} else {
|
|
8258
|
-
entity = await this.entityService.fetchEntity(request.path, request.entityId, request.collection?.databaseId);
|
|
8259
|
-
}
|
|
8564
|
+
const entity = await this.fetchEntityWithAuth(request.path, String(request.entityId), authContext);
|
|
8260
8565
|
this.sendEntityUpdate(clientId, subscriptionId, entity || null);
|
|
8261
8566
|
} catch (error) {
|
|
8262
8567
|
this.sendError(clientId, `Failed to subscribe to entity: ${request.path} ${request.entityId} ${error}`, subscriptionId);
|
|
@@ -8401,87 +8706,77 @@ class RealtimeService extends EventEmitter {
|
|
|
8401
8706
|
async fetchCollectionWithAuth(notifyPath, collectionRequest, authContext) {
|
|
8402
8707
|
if (this.driver) {
|
|
8403
8708
|
const collection = this.registry.getCollectionByPath(notifyPath);
|
|
8404
|
-
const
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8709
|
+
const activeAuth = authContext || {
|
|
8710
|
+
userId: "anon",
|
|
8711
|
+
roles: ["anon"]
|
|
8712
|
+
};
|
|
8713
|
+
return await this.db.transaction(async (tx) => {
|
|
8714
|
+
await tx.execute(sql`SELECT set_config('app.user_id', ${activeAuth.userId}, true)`);
|
|
8715
|
+
await tx.execute(sql`SELECT set_config('app.user_roles', ${activeAuth.roles.join(",")}, true)`);
|
|
8716
|
+
await tx.execute(sql`SELECT set_config('app.jwt', ${JSON.stringify({
|
|
8717
|
+
sub: activeAuth.userId,
|
|
8718
|
+
roles: activeAuth.roles
|
|
8719
|
+
})}, true)`);
|
|
8720
|
+
const txEntityService = new EntityService(tx, this.registry);
|
|
8721
|
+
let fetchedEntities;
|
|
8722
|
+
if (collectionRequest.searchString) {
|
|
8723
|
+
fetchedEntities = await txEntityService.searchEntities(notifyPath, collectionRequest.searchString, {
|
|
8724
|
+
filter: collectionRequest.filter,
|
|
8725
|
+
orderBy: collectionRequest.orderBy,
|
|
8726
|
+
order: collectionRequest.order,
|
|
8727
|
+
limit: collectionRequest.limit,
|
|
8728
|
+
databaseId: collectionRequest.databaseId
|
|
8729
|
+
});
|
|
8730
|
+
} else {
|
|
8731
|
+
fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
8732
|
+
filter: collectionRequest.filter,
|
|
8733
|
+
orderBy: collectionRequest.orderBy,
|
|
8734
|
+
order: collectionRequest.order,
|
|
8735
|
+
limit: collectionRequest.limit,
|
|
8736
|
+
offset: collectionRequest.offset,
|
|
8737
|
+
startAfter: collectionRequest.startAfter,
|
|
8738
|
+
databaseId: collectionRequest.databaseId
|
|
8739
|
+
});
|
|
8740
|
+
}
|
|
8741
|
+
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
8742
|
+
const resolvedCollection = collection ? {
|
|
8743
|
+
...collection,
|
|
8744
|
+
...registryCollection
|
|
8745
|
+
} : registryCollection;
|
|
8746
|
+
const callbacks = resolvedCollection?.callbacks;
|
|
8747
|
+
const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : void 0;
|
|
8748
|
+
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
8749
|
+
const contextForCallback = {
|
|
8750
|
+
user: {
|
|
8751
|
+
uid: activeAuth.userId,
|
|
8752
|
+
roles: activeAuth.roles
|
|
8753
|
+
},
|
|
8754
|
+
driver: this.driver,
|
|
8755
|
+
data: this.driver && "data" in this.driver ? this.driver.data : void 0
|
|
8756
|
+
};
|
|
8757
|
+
return await Promise.all(fetchedEntities.map(async (entity) => {
|
|
8758
|
+
let processedEntity = entity;
|
|
8759
|
+
if (callbacks?.afterRead) {
|
|
8760
|
+
processedEntity = await callbacks.afterRead({
|
|
8761
|
+
collection: resolvedCollection,
|
|
8762
|
+
path: notifyPath,
|
|
8763
|
+
entity: processedEntity,
|
|
8764
|
+
context: contextForCallback
|
|
8765
|
+
}) ?? processedEntity;
|
|
8766
|
+
}
|
|
8767
|
+
if (propertyCallbacks?.afterRead) {
|
|
8768
|
+
processedEntity = await propertyCallbacks.afterRead({
|
|
8769
|
+
collection: resolvedCollection,
|
|
8770
|
+
path: notifyPath,
|
|
8771
|
+
entity: processedEntity,
|
|
8772
|
+
context: contextForCallback
|
|
8773
|
+
}) ?? processedEntity;
|
|
8774
|
+
}
|
|
8775
|
+
return processedEntity;
|
|
8776
|
+
}));
|
|
8777
|
+
}
|
|
8778
|
+
return fetchedEntities;
|
|
8414
8779
|
});
|
|
8415
|
-
if (authContext) {
|
|
8416
|
-
return await this.db.transaction(async (tx) => {
|
|
8417
|
-
await tx.execute(sql`SELECT set_config('app.user_id', ${authContext.userId}, true)`);
|
|
8418
|
-
await tx.execute(sql`SELECT set_config('app.user_roles', ${authContext.roles.join(",")}, true)`);
|
|
8419
|
-
await tx.execute(sql`SELECT set_config('app.jwt', ${JSON.stringify({
|
|
8420
|
-
sub: authContext.userId,
|
|
8421
|
-
roles: authContext.roles
|
|
8422
|
-
})}, true)`);
|
|
8423
|
-
const txEntityService = new EntityService(tx, this.registry);
|
|
8424
|
-
let fetchedEntities;
|
|
8425
|
-
if (collectionRequest.searchString) {
|
|
8426
|
-
fetchedEntities = await txEntityService.searchEntities(notifyPath, collectionRequest.searchString, {
|
|
8427
|
-
filter: collectionRequest.filter,
|
|
8428
|
-
orderBy: collectionRequest.orderBy,
|
|
8429
|
-
order: collectionRequest.order,
|
|
8430
|
-
limit: collectionRequest.limit,
|
|
8431
|
-
databaseId: collectionRequest.databaseId
|
|
8432
|
-
});
|
|
8433
|
-
} else {
|
|
8434
|
-
fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
8435
|
-
filter: collectionRequest.filter,
|
|
8436
|
-
orderBy: collectionRequest.orderBy,
|
|
8437
|
-
order: collectionRequest.order,
|
|
8438
|
-
limit: collectionRequest.limit,
|
|
8439
|
-
offset: collectionRequest.offset,
|
|
8440
|
-
startAfter: collectionRequest.startAfter,
|
|
8441
|
-
databaseId: collectionRequest.databaseId
|
|
8442
|
-
});
|
|
8443
|
-
}
|
|
8444
|
-
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
8445
|
-
const resolvedCollection = collection ? {
|
|
8446
|
-
...collection,
|
|
8447
|
-
...registryCollection
|
|
8448
|
-
} : registryCollection;
|
|
8449
|
-
const callbacks = resolvedCollection?.callbacks;
|
|
8450
|
-
const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : void 0;
|
|
8451
|
-
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
8452
|
-
const contextForCallback = {
|
|
8453
|
-
user: {
|
|
8454
|
-
uid: authContext.userId,
|
|
8455
|
-
roles: authContext.roles
|
|
8456
|
-
},
|
|
8457
|
-
driver: this.driver,
|
|
8458
|
-
data: this.driver ? this.driver.data : void 0
|
|
8459
|
-
};
|
|
8460
|
-
return await Promise.all(fetchedEntities.map(async (entity) => {
|
|
8461
|
-
let processedEntity = entity;
|
|
8462
|
-
if (callbacks?.afterRead) {
|
|
8463
|
-
processedEntity = await callbacks.afterRead({
|
|
8464
|
-
collection: resolvedCollection,
|
|
8465
|
-
path: notifyPath,
|
|
8466
|
-
entity: processedEntity,
|
|
8467
|
-
context: contextForCallback
|
|
8468
|
-
}) ?? processedEntity;
|
|
8469
|
-
}
|
|
8470
|
-
if (propertyCallbacks?.afterRead) {
|
|
8471
|
-
processedEntity = await propertyCallbacks.afterRead({
|
|
8472
|
-
collection: resolvedCollection,
|
|
8473
|
-
path: notifyPath,
|
|
8474
|
-
entity: processedEntity,
|
|
8475
|
-
context: contextForCallback
|
|
8476
|
-
}) ?? processedEntity;
|
|
8477
|
-
}
|
|
8478
|
-
return processedEntity;
|
|
8479
|
-
}));
|
|
8480
|
-
}
|
|
8481
|
-
return fetchedEntities;
|
|
8482
|
-
});
|
|
8483
|
-
}
|
|
8484
|
-
return fetchFn();
|
|
8485
8780
|
}
|
|
8486
8781
|
if (collectionRequest.searchString) {
|
|
8487
8782
|
return await this.entityService.searchEntities(notifyPath, collectionRequest.searchString, {
|
|
@@ -8545,60 +8840,56 @@ class RealtimeService extends EventEmitter {
|
|
|
8545
8840
|
async fetchEntityWithAuth(notifyPath, entityId, authContext) {
|
|
8546
8841
|
if (this.driver) {
|
|
8547
8842
|
const collection = this.registry.getCollectionByPath(notifyPath);
|
|
8548
|
-
const
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
const
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
context: contextForCallback
|
|
8594
|
-
}) ?? processedEntity;
|
|
8595
|
-
}
|
|
8843
|
+
const activeAuth = authContext || {
|
|
8844
|
+
userId: "anon",
|
|
8845
|
+
roles: ["anon"]
|
|
8846
|
+
};
|
|
8847
|
+
return await this.db.transaction(async (tx) => {
|
|
8848
|
+
await tx.execute(sql`SELECT set_config('app.user_id', ${activeAuth.userId}, true)`);
|
|
8849
|
+
await tx.execute(sql`SELECT set_config('app.user_roles', ${activeAuth.roles.join(",")}, true)`);
|
|
8850
|
+
await tx.execute(sql`SELECT set_config('app.jwt', ${JSON.stringify({
|
|
8851
|
+
sub: activeAuth.userId,
|
|
8852
|
+
roles: activeAuth.roles
|
|
8853
|
+
})}, true)`);
|
|
8854
|
+
const txEntityService = new EntityService(tx, this.registry);
|
|
8855
|
+
let processedEntity = await txEntityService.fetchEntity(notifyPath, entityId, collection?.databaseId);
|
|
8856
|
+
if (processedEntity) {
|
|
8857
|
+
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
8858
|
+
const resolvedCollection = collection ? {
|
|
8859
|
+
...collection,
|
|
8860
|
+
...registryCollection
|
|
8861
|
+
} : registryCollection;
|
|
8862
|
+
const callbacks = resolvedCollection?.callbacks;
|
|
8863
|
+
const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : void 0;
|
|
8864
|
+
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
8865
|
+
const contextForCallback = {
|
|
8866
|
+
user: {
|
|
8867
|
+
uid: activeAuth.userId,
|
|
8868
|
+
roles: activeAuth.roles
|
|
8869
|
+
},
|
|
8870
|
+
driver: this.driver,
|
|
8871
|
+
data: this.driver && "data" in this.driver ? this.driver.data : void 0
|
|
8872
|
+
};
|
|
8873
|
+
if (callbacks?.afterRead) {
|
|
8874
|
+
processedEntity = await callbacks.afterRead({
|
|
8875
|
+
collection: resolvedCollection,
|
|
8876
|
+
path: notifyPath,
|
|
8877
|
+
entity: processedEntity,
|
|
8878
|
+
context: contextForCallback
|
|
8879
|
+
}) ?? processedEntity;
|
|
8880
|
+
}
|
|
8881
|
+
if (propertyCallbacks?.afterRead) {
|
|
8882
|
+
processedEntity = await propertyCallbacks.afterRead({
|
|
8883
|
+
collection: resolvedCollection,
|
|
8884
|
+
path: notifyPath,
|
|
8885
|
+
entity: processedEntity,
|
|
8886
|
+
context: contextForCallback
|
|
8887
|
+
}) ?? processedEntity;
|
|
8596
8888
|
}
|
|
8597
8889
|
}
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
}
|
|
8601
|
-
return fetchFn();
|
|
8890
|
+
}
|
|
8891
|
+
return processedEntity;
|
|
8892
|
+
});
|
|
8602
8893
|
}
|
|
8603
8894
|
return await this.entityService.fetchEntity(notifyPath, entityId);
|
|
8604
8895
|
}
|
|
@@ -8666,6 +8957,31 @@ class RealtimeService extends EventEmitter {
|
|
|
8666
8957
|
return parentPaths;
|
|
8667
8958
|
}
|
|
8668
8959
|
// =============================================================================
|
|
8960
|
+
// Lifecycle / Cleanup
|
|
8961
|
+
// =============================================================================
|
|
8962
|
+
/**
|
|
8963
|
+
* Gracefully tear down all realtime resources.
|
|
8964
|
+
*
|
|
8965
|
+
* This MUST be called during process shutdown, **before** `pool.end()`.
|
|
8966
|
+
* It ensures:
|
|
8967
|
+
* 1. All debounced refetch timers are cancelled (prevents queries after pool closes).
|
|
8968
|
+
* 2. All subscription state and callbacks are cleared.
|
|
8969
|
+
* 3. The dedicated LISTEN client (outside the pool) is disconnected.
|
|
8970
|
+
* 4. All WebSocket clients are removed (but not forcefully closed — the
|
|
8971
|
+
* HTTP server close will handle that).
|
|
8972
|
+
*/
|
|
8973
|
+
async destroy() {
|
|
8974
|
+
for (const [key, timer] of this.refetchTimers) {
|
|
8975
|
+
clearTimeout(timer);
|
|
8976
|
+
this.refetchTimers.delete(key);
|
|
8977
|
+
}
|
|
8978
|
+
this._subscriptions.clear();
|
|
8979
|
+
this.subscriptionCallbacks.clear();
|
|
8980
|
+
await this.stopListening();
|
|
8981
|
+
this.clients.clear();
|
|
8982
|
+
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
8983
|
+
}
|
|
8984
|
+
// =============================================================================
|
|
8669
8985
|
// Cross-Instance LISTEN/NOTIFY
|
|
8670
8986
|
// =============================================================================
|
|
8671
8987
|
/**
|
|
@@ -8806,8 +9122,23 @@ const clientSessions = /* @__PURE__ */ new Map();
|
|
|
8806
9122
|
const WS_RATE_LIMIT = 2e3;
|
|
8807
9123
|
const WS_RATE_WINDOW_MS = 6e4;
|
|
8808
9124
|
const ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set(["EXECUTE_SQL", "FETCH_DATABASES", "FETCH_ROLES", "FETCH_UNMAPPED_TABLES", "FETCH_TABLE_METADATA", "FETCH_CURRENT_DATABASE", "CREATE_BRANCH", "DELETE_BRANCH", "LIST_BRANCHES"]);
|
|
9125
|
+
function extractErrorMessage(error) {
|
|
9126
|
+
if (!error) return "Unknown error";
|
|
9127
|
+
if (typeof error === "object") {
|
|
9128
|
+
const err = error;
|
|
9129
|
+
if (err.cause) {
|
|
9130
|
+
return extractErrorMessage(err.cause);
|
|
9131
|
+
}
|
|
9132
|
+
if (typeof err.message === "string") {
|
|
9133
|
+
return err.message;
|
|
9134
|
+
}
|
|
9135
|
+
}
|
|
9136
|
+
return String(error);
|
|
9137
|
+
}
|
|
8809
9138
|
function isAdminSession(session) {
|
|
8810
|
-
if (!session?.user
|
|
9139
|
+
if (!session?.user) return false;
|
|
9140
|
+
if (session.user.isAdmin) return true;
|
|
9141
|
+
if (!session.user.roles) return false;
|
|
8811
9142
|
return session.user.roles.some((r) => {
|
|
8812
9143
|
if (typeof r === "string") return r === "admin";
|
|
8813
9144
|
if (r && typeof r === "object" && "isAdmin" in r) return r.isAdmin;
|
|
@@ -8815,7 +9146,7 @@ function isAdminSession(session) {
|
|
|
8815
9146
|
return false;
|
|
8816
9147
|
});
|
|
8817
9148
|
}
|
|
8818
|
-
function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
9149
|
+
function createPostgresWebSocket(server, realtimeService, driver, authConfig, authAdapter) {
|
|
8819
9150
|
const isProduction = process.env.NODE_ENV === "production";
|
|
8820
9151
|
const wsDebug = (...args) => {
|
|
8821
9152
|
if (!isProduction) console.debug(...args);
|
|
@@ -8829,7 +9160,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
8829
9160
|
}
|
|
8830
9161
|
console.error("❌ [WebSocket Server] Error:", err);
|
|
8831
9162
|
});
|
|
8832
|
-
const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
|
|
9163
|
+
const requireAuth = authAdapter ? true : authConfig?.requireAuth !== false && !!authConfig?.jwtSecret;
|
|
8833
9164
|
wss.on("connection", (ws) => {
|
|
8834
9165
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
8835
9166
|
wsDebug(`WebSocket client connected: ${clientId}`);
|
|
@@ -8874,11 +9205,37 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
8874
9205
|
sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
|
|
8875
9206
|
return;
|
|
8876
9207
|
}
|
|
8877
|
-
|
|
8878
|
-
if (
|
|
9208
|
+
let verifiedUser = null;
|
|
9209
|
+
if (authAdapter) {
|
|
9210
|
+
try {
|
|
9211
|
+
const adapterUser = authAdapter.verifyToken ? await authAdapter.verifyToken(token) : await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", {
|
|
9212
|
+
headers: {
|
|
9213
|
+
Authorization: `Bearer ${token}`
|
|
9214
|
+
}
|
|
9215
|
+
}));
|
|
9216
|
+
if (adapterUser) {
|
|
9217
|
+
verifiedUser = {
|
|
9218
|
+
userId: adapterUser.uid,
|
|
9219
|
+
roles: adapterUser.roles,
|
|
9220
|
+
isAdmin: adapterUser.isAdmin
|
|
9221
|
+
};
|
|
9222
|
+
}
|
|
9223
|
+
} catch {
|
|
9224
|
+
}
|
|
9225
|
+
} else {
|
|
9226
|
+
const jwtPayload = extractUserFromToken(token);
|
|
9227
|
+
if (jwtPayload) {
|
|
9228
|
+
verifiedUser = {
|
|
9229
|
+
userId: jwtPayload.userId,
|
|
9230
|
+
roles: jwtPayload.roles ?? [],
|
|
9231
|
+
isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin")
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
if (verifiedUser) {
|
|
8879
9236
|
const session = clientSessions.get(clientId);
|
|
8880
9237
|
if (session) {
|
|
8881
|
-
session.user =
|
|
9238
|
+
session.user = verifiedUser;
|
|
8882
9239
|
session.authenticated = true;
|
|
8883
9240
|
}
|
|
8884
9241
|
wsDebug(`[WS] replying AUTH_SUCCESS for requestId ${requestId}`);
|
|
@@ -8886,11 +9243,11 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
8886
9243
|
type: "AUTH_SUCCESS",
|
|
8887
9244
|
requestId,
|
|
8888
9245
|
payload: {
|
|
8889
|
-
userId:
|
|
8890
|
-
roles:
|
|
9246
|
+
userId: verifiedUser.userId,
|
|
9247
|
+
roles: verifiedUser.roles
|
|
8891
9248
|
}
|
|
8892
9249
|
}));
|
|
8893
|
-
wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${
|
|
9250
|
+
wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.userId}`);
|
|
8894
9251
|
} else {
|
|
8895
9252
|
wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);
|
|
8896
9253
|
sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
|
|
@@ -8928,16 +9285,19 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
8928
9285
|
}
|
|
8929
9286
|
const getScopedDelegate = async () => {
|
|
8930
9287
|
const session = clientSessions.get(clientId);
|
|
8931
|
-
if (
|
|
9288
|
+
if ("withAuth" in driver && typeof driver.withAuth === "function") {
|
|
8932
9289
|
try {
|
|
8933
|
-
const userForAuth = {
|
|
9290
|
+
const userForAuth = session?.user ? {
|
|
8934
9291
|
uid: session.user.userId,
|
|
8935
9292
|
roles: session.user.roles ?? []
|
|
9293
|
+
} : {
|
|
9294
|
+
uid: "anon",
|
|
9295
|
+
roles: ["anon"]
|
|
8936
9296
|
};
|
|
8937
9297
|
return await driver.withAuth(userForAuth);
|
|
8938
9298
|
} catch (e) {
|
|
8939
|
-
console.error("Failed to create
|
|
8940
|
-
|
|
9299
|
+
console.error("Failed to create RLS scoped delegate for WS request", e);
|
|
9300
|
+
throw new Error("Internal authentication error");
|
|
8941
9301
|
}
|
|
8942
9302
|
}
|
|
8943
9303
|
return driver;
|
|
@@ -9068,24 +9428,29 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9068
9428
|
sql: sql2,
|
|
9069
9429
|
options
|
|
9070
9430
|
} = payload;
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9431
|
+
try {
|
|
9432
|
+
const delegate = await getScopedDelegate();
|
|
9433
|
+
const admin = delegate.admin;
|
|
9434
|
+
if (!isSQLAdmin(admin)) {
|
|
9435
|
+
sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
|
|
9436
|
+
break;
|
|
9437
|
+
}
|
|
9438
|
+
const result = await admin.executeSql(sql2, options);
|
|
9439
|
+
if (process.env.NODE_ENV !== "production") {
|
|
9440
|
+
wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
|
|
9441
|
+
}
|
|
9442
|
+
const response = {
|
|
9443
|
+
type: "EXECUTE_SQL_SUCCESS",
|
|
9444
|
+
payload: {
|
|
9445
|
+
result
|
|
9446
|
+
},
|
|
9447
|
+
requestId
|
|
9448
|
+
};
|
|
9449
|
+
ws.send(JSON.stringify(response));
|
|
9450
|
+
} catch (sqlError) {
|
|
9451
|
+
const errMsg = extractErrorMessage(sqlError);
|
|
9452
|
+
sendError("ERROR", "SQL_ERROR", errMsg);
|
|
9080
9453
|
}
|
|
9081
|
-
const response = {
|
|
9082
|
-
type: "EXECUTE_SQL_SUCCESS",
|
|
9083
|
-
payload: {
|
|
9084
|
-
result
|
|
9085
|
-
},
|
|
9086
|
-
requestId
|
|
9087
|
-
};
|
|
9088
|
-
ws.send(JSON.stringify(response));
|
|
9089
9454
|
}
|
|
9090
9455
|
break;
|
|
9091
9456
|
case "FETCH_DATABASES":
|
|
@@ -9264,7 +9629,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9264
9629
|
const authContext = session?.user ? {
|
|
9265
9630
|
userId: session.user.userId,
|
|
9266
9631
|
roles: session.user.roles ?? []
|
|
9267
|
-
} :
|
|
9632
|
+
} : {
|
|
9633
|
+
userId: "anon",
|
|
9634
|
+
roles: ["anon"]
|
|
9635
|
+
};
|
|
9268
9636
|
await realtimeService.handleClientMessage(clientId, {
|
|
9269
9637
|
type,
|
|
9270
9638
|
payload,
|
|
@@ -9408,29 +9776,58 @@ const DEFAULT_ROLES = [{
|
|
|
9408
9776
|
},
|
|
9409
9777
|
config: null
|
|
9410
9778
|
}];
|
|
9411
|
-
async function ensureAuthTablesExist(db) {
|
|
9779
|
+
async function ensureAuthTablesExist(db, registry) {
|
|
9412
9780
|
console.log("🔍 Checking auth tables...");
|
|
9413
9781
|
try {
|
|
9782
|
+
let usersTableName = '"users"';
|
|
9783
|
+
let userIdType = "TEXT";
|
|
9784
|
+
let usersSchema2 = "public";
|
|
9785
|
+
if (registry) {
|
|
9786
|
+
const usersTable = registry.getTable("users");
|
|
9787
|
+
if (usersTable) {
|
|
9788
|
+
const {
|
|
9789
|
+
getTableName: getTableName2
|
|
9790
|
+
} = await import("drizzle-orm");
|
|
9791
|
+
usersSchema2 = getTableConfig(usersTable).schema || "public";
|
|
9792
|
+
usersTableName = usersSchema2 === "public" ? `"${getTableName2(usersTable)}"` : `"${usersSchema2}"."${getTableName2(usersTable)}"`;
|
|
9793
|
+
if (usersTable.id) {
|
|
9794
|
+
const col = usersTable.id;
|
|
9795
|
+
const meta = getColumnMeta(col);
|
|
9796
|
+
const columnType = meta.columnType;
|
|
9797
|
+
if (columnType === "PgUUID") {
|
|
9798
|
+
userIdType = "UUID";
|
|
9799
|
+
} else if (columnType === "PgSerial" || columnType === "PgInteger") {
|
|
9800
|
+
userIdType = "INTEGER";
|
|
9801
|
+
} else if (columnType === "PgBigInt" || columnType === "PgBigSerial") {
|
|
9802
|
+
userIdType = "BIGINT";
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9807
|
+
let rolesSchema = "rebase";
|
|
9808
|
+
if (registry) {
|
|
9809
|
+
const rolesTable = registry.getTable("roles");
|
|
9810
|
+
if (rolesTable) {
|
|
9811
|
+
rolesSchema = getTableConfig(rolesTable).schema || "public";
|
|
9812
|
+
}
|
|
9813
|
+
}
|
|
9814
|
+
if (usersSchema2 !== "public") {
|
|
9815
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema2)}`);
|
|
9816
|
+
}
|
|
9817
|
+
if (rolesSchema !== "public" && rolesSchema !== usersSchema2) {
|
|
9818
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
|
|
9819
|
+
}
|
|
9414
9820
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
9821
|
+
const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
|
|
9822
|
+
const rolesTableName = `"${rolesSchema}"."roles"`;
|
|
9823
|
+
const userRolesTableName = `"${rolesSchema}"."user_roles"`;
|
|
9824
|
+
const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
|
|
9825
|
+
const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
|
|
9826
|
+
const appConfigTableName = `"${rolesSchema}"."app_config"`;
|
|
9415
9827
|
await db.execute(sql`
|
|
9416
|
-
CREATE TABLE IF NOT EXISTS
|
|
9828
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
|
|
9417
9829
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
9418
|
-
|
|
9419
|
-
password_hash TEXT,
|
|
9420
|
-
display_name TEXT,
|
|
9421
|
-
photo_url TEXT,
|
|
9422
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
9423
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9424
|
-
)
|
|
9425
|
-
`);
|
|
9426
|
-
await db.execute(sql`
|
|
9427
|
-
CREATE INDEX IF NOT EXISTS idx_users_email
|
|
9428
|
-
ON rebase.users(email)
|
|
9429
|
-
`);
|
|
9430
|
-
await db.execute(sql`
|
|
9431
|
-
CREATE TABLE IF NOT EXISTS rebase.user_identities (
|
|
9432
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
9433
|
-
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
9830
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
9434
9831
|
provider TEXT NOT NULL,
|
|
9435
9832
|
provider_id TEXT NOT NULL,
|
|
9436
9833
|
profile_data JSONB,
|
|
@@ -9441,10 +9838,10 @@ async function ensureAuthTablesExist(db) {
|
|
|
9441
9838
|
`);
|
|
9442
9839
|
await db.execute(sql`
|
|
9443
9840
|
CREATE INDEX IF NOT EXISTS idx_user_identities_user
|
|
9444
|
-
ON
|
|
9841
|
+
ON ${sql.raw(userIdentitiesTable)}(user_id)
|
|
9445
9842
|
`);
|
|
9446
9843
|
await db.execute(sql`
|
|
9447
|
-
CREATE TABLE IF NOT EXISTS
|
|
9844
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
|
|
9448
9845
|
id TEXT PRIMARY KEY,
|
|
9449
9846
|
name TEXT NOT NULL,
|
|
9450
9847
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
@@ -9455,20 +9852,20 @@ async function ensureAuthTablesExist(db) {
|
|
|
9455
9852
|
)
|
|
9456
9853
|
`);
|
|
9457
9854
|
await db.execute(sql`
|
|
9458
|
-
CREATE TABLE IF NOT EXISTS
|
|
9459
|
-
user_id
|
|
9460
|
-
role_id TEXT NOT NULL REFERENCES
|
|
9855
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
|
|
9856
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
9857
|
+
role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
|
|
9461
9858
|
PRIMARY KEY (user_id, role_id)
|
|
9462
9859
|
)
|
|
9463
9860
|
`);
|
|
9464
9861
|
await db.execute(sql`
|
|
9465
9862
|
CREATE INDEX IF NOT EXISTS idx_user_roles_user
|
|
9466
|
-
ON
|
|
9863
|
+
ON ${sql.raw(userRolesTableName)}(user_id)
|
|
9467
9864
|
`);
|
|
9468
9865
|
await db.execute(sql`
|
|
9469
|
-
CREATE TABLE IF NOT EXISTS
|
|
9866
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
9470
9867
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
9471
|
-
user_id
|
|
9868
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
9472
9869
|
token_hash TEXT NOT NULL UNIQUE,
|
|
9473
9870
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
9474
9871
|
user_agent TEXT,
|
|
@@ -9479,16 +9876,16 @@ async function ensureAuthTablesExist(db) {
|
|
|
9479
9876
|
`);
|
|
9480
9877
|
await db.execute(sql`
|
|
9481
9878
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
|
|
9482
|
-
ON
|
|
9879
|
+
ON ${sql.raw(refreshTokensTableName)}(token_hash)
|
|
9483
9880
|
`);
|
|
9484
9881
|
await db.execute(sql`
|
|
9485
9882
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
|
|
9486
|
-
ON
|
|
9883
|
+
ON ${sql.raw(refreshTokensTableName)}(user_id)
|
|
9487
9884
|
`);
|
|
9488
9885
|
await db.execute(sql`
|
|
9489
|
-
CREATE TABLE IF NOT EXISTS
|
|
9886
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
|
|
9490
9887
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
9491
|
-
user_id
|
|
9888
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
9492
9889
|
token_hash TEXT NOT NULL UNIQUE,
|
|
9493
9890
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
9494
9891
|
used_at TIMESTAMP WITH TIME ZONE,
|
|
@@ -9497,20 +9894,19 @@ async function ensureAuthTablesExist(db) {
|
|
|
9497
9894
|
`);
|
|
9498
9895
|
await db.execute(sql`
|
|
9499
9896
|
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
|
|
9500
|
-
ON
|
|
9897
|
+
ON ${sql.raw(passwordResetTokensTableName)}(token_hash)
|
|
9501
9898
|
`);
|
|
9502
9899
|
await db.execute(sql`
|
|
9503
9900
|
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
|
|
9504
|
-
ON
|
|
9901
|
+
ON ${sql.raw(passwordResetTokensTableName)}(user_id)
|
|
9505
9902
|
`);
|
|
9506
9903
|
await db.execute(sql`
|
|
9507
|
-
CREATE TABLE IF NOT EXISTS
|
|
9904
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (
|
|
9508
9905
|
key TEXT PRIMARY KEY,
|
|
9509
9906
|
value JSONB NOT NULL,
|
|
9510
9907
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9511
9908
|
)
|
|
9512
9909
|
`);
|
|
9513
|
-
await applyInternalMigrations(db);
|
|
9514
9910
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
9515
9911
|
await db.transaction(async (tx) => {
|
|
9516
9912
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
@@ -9533,15 +9929,15 @@ async function ensureAuthTablesExist(db) {
|
|
|
9533
9929
|
$$ LANGUAGE sql STABLE
|
|
9534
9930
|
`);
|
|
9535
9931
|
});
|
|
9536
|
-
await seedDefaultRoles(db);
|
|
9932
|
+
await seedDefaultRoles(db, rolesTableName);
|
|
9537
9933
|
console.log("✅ Auth tables ready");
|
|
9538
9934
|
} catch (error) {
|
|
9539
9935
|
console.error("❌ Failed to create auth tables:", error);
|
|
9540
9936
|
console.warn("⚠️ Continuing without creating auth tables.");
|
|
9541
9937
|
}
|
|
9542
9938
|
}
|
|
9543
|
-
async function seedDefaultRoles(db) {
|
|
9544
|
-
const result = await db.execute(sql`SELECT COUNT(*) as count FROM
|
|
9939
|
+
async function seedDefaultRoles(db, rolesTableName) {
|
|
9940
|
+
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
9545
9941
|
const count2 = parseInt(result.rows[0]?.count || "0", 10);
|
|
9546
9942
|
if (count2 > 0) {
|
|
9547
9943
|
console.log(`📋 Found ${count2} existing roles`);
|
|
@@ -9550,7 +9946,7 @@ async function seedDefaultRoles(db) {
|
|
|
9550
9946
|
console.log("🌱 Seeding default roles...");
|
|
9551
9947
|
for (const role of DEFAULT_ROLES) {
|
|
9552
9948
|
await db.execute(sql`
|
|
9553
|
-
INSERT INTO
|
|
9949
|
+
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
|
|
9554
9950
|
VALUES (
|
|
9555
9951
|
${role.id},
|
|
9556
9952
|
${role.name},
|
|
@@ -9563,142 +9959,156 @@ async function seedDefaultRoles(db) {
|
|
|
9563
9959
|
}
|
|
9564
9960
|
console.log("✅ Default roles created: admin, editor, viewer");
|
|
9565
9961
|
}
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
const columnsCheck = await db.execute(sql`
|
|
9575
|
-
SELECT column_name
|
|
9576
|
-
FROM information_schema.columns
|
|
9577
|
-
WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
|
|
9578
|
-
`);
|
|
9579
|
-
const existingColumns = columnsCheck.rows.map((r) => r.column_name);
|
|
9580
|
-
if (existingColumns.includes("google_id")) {
|
|
9581
|
-
await db.execute(sql`
|
|
9582
|
-
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
9583
|
-
SELECT id, 'google', google_id
|
|
9584
|
-
FROM rebase.users
|
|
9585
|
-
WHERE google_id IS NOT NULL
|
|
9586
|
-
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
9587
|
-
`);
|
|
9588
|
-
}
|
|
9589
|
-
if (existingColumns.includes("linkedin_id")) {
|
|
9590
|
-
await db.execute(sql`
|
|
9591
|
-
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
9592
|
-
SELECT id, 'linkedin', linkedin_id
|
|
9593
|
-
FROM rebase.users
|
|
9594
|
-
WHERE linkedin_id IS NOT NULL
|
|
9595
|
-
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
9596
|
-
`);
|
|
9597
|
-
}
|
|
9598
|
-
if (existingColumns.length > 0) {
|
|
9599
|
-
await db.execute(sql`
|
|
9600
|
-
ALTER TABLE rebase.users
|
|
9601
|
-
DROP COLUMN IF EXISTS provider,
|
|
9602
|
-
DROP COLUMN IF EXISTS google_id,
|
|
9603
|
-
DROP COLUMN IF EXISTS linkedin_id
|
|
9604
|
-
`);
|
|
9605
|
-
await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
|
|
9606
|
-
await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
|
|
9607
|
-
console.log("✅ Migrated to user_identities and dropped legacy columns.");
|
|
9608
|
-
}
|
|
9609
|
-
await db.execute(sql`
|
|
9610
|
-
ALTER TABLE rebase.roles
|
|
9611
|
-
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
9612
|
-
`);
|
|
9613
|
-
await db.execute(sql`
|
|
9614
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9615
|
-
ADD COLUMN IF NOT EXISTS user_agent TEXT,
|
|
9616
|
-
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
9617
|
-
`);
|
|
9618
|
-
const constraintCheck = await db.execute(sql`
|
|
9619
|
-
SELECT 1 FROM information_schema.table_constraints
|
|
9620
|
-
WHERE constraint_name = 'unique_device_session'
|
|
9621
|
-
AND table_schema = 'rebase'
|
|
9622
|
-
AND table_name = 'refresh_tokens'
|
|
9623
|
-
`);
|
|
9624
|
-
if (constraintCheck.rows.length === 0) {
|
|
9625
|
-
try {
|
|
9626
|
-
await db.execute(sql`
|
|
9627
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9628
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9629
|
-
`);
|
|
9630
|
-
console.log("✅ Added unique_device_session constraint");
|
|
9631
|
-
} catch (e) {
|
|
9632
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
9633
|
-
if (errorMessage.includes("could not create unique index")) {
|
|
9634
|
-
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
9635
|
-
await db.execute(sql`
|
|
9636
|
-
DELETE FROM rebase.refresh_tokens a
|
|
9637
|
-
USING rebase.refresh_tokens b
|
|
9638
|
-
WHERE a.user_id = b.user_id
|
|
9639
|
-
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
9640
|
-
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
9641
|
-
AND a.created_at < b.created_at
|
|
9642
|
-
`);
|
|
9643
|
-
await db.execute(sql`
|
|
9644
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9645
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9646
|
-
`).catch((retryErr) => {
|
|
9647
|
-
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
9648
|
-
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
9649
|
-
});
|
|
9650
|
-
} else {
|
|
9651
|
-
console.error("Constraint migration issue:", errorMessage);
|
|
9652
|
-
}
|
|
9653
|
-
}
|
|
9654
|
-
}
|
|
9655
|
-
} catch (error) {
|
|
9656
|
-
console.error("❌ Failed to run internal migrations:", error);
|
|
9962
|
+
function getColumnKey(table, ...keys2) {
|
|
9963
|
+
if (!table) return void 0;
|
|
9964
|
+
for (const key of keys2) {
|
|
9965
|
+
if (key in table) return key;
|
|
9966
|
+
const snake = toSnakeCase(key);
|
|
9967
|
+
if (snake in table) return snake;
|
|
9968
|
+
const camel = camelCase(key);
|
|
9969
|
+
if (camel in table) return camel;
|
|
9657
9970
|
}
|
|
9971
|
+
return void 0;
|
|
9972
|
+
}
|
|
9973
|
+
function getColumn(table, ...keys2) {
|
|
9974
|
+
if (!table) return void 0;
|
|
9975
|
+
const key = getColumnKey(table, ...keys2);
|
|
9976
|
+
return key ? table[key] : void 0;
|
|
9658
9977
|
}
|
|
9659
9978
|
class UserService {
|
|
9660
|
-
constructor(db) {
|
|
9979
|
+
constructor(db, tableOrTables) {
|
|
9661
9980
|
this.db = db;
|
|
9981
|
+
if (tableOrTables && (tableOrTables.users || tableOrTables.roles)) {
|
|
9982
|
+
const tables = tableOrTables;
|
|
9983
|
+
this.usersTable = tables.users || users;
|
|
9984
|
+
this.userIdentitiesTable = tables.userIdentities || userIdentities;
|
|
9985
|
+
this.userRolesTable = tables.userRoles || userRoles;
|
|
9986
|
+
this.rolesTable = tables.roles || roles;
|
|
9987
|
+
} else {
|
|
9988
|
+
const table = tableOrTables;
|
|
9989
|
+
this.usersTable = table || users;
|
|
9990
|
+
this.userIdentitiesTable = userIdentities;
|
|
9991
|
+
this.userRolesTable = userRoles;
|
|
9992
|
+
this.rolesTable = roles;
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
usersTable;
|
|
9996
|
+
userIdentitiesTable;
|
|
9997
|
+
userRolesTable;
|
|
9998
|
+
rolesTable;
|
|
9999
|
+
getQualifiedUsersTableName() {
|
|
10000
|
+
const name = getTableName$1(this.usersTable);
|
|
10001
|
+
const schema = getTableConfig(this.usersTable).schema || "public";
|
|
10002
|
+
return `"${schema}"."${name}"`;
|
|
10003
|
+
}
|
|
10004
|
+
mapRowToUser(row) {
|
|
10005
|
+
if (!row) return row;
|
|
10006
|
+
const id = row.id ?? row.uid;
|
|
10007
|
+
const email = row.email;
|
|
10008
|
+
const passwordHash = row.password_hash ?? row.passwordHash ?? null;
|
|
10009
|
+
const displayName = row.display_name ?? row.displayName ?? null;
|
|
10010
|
+
const photoUrl = row.photo_url ?? row.photoUrl ?? row.photoURL ?? null;
|
|
10011
|
+
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10012
|
+
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10013
|
+
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10014
|
+
const createdAt = row.created_at ?? row.createdAt;
|
|
10015
|
+
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10016
|
+
const metadata = {
|
|
10017
|
+
...row.metadata || {}
|
|
10018
|
+
};
|
|
10019
|
+
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"]);
|
|
10020
|
+
for (const [key, val] of Object.entries(row)) {
|
|
10021
|
+
if (!knownKeys.has(key)) {
|
|
10022
|
+
const camelKey = camelCase(key);
|
|
10023
|
+
metadata[camelKey] = val;
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
return {
|
|
10027
|
+
id,
|
|
10028
|
+
email,
|
|
10029
|
+
passwordHash,
|
|
10030
|
+
displayName,
|
|
10031
|
+
photoUrl,
|
|
10032
|
+
emailVerified,
|
|
10033
|
+
emailVerificationToken,
|
|
10034
|
+
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10035
|
+
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10036
|
+
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10037
|
+
metadata
|
|
10038
|
+
};
|
|
10039
|
+
}
|
|
10040
|
+
mapPayload(data) {
|
|
10041
|
+
if (!data) return {};
|
|
10042
|
+
const payload = {};
|
|
10043
|
+
const idKey = getColumnKey(this.usersTable, "id") || "id";
|
|
10044
|
+
const emailKey = getColumnKey(this.usersTable, "email") || "email";
|
|
10045
|
+
const passwordHashKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
10046
|
+
const displayNameKey = getColumnKey(this.usersTable, "displayName", "display_name") || "displayName";
|
|
10047
|
+
const photoUrlKey = getColumnKey(this.usersTable, "photoUrl", "photo_url") || "photoUrl";
|
|
10048
|
+
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10049
|
+
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10050
|
+
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10051
|
+
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10052
|
+
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10053
|
+
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
10054
|
+
if ("id" in data) payload[idKey] = data.id;
|
|
10055
|
+
if ("email" in data) payload[emailKey] = data.email;
|
|
10056
|
+
if ("passwordHash" in data) payload[passwordHashKey] = data.passwordHash;
|
|
10057
|
+
if ("displayName" in data) payload[displayNameKey] = data.displayName;
|
|
10058
|
+
if ("photoUrl" in data) payload[photoUrlKey] = data.photoUrl;
|
|
10059
|
+
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10060
|
+
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10061
|
+
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10062
|
+
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10063
|
+
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10064
|
+
const metadata = {
|
|
10065
|
+
...data.metadata || {}
|
|
10066
|
+
};
|
|
10067
|
+
const remainingMetadata = {};
|
|
10068
|
+
for (const [key, val] of Object.entries(metadata)) {
|
|
10069
|
+
const tableColKey = getColumnKey(this.usersTable, key);
|
|
10070
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10071
|
+
payload[tableColKey] = val;
|
|
10072
|
+
} else {
|
|
10073
|
+
remainingMetadata[key] = val;
|
|
10074
|
+
}
|
|
10075
|
+
}
|
|
10076
|
+
if (metadataKey in this.usersTable) {
|
|
10077
|
+
payload[metadataKey] = remainingMetadata;
|
|
10078
|
+
}
|
|
10079
|
+
return payload;
|
|
9662
10080
|
}
|
|
9663
10081
|
async createUser(data) {
|
|
9664
|
-
const
|
|
9665
|
-
|
|
10082
|
+
const payload = this.mapPayload(data);
|
|
10083
|
+
const [row] = await this.db.insert(this.usersTable).values(payload).returning();
|
|
10084
|
+
return this.mapRowToUser(row);
|
|
9666
10085
|
}
|
|
9667
10086
|
async getUserById(id) {
|
|
9668
|
-
const
|
|
9669
|
-
|
|
10087
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10088
|
+
if (!idCol) return null;
|
|
10089
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(idCol, id));
|
|
10090
|
+
return row ? this.mapRowToUser(row) : null;
|
|
9670
10091
|
}
|
|
9671
10092
|
async getUserByEmail(email) {
|
|
9672
|
-
const
|
|
9673
|
-
|
|
10093
|
+
const emailCol = getColumn(this.usersTable, "email");
|
|
10094
|
+
if (!emailCol) return null;
|
|
10095
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, email.toLowerCase()));
|
|
10096
|
+
return row ? this.mapRowToUser(row) : null;
|
|
9674
10097
|
}
|
|
9675
10098
|
async getUserByIdentity(provider, providerId) {
|
|
9676
|
-
const
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
if (result.rows.length === 0) return null;
|
|
9684
|
-
const row = result.rows[0];
|
|
9685
|
-
return {
|
|
9686
|
-
id: row.id,
|
|
9687
|
-
email: row.email,
|
|
9688
|
-
passwordHash: row.password_hash ?? null,
|
|
9689
|
-
displayName: row.display_name ?? null,
|
|
9690
|
-
photoUrl: row.photo_url ?? null,
|
|
9691
|
-
emailVerified: row.email_verified ?? false,
|
|
9692
|
-
emailVerificationToken: row.email_verification_token ?? null,
|
|
9693
|
-
emailVerificationSentAt: row.email_verification_sent_at ?? null,
|
|
9694
|
-
createdAt: row.created_at,
|
|
9695
|
-
updatedAt: row.updated_at
|
|
9696
|
-
};
|
|
10099
|
+
const userIdCol = getColumn(this.usersTable, "id");
|
|
10100
|
+
if (!userIdCol) return null;
|
|
10101
|
+
const result = await this.db.select({
|
|
10102
|
+
user: this.usersTable
|
|
10103
|
+
}).from(this.usersTable).innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.userId)).where(sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`).limit(1);
|
|
10104
|
+
if (result.length === 0) return null;
|
|
10105
|
+
return this.mapRowToUser(result[0].user);
|
|
9697
10106
|
}
|
|
9698
10107
|
async getUserIdentities(userId) {
|
|
10108
|
+
const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
|
|
9699
10109
|
const result = await this.db.execute(sql`
|
|
9700
10110
|
SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
|
|
9701
|
-
FROM
|
|
10111
|
+
FROM ${sql.raw(`"${schema}"."user_identities"`)}
|
|
9702
10112
|
WHERE user_id = ${userId}
|
|
9703
10113
|
`);
|
|
9704
10114
|
return result.rows.map((row) => ({
|
|
@@ -9712,27 +10122,32 @@ class UserService {
|
|
|
9712
10122
|
}));
|
|
9713
10123
|
}
|
|
9714
10124
|
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
9715
|
-
await this.db.insert(
|
|
10125
|
+
await this.db.insert(this.userIdentitiesTable).values({
|
|
9716
10126
|
userId,
|
|
9717
10127
|
provider,
|
|
9718
10128
|
providerId,
|
|
9719
10129
|
profileData: profileData || null
|
|
9720
10130
|
}).onConflictDoNothing({
|
|
9721
|
-
target: [
|
|
10131
|
+
target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId]
|
|
9722
10132
|
});
|
|
9723
10133
|
}
|
|
9724
10134
|
async updateUser(id, data) {
|
|
9725
|
-
const
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
10135
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10136
|
+
if (!idCol) return null;
|
|
10137
|
+
const payload = this.mapPayload(data);
|
|
10138
|
+
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10139
|
+
payload[updatedAtKey] = /* @__PURE__ */ new Date();
|
|
10140
|
+
const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
|
|
10141
|
+
return row ? this.mapRowToUser(row) : null;
|
|
9730
10142
|
}
|
|
9731
10143
|
async deleteUser(id) {
|
|
9732
|
-
|
|
10144
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10145
|
+
if (!idCol) return;
|
|
10146
|
+
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
9733
10147
|
}
|
|
9734
10148
|
async listUsers() {
|
|
9735
|
-
|
|
10149
|
+
const rows = await this.db.select().from(this.usersTable);
|
|
10150
|
+
return rows.map((row) => this.mapRowToUser(row));
|
|
9736
10151
|
}
|
|
9737
10152
|
async listUsersPaginated(options) {
|
|
9738
10153
|
const limit = options?.limit ?? 25;
|
|
@@ -9741,49 +10156,40 @@ class UserService {
|
|
|
9741
10156
|
const orderBy = options?.orderBy || "createdAt";
|
|
9742
10157
|
const orderDir = options?.orderDir || "desc";
|
|
9743
10158
|
const roleId = options?.roleId;
|
|
9744
|
-
const
|
|
9745
|
-
|
|
9746
|
-
displayName: "display_name",
|
|
9747
|
-
createdAt: "created_at",
|
|
9748
|
-
updatedAt: "updated_at",
|
|
9749
|
-
provider: "provider"
|
|
9750
|
-
};
|
|
9751
|
-
const orderColumn = columnMap[orderBy] || "created_at";
|
|
10159
|
+
const orderCol = getColumn(this.usersTable, orderBy);
|
|
10160
|
+
const orderColumn = orderCol ? orderCol.name : "created_at";
|
|
9752
10161
|
const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
|
|
10162
|
+
const emailCol = getColumn(this.usersTable, "email");
|
|
10163
|
+
const emailColumn = emailCol ? emailCol.name : "email";
|
|
10164
|
+
const displayNameCol = getColumn(this.usersTable, "displayName", "display_name");
|
|
10165
|
+
const displayNameColumn = displayNameCol ? displayNameCol.name : "display_name";
|
|
10166
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10167
|
+
const idColumn = idCol ? idCol.name : "id";
|
|
10168
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
10169
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
9753
10170
|
const conditions = [];
|
|
9754
10171
|
if (roleId) {
|
|
9755
|
-
conditions.push(sql`EXISTS (SELECT 1 FROM
|
|
10172
|
+
conditions.push(sql`EXISTS (SELECT 1 FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
|
|
9756
10173
|
}
|
|
9757
10174
|
if (search) {
|
|
9758
10175
|
const pattern = `%${search}%`;
|
|
9759
|
-
conditions.push(sql`(
|
|
10176
|
+
conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);
|
|
9760
10177
|
}
|
|
9761
10178
|
const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
|
9762
|
-
const orderByClause = roleId ? sql`ORDER BY ${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (SELECT count(*) FROM
|
|
10179
|
+
const orderByClause = roleId ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (SELECT count(*) FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)}) DESC, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
|
|
9763
10180
|
const countResult = await this.db.execute(sql`
|
|
9764
|
-
SELECT count(*)::int as total FROM
|
|
10181
|
+
SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
|
|
9765
10182
|
${whereClause}
|
|
9766
10183
|
`);
|
|
9767
10184
|
const total = countResult.rows[0].total;
|
|
9768
10185
|
const dataResult = await this.db.execute(sql`
|
|
9769
|
-
SELECT * FROM
|
|
10186
|
+
SELECT * FROM ${sql.raw(usersTableName)}
|
|
9770
10187
|
${whereClause}
|
|
9771
10188
|
${orderByClause}
|
|
9772
10189
|
LIMIT ${limit} OFFSET ${offset}
|
|
9773
10190
|
`);
|
|
9774
10191
|
const rows = dataResult.rows;
|
|
9775
|
-
const mappedUsers = rows.map((row) => (
|
|
9776
|
-
id: row.id,
|
|
9777
|
-
email: row.email,
|
|
9778
|
-
passwordHash: row.password_hash ?? row.passwordHash ?? null,
|
|
9779
|
-
displayName: row.display_name ?? row.displayName ?? null,
|
|
9780
|
-
photoUrl: row.photo_url ?? row.photoUrl ?? null,
|
|
9781
|
-
emailVerified: row.email_verified ?? row.emailVerified ?? false,
|
|
9782
|
-
emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
|
|
9783
|
-
emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
|
|
9784
|
-
createdAt: row.created_at ?? row.createdAt,
|
|
9785
|
-
updatedAt: row.updated_at ?? row.updatedAt
|
|
9786
|
-
}));
|
|
10192
|
+
const mappedUsers = rows.map((row) => this.mapRowToUser(row));
|
|
9787
10193
|
return {
|
|
9788
10194
|
users: mappedUsers,
|
|
9789
10195
|
total,
|
|
@@ -9795,46 +10201,63 @@ class UserService {
|
|
|
9795
10201
|
* Update user's password hash
|
|
9796
10202
|
*/
|
|
9797
10203
|
async updatePassword(id, passwordHash) {
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
10204
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10205
|
+
if (!idCol) return;
|
|
10206
|
+
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
10207
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10208
|
+
await this.db.update(this.usersTable).set({
|
|
10209
|
+
[passwordHashColKey]: passwordHash,
|
|
10210
|
+
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
10211
|
+
}).where(eq(idCol, id));
|
|
9802
10212
|
}
|
|
9803
10213
|
/**
|
|
9804
10214
|
* Set email verification status
|
|
9805
10215
|
*/
|
|
9806
10216
|
async setEmailVerified(id, verified) {
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
10217
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10218
|
+
if (!idCol) return;
|
|
10219
|
+
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10220
|
+
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10221
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10222
|
+
await this.db.update(this.usersTable).set({
|
|
10223
|
+
[emailVerifiedColKey]: verified,
|
|
10224
|
+
[emailVerificationTokenColKey]: null,
|
|
10225
|
+
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
10226
|
+
}).where(eq(idCol, id));
|
|
9812
10227
|
}
|
|
9813
10228
|
/**
|
|
9814
10229
|
* Set email verification token
|
|
9815
10230
|
*/
|
|
9816
10231
|
async setVerificationToken(id, token) {
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
10232
|
+
const idCol = getColumn(this.usersTable, "id");
|
|
10233
|
+
if (!idCol) return;
|
|
10234
|
+
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10235
|
+
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10236
|
+
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10237
|
+
await this.db.update(this.usersTable).set({
|
|
10238
|
+
[emailVerificationTokenColKey]: token,
|
|
10239
|
+
[emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
|
|
10240
|
+
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
10241
|
+
}).where(eq(idCol, id));
|
|
9822
10242
|
}
|
|
9823
10243
|
/**
|
|
9824
10244
|
* Find user by email verification token
|
|
9825
10245
|
*/
|
|
9826
10246
|
async getUserByVerificationToken(token) {
|
|
9827
|
-
const
|
|
9828
|
-
|
|
10247
|
+
const tokenCol = getColumn(this.usersTable, "emailVerificationToken", "email_verification_token");
|
|
10248
|
+
if (!tokenCol) return null;
|
|
10249
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(tokenCol, token));
|
|
10250
|
+
return row ? this.mapRowToUser(row) : null;
|
|
9829
10251
|
}
|
|
9830
10252
|
/**
|
|
9831
10253
|
* Get roles for a user from database
|
|
9832
10254
|
*/
|
|
9833
10255
|
async getUserRoles(userId) {
|
|
10256
|
+
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
9834
10257
|
const result = await this.db.execute(sql`
|
|
9835
10258
|
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
|
|
9836
|
-
FROM
|
|
9837
|
-
INNER JOIN
|
|
10259
|
+
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10260
|
+
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
9838
10261
|
WHERE ur.user_id = ${userId}
|
|
9839
10262
|
`);
|
|
9840
10263
|
return result.rows.map((row) => ({
|
|
@@ -9857,10 +10280,11 @@ class UserService {
|
|
|
9857
10280
|
* Set roles for a user
|
|
9858
10281
|
*/
|
|
9859
10282
|
async setUserRoles(userId, roleIds) {
|
|
9860
|
-
|
|
10283
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
10284
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
|
|
9861
10285
|
for (const roleId of roleIds) {
|
|
9862
10286
|
await this.db.execute(sql`
|
|
9863
|
-
INSERT INTO
|
|
10287
|
+
INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
|
|
9864
10288
|
VALUES (${userId}, ${roleId})
|
|
9865
10289
|
ON CONFLICT DO NOTHING
|
|
9866
10290
|
`);
|
|
@@ -9870,8 +10294,9 @@ class UserService {
|
|
|
9870
10294
|
* Assign a specific role to new user
|
|
9871
10295
|
*/
|
|
9872
10296
|
async assignDefaultRole(userId, roleId) {
|
|
10297
|
+
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
9873
10298
|
await this.db.execute(sql`
|
|
9874
|
-
INSERT INTO
|
|
10299
|
+
INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
|
|
9875
10300
|
VALUES (${userId}, ${roleId})
|
|
9876
10301
|
ON CONFLICT DO NOTHING
|
|
9877
10302
|
`);
|
|
@@ -9890,13 +10315,25 @@ class UserService {
|
|
|
9890
10315
|
}
|
|
9891
10316
|
}
|
|
9892
10317
|
class RoleService {
|
|
9893
|
-
constructor(db) {
|
|
10318
|
+
constructor(db, tableOrTables) {
|
|
9894
10319
|
this.db = db;
|
|
10320
|
+
if (tableOrTables && (tableOrTables.roles || tableOrTables.users)) {
|
|
10321
|
+
this.rolesTable = tableOrTables.roles || roles;
|
|
10322
|
+
} else {
|
|
10323
|
+
this.rolesTable = tableOrTables || roles;
|
|
10324
|
+
}
|
|
10325
|
+
}
|
|
10326
|
+
rolesTable;
|
|
10327
|
+
getQualifiedRolesTableName() {
|
|
10328
|
+
const name = getTableName$1(this.rolesTable);
|
|
10329
|
+
const schema = getTableConfig(this.rolesTable).schema || "public";
|
|
10330
|
+
return `"${schema}"."${name}"`;
|
|
9895
10331
|
}
|
|
9896
10332
|
async getRoleById(id) {
|
|
10333
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
9897
10334
|
const result = await this.db.execute(sql`
|
|
9898
10335
|
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
9899
|
-
FROM
|
|
10336
|
+
FROM ${sql.raw(tableName)}
|
|
9900
10337
|
WHERE id = ${id}
|
|
9901
10338
|
`);
|
|
9902
10339
|
if (result.rows.length === 0) return null;
|
|
@@ -9911,9 +10348,10 @@ class RoleService {
|
|
|
9911
10348
|
};
|
|
9912
10349
|
}
|
|
9913
10350
|
async listRoles() {
|
|
10351
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
9914
10352
|
const result = await this.db.execute(sql`
|
|
9915
10353
|
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
9916
|
-
FROM
|
|
10354
|
+
FROM ${sql.raw(tableName)}
|
|
9917
10355
|
ORDER BY name
|
|
9918
10356
|
`);
|
|
9919
10357
|
return result.rows.map((row) => ({
|
|
@@ -9926,8 +10364,9 @@ class RoleService {
|
|
|
9926
10364
|
}));
|
|
9927
10365
|
}
|
|
9928
10366
|
async createRole(data) {
|
|
10367
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
9929
10368
|
const result = await this.db.execute(sql`
|
|
9930
|
-
INSERT INTO
|
|
10369
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
|
|
9931
10370
|
VALUES (
|
|
9932
10371
|
${data.id},
|
|
9933
10372
|
${data.name},
|
|
@@ -9951,8 +10390,9 @@ class RoleService {
|
|
|
9951
10390
|
async updateRole(id, data) {
|
|
9952
10391
|
const existing = await this.getRoleById(id);
|
|
9953
10392
|
if (!existing) return null;
|
|
10393
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
9954
10394
|
await this.db.execute(sql`
|
|
9955
|
-
UPDATE
|
|
10395
|
+
UPDATE ${sql.raw(tableName)}
|
|
9956
10396
|
SET
|
|
9957
10397
|
name = ${data.name ?? existing.name},
|
|
9958
10398
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
@@ -9964,23 +10404,36 @@ class RoleService {
|
|
|
9964
10404
|
return this.getRoleById(id);
|
|
9965
10405
|
}
|
|
9966
10406
|
async deleteRole(id) {
|
|
9967
|
-
|
|
10407
|
+
const tableName = this.getQualifiedRolesTableName();
|
|
10408
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
|
|
9968
10409
|
}
|
|
9969
10410
|
}
|
|
9970
10411
|
class RefreshTokenService {
|
|
9971
|
-
constructor(db) {
|
|
10412
|
+
constructor(db, tableOrTables) {
|
|
9972
10413
|
this.db = db;
|
|
10414
|
+
if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users)) {
|
|
10415
|
+
this.refreshTokensTable = tableOrTables.refreshTokens || refreshTokens;
|
|
10416
|
+
} else {
|
|
10417
|
+
this.refreshTokensTable = tableOrTables || refreshTokens;
|
|
10418
|
+
}
|
|
10419
|
+
}
|
|
10420
|
+
refreshTokensTable;
|
|
10421
|
+
getQualifiedRefreshTokensTableName() {
|
|
10422
|
+
const name = getTableName$1(this.refreshTokensTable);
|
|
10423
|
+
const schema = getTableConfig(this.refreshTokensTable).schema || "public";
|
|
10424
|
+
return `"${schema}"."${name}"`;
|
|
9973
10425
|
}
|
|
9974
10426
|
async createToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
9975
10427
|
const safeUserAgent = userAgent || "";
|
|
9976
10428
|
const safeIpAddress = ipAddress || "";
|
|
10429
|
+
const tableName = this.getQualifiedRefreshTokensTableName();
|
|
9977
10430
|
await this.db.execute(sql`
|
|
9978
|
-
DELETE FROM
|
|
10431
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
9979
10432
|
WHERE user_id = ${userId}
|
|
9980
10433
|
AND user_agent = ${safeUserAgent}
|
|
9981
10434
|
AND ip_address = ${safeIpAddress}
|
|
9982
10435
|
`);
|
|
9983
|
-
await this.db.insert(
|
|
10436
|
+
await this.db.insert(this.refreshTokensTable).values({
|
|
9984
10437
|
userId,
|
|
9985
10438
|
tokenHash,
|
|
9986
10439
|
expiresAt,
|
|
@@ -9990,51 +10443,63 @@ class RefreshTokenService {
|
|
|
9990
10443
|
}
|
|
9991
10444
|
async findByHash(tokenHash) {
|
|
9992
10445
|
const [token] = await this.db.select({
|
|
9993
|
-
id:
|
|
9994
|
-
userId:
|
|
9995
|
-
tokenHash:
|
|
9996
|
-
expiresAt:
|
|
9997
|
-
createdAt:
|
|
9998
|
-
userAgent:
|
|
9999
|
-
ipAddress:
|
|
10000
|
-
}).from(
|
|
10446
|
+
id: this.refreshTokensTable.id,
|
|
10447
|
+
userId: this.refreshTokensTable.userId,
|
|
10448
|
+
tokenHash: this.refreshTokensTable.tokenHash,
|
|
10449
|
+
expiresAt: this.refreshTokensTable.expiresAt,
|
|
10450
|
+
createdAt: this.refreshTokensTable.createdAt,
|
|
10451
|
+
userAgent: this.refreshTokensTable.userAgent,
|
|
10452
|
+
ipAddress: this.refreshTokensTable.ipAddress
|
|
10453
|
+
}).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
10001
10454
|
return token || null;
|
|
10002
10455
|
}
|
|
10003
10456
|
async deleteByHash(tokenHash) {
|
|
10004
|
-
await this.db.delete(
|
|
10457
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
10005
10458
|
}
|
|
10006
10459
|
async deleteAllForUser(userId) {
|
|
10007
|
-
await this.db.delete(
|
|
10460
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.userId, userId));
|
|
10008
10461
|
}
|
|
10009
10462
|
async listForUser(userId) {
|
|
10010
10463
|
const tokens = await this.db.select({
|
|
10011
|
-
id:
|
|
10012
|
-
userId:
|
|
10013
|
-
tokenHash:
|
|
10014
|
-
expiresAt:
|
|
10015
|
-
createdAt:
|
|
10016
|
-
userAgent:
|
|
10017
|
-
ipAddress:
|
|
10018
|
-
}).from(
|
|
10464
|
+
id: this.refreshTokensTable.id,
|
|
10465
|
+
userId: this.refreshTokensTable.userId,
|
|
10466
|
+
tokenHash: this.refreshTokensTable.tokenHash,
|
|
10467
|
+
expiresAt: this.refreshTokensTable.expiresAt,
|
|
10468
|
+
createdAt: this.refreshTokensTable.createdAt,
|
|
10469
|
+
userAgent: this.refreshTokensTable.userAgent,
|
|
10470
|
+
ipAddress: this.refreshTokensTable.ipAddress
|
|
10471
|
+
}).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.userId, userId)).orderBy(this.refreshTokensTable.createdAt);
|
|
10019
10472
|
return tokens;
|
|
10020
10473
|
}
|
|
10021
10474
|
async deleteById(id, userId) {
|
|
10022
|
-
await this.db.delete(
|
|
10475
|
+
await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.userId} = ${userId}`);
|
|
10023
10476
|
}
|
|
10024
10477
|
}
|
|
10025
10478
|
class PasswordResetTokenService {
|
|
10026
|
-
constructor(db) {
|
|
10479
|
+
constructor(db, tableOrTables) {
|
|
10027
10480
|
this.db = db;
|
|
10481
|
+
if (tableOrTables && (tableOrTables.passwordResetTokens || tableOrTables.users)) {
|
|
10482
|
+
this.passwordResetTokensTable = tableOrTables.passwordResetTokens || passwordResetTokens;
|
|
10483
|
+
} else {
|
|
10484
|
+
this.passwordResetTokensTable = tableOrTables || passwordResetTokens;
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
passwordResetTokensTable;
|
|
10488
|
+
getQualifiedPasswordResetTokensTableName() {
|
|
10489
|
+
const name = getTableName$1(this.passwordResetTokensTable);
|
|
10490
|
+
const schema = getTableConfig(this.passwordResetTokensTable).schema || "public";
|
|
10491
|
+
return `"${schema}"."${name}"`;
|
|
10028
10492
|
}
|
|
10029
10493
|
/**
|
|
10030
10494
|
* Create a password reset token
|
|
10031
10495
|
*/
|
|
10032
10496
|
async createToken(userId, tokenHash, expiresAt) {
|
|
10497
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
10033
10498
|
await this.db.execute(sql`
|
|
10034
|
-
DELETE FROM
|
|
10499
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
10035
10500
|
WHERE user_id = ${userId} AND used_at IS NULL
|
|
10036
10501
|
`);
|
|
10037
|
-
await this.db.insert(
|
|
10502
|
+
await this.db.insert(this.passwordResetTokensTable).values({
|
|
10038
10503
|
userId,
|
|
10039
10504
|
tokenHash,
|
|
10040
10505
|
expiresAt
|
|
@@ -10045,13 +10510,14 @@ class PasswordResetTokenService {
|
|
|
10045
10510
|
*/
|
|
10046
10511
|
async findValidByHash(tokenHash) {
|
|
10047
10512
|
const [token] = await this.db.select({
|
|
10048
|
-
userId:
|
|
10049
|
-
expiresAt:
|
|
10050
|
-
}).from(
|
|
10513
|
+
userId: this.passwordResetTokensTable.userId,
|
|
10514
|
+
expiresAt: this.passwordResetTokensTable.expiresAt
|
|
10515
|
+
}).from(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));
|
|
10051
10516
|
if (!token) return null;
|
|
10517
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
10052
10518
|
const result = await this.db.execute(sql`
|
|
10053
10519
|
SELECT user_id, expires_at
|
|
10054
|
-
FROM
|
|
10520
|
+
FROM ${sql.raw(tableName)}
|
|
10055
10521
|
WHERE token_hash = ${tokenHash}
|
|
10056
10522
|
AND used_at IS NULL
|
|
10057
10523
|
AND expires_at > NOW()
|
|
@@ -10067,31 +10533,32 @@ class PasswordResetTokenService {
|
|
|
10067
10533
|
* Mark token as used
|
|
10068
10534
|
*/
|
|
10069
10535
|
async markAsUsed(tokenHash) {
|
|
10070
|
-
await this.db.update(
|
|
10536
|
+
await this.db.update(this.passwordResetTokensTable).set({
|
|
10071
10537
|
usedAt: /* @__PURE__ */ new Date()
|
|
10072
|
-
}).where(eq(
|
|
10538
|
+
}).where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));
|
|
10073
10539
|
}
|
|
10074
10540
|
/**
|
|
10075
10541
|
* Delete all tokens for a user
|
|
10076
10542
|
*/
|
|
10077
10543
|
async deleteAllForUser(userId) {
|
|
10078
|
-
await this.db.delete(
|
|
10544
|
+
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.userId, userId));
|
|
10079
10545
|
}
|
|
10080
10546
|
/**
|
|
10081
10547
|
* Clean up expired tokens
|
|
10082
10548
|
*/
|
|
10083
10549
|
async deleteExpired() {
|
|
10550
|
+
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
10084
10551
|
await this.db.execute(sql`
|
|
10085
|
-
DELETE FROM
|
|
10552
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
10086
10553
|
WHERE expires_at < NOW()
|
|
10087
10554
|
`);
|
|
10088
10555
|
}
|
|
10089
10556
|
}
|
|
10090
10557
|
class PostgresTokenRepository {
|
|
10091
|
-
constructor(db) {
|
|
10558
|
+
constructor(db, tableOrTables) {
|
|
10092
10559
|
this.db = db;
|
|
10093
|
-
this.refreshTokenService = new RefreshTokenService(db);
|
|
10094
|
-
this.passwordResetTokenService = new PasswordResetTokenService(db);
|
|
10560
|
+
this.refreshTokenService = new RefreshTokenService(db, tableOrTables);
|
|
10561
|
+
this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
|
|
10095
10562
|
}
|
|
10096
10563
|
refreshTokenService;
|
|
10097
10564
|
passwordResetTokenService;
|
|
@@ -10132,11 +10599,11 @@ class PostgresTokenRepository {
|
|
|
10132
10599
|
}
|
|
10133
10600
|
}
|
|
10134
10601
|
class PostgresAuthRepository {
|
|
10135
|
-
constructor(db) {
|
|
10602
|
+
constructor(db, tableOrTables) {
|
|
10136
10603
|
this.db = db;
|
|
10137
|
-
this.userService = new UserService(db);
|
|
10138
|
-
this.roleService = new RoleService(db);
|
|
10139
|
-
this.tokenRepository = new PostgresTokenRepository(db);
|
|
10604
|
+
this.userService = new UserService(db, tableOrTables);
|
|
10605
|
+
this.roleService = new RoleService(db, tableOrTables);
|
|
10606
|
+
this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
|
|
10140
10607
|
}
|
|
10141
10608
|
userService;
|
|
10142
10609
|
roleService;
|
|
@@ -10197,8 +10664,7 @@ class PostgresAuthRepository {
|
|
|
10197
10664
|
await this.userService.assignDefaultRole(userId, roleId);
|
|
10198
10665
|
}
|
|
10199
10666
|
async getUserWithRoles(userId) {
|
|
10200
|
-
|
|
10201
|
-
return result;
|
|
10667
|
+
return this.userService.getUserWithRoles(userId);
|
|
10202
10668
|
}
|
|
10203
10669
|
// Role operations (delegate to RoleService)
|
|
10204
10670
|
async getRoleById(id) {
|
|
@@ -10386,6 +10852,24 @@ class HistoryService {
|
|
|
10386
10852
|
return result.rowCount ?? 0;
|
|
10387
10853
|
}
|
|
10388
10854
|
}
|
|
10855
|
+
function deepEqual(a, b) {
|
|
10856
|
+
if (a === b) return true;
|
|
10857
|
+
if (a == null || b == null) return false;
|
|
10858
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
10859
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
10860
|
+
if (a.length !== b.length) return false;
|
|
10861
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
10862
|
+
}
|
|
10863
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
10864
|
+
const aObj = a;
|
|
10865
|
+
const bObj = b;
|
|
10866
|
+
const aKeys = Object.keys(aObj);
|
|
10867
|
+
const bKeys = Object.keys(bObj);
|
|
10868
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
10869
|
+
return aKeys.every((k) => deepEqual(aObj[k], bObj[k]));
|
|
10870
|
+
}
|
|
10871
|
+
return false;
|
|
10872
|
+
}
|
|
10389
10873
|
function findChangedFields(oldValues, newValues) {
|
|
10390
10874
|
const changed = [];
|
|
10391
10875
|
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldValues), ...Object.keys(newValues)]);
|
|
@@ -10395,7 +10879,7 @@ function findChangedFields(oldValues, newValues) {
|
|
|
10395
10879
|
if (key.startsWith("__")) continue;
|
|
10396
10880
|
if (oldVal !== newVal) {
|
|
10397
10881
|
if (typeof oldVal === "object" && oldVal !== null && typeof newVal === "object" && newVal !== null) {
|
|
10398
|
-
if (
|
|
10882
|
+
if (!deepEqual(oldVal, newVal)) {
|
|
10399
10883
|
changed.push(key);
|
|
10400
10884
|
}
|
|
10401
10885
|
} else {
|
|
@@ -10513,14 +10997,32 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10513
10997
|
if (!authConfig) return void 0;
|
|
10514
10998
|
const internals = driverResult.internals;
|
|
10515
10999
|
const db = internals.db;
|
|
10516
|
-
|
|
11000
|
+
const registry = internals.registry;
|
|
11001
|
+
await ensureAuthTablesExist(db, registry);
|
|
10517
11002
|
let emailService;
|
|
10518
11003
|
if (authConfig.email) {
|
|
10519
11004
|
emailService = createEmailService(authConfig.email);
|
|
10520
11005
|
}
|
|
10521
|
-
const
|
|
10522
|
-
const
|
|
10523
|
-
|
|
11006
|
+
const customUsersTable = registry?.getTable("users");
|
|
11007
|
+
const customRolesTable = registry?.getTable("roles");
|
|
11008
|
+
let usersSchemaName = "rebase";
|
|
11009
|
+
let rolesSchemaName = "rebase";
|
|
11010
|
+
if (customUsersTable) {
|
|
11011
|
+
usersSchemaName = getTableConfig(customUsersTable).schema || "public";
|
|
11012
|
+
}
|
|
11013
|
+
if (customRolesTable) {
|
|
11014
|
+
rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
|
|
11015
|
+
}
|
|
11016
|
+
const authTables = createAuthSchema(rolesSchemaName, usersSchemaName);
|
|
11017
|
+
if (customUsersTable) {
|
|
11018
|
+
authTables.users = customUsersTable;
|
|
11019
|
+
}
|
|
11020
|
+
if (customRolesTable) {
|
|
11021
|
+
authTables.roles = customRolesTable;
|
|
11022
|
+
}
|
|
11023
|
+
const userService = new UserService(db, authTables);
|
|
11024
|
+
const roleService = new RoleService(db, authTables);
|
|
11025
|
+
const authRepository = new PostgresAuthRepository(db, authTables);
|
|
10524
11026
|
return {
|
|
10525
11027
|
userService,
|
|
10526
11028
|
roleService,
|
|
@@ -10552,11 +11054,54 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10552
11054
|
},
|
|
10553
11055
|
mountRoutes(app, basePath, driverResult) {
|
|
10554
11056
|
},
|
|
10555
|
-
async initializeWebsockets(server, realtimeService, driver, config) {
|
|
11057
|
+
async initializeWebsockets(server, realtimeService, driver, config, adapter) {
|
|
10556
11058
|
const {
|
|
10557
11059
|
createPostgresWebSocket: createPostgresWebSocket2
|
|
10558
11060
|
} = await Promise.resolve().then(() => websocket);
|
|
10559
|
-
createPostgresWebSocket2(server, realtimeService, driver, config);
|
|
11061
|
+
createPostgresWebSocket2(server, realtimeService, driver, config, adapter);
|
|
11062
|
+
}
|
|
11063
|
+
};
|
|
11064
|
+
}
|
|
11065
|
+
function createPostgresAdapter(pgConfig) {
|
|
11066
|
+
const bootstrapper = createPostgresBootstrapper(pgConfig);
|
|
11067
|
+
return {
|
|
11068
|
+
type: bootstrapper.type,
|
|
11069
|
+
async initializeDriver(config) {
|
|
11070
|
+
return bootstrapper.initializeDriver(config);
|
|
11071
|
+
},
|
|
11072
|
+
async initializeRealtime(driverResult) {
|
|
11073
|
+
if (bootstrapper.initializeRealtime) {
|
|
11074
|
+
return bootstrapper.initializeRealtime({}, driverResult);
|
|
11075
|
+
}
|
|
11076
|
+
return void 0;
|
|
11077
|
+
},
|
|
11078
|
+
async initializeAuth(config, driverResult) {
|
|
11079
|
+
if (bootstrapper.initializeAuth) {
|
|
11080
|
+
return bootstrapper.initializeAuth(config, driverResult);
|
|
11081
|
+
}
|
|
11082
|
+
return void 0;
|
|
11083
|
+
},
|
|
11084
|
+
async initializeHistory(config, driverResult) {
|
|
11085
|
+
if (bootstrapper.initializeHistory) {
|
|
11086
|
+
return bootstrapper.initializeHistory(config, driverResult);
|
|
11087
|
+
}
|
|
11088
|
+
return void 0;
|
|
11089
|
+
},
|
|
11090
|
+
initializeWebsockets(server, realtimeService, driver, config) {
|
|
11091
|
+
if (bootstrapper.initializeWebsockets) {
|
|
11092
|
+
return bootstrapper.initializeWebsockets(server, realtimeService, driver, config);
|
|
11093
|
+
}
|
|
11094
|
+
},
|
|
11095
|
+
getAdmin(driverResult) {
|
|
11096
|
+
if (bootstrapper.getAdmin) {
|
|
11097
|
+
return bootstrapper.getAdmin(driverResult);
|
|
11098
|
+
}
|
|
11099
|
+
return void 0;
|
|
11100
|
+
},
|
|
11101
|
+
mountRoutes(app, basePath, driverResult) {
|
|
11102
|
+
if (bootstrapper.mountRoutes) {
|
|
11103
|
+
bootstrapper.mountRoutes(app, basePath, driverResult);
|
|
11104
|
+
}
|
|
10560
11105
|
}
|
|
10561
11106
|
};
|
|
10562
11107
|
}
|
|
@@ -10571,6 +11116,8 @@ export {
|
|
|
10571
11116
|
PostgresRealtimeProvider,
|
|
10572
11117
|
RealtimeService,
|
|
10573
11118
|
appConfig,
|
|
11119
|
+
createAuthSchema,
|
|
11120
|
+
createPostgresAdapter,
|
|
10574
11121
|
createPostgresBootstrapper,
|
|
10575
11122
|
createPostgresDatabaseConnection,
|
|
10576
11123
|
createPostgresWebSocket,
|
|
@@ -10587,6 +11134,7 @@ export {
|
|
|
10587
11134
|
userRoles,
|
|
10588
11135
|
userRolesRelations,
|
|
10589
11136
|
users,
|
|
10590
|
-
usersRelations
|
|
11137
|
+
usersRelations,
|
|
11138
|
+
usersSchema
|
|
10591
11139
|
};
|
|
10592
11140
|
//# sourceMappingURL=index.es.js.map
|