@rebasepro/server-postgresql 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +1202 -369
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1200 -367
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +7 -6
- package/src/PostgresBackendDriver.ts +13 -7
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/cli.ts +5 -0
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/default-collections.ts +1 -0
- package/src/schema/doctor.ts +82 -41
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- package/test/postgresDataDriver.test.ts +130 -1
- package/vite.config.ts +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -59,6 +59,70 @@
|
|
|
59
59
|
connectionString
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
63
|
+
const opts = {
|
|
64
|
+
...DEFAULT_POOL,
|
|
65
|
+
max: 5,
|
|
66
|
+
...poolConfig
|
|
67
|
+
};
|
|
68
|
+
const pgPoolConfig = {
|
|
69
|
+
connectionString,
|
|
70
|
+
max: opts.max,
|
|
71
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
72
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
73
|
+
query_timeout: opts.queryTimeout,
|
|
74
|
+
statement_timeout: opts.statementTimeout,
|
|
75
|
+
keepAlive: opts.keepAlive,
|
|
76
|
+
keepAliveInitialDelayMillis: 0
|
|
77
|
+
};
|
|
78
|
+
const pool = new pg.Pool(pgPoolConfig);
|
|
79
|
+
pool.on("error", (err) => {
|
|
80
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
81
|
+
});
|
|
82
|
+
const db = schema ? nodePostgres.drizzle(pool, {
|
|
83
|
+
schema
|
|
84
|
+
}) : nodePostgres.drizzle(pool);
|
|
85
|
+
return {
|
|
86
|
+
db,
|
|
87
|
+
pool,
|
|
88
|
+
connectionString
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
92
|
+
const opts = {
|
|
93
|
+
...DEFAULT_POOL,
|
|
94
|
+
max: 10,
|
|
95
|
+
...poolConfig
|
|
96
|
+
};
|
|
97
|
+
const pgPoolConfig = {
|
|
98
|
+
connectionString,
|
|
99
|
+
max: opts.max,
|
|
100
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
101
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
102
|
+
query_timeout: opts.queryTimeout,
|
|
103
|
+
statement_timeout: opts.statementTimeout,
|
|
104
|
+
keepAlive: opts.keepAlive,
|
|
105
|
+
keepAliveInitialDelayMillis: 0
|
|
106
|
+
};
|
|
107
|
+
const pool = new pg.Pool(pgPoolConfig);
|
|
108
|
+
pool.on("error", (err) => {
|
|
109
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
110
|
+
});
|
|
111
|
+
const db = schema ? nodePostgres.drizzle(pool, {
|
|
112
|
+
schema
|
|
113
|
+
}) : nodePostgres.drizzle(pool);
|
|
114
|
+
return {
|
|
115
|
+
db,
|
|
116
|
+
pool,
|
|
117
|
+
connectionString
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
121
|
+
__proto__: null,
|
|
122
|
+
createDirectDatabaseConnection,
|
|
123
|
+
createPostgresDatabaseConnection,
|
|
124
|
+
createReadReplicaConnection
|
|
125
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
62
126
|
class Vector {
|
|
63
127
|
value;
|
|
64
128
|
constructor(value) {
|
|
@@ -978,6 +1042,9 @@
|
|
|
978
1042
|
return output;
|
|
979
1043
|
}
|
|
980
1044
|
for (const key in source) {
|
|
1045
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
981
1048
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
982
1049
|
const sourceValue = source[key];
|
|
983
1050
|
const outputValue = output[key];
|
|
@@ -1166,118 +1233,6 @@
|
|
|
1166
1233
|
});
|
|
1167
1234
|
}
|
|
1168
1235
|
}
|
|
1169
|
-
function getSubcollections(collection) {
|
|
1170
|
-
if (collection.childCollections) {
|
|
1171
|
-
return collection.childCollections() ?? [];
|
|
1172
|
-
}
|
|
1173
|
-
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
1174
|
-
return collection.subcollections() ?? [];
|
|
1175
|
-
}
|
|
1176
|
-
if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
|
|
1177
|
-
const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
|
|
1178
|
-
return manyRelations.map((r) => {
|
|
1179
|
-
const target = r.target();
|
|
1180
|
-
if (!target) return void 0;
|
|
1181
|
-
const relationKey = r.relationName || target.slug;
|
|
1182
|
-
let customName;
|
|
1183
|
-
if (collection.properties) {
|
|
1184
|
-
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1185
|
-
if (prop && prop[1].name) {
|
|
1186
|
-
customName = prop[1].name;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
const baseOverrides = {
|
|
1190
|
-
slug: relationKey
|
|
1191
|
-
};
|
|
1192
|
-
if (customName) {
|
|
1193
|
-
baseOverrides.name = customName;
|
|
1194
|
-
baseOverrides.singularName = customName;
|
|
1195
|
-
}
|
|
1196
|
-
const targetWithOverrides = {
|
|
1197
|
-
...target,
|
|
1198
|
-
...baseOverrides
|
|
1199
|
-
};
|
|
1200
|
-
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1201
|
-
}).filter((c) => Boolean(c));
|
|
1202
|
-
}
|
|
1203
|
-
return [];
|
|
1204
|
-
}
|
|
1205
|
-
function hasPropertyCallbacks(properties, callbackName) {
|
|
1206
|
-
if (!properties) return false;
|
|
1207
|
-
for (const property of Object.values(properties)) {
|
|
1208
|
-
if (property.callbacks?.[callbackName]) return true;
|
|
1209
|
-
if (property.type === "map" && property.properties) {
|
|
1210
|
-
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
1211
|
-
} else if (property.type === "array" && property.of) {
|
|
1212
|
-
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
1213
|
-
for (const of of ofs) {
|
|
1214
|
-
if (of.callbacks?.[callbackName]) return true;
|
|
1215
|
-
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
return false;
|
|
1220
|
-
}
|
|
1221
|
-
async function processProperties(properties, values, previousValues, propsContext, callbackName) {
|
|
1222
|
-
if (!values || typeof values !== "object") return values;
|
|
1223
|
-
const result = {
|
|
1224
|
-
...values
|
|
1225
|
-
};
|
|
1226
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
1227
|
-
if (result[key] === void 0) continue;
|
|
1228
|
-
let currentValue = result[key];
|
|
1229
|
-
const previousValue = previousValues?.[key];
|
|
1230
|
-
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
1231
|
-
if (property.of && !Array.isArray(property.of)) {
|
|
1232
|
-
currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
1233
|
-
const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
|
|
1234
|
-
const singlePropData = {
|
|
1235
|
-
"_tmp": property.of
|
|
1236
|
-
};
|
|
1237
|
-
const res = await processProperties(singlePropData, {
|
|
1238
|
-
"_tmp": item
|
|
1239
|
-
}, {
|
|
1240
|
-
"_tmp": prevItem
|
|
1241
|
-
}, propsContext, callbackName);
|
|
1242
|
-
return res["_tmp"];
|
|
1243
|
-
}));
|
|
1244
|
-
}
|
|
1245
|
-
} else if (property.type === "map" && property.properties && typeof currentValue === "object") {
|
|
1246
|
-
currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
|
|
1247
|
-
}
|
|
1248
|
-
if (property.callbacks?.[callbackName]) {
|
|
1249
|
-
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
1250
|
-
...propsContext,
|
|
1251
|
-
value: currentValue,
|
|
1252
|
-
previousValue
|
|
1253
|
-
}));
|
|
1254
|
-
if (cbRes !== void 0) {
|
|
1255
|
-
currentValue = cbRes;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
result[key] = currentValue;
|
|
1259
|
-
}
|
|
1260
|
-
return result;
|
|
1261
|
-
}
|
|
1262
|
-
const buildPropertyCallbacks = (properties) => {
|
|
1263
|
-
if (!properties) return void 0;
|
|
1264
|
-
const propertyCallbacks = {};
|
|
1265
|
-
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
1266
|
-
propertyCallbacks.afterRead = async (props) => {
|
|
1267
|
-
const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
|
|
1268
|
-
return {
|
|
1269
|
-
...props.entity,
|
|
1270
|
-
values: processedValues
|
|
1271
|
-
};
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
if (hasPropertyCallbacks(properties, "beforeSave")) {
|
|
1275
|
-
propertyCallbacks.beforeSave = async (props) => {
|
|
1276
|
-
return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1280
|
-
};
|
|
1281
1236
|
function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
1282
1237
|
if (!relation.target) {
|
|
1283
1238
|
throw new Error("Relation is missing a `target` collection.");
|
|
@@ -1309,6 +1264,8 @@
|
|
|
1309
1264
|
} else {
|
|
1310
1265
|
targetCollection = evaluated;
|
|
1311
1266
|
}
|
|
1267
|
+
} else if (rawTarget && typeof rawTarget === "object") {
|
|
1268
|
+
targetCollection = rawTarget;
|
|
1312
1269
|
}
|
|
1313
1270
|
if (!targetCollection) {
|
|
1314
1271
|
throw new Error("Relation is missing a valid `target` collection.");
|
|
@@ -1428,11 +1385,14 @@
|
|
|
1428
1385
|
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
1429
1386
|
if (relCollection.relations) {
|
|
1430
1387
|
relCollection.relations.forEach((relation) => {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1388
|
+
try {
|
|
1389
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1390
|
+
const relationKey = normalizedRelation.relationName;
|
|
1391
|
+
if (relationKey) {
|
|
1392
|
+
relations[relationKey] = normalizedRelation;
|
|
1393
|
+
registeredRelationNames.add(relationKey);
|
|
1394
|
+
}
|
|
1395
|
+
} catch (e) {
|
|
1436
1396
|
}
|
|
1437
1397
|
});
|
|
1438
1398
|
}
|
|
@@ -1480,12 +1440,8 @@
|
|
|
1480
1440
|
overrides: relProp.overrides
|
|
1481
1441
|
};
|
|
1482
1442
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1486
|
-
return void 0;
|
|
1487
|
-
}
|
|
1488
|
-
return relation;
|
|
1443
|
+
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1444
|
+
return void 0;
|
|
1489
1445
|
}
|
|
1490
1446
|
function getTableName(collection) {
|
|
1491
1447
|
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
@@ -1512,6 +1468,119 @@
|
|
|
1512
1468
|
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
1513
1469
|
return void 0;
|
|
1514
1470
|
}
|
|
1471
|
+
function getSubcollections(collection) {
|
|
1472
|
+
if (collection.childCollections) {
|
|
1473
|
+
return collection.childCollections() ?? [];
|
|
1474
|
+
}
|
|
1475
|
+
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
1476
|
+
return collection.subcollections() ?? [];
|
|
1477
|
+
}
|
|
1478
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
1479
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
1480
|
+
const manyRelations = Object.values(resolvedRelations).filter((r) => r.cardinality === "many");
|
|
1481
|
+
return manyRelations.map((r) => {
|
|
1482
|
+
const target = r.target();
|
|
1483
|
+
if (!target) return void 0;
|
|
1484
|
+
const relationKey = r.relationName || target.slug;
|
|
1485
|
+
let customName;
|
|
1486
|
+
if (collection.properties) {
|
|
1487
|
+
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1488
|
+
if (prop && prop[1].name) {
|
|
1489
|
+
customName = prop[1].name;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const baseOverrides = {
|
|
1493
|
+
slug: relationKey
|
|
1494
|
+
};
|
|
1495
|
+
if (customName) {
|
|
1496
|
+
baseOverrides.name = customName;
|
|
1497
|
+
baseOverrides.singularName = customName;
|
|
1498
|
+
}
|
|
1499
|
+
const targetWithOverrides = {
|
|
1500
|
+
...target,
|
|
1501
|
+
...baseOverrides
|
|
1502
|
+
};
|
|
1503
|
+
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1504
|
+
}).filter((c) => Boolean(c));
|
|
1505
|
+
}
|
|
1506
|
+
return [];
|
|
1507
|
+
}
|
|
1508
|
+
function hasPropertyCallbacks(properties, callbackName) {
|
|
1509
|
+
if (!properties) return false;
|
|
1510
|
+
for (const property of Object.values(properties)) {
|
|
1511
|
+
if (property.callbacks?.[callbackName]) return true;
|
|
1512
|
+
if (property.type === "map" && property.properties) {
|
|
1513
|
+
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
1514
|
+
} else if (property.type === "array" && property.of) {
|
|
1515
|
+
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
1516
|
+
for (const of of ofs) {
|
|
1517
|
+
if (of.callbacks?.[callbackName]) return true;
|
|
1518
|
+
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
async function processProperties(properties, values, previousValues, propsContext, callbackName) {
|
|
1525
|
+
if (!values || typeof values !== "object") return values;
|
|
1526
|
+
const result = {
|
|
1527
|
+
...values
|
|
1528
|
+
};
|
|
1529
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
1530
|
+
if (result[key] === void 0) continue;
|
|
1531
|
+
let currentValue = result[key];
|
|
1532
|
+
const previousValue = previousValues?.[key];
|
|
1533
|
+
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
1534
|
+
if (property.of && !Array.isArray(property.of)) {
|
|
1535
|
+
currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
1536
|
+
const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
|
|
1537
|
+
const singlePropData = {
|
|
1538
|
+
"_tmp": property.of
|
|
1539
|
+
};
|
|
1540
|
+
const res = await processProperties(singlePropData, {
|
|
1541
|
+
"_tmp": item
|
|
1542
|
+
}, {
|
|
1543
|
+
"_tmp": prevItem
|
|
1544
|
+
}, propsContext, callbackName);
|
|
1545
|
+
return res["_tmp"];
|
|
1546
|
+
}));
|
|
1547
|
+
}
|
|
1548
|
+
} else if (property.type === "map" && property.properties && typeof currentValue === "object") {
|
|
1549
|
+
currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
|
|
1550
|
+
}
|
|
1551
|
+
if (property.callbacks?.[callbackName]) {
|
|
1552
|
+
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
1553
|
+
...propsContext,
|
|
1554
|
+
value: currentValue,
|
|
1555
|
+
previousValue
|
|
1556
|
+
}));
|
|
1557
|
+
if (cbRes !== void 0) {
|
|
1558
|
+
currentValue = cbRes;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
result[key] = currentValue;
|
|
1562
|
+
}
|
|
1563
|
+
return result;
|
|
1564
|
+
}
|
|
1565
|
+
const buildPropertyCallbacks = (properties) => {
|
|
1566
|
+
if (!properties) return void 0;
|
|
1567
|
+
const propertyCallbacks = {};
|
|
1568
|
+
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
1569
|
+
propertyCallbacks.afterRead = async (props) => {
|
|
1570
|
+
const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
|
|
1571
|
+
return {
|
|
1572
|
+
...props.entity,
|
|
1573
|
+
values: processedValues
|
|
1574
|
+
};
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
if (hasPropertyCallbacks(properties, "beforeSave")) {
|
|
1578
|
+
propertyCallbacks.beforeSave = async (props) => {
|
|
1579
|
+
return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1583
|
+
};
|
|
1515
1584
|
var logic = { exports: {} };
|
|
1516
1585
|
(function(module2, exports3) {
|
|
1517
1586
|
(function(root, factory) {
|
|
@@ -2419,8 +2488,18 @@
|
|
|
2419
2488
|
const mergedRelationsRaw = [...extractedRelations];
|
|
2420
2489
|
for (const manual of manualRelations) {
|
|
2421
2490
|
const name = manual.relationName;
|
|
2422
|
-
if (!name
|
|
2491
|
+
if (!name) {
|
|
2423
2492
|
mergedRelationsRaw.push(manual);
|
|
2493
|
+
} else {
|
|
2494
|
+
const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
|
|
2495
|
+
if (existingIndex === -1) {
|
|
2496
|
+
mergedRelationsRaw.push(manual);
|
|
2497
|
+
} else {
|
|
2498
|
+
mergedRelationsRaw[existingIndex] = {
|
|
2499
|
+
...manual,
|
|
2500
|
+
...mergedRelationsRaw[existingIndex]
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2424
2503
|
}
|
|
2425
2504
|
}
|
|
2426
2505
|
let mergedRelations = mergedRelationsRaw;
|
|
@@ -2633,11 +2712,207 @@
|
|
|
2633
2712
|
collections.push(currentCollection);
|
|
2634
2713
|
}
|
|
2635
2714
|
}
|
|
2636
|
-
return {
|
|
2637
|
-
collections,
|
|
2638
|
-
entityIds,
|
|
2639
|
-
finalCollection: currentCollection
|
|
2640
|
-
};
|
|
2715
|
+
return {
|
|
2716
|
+
collections,
|
|
2717
|
+
entityIds,
|
|
2718
|
+
finalCollection: currentCollection
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const defaultUsersCollection = {
|
|
2723
|
+
name: "Users",
|
|
2724
|
+
singularName: "User",
|
|
2725
|
+
slug: "users",
|
|
2726
|
+
table: "users",
|
|
2727
|
+
schema: "rebase",
|
|
2728
|
+
icon: "Users",
|
|
2729
|
+
group: "Settings",
|
|
2730
|
+
properties: {
|
|
2731
|
+
id: {
|
|
2732
|
+
name: "ID",
|
|
2733
|
+
type: "string",
|
|
2734
|
+
isId: "uuid"
|
|
2735
|
+
},
|
|
2736
|
+
email: {
|
|
2737
|
+
name: "Email",
|
|
2738
|
+
type: "string",
|
|
2739
|
+
validation: {
|
|
2740
|
+
required: true,
|
|
2741
|
+
unique: true
|
|
2742
|
+
}
|
|
2743
|
+
},
|
|
2744
|
+
password_hash: {
|
|
2745
|
+
name: "Password Hash",
|
|
2746
|
+
type: "string",
|
|
2747
|
+
ui: {
|
|
2748
|
+
hideFromCollection: true
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2751
|
+
display_name: {
|
|
2752
|
+
name: "Display Name",
|
|
2753
|
+
type: "string"
|
|
2754
|
+
},
|
|
2755
|
+
photo_url: {
|
|
2756
|
+
name: "Photo URL",
|
|
2757
|
+
type: "string"
|
|
2758
|
+
},
|
|
2759
|
+
email_verified: {
|
|
2760
|
+
name: "Email Verified",
|
|
2761
|
+
type: "boolean",
|
|
2762
|
+
defaultValue: false
|
|
2763
|
+
},
|
|
2764
|
+
email_verification_token: {
|
|
2765
|
+
name: "Email Verification Token",
|
|
2766
|
+
type: "string",
|
|
2767
|
+
ui: {
|
|
2768
|
+
hideFromCollection: true
|
|
2769
|
+
}
|
|
2770
|
+
},
|
|
2771
|
+
email_verification_sent_at: {
|
|
2772
|
+
name: "Email Verification Sent At",
|
|
2773
|
+
type: "date",
|
|
2774
|
+
ui: {
|
|
2775
|
+
hideFromCollection: true
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
metadata: {
|
|
2779
|
+
name: "Metadata",
|
|
2780
|
+
type: "map",
|
|
2781
|
+
defaultValue: {},
|
|
2782
|
+
ui: {
|
|
2783
|
+
hideFromCollection: true
|
|
2784
|
+
}
|
|
2785
|
+
},
|
|
2786
|
+
created_at: {
|
|
2787
|
+
name: "Created At",
|
|
2788
|
+
type: "date",
|
|
2789
|
+
autoValue: "on_create",
|
|
2790
|
+
ui: {
|
|
2791
|
+
readOnly: true,
|
|
2792
|
+
hideFromCollection: true
|
|
2793
|
+
}
|
|
2794
|
+
},
|
|
2795
|
+
updated_at: {
|
|
2796
|
+
name: "Updated At",
|
|
2797
|
+
type: "date",
|
|
2798
|
+
autoValue: "on_update",
|
|
2799
|
+
ui: {
|
|
2800
|
+
readOnly: true,
|
|
2801
|
+
hideFromCollection: true
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
function mapOperator(op) {
|
|
2807
|
+
switch (op) {
|
|
2808
|
+
case "==":
|
|
2809
|
+
return "eq";
|
|
2810
|
+
case "!=":
|
|
2811
|
+
return "neq";
|
|
2812
|
+
case ">":
|
|
2813
|
+
return "gt";
|
|
2814
|
+
case ">=":
|
|
2815
|
+
return "gte";
|
|
2816
|
+
case "<":
|
|
2817
|
+
return "lt";
|
|
2818
|
+
case "<=":
|
|
2819
|
+
return "lte";
|
|
2820
|
+
case "array-contains":
|
|
2821
|
+
return "cs";
|
|
2822
|
+
case "array-contains-any":
|
|
2823
|
+
return "csa";
|
|
2824
|
+
case "not-in":
|
|
2825
|
+
return "nin";
|
|
2826
|
+
default:
|
|
2827
|
+
return op;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
class QueryBuilder {
|
|
2831
|
+
constructor(collection) {
|
|
2832
|
+
this.collection = collection;
|
|
2833
|
+
}
|
|
2834
|
+
params = {
|
|
2835
|
+
where: {}
|
|
2836
|
+
};
|
|
2837
|
+
/**
|
|
2838
|
+
* Add a filter condition to your query.
|
|
2839
|
+
* @example
|
|
2840
|
+
* client.collection('users').where('age', '>=', 18).find()
|
|
2841
|
+
*/
|
|
2842
|
+
where(column, operator, value) {
|
|
2843
|
+
if (!this.params.where) {
|
|
2844
|
+
this.params.where = {};
|
|
2845
|
+
}
|
|
2846
|
+
const mappedOp = mapOperator(operator);
|
|
2847
|
+
let formattedValue = value;
|
|
2848
|
+
if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
|
|
2849
|
+
formattedValue = `(${value.join(",")})`;
|
|
2850
|
+
} else if (value === null) {
|
|
2851
|
+
formattedValue = "null";
|
|
2852
|
+
}
|
|
2853
|
+
this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
|
|
2854
|
+
return this;
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Order the results by a specific column.
|
|
2858
|
+
* @example
|
|
2859
|
+
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
2860
|
+
*/
|
|
2861
|
+
orderBy(column, ascending = "asc") {
|
|
2862
|
+
this.params.orderBy = `${column}:${ascending}`;
|
|
2863
|
+
return this;
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Limit the number of results returned.
|
|
2867
|
+
*/
|
|
2868
|
+
limit(count) {
|
|
2869
|
+
this.params.limit = count;
|
|
2870
|
+
return this;
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Skip the first N results.
|
|
2874
|
+
*/
|
|
2875
|
+
offset(count) {
|
|
2876
|
+
this.params.offset = count;
|
|
2877
|
+
return this;
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Set a free-text search string if supported by the backend.
|
|
2881
|
+
*/
|
|
2882
|
+
search(searchString) {
|
|
2883
|
+
this.params.searchString = searchString;
|
|
2884
|
+
return this;
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Include related entities in the response.
|
|
2888
|
+
* Relations will be populated with full entity data instead of just IDs.
|
|
2889
|
+
*
|
|
2890
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
2891
|
+
* @example
|
|
2892
|
+
* // Include specific relations
|
|
2893
|
+
* client.data.posts.include("tags", "author").find()
|
|
2894
|
+
*
|
|
2895
|
+
* // Include all relations
|
|
2896
|
+
* client.data.posts.include("*").find()
|
|
2897
|
+
*/
|
|
2898
|
+
include(...relations) {
|
|
2899
|
+
this.params.include = relations;
|
|
2900
|
+
return this;
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Execute the find query and return the results.
|
|
2904
|
+
*/
|
|
2905
|
+
async find() {
|
|
2906
|
+
return this.collection.find(this.params);
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Listen to realtime updates matching this query.
|
|
2910
|
+
*/
|
|
2911
|
+
listen(onUpdate, onError) {
|
|
2912
|
+
if (!this.collection.listen) {
|
|
2913
|
+
throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
2914
|
+
}
|
|
2915
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
2641
2916
|
}
|
|
2642
2917
|
}
|
|
2643
2918
|
function convertWhereToFilter(where) {
|
|
@@ -2719,7 +2994,7 @@
|
|
|
2719
2994
|
return [field, direction];
|
|
2720
2995
|
}
|
|
2721
2996
|
function createDriverAccessor(driver, slug) {
|
|
2722
|
-
|
|
2997
|
+
const accessor = {
|
|
2723
2998
|
async find(params) {
|
|
2724
2999
|
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2725
3000
|
const entities = await driver.fetchCollection({
|
|
@@ -2813,8 +3088,28 @@
|
|
|
2813
3088
|
onUpdate: (entity) => onUpdate(entity ?? void 0),
|
|
2814
3089
|
onError
|
|
2815
3090
|
});
|
|
2816
|
-
} : void 0
|
|
3091
|
+
} : void 0,
|
|
3092
|
+
// Fluent Query Builder
|
|
3093
|
+
where(column, operator, value) {
|
|
3094
|
+
return new QueryBuilder(accessor).where(column, operator, value);
|
|
3095
|
+
},
|
|
3096
|
+
orderBy(column, ascending) {
|
|
3097
|
+
return new QueryBuilder(accessor).orderBy(column, ascending);
|
|
3098
|
+
},
|
|
3099
|
+
limit(count) {
|
|
3100
|
+
return new QueryBuilder(accessor).limit(count);
|
|
3101
|
+
},
|
|
3102
|
+
offset(count) {
|
|
3103
|
+
return new QueryBuilder(accessor).offset(count);
|
|
3104
|
+
},
|
|
3105
|
+
search(searchString) {
|
|
3106
|
+
return new QueryBuilder(accessor).search(searchString);
|
|
3107
|
+
},
|
|
3108
|
+
include(...relations) {
|
|
3109
|
+
return new QueryBuilder(accessor).include(...relations);
|
|
3110
|
+
}
|
|
2817
3111
|
};
|
|
3112
|
+
return accessor;
|
|
2818
3113
|
}
|
|
2819
3114
|
function buildRebaseData(driver) {
|
|
2820
3115
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2848,7 +3143,13 @@
|
|
|
2848
3143
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
2849
3144
|
if (!filterParam) continue;
|
|
2850
3145
|
const [op, value] = filterParam;
|
|
2851
|
-
|
|
3146
|
+
let fieldColumn = table[field];
|
|
3147
|
+
if (!fieldColumn) {
|
|
3148
|
+
const relationKey = `${field}_id`;
|
|
3149
|
+
if (relationKey in table) {
|
|
3150
|
+
fieldColumn = table[relationKey];
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
2852
3153
|
if (!fieldColumn) {
|
|
2853
3154
|
console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
2854
3155
|
continue;
|
|
@@ -2890,6 +3191,17 @@
|
|
|
2890
3191
|
return null;
|
|
2891
3192
|
case "array-contains":
|
|
2892
3193
|
return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
|
|
3194
|
+
case "array-contains-any":
|
|
3195
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3196
|
+
const textValues = value.map((v) => String(v));
|
|
3197
|
+
return drizzleOrm.sql`${column} ?| array[${drizzleOrm.sql.join(textValues.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)}]`;
|
|
3198
|
+
}
|
|
3199
|
+
return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
|
|
3200
|
+
case "not-in":
|
|
3201
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3202
|
+
return drizzleOrm.sql`${column} NOT IN (${drizzleOrm.sql.join(value.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)})`;
|
|
3203
|
+
}
|
|
3204
|
+
return null;
|
|
2893
3205
|
default:
|
|
2894
3206
|
console.warn(`Unsupported filter operation: ${op}`);
|
|
2895
3207
|
return null;
|
|
@@ -3405,6 +3717,40 @@
|
|
|
3405
3717
|
return null;
|
|
3406
3718
|
}
|
|
3407
3719
|
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Build vector similarity search expressions for pgvector.
|
|
3722
|
+
*
|
|
3723
|
+
* Returns:
|
|
3724
|
+
* - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
|
|
3725
|
+
* - `filter`: optional WHERE clause for distance threshold
|
|
3726
|
+
* - `distanceSelect`: SQL expression for selecting the distance as `_distance`
|
|
3727
|
+
*/
|
|
3728
|
+
static buildVectorSearchConditions(table, vectorSearch) {
|
|
3729
|
+
const column = table[vectorSearch.property];
|
|
3730
|
+
if (!column) {
|
|
3731
|
+
throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
3732
|
+
}
|
|
3733
|
+
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
3734
|
+
const distanceFn = vectorSearch.distance || "cosine";
|
|
3735
|
+
let operator;
|
|
3736
|
+
switch (distanceFn) {
|
|
3737
|
+
case "cosine":
|
|
3738
|
+
operator = "<=>";
|
|
3739
|
+
break;
|
|
3740
|
+
case "l2":
|
|
3741
|
+
operator = "<->";
|
|
3742
|
+
break;
|
|
3743
|
+
case "inner_product":
|
|
3744
|
+
operator = "<#>";
|
|
3745
|
+
break;
|
|
3746
|
+
}
|
|
3747
|
+
const distanceExpr = drizzleOrm.sql`${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}`;
|
|
3748
|
+
return {
|
|
3749
|
+
orderBy: distanceExpr,
|
|
3750
|
+
filter: vectorSearch.threshold != null ? drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
|
|
3751
|
+
distanceSelect: drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)})`
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3408
3754
|
}
|
|
3409
3755
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
3410
3756
|
function getColumnMeta(col) {
|
|
@@ -5350,7 +5696,7 @@
|
|
|
5350
5696
|
const qb = this.getQueryBuilder(tableName);
|
|
5351
5697
|
const withConfig = this.buildWithConfig(collection);
|
|
5352
5698
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
5353
|
-
if (qb && !options.searchString && !hasRelations) {
|
|
5699
|
+
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
|
|
5354
5700
|
try {
|
|
5355
5701
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5356
5702
|
const results2 = await qb.findMany(queryOpts);
|
|
@@ -5364,7 +5710,14 @@
|
|
|
5364
5710
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
5365
5711
|
}
|
|
5366
5712
|
}
|
|
5367
|
-
let
|
|
5713
|
+
let vectorMeta;
|
|
5714
|
+
if (options.vectorSearch) {
|
|
5715
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
5716
|
+
}
|
|
5717
|
+
let query = vectorMeta ? this.db.select({
|
|
5718
|
+
table_row: table,
|
|
5719
|
+
_distance: vectorMeta.distanceSelect
|
|
5720
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5368
5721
|
const allConditions = [];
|
|
5369
5722
|
if (options.searchString) {
|
|
5370
5723
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5375,12 +5728,17 @@
|
|
|
5375
5728
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5376
5729
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5377
5730
|
}
|
|
5731
|
+
if (vectorMeta?.filter) {
|
|
5732
|
+
allConditions.push(vectorMeta.filter);
|
|
5733
|
+
}
|
|
5378
5734
|
if (allConditions.length > 0) {
|
|
5379
5735
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5380
5736
|
if (finalCondition) query = query.where(finalCondition);
|
|
5381
5737
|
}
|
|
5382
5738
|
const orderExpressions = [];
|
|
5383
|
-
if (
|
|
5739
|
+
if (vectorMeta) {
|
|
5740
|
+
orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
|
|
5741
|
+
} else if (options.orderBy) {
|
|
5384
5742
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5385
5743
|
if (orderByField) {
|
|
5386
5744
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
@@ -5396,10 +5754,14 @@
|
|
|
5396
5754
|
if (finalCondition) query = query.where(finalCondition);
|
|
5397
5755
|
}
|
|
5398
5756
|
}
|
|
5399
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5757
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5400
5758
|
if (limitValue) query = query.limit(limitValue);
|
|
5401
5759
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5402
|
-
const
|
|
5760
|
+
const rawResults = await query;
|
|
5761
|
+
const results = vectorMeta ? rawResults.map((r) => ({
|
|
5762
|
+
...r.table_row,
|
|
5763
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
5764
|
+
})) : rawResults;
|
|
5403
5765
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5404
5766
|
}
|
|
5405
5767
|
/**
|
|
@@ -5621,7 +5983,7 @@
|
|
|
5621
5983
|
const idField = table[idInfo.fieldName];
|
|
5622
5984
|
const tableName = drizzleOrm.getTableName(table);
|
|
5623
5985
|
const qb = this.getQueryBuilder(tableName);
|
|
5624
|
-
if (qb && !options.searchString) {
|
|
5986
|
+
if (qb && !options.searchString && !options.vectorSearch) {
|
|
5625
5987
|
try {
|
|
5626
5988
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5627
5989
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
@@ -5767,7 +6129,14 @@
|
|
|
5767
6129
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
5768
6130
|
const idInfo = idInfoArray[0];
|
|
5769
6131
|
const idField = table[idInfo.fieldName];
|
|
5770
|
-
let
|
|
6132
|
+
let vectorMeta;
|
|
6133
|
+
if (options.vectorSearch) {
|
|
6134
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
6135
|
+
}
|
|
6136
|
+
let query = vectorMeta ? this.db.select({
|
|
6137
|
+
table_row: table,
|
|
6138
|
+
_distance: vectorMeta.distanceSelect
|
|
6139
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5771
6140
|
const allConditions = [];
|
|
5772
6141
|
if (options.searchString) {
|
|
5773
6142
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5778,12 +6147,17 @@
|
|
|
5778
6147
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5779
6148
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5780
6149
|
}
|
|
6150
|
+
if (vectorMeta?.filter) {
|
|
6151
|
+
allConditions.push(vectorMeta.filter);
|
|
6152
|
+
}
|
|
5781
6153
|
if (allConditions.length > 0) {
|
|
5782
6154
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5783
6155
|
if (finalCondition) query = query.where(finalCondition);
|
|
5784
6156
|
}
|
|
5785
6157
|
const orderExpressions = [];
|
|
5786
|
-
if (
|
|
6158
|
+
if (vectorMeta) {
|
|
6159
|
+
orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
|
|
6160
|
+
} else if (options.orderBy) {
|
|
5787
6161
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5788
6162
|
if (orderByField) {
|
|
5789
6163
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
@@ -5791,10 +6165,17 @@
|
|
|
5791
6165
|
}
|
|
5792
6166
|
orderExpressions.push(drizzleOrm.desc(idField));
|
|
5793
6167
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5794
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
6168
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5795
6169
|
if (limitValue) query = query.limit(limitValue);
|
|
5796
6170
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5797
|
-
|
|
6171
|
+
const rawResults = await query;
|
|
6172
|
+
if (vectorMeta) {
|
|
6173
|
+
return rawResults.map((r) => ({
|
|
6174
|
+
...r.table_row,
|
|
6175
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
6176
|
+
}));
|
|
6177
|
+
}
|
|
6178
|
+
return rawResults;
|
|
5798
6179
|
}
|
|
5799
6180
|
/**
|
|
5800
6181
|
* Check if the Drizzle instance has the relational query API available
|
|
@@ -6522,7 +6903,7 @@
|
|
|
6522
6903
|
callbacks: void 0,
|
|
6523
6904
|
propertyCallbacks: void 0
|
|
6524
6905
|
};
|
|
6525
|
-
const registryCollection = this.registry
|
|
6906
|
+
const registryCollection = this.registry?.getCollectionByPath(path2);
|
|
6526
6907
|
const resolvedCollection = registryCollection ? {
|
|
6527
6908
|
...collection,
|
|
6528
6909
|
...registryCollection
|
|
@@ -6548,7 +6929,8 @@
|
|
|
6548
6929
|
startAfter,
|
|
6549
6930
|
orderBy,
|
|
6550
6931
|
searchString,
|
|
6551
|
-
order
|
|
6932
|
+
order,
|
|
6933
|
+
vectorSearch
|
|
6552
6934
|
}) {
|
|
6553
6935
|
const entities = await this.entityService.fetchCollection(path2, {
|
|
6554
6936
|
filter,
|
|
@@ -6558,7 +6940,8 @@
|
|
|
6558
6940
|
offset,
|
|
6559
6941
|
startAfter,
|
|
6560
6942
|
databaseId: collection?.databaseId,
|
|
6561
|
-
searchString
|
|
6943
|
+
searchString,
|
|
6944
|
+
vectorSearch
|
|
6562
6945
|
});
|
|
6563
6946
|
const {
|
|
6564
6947
|
collection: resolvedCollection,
|
|
@@ -6570,7 +6953,8 @@
|
|
|
6570
6953
|
user: this.user,
|
|
6571
6954
|
driver: this,
|
|
6572
6955
|
data: this.data,
|
|
6573
|
-
client: this.client
|
|
6956
|
+
client: this.client,
|
|
6957
|
+
storageSource: this.client?.storage
|
|
6574
6958
|
};
|
|
6575
6959
|
return Promise.all(entities.map(async (entity) => {
|
|
6576
6960
|
let fetched = entity;
|
|
@@ -6665,7 +7049,8 @@
|
|
|
6665
7049
|
user: this.user,
|
|
6666
7050
|
driver: this,
|
|
6667
7051
|
data: this.data,
|
|
6668
|
-
client: this.client
|
|
7052
|
+
client: this.client,
|
|
7053
|
+
storageSource: this.client?.storage
|
|
6669
7054
|
};
|
|
6670
7055
|
if (callbacks?.afterRead) {
|
|
6671
7056
|
entity = await callbacks.afterRead({
|
|
@@ -6735,7 +7120,8 @@
|
|
|
6735
7120
|
user: this.user,
|
|
6736
7121
|
driver: this,
|
|
6737
7122
|
data: this.data,
|
|
6738
|
-
client: this.client
|
|
7123
|
+
client: this.client,
|
|
7124
|
+
storageSource: this.client?.storage
|
|
6739
7125
|
};
|
|
6740
7126
|
let previousValuesForHistory;
|
|
6741
7127
|
if (status === "existing" && entityId) {
|
|
@@ -6884,7 +7270,8 @@
|
|
|
6884
7270
|
user: this.user,
|
|
6885
7271
|
driver: this,
|
|
6886
7272
|
data: this.data,
|
|
6887
|
-
client: this.client
|
|
7273
|
+
client: this.client,
|
|
7274
|
+
storageSource: this.client?.storage
|
|
6888
7275
|
};
|
|
6889
7276
|
if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
6890
7277
|
let preventDefault = false;
|
|
@@ -7397,6 +7784,7 @@
|
|
|
7397
7784
|
length: 255
|
|
7398
7785
|
}),
|
|
7399
7786
|
emailVerificationSentAt: pgCore.timestamp("email_verification_sent_at"),
|
|
7787
|
+
isAnonymous: pgCore.boolean("is_anonymous").default(false).notNull(),
|
|
7400
7788
|
metadata: pgCore.jsonb("metadata").$type().default({}).notNull(),
|
|
7401
7789
|
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7402
7790
|
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
@@ -7411,8 +7799,7 @@
|
|
|
7411
7799
|
}).notNull(),
|
|
7412
7800
|
isAdmin: pgCore.boolean("is_admin").default(false).notNull(),
|
|
7413
7801
|
defaultPermissions: pgCore.jsonb("default_permissions").$type(),
|
|
7414
|
-
collectionPermissions: pgCore.jsonb("collection_permissions").$type()
|
|
7415
|
-
config: pgCore.jsonb("config").$type()
|
|
7802
|
+
collectionPermissions: pgCore.jsonb("collection_permissions").$type()
|
|
7416
7803
|
});
|
|
7417
7804
|
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7418
7805
|
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
@@ -7484,6 +7871,48 @@
|
|
|
7484
7871
|
}, (table) => ({
|
|
7485
7872
|
uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7486
7873
|
}));
|
|
7874
|
+
const mfaFactors2 = rolesTableCreator("mfa_factors", {
|
|
7875
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7876
|
+
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
7877
|
+
onDelete: "cascade"
|
|
7878
|
+
}),
|
|
7879
|
+
factorType: pgCore.varchar("factor_type", {
|
|
7880
|
+
length: 20
|
|
7881
|
+
}).notNull(),
|
|
7882
|
+
// 'totp'
|
|
7883
|
+
secretEncrypted: pgCore.varchar("secret_encrypted", {
|
|
7884
|
+
length: 500
|
|
7885
|
+
}).notNull(),
|
|
7886
|
+
friendlyName: pgCore.varchar("friendly_name", {
|
|
7887
|
+
length: 255
|
|
7888
|
+
}),
|
|
7889
|
+
verified: pgCore.boolean("verified").default(false).notNull(),
|
|
7890
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7891
|
+
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
7892
|
+
});
|
|
7893
|
+
const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
|
|
7894
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7895
|
+
factorId: pgCore.uuid("factor_id").notNull().references(() => mfaFactors2.id, {
|
|
7896
|
+
onDelete: "cascade"
|
|
7897
|
+
}),
|
|
7898
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
7899
|
+
verifiedAt: pgCore.timestamp("verified_at"),
|
|
7900
|
+
ipAddress: pgCore.varchar("ip_address", {
|
|
7901
|
+
length: 45
|
|
7902
|
+
}),
|
|
7903
|
+
expiresAt: pgCore.timestamp("expires_at").notNull()
|
|
7904
|
+
});
|
|
7905
|
+
const recoveryCodes2 = rolesTableCreator("recovery_codes", {
|
|
7906
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
7907
|
+
userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
|
|
7908
|
+
onDelete: "cascade"
|
|
7909
|
+
}),
|
|
7910
|
+
codeHash: pgCore.varchar("code_hash", {
|
|
7911
|
+
length: 255
|
|
7912
|
+
}).notNull(),
|
|
7913
|
+
usedAt: pgCore.timestamp("used_at"),
|
|
7914
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
|
|
7915
|
+
});
|
|
7487
7916
|
return {
|
|
7488
7917
|
rolesSchema,
|
|
7489
7918
|
usersSchema: usersSchema2,
|
|
@@ -7493,7 +7922,10 @@
|
|
|
7493
7922
|
refreshTokens: refreshTokens2,
|
|
7494
7923
|
passwordResetTokens: passwordResetTokens2,
|
|
7495
7924
|
appConfig: appConfig2,
|
|
7496
|
-
userIdentities: userIdentities2
|
|
7925
|
+
userIdentities: userIdentities2,
|
|
7926
|
+
mfaFactors: mfaFactors2,
|
|
7927
|
+
mfaChallenges: mfaChallenges2,
|
|
7928
|
+
recoveryCodes: recoveryCodes2
|
|
7497
7929
|
};
|
|
7498
7930
|
}
|
|
7499
7931
|
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
@@ -7506,13 +7938,18 @@
|
|
|
7506
7938
|
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7507
7939
|
const appConfig = defaultAuthSchema.appConfig;
|
|
7508
7940
|
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7941
|
+
const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
7942
|
+
const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
7943
|
+
const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
7509
7944
|
const usersRelations = drizzleOrm.relations(users, ({
|
|
7510
7945
|
many
|
|
7511
7946
|
}) => ({
|
|
7512
7947
|
userRoles: many(userRoles),
|
|
7513
7948
|
refreshTokens: many(refreshTokens),
|
|
7514
7949
|
passwordResetTokens: many(passwordResetTokens),
|
|
7515
|
-
userIdentities: many(userIdentities)
|
|
7950
|
+
userIdentities: many(userIdentities),
|
|
7951
|
+
mfaFactors: many(mfaFactors),
|
|
7952
|
+
recoveryCodes: many(recoveryCodes)
|
|
7516
7953
|
}));
|
|
7517
7954
|
const rolesRelations = drizzleOrm.relations(roles, ({
|
|
7518
7955
|
many
|
|
@@ -7555,6 +7992,32 @@
|
|
|
7555
7992
|
references: [users.id]
|
|
7556
7993
|
})
|
|
7557
7994
|
}));
|
|
7995
|
+
const mfaFactorsRelations = drizzleOrm.relations(mfaFactors, ({
|
|
7996
|
+
one,
|
|
7997
|
+
many
|
|
7998
|
+
}) => ({
|
|
7999
|
+
user: one(users, {
|
|
8000
|
+
fields: [mfaFactors.userId],
|
|
8001
|
+
references: [users.id]
|
|
8002
|
+
}),
|
|
8003
|
+
challenges: many(mfaChallenges)
|
|
8004
|
+
}));
|
|
8005
|
+
const mfaChallengesRelations = drizzleOrm.relations(mfaChallenges, ({
|
|
8006
|
+
one
|
|
8007
|
+
}) => ({
|
|
8008
|
+
factor: one(mfaFactors, {
|
|
8009
|
+
fields: [mfaChallenges.factorId],
|
|
8010
|
+
references: [mfaFactors.id]
|
|
8011
|
+
})
|
|
8012
|
+
}));
|
|
8013
|
+
const recoveryCodesRelations = drizzleOrm.relations(recoveryCodes, ({
|
|
8014
|
+
one
|
|
8015
|
+
}) => ({
|
|
8016
|
+
user: one(users, {
|
|
8017
|
+
fields: [recoveryCodes.userId],
|
|
8018
|
+
references: [users.id]
|
|
8019
|
+
})
|
|
8020
|
+
}));
|
|
7558
8021
|
const resolveColumnName = (propName, prop) => {
|
|
7559
8022
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
7560
8023
|
return prop.columnName;
|
|
@@ -8079,167 +8542,84 @@
|
|
|
8079
8542
|
fields: [${tableVarName}.${rel.localKey}],
|
|
8080
8543
|
references: [${targetTableVar}.${getPrimaryKeyName(target)}],
|
|
8081
8544
|
relationName: "${drizzleRelationName}"
|
|
8082
|
-
})`);
|
|
8083
|
-
} else if (rel.direction === "inverse") {
|
|
8084
|
-
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8085
|
-
relationName: "${drizzleRelationName}"
|
|
8086
|
-
})`);
|
|
8087
|
-
}
|
|
8088
|
-
} else if (rel.cardinality === "many") {
|
|
8089
|
-
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8090
|
-
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8091
|
-
} else if (rel.through) {
|
|
8092
|
-
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8093
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8094
|
-
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8095
|
-
try {
|
|
8096
|
-
const targetCollection = rel.target();
|
|
8097
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8098
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8099
|
-
if (correspondingRelation && correspondingRelation.through) {
|
|
8100
|
-
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8101
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8102
|
-
} else {
|
|
8103
|
-
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8104
|
-
}
|
|
8105
|
-
} catch (e) {
|
|
8106
|
-
console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
|
|
8107
|
-
}
|
|
8108
|
-
}
|
|
8109
|
-
}
|
|
8110
|
-
} catch (e) {
|
|
8111
|
-
console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
|
|
8112
|
-
}
|
|
8113
|
-
}
|
|
8114
|
-
for (const otherCollection of collections) {
|
|
8115
|
-
if (otherCollection.slug === collection.slug) continue;
|
|
8116
|
-
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
8117
|
-
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
8118
|
-
if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
|
|
8119
|
-
try {
|
|
8120
|
-
const otherTarget = otherRel.target();
|
|
8121
|
-
if (otherTarget.slug === collection.slug) {
|
|
8122
|
-
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
8123
|
-
const deduplicationKey = `${drizzleRelationName}::owning`;
|
|
8124
|
-
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
8125
|
-
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
8126
|
-
const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
|
|
8127
|
-
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
|
|
8128
|
-
fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
|
|
8129
|
-
references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
|
|
8130
|
-
relationName: "${drizzleRelationName}"
|
|
8131
|
-
})`);
|
|
8132
|
-
emittedRelationNames.add(deduplicationKey);
|
|
8133
|
-
}
|
|
8134
|
-
}
|
|
8135
|
-
} catch (e) {
|
|
8136
|
-
}
|
|
8137
|
-
}
|
|
8138
|
-
}
|
|
8139
|
-
}
|
|
8140
|
-
}
|
|
8141
|
-
if (tableRelations.length > 0) {
|
|
8142
|
-
const relVarName = `${tableVarName}Relations`;
|
|
8143
|
-
schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
|
|
8144
|
-
${tableRelations.join(",\n")}
|
|
8145
|
-
}));
|
|
8146
|
-
|
|
8147
|
-
`;
|
|
8148
|
-
if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
|
|
8149
|
-
}
|
|
8150
|
-
}
|
|
8151
|
-
const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
|
|
8152
|
-
`;
|
|
8153
|
-
const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
|
|
8154
|
-
`;
|
|
8155
|
-
const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
|
|
8156
|
-
|
|
8157
|
-
`;
|
|
8158
|
-
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8159
|
-
return schemaContent;
|
|
8160
|
-
};
|
|
8161
|
-
const defaultUsersCollection = {
|
|
8162
|
-
name: "Users",
|
|
8163
|
-
singularName: "User",
|
|
8164
|
-
slug: "users",
|
|
8165
|
-
table: "users",
|
|
8166
|
-
icon: "Users",
|
|
8167
|
-
group: "Settings",
|
|
8168
|
-
properties: {
|
|
8169
|
-
id: {
|
|
8170
|
-
name: "ID",
|
|
8171
|
-
type: "string",
|
|
8172
|
-
isId: "uuid"
|
|
8173
|
-
},
|
|
8174
|
-
email: {
|
|
8175
|
-
name: "Email",
|
|
8176
|
-
type: "string",
|
|
8177
|
-
validation: {
|
|
8178
|
-
required: true,
|
|
8179
|
-
unique: true
|
|
8180
|
-
}
|
|
8181
|
-
},
|
|
8182
|
-
password_hash: {
|
|
8183
|
-
name: "Password Hash",
|
|
8184
|
-
type: "string",
|
|
8185
|
-
ui: {
|
|
8186
|
-
hideFromCollection: true
|
|
8187
|
-
}
|
|
8188
|
-
},
|
|
8189
|
-
display_name: {
|
|
8190
|
-
name: "Display Name",
|
|
8191
|
-
type: "string"
|
|
8192
|
-
},
|
|
8193
|
-
photo_url: {
|
|
8194
|
-
name: "Photo URL",
|
|
8195
|
-
type: "string"
|
|
8196
|
-
},
|
|
8197
|
-
email_verified: {
|
|
8198
|
-
name: "Email Verified",
|
|
8199
|
-
type: "boolean",
|
|
8200
|
-
defaultValue: false
|
|
8201
|
-
},
|
|
8202
|
-
email_verification_token: {
|
|
8203
|
-
name: "Email Verification Token",
|
|
8204
|
-
type: "string",
|
|
8205
|
-
ui: {
|
|
8206
|
-
hideFromCollection: true
|
|
8207
|
-
}
|
|
8208
|
-
},
|
|
8209
|
-
email_verification_sent_at: {
|
|
8210
|
-
name: "Email Verification Sent At",
|
|
8211
|
-
type: "date",
|
|
8212
|
-
ui: {
|
|
8213
|
-
hideFromCollection: true
|
|
8214
|
-
}
|
|
8215
|
-
},
|
|
8216
|
-
metadata: {
|
|
8217
|
-
name: "Metadata",
|
|
8218
|
-
type: "map",
|
|
8219
|
-
defaultValue: {},
|
|
8220
|
-
ui: {
|
|
8221
|
-
hideFromCollection: true
|
|
8222
|
-
}
|
|
8223
|
-
},
|
|
8224
|
-
created_at: {
|
|
8225
|
-
name: "Created At",
|
|
8226
|
-
type: "date",
|
|
8227
|
-
autoValue: "on_create",
|
|
8228
|
-
ui: {
|
|
8229
|
-
readOnly: true,
|
|
8230
|
-
hideFromCollection: true
|
|
8545
|
+
})`);
|
|
8546
|
+
} else if (rel.direction === "inverse") {
|
|
8547
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8548
|
+
relationName: "${drizzleRelationName}"
|
|
8549
|
+
})`);
|
|
8550
|
+
}
|
|
8551
|
+
} else if (rel.cardinality === "many") {
|
|
8552
|
+
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8553
|
+
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8554
|
+
} else if (rel.through) {
|
|
8555
|
+
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8556
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8557
|
+
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8558
|
+
try {
|
|
8559
|
+
const targetCollection = rel.target();
|
|
8560
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8561
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8562
|
+
if (correspondingRelation && correspondingRelation.through) {
|
|
8563
|
+
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8564
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8565
|
+
} else {
|
|
8566
|
+
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8567
|
+
}
|
|
8568
|
+
} catch (e) {
|
|
8569
|
+
console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
}
|
|
8573
|
+
} catch (e) {
|
|
8574
|
+
console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
|
|
8575
|
+
}
|
|
8231
8576
|
}
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8577
|
+
for (const otherCollection of collections) {
|
|
8578
|
+
if (otherCollection.slug === collection.slug) continue;
|
|
8579
|
+
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
8580
|
+
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
8581
|
+
if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
|
|
8582
|
+
try {
|
|
8583
|
+
const otherTarget = otherRel.target();
|
|
8584
|
+
if (otherTarget.slug === collection.slug) {
|
|
8585
|
+
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
8586
|
+
const deduplicationKey = `${drizzleRelationName}::owning`;
|
|
8587
|
+
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
8588
|
+
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
8589
|
+
const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
|
|
8590
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
|
|
8591
|
+
fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
|
|
8592
|
+
references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
|
|
8593
|
+
relationName: "${drizzleRelationName}"
|
|
8594
|
+
})`);
|
|
8595
|
+
emittedRelationNames.add(deduplicationKey);
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
} catch (e) {
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8240
8602
|
}
|
|
8241
8603
|
}
|
|
8604
|
+
if (tableRelations.length > 0) {
|
|
8605
|
+
const relVarName = `${tableVarName}Relations`;
|
|
8606
|
+
schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
|
|
8607
|
+
${tableRelations.join(",\n")}
|
|
8608
|
+
}));
|
|
8609
|
+
|
|
8610
|
+
`;
|
|
8611
|
+
if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
|
|
8612
|
+
}
|
|
8242
8613
|
}
|
|
8614
|
+
const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
|
|
8615
|
+
`;
|
|
8616
|
+
const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
|
|
8617
|
+
`;
|
|
8618
|
+
const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
|
|
8619
|
+
|
|
8620
|
+
`;
|
|
8621
|
+
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8622
|
+
return schemaContent;
|
|
8243
8623
|
};
|
|
8244
8624
|
const formatTerminalText = (text, options = {}) => {
|
|
8245
8625
|
let codes = "";
|
|
@@ -8306,10 +8686,7 @@ ${tableRelations.join(",\n")}
|
|
|
8306
8686
|
if (!collections || !Array.isArray(collections)) {
|
|
8307
8687
|
collections = [];
|
|
8308
8688
|
}
|
|
8309
|
-
|
|
8310
|
-
if (!hasUsersCollection) {
|
|
8311
|
-
collections.push(defaultUsersCollection);
|
|
8312
|
-
}
|
|
8689
|
+
collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
|
|
8313
8690
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
8314
8691
|
const schemaContent = await generateSchema(collections);
|
|
8315
8692
|
if (outputPath) {
|
|
@@ -8370,6 +8747,13 @@ ${tableRelations.join(",\n")}
|
|
|
8370
8747
|
this.entityService = new EntityService(db, registry);
|
|
8371
8748
|
}
|
|
8372
8749
|
clients = /* @__PURE__ */ new Map();
|
|
8750
|
+
// Broadcast channels: channel name → set of client IDs
|
|
8751
|
+
channels = /* @__PURE__ */ new Map();
|
|
8752
|
+
// Presence: channel → Map<clientId, { state, lastSeen }>
|
|
8753
|
+
presence = /* @__PURE__ */ new Map();
|
|
8754
|
+
presenceInterval;
|
|
8755
|
+
static PRESENCE_TIMEOUT_MS = 3e4;
|
|
8756
|
+
// 30s
|
|
8373
8757
|
entityService;
|
|
8374
8758
|
// Enhanced subscriptions storage with full request parameters
|
|
8375
8759
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -8496,8 +8880,19 @@ ${tableRelations.join(",\n")}
|
|
|
8496
8880
|
}
|
|
8497
8881
|
}
|
|
8498
8882
|
}
|
|
8883
|
+
for (const [channel, members] of this.channels.entries()) {
|
|
8884
|
+
if (members.has(clientId)) {
|
|
8885
|
+
members.delete(clientId);
|
|
8886
|
+
this.removePresence(clientId, channel);
|
|
8887
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
for (const [channel] of this.presence) {
|
|
8891
|
+
this.removePresence(clientId, channel);
|
|
8892
|
+
}
|
|
8499
8893
|
}
|
|
8500
8894
|
async handleMessage(clientId, message, authContext) {
|
|
8895
|
+
const payload = message.payload;
|
|
8501
8896
|
switch (message.type) {
|
|
8502
8897
|
case "subscribe_collection":
|
|
8503
8898
|
await this.handleCollectionSubscription(clientId, message.payload, authContext);
|
|
@@ -8508,6 +8903,25 @@ ${tableRelations.join(",\n")}
|
|
|
8508
8903
|
case "unsubscribe":
|
|
8509
8904
|
await this.handleUnsubscribe(clientId, message.subscriptionId);
|
|
8510
8905
|
break;
|
|
8906
|
+
case "join_channel":
|
|
8907
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8908
|
+
break;
|
|
8909
|
+
case "leave_channel":
|
|
8910
|
+
this.leaveChannel(clientId, payload?.channel);
|
|
8911
|
+
break;
|
|
8912
|
+
case "broadcast":
|
|
8913
|
+
this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
|
|
8914
|
+
break;
|
|
8915
|
+
case "presence_track":
|
|
8916
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8917
|
+
this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
|
|
8918
|
+
break;
|
|
8919
|
+
case "presence_untrack":
|
|
8920
|
+
this.removePresence(clientId, payload?.channel);
|
|
8921
|
+
break;
|
|
8922
|
+
case "presence_state":
|
|
8923
|
+
this.sendPresenceState(clientId, payload?.channel);
|
|
8924
|
+
break;
|
|
8511
8925
|
default:
|
|
8512
8926
|
this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
|
|
8513
8927
|
}
|
|
@@ -8965,6 +9379,132 @@ ${tableRelations.join(",\n")}
|
|
|
8965
9379
|
return parentPaths;
|
|
8966
9380
|
}
|
|
8967
9381
|
// =============================================================================
|
|
9382
|
+
// Broadcast Channels
|
|
9383
|
+
// =============================================================================
|
|
9384
|
+
/** Join a broadcast channel */
|
|
9385
|
+
joinChannel(clientId, channel) {
|
|
9386
|
+
if (!this.channels.has(channel)) {
|
|
9387
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
9388
|
+
}
|
|
9389
|
+
this.channels.get(channel).add(clientId);
|
|
9390
|
+
this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
|
|
9391
|
+
}
|
|
9392
|
+
/** Leave a broadcast channel */
|
|
9393
|
+
leaveChannel(clientId, channel) {
|
|
9394
|
+
const members = this.channels.get(channel);
|
|
9395
|
+
if (members) {
|
|
9396
|
+
members.delete(clientId);
|
|
9397
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
9398
|
+
}
|
|
9399
|
+
this.removePresence(clientId, channel);
|
|
9400
|
+
}
|
|
9401
|
+
/** Broadcast a message to all clients in a channel except sender */
|
|
9402
|
+
broadcastToChannel(clientId, channel, event, payload) {
|
|
9403
|
+
const members = this.channels.get(channel);
|
|
9404
|
+
if (!members) return;
|
|
9405
|
+
const message = JSON.stringify({
|
|
9406
|
+
type: "broadcast",
|
|
9407
|
+
channel,
|
|
9408
|
+
event,
|
|
9409
|
+
payload
|
|
9410
|
+
});
|
|
9411
|
+
for (const memberId of members) {
|
|
9412
|
+
if (memberId === clientId) continue;
|
|
9413
|
+
const ws$1 = this.clients.get(memberId);
|
|
9414
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9415
|
+
ws$1.send(message);
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
// =============================================================================
|
|
9420
|
+
// Presence
|
|
9421
|
+
// =============================================================================
|
|
9422
|
+
/** Track presence in a channel */
|
|
9423
|
+
trackPresence(clientId, channel, state) {
|
|
9424
|
+
if (!this.presence.has(channel)) {
|
|
9425
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
9426
|
+
}
|
|
9427
|
+
const channelPresence = this.presence.get(channel);
|
|
9428
|
+
channelPresence.set(clientId, {
|
|
9429
|
+
state,
|
|
9430
|
+
lastSeen: Date.now()
|
|
9431
|
+
});
|
|
9432
|
+
this.broadcastPresenceDiff(channel, {
|
|
9433
|
+
[clientId]: state
|
|
9434
|
+
}, {});
|
|
9435
|
+
this.ensurePresenceCleanup();
|
|
9436
|
+
}
|
|
9437
|
+
/** Remove presence from a channel */
|
|
9438
|
+
removePresence(clientId, channel) {
|
|
9439
|
+
const channelPresence = this.presence.get(channel);
|
|
9440
|
+
if (!channelPresence) return;
|
|
9441
|
+
const entry = channelPresence.get(clientId);
|
|
9442
|
+
if (entry) {
|
|
9443
|
+
channelPresence.delete(clientId);
|
|
9444
|
+
this.broadcastPresenceDiff(channel, {}, {
|
|
9445
|
+
[clientId]: entry.state
|
|
9446
|
+
});
|
|
9447
|
+
}
|
|
9448
|
+
if (channelPresence.size === 0) {
|
|
9449
|
+
this.presence.delete(channel);
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
/** Send full presence state to a specific client */
|
|
9453
|
+
sendPresenceState(clientId, channel) {
|
|
9454
|
+
const channelPresence = this.presence.get(channel);
|
|
9455
|
+
const presences = {};
|
|
9456
|
+
if (channelPresence) {
|
|
9457
|
+
for (const [id, {
|
|
9458
|
+
state
|
|
9459
|
+
}] of channelPresence) {
|
|
9460
|
+
presences[id] = state;
|
|
9461
|
+
}
|
|
9462
|
+
}
|
|
9463
|
+
const ws$1 = this.clients.get(clientId);
|
|
9464
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9465
|
+
ws$1.send(JSON.stringify({
|
|
9466
|
+
type: "presence_state",
|
|
9467
|
+
channel,
|
|
9468
|
+
presences
|
|
9469
|
+
}));
|
|
9470
|
+
}
|
|
9471
|
+
}
|
|
9472
|
+
/** Broadcast presence diff (joins/leaves) to channel */
|
|
9473
|
+
broadcastPresenceDiff(channel, joins, leaves) {
|
|
9474
|
+
const members = this.channels.get(channel);
|
|
9475
|
+
if (!members) return;
|
|
9476
|
+
const message = JSON.stringify({
|
|
9477
|
+
type: "presence_diff",
|
|
9478
|
+
channel,
|
|
9479
|
+
joins,
|
|
9480
|
+
leaves
|
|
9481
|
+
});
|
|
9482
|
+
for (const memberId of members) {
|
|
9483
|
+
const ws$1 = this.clients.get(memberId);
|
|
9484
|
+
if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
|
|
9485
|
+
ws$1.send(message);
|
|
9486
|
+
}
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
/** Periodic cleanup for stale presences */
|
|
9490
|
+
ensurePresenceCleanup() {
|
|
9491
|
+
if (this.presenceInterval) return;
|
|
9492
|
+
this.presenceInterval = setInterval(() => {
|
|
9493
|
+
const now = Date.now();
|
|
9494
|
+
for (const [channel, channelPresence] of this.presence) {
|
|
9495
|
+
for (const [clientId, entry] of channelPresence) {
|
|
9496
|
+
if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
|
|
9497
|
+
this.removePresence(clientId, channel);
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
if (this.presence.size === 0 && this.presenceInterval) {
|
|
9502
|
+
clearInterval(this.presenceInterval);
|
|
9503
|
+
this.presenceInterval = void 0;
|
|
9504
|
+
}
|
|
9505
|
+
}, 1e4);
|
|
9506
|
+
}
|
|
9507
|
+
// =============================================================================
|
|
8968
9508
|
// Lifecycle / Cleanup
|
|
8969
9509
|
// =============================================================================
|
|
8970
9510
|
/**
|
|
@@ -8985,6 +9525,12 @@ ${tableRelations.join(",\n")}
|
|
|
8985
9525
|
}
|
|
8986
9526
|
this._subscriptions.clear();
|
|
8987
9527
|
this.subscriptionCallbacks.clear();
|
|
9528
|
+
this.channels.clear();
|
|
9529
|
+
this.presence.clear();
|
|
9530
|
+
if (this.presenceInterval) {
|
|
9531
|
+
clearInterval(this.presenceInterval);
|
|
9532
|
+
this.presenceInterval = void 0;
|
|
9533
|
+
}
|
|
8988
9534
|
await this.stopListening();
|
|
8989
9535
|
this.clients.clear();
|
|
8990
9536
|
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
@@ -9631,8 +10177,14 @@ ${tableRelations.join(",\n")}
|
|
|
9631
10177
|
break;
|
|
9632
10178
|
case "subscribe_collection":
|
|
9633
10179
|
case "subscribe_entity":
|
|
9634
|
-
case "unsubscribe":
|
|
9635
|
-
|
|
10180
|
+
case "unsubscribe":
|
|
10181
|
+
case "join_channel":
|
|
10182
|
+
case "leave_channel":
|
|
10183
|
+
case "broadcast":
|
|
10184
|
+
case "presence_track":
|
|
10185
|
+
case "presence_untrack":
|
|
10186
|
+
case "presence_state": {
|
|
10187
|
+
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
9636
10188
|
const session = clientSessions.get(clientId);
|
|
9637
10189
|
const authContext = session?.user ? {
|
|
9638
10190
|
userId: session.user.userId,
|
|
@@ -9751,11 +10303,6 @@ ${tableRelations.join(",\n")}
|
|
|
9751
10303
|
create: true,
|
|
9752
10304
|
edit: true,
|
|
9753
10305
|
delete: true
|
|
9754
|
-
},
|
|
9755
|
-
config: {
|
|
9756
|
-
createCollections: true,
|
|
9757
|
-
editCollections: "all",
|
|
9758
|
-
deleteCollections: "all"
|
|
9759
10306
|
}
|
|
9760
10307
|
}, {
|
|
9761
10308
|
id: "editor",
|
|
@@ -9766,11 +10313,6 @@ ${tableRelations.join(",\n")}
|
|
|
9766
10313
|
create: true,
|
|
9767
10314
|
edit: true,
|
|
9768
10315
|
delete: true
|
|
9769
|
-
},
|
|
9770
|
-
config: {
|
|
9771
|
-
createCollections: true,
|
|
9772
|
-
editCollections: "own",
|
|
9773
|
-
deleteCollections: "own"
|
|
9774
10316
|
}
|
|
9775
10317
|
}, {
|
|
9776
10318
|
id: "viewer",
|
|
@@ -9781,11 +10323,10 @@ ${tableRelations.join(",\n")}
|
|
|
9781
10323
|
create: false,
|
|
9782
10324
|
edit: false,
|
|
9783
10325
|
delete: false
|
|
9784
|
-
}
|
|
9785
|
-
config: null
|
|
10326
|
+
}
|
|
9786
10327
|
}];
|
|
9787
10328
|
async function ensureAuthTablesExist(db, registry) {
|
|
9788
|
-
|
|
10329
|
+
serverCore.logger.info("🔍 Checking auth tables...");
|
|
9789
10330
|
try {
|
|
9790
10331
|
let usersTableName = '"users"';
|
|
9791
10332
|
let userIdType = "TEXT";
|
|
@@ -9855,7 +10396,6 @@ ${tableRelations.join(",\n")}
|
|
|
9855
10396
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
9856
10397
|
default_permissions JSONB,
|
|
9857
10398
|
collection_permissions JSONB,
|
|
9858
|
-
config JSONB,
|
|
9859
10399
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9860
10400
|
)
|
|
9861
10401
|
`);
|
|
@@ -9938,34 +10478,85 @@ ${tableRelations.join(",\n")}
|
|
|
9938
10478
|
`);
|
|
9939
10479
|
});
|
|
9940
10480
|
await seedDefaultRoles(db, rolesTableName);
|
|
9941
|
-
|
|
10481
|
+
await db.execute(drizzleOrm.sql`
|
|
10482
|
+
ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
|
|
10483
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
10484
|
+
`);
|
|
10485
|
+
const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
|
|
10486
|
+
const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
|
|
10487
|
+
const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
|
|
10488
|
+
await db.execute(drizzleOrm.sql`
|
|
10489
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaFactorsTableName)} (
|
|
10490
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10491
|
+
user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10492
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
10493
|
+
secret_encrypted TEXT NOT NULL,
|
|
10494
|
+
friendly_name TEXT,
|
|
10495
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
10496
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10497
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10498
|
+
)
|
|
10499
|
+
`);
|
|
10500
|
+
await db.execute(drizzleOrm.sql`
|
|
10501
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
10502
|
+
ON ${drizzleOrm.sql.raw(mfaFactorsTableName)}(user_id)
|
|
10503
|
+
`);
|
|
10504
|
+
await db.execute(drizzleOrm.sql`
|
|
10505
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaChallengesTableName)} (
|
|
10506
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10507
|
+
factor_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
10508
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10509
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
10510
|
+
ip_address TEXT,
|
|
10511
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
10512
|
+
)
|
|
10513
|
+
`);
|
|
10514
|
+
await db.execute(drizzleOrm.sql`
|
|
10515
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
10516
|
+
ON ${drizzleOrm.sql.raw(mfaChallengesTableName)}(factor_id)
|
|
10517
|
+
`);
|
|
10518
|
+
await db.execute(drizzleOrm.sql`
|
|
10519
|
+
CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(recoveryCodesTableName)} (
|
|
10520
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10521
|
+
user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10522
|
+
code_hash TEXT NOT NULL,
|
|
10523
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
10524
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10525
|
+
)
|
|
10526
|
+
`);
|
|
10527
|
+
await db.execute(drizzleOrm.sql`
|
|
10528
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
10529
|
+
ON ${drizzleOrm.sql.raw(recoveryCodesTableName)}(user_id)
|
|
10530
|
+
`);
|
|
10531
|
+
serverCore.logger.info("✅ Auth tables ready");
|
|
9942
10532
|
} catch (error) {
|
|
9943
|
-
|
|
9944
|
-
|
|
10533
|
+
serverCore.logger.error("❌ Failed to create auth tables", {
|
|
10534
|
+
error
|
|
10535
|
+
});
|
|
10536
|
+
serverCore.logger.warn("⚠️ Continuing without creating auth tables.");
|
|
9945
10537
|
}
|
|
9946
10538
|
}
|
|
9947
10539
|
async function seedDefaultRoles(db, rolesTableName) {
|
|
9948
10540
|
const result = await db.execute(drizzleOrm.sql`SELECT COUNT(*) as count FROM ${drizzleOrm.sql.raw(rolesTableName)}`);
|
|
9949
10541
|
const count = parseInt(result.rows[0]?.count || "0", 10);
|
|
9950
10542
|
if (count > 0) {
|
|
9951
|
-
|
|
10543
|
+
serverCore.logger.info(`📋 Found ${count} existing roles`);
|
|
9952
10544
|
return;
|
|
9953
10545
|
}
|
|
9954
|
-
|
|
10546
|
+
serverCore.logger.info("🌱 Seeding default roles...");
|
|
9955
10547
|
for (const role of DEFAULT_ROLES) {
|
|
9956
10548
|
await db.execute(drizzleOrm.sql`
|
|
9957
|
-
INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions
|
|
10549
|
+
INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
9958
10550
|
VALUES (
|
|
9959
10551
|
${role.id},
|
|
9960
10552
|
${role.name},
|
|
9961
10553
|
${role.is_admin},
|
|
9962
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
9963
|
-
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
10554
|
+
${JSON.stringify(role.default_permissions)}::jsonb
|
|
9964
10555
|
)
|
|
9965
10556
|
ON CONFLICT (id) DO NOTHING
|
|
9966
10557
|
`);
|
|
9967
10558
|
}
|
|
9968
|
-
|
|
10559
|
+
serverCore.logger.info("✅ Default roles created: admin, editor, viewer");
|
|
9969
10560
|
}
|
|
9970
10561
|
function getColumnKey(table, ...keys2) {
|
|
9971
10562
|
if (!table) return void 0;
|
|
@@ -10019,12 +10610,13 @@ ${tableRelations.join(",\n")}
|
|
|
10019
10610
|
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10020
10611
|
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10021
10612
|
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10613
|
+
const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
|
|
10022
10614
|
const createdAt = row.created_at ?? row.createdAt;
|
|
10023
10615
|
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10024
10616
|
const metadata = {
|
|
10025
10617
|
...row.metadata || {}
|
|
10026
10618
|
};
|
|
10027
|
-
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10619
|
+
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10028
10620
|
for (const [key, val] of Object.entries(row)) {
|
|
10029
10621
|
if (!knownKeys.has(key)) {
|
|
10030
10622
|
const camelKey = camelCase(key);
|
|
@@ -10040,6 +10632,7 @@ ${tableRelations.join(",\n")}
|
|
|
10040
10632
|
emailVerified,
|
|
10041
10633
|
emailVerificationToken,
|
|
10042
10634
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10635
|
+
isAnonymous,
|
|
10043
10636
|
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10044
10637
|
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10045
10638
|
metadata
|
|
@@ -10056,6 +10649,7 @@ ${tableRelations.join(",\n")}
|
|
|
10056
10649
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10057
10650
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10058
10651
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10652
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
10059
10653
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10060
10654
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10061
10655
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -10067,6 +10661,7 @@ ${tableRelations.join(",\n")}
|
|
|
10067
10661
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10068
10662
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10069
10663
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10664
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
10070
10665
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10071
10666
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10072
10667
|
const metadata = {
|
|
@@ -10075,7 +10670,7 @@ ${tableRelations.join(",\n")}
|
|
|
10075
10670
|
const remainingMetadata = {};
|
|
10076
10671
|
for (const [key, val] of Object.entries(metadata)) {
|
|
10077
10672
|
const tableColKey = getColumnKey(this.usersTable, key);
|
|
10078
|
-
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10673
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10079
10674
|
payload[tableColKey] = val;
|
|
10080
10675
|
} else {
|
|
10081
10676
|
remainingMetadata[key] = val;
|
|
@@ -10263,7 +10858,7 @@ ${tableRelations.join(",\n")}
|
|
|
10263
10858
|
async getUserRoles(userId) {
|
|
10264
10859
|
const rolesSchema = pgCore.getTableConfig(this.rolesTable).schema || "public";
|
|
10265
10860
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10266
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10861
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10267
10862
|
FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10268
10863
|
INNER JOIN ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
10269
10864
|
WHERE ur.user_id = ${userId}
|
|
@@ -10273,8 +10868,7 @@ ${tableRelations.join(",\n")}
|
|
|
10273
10868
|
name: row.name,
|
|
10274
10869
|
isAdmin: row.is_admin,
|
|
10275
10870
|
defaultPermissions: row.default_permissions,
|
|
10276
|
-
collectionPermissions: row.collection_permissions
|
|
10277
|
-
config: row.config
|
|
10871
|
+
collectionPermissions: row.collection_permissions
|
|
10278
10872
|
}));
|
|
10279
10873
|
}
|
|
10280
10874
|
/**
|
|
@@ -10340,7 +10934,7 @@ ${tableRelations.join(",\n")}
|
|
|
10340
10934
|
async getRoleById(id) {
|
|
10341
10935
|
const tableName = this.getQualifiedRolesTableName();
|
|
10342
10936
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10343
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10937
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10344
10938
|
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
10345
10939
|
WHERE id = ${id}
|
|
10346
10940
|
`);
|
|
@@ -10351,14 +10945,13 @@ ${tableRelations.join(",\n")}
|
|
|
10351
10945
|
name: row.name,
|
|
10352
10946
|
isAdmin: row.is_admin,
|
|
10353
10947
|
defaultPermissions: row.default_permissions,
|
|
10354
|
-
collectionPermissions: row.collection_permissions
|
|
10355
|
-
config: row.config
|
|
10948
|
+
collectionPermissions: row.collection_permissions
|
|
10356
10949
|
};
|
|
10357
10950
|
}
|
|
10358
10951
|
async listRoles() {
|
|
10359
10952
|
const tableName = this.getQualifiedRolesTableName();
|
|
10360
10953
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10361
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10954
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10362
10955
|
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
10363
10956
|
ORDER BY name
|
|
10364
10957
|
`);
|
|
@@ -10367,23 +10960,21 @@ ${tableRelations.join(",\n")}
|
|
|
10367
10960
|
name: row.name,
|
|
10368
10961
|
isAdmin: row.is_admin,
|
|
10369
10962
|
defaultPermissions: row.default_permissions,
|
|
10370
|
-
collectionPermissions: row.collection_permissions
|
|
10371
|
-
config: row.config
|
|
10963
|
+
collectionPermissions: row.collection_permissions
|
|
10372
10964
|
}));
|
|
10373
10965
|
}
|
|
10374
10966
|
async createRole(data) {
|
|
10375
10967
|
const tableName = this.getQualifiedRolesTableName();
|
|
10376
10968
|
const result = await this.db.execute(drizzleOrm.sql`
|
|
10377
|
-
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
10969
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
10378
10970
|
VALUES (
|
|
10379
10971
|
${data.id},
|
|
10380
10972
|
${data.name},
|
|
10381
10973
|
${data.isAdmin ?? false},
|
|
10382
10974
|
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
10383
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10384
|
-
${data.config ? JSON.stringify(data.config) : null}::jsonb
|
|
10975
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10385
10976
|
)
|
|
10386
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10977
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10387
10978
|
`);
|
|
10388
10979
|
const row = result.rows[0];
|
|
10389
10980
|
return {
|
|
@@ -10391,8 +10982,7 @@ ${tableRelations.join(",\n")}
|
|
|
10391
10982
|
name: row.name,
|
|
10392
10983
|
isAdmin: row.is_admin,
|
|
10393
10984
|
defaultPermissions: row.default_permissions,
|
|
10394
|
-
collectionPermissions: row.collection_permissions
|
|
10395
|
-
config: row.config
|
|
10985
|
+
collectionPermissions: row.collection_permissions
|
|
10396
10986
|
};
|
|
10397
10987
|
}
|
|
10398
10988
|
async updateRole(id, data) {
|
|
@@ -10405,8 +10995,7 @@ ${tableRelations.join(",\n")}
|
|
|
10405
10995
|
name = ${data.name ?? existing.name},
|
|
10406
10996
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
10407
10997
|
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
10408
|
-
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10409
|
-
config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
|
|
10998
|
+
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10410
10999
|
WHERE id = ${id}
|
|
10411
11000
|
`);
|
|
10412
11001
|
return this.getRoleById(id);
|
|
@@ -10685,8 +11274,7 @@ ${tableRelations.join(",\n")}
|
|
|
10685
11274
|
return this.roleService.createRole({
|
|
10686
11275
|
...data,
|
|
10687
11276
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
10688
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
10689
|
-
config: data.config ?? null
|
|
11277
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
10690
11278
|
});
|
|
10691
11279
|
}
|
|
10692
11280
|
async updateRole(id, data) {
|
|
@@ -10729,6 +11317,219 @@ ${tableRelations.join(",\n")}
|
|
|
10729
11317
|
async deleteExpiredTokens() {
|
|
10730
11318
|
await this.tokenRepository.deleteExpiredTokens();
|
|
10731
11319
|
}
|
|
11320
|
+
// MFA operations (delegate to MfaService)
|
|
11321
|
+
_mfaService = null;
|
|
11322
|
+
getMfaService() {
|
|
11323
|
+
if (!this._mfaService) {
|
|
11324
|
+
this._mfaService = new MfaService(this.db);
|
|
11325
|
+
}
|
|
11326
|
+
return this._mfaService;
|
|
11327
|
+
}
|
|
11328
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11329
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
11330
|
+
}
|
|
11331
|
+
async getMfaFactors(userId) {
|
|
11332
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
11333
|
+
}
|
|
11334
|
+
async getMfaFactorById(factorId) {
|
|
11335
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
11336
|
+
}
|
|
11337
|
+
async verifyMfaFactor(factorId) {
|
|
11338
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
11339
|
+
}
|
|
11340
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11341
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
11342
|
+
}
|
|
11343
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11344
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
11345
|
+
}
|
|
11346
|
+
async getMfaChallengeById(challengeId) {
|
|
11347
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
11348
|
+
}
|
|
11349
|
+
async verifyMfaChallenge(challengeId) {
|
|
11350
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
11351
|
+
}
|
|
11352
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11353
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
11354
|
+
}
|
|
11355
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11356
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
11357
|
+
}
|
|
11358
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11359
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
11360
|
+
}
|
|
11361
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11362
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
11363
|
+
}
|
|
11364
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11365
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
class MfaService {
|
|
11369
|
+
constructor(db, schemaName = "rebase") {
|
|
11370
|
+
this.db = db;
|
|
11371
|
+
this.schemaName = schemaName;
|
|
11372
|
+
}
|
|
11373
|
+
qualify(tableName) {
|
|
11374
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
11375
|
+
}
|
|
11376
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11377
|
+
const tableName = this.qualify("mfa_factors");
|
|
11378
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11379
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
11380
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
11381
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11382
|
+
`);
|
|
11383
|
+
const row = result.rows[0];
|
|
11384
|
+
return {
|
|
11385
|
+
id: row.id,
|
|
11386
|
+
userId: row.user_id,
|
|
11387
|
+
factorType: row.factor_type,
|
|
11388
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11389
|
+
verified: row.verified,
|
|
11390
|
+
createdAt: new Date(row.created_at),
|
|
11391
|
+
updatedAt: new Date(row.updated_at)
|
|
11392
|
+
};
|
|
11393
|
+
}
|
|
11394
|
+
async getMfaFactors(userId) {
|
|
11395
|
+
const tableName = this.qualify("mfa_factors");
|
|
11396
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11397
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11398
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11399
|
+
WHERE user_id = ${userId}
|
|
11400
|
+
ORDER BY created_at
|
|
11401
|
+
`);
|
|
11402
|
+
return result.rows.map((row) => ({
|
|
11403
|
+
id: row.id,
|
|
11404
|
+
userId: row.user_id,
|
|
11405
|
+
factorType: row.factor_type,
|
|
11406
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11407
|
+
verified: row.verified,
|
|
11408
|
+
createdAt: new Date(row.created_at),
|
|
11409
|
+
updatedAt: new Date(row.updated_at)
|
|
11410
|
+
}));
|
|
11411
|
+
}
|
|
11412
|
+
async getMfaFactorById(factorId) {
|
|
11413
|
+
const tableName = this.qualify("mfa_factors");
|
|
11414
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11415
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
11416
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11417
|
+
WHERE id = ${factorId}
|
|
11418
|
+
`);
|
|
11419
|
+
if (result.rows.length === 0) return null;
|
|
11420
|
+
const row = result.rows[0];
|
|
11421
|
+
return {
|
|
11422
|
+
id: row.id,
|
|
11423
|
+
userId: row.user_id,
|
|
11424
|
+
factorType: row.factor_type,
|
|
11425
|
+
secretEncrypted: row.secret_encrypted,
|
|
11426
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11427
|
+
verified: row.verified,
|
|
11428
|
+
createdAt: new Date(row.created_at),
|
|
11429
|
+
updatedAt: new Date(row.updated_at)
|
|
11430
|
+
};
|
|
11431
|
+
}
|
|
11432
|
+
async verifyMfaFactor(factorId) {
|
|
11433
|
+
const tableName = this.qualify("mfa_factors");
|
|
11434
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11435
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11436
|
+
SET verified = TRUE, updated_at = NOW()
|
|
11437
|
+
WHERE id = ${factorId}
|
|
11438
|
+
`);
|
|
11439
|
+
}
|
|
11440
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11441
|
+
const tableName = this.qualify("mfa_factors");
|
|
11442
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11443
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11444
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
11445
|
+
`);
|
|
11446
|
+
}
|
|
11447
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11448
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11449
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
|
|
11450
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11451
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
11452
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
11453
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
11454
|
+
`);
|
|
11455
|
+
const row = result.rows[0];
|
|
11456
|
+
return {
|
|
11457
|
+
id: row.id,
|
|
11458
|
+
factorId: row.factor_id,
|
|
11459
|
+
createdAt: new Date(row.created_at),
|
|
11460
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11461
|
+
ipAddress: row.ip_address ?? void 0
|
|
11462
|
+
};
|
|
11463
|
+
}
|
|
11464
|
+
async getMfaChallengeById(challengeId) {
|
|
11465
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11466
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11467
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
11468
|
+
FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11469
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
11470
|
+
`);
|
|
11471
|
+
if (result.rows.length === 0) return null;
|
|
11472
|
+
const row = result.rows[0];
|
|
11473
|
+
return {
|
|
11474
|
+
id: row.id,
|
|
11475
|
+
factorId: row.factor_id,
|
|
11476
|
+
createdAt: new Date(row.created_at),
|
|
11477
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11478
|
+
ipAddress: row.ip_address ?? void 0
|
|
11479
|
+
};
|
|
11480
|
+
}
|
|
11481
|
+
async verifyMfaChallenge(challengeId) {
|
|
11482
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11483
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11484
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11485
|
+
SET verified_at = NOW()
|
|
11486
|
+
WHERE id = ${challengeId}
|
|
11487
|
+
`);
|
|
11488
|
+
}
|
|
11489
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11490
|
+
const tableName = this.qualify("recovery_codes");
|
|
11491
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11492
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11493
|
+
`);
|
|
11494
|
+
for (const hash of codeHashes) {
|
|
11495
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11496
|
+
INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, code_hash)
|
|
11497
|
+
VALUES (${userId}, ${hash})
|
|
11498
|
+
`);
|
|
11499
|
+
}
|
|
11500
|
+
}
|
|
11501
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11502
|
+
const tableName = this.qualify("recovery_codes");
|
|
11503
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11504
|
+
UPDATE ${drizzleOrm.sql.raw(tableName)}
|
|
11505
|
+
SET used_at = NOW()
|
|
11506
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
11507
|
+
RETURNING id
|
|
11508
|
+
`);
|
|
11509
|
+
return result.rows.length > 0;
|
|
11510
|
+
}
|
|
11511
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11512
|
+
const tableName = this.qualify("recovery_codes");
|
|
11513
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11514
|
+
SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11515
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
11516
|
+
`);
|
|
11517
|
+
return result.rows[0].count;
|
|
11518
|
+
}
|
|
11519
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11520
|
+
const tableName = this.qualify("recovery_codes");
|
|
11521
|
+
await this.db.execute(drizzleOrm.sql`
|
|
11522
|
+
DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11523
|
+
`);
|
|
11524
|
+
}
|
|
11525
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11526
|
+
const tableName = this.qualify("mfa_factors");
|
|
11527
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
11528
|
+
SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
|
|
11529
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
11530
|
+
`);
|
|
11531
|
+
return result.rows[0].count > 0;
|
|
11532
|
+
}
|
|
10732
11533
|
}
|
|
10733
11534
|
const DEFAULT_RETENTION = {
|
|
10734
11535
|
maxEntries: 200,
|
|
@@ -10939,7 +11740,7 @@ ${tableRelations.join(",\n")}
|
|
|
10939
11740
|
const registry = new PostgresCollectionRegistry();
|
|
10940
11741
|
if (collections) {
|
|
10941
11742
|
registry.registerMultiple(collections);
|
|
10942
|
-
|
|
11743
|
+
serverCore.logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
10943
11744
|
}
|
|
10944
11745
|
if (pgConfig.schema?.tables) {
|
|
10945
11746
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -10965,10 +11766,28 @@ ${tableRelations.join(",\n")}
|
|
|
10965
11766
|
try {
|
|
10966
11767
|
await schemaAwareDb.execute(drizzleOrm.sql`SELECT 1`);
|
|
10967
11768
|
} catch (err) {
|
|
10968
|
-
|
|
10969
|
-
|
|
11769
|
+
serverCore.logger.error("❌ Failed to connect to PostgreSQL", {
|
|
11770
|
+
error: err
|
|
11771
|
+
});
|
|
11772
|
+
serverCore.logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
10970
11773
|
}
|
|
10971
11774
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11775
|
+
let readDb;
|
|
11776
|
+
const readUrl = process.env.DATABASE_READ_URL;
|
|
11777
|
+
if (readUrl && readUrl !== pgConfig.connectionString) {
|
|
11778
|
+
try {
|
|
11779
|
+
const {
|
|
11780
|
+
createReadReplicaConnection: createReadReplicaConnection2
|
|
11781
|
+
} = await Promise.resolve().then(() => connection);
|
|
11782
|
+
const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
|
|
11783
|
+
readDb = readResources.db;
|
|
11784
|
+
serverCore.logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11785
|
+
} catch (err) {
|
|
11786
|
+
serverCore.logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
|
|
11787
|
+
error: err
|
|
11788
|
+
});
|
|
11789
|
+
}
|
|
11790
|
+
}
|
|
10972
11791
|
const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
|
|
10973
11792
|
const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
|
|
10974
11793
|
realtimeService.setDataDriver(driver);
|
|
@@ -10976,18 +11795,24 @@ ${tableRelations.join(",\n")}
|
|
|
10976
11795
|
try {
|
|
10977
11796
|
await driver.branchService.ensureBranchMetadataTable();
|
|
10978
11797
|
} catch (err) {
|
|
10979
|
-
|
|
11798
|
+
serverCore.logger.warn("⚠️ Could not initialize branch metadata table", {
|
|
11799
|
+
error: err
|
|
11800
|
+
});
|
|
10980
11801
|
}
|
|
10981
11802
|
}
|
|
10982
|
-
|
|
11803
|
+
const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
|
|
11804
|
+
if (directUrl) {
|
|
10983
11805
|
try {
|
|
10984
|
-
await realtimeService.startListening(
|
|
11806
|
+
await realtimeService.startListening(directUrl);
|
|
10985
11807
|
} catch (err) {
|
|
10986
|
-
|
|
11808
|
+
serverCore.logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
11809
|
+
error: err
|
|
11810
|
+
});
|
|
10987
11811
|
}
|
|
10988
11812
|
}
|
|
10989
11813
|
const internals = {
|
|
10990
11814
|
db: schemaAwareDb,
|
|
11815
|
+
readDb,
|
|
10991
11816
|
registry,
|
|
10992
11817
|
realtimeService,
|
|
10993
11818
|
driver,
|
|
@@ -11124,14 +11949,22 @@ ${tableRelations.join(",\n")}
|
|
|
11124
11949
|
exports2.RealtimeService = RealtimeService;
|
|
11125
11950
|
exports2.appConfig = appConfig;
|
|
11126
11951
|
exports2.createAuthSchema = createAuthSchema;
|
|
11952
|
+
exports2.createDirectDatabaseConnection = createDirectDatabaseConnection;
|
|
11127
11953
|
exports2.createPostgresAdapter = createPostgresAdapter;
|
|
11128
11954
|
exports2.createPostgresBootstrapper = createPostgresBootstrapper;
|
|
11129
11955
|
exports2.createPostgresDatabaseConnection = createPostgresDatabaseConnection;
|
|
11130
11956
|
exports2.createPostgresWebSocket = createPostgresWebSocket;
|
|
11957
|
+
exports2.createReadReplicaConnection = createReadReplicaConnection;
|
|
11131
11958
|
exports2.generateSchema = generateSchema;
|
|
11959
|
+
exports2.mfaChallenges = mfaChallenges;
|
|
11960
|
+
exports2.mfaChallengesRelations = mfaChallengesRelations;
|
|
11961
|
+
exports2.mfaFactors = mfaFactors;
|
|
11962
|
+
exports2.mfaFactorsRelations = mfaFactorsRelations;
|
|
11132
11963
|
exports2.passwordResetTokens = passwordResetTokens;
|
|
11133
11964
|
exports2.passwordResetTokensRelations = passwordResetTokensRelations;
|
|
11134
11965
|
exports2.rebaseSchema = rebaseSchema;
|
|
11966
|
+
exports2.recoveryCodes = recoveryCodes;
|
|
11967
|
+
exports2.recoveryCodesRelations = recoveryCodesRelations;
|
|
11135
11968
|
exports2.refreshTokens = refreshTokens;
|
|
11136
11969
|
exports2.refreshTokensRelations = refreshTokensRelations;
|
|
11137
11970
|
exports2.roles = roles;
|