@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.es.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Pool, Client } from "pg";
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
|
|
4
|
-
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb,
|
|
4
|
+
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, boolean, varchar, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
|
|
5
5
|
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import { promises } from "fs";
|
|
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
|
|
|
11
11
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12
12
|
import { EventEmitter } from "events";
|
|
13
13
|
import { inspect } from "util";
|
|
14
|
-
import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
|
|
14
|
+
import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
|
|
15
15
|
const DEFAULT_POOL = {
|
|
16
16
|
max: 20,
|
|
17
17
|
idleTimeoutMillis: 3e4,
|
|
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
51
51
|
connectionString
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
55
|
+
const opts = {
|
|
56
|
+
...DEFAULT_POOL,
|
|
57
|
+
max: 5,
|
|
58
|
+
...poolConfig
|
|
59
|
+
};
|
|
60
|
+
const pgPoolConfig = {
|
|
61
|
+
connectionString,
|
|
62
|
+
max: opts.max,
|
|
63
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
64
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
65
|
+
query_timeout: opts.queryTimeout,
|
|
66
|
+
statement_timeout: opts.statementTimeout,
|
|
67
|
+
keepAlive: opts.keepAlive,
|
|
68
|
+
keepAliveInitialDelayMillis: 0
|
|
69
|
+
};
|
|
70
|
+
const pool = new Pool(pgPoolConfig);
|
|
71
|
+
pool.on("error", (err) => {
|
|
72
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
73
|
+
});
|
|
74
|
+
const db = schema ? drizzle(pool, {
|
|
75
|
+
schema
|
|
76
|
+
}) : drizzle(pool);
|
|
77
|
+
return {
|
|
78
|
+
db,
|
|
79
|
+
pool,
|
|
80
|
+
connectionString
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
84
|
+
const opts = {
|
|
85
|
+
...DEFAULT_POOL,
|
|
86
|
+
max: 10,
|
|
87
|
+
...poolConfig
|
|
88
|
+
};
|
|
89
|
+
const pgPoolConfig = {
|
|
90
|
+
connectionString,
|
|
91
|
+
max: opts.max,
|
|
92
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
93
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
94
|
+
query_timeout: opts.queryTimeout,
|
|
95
|
+
statement_timeout: opts.statementTimeout,
|
|
96
|
+
keepAlive: opts.keepAlive,
|
|
97
|
+
keepAliveInitialDelayMillis: 0
|
|
98
|
+
};
|
|
99
|
+
const pool = new Pool(pgPoolConfig);
|
|
100
|
+
pool.on("error", (err) => {
|
|
101
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
102
|
+
});
|
|
103
|
+
const db = schema ? drizzle(pool, {
|
|
104
|
+
schema
|
|
105
|
+
}) : drizzle(pool);
|
|
106
|
+
return {
|
|
107
|
+
db,
|
|
108
|
+
pool,
|
|
109
|
+
connectionString
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
113
|
+
__proto__: null,
|
|
114
|
+
createDirectDatabaseConnection,
|
|
115
|
+
createPostgresDatabaseConnection,
|
|
116
|
+
createReadReplicaConnection
|
|
117
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
54
118
|
class Vector {
|
|
55
119
|
value;
|
|
56
120
|
constructor(value) {
|
|
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
|
|
|
970
1034
|
return output;
|
|
971
1035
|
}
|
|
972
1036
|
for (const key in source) {
|
|
1037
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
973
1040
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
974
1041
|
const sourceValue = source[key];
|
|
975
1042
|
const outputValue = output[key];
|
|
@@ -1158,118 +1225,6 @@ function enumToObjectEntries(enumValues) {
|
|
|
1158
1225
|
});
|
|
1159
1226
|
}
|
|
1160
1227
|
}
|
|
1161
|
-
function getSubcollections(collection) {
|
|
1162
|
-
if (collection.childCollections) {
|
|
1163
|
-
return collection.childCollections() ?? [];
|
|
1164
|
-
}
|
|
1165
|
-
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
1166
|
-
return collection.subcollections() ?? [];
|
|
1167
|
-
}
|
|
1168
|
-
if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
|
|
1169
|
-
const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
|
|
1170
|
-
return manyRelations.map((r) => {
|
|
1171
|
-
const target = r.target();
|
|
1172
|
-
if (!target) return void 0;
|
|
1173
|
-
const relationKey = r.relationName || target.slug;
|
|
1174
|
-
let customName;
|
|
1175
|
-
if (collection.properties) {
|
|
1176
|
-
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1177
|
-
if (prop && prop[1].name) {
|
|
1178
|
-
customName = prop[1].name;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
const baseOverrides = {
|
|
1182
|
-
slug: relationKey
|
|
1183
|
-
};
|
|
1184
|
-
if (customName) {
|
|
1185
|
-
baseOverrides.name = customName;
|
|
1186
|
-
baseOverrides.singularName = customName;
|
|
1187
|
-
}
|
|
1188
|
-
const targetWithOverrides = {
|
|
1189
|
-
...target,
|
|
1190
|
-
...baseOverrides
|
|
1191
|
-
};
|
|
1192
|
-
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1193
|
-
}).filter((c) => Boolean(c));
|
|
1194
|
-
}
|
|
1195
|
-
return [];
|
|
1196
|
-
}
|
|
1197
|
-
function hasPropertyCallbacks(properties, callbackName) {
|
|
1198
|
-
if (!properties) return false;
|
|
1199
|
-
for (const property of Object.values(properties)) {
|
|
1200
|
-
if (property.callbacks?.[callbackName]) return true;
|
|
1201
|
-
if (property.type === "map" && property.properties) {
|
|
1202
|
-
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
1203
|
-
} else if (property.type === "array" && property.of) {
|
|
1204
|
-
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
1205
|
-
for (const of of ofs) {
|
|
1206
|
-
if (of.callbacks?.[callbackName]) return true;
|
|
1207
|
-
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
async function processProperties(properties, values, previousValues, propsContext, callbackName) {
|
|
1214
|
-
if (!values || typeof values !== "object") return values;
|
|
1215
|
-
const result = {
|
|
1216
|
-
...values
|
|
1217
|
-
};
|
|
1218
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
1219
|
-
if (result[key] === void 0) continue;
|
|
1220
|
-
let currentValue = result[key];
|
|
1221
|
-
const previousValue = previousValues?.[key];
|
|
1222
|
-
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
1223
|
-
if (property.of && !Array.isArray(property.of)) {
|
|
1224
|
-
currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
1225
|
-
const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
|
|
1226
|
-
const singlePropData = {
|
|
1227
|
-
"_tmp": property.of
|
|
1228
|
-
};
|
|
1229
|
-
const res = await processProperties(singlePropData, {
|
|
1230
|
-
"_tmp": item
|
|
1231
|
-
}, {
|
|
1232
|
-
"_tmp": prevItem
|
|
1233
|
-
}, propsContext, callbackName);
|
|
1234
|
-
return res["_tmp"];
|
|
1235
|
-
}));
|
|
1236
|
-
}
|
|
1237
|
-
} else if (property.type === "map" && property.properties && typeof currentValue === "object") {
|
|
1238
|
-
currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
|
|
1239
|
-
}
|
|
1240
|
-
if (property.callbacks?.[callbackName]) {
|
|
1241
|
-
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
1242
|
-
...propsContext,
|
|
1243
|
-
value: currentValue,
|
|
1244
|
-
previousValue
|
|
1245
|
-
}));
|
|
1246
|
-
if (cbRes !== void 0) {
|
|
1247
|
-
currentValue = cbRes;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
result[key] = currentValue;
|
|
1251
|
-
}
|
|
1252
|
-
return result;
|
|
1253
|
-
}
|
|
1254
|
-
const buildPropertyCallbacks = (properties) => {
|
|
1255
|
-
if (!properties) return void 0;
|
|
1256
|
-
const propertyCallbacks = {};
|
|
1257
|
-
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
1258
|
-
propertyCallbacks.afterRead = async (props) => {
|
|
1259
|
-
const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
|
|
1260
|
-
return {
|
|
1261
|
-
...props.entity,
|
|
1262
|
-
values: processedValues
|
|
1263
|
-
};
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
if (hasPropertyCallbacks(properties, "beforeSave")) {
|
|
1267
|
-
propertyCallbacks.beforeSave = async (props) => {
|
|
1268
|
-
return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1272
|
-
};
|
|
1273
1228
|
function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
1274
1229
|
if (!relation.target) {
|
|
1275
1230
|
throw new Error("Relation is missing a `target` collection.");
|
|
@@ -1301,6 +1256,8 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
|
1301
1256
|
} else {
|
|
1302
1257
|
targetCollection = evaluated;
|
|
1303
1258
|
}
|
|
1259
|
+
} else if (rawTarget && typeof rawTarget === "object") {
|
|
1260
|
+
targetCollection = rawTarget;
|
|
1304
1261
|
}
|
|
1305
1262
|
if (!targetCollection) {
|
|
1306
1263
|
throw new Error("Relation is missing a valid `target` collection.");
|
|
@@ -1420,11 +1377,14 @@ function resolveCollectionRelations(collection) {
|
|
|
1420
1377
|
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
1421
1378
|
if (relCollection.relations) {
|
|
1422
1379
|
relCollection.relations.forEach((relation) => {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1380
|
+
try {
|
|
1381
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1382
|
+
const relationKey = normalizedRelation.relationName;
|
|
1383
|
+
if (relationKey) {
|
|
1384
|
+
relations2[relationKey] = normalizedRelation;
|
|
1385
|
+
registeredRelationNames.add(relationKey);
|
|
1386
|
+
}
|
|
1387
|
+
} catch (e) {
|
|
1428
1388
|
}
|
|
1429
1389
|
});
|
|
1430
1390
|
}
|
|
@@ -1472,12 +1432,8 @@ function resolvePropertyRelation({
|
|
|
1472
1432
|
overrides: relProp.overrides
|
|
1473
1433
|
};
|
|
1474
1434
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1478
|
-
return void 0;
|
|
1479
|
-
}
|
|
1480
|
-
return relation;
|
|
1435
|
+
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1436
|
+
return void 0;
|
|
1481
1437
|
}
|
|
1482
1438
|
function getTableName(collection) {
|
|
1483
1439
|
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
@@ -1504,6 +1460,119 @@ function findRelation(resolvedRelations, key) {
|
|
|
1504
1460
|
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
1505
1461
|
return void 0;
|
|
1506
1462
|
}
|
|
1463
|
+
function getSubcollections(collection) {
|
|
1464
|
+
if (collection.childCollections) {
|
|
1465
|
+
return collection.childCollections() ?? [];
|
|
1466
|
+
}
|
|
1467
|
+
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
1468
|
+
return collection.subcollections() ?? [];
|
|
1469
|
+
}
|
|
1470
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
1471
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
1472
|
+
const manyRelations = Object.values(resolvedRelations).filter((r) => r.cardinality === "many");
|
|
1473
|
+
return manyRelations.map((r) => {
|
|
1474
|
+
const target = r.target();
|
|
1475
|
+
if (!target) return void 0;
|
|
1476
|
+
const relationKey = r.relationName || target.slug;
|
|
1477
|
+
let customName;
|
|
1478
|
+
if (collection.properties) {
|
|
1479
|
+
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1480
|
+
if (prop && prop[1].name) {
|
|
1481
|
+
customName = prop[1].name;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const baseOverrides = {
|
|
1485
|
+
slug: relationKey
|
|
1486
|
+
};
|
|
1487
|
+
if (customName) {
|
|
1488
|
+
baseOverrides.name = customName;
|
|
1489
|
+
baseOverrides.singularName = customName;
|
|
1490
|
+
}
|
|
1491
|
+
const targetWithOverrides = {
|
|
1492
|
+
...target,
|
|
1493
|
+
...baseOverrides
|
|
1494
|
+
};
|
|
1495
|
+
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1496
|
+
}).filter((c) => Boolean(c));
|
|
1497
|
+
}
|
|
1498
|
+
return [];
|
|
1499
|
+
}
|
|
1500
|
+
function hasPropertyCallbacks(properties, callbackName) {
|
|
1501
|
+
if (!properties) return false;
|
|
1502
|
+
for (const property of Object.values(properties)) {
|
|
1503
|
+
if (property.callbacks?.[callbackName]) return true;
|
|
1504
|
+
if (property.type === "map" && property.properties) {
|
|
1505
|
+
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
1506
|
+
} else if (property.type === "array" && property.of) {
|
|
1507
|
+
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
1508
|
+
for (const of of ofs) {
|
|
1509
|
+
if (of.callbacks?.[callbackName]) return true;
|
|
1510
|
+
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
async function processProperties(properties, values, previousValues, propsContext, callbackName) {
|
|
1517
|
+
if (!values || typeof values !== "object") return values;
|
|
1518
|
+
const result = {
|
|
1519
|
+
...values
|
|
1520
|
+
};
|
|
1521
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
1522
|
+
if (result[key] === void 0) continue;
|
|
1523
|
+
let currentValue = result[key];
|
|
1524
|
+
const previousValue = previousValues?.[key];
|
|
1525
|
+
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
1526
|
+
if (property.of && !Array.isArray(property.of)) {
|
|
1527
|
+
currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
1528
|
+
const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
|
|
1529
|
+
const singlePropData = {
|
|
1530
|
+
"_tmp": property.of
|
|
1531
|
+
};
|
|
1532
|
+
const res = await processProperties(singlePropData, {
|
|
1533
|
+
"_tmp": item
|
|
1534
|
+
}, {
|
|
1535
|
+
"_tmp": prevItem
|
|
1536
|
+
}, propsContext, callbackName);
|
|
1537
|
+
return res["_tmp"];
|
|
1538
|
+
}));
|
|
1539
|
+
}
|
|
1540
|
+
} else if (property.type === "map" && property.properties && typeof currentValue === "object") {
|
|
1541
|
+
currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
|
|
1542
|
+
}
|
|
1543
|
+
if (property.callbacks?.[callbackName]) {
|
|
1544
|
+
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
1545
|
+
...propsContext,
|
|
1546
|
+
value: currentValue,
|
|
1547
|
+
previousValue
|
|
1548
|
+
}));
|
|
1549
|
+
if (cbRes !== void 0) {
|
|
1550
|
+
currentValue = cbRes;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
result[key] = currentValue;
|
|
1554
|
+
}
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
const buildPropertyCallbacks = (properties) => {
|
|
1558
|
+
if (!properties) return void 0;
|
|
1559
|
+
const propertyCallbacks = {};
|
|
1560
|
+
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
1561
|
+
propertyCallbacks.afterRead = async (props) => {
|
|
1562
|
+
const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
|
|
1563
|
+
return {
|
|
1564
|
+
...props.entity,
|
|
1565
|
+
values: processedValues
|
|
1566
|
+
};
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
if (hasPropertyCallbacks(properties, "beforeSave")) {
|
|
1570
|
+
propertyCallbacks.beforeSave = async (props) => {
|
|
1571
|
+
return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
1575
|
+
};
|
|
1507
1576
|
var logic = { exports: {} };
|
|
1508
1577
|
(function(module, exports) {
|
|
1509
1578
|
(function(root, factory) {
|
|
@@ -2411,8 +2480,18 @@ class CollectionRegistry {
|
|
|
2411
2480
|
const mergedRelationsRaw = [...extractedRelations];
|
|
2412
2481
|
for (const manual of manualRelations) {
|
|
2413
2482
|
const name = manual.relationName;
|
|
2414
|
-
if (!name
|
|
2483
|
+
if (!name) {
|
|
2415
2484
|
mergedRelationsRaw.push(manual);
|
|
2485
|
+
} else {
|
|
2486
|
+
const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
|
|
2487
|
+
if (existingIndex === -1) {
|
|
2488
|
+
mergedRelationsRaw.push(manual);
|
|
2489
|
+
} else {
|
|
2490
|
+
mergedRelationsRaw[existingIndex] = {
|
|
2491
|
+
...manual,
|
|
2492
|
+
...mergedRelationsRaw[existingIndex]
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2416
2495
|
}
|
|
2417
2496
|
}
|
|
2418
2497
|
let mergedRelations = mergedRelationsRaw;
|
|
@@ -2625,11 +2704,207 @@ class CollectionRegistry {
|
|
|
2625
2704
|
collections.push(currentCollection);
|
|
2626
2705
|
}
|
|
2627
2706
|
}
|
|
2628
|
-
return {
|
|
2629
|
-
collections,
|
|
2630
|
-
entityIds,
|
|
2631
|
-
finalCollection: currentCollection
|
|
2632
|
-
};
|
|
2707
|
+
return {
|
|
2708
|
+
collections,
|
|
2709
|
+
entityIds,
|
|
2710
|
+
finalCollection: currentCollection
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const defaultUsersCollection = {
|
|
2715
|
+
name: "Users",
|
|
2716
|
+
singularName: "User",
|
|
2717
|
+
slug: "users",
|
|
2718
|
+
table: "users",
|
|
2719
|
+
schema: "rebase",
|
|
2720
|
+
icon: "Users",
|
|
2721
|
+
group: "Settings",
|
|
2722
|
+
properties: {
|
|
2723
|
+
id: {
|
|
2724
|
+
name: "ID",
|
|
2725
|
+
type: "string",
|
|
2726
|
+
isId: "uuid"
|
|
2727
|
+
},
|
|
2728
|
+
email: {
|
|
2729
|
+
name: "Email",
|
|
2730
|
+
type: "string",
|
|
2731
|
+
validation: {
|
|
2732
|
+
required: true,
|
|
2733
|
+
unique: true
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
password_hash: {
|
|
2737
|
+
name: "Password Hash",
|
|
2738
|
+
type: "string",
|
|
2739
|
+
ui: {
|
|
2740
|
+
hideFromCollection: true
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
display_name: {
|
|
2744
|
+
name: "Display Name",
|
|
2745
|
+
type: "string"
|
|
2746
|
+
},
|
|
2747
|
+
photo_url: {
|
|
2748
|
+
name: "Photo URL",
|
|
2749
|
+
type: "string"
|
|
2750
|
+
},
|
|
2751
|
+
email_verified: {
|
|
2752
|
+
name: "Email Verified",
|
|
2753
|
+
type: "boolean",
|
|
2754
|
+
defaultValue: false
|
|
2755
|
+
},
|
|
2756
|
+
email_verification_token: {
|
|
2757
|
+
name: "Email Verification Token",
|
|
2758
|
+
type: "string",
|
|
2759
|
+
ui: {
|
|
2760
|
+
hideFromCollection: true
|
|
2761
|
+
}
|
|
2762
|
+
},
|
|
2763
|
+
email_verification_sent_at: {
|
|
2764
|
+
name: "Email Verification Sent At",
|
|
2765
|
+
type: "date",
|
|
2766
|
+
ui: {
|
|
2767
|
+
hideFromCollection: true
|
|
2768
|
+
}
|
|
2769
|
+
},
|
|
2770
|
+
metadata: {
|
|
2771
|
+
name: "Metadata",
|
|
2772
|
+
type: "map",
|
|
2773
|
+
defaultValue: {},
|
|
2774
|
+
ui: {
|
|
2775
|
+
hideFromCollection: true
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
created_at: {
|
|
2779
|
+
name: "Created At",
|
|
2780
|
+
type: "date",
|
|
2781
|
+
autoValue: "on_create",
|
|
2782
|
+
ui: {
|
|
2783
|
+
readOnly: true,
|
|
2784
|
+
hideFromCollection: true
|
|
2785
|
+
}
|
|
2786
|
+
},
|
|
2787
|
+
updated_at: {
|
|
2788
|
+
name: "Updated At",
|
|
2789
|
+
type: "date",
|
|
2790
|
+
autoValue: "on_update",
|
|
2791
|
+
ui: {
|
|
2792
|
+
readOnly: true,
|
|
2793
|
+
hideFromCollection: true
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
function mapOperator(op) {
|
|
2799
|
+
switch (op) {
|
|
2800
|
+
case "==":
|
|
2801
|
+
return "eq";
|
|
2802
|
+
case "!=":
|
|
2803
|
+
return "neq";
|
|
2804
|
+
case ">":
|
|
2805
|
+
return "gt";
|
|
2806
|
+
case ">=":
|
|
2807
|
+
return "gte";
|
|
2808
|
+
case "<":
|
|
2809
|
+
return "lt";
|
|
2810
|
+
case "<=":
|
|
2811
|
+
return "lte";
|
|
2812
|
+
case "array-contains":
|
|
2813
|
+
return "cs";
|
|
2814
|
+
case "array-contains-any":
|
|
2815
|
+
return "csa";
|
|
2816
|
+
case "not-in":
|
|
2817
|
+
return "nin";
|
|
2818
|
+
default:
|
|
2819
|
+
return op;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
class QueryBuilder {
|
|
2823
|
+
constructor(collection) {
|
|
2824
|
+
this.collection = collection;
|
|
2825
|
+
}
|
|
2826
|
+
params = {
|
|
2827
|
+
where: {}
|
|
2828
|
+
};
|
|
2829
|
+
/**
|
|
2830
|
+
* Add a filter condition to your query.
|
|
2831
|
+
* @example
|
|
2832
|
+
* client.collection('users').where('age', '>=', 18).find()
|
|
2833
|
+
*/
|
|
2834
|
+
where(column, operator, value) {
|
|
2835
|
+
if (!this.params.where) {
|
|
2836
|
+
this.params.where = {};
|
|
2837
|
+
}
|
|
2838
|
+
const mappedOp = mapOperator(operator);
|
|
2839
|
+
let formattedValue = value;
|
|
2840
|
+
if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
|
|
2841
|
+
formattedValue = `(${value.join(",")})`;
|
|
2842
|
+
} else if (value === null) {
|
|
2843
|
+
formattedValue = "null";
|
|
2844
|
+
}
|
|
2845
|
+
this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
|
|
2846
|
+
return this;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Order the results by a specific column.
|
|
2850
|
+
* @example
|
|
2851
|
+
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
2852
|
+
*/
|
|
2853
|
+
orderBy(column, ascending = "asc") {
|
|
2854
|
+
this.params.orderBy = `${column}:${ascending}`;
|
|
2855
|
+
return this;
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Limit the number of results returned.
|
|
2859
|
+
*/
|
|
2860
|
+
limit(count2) {
|
|
2861
|
+
this.params.limit = count2;
|
|
2862
|
+
return this;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Skip the first N results.
|
|
2866
|
+
*/
|
|
2867
|
+
offset(count2) {
|
|
2868
|
+
this.params.offset = count2;
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Set a free-text search string if supported by the backend.
|
|
2873
|
+
*/
|
|
2874
|
+
search(searchString) {
|
|
2875
|
+
this.params.searchString = searchString;
|
|
2876
|
+
return this;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Include related entities in the response.
|
|
2880
|
+
* Relations will be populated with full entity data instead of just IDs.
|
|
2881
|
+
*
|
|
2882
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
2883
|
+
* @example
|
|
2884
|
+
* // Include specific relations
|
|
2885
|
+
* client.data.posts.include("tags", "author").find()
|
|
2886
|
+
*
|
|
2887
|
+
* // Include all relations
|
|
2888
|
+
* client.data.posts.include("*").find()
|
|
2889
|
+
*/
|
|
2890
|
+
include(...relations2) {
|
|
2891
|
+
this.params.include = relations2;
|
|
2892
|
+
return this;
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Execute the find query and return the results.
|
|
2896
|
+
*/
|
|
2897
|
+
async find() {
|
|
2898
|
+
return this.collection.find(this.params);
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Listen to realtime updates matching this query.
|
|
2902
|
+
*/
|
|
2903
|
+
listen(onUpdate, onError) {
|
|
2904
|
+
if (!this.collection.listen) {
|
|
2905
|
+
throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
2906
|
+
}
|
|
2907
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
2633
2908
|
}
|
|
2634
2909
|
}
|
|
2635
2910
|
function convertWhereToFilter(where) {
|
|
@@ -2711,7 +2986,7 @@ function parseOrderBy(orderBy) {
|
|
|
2711
2986
|
return [field, direction];
|
|
2712
2987
|
}
|
|
2713
2988
|
function createDriverAccessor(driver, slug) {
|
|
2714
|
-
|
|
2989
|
+
const accessor = {
|
|
2715
2990
|
async find(params) {
|
|
2716
2991
|
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2717
2992
|
const entities = await driver.fetchCollection({
|
|
@@ -2805,8 +3080,28 @@ function createDriverAccessor(driver, slug) {
|
|
|
2805
3080
|
onUpdate: (entity) => onUpdate(entity ?? void 0),
|
|
2806
3081
|
onError
|
|
2807
3082
|
});
|
|
2808
|
-
} : void 0
|
|
3083
|
+
} : void 0,
|
|
3084
|
+
// Fluent Query Builder
|
|
3085
|
+
where(column, operator, value) {
|
|
3086
|
+
return new QueryBuilder(accessor).where(column, operator, value);
|
|
3087
|
+
},
|
|
3088
|
+
orderBy(column, ascending) {
|
|
3089
|
+
return new QueryBuilder(accessor).orderBy(column, ascending);
|
|
3090
|
+
},
|
|
3091
|
+
limit(count2) {
|
|
3092
|
+
return new QueryBuilder(accessor).limit(count2);
|
|
3093
|
+
},
|
|
3094
|
+
offset(count2) {
|
|
3095
|
+
return new QueryBuilder(accessor).offset(count2);
|
|
3096
|
+
},
|
|
3097
|
+
search(searchString) {
|
|
3098
|
+
return new QueryBuilder(accessor).search(searchString);
|
|
3099
|
+
},
|
|
3100
|
+
include(...relations2) {
|
|
3101
|
+
return new QueryBuilder(accessor).include(...relations2);
|
|
3102
|
+
}
|
|
2809
3103
|
};
|
|
3104
|
+
return accessor;
|
|
2810
3105
|
}
|
|
2811
3106
|
function buildRebaseData(driver) {
|
|
2812
3107
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2840,7 +3135,13 @@ class DrizzleConditionBuilder {
|
|
|
2840
3135
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
2841
3136
|
if (!filterParam) continue;
|
|
2842
3137
|
const [op, value] = filterParam;
|
|
2843
|
-
|
|
3138
|
+
let fieldColumn = table[field];
|
|
3139
|
+
if (!fieldColumn) {
|
|
3140
|
+
const relationKey = `${field}_id`;
|
|
3141
|
+
if (relationKey in table) {
|
|
3142
|
+
fieldColumn = table[relationKey];
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2844
3145
|
if (!fieldColumn) {
|
|
2845
3146
|
console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
2846
3147
|
continue;
|
|
@@ -2882,6 +3183,17 @@ class DrizzleConditionBuilder {
|
|
|
2882
3183
|
return null;
|
|
2883
3184
|
case "array-contains":
|
|
2884
3185
|
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3186
|
+
case "array-contains-any":
|
|
3187
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3188
|
+
const textValues = value.map((v) => String(v));
|
|
3189
|
+
return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
|
|
3190
|
+
}
|
|
3191
|
+
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3192
|
+
case "not-in":
|
|
3193
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3194
|
+
return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
|
|
3195
|
+
}
|
|
3196
|
+
return null;
|
|
2885
3197
|
default:
|
|
2886
3198
|
console.warn(`Unsupported filter operation: ${op}`);
|
|
2887
3199
|
return null;
|
|
@@ -3397,6 +3709,40 @@ class DrizzleConditionBuilder {
|
|
|
3397
3709
|
return null;
|
|
3398
3710
|
}
|
|
3399
3711
|
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Build vector similarity search expressions for pgvector.
|
|
3714
|
+
*
|
|
3715
|
+
* Returns:
|
|
3716
|
+
* - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
|
|
3717
|
+
* - `filter`: optional WHERE clause for distance threshold
|
|
3718
|
+
* - `distanceSelect`: SQL expression for selecting the distance as `_distance`
|
|
3719
|
+
*/
|
|
3720
|
+
static buildVectorSearchConditions(table, vectorSearch) {
|
|
3721
|
+
const column = table[vectorSearch.property];
|
|
3722
|
+
if (!column) {
|
|
3723
|
+
throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
3724
|
+
}
|
|
3725
|
+
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
3726
|
+
const distanceFn = vectorSearch.distance || "cosine";
|
|
3727
|
+
let operator;
|
|
3728
|
+
switch (distanceFn) {
|
|
3729
|
+
case "cosine":
|
|
3730
|
+
operator = "<=>";
|
|
3731
|
+
break;
|
|
3732
|
+
case "l2":
|
|
3733
|
+
operator = "<->";
|
|
3734
|
+
break;
|
|
3735
|
+
case "inner_product":
|
|
3736
|
+
operator = "<#>";
|
|
3737
|
+
break;
|
|
3738
|
+
}
|
|
3739
|
+
const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
|
|
3740
|
+
return {
|
|
3741
|
+
orderBy: distanceExpr,
|
|
3742
|
+
filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
|
|
3743
|
+
distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3400
3746
|
}
|
|
3401
3747
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
3402
3748
|
function getColumnMeta(col) {
|
|
@@ -5342,7 +5688,7 @@ class EntityFetchService {
|
|
|
5342
5688
|
const qb = this.getQueryBuilder(tableName);
|
|
5343
5689
|
const withConfig = this.buildWithConfig(collection);
|
|
5344
5690
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
5345
|
-
if (qb && !options.searchString && !hasRelations) {
|
|
5691
|
+
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
|
|
5346
5692
|
try {
|
|
5347
5693
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5348
5694
|
const results2 = await qb.findMany(queryOpts);
|
|
@@ -5356,7 +5702,14 @@ class EntityFetchService {
|
|
|
5356
5702
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
5357
5703
|
}
|
|
5358
5704
|
}
|
|
5359
|
-
let
|
|
5705
|
+
let vectorMeta;
|
|
5706
|
+
if (options.vectorSearch) {
|
|
5707
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
5708
|
+
}
|
|
5709
|
+
let query = vectorMeta ? this.db.select({
|
|
5710
|
+
table_row: table,
|
|
5711
|
+
_distance: vectorMeta.distanceSelect
|
|
5712
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5360
5713
|
const allConditions = [];
|
|
5361
5714
|
if (options.searchString) {
|
|
5362
5715
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5367,12 +5720,17 @@ class EntityFetchService {
|
|
|
5367
5720
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5368
5721
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5369
5722
|
}
|
|
5723
|
+
if (vectorMeta?.filter) {
|
|
5724
|
+
allConditions.push(vectorMeta.filter);
|
|
5725
|
+
}
|
|
5370
5726
|
if (allConditions.length > 0) {
|
|
5371
5727
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5372
5728
|
if (finalCondition) query = query.where(finalCondition);
|
|
5373
5729
|
}
|
|
5374
5730
|
const orderExpressions = [];
|
|
5375
|
-
if (
|
|
5731
|
+
if (vectorMeta) {
|
|
5732
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
5733
|
+
} else if (options.orderBy) {
|
|
5376
5734
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5377
5735
|
if (orderByField) {
|
|
5378
5736
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5388,10 +5746,14 @@ class EntityFetchService {
|
|
|
5388
5746
|
if (finalCondition) query = query.where(finalCondition);
|
|
5389
5747
|
}
|
|
5390
5748
|
}
|
|
5391
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5749
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5392
5750
|
if (limitValue) query = query.limit(limitValue);
|
|
5393
5751
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5394
|
-
const
|
|
5752
|
+
const rawResults = await query;
|
|
5753
|
+
const results = vectorMeta ? rawResults.map((r) => ({
|
|
5754
|
+
...r.table_row,
|
|
5755
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
5756
|
+
})) : rawResults;
|
|
5395
5757
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5396
5758
|
}
|
|
5397
5759
|
/**
|
|
@@ -5613,7 +5975,7 @@ class EntityFetchService {
|
|
|
5613
5975
|
const idField = table[idInfo.fieldName];
|
|
5614
5976
|
const tableName = getTableName$1(table);
|
|
5615
5977
|
const qb = this.getQueryBuilder(tableName);
|
|
5616
|
-
if (qb && !options.searchString) {
|
|
5978
|
+
if (qb && !options.searchString && !options.vectorSearch) {
|
|
5617
5979
|
try {
|
|
5618
5980
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5619
5981
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
@@ -5759,7 +6121,14 @@ class EntityFetchService {
|
|
|
5759
6121
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
5760
6122
|
const idInfo = idInfoArray[0];
|
|
5761
6123
|
const idField = table[idInfo.fieldName];
|
|
5762
|
-
let
|
|
6124
|
+
let vectorMeta;
|
|
6125
|
+
if (options.vectorSearch) {
|
|
6126
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
6127
|
+
}
|
|
6128
|
+
let query = vectorMeta ? this.db.select({
|
|
6129
|
+
table_row: table,
|
|
6130
|
+
_distance: vectorMeta.distanceSelect
|
|
6131
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5763
6132
|
const allConditions = [];
|
|
5764
6133
|
if (options.searchString) {
|
|
5765
6134
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5770,12 +6139,17 @@ class EntityFetchService {
|
|
|
5770
6139
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5771
6140
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5772
6141
|
}
|
|
6142
|
+
if (vectorMeta?.filter) {
|
|
6143
|
+
allConditions.push(vectorMeta.filter);
|
|
6144
|
+
}
|
|
5773
6145
|
if (allConditions.length > 0) {
|
|
5774
6146
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5775
6147
|
if (finalCondition) query = query.where(finalCondition);
|
|
5776
6148
|
}
|
|
5777
6149
|
const orderExpressions = [];
|
|
5778
|
-
if (
|
|
6150
|
+
if (vectorMeta) {
|
|
6151
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
6152
|
+
} else if (options.orderBy) {
|
|
5779
6153
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5780
6154
|
if (orderByField) {
|
|
5781
6155
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5783,10 +6157,17 @@ class EntityFetchService {
|
|
|
5783
6157
|
}
|
|
5784
6158
|
orderExpressions.push(desc(idField));
|
|
5785
6159
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5786
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
6160
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5787
6161
|
if (limitValue) query = query.limit(limitValue);
|
|
5788
6162
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5789
|
-
|
|
6163
|
+
const rawResults = await query;
|
|
6164
|
+
if (vectorMeta) {
|
|
6165
|
+
return rawResults.map((r) => ({
|
|
6166
|
+
...r.table_row,
|
|
6167
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
6168
|
+
}));
|
|
6169
|
+
}
|
|
6170
|
+
return rawResults;
|
|
5790
6171
|
}
|
|
5791
6172
|
/**
|
|
5792
6173
|
* Check if the Drizzle instance has the relational query API available
|
|
@@ -6514,7 +6895,7 @@ class PostgresBackendDriver {
|
|
|
6514
6895
|
callbacks: void 0,
|
|
6515
6896
|
propertyCallbacks: void 0
|
|
6516
6897
|
};
|
|
6517
|
-
const registryCollection = this.registry
|
|
6898
|
+
const registryCollection = this.registry?.getCollectionByPath(path2);
|
|
6518
6899
|
const resolvedCollection = registryCollection ? {
|
|
6519
6900
|
...collection,
|
|
6520
6901
|
...registryCollection
|
|
@@ -6540,7 +6921,8 @@ class PostgresBackendDriver {
|
|
|
6540
6921
|
startAfter,
|
|
6541
6922
|
orderBy,
|
|
6542
6923
|
searchString,
|
|
6543
|
-
order
|
|
6924
|
+
order,
|
|
6925
|
+
vectorSearch
|
|
6544
6926
|
}) {
|
|
6545
6927
|
const entities = await this.entityService.fetchCollection(path2, {
|
|
6546
6928
|
filter,
|
|
@@ -6550,7 +6932,8 @@ class PostgresBackendDriver {
|
|
|
6550
6932
|
offset,
|
|
6551
6933
|
startAfter,
|
|
6552
6934
|
databaseId: collection?.databaseId,
|
|
6553
|
-
searchString
|
|
6935
|
+
searchString,
|
|
6936
|
+
vectorSearch
|
|
6554
6937
|
});
|
|
6555
6938
|
const {
|
|
6556
6939
|
collection: resolvedCollection,
|
|
@@ -6562,7 +6945,8 @@ class PostgresBackendDriver {
|
|
|
6562
6945
|
user: this.user,
|
|
6563
6946
|
driver: this,
|
|
6564
6947
|
data: this.data,
|
|
6565
|
-
client: this.client
|
|
6948
|
+
client: this.client,
|
|
6949
|
+
storageSource: this.client?.storage
|
|
6566
6950
|
};
|
|
6567
6951
|
return Promise.all(entities.map(async (entity) => {
|
|
6568
6952
|
let fetched = entity;
|
|
@@ -6657,7 +7041,8 @@ class PostgresBackendDriver {
|
|
|
6657
7041
|
user: this.user,
|
|
6658
7042
|
driver: this,
|
|
6659
7043
|
data: this.data,
|
|
6660
|
-
client: this.client
|
|
7044
|
+
client: this.client,
|
|
7045
|
+
storageSource: this.client?.storage
|
|
6661
7046
|
};
|
|
6662
7047
|
if (callbacks?.afterRead) {
|
|
6663
7048
|
entity = await callbacks.afterRead({
|
|
@@ -6727,7 +7112,8 @@ class PostgresBackendDriver {
|
|
|
6727
7112
|
user: this.user,
|
|
6728
7113
|
driver: this,
|
|
6729
7114
|
data: this.data,
|
|
6730
|
-
client: this.client
|
|
7115
|
+
client: this.client,
|
|
7116
|
+
storageSource: this.client?.storage
|
|
6731
7117
|
};
|
|
6732
7118
|
let previousValuesForHistory;
|
|
6733
7119
|
if (status === "existing" && entityId) {
|
|
@@ -6876,7 +7262,8 @@ class PostgresBackendDriver {
|
|
|
6876
7262
|
user: this.user,
|
|
6877
7263
|
driver: this,
|
|
6878
7264
|
data: this.data,
|
|
6879
|
-
client: this.client
|
|
7265
|
+
client: this.client,
|
|
7266
|
+
storageSource: this.client?.storage
|
|
6880
7267
|
};
|
|
6881
7268
|
if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
6882
7269
|
let preventDefault = false;
|
|
@@ -7389,6 +7776,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7389
7776
|
length: 255
|
|
7390
7777
|
}),
|
|
7391
7778
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
7779
|
+
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
7392
7780
|
metadata: jsonb("metadata").$type().default({}).notNull(),
|
|
7393
7781
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7394
7782
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -7403,8 +7791,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7403
7791
|
}).notNull(),
|
|
7404
7792
|
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
7405
7793
|
defaultPermissions: jsonb("default_permissions").$type(),
|
|
7406
|
-
collectionPermissions: jsonb("collection_permissions").$type()
|
|
7407
|
-
config: jsonb("config").$type()
|
|
7794
|
+
collectionPermissions: jsonb("collection_permissions").$type()
|
|
7408
7795
|
});
|
|
7409
7796
|
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7410
7797
|
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
@@ -7476,6 +7863,48 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7476
7863
|
}, (table) => ({
|
|
7477
7864
|
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7478
7865
|
}));
|
|
7866
|
+
const mfaFactors2 = rolesTableCreator("mfa_factors", {
|
|
7867
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7868
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7869
|
+
onDelete: "cascade"
|
|
7870
|
+
}),
|
|
7871
|
+
factorType: varchar("factor_type", {
|
|
7872
|
+
length: 20
|
|
7873
|
+
}).notNull(),
|
|
7874
|
+
// 'totp'
|
|
7875
|
+
secretEncrypted: varchar("secret_encrypted", {
|
|
7876
|
+
length: 500
|
|
7877
|
+
}).notNull(),
|
|
7878
|
+
friendlyName: varchar("friendly_name", {
|
|
7879
|
+
length: 255
|
|
7880
|
+
}),
|
|
7881
|
+
verified: boolean("verified").default(false).notNull(),
|
|
7882
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7883
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7884
|
+
});
|
|
7885
|
+
const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
|
|
7886
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7887
|
+
factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
|
|
7888
|
+
onDelete: "cascade"
|
|
7889
|
+
}),
|
|
7890
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7891
|
+
verifiedAt: timestamp("verified_at"),
|
|
7892
|
+
ipAddress: varchar("ip_address", {
|
|
7893
|
+
length: 45
|
|
7894
|
+
}),
|
|
7895
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
7896
|
+
});
|
|
7897
|
+
const recoveryCodes2 = rolesTableCreator("recovery_codes", {
|
|
7898
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7899
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7900
|
+
onDelete: "cascade"
|
|
7901
|
+
}),
|
|
7902
|
+
codeHash: varchar("code_hash", {
|
|
7903
|
+
length: 255
|
|
7904
|
+
}).notNull(),
|
|
7905
|
+
usedAt: timestamp("used_at"),
|
|
7906
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7907
|
+
});
|
|
7479
7908
|
return {
|
|
7480
7909
|
rolesSchema,
|
|
7481
7910
|
usersSchema: usersSchema2,
|
|
@@ -7485,7 +7914,10 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7485
7914
|
refreshTokens: refreshTokens2,
|
|
7486
7915
|
passwordResetTokens: passwordResetTokens2,
|
|
7487
7916
|
appConfig: appConfig2,
|
|
7488
|
-
userIdentities: userIdentities2
|
|
7917
|
+
userIdentities: userIdentities2,
|
|
7918
|
+
mfaFactors: mfaFactors2,
|
|
7919
|
+
mfaChallenges: mfaChallenges2,
|
|
7920
|
+
recoveryCodes: recoveryCodes2
|
|
7489
7921
|
};
|
|
7490
7922
|
}
|
|
7491
7923
|
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
@@ -7498,13 +7930,18 @@ const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
|
7498
7930
|
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7499
7931
|
const appConfig = defaultAuthSchema.appConfig;
|
|
7500
7932
|
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7933
|
+
const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
7934
|
+
const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
7935
|
+
const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
7501
7936
|
const usersRelations = relations(users, ({
|
|
7502
7937
|
many
|
|
7503
7938
|
}) => ({
|
|
7504
7939
|
userRoles: many(userRoles),
|
|
7505
7940
|
refreshTokens: many(refreshTokens),
|
|
7506
7941
|
passwordResetTokens: many(passwordResetTokens),
|
|
7507
|
-
userIdentities: many(userIdentities)
|
|
7942
|
+
userIdentities: many(userIdentities),
|
|
7943
|
+
mfaFactors: many(mfaFactors),
|
|
7944
|
+
recoveryCodes: many(recoveryCodes)
|
|
7508
7945
|
}));
|
|
7509
7946
|
const rolesRelations = relations(roles, ({
|
|
7510
7947
|
many
|
|
@@ -7547,6 +7984,32 @@ const userIdentitiesRelations = relations(userIdentities, ({
|
|
|
7547
7984
|
references: [users.id]
|
|
7548
7985
|
})
|
|
7549
7986
|
}));
|
|
7987
|
+
const mfaFactorsRelations = relations(mfaFactors, ({
|
|
7988
|
+
one,
|
|
7989
|
+
many
|
|
7990
|
+
}) => ({
|
|
7991
|
+
user: one(users, {
|
|
7992
|
+
fields: [mfaFactors.userId],
|
|
7993
|
+
references: [users.id]
|
|
7994
|
+
}),
|
|
7995
|
+
challenges: many(mfaChallenges)
|
|
7996
|
+
}));
|
|
7997
|
+
const mfaChallengesRelations = relations(mfaChallenges, ({
|
|
7998
|
+
one
|
|
7999
|
+
}) => ({
|
|
8000
|
+
factor: one(mfaFactors, {
|
|
8001
|
+
fields: [mfaChallenges.factorId],
|
|
8002
|
+
references: [mfaFactors.id]
|
|
8003
|
+
})
|
|
8004
|
+
}));
|
|
8005
|
+
const recoveryCodesRelations = relations(recoveryCodes, ({
|
|
8006
|
+
one
|
|
8007
|
+
}) => ({
|
|
8008
|
+
user: one(users, {
|
|
8009
|
+
fields: [recoveryCodes.userId],
|
|
8010
|
+
references: [users.id]
|
|
8011
|
+
})
|
|
8012
|
+
}));
|
|
7550
8013
|
const resolveColumnName = (propName, prop) => {
|
|
7551
8014
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
7552
8015
|
return prop.columnName;
|
|
@@ -8071,167 +8534,84 @@ const generateSchema = async (collections, stripPolicies = false) => {
|
|
|
8071
8534
|
fields: [${tableVarName}.${rel.localKey}],
|
|
8072
8535
|
references: [${targetTableVar}.${getPrimaryKeyName(target)}],
|
|
8073
8536
|
relationName: "${drizzleRelationName}"
|
|
8074
|
-
})`);
|
|
8075
|
-
} else if (rel.direction === "inverse") {
|
|
8076
|
-
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8077
|
-
relationName: "${drizzleRelationName}"
|
|
8078
|
-
})`);
|
|
8079
|
-
}
|
|
8080
|
-
} else if (rel.cardinality === "many") {
|
|
8081
|
-
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8082
|
-
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8083
|
-
} else if (rel.through) {
|
|
8084
|
-
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8085
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8086
|
-
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8087
|
-
try {
|
|
8088
|
-
const targetCollection = rel.target();
|
|
8089
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8090
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8091
|
-
if (correspondingRelation && correspondingRelation.through) {
|
|
8092
|
-
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8093
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8094
|
-
} else {
|
|
8095
|
-
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8096
|
-
}
|
|
8097
|
-
} catch (e) {
|
|
8098
|
-
console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
|
|
8099
|
-
}
|
|
8100
|
-
}
|
|
8101
|
-
}
|
|
8102
|
-
} catch (e) {
|
|
8103
|
-
console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
|
|
8104
|
-
}
|
|
8105
|
-
}
|
|
8106
|
-
for (const otherCollection of collections) {
|
|
8107
|
-
if (otherCollection.slug === collection.slug) continue;
|
|
8108
|
-
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
8109
|
-
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
8110
|
-
if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
|
|
8111
|
-
try {
|
|
8112
|
-
const otherTarget = otherRel.target();
|
|
8113
|
-
if (otherTarget.slug === collection.slug) {
|
|
8114
|
-
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
8115
|
-
const deduplicationKey = `${drizzleRelationName}::owning`;
|
|
8116
|
-
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
8117
|
-
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
8118
|
-
const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
|
|
8119
|
-
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
|
|
8120
|
-
fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
|
|
8121
|
-
references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
|
|
8122
|
-
relationName: "${drizzleRelationName}"
|
|
8123
|
-
})`);
|
|
8124
|
-
emittedRelationNames.add(deduplicationKey);
|
|
8125
|
-
}
|
|
8126
|
-
}
|
|
8127
|
-
} catch (e) {
|
|
8128
|
-
}
|
|
8129
|
-
}
|
|
8130
|
-
}
|
|
8131
|
-
}
|
|
8132
|
-
}
|
|
8133
|
-
if (tableRelations.length > 0) {
|
|
8134
|
-
const relVarName = `${tableVarName}Relations`;
|
|
8135
|
-
schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
|
|
8136
|
-
${tableRelations.join(",\n")}
|
|
8137
|
-
}));
|
|
8138
|
-
|
|
8139
|
-
`;
|
|
8140
|
-
if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
|
|
8141
|
-
}
|
|
8142
|
-
}
|
|
8143
|
-
const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
|
|
8144
|
-
`;
|
|
8145
|
-
const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
|
|
8146
|
-
`;
|
|
8147
|
-
const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
|
|
8148
|
-
|
|
8149
|
-
`;
|
|
8150
|
-
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8151
|
-
return schemaContent;
|
|
8152
|
-
};
|
|
8153
|
-
const defaultUsersCollection = {
|
|
8154
|
-
name: "Users",
|
|
8155
|
-
singularName: "User",
|
|
8156
|
-
slug: "users",
|
|
8157
|
-
table: "users",
|
|
8158
|
-
icon: "Users",
|
|
8159
|
-
group: "Settings",
|
|
8160
|
-
properties: {
|
|
8161
|
-
id: {
|
|
8162
|
-
name: "ID",
|
|
8163
|
-
type: "string",
|
|
8164
|
-
isId: "uuid"
|
|
8165
|
-
},
|
|
8166
|
-
email: {
|
|
8167
|
-
name: "Email",
|
|
8168
|
-
type: "string",
|
|
8169
|
-
validation: {
|
|
8170
|
-
required: true,
|
|
8171
|
-
unique: true
|
|
8172
|
-
}
|
|
8173
|
-
},
|
|
8174
|
-
password_hash: {
|
|
8175
|
-
name: "Password Hash",
|
|
8176
|
-
type: "string",
|
|
8177
|
-
ui: {
|
|
8178
|
-
hideFromCollection: true
|
|
8179
|
-
}
|
|
8180
|
-
},
|
|
8181
|
-
display_name: {
|
|
8182
|
-
name: "Display Name",
|
|
8183
|
-
type: "string"
|
|
8184
|
-
},
|
|
8185
|
-
photo_url: {
|
|
8186
|
-
name: "Photo URL",
|
|
8187
|
-
type: "string"
|
|
8188
|
-
},
|
|
8189
|
-
email_verified: {
|
|
8190
|
-
name: "Email Verified",
|
|
8191
|
-
type: "boolean",
|
|
8192
|
-
defaultValue: false
|
|
8193
|
-
},
|
|
8194
|
-
email_verification_token: {
|
|
8195
|
-
name: "Email Verification Token",
|
|
8196
|
-
type: "string",
|
|
8197
|
-
ui: {
|
|
8198
|
-
hideFromCollection: true
|
|
8199
|
-
}
|
|
8200
|
-
},
|
|
8201
|
-
email_verification_sent_at: {
|
|
8202
|
-
name: "Email Verification Sent At",
|
|
8203
|
-
type: "date",
|
|
8204
|
-
ui: {
|
|
8205
|
-
hideFromCollection: true
|
|
8206
|
-
}
|
|
8207
|
-
},
|
|
8208
|
-
metadata: {
|
|
8209
|
-
name: "Metadata",
|
|
8210
|
-
type: "map",
|
|
8211
|
-
defaultValue: {},
|
|
8212
|
-
ui: {
|
|
8213
|
-
hideFromCollection: true
|
|
8214
|
-
}
|
|
8215
|
-
},
|
|
8216
|
-
created_at: {
|
|
8217
|
-
name: "Created At",
|
|
8218
|
-
type: "date",
|
|
8219
|
-
autoValue: "on_create",
|
|
8220
|
-
ui: {
|
|
8221
|
-
readOnly: true,
|
|
8222
|
-
hideFromCollection: true
|
|
8537
|
+
})`);
|
|
8538
|
+
} else if (rel.direction === "inverse") {
|
|
8539
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8540
|
+
relationName: "${drizzleRelationName}"
|
|
8541
|
+
})`);
|
|
8542
|
+
}
|
|
8543
|
+
} else if (rel.cardinality === "many") {
|
|
8544
|
+
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8545
|
+
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8546
|
+
} else if (rel.through) {
|
|
8547
|
+
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8548
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8549
|
+
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8550
|
+
try {
|
|
8551
|
+
const targetCollection = rel.target();
|
|
8552
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8553
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8554
|
+
if (correspondingRelation && correspondingRelation.through) {
|
|
8555
|
+
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8556
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8557
|
+
} else {
|
|
8558
|
+
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8559
|
+
}
|
|
8560
|
+
} catch (e) {
|
|
8561
|
+
console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
}
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
|
|
8567
|
+
}
|
|
8223
8568
|
}
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8569
|
+
for (const otherCollection of collections) {
|
|
8570
|
+
if (otherCollection.slug === collection.slug) continue;
|
|
8571
|
+
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
8572
|
+
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
8573
|
+
if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
|
|
8574
|
+
try {
|
|
8575
|
+
const otherTarget = otherRel.target();
|
|
8576
|
+
if (otherTarget.slug === collection.slug) {
|
|
8577
|
+
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
8578
|
+
const deduplicationKey = `${drizzleRelationName}::owning`;
|
|
8579
|
+
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
8580
|
+
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
8581
|
+
const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
|
|
8582
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {
|
|
8583
|
+
fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],
|
|
8584
|
+
references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],
|
|
8585
|
+
relationName: "${drizzleRelationName}"
|
|
8586
|
+
})`);
|
|
8587
|
+
emittedRelationNames.add(deduplicationKey);
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
} catch (e) {
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8232
8594
|
}
|
|
8233
8595
|
}
|
|
8596
|
+
if (tableRelations.length > 0) {
|
|
8597
|
+
const relVarName = `${tableVarName}Relations`;
|
|
8598
|
+
schemaContent += `export const ${relVarName} = drizzleRelations(${tableVarName}, ({ one, many }) => ({
|
|
8599
|
+
${tableRelations.join(",\n")}
|
|
8600
|
+
}));
|
|
8601
|
+
|
|
8602
|
+
`;
|
|
8603
|
+
if (!exportedRelationVars.includes(relVarName)) exportedRelationVars.push(relVarName);
|
|
8604
|
+
}
|
|
8234
8605
|
}
|
|
8606
|
+
const tablesExport = `export const tables = { ${exportedTableVars.join(", ")} };
|
|
8607
|
+
`;
|
|
8608
|
+
const enumsExport = `export const enums = { ${exportedEnumVars.join(", ")} };
|
|
8609
|
+
`;
|
|
8610
|
+
const relationsExport = `export const relations = { ${exportedRelationVars.join(", ")} };
|
|
8611
|
+
|
|
8612
|
+
`;
|
|
8613
|
+
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8614
|
+
return schemaContent;
|
|
8235
8615
|
};
|
|
8236
8616
|
const formatTerminalText = (text, options = {}) => {
|
|
8237
8617
|
let codes = "";
|
|
@@ -8298,10 +8678,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
|
|
|
8298
8678
|
if (!collections || !Array.isArray(collections)) {
|
|
8299
8679
|
collections = [];
|
|
8300
8680
|
}
|
|
8301
|
-
|
|
8302
|
-
if (!hasUsersCollection) {
|
|
8303
|
-
collections.push(defaultUsersCollection);
|
|
8304
|
-
}
|
|
8681
|
+
collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
|
|
8305
8682
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
8306
8683
|
const schemaContent = await generateSchema(collections);
|
|
8307
8684
|
if (outputPath) {
|
|
@@ -8362,6 +8739,13 @@ class RealtimeService extends EventEmitter {
|
|
|
8362
8739
|
this.entityService = new EntityService(db, registry);
|
|
8363
8740
|
}
|
|
8364
8741
|
clients = /* @__PURE__ */ new Map();
|
|
8742
|
+
// Broadcast channels: channel name → set of client IDs
|
|
8743
|
+
channels = /* @__PURE__ */ new Map();
|
|
8744
|
+
// Presence: channel → Map<clientId, { state, lastSeen }>
|
|
8745
|
+
presence = /* @__PURE__ */ new Map();
|
|
8746
|
+
presenceInterval;
|
|
8747
|
+
static PRESENCE_TIMEOUT_MS = 3e4;
|
|
8748
|
+
// 30s
|
|
8365
8749
|
entityService;
|
|
8366
8750
|
// Enhanced subscriptions storage with full request parameters
|
|
8367
8751
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -8488,8 +8872,19 @@ class RealtimeService extends EventEmitter {
|
|
|
8488
8872
|
}
|
|
8489
8873
|
}
|
|
8490
8874
|
}
|
|
8875
|
+
for (const [channel, members] of this.channels.entries()) {
|
|
8876
|
+
if (members.has(clientId)) {
|
|
8877
|
+
members.delete(clientId);
|
|
8878
|
+
this.removePresence(clientId, channel);
|
|
8879
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
for (const [channel] of this.presence) {
|
|
8883
|
+
this.removePresence(clientId, channel);
|
|
8884
|
+
}
|
|
8491
8885
|
}
|
|
8492
8886
|
async handleMessage(clientId, message, authContext) {
|
|
8887
|
+
const payload = message.payload;
|
|
8493
8888
|
switch (message.type) {
|
|
8494
8889
|
case "subscribe_collection":
|
|
8495
8890
|
await this.handleCollectionSubscription(clientId, message.payload, authContext);
|
|
@@ -8500,6 +8895,25 @@ class RealtimeService extends EventEmitter {
|
|
|
8500
8895
|
case "unsubscribe":
|
|
8501
8896
|
await this.handleUnsubscribe(clientId, message.subscriptionId);
|
|
8502
8897
|
break;
|
|
8898
|
+
case "join_channel":
|
|
8899
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8900
|
+
break;
|
|
8901
|
+
case "leave_channel":
|
|
8902
|
+
this.leaveChannel(clientId, payload?.channel);
|
|
8903
|
+
break;
|
|
8904
|
+
case "broadcast":
|
|
8905
|
+
this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
|
|
8906
|
+
break;
|
|
8907
|
+
case "presence_track":
|
|
8908
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8909
|
+
this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
|
|
8910
|
+
break;
|
|
8911
|
+
case "presence_untrack":
|
|
8912
|
+
this.removePresence(clientId, payload?.channel);
|
|
8913
|
+
break;
|
|
8914
|
+
case "presence_state":
|
|
8915
|
+
this.sendPresenceState(clientId, payload?.channel);
|
|
8916
|
+
break;
|
|
8503
8917
|
default:
|
|
8504
8918
|
this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
|
|
8505
8919
|
}
|
|
@@ -8957,6 +9371,132 @@ class RealtimeService extends EventEmitter {
|
|
|
8957
9371
|
return parentPaths;
|
|
8958
9372
|
}
|
|
8959
9373
|
// =============================================================================
|
|
9374
|
+
// Broadcast Channels
|
|
9375
|
+
// =============================================================================
|
|
9376
|
+
/** Join a broadcast channel */
|
|
9377
|
+
joinChannel(clientId, channel) {
|
|
9378
|
+
if (!this.channels.has(channel)) {
|
|
9379
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
9380
|
+
}
|
|
9381
|
+
this.channels.get(channel).add(clientId);
|
|
9382
|
+
this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
|
|
9383
|
+
}
|
|
9384
|
+
/** Leave a broadcast channel */
|
|
9385
|
+
leaveChannel(clientId, channel) {
|
|
9386
|
+
const members = this.channels.get(channel);
|
|
9387
|
+
if (members) {
|
|
9388
|
+
members.delete(clientId);
|
|
9389
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
9390
|
+
}
|
|
9391
|
+
this.removePresence(clientId, channel);
|
|
9392
|
+
}
|
|
9393
|
+
/** Broadcast a message to all clients in a channel except sender */
|
|
9394
|
+
broadcastToChannel(clientId, channel, event, payload) {
|
|
9395
|
+
const members = this.channels.get(channel);
|
|
9396
|
+
if (!members) return;
|
|
9397
|
+
const message = JSON.stringify({
|
|
9398
|
+
type: "broadcast",
|
|
9399
|
+
channel,
|
|
9400
|
+
event,
|
|
9401
|
+
payload
|
|
9402
|
+
});
|
|
9403
|
+
for (const memberId of members) {
|
|
9404
|
+
if (memberId === clientId) continue;
|
|
9405
|
+
const ws = this.clients.get(memberId);
|
|
9406
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9407
|
+
ws.send(message);
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
}
|
|
9411
|
+
// =============================================================================
|
|
9412
|
+
// Presence
|
|
9413
|
+
// =============================================================================
|
|
9414
|
+
/** Track presence in a channel */
|
|
9415
|
+
trackPresence(clientId, channel, state) {
|
|
9416
|
+
if (!this.presence.has(channel)) {
|
|
9417
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
9418
|
+
}
|
|
9419
|
+
const channelPresence = this.presence.get(channel);
|
|
9420
|
+
channelPresence.set(clientId, {
|
|
9421
|
+
state,
|
|
9422
|
+
lastSeen: Date.now()
|
|
9423
|
+
});
|
|
9424
|
+
this.broadcastPresenceDiff(channel, {
|
|
9425
|
+
[clientId]: state
|
|
9426
|
+
}, {});
|
|
9427
|
+
this.ensurePresenceCleanup();
|
|
9428
|
+
}
|
|
9429
|
+
/** Remove presence from a channel */
|
|
9430
|
+
removePresence(clientId, channel) {
|
|
9431
|
+
const channelPresence = this.presence.get(channel);
|
|
9432
|
+
if (!channelPresence) return;
|
|
9433
|
+
const entry = channelPresence.get(clientId);
|
|
9434
|
+
if (entry) {
|
|
9435
|
+
channelPresence.delete(clientId);
|
|
9436
|
+
this.broadcastPresenceDiff(channel, {}, {
|
|
9437
|
+
[clientId]: entry.state
|
|
9438
|
+
});
|
|
9439
|
+
}
|
|
9440
|
+
if (channelPresence.size === 0) {
|
|
9441
|
+
this.presence.delete(channel);
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
/** Send full presence state to a specific client */
|
|
9445
|
+
sendPresenceState(clientId, channel) {
|
|
9446
|
+
const channelPresence = this.presence.get(channel);
|
|
9447
|
+
const presences = {};
|
|
9448
|
+
if (channelPresence) {
|
|
9449
|
+
for (const [id, {
|
|
9450
|
+
state
|
|
9451
|
+
}] of channelPresence) {
|
|
9452
|
+
presences[id] = state;
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
const ws = this.clients.get(clientId);
|
|
9456
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9457
|
+
ws.send(JSON.stringify({
|
|
9458
|
+
type: "presence_state",
|
|
9459
|
+
channel,
|
|
9460
|
+
presences
|
|
9461
|
+
}));
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
/** Broadcast presence diff (joins/leaves) to channel */
|
|
9465
|
+
broadcastPresenceDiff(channel, joins, leaves) {
|
|
9466
|
+
const members = this.channels.get(channel);
|
|
9467
|
+
if (!members) return;
|
|
9468
|
+
const message = JSON.stringify({
|
|
9469
|
+
type: "presence_diff",
|
|
9470
|
+
channel,
|
|
9471
|
+
joins,
|
|
9472
|
+
leaves
|
|
9473
|
+
});
|
|
9474
|
+
for (const memberId of members) {
|
|
9475
|
+
const ws = this.clients.get(memberId);
|
|
9476
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9477
|
+
ws.send(message);
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
/** Periodic cleanup for stale presences */
|
|
9482
|
+
ensurePresenceCleanup() {
|
|
9483
|
+
if (this.presenceInterval) return;
|
|
9484
|
+
this.presenceInterval = setInterval(() => {
|
|
9485
|
+
const now = Date.now();
|
|
9486
|
+
for (const [channel, channelPresence] of this.presence) {
|
|
9487
|
+
for (const [clientId, entry] of channelPresence) {
|
|
9488
|
+
if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
|
|
9489
|
+
this.removePresence(clientId, channel);
|
|
9490
|
+
}
|
|
9491
|
+
}
|
|
9492
|
+
}
|
|
9493
|
+
if (this.presence.size === 0 && this.presenceInterval) {
|
|
9494
|
+
clearInterval(this.presenceInterval);
|
|
9495
|
+
this.presenceInterval = void 0;
|
|
9496
|
+
}
|
|
9497
|
+
}, 1e4);
|
|
9498
|
+
}
|
|
9499
|
+
// =============================================================================
|
|
8960
9500
|
// Lifecycle / Cleanup
|
|
8961
9501
|
// =============================================================================
|
|
8962
9502
|
/**
|
|
@@ -8977,6 +9517,12 @@ class RealtimeService extends EventEmitter {
|
|
|
8977
9517
|
}
|
|
8978
9518
|
this._subscriptions.clear();
|
|
8979
9519
|
this.subscriptionCallbacks.clear();
|
|
9520
|
+
this.channels.clear();
|
|
9521
|
+
this.presence.clear();
|
|
9522
|
+
if (this.presenceInterval) {
|
|
9523
|
+
clearInterval(this.presenceInterval);
|
|
9524
|
+
this.presenceInterval = void 0;
|
|
9525
|
+
}
|
|
8980
9526
|
await this.stopListening();
|
|
8981
9527
|
this.clients.clear();
|
|
8982
9528
|
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
@@ -9623,8 +10169,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
9623
10169
|
break;
|
|
9624
10170
|
case "subscribe_collection":
|
|
9625
10171
|
case "subscribe_entity":
|
|
9626
|
-
case "unsubscribe":
|
|
9627
|
-
|
|
10172
|
+
case "unsubscribe":
|
|
10173
|
+
case "join_channel":
|
|
10174
|
+
case "leave_channel":
|
|
10175
|
+
case "broadcast":
|
|
10176
|
+
case "presence_track":
|
|
10177
|
+
case "presence_untrack":
|
|
10178
|
+
case "presence_state": {
|
|
10179
|
+
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
9628
10180
|
const session = clientSessions.get(clientId);
|
|
9629
10181
|
const authContext = session?.user ? {
|
|
9630
10182
|
userId: session.user.userId,
|
|
@@ -9743,11 +10295,6 @@ const DEFAULT_ROLES = [{
|
|
|
9743
10295
|
create: true,
|
|
9744
10296
|
edit: true,
|
|
9745
10297
|
delete: true
|
|
9746
|
-
},
|
|
9747
|
-
config: {
|
|
9748
|
-
createCollections: true,
|
|
9749
|
-
editCollections: "all",
|
|
9750
|
-
deleteCollections: "all"
|
|
9751
10298
|
}
|
|
9752
10299
|
}, {
|
|
9753
10300
|
id: "editor",
|
|
@@ -9758,11 +10305,6 @@ const DEFAULT_ROLES = [{
|
|
|
9758
10305
|
create: true,
|
|
9759
10306
|
edit: true,
|
|
9760
10307
|
delete: true
|
|
9761
|
-
},
|
|
9762
|
-
config: {
|
|
9763
|
-
createCollections: true,
|
|
9764
|
-
editCollections: "own",
|
|
9765
|
-
deleteCollections: "own"
|
|
9766
10308
|
}
|
|
9767
10309
|
}, {
|
|
9768
10310
|
id: "viewer",
|
|
@@ -9773,11 +10315,10 @@ const DEFAULT_ROLES = [{
|
|
|
9773
10315
|
create: false,
|
|
9774
10316
|
edit: false,
|
|
9775
10317
|
delete: false
|
|
9776
|
-
}
|
|
9777
|
-
config: null
|
|
10318
|
+
}
|
|
9778
10319
|
}];
|
|
9779
10320
|
async function ensureAuthTablesExist(db, registry) {
|
|
9780
|
-
|
|
10321
|
+
logger.info("🔍 Checking auth tables...");
|
|
9781
10322
|
try {
|
|
9782
10323
|
let usersTableName = '"users"';
|
|
9783
10324
|
let userIdType = "TEXT";
|
|
@@ -9847,7 +10388,6 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
9847
10388
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
9848
10389
|
default_permissions JSONB,
|
|
9849
10390
|
collection_permissions JSONB,
|
|
9850
|
-
config JSONB,
|
|
9851
10391
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9852
10392
|
)
|
|
9853
10393
|
`);
|
|
@@ -9930,34 +10470,85 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
9930
10470
|
`);
|
|
9931
10471
|
});
|
|
9932
10472
|
await seedDefaultRoles(db, rolesTableName);
|
|
9933
|
-
|
|
10473
|
+
await db.execute(sql`
|
|
10474
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10475
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
10476
|
+
`);
|
|
10477
|
+
const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
|
|
10478
|
+
const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
|
|
10479
|
+
const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
|
|
10480
|
+
await db.execute(sql`
|
|
10481
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
|
|
10482
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10483
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10484
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
10485
|
+
secret_encrypted TEXT NOT NULL,
|
|
10486
|
+
friendly_name TEXT,
|
|
10487
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
10488
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10489
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10490
|
+
)
|
|
10491
|
+
`);
|
|
10492
|
+
await db.execute(sql`
|
|
10493
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
10494
|
+
ON ${sql.raw(mfaFactorsTableName)}(user_id)
|
|
10495
|
+
`);
|
|
10496
|
+
await db.execute(sql`
|
|
10497
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
|
|
10498
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10499
|
+
factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
10500
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10501
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
10502
|
+
ip_address TEXT,
|
|
10503
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
10504
|
+
)
|
|
10505
|
+
`);
|
|
10506
|
+
await db.execute(sql`
|
|
10507
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
10508
|
+
ON ${sql.raw(mfaChallengesTableName)}(factor_id)
|
|
10509
|
+
`);
|
|
10510
|
+
await db.execute(sql`
|
|
10511
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
|
|
10512
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10513
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10514
|
+
code_hash TEXT NOT NULL,
|
|
10515
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
10516
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10517
|
+
)
|
|
10518
|
+
`);
|
|
10519
|
+
await db.execute(sql`
|
|
10520
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
10521
|
+
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
10522
|
+
`);
|
|
10523
|
+
logger.info("✅ Auth tables ready");
|
|
9934
10524
|
} catch (error) {
|
|
9935
|
-
|
|
9936
|
-
|
|
10525
|
+
logger.error("❌ Failed to create auth tables", {
|
|
10526
|
+
error
|
|
10527
|
+
});
|
|
10528
|
+
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
9937
10529
|
}
|
|
9938
10530
|
}
|
|
9939
10531
|
async function seedDefaultRoles(db, rolesTableName) {
|
|
9940
10532
|
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
9941
10533
|
const count2 = parseInt(result.rows[0]?.count || "0", 10);
|
|
9942
10534
|
if (count2 > 0) {
|
|
9943
|
-
|
|
10535
|
+
logger.info(`📋 Found ${count2} existing roles`);
|
|
9944
10536
|
return;
|
|
9945
10537
|
}
|
|
9946
|
-
|
|
10538
|
+
logger.info("🌱 Seeding default roles...");
|
|
9947
10539
|
for (const role of DEFAULT_ROLES) {
|
|
9948
10540
|
await db.execute(sql`
|
|
9949
|
-
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions
|
|
10541
|
+
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
9950
10542
|
VALUES (
|
|
9951
10543
|
${role.id},
|
|
9952
10544
|
${role.name},
|
|
9953
10545
|
${role.is_admin},
|
|
9954
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
9955
|
-
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
10546
|
+
${JSON.stringify(role.default_permissions)}::jsonb
|
|
9956
10547
|
)
|
|
9957
10548
|
ON CONFLICT (id) DO NOTHING
|
|
9958
10549
|
`);
|
|
9959
10550
|
}
|
|
9960
|
-
|
|
10551
|
+
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
9961
10552
|
}
|
|
9962
10553
|
function getColumnKey(table, ...keys2) {
|
|
9963
10554
|
if (!table) return void 0;
|
|
@@ -10011,12 +10602,13 @@ class UserService {
|
|
|
10011
10602
|
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10012
10603
|
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10013
10604
|
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10605
|
+
const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
|
|
10014
10606
|
const createdAt = row.created_at ?? row.createdAt;
|
|
10015
10607
|
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10016
10608
|
const metadata = {
|
|
10017
10609
|
...row.metadata || {}
|
|
10018
10610
|
};
|
|
10019
|
-
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10611
|
+
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10020
10612
|
for (const [key, val] of Object.entries(row)) {
|
|
10021
10613
|
if (!knownKeys.has(key)) {
|
|
10022
10614
|
const camelKey = camelCase(key);
|
|
@@ -10032,6 +10624,7 @@ class UserService {
|
|
|
10032
10624
|
emailVerified,
|
|
10033
10625
|
emailVerificationToken,
|
|
10034
10626
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10627
|
+
isAnonymous,
|
|
10035
10628
|
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10036
10629
|
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10037
10630
|
metadata
|
|
@@ -10048,6 +10641,7 @@ class UserService {
|
|
|
10048
10641
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10049
10642
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10050
10643
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10644
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
10051
10645
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10052
10646
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10053
10647
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -10059,6 +10653,7 @@ class UserService {
|
|
|
10059
10653
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10060
10654
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10061
10655
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10656
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
10062
10657
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10063
10658
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10064
10659
|
const metadata = {
|
|
@@ -10067,7 +10662,7 @@ class UserService {
|
|
|
10067
10662
|
const remainingMetadata = {};
|
|
10068
10663
|
for (const [key, val] of Object.entries(metadata)) {
|
|
10069
10664
|
const tableColKey = getColumnKey(this.usersTable, key);
|
|
10070
|
-
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10665
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10071
10666
|
payload[tableColKey] = val;
|
|
10072
10667
|
} else {
|
|
10073
10668
|
remainingMetadata[key] = val;
|
|
@@ -10255,7 +10850,7 @@ class UserService {
|
|
|
10255
10850
|
async getUserRoles(userId) {
|
|
10256
10851
|
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
10257
10852
|
const result = await this.db.execute(sql`
|
|
10258
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10853
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
10259
10854
|
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10260
10855
|
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
10261
10856
|
WHERE ur.user_id = ${userId}
|
|
@@ -10265,8 +10860,7 @@ class UserService {
|
|
|
10265
10860
|
name: row.name,
|
|
10266
10861
|
isAdmin: row.is_admin,
|
|
10267
10862
|
defaultPermissions: row.default_permissions,
|
|
10268
|
-
collectionPermissions: row.collection_permissions
|
|
10269
|
-
config: row.config
|
|
10863
|
+
collectionPermissions: row.collection_permissions
|
|
10270
10864
|
}));
|
|
10271
10865
|
}
|
|
10272
10866
|
/**
|
|
@@ -10332,7 +10926,7 @@ class RoleService {
|
|
|
10332
10926
|
async getRoleById(id) {
|
|
10333
10927
|
const tableName = this.getQualifiedRolesTableName();
|
|
10334
10928
|
const result = await this.db.execute(sql`
|
|
10335
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10929
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10336
10930
|
FROM ${sql.raw(tableName)}
|
|
10337
10931
|
WHERE id = ${id}
|
|
10338
10932
|
`);
|
|
@@ -10343,14 +10937,13 @@ class RoleService {
|
|
|
10343
10937
|
name: row.name,
|
|
10344
10938
|
isAdmin: row.is_admin,
|
|
10345
10939
|
defaultPermissions: row.default_permissions,
|
|
10346
|
-
collectionPermissions: row.collection_permissions
|
|
10347
|
-
config: row.config
|
|
10940
|
+
collectionPermissions: row.collection_permissions
|
|
10348
10941
|
};
|
|
10349
10942
|
}
|
|
10350
10943
|
async listRoles() {
|
|
10351
10944
|
const tableName = this.getQualifiedRolesTableName();
|
|
10352
10945
|
const result = await this.db.execute(sql`
|
|
10353
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10946
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
10354
10947
|
FROM ${sql.raw(tableName)}
|
|
10355
10948
|
ORDER BY name
|
|
10356
10949
|
`);
|
|
@@ -10359,23 +10952,21 @@ class RoleService {
|
|
|
10359
10952
|
name: row.name,
|
|
10360
10953
|
isAdmin: row.is_admin,
|
|
10361
10954
|
defaultPermissions: row.default_permissions,
|
|
10362
|
-
collectionPermissions: row.collection_permissions
|
|
10363
|
-
config: row.config
|
|
10955
|
+
collectionPermissions: row.collection_permissions
|
|
10364
10956
|
}));
|
|
10365
10957
|
}
|
|
10366
10958
|
async createRole(data) {
|
|
10367
10959
|
const tableName = this.getQualifiedRolesTableName();
|
|
10368
10960
|
const result = await this.db.execute(sql`
|
|
10369
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
10961
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
10370
10962
|
VALUES (
|
|
10371
10963
|
${data.id},
|
|
10372
10964
|
${data.name},
|
|
10373
10965
|
${data.isAdmin ?? false},
|
|
10374
10966
|
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
10375
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10376
|
-
${data.config ? JSON.stringify(data.config) : null}::jsonb
|
|
10967
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
10377
10968
|
)
|
|
10378
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10969
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
10379
10970
|
`);
|
|
10380
10971
|
const row = result.rows[0];
|
|
10381
10972
|
return {
|
|
@@ -10383,8 +10974,7 @@ class RoleService {
|
|
|
10383
10974
|
name: row.name,
|
|
10384
10975
|
isAdmin: row.is_admin,
|
|
10385
10976
|
defaultPermissions: row.default_permissions,
|
|
10386
|
-
collectionPermissions: row.collection_permissions
|
|
10387
|
-
config: row.config
|
|
10977
|
+
collectionPermissions: row.collection_permissions
|
|
10388
10978
|
};
|
|
10389
10979
|
}
|
|
10390
10980
|
async updateRole(id, data) {
|
|
@@ -10397,8 +10987,7 @@ class RoleService {
|
|
|
10397
10987
|
name = ${data.name ?? existing.name},
|
|
10398
10988
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
10399
10989
|
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
10400
|
-
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10401
|
-
config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
|
|
10990
|
+
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
|
|
10402
10991
|
WHERE id = ${id}
|
|
10403
10992
|
`);
|
|
10404
10993
|
return this.getRoleById(id);
|
|
@@ -10677,8 +11266,7 @@ class PostgresAuthRepository {
|
|
|
10677
11266
|
return this.roleService.createRole({
|
|
10678
11267
|
...data,
|
|
10679
11268
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
10680
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
10681
|
-
config: data.config ?? null
|
|
11269
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
10682
11270
|
});
|
|
10683
11271
|
}
|
|
10684
11272
|
async updateRole(id, data) {
|
|
@@ -10721,6 +11309,219 @@ class PostgresAuthRepository {
|
|
|
10721
11309
|
async deleteExpiredTokens() {
|
|
10722
11310
|
await this.tokenRepository.deleteExpiredTokens();
|
|
10723
11311
|
}
|
|
11312
|
+
// MFA operations (delegate to MfaService)
|
|
11313
|
+
_mfaService = null;
|
|
11314
|
+
getMfaService() {
|
|
11315
|
+
if (!this._mfaService) {
|
|
11316
|
+
this._mfaService = new MfaService(this.db);
|
|
11317
|
+
}
|
|
11318
|
+
return this._mfaService;
|
|
11319
|
+
}
|
|
11320
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11321
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
11322
|
+
}
|
|
11323
|
+
async getMfaFactors(userId) {
|
|
11324
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
11325
|
+
}
|
|
11326
|
+
async getMfaFactorById(factorId) {
|
|
11327
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
11328
|
+
}
|
|
11329
|
+
async verifyMfaFactor(factorId) {
|
|
11330
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
11331
|
+
}
|
|
11332
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11333
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
11334
|
+
}
|
|
11335
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11336
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
11337
|
+
}
|
|
11338
|
+
async getMfaChallengeById(challengeId) {
|
|
11339
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
11340
|
+
}
|
|
11341
|
+
async verifyMfaChallenge(challengeId) {
|
|
11342
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
11343
|
+
}
|
|
11344
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11345
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
11346
|
+
}
|
|
11347
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11348
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
11349
|
+
}
|
|
11350
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11351
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
11352
|
+
}
|
|
11353
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11354
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
11355
|
+
}
|
|
11356
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11357
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
11358
|
+
}
|
|
11359
|
+
}
|
|
11360
|
+
class MfaService {
|
|
11361
|
+
constructor(db, schemaName = "rebase") {
|
|
11362
|
+
this.db = db;
|
|
11363
|
+
this.schemaName = schemaName;
|
|
11364
|
+
}
|
|
11365
|
+
qualify(tableName) {
|
|
11366
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
11367
|
+
}
|
|
11368
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11369
|
+
const tableName = this.qualify("mfa_factors");
|
|
11370
|
+
const result = await this.db.execute(sql`
|
|
11371
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
11372
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
11373
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11374
|
+
`);
|
|
11375
|
+
const row = result.rows[0];
|
|
11376
|
+
return {
|
|
11377
|
+
id: row.id,
|
|
11378
|
+
userId: row.user_id,
|
|
11379
|
+
factorType: row.factor_type,
|
|
11380
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11381
|
+
verified: row.verified,
|
|
11382
|
+
createdAt: new Date(row.created_at),
|
|
11383
|
+
updatedAt: new Date(row.updated_at)
|
|
11384
|
+
};
|
|
11385
|
+
}
|
|
11386
|
+
async getMfaFactors(userId) {
|
|
11387
|
+
const tableName = this.qualify("mfa_factors");
|
|
11388
|
+
const result = await this.db.execute(sql`
|
|
11389
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11390
|
+
FROM ${sql.raw(tableName)}
|
|
11391
|
+
WHERE user_id = ${userId}
|
|
11392
|
+
ORDER BY created_at
|
|
11393
|
+
`);
|
|
11394
|
+
return result.rows.map((row) => ({
|
|
11395
|
+
id: row.id,
|
|
11396
|
+
userId: row.user_id,
|
|
11397
|
+
factorType: row.factor_type,
|
|
11398
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11399
|
+
verified: row.verified,
|
|
11400
|
+
createdAt: new Date(row.created_at),
|
|
11401
|
+
updatedAt: new Date(row.updated_at)
|
|
11402
|
+
}));
|
|
11403
|
+
}
|
|
11404
|
+
async getMfaFactorById(factorId) {
|
|
11405
|
+
const tableName = this.qualify("mfa_factors");
|
|
11406
|
+
const result = await this.db.execute(sql`
|
|
11407
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
11408
|
+
FROM ${sql.raw(tableName)}
|
|
11409
|
+
WHERE id = ${factorId}
|
|
11410
|
+
`);
|
|
11411
|
+
if (result.rows.length === 0) return null;
|
|
11412
|
+
const row = result.rows[0];
|
|
11413
|
+
return {
|
|
11414
|
+
id: row.id,
|
|
11415
|
+
userId: row.user_id,
|
|
11416
|
+
factorType: row.factor_type,
|
|
11417
|
+
secretEncrypted: row.secret_encrypted,
|
|
11418
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11419
|
+
verified: row.verified,
|
|
11420
|
+
createdAt: new Date(row.created_at),
|
|
11421
|
+
updatedAt: new Date(row.updated_at)
|
|
11422
|
+
};
|
|
11423
|
+
}
|
|
11424
|
+
async verifyMfaFactor(factorId) {
|
|
11425
|
+
const tableName = this.qualify("mfa_factors");
|
|
11426
|
+
await this.db.execute(sql`
|
|
11427
|
+
UPDATE ${sql.raw(tableName)}
|
|
11428
|
+
SET verified = TRUE, updated_at = NOW()
|
|
11429
|
+
WHERE id = ${factorId}
|
|
11430
|
+
`);
|
|
11431
|
+
}
|
|
11432
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11433
|
+
const tableName = this.qualify("mfa_factors");
|
|
11434
|
+
await this.db.execute(sql`
|
|
11435
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
11436
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
11437
|
+
`);
|
|
11438
|
+
}
|
|
11439
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11440
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11441
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
|
|
11442
|
+
const result = await this.db.execute(sql`
|
|
11443
|
+
INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
11444
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
11445
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
11446
|
+
`);
|
|
11447
|
+
const row = result.rows[0];
|
|
11448
|
+
return {
|
|
11449
|
+
id: row.id,
|
|
11450
|
+
factorId: row.factor_id,
|
|
11451
|
+
createdAt: new Date(row.created_at),
|
|
11452
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11453
|
+
ipAddress: row.ip_address ?? void 0
|
|
11454
|
+
};
|
|
11455
|
+
}
|
|
11456
|
+
async getMfaChallengeById(challengeId) {
|
|
11457
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11458
|
+
const result = await this.db.execute(sql`
|
|
11459
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
11460
|
+
FROM ${sql.raw(tableName)}
|
|
11461
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
11462
|
+
`);
|
|
11463
|
+
if (result.rows.length === 0) return null;
|
|
11464
|
+
const row = result.rows[0];
|
|
11465
|
+
return {
|
|
11466
|
+
id: row.id,
|
|
11467
|
+
factorId: row.factor_id,
|
|
11468
|
+
createdAt: new Date(row.created_at),
|
|
11469
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11470
|
+
ipAddress: row.ip_address ?? void 0
|
|
11471
|
+
};
|
|
11472
|
+
}
|
|
11473
|
+
async verifyMfaChallenge(challengeId) {
|
|
11474
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11475
|
+
await this.db.execute(sql`
|
|
11476
|
+
UPDATE ${sql.raw(tableName)}
|
|
11477
|
+
SET verified_at = NOW()
|
|
11478
|
+
WHERE id = ${challengeId}
|
|
11479
|
+
`);
|
|
11480
|
+
}
|
|
11481
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11482
|
+
const tableName = this.qualify("recovery_codes");
|
|
11483
|
+
await this.db.execute(sql`
|
|
11484
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11485
|
+
`);
|
|
11486
|
+
for (const hash of codeHashes) {
|
|
11487
|
+
await this.db.execute(sql`
|
|
11488
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
|
|
11489
|
+
VALUES (${userId}, ${hash})
|
|
11490
|
+
`);
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
11493
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11494
|
+
const tableName = this.qualify("recovery_codes");
|
|
11495
|
+
const result = await this.db.execute(sql`
|
|
11496
|
+
UPDATE ${sql.raw(tableName)}
|
|
11497
|
+
SET used_at = NOW()
|
|
11498
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
11499
|
+
RETURNING id
|
|
11500
|
+
`);
|
|
11501
|
+
return result.rows.length > 0;
|
|
11502
|
+
}
|
|
11503
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11504
|
+
const tableName = this.qualify("recovery_codes");
|
|
11505
|
+
const result = await this.db.execute(sql`
|
|
11506
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11507
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
11508
|
+
`);
|
|
11509
|
+
return result.rows[0].count;
|
|
11510
|
+
}
|
|
11511
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11512
|
+
const tableName = this.qualify("recovery_codes");
|
|
11513
|
+
await this.db.execute(sql`
|
|
11514
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11515
|
+
`);
|
|
11516
|
+
}
|
|
11517
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11518
|
+
const tableName = this.qualify("mfa_factors");
|
|
11519
|
+
const result = await this.db.execute(sql`
|
|
11520
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11521
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
11522
|
+
`);
|
|
11523
|
+
return result.rows[0].count > 0;
|
|
11524
|
+
}
|
|
10724
11525
|
}
|
|
10725
11526
|
const DEFAULT_RETENTION = {
|
|
10726
11527
|
maxEntries: 200,
|
|
@@ -10931,7 +11732,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10931
11732
|
const registry = new PostgresCollectionRegistry();
|
|
10932
11733
|
if (collections) {
|
|
10933
11734
|
registry.registerMultiple(collections);
|
|
10934
|
-
|
|
11735
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
10935
11736
|
}
|
|
10936
11737
|
if (pgConfig.schema?.tables) {
|
|
10937
11738
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -10957,10 +11758,28 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10957
11758
|
try {
|
|
10958
11759
|
await schemaAwareDb.execute(sql`SELECT 1`);
|
|
10959
11760
|
} catch (err) {
|
|
10960
|
-
|
|
10961
|
-
|
|
11761
|
+
logger.error("❌ Failed to connect to PostgreSQL", {
|
|
11762
|
+
error: err
|
|
11763
|
+
});
|
|
11764
|
+
logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
10962
11765
|
}
|
|
10963
11766
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11767
|
+
let readDb;
|
|
11768
|
+
const readUrl = process.env.DATABASE_READ_URL;
|
|
11769
|
+
if (readUrl && readUrl !== pgConfig.connectionString) {
|
|
11770
|
+
try {
|
|
11771
|
+
const {
|
|
11772
|
+
createReadReplicaConnection: createReadReplicaConnection2
|
|
11773
|
+
} = await Promise.resolve().then(() => connection);
|
|
11774
|
+
const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
|
|
11775
|
+
readDb = readResources.db;
|
|
11776
|
+
logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11777
|
+
} catch (err) {
|
|
11778
|
+
logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
|
|
11779
|
+
error: err
|
|
11780
|
+
});
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
10964
11783
|
const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
|
|
10965
11784
|
const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
|
|
10966
11785
|
realtimeService.setDataDriver(driver);
|
|
@@ -10968,18 +11787,24 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10968
11787
|
try {
|
|
10969
11788
|
await driver.branchService.ensureBranchMetadataTable();
|
|
10970
11789
|
} catch (err) {
|
|
10971
|
-
|
|
11790
|
+
logger.warn("⚠️ Could not initialize branch metadata table", {
|
|
11791
|
+
error: err
|
|
11792
|
+
});
|
|
10972
11793
|
}
|
|
10973
11794
|
}
|
|
10974
|
-
|
|
11795
|
+
const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
|
|
11796
|
+
if (directUrl) {
|
|
10975
11797
|
try {
|
|
10976
|
-
await realtimeService.startListening(
|
|
11798
|
+
await realtimeService.startListening(directUrl);
|
|
10977
11799
|
} catch (err) {
|
|
10978
|
-
|
|
11800
|
+
logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
11801
|
+
error: err
|
|
11802
|
+
});
|
|
10979
11803
|
}
|
|
10980
11804
|
}
|
|
10981
11805
|
const internals = {
|
|
10982
11806
|
db: schemaAwareDb,
|
|
11807
|
+
readDb,
|
|
10983
11808
|
registry,
|
|
10984
11809
|
realtimeService,
|
|
10985
11810
|
driver,
|
|
@@ -11117,14 +11942,22 @@ export {
|
|
|
11117
11942
|
RealtimeService,
|
|
11118
11943
|
appConfig,
|
|
11119
11944
|
createAuthSchema,
|
|
11945
|
+
createDirectDatabaseConnection,
|
|
11120
11946
|
createPostgresAdapter,
|
|
11121
11947
|
createPostgresBootstrapper,
|
|
11122
11948
|
createPostgresDatabaseConnection,
|
|
11123
11949
|
createPostgresWebSocket,
|
|
11950
|
+
createReadReplicaConnection,
|
|
11124
11951
|
generateSchema,
|
|
11952
|
+
mfaChallenges,
|
|
11953
|
+
mfaChallengesRelations,
|
|
11954
|
+
mfaFactors,
|
|
11955
|
+
mfaFactorsRelations,
|
|
11125
11956
|
passwordResetTokens,
|
|
11126
11957
|
passwordResetTokensRelations,
|
|
11127
11958
|
rebaseSchema,
|
|
11959
|
+
recoveryCodes,
|
|
11960
|
+
recoveryCodesRelations,
|
|
11128
11961
|
refreshTokens,
|
|
11129
11962
|
refreshTokensRelations,
|
|
11130
11963
|
roles,
|