@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +5 -5
package/dist/index.es.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Pool, Client } from "pg";
|
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import { sql, inArray, eq as eq$3, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
|
|
4
4
|
import { pgSchema, timestamp, varchar, boolean, uuid, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
|
|
5
|
+
import { createHash, randomUUID } from "crypto";
|
|
5
6
|
import * as fs from "fs";
|
|
6
7
|
import { promises } from "fs";
|
|
7
8
|
import path from "path";
|
|
@@ -9,67 +10,120 @@ import { pathToFileURL } from "url";
|
|
|
9
10
|
import chokidar from "chokidar";
|
|
10
11
|
import { WebSocket, WebSocketServer } from "ws";
|
|
11
12
|
import { EventEmitter } from "events";
|
|
12
|
-
import { randomUUID } from "crypto";
|
|
13
13
|
import { inspect } from "util";
|
|
14
|
-
import { extractUserFromToken,
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
|
|
15
|
+
const DEFAULT_POOL = {
|
|
16
|
+
max: 20,
|
|
17
|
+
idleTimeoutMillis: 3e4,
|
|
18
|
+
connectionTimeoutMillis: 1e4,
|
|
19
|
+
queryTimeout: 3e4,
|
|
20
|
+
statementTimeout: 3e4,
|
|
21
|
+
keepAlive: true
|
|
22
|
+
};
|
|
23
|
+
function createPostgresDatabaseConnection(connectionString, schema, poolConfig) {
|
|
24
|
+
const opts = {
|
|
25
|
+
...DEFAULT_POOL,
|
|
26
|
+
...poolConfig
|
|
27
|
+
};
|
|
28
|
+
const pgPoolConfig = {
|
|
17
29
|
connectionString,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Timeout for new connections
|
|
25
|
-
// Retry configuration
|
|
26
|
-
query_timeout: 3e4,
|
|
27
|
-
// Query timeout
|
|
28
|
-
statement_timeout: 3e4,
|
|
29
|
-
// Statement timeout
|
|
30
|
-
// Keep connections alive
|
|
31
|
-
keepAlive: true,
|
|
30
|
+
max: opts.max,
|
|
31
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
32
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
33
|
+
query_timeout: opts.queryTimeout,
|
|
34
|
+
statement_timeout: opts.statementTimeout,
|
|
35
|
+
keepAlive: opts.keepAlive,
|
|
32
36
|
keepAliveInitialDelayMillis: 0
|
|
33
|
-
}
|
|
37
|
+
};
|
|
38
|
+
const pool = new Pool(pgPoolConfig);
|
|
34
39
|
pool.on("error", (err) => {
|
|
35
|
-
console.error("
|
|
40
|
+
console.error("[pg-pool] Unexpected pool error:", err.message);
|
|
36
41
|
if (err.message.includes("ETIMEDOUT")) {
|
|
37
|
-
console.warn("Connection timeout detected
|
|
42
|
+
console.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
|
|
38
43
|
}
|
|
39
44
|
});
|
|
40
|
-
pool.on("connect", (client) => {
|
|
41
|
-
console.debug("Database client connected");
|
|
42
|
-
client.on("error", (err) => {
|
|
43
|
-
console.error("Database client error:", err);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
pool.on("remove", (client) => {
|
|
47
|
-
console.debug("Database client removed from pool");
|
|
48
|
-
});
|
|
49
45
|
const db = schema ? drizzle(pool, {
|
|
50
46
|
schema
|
|
51
47
|
}) : drizzle(pool);
|
|
52
|
-
process.on("SIGINT", async () => {
|
|
53
|
-
console.log("SIGINT: Closing database pool...");
|
|
54
|
-
await pool.end();
|
|
55
|
-
process.exit(0);
|
|
56
|
-
});
|
|
57
|
-
process.on("SIGTERM", async () => {
|
|
58
|
-
console.log("SIGTERM: Closing database pool...");
|
|
59
|
-
await pool.end();
|
|
60
|
-
process.exit(0);
|
|
61
|
-
});
|
|
62
48
|
return {
|
|
63
49
|
db,
|
|
50
|
+
pool,
|
|
64
51
|
connectionString
|
|
65
52
|
};
|
|
66
53
|
}
|
|
67
54
|
function isPostgresCollection(collection) {
|
|
68
55
|
return !collection.driver || collection.driver === "postgres";
|
|
69
56
|
}
|
|
70
|
-
function
|
|
71
|
-
return
|
|
57
|
+
function isSQLAdmin(admin) {
|
|
58
|
+
return !!admin && typeof admin.executeSql === "function";
|
|
59
|
+
}
|
|
60
|
+
function isSchemaAdmin(admin) {
|
|
61
|
+
return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
|
|
62
|
+
}
|
|
63
|
+
const POSTGRES_CAPABILITIES = {
|
|
64
|
+
key: "postgres",
|
|
65
|
+
label: "PostgreSQL",
|
|
66
|
+
supportsRelations: true,
|
|
67
|
+
supportsSubcollections: false,
|
|
68
|
+
supportsRLS: true,
|
|
69
|
+
supportsReferences: false,
|
|
70
|
+
supportsColumnTypes: true,
|
|
71
|
+
supportsRealtime: true,
|
|
72
|
+
supportsSQLAdmin: true,
|
|
73
|
+
supportsDocumentAdmin: false,
|
|
74
|
+
supportsSchemaAdmin: true
|
|
75
|
+
};
|
|
76
|
+
const FIREBASE_CAPABILITIES = {
|
|
77
|
+
key: "firestore",
|
|
78
|
+
label: "Firebase / Firestore",
|
|
79
|
+
supportsRelations: false,
|
|
80
|
+
supportsSubcollections: true,
|
|
81
|
+
supportsRLS: false,
|
|
82
|
+
supportsReferences: true,
|
|
83
|
+
supportsColumnTypes: false,
|
|
84
|
+
supportsRealtime: true,
|
|
85
|
+
supportsSQLAdmin: false,
|
|
86
|
+
supportsDocumentAdmin: false,
|
|
87
|
+
supportsSchemaAdmin: false
|
|
88
|
+
};
|
|
89
|
+
const MONGODB_CAPABILITIES = {
|
|
90
|
+
key: "mongodb",
|
|
91
|
+
label: "MongoDB",
|
|
92
|
+
supportsRelations: false,
|
|
93
|
+
supportsSubcollections: true,
|
|
94
|
+
supportsRLS: false,
|
|
95
|
+
supportsReferences: true,
|
|
96
|
+
supportsColumnTypes: false,
|
|
97
|
+
supportsRealtime: false,
|
|
98
|
+
supportsSQLAdmin: false,
|
|
99
|
+
supportsDocumentAdmin: true,
|
|
100
|
+
supportsSchemaAdmin: true
|
|
101
|
+
};
|
|
102
|
+
const DEFAULT_CAPABILITIES = {
|
|
103
|
+
key: "(default)",
|
|
104
|
+
label: "Default",
|
|
105
|
+
supportsRelations: true,
|
|
106
|
+
supportsSubcollections: true,
|
|
107
|
+
supportsRLS: true,
|
|
108
|
+
supportsReferences: true,
|
|
109
|
+
supportsColumnTypes: true,
|
|
110
|
+
supportsRealtime: true,
|
|
111
|
+
supportsSQLAdmin: true,
|
|
112
|
+
supportsDocumentAdmin: true,
|
|
113
|
+
supportsSchemaAdmin: true
|
|
114
|
+
};
|
|
115
|
+
const CAPABILITIES_REGISTRY = {
|
|
116
|
+
postgres: POSTGRES_CAPABILITIES,
|
|
117
|
+
firestore: FIREBASE_CAPABILITIES,
|
|
118
|
+
mongodb: MONGODB_CAPABILITIES,
|
|
119
|
+
"(default)": DEFAULT_CAPABILITIES
|
|
120
|
+
};
|
|
121
|
+
function getDataSourceCapabilities(driver) {
|
|
122
|
+
if (!driver) return POSTGRES_CAPABILITIES;
|
|
123
|
+
return CAPABILITIES_REGISTRY[driver] ?? DEFAULT_CAPABILITIES;
|
|
72
124
|
}
|
|
125
|
+
const DEFAULT_ONE_OF_TYPE = "type";
|
|
126
|
+
const DEFAULT_ONE_OF_VALUE = "value";
|
|
73
127
|
const snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g;
|
|
74
128
|
const toSnakeCase = (str) => {
|
|
75
129
|
const regExpMatchArray = str.match(snakeCaseRegex);
|
|
@@ -967,6 +1021,21 @@ function generateForeignKeyName(name) {
|
|
|
967
1021
|
const singularName = snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName;
|
|
968
1022
|
return `${singularName}_id`;
|
|
969
1023
|
}
|
|
1024
|
+
function createRelationRef(id, path2) {
|
|
1025
|
+
return {
|
|
1026
|
+
id,
|
|
1027
|
+
path: path2,
|
|
1028
|
+
__type: "relation"
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function createRelationRefWithData(id, path2, data) {
|
|
1032
|
+
return {
|
|
1033
|
+
id,
|
|
1034
|
+
path: path2,
|
|
1035
|
+
__type: "relation",
|
|
1036
|
+
data
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
970
1039
|
function enumToObjectEntries(enumValues) {
|
|
971
1040
|
if (Array.isArray(enumValues)) {
|
|
972
1041
|
return enumValues;
|
|
@@ -990,15 +1059,35 @@ function getSubcollections(collection) {
|
|
|
990
1059
|
if (collection.childCollections) {
|
|
991
1060
|
return collection.childCollections() ?? [];
|
|
992
1061
|
}
|
|
993
|
-
if (
|
|
1062
|
+
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
994
1063
|
return collection.subcollections() ?? [];
|
|
995
1064
|
}
|
|
996
|
-
if (
|
|
1065
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
|
|
997
1066
|
const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
|
|
998
1067
|
return manyRelations.map((r) => {
|
|
999
1068
|
const target = r.target();
|
|
1000
|
-
|
|
1001
|
-
|
|
1069
|
+
if (!target) return void 0;
|
|
1070
|
+
const relationKey = r.relationName || target.slug;
|
|
1071
|
+
let customName;
|
|
1072
|
+
if (collection.properties) {
|
|
1073
|
+
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1074
|
+
if (prop && prop[1].name) {
|
|
1075
|
+
customName = prop[1].name;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const baseOverrides = {
|
|
1079
|
+
slug: relationKey
|
|
1080
|
+
};
|
|
1081
|
+
if (customName) {
|
|
1082
|
+
baseOverrides.name = customName;
|
|
1083
|
+
baseOverrides.singularName = customName;
|
|
1084
|
+
}
|
|
1085
|
+
const targetWithOverrides = {
|
|
1086
|
+
...target,
|
|
1087
|
+
...baseOverrides
|
|
1088
|
+
};
|
|
1089
|
+
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1090
|
+
}).filter((c) => Boolean(c));
|
|
1002
1091
|
}
|
|
1003
1092
|
return [];
|
|
1004
1093
|
}
|
|
@@ -1105,7 +1194,7 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1105
1194
|
if (!newRelation.foreignKeyOnTarget) {
|
|
1106
1195
|
let foundForeignKey = false;
|
|
1107
1196
|
try {
|
|
1108
|
-
const targetRelations = targetCollection.relations || [];
|
|
1197
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1109
1198
|
for (const targetRel of targetRelations) {
|
|
1110
1199
|
if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) {
|
|
1111
1200
|
try {
|
|
@@ -1131,7 +1220,7 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1131
1220
|
let isManyToManyInverse = false;
|
|
1132
1221
|
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
|
|
1133
1222
|
try {
|
|
1134
|
-
const targetRelations = targetCollection.relations || [];
|
|
1223
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1135
1224
|
for (const targetRel of targetRelations) {
|
|
1136
1225
|
if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
|
|
1137
1226
|
isManyToManyInverse = true;
|
|
@@ -1165,14 +1254,21 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1165
1254
|
}
|
|
1166
1255
|
return newRelation;
|
|
1167
1256
|
}
|
|
1257
|
+
const _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
1168
1258
|
function resolveCollectionRelations(collection) {
|
|
1259
|
+
const cached = _resolvedRelationsCache.get(collection);
|
|
1260
|
+
if (cached) return cached;
|
|
1261
|
+
if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
|
|
1262
|
+
const relCollection = collection;
|
|
1169
1263
|
const relations2 = {};
|
|
1170
|
-
|
|
1171
|
-
|
|
1264
|
+
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
1265
|
+
if (relCollection.relations) {
|
|
1266
|
+
relCollection.relations.forEach((relation) => {
|
|
1172
1267
|
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1173
1268
|
const relationKey = normalizedRelation.relationName;
|
|
1174
1269
|
if (relationKey) {
|
|
1175
1270
|
relations2[relationKey] = normalizedRelation;
|
|
1271
|
+
registeredRelationNames.add(relationKey);
|
|
1176
1272
|
}
|
|
1177
1273
|
});
|
|
1178
1274
|
}
|
|
@@ -1184,15 +1280,17 @@ function resolveCollectionRelations(collection) {
|
|
|
1184
1280
|
sourceCollection: collection
|
|
1185
1281
|
});
|
|
1186
1282
|
if (relation) {
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
relations2[propKey] = sanitizeRelation(relation, collection);
|
|
1283
|
+
if (relations2[propKey]) return;
|
|
1284
|
+
if (!relation.relationName) {
|
|
1285
|
+
relation.relationName = propKey;
|
|
1192
1286
|
}
|
|
1287
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1288
|
+
relations2[propKey] = normalizedRelation;
|
|
1289
|
+
registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
|
|
1193
1290
|
}
|
|
1194
1291
|
});
|
|
1195
1292
|
}
|
|
1293
|
+
_resolvedRelationsCache.set(collection, relations2);
|
|
1196
1294
|
return relations2;
|
|
1197
1295
|
}
|
|
1198
1296
|
function resolvePropertyRelation({
|
|
@@ -1201,7 +1299,24 @@ function resolvePropertyRelation({
|
|
|
1201
1299
|
sourceCollection
|
|
1202
1300
|
}) {
|
|
1203
1301
|
if (property.type !== "relation") return void 0;
|
|
1204
|
-
const
|
|
1302
|
+
const relProp = property;
|
|
1303
|
+
if (relProp.target) {
|
|
1304
|
+
return {
|
|
1305
|
+
relationName: relProp.relationName || propertyKey,
|
|
1306
|
+
target: relProp.target,
|
|
1307
|
+
cardinality: relProp.cardinality || "one",
|
|
1308
|
+
direction: relProp.direction || "owning",
|
|
1309
|
+
inverseRelationName: relProp.inverseRelationName,
|
|
1310
|
+
localKey: relProp.localKey,
|
|
1311
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget,
|
|
1312
|
+
through: relProp.through,
|
|
1313
|
+
joinPath: relProp.joinPath,
|
|
1314
|
+
onUpdate: relProp.onUpdate,
|
|
1315
|
+
onDelete: relProp.onDelete,
|
|
1316
|
+
overrides: relProp.overrides
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
|
|
1205
1320
|
if (!relation) {
|
|
1206
1321
|
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1207
1322
|
return void 0;
|
|
@@ -1209,7 +1324,7 @@ function resolvePropertyRelation({
|
|
|
1209
1324
|
return relation;
|
|
1210
1325
|
}
|
|
1211
1326
|
function getTableName(collection) {
|
|
1212
|
-
if (
|
|
1327
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
1213
1328
|
return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1214
1329
|
}
|
|
1215
1330
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -1225,6 +1340,14 @@ function getEnumVarName(tableName, propName) {
|
|
|
1225
1340
|
function getColumnName(fullColumn) {
|
|
1226
1341
|
return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
|
|
1227
1342
|
}
|
|
1343
|
+
function findRelation(resolvedRelations, key) {
|
|
1344
|
+
if (resolvedRelations[key]) return resolvedRelations[key];
|
|
1345
|
+
const slugKey = key.replace(/_/g, "-");
|
|
1346
|
+
if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
|
|
1347
|
+
const snakeKey = key.replace(/-/g, "_");
|
|
1348
|
+
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
1349
|
+
return void 0;
|
|
1350
|
+
}
|
|
1228
1351
|
var logic = { exports: {} };
|
|
1229
1352
|
(function(module, exports$1) {
|
|
1230
1353
|
(function(root2, factory) {
|
|
@@ -2997,10 +3120,12 @@ class CollectionRegistry {
|
|
|
2997
3120
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
2998
3121
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
2999
3122
|
rootCollections = [];
|
|
3123
|
+
cachedCollectionsList = null;
|
|
3000
3124
|
// Raw configuration layer (used by Collection Editor AST generator)
|
|
3001
3125
|
rawCollectionsByTableName = /* @__PURE__ */ new Map();
|
|
3002
3126
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
3003
3127
|
rawRootCollections = [];
|
|
3128
|
+
cachedRawCollectionsList = null;
|
|
3004
3129
|
// Snapshot of raw input for idempotency check — compared BEFORE normalization
|
|
3005
3130
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
3006
3131
|
lastRawInputSnapshot = null;
|
|
@@ -3013,9 +3138,11 @@ class CollectionRegistry {
|
|
|
3013
3138
|
this.collectionsByTableName.clear();
|
|
3014
3139
|
this.collectionsBySlug.clear();
|
|
3015
3140
|
this.rootCollections = [];
|
|
3141
|
+
this.cachedCollectionsList = null;
|
|
3016
3142
|
this.rawCollectionsByTableName.clear();
|
|
3017
3143
|
this.rawCollectionsBySlug.clear();
|
|
3018
3144
|
this.rawRootCollections = [];
|
|
3145
|
+
this.cachedRawCollectionsList = null;
|
|
3019
3146
|
}
|
|
3020
3147
|
/**
|
|
3021
3148
|
* Registers a collection and its subcollections recursively.
|
|
@@ -3048,12 +3175,14 @@ class CollectionRegistry {
|
|
|
3048
3175
|
this.rawCollectionsBySlug.set(raw.slug, raw);
|
|
3049
3176
|
}
|
|
3050
3177
|
});
|
|
3051
|
-
normalizedCollections.forEach((c
|
|
3178
|
+
normalizedCollections.forEach((c) => {
|
|
3052
3179
|
const subcollections = getSubcollections(c);
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
this._registerRecursively(this.normalizeCollection(
|
|
3180
|
+
if (subcollections && subcollections.length > 0) {
|
|
3181
|
+
subcollections.forEach((subCollection) => {
|
|
3182
|
+
if (!subCollection) return;
|
|
3183
|
+
this._registerRecursively(this.normalizeCollection({
|
|
3184
|
+
...subCollection
|
|
3185
|
+
}), cloneDeep$1(subCollection));
|
|
3057
3186
|
});
|
|
3058
3187
|
}
|
|
3059
3188
|
});
|
|
@@ -3079,41 +3208,100 @@ class CollectionRegistry {
|
|
|
3079
3208
|
if (rawCollection.slug) {
|
|
3080
3209
|
this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
|
|
3081
3210
|
}
|
|
3082
|
-
const subcollections = getSubcollections(
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
this._registerRecursively(this.normalizeCollection(
|
|
3211
|
+
const subcollections = getSubcollections(normalizedCollection);
|
|
3212
|
+
if (subcollections && subcollections.length > 0) {
|
|
3213
|
+
subcollections.forEach((subCollection) => {
|
|
3214
|
+
if (!subCollection) return;
|
|
3215
|
+
this._registerRecursively(this.normalizeCollection({
|
|
3216
|
+
...subCollection
|
|
3217
|
+
}), cloneDeep$1(subCollection));
|
|
3087
3218
|
});
|
|
3088
3219
|
}
|
|
3089
3220
|
}
|
|
3090
3221
|
normalizeCollection(collection) {
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3222
|
+
const result = {
|
|
3223
|
+
...collection
|
|
3224
|
+
};
|
|
3225
|
+
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
3226
|
+
const relResult = result;
|
|
3227
|
+
const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
|
|
3228
|
+
const mergedRelationsRaw = [...extractedRelations];
|
|
3229
|
+
for (const manual of manualRelations) {
|
|
3230
|
+
const name = manual.relationName;
|
|
3231
|
+
if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
|
|
3232
|
+
mergedRelationsRaw.push(manual);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
let mergedRelations = mergedRelationsRaw;
|
|
3236
|
+
if (getDataSourceCapabilities(result.driver).supportsRelations) {
|
|
3237
|
+
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
3238
|
+
try {
|
|
3239
|
+
return sanitizeRelation(r, result);
|
|
3240
|
+
} catch {
|
|
3241
|
+
return r;
|
|
3242
|
+
}
|
|
3243
|
+
});
|
|
3244
|
+
relResult.relations = mergedRelations;
|
|
3245
|
+
}
|
|
3246
|
+
const properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
3247
|
+
result.properties = properties;
|
|
3248
|
+
if (!result.childCollections) {
|
|
3249
|
+
if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) {
|
|
3250
|
+
result.childCollections = result.subcollections;
|
|
3251
|
+
} else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
|
|
3252
|
+
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
3099
3253
|
if (manyRelations.length > 0) {
|
|
3100
|
-
|
|
3254
|
+
result.childCollections = () => manyRelations.map((r) => {
|
|
3101
3255
|
const target = r.target();
|
|
3102
3256
|
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
3103
3257
|
});
|
|
3104
3258
|
}
|
|
3105
3259
|
}
|
|
3106
3260
|
}
|
|
3107
|
-
return
|
|
3261
|
+
return result;
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
3265
|
+
* This allows developers to define relations directly on properties without a separate
|
|
3266
|
+
* `relations[]` entry on the collection.
|
|
3267
|
+
*/
|
|
3268
|
+
extractRelationsFromProperties(properties) {
|
|
3269
|
+
const relations2 = [];
|
|
3270
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
3271
|
+
if (property.type === "relation") {
|
|
3272
|
+
const relProp = property;
|
|
3273
|
+
const target = relProp.target ?? relProp.relation?.target;
|
|
3274
|
+
if (target) {
|
|
3275
|
+
const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
|
|
3276
|
+
relations2.push({
|
|
3277
|
+
relationName,
|
|
3278
|
+
target,
|
|
3279
|
+
cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
|
|
3280
|
+
direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
|
|
3281
|
+
inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
|
|
3282
|
+
localKey: relProp.localKey ?? relProp.relation?.localKey,
|
|
3283
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
|
|
3284
|
+
through: relProp.through ?? relProp.relation?.through,
|
|
3285
|
+
joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
|
|
3286
|
+
onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
|
|
3287
|
+
onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
|
|
3288
|
+
overrides: relProp.overrides ?? relProp.relation?.overrides
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
} else if (property.type === "map" && property.properties) {
|
|
3292
|
+
relations2.push(...this.extractRelationsFromProperties(property.properties));
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return relations2;
|
|
3108
3296
|
}
|
|
3109
3297
|
normalizeProperties(properties, relations2) {
|
|
3110
3298
|
const newProperties = {};
|
|
3111
3299
|
for (const key in properties) {
|
|
3112
|
-
newProperties[key] = this.normalizeProperty(properties[key], relations2);
|
|
3300
|
+
newProperties[key] = this.normalizeProperty(key, properties[key], relations2);
|
|
3113
3301
|
}
|
|
3114
3302
|
return newProperties;
|
|
3115
3303
|
}
|
|
3116
|
-
normalizeProperty(property, relations2) {
|
|
3304
|
+
normalizeProperty(key, property, relations2) {
|
|
3117
3305
|
const newProperty = {
|
|
3118
3306
|
...property
|
|
3119
3307
|
};
|
|
@@ -3123,9 +3311,9 @@ class CollectionRegistry {
|
|
|
3123
3311
|
const arrayProp = newProperty;
|
|
3124
3312
|
if (arrayProp.of) {
|
|
3125
3313
|
if (Array.isArray(arrayProp.of)) {
|
|
3126
|
-
arrayProp.of = arrayProp.of.map((p) => this.normalizeProperty(p, relations2));
|
|
3314
|
+
arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations2));
|
|
3127
3315
|
} else {
|
|
3128
|
-
arrayProp.of = this.normalizeProperty(arrayProp.of, relations2);
|
|
3316
|
+
arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations2);
|
|
3129
3317
|
}
|
|
3130
3318
|
} else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
|
|
3131
3319
|
arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations2);
|
|
@@ -3137,11 +3325,12 @@ class CollectionRegistry {
|
|
|
3137
3325
|
}
|
|
3138
3326
|
} else if (newProperty.type === "relation") {
|
|
3139
3327
|
const relationProperty = newProperty;
|
|
3140
|
-
const
|
|
3328
|
+
const name = relationProperty.relationName || key;
|
|
3329
|
+
const relation = relations2.find((r) => r.relationName === name);
|
|
3141
3330
|
if (relation) {
|
|
3142
3331
|
relationProperty.relation = relation;
|
|
3143
3332
|
} else {
|
|
3144
|
-
console.warn(`Could not find relation for property with relationName: ${
|
|
3333
|
+
console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
|
|
3145
3334
|
}
|
|
3146
3335
|
}
|
|
3147
3336
|
return newProperty;
|
|
@@ -3149,6 +3338,11 @@ class CollectionRegistry {
|
|
|
3149
3338
|
get(path2) {
|
|
3150
3339
|
const bySlug = this.collectionsBySlug.get(path2);
|
|
3151
3340
|
if (bySlug) return bySlug;
|
|
3341
|
+
if (path2.includes("-")) {
|
|
3342
|
+
const normalized = path2.replace(/-/g, "_");
|
|
3343
|
+
const byNormalized = this.collectionsBySlug.get(normalized);
|
|
3344
|
+
if (byNormalized) return byNormalized;
|
|
3345
|
+
}
|
|
3152
3346
|
return this.collectionsByTableName.get(path2);
|
|
3153
3347
|
}
|
|
3154
3348
|
/**
|
|
@@ -3158,6 +3352,11 @@ class CollectionRegistry {
|
|
|
3158
3352
|
getRaw(path2) {
|
|
3159
3353
|
const bySlug = this.rawCollectionsBySlug.get(path2);
|
|
3160
3354
|
if (bySlug) return bySlug;
|
|
3355
|
+
if (path2.includes("-")) {
|
|
3356
|
+
const normalized = path2.replace(/-/g, "_");
|
|
3357
|
+
const byNormalized = this.rawCollectionsBySlug.get(normalized);
|
|
3358
|
+
if (byNormalized) return byNormalized;
|
|
3359
|
+
}
|
|
3161
3360
|
return this.rawCollectionsByTableName.get(path2);
|
|
3162
3361
|
}
|
|
3163
3362
|
/**
|
|
@@ -3179,11 +3378,11 @@ class CollectionRegistry {
|
|
|
3179
3378
|
}
|
|
3180
3379
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
3181
3380
|
const relationKey = pathSegments[i];
|
|
3182
|
-
if (!
|
|
3183
|
-
throw new Error(`Relation path navigation requires a
|
|
3381
|
+
if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) {
|
|
3382
|
+
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
|
|
3184
3383
|
}
|
|
3185
3384
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
3186
|
-
const relation = resolvedRelations
|
|
3385
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
3187
3386
|
if (!relation) {
|
|
3188
3387
|
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
3189
3388
|
}
|
|
@@ -3193,10 +3392,16 @@ class CollectionRegistry {
|
|
|
3193
3392
|
return currentCollection;
|
|
3194
3393
|
}
|
|
3195
3394
|
getCollections() {
|
|
3196
|
-
|
|
3395
|
+
if (!this.cachedCollectionsList) {
|
|
3396
|
+
this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
3397
|
+
}
|
|
3398
|
+
return this.cachedCollectionsList;
|
|
3197
3399
|
}
|
|
3198
3400
|
getRawCollections() {
|
|
3199
|
-
|
|
3401
|
+
if (!this.cachedRawCollectionsList) {
|
|
3402
|
+
this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
3403
|
+
}
|
|
3404
|
+
return this.cachedRawCollectionsList;
|
|
3200
3405
|
}
|
|
3201
3406
|
/**
|
|
3202
3407
|
* Resolves a multi-segment path like "products/123/locales" and returns
|
|
@@ -3252,24 +3457,62 @@ function convertWhereToFilter(where) {
|
|
|
3252
3457
|
"lte": "<=",
|
|
3253
3458
|
"in": "in",
|
|
3254
3459
|
"nin": "not-in",
|
|
3460
|
+
"not-in": "not-in",
|
|
3255
3461
|
"cs": "array-contains",
|
|
3256
|
-
"csa": "array-contains-any"
|
|
3462
|
+
"csa": "array-contains-any",
|
|
3463
|
+
"==": "==",
|
|
3464
|
+
"!=": "!=",
|
|
3465
|
+
">": ">",
|
|
3466
|
+
">=": ">=",
|
|
3467
|
+
"<": "<",
|
|
3468
|
+
"<=": "<=",
|
|
3469
|
+
"array-contains": "array-contains",
|
|
3470
|
+
"array-contains-any": "array-contains-any"
|
|
3257
3471
|
};
|
|
3258
3472
|
const filter = {};
|
|
3259
3473
|
for (const [field, rawValue] of Object.entries(where)) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
if (typeof
|
|
3265
|
-
|
|
3474
|
+
if (rawValue === null) {
|
|
3475
|
+
filter[field] = ["==", null];
|
|
3476
|
+
continue;
|
|
3477
|
+
}
|
|
3478
|
+
if (typeof rawValue === "boolean") {
|
|
3479
|
+
filter[field] = ["==", rawValue];
|
|
3480
|
+
continue;
|
|
3481
|
+
}
|
|
3482
|
+
if (typeof rawValue === "number") {
|
|
3483
|
+
filter[field] = ["==", rawValue];
|
|
3484
|
+
continue;
|
|
3266
3485
|
}
|
|
3267
|
-
if (
|
|
3268
|
-
|
|
3486
|
+
if (Array.isArray(rawValue) && rawValue.length === 2) {
|
|
3487
|
+
const [rawOp, val] = rawValue;
|
|
3488
|
+
const mappedOp = operatorMap[rawOp] ?? "==";
|
|
3489
|
+
filter[field] = [mappedOp, val];
|
|
3490
|
+
continue;
|
|
3269
3491
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3492
|
+
if (typeof rawValue === "string") {
|
|
3493
|
+
const dotIndex = rawValue.indexOf(".");
|
|
3494
|
+
if (dotIndex === -1) {
|
|
3495
|
+
filter[field] = ["==", rawValue];
|
|
3496
|
+
continue;
|
|
3497
|
+
}
|
|
3498
|
+
const op = rawValue.substring(0, dotIndex);
|
|
3499
|
+
let value = rawValue.substring(dotIndex + 1);
|
|
3500
|
+
if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
|
|
3501
|
+
value = value.slice(1, -1).split(",").map((v) => v.trim());
|
|
3502
|
+
}
|
|
3503
|
+
if (value === "null") {
|
|
3504
|
+
value = null;
|
|
3505
|
+
} else if (value === "true") {
|
|
3506
|
+
value = true;
|
|
3507
|
+
} else if (value === "false") {
|
|
3508
|
+
value = false;
|
|
3509
|
+
} else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
|
|
3510
|
+
value = Number(value);
|
|
3511
|
+
}
|
|
3512
|
+
const mappedOp = operatorMap[op];
|
|
3513
|
+
if (mappedOp) {
|
|
3514
|
+
filter[field] = [mappedOp, value];
|
|
3515
|
+
}
|
|
3273
3516
|
}
|
|
3274
3517
|
}
|
|
3275
3518
|
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
@@ -3288,7 +3531,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
3288
3531
|
const entities = await driver.fetchCollection({
|
|
3289
3532
|
path: slug,
|
|
3290
3533
|
limit: params?.limit,
|
|
3291
|
-
|
|
3534
|
+
offset: params?.offset,
|
|
3292
3535
|
filter: convertWhereToFilter(params?.where),
|
|
3293
3536
|
orderBy: orderParsed?.[0],
|
|
3294
3537
|
order: orderParsed?.[1],
|
|
@@ -3350,7 +3593,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
3350
3593
|
return driver.listenCollection({
|
|
3351
3594
|
path: slug,
|
|
3352
3595
|
limit: params?.limit,
|
|
3353
|
-
|
|
3596
|
+
offset: params?.offset,
|
|
3354
3597
|
filter: convertWhereToFilter(params?.where),
|
|
3355
3598
|
orderBy: orderParsed?.[0],
|
|
3356
3599
|
order: orderParsed?.[1],
|
|
@@ -3397,7 +3640,8 @@ function buildRebaseData(driver) {
|
|
|
3397
3640
|
if (prop === "collection") return getAccessor;
|
|
3398
3641
|
if (typeof prop === "symbol") return void 0;
|
|
3399
3642
|
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3400
|
-
|
|
3643
|
+
const slug = toSnakeCase(prop);
|
|
3644
|
+
return getAccessor(slug);
|
|
3401
3645
|
}
|
|
3402
3646
|
});
|
|
3403
3647
|
}
|
|
@@ -3455,7 +3699,7 @@ class DrizzleConditionBuilder {
|
|
|
3455
3699
|
* Build relation-based conditions for different relation types
|
|
3456
3700
|
*/
|
|
3457
3701
|
static buildRelationConditions(relation, parentEntityId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry) {
|
|
3458
|
-
console.debug(
|
|
3702
|
+
console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
|
|
3459
3703
|
relationName: relation.relationName,
|
|
3460
3704
|
cardinality: relation.cardinality,
|
|
3461
3705
|
direction: relation.direction,
|
|
@@ -3467,7 +3711,7 @@ class DrizzleConditionBuilder {
|
|
|
3467
3711
|
const joinConditions = [];
|
|
3468
3712
|
const whereConditions = [];
|
|
3469
3713
|
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
3470
|
-
console.debug(
|
|
3714
|
+
console.debug("🔍 [buildRelationConditions] Using joinPath logic");
|
|
3471
3715
|
const {
|
|
3472
3716
|
joins,
|
|
3473
3717
|
finalCondition
|
|
@@ -3475,37 +3719,37 @@ class DrizzleConditionBuilder {
|
|
|
3475
3719
|
joinConditions.push(...joins);
|
|
3476
3720
|
whereConditions.push(finalCondition);
|
|
3477
3721
|
} else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
3478
|
-
console.debug(
|
|
3722
|
+
console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
|
|
3479
3723
|
const junctionResult = this.buildJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
|
|
3480
3724
|
joinConditions.push(junctionResult.join);
|
|
3481
3725
|
whereConditions.push(junctionResult.condition);
|
|
3482
3726
|
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
|
|
3483
|
-
console.debug(
|
|
3727
|
+
console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
|
|
3484
3728
|
const junctionResult = this.buildInverseJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
|
|
3485
3729
|
joinConditions.push(junctionResult.join);
|
|
3486
3730
|
whereConditions.push(junctionResult.condition);
|
|
3487
3731
|
} else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
|
|
3488
|
-
console.debug(
|
|
3732
|
+
console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
|
|
3489
3733
|
const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
|
|
3490
3734
|
if (junctionInfo) {
|
|
3491
|
-
console.debug(
|
|
3735
|
+
console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
|
|
3492
3736
|
const junctionResult = this.buildInverseJunctionTableConditions(junctionInfo, targetIdColumn, parentEntityId, registry);
|
|
3493
3737
|
joinConditions.push(junctionResult.join);
|
|
3494
3738
|
whereConditions.push(junctionResult.condition);
|
|
3495
3739
|
} else if (relation.foreignKeyOnTarget) {
|
|
3496
|
-
console.debug(
|
|
3740
|
+
console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
|
|
3497
3741
|
const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
|
|
3498
3742
|
whereConditions.push(simpleCondition);
|
|
3499
3743
|
} else {
|
|
3500
|
-
console.error(
|
|
3744
|
+
console.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
|
|
3501
3745
|
throw new Error(`Cannot resolve inverse many relation '${relation.relationName}'. Either specify 'through' property, ensure corresponding owning relation exists with junction table configuration, or specify 'foreignKeyOnTarget' for one-to-many relationships.`);
|
|
3502
3746
|
}
|
|
3503
3747
|
} else {
|
|
3504
|
-
console.debug(
|
|
3748
|
+
console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
|
|
3505
3749
|
const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
|
|
3506
3750
|
whereConditions.push(simpleCondition);
|
|
3507
3751
|
}
|
|
3508
|
-
console.debug(
|
|
3752
|
+
console.debug("🔍 [buildRelationConditions] Final result:", {
|
|
3509
3753
|
joinConditionsCount: joinConditions.length,
|
|
3510
3754
|
whereConditionsCount: whereConditions.length
|
|
3511
3755
|
});
|
|
@@ -3756,7 +4000,8 @@ class DrizzleConditionBuilder {
|
|
|
3756
4000
|
static buildSearchConditions(searchString, properties, table) {
|
|
3757
4001
|
const searchConditions = [];
|
|
3758
4002
|
for (const [key, prop] of Object.entries(properties)) {
|
|
3759
|
-
|
|
4003
|
+
const p = prop;
|
|
4004
|
+
if (p.type === "string" && !p.enum && p.isId !== "uuid") {
|
|
3760
4005
|
const fieldColumn = table[key];
|
|
3761
4006
|
if (fieldColumn) {
|
|
3762
4007
|
searchConditions.push(ilike(fieldColumn, `%${searchString}%`));
|
|
@@ -3919,29 +4164,29 @@ class DrizzleConditionBuilder {
|
|
|
3919
4164
|
try {
|
|
3920
4165
|
console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
|
|
3921
4166
|
if (!relation.inverseRelationName) {
|
|
3922
|
-
console.debug(
|
|
4167
|
+
console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
|
|
3923
4168
|
return null;
|
|
3924
4169
|
}
|
|
3925
4170
|
const targetCollection = relation.target();
|
|
3926
4171
|
console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
|
|
3927
4172
|
const targetCollectionRelations = resolveCollectionRelations(targetCollection);
|
|
3928
|
-
console.debug(
|
|
4173
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
|
|
3929
4174
|
const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
|
|
3930
4175
|
if (!correspondingRelation) {
|
|
3931
4176
|
console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
|
|
3932
4177
|
return null;
|
|
3933
4178
|
}
|
|
3934
|
-
console.debug(
|
|
4179
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
|
|
3935
4180
|
relationName: correspondingRelation.relationName,
|
|
3936
4181
|
cardinality: correspondingRelation.cardinality,
|
|
3937
4182
|
direction: correspondingRelation.direction,
|
|
3938
4183
|
hasThrough: !!correspondingRelation.through
|
|
3939
4184
|
});
|
|
3940
4185
|
if (correspondingRelation.cardinality !== "many" || correspondingRelation.direction !== "owning" || !correspondingRelation.through) {
|
|
3941
|
-
console.debug(
|
|
4186
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
|
|
3942
4187
|
return null;
|
|
3943
4188
|
}
|
|
3944
|
-
console.debug(
|
|
4189
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
|
|
3945
4190
|
const through = correspondingRelation.through;
|
|
3946
4191
|
const result = {
|
|
3947
4192
|
table: through.table,
|
|
@@ -3950,7 +4195,7 @@ class DrizzleConditionBuilder {
|
|
|
3950
4195
|
targetColumn: through.sourceColumn
|
|
3951
4196
|
// Swapped for inverse relation
|
|
3952
4197
|
};
|
|
3953
|
-
console.debug(
|
|
4198
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
|
|
3954
4199
|
return result;
|
|
3955
4200
|
} catch (error) {
|
|
3956
4201
|
console.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}':`, error);
|
|
@@ -3959,10 +4204,19 @@ class DrizzleConditionBuilder {
|
|
|
3959
4204
|
}
|
|
3960
4205
|
}
|
|
3961
4206
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
4207
|
+
function getColumnMeta(col) {
|
|
4208
|
+
const raw = col;
|
|
4209
|
+
return {
|
|
4210
|
+
columnType: typeof raw.columnType === "string" ? raw.columnType : void 0,
|
|
4211
|
+
dataType: typeof raw.dataType === "string" ? raw.dataType : void 0,
|
|
4212
|
+
primary: typeof raw.primary === "boolean" ? raw.primary : void 0
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
3962
4215
|
function getCollectionByPath(collectionPath, registry) {
|
|
3963
4216
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
3964
4217
|
if (!collection) {
|
|
3965
|
-
|
|
4218
|
+
const registered = registry.getCollections().map((c) => c.slug).join(", ");
|
|
4219
|
+
throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
|
|
3966
4220
|
}
|
|
3967
4221
|
return collection;
|
|
3968
4222
|
}
|
|
@@ -3979,7 +4233,8 @@ function getPrimaryKeys(collection, registry) {
|
|
|
3979
4233
|
if (collection.properties) {
|
|
3980
4234
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
3981
4235
|
fieldName: key,
|
|
3982
|
-
type: prop.type === "number" ? "number" : "string"
|
|
4236
|
+
type: prop.type === "number" ? "number" : "string",
|
|
4237
|
+
isUUID: prop.isId === "uuid"
|
|
3983
4238
|
}));
|
|
3984
4239
|
if (idProps.length > 0) {
|
|
3985
4240
|
return idProps;
|
|
@@ -3989,19 +4244,25 @@ function getPrimaryKeys(collection, registry) {
|
|
|
3989
4244
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
3990
4245
|
const col = colRaw;
|
|
3991
4246
|
if (col && typeof col === "object" && "primary" in col && col.primary) {
|
|
3992
|
-
const
|
|
4247
|
+
const meta = getColumnMeta(col);
|
|
4248
|
+
const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
|
|
4249
|
+
const isUUID = meta.columnType === "PgUUID";
|
|
3993
4250
|
keys2.push({
|
|
3994
4251
|
fieldName: key,
|
|
3995
|
-
type
|
|
4252
|
+
type,
|
|
4253
|
+
isUUID
|
|
3996
4254
|
});
|
|
3997
4255
|
}
|
|
3998
4256
|
}
|
|
3999
4257
|
if (keys2.length === 0 && "id" in table) {
|
|
4000
4258
|
const idCol = table["id"];
|
|
4001
|
-
const
|
|
4259
|
+
const idMeta = getColumnMeta(idCol);
|
|
4260
|
+
const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
|
|
4261
|
+
const isUUID = idMeta.columnType === "PgUUID";
|
|
4002
4262
|
keys2.push({
|
|
4003
4263
|
fieldName: "id",
|
|
4004
|
-
type
|
|
4264
|
+
type,
|
|
4265
|
+
isUUID
|
|
4005
4266
|
});
|
|
4006
4267
|
}
|
|
4007
4268
|
return keys2;
|
|
@@ -4013,7 +4274,7 @@ function parseIdValues(idValue, primaryKeys) {
|
|
|
4013
4274
|
}
|
|
4014
4275
|
if (primaryKeys.length === 1) {
|
|
4015
4276
|
const pk = primaryKeys[0];
|
|
4016
|
-
if (pk.type === "number") {
|
|
4277
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4017
4278
|
const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
|
|
4018
4279
|
if (isNaN(parsed)) {
|
|
4019
4280
|
throw new Error(`Invalid numeric ID: ${idValue}`);
|
|
@@ -4031,7 +4292,7 @@ function parseIdValues(idValue, primaryKeys) {
|
|
|
4031
4292
|
for (let i = 0; i < primaryKeys.length; i++) {
|
|
4032
4293
|
const pk = primaryKeys[i];
|
|
4033
4294
|
const val = parts[i];
|
|
4034
|
-
if (pk.type === "number") {
|
|
4295
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4035
4296
|
const parsed = parseInt(val, 10);
|
|
4036
4297
|
if (isNaN(parsed)) {
|
|
4037
4298
|
throw new Error(`Invalid numeric ID component: ${val}`);
|
|
@@ -4090,28 +4351,37 @@ function sanitizeAndConvertDates(obj) {
|
|
|
4090
4351
|
return obj;
|
|
4091
4352
|
}
|
|
4092
4353
|
function serializeDataToServer(entity, properties, collection, registry) {
|
|
4093
|
-
if (!entity || !properties) return
|
|
4354
|
+
if (!entity || !properties) return {
|
|
4355
|
+
scalarData: entity ?? {},
|
|
4356
|
+
inverseRelationUpdates: [],
|
|
4357
|
+
joinPathRelationUpdates: []
|
|
4358
|
+
};
|
|
4094
4359
|
const result = {};
|
|
4095
4360
|
const resolvedRelations = collection ? resolveCollectionRelations(collection) : {};
|
|
4096
4361
|
const inverseRelationUpdates = [];
|
|
4097
4362
|
const joinPathRelationUpdates = [];
|
|
4363
|
+
const foreignKeys = /* @__PURE__ */ new Set();
|
|
4364
|
+
Object.values(resolvedRelations).forEach((relation) => {
|
|
4365
|
+
if (relation.localKey) foreignKeys.add(relation.localKey);
|
|
4366
|
+
});
|
|
4098
4367
|
for (const [key, value] of Object.entries(entity)) {
|
|
4099
4368
|
const property = properties[key];
|
|
4369
|
+
const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
|
|
4100
4370
|
if (!property) {
|
|
4101
|
-
result[key] =
|
|
4371
|
+
result[key] = effectiveValue;
|
|
4102
4372
|
continue;
|
|
4103
4373
|
}
|
|
4104
4374
|
if (property.type === "relation" && collection) {
|
|
4105
|
-
const relation = resolvedRelations
|
|
4375
|
+
const relation = findRelation(resolvedRelations, key);
|
|
4106
4376
|
if (relation) {
|
|
4107
4377
|
if (relation.direction === "owning" && relation.localKey) {
|
|
4108
|
-
const serializedValue = serializePropertyToServer(
|
|
4109
|
-
if (serializedValue !==
|
|
4378
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4379
|
+
if (serializedValue !== void 0) {
|
|
4110
4380
|
result[relation.localKey] = serializedValue;
|
|
4111
4381
|
}
|
|
4112
4382
|
continue;
|
|
4113
4383
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
4114
|
-
const serializedValue = serializePropertyToServer(
|
|
4384
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4115
4385
|
const pks = getPrimaryKeys(collection, registry);
|
|
4116
4386
|
inverseRelationUpdates.push({
|
|
4117
4387
|
relationKey: key,
|
|
@@ -4121,7 +4391,7 @@ function serializeDataToServer(entity, properties, collection, registry) {
|
|
|
4121
4391
|
});
|
|
4122
4392
|
continue;
|
|
4123
4393
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
4124
|
-
const serializedValue = serializePropertyToServer(
|
|
4394
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4125
4395
|
if (relation.cardinality === "one") {
|
|
4126
4396
|
joinPathRelationUpdates.push({
|
|
4127
4397
|
relationKey: key,
|
|
@@ -4139,7 +4409,7 @@ function serializeDataToServer(entity, properties, collection, registry) {
|
|
|
4139
4409
|
}
|
|
4140
4410
|
continue;
|
|
4141
4411
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
4142
|
-
const serializedValue = serializePropertyToServer(
|
|
4412
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4143
4413
|
joinPathRelationUpdates.push({
|
|
4144
4414
|
relationKey: key,
|
|
4145
4415
|
relation,
|
|
@@ -4149,15 +4419,13 @@ function serializeDataToServer(entity, properties, collection, registry) {
|
|
|
4149
4419
|
}
|
|
4150
4420
|
}
|
|
4151
4421
|
}
|
|
4152
|
-
result[key] = serializePropertyToServer(
|
|
4422
|
+
result[key] = serializePropertyToServer(effectiveValue, property);
|
|
4153
4423
|
}
|
|
4154
|
-
|
|
4155
|
-
result
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
}
|
|
4160
|
-
return result;
|
|
4424
|
+
return {
|
|
4425
|
+
scalarData: result,
|
|
4426
|
+
inverseRelationUpdates,
|
|
4427
|
+
joinPathRelationUpdates
|
|
4428
|
+
};
|
|
4161
4429
|
}
|
|
4162
4430
|
function serializePropertyToServer(value, property) {
|
|
4163
4431
|
if (value === null || value === void 0) {
|
|
@@ -4171,10 +4439,28 @@ function serializePropertyToServer(value, property) {
|
|
|
4171
4439
|
} else if (typeof value === "object" && value !== null && "id" in value) {
|
|
4172
4440
|
return value.id;
|
|
4173
4441
|
}
|
|
4442
|
+
if (value === "") return null;
|
|
4174
4443
|
return value;
|
|
4175
4444
|
case "array":
|
|
4176
|
-
if (Array.isArray(value)
|
|
4177
|
-
|
|
4445
|
+
if (Array.isArray(value)) {
|
|
4446
|
+
if (property.of) {
|
|
4447
|
+
return value.map((item) => serializePropertyToServer(item, property.of));
|
|
4448
|
+
} else if (property.oneOf) {
|
|
4449
|
+
const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
|
|
4450
|
+
const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
|
|
4451
|
+
return value.map((e) => {
|
|
4452
|
+
if (e === null) return null;
|
|
4453
|
+
if (typeof e !== "object") return e;
|
|
4454
|
+
const rec = e;
|
|
4455
|
+
const type = rec[typeField];
|
|
4456
|
+
const childProperty = property.oneOf?.properties[type];
|
|
4457
|
+
if (!type || !childProperty) return e;
|
|
4458
|
+
return {
|
|
4459
|
+
[typeField]: type,
|
|
4460
|
+
[valueField]: serializePropertyToServer(rec[valueField], childProperty)
|
|
4461
|
+
};
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4178
4464
|
}
|
|
4179
4465
|
return value;
|
|
4180
4466
|
case "map":
|
|
@@ -4198,38 +4484,20 @@ function serializePropertyToServer(value, property) {
|
|
|
4198
4484
|
async function parseDataFromServer(data, collection, db, registry) {
|
|
4199
4485
|
const properties = collection.properties;
|
|
4200
4486
|
if (!data || !properties) return data;
|
|
4201
|
-
const result = {};
|
|
4202
4487
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
if (relation.localKey && !properties[relation.localKey]) {
|
|
4206
|
-
internalFKColumns.add(relation.localKey);
|
|
4207
|
-
}
|
|
4488
|
+
const result = normalizeScalarValues(data, properties, collection, resolvedRelations, {
|
|
4489
|
+
skipRelations: false
|
|
4208
4490
|
});
|
|
4209
|
-
for (const [key, value] of Object.entries(data)) {
|
|
4210
|
-
if (internalFKColumns.has(key)) {
|
|
4211
|
-
continue;
|
|
4212
|
-
}
|
|
4213
|
-
const property = properties[key];
|
|
4214
|
-
if (!property) {
|
|
4215
|
-
continue;
|
|
4216
|
-
}
|
|
4217
|
-
result[key] = parsePropertyFromServer(value, property, collection, key);
|
|
4218
|
-
}
|
|
4219
4491
|
for (const [propKey, property] of Object.entries(properties)) {
|
|
4220
4492
|
if (property.type === "relation" && !(propKey in result)) {
|
|
4221
|
-
const relation = resolvedRelations
|
|
4493
|
+
const relation = findRelation(resolvedRelations, propKey);
|
|
4222
4494
|
if (relation) {
|
|
4223
4495
|
if (relation.direction === "owning" && relation.localKey && relation.localKey in data) {
|
|
4224
4496
|
const fkValue = data[relation.localKey];
|
|
4225
4497
|
if (fkValue !== null && fkValue !== void 0) {
|
|
4226
4498
|
try {
|
|
4227
4499
|
const targetCollection = relation.target();
|
|
4228
|
-
result[propKey] =
|
|
4229
|
-
id: fkValue.toString(),
|
|
4230
|
-
path: targetCollection.slug,
|
|
4231
|
-
__type: "relation"
|
|
4232
|
-
};
|
|
4500
|
+
result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
|
|
4233
4501
|
} catch (e) {
|
|
4234
4502
|
console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
|
|
4235
4503
|
}
|
|
@@ -4248,18 +4516,10 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
4248
4516
|
if (relation.cardinality === "one") {
|
|
4249
4517
|
const targetPks = getPrimaryKeys(targetCollection, registry);
|
|
4250
4518
|
const relatedEntity = relatedEntities[0];
|
|
4251
|
-
result[propKey] =
|
|
4252
|
-
id: buildCompositeId(relatedEntity, targetPks),
|
|
4253
|
-
path: targetCollection.slug,
|
|
4254
|
-
__type: "relation"
|
|
4255
|
-
};
|
|
4519
|
+
result[propKey] = createRelationRef(buildCompositeId(relatedEntity, targetPks), targetCollection.slug);
|
|
4256
4520
|
} else {
|
|
4257
4521
|
const targetPks = getPrimaryKeys(targetCollection, registry);
|
|
4258
|
-
result[propKey] = relatedEntities.map((entity) => (
|
|
4259
|
-
id: buildCompositeId(entity, targetPks),
|
|
4260
|
-
path: targetCollection.slug,
|
|
4261
|
-
__type: "relation"
|
|
4262
|
-
}));
|
|
4522
|
+
result[propKey] = relatedEntities.map((entity) => createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug));
|
|
4263
4523
|
}
|
|
4264
4524
|
}
|
|
4265
4525
|
}
|
|
@@ -4320,19 +4580,11 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
4320
4580
|
if (relation.cardinality === "one") {
|
|
4321
4581
|
const joinResult = joinResults[0];
|
|
4322
4582
|
const targetEntity = joinResult[targetTableName] || joinResult;
|
|
4323
|
-
result[propKey] =
|
|
4324
|
-
id: buildCompositeId(targetEntity, targetPks),
|
|
4325
|
-
path: targetCollection.slug,
|
|
4326
|
-
__type: "relation"
|
|
4327
|
-
};
|
|
4583
|
+
result[propKey] = createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
|
|
4328
4584
|
} else {
|
|
4329
4585
|
result[propKey] = joinResults.map((joinResult) => {
|
|
4330
4586
|
const targetEntity = joinResult[targetTableName] || joinResult;
|
|
4331
|
-
return
|
|
4332
|
-
id: buildCompositeId(targetEntity, targetPks),
|
|
4333
|
-
path: targetCollection.slug,
|
|
4334
|
-
__type: "relation"
|
|
4335
|
-
};
|
|
4587
|
+
return createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
|
|
4336
4588
|
});
|
|
4337
4589
|
}
|
|
4338
4590
|
}
|
|
@@ -4356,7 +4608,7 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
4356
4608
|
let relationDef = property.relation;
|
|
4357
4609
|
if (!relationDef && propertyKey) {
|
|
4358
4610
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4359
|
-
relationDef = resolvedRelations
|
|
4611
|
+
relationDef = findRelation(resolvedRelations, propertyKey);
|
|
4360
4612
|
}
|
|
4361
4613
|
if (!relationDef) {
|
|
4362
4614
|
relationDef = collection.relations?.find((rel) => rel.relationName === property.relationName);
|
|
@@ -4367,11 +4619,7 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
4367
4619
|
}
|
|
4368
4620
|
try {
|
|
4369
4621
|
const targetCollection = relationDef.target();
|
|
4370
|
-
return
|
|
4371
|
-
id: value.toString(),
|
|
4372
|
-
path: targetCollection.slug,
|
|
4373
|
-
__type: "relation"
|
|
4374
|
-
};
|
|
4622
|
+
return createRelationRef(value.toString(), targetCollection.slug);
|
|
4375
4623
|
} catch (e) {
|
|
4376
4624
|
console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
|
|
4377
4625
|
return value;
|
|
@@ -4379,8 +4627,25 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
4379
4627
|
}
|
|
4380
4628
|
return value;
|
|
4381
4629
|
case "array":
|
|
4382
|
-
if (Array.isArray(value)
|
|
4383
|
-
|
|
4630
|
+
if (Array.isArray(value)) {
|
|
4631
|
+
if (property.of) {
|
|
4632
|
+
return value.map((item) => parsePropertyFromServer(item, property.of, collection));
|
|
4633
|
+
} else if (property.oneOf) {
|
|
4634
|
+
const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
|
|
4635
|
+
const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
|
|
4636
|
+
return value.map((e) => {
|
|
4637
|
+
if (e === null) return null;
|
|
4638
|
+
if (typeof e !== "object") return e;
|
|
4639
|
+
const rec = e;
|
|
4640
|
+
const type = rec[typeField];
|
|
4641
|
+
const childProperty = property.oneOf?.properties[type];
|
|
4642
|
+
if (!type || !childProperty) return e;
|
|
4643
|
+
return {
|
|
4644
|
+
[typeField]: type,
|
|
4645
|
+
[valueField]: parsePropertyFromServer(rec[valueField], childProperty, collection)
|
|
4646
|
+
};
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
4384
4649
|
}
|
|
4385
4650
|
return value;
|
|
4386
4651
|
case "map":
|
|
@@ -4425,11 +4690,8 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
4425
4690
|
return value;
|
|
4426
4691
|
}
|
|
4427
4692
|
}
|
|
4428
|
-
function
|
|
4429
|
-
const properties = collection.properties;
|
|
4430
|
-
if (!data || !properties) return data;
|
|
4693
|
+
function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
|
|
4431
4694
|
const result = {};
|
|
4432
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4433
4695
|
const internalFKColumns = /* @__PURE__ */ new Set();
|
|
4434
4696
|
Object.values(resolvedRelations).forEach((relation) => {
|
|
4435
4697
|
if (relation.localKey && !properties[relation.localKey]) {
|
|
@@ -4437,14 +4699,25 @@ function normalizeDbValues(data, collection) {
|
|
|
4437
4699
|
}
|
|
4438
4700
|
});
|
|
4439
4701
|
for (const [key, value] of Object.entries(data)) {
|
|
4440
|
-
if (internalFKColumns.has(key))
|
|
4702
|
+
if (internalFKColumns.has(key)) {
|
|
4703
|
+
result[key] = value === null ? null : typeof value === "number" ? value : String(value);
|
|
4704
|
+
continue;
|
|
4705
|
+
}
|
|
4441
4706
|
const property = properties[key];
|
|
4442
4707
|
if (!property) continue;
|
|
4443
|
-
if (property.type === "relation") continue;
|
|
4708
|
+
if (options.skipRelations && property.type === "relation") continue;
|
|
4444
4709
|
result[key] = parsePropertyFromServer(value, property, collection, key);
|
|
4445
4710
|
}
|
|
4446
4711
|
return result;
|
|
4447
4712
|
}
|
|
4713
|
+
function normalizeDbValues(data, collection) {
|
|
4714
|
+
const properties = collection.properties;
|
|
4715
|
+
if (!data || !properties) return data;
|
|
4716
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4717
|
+
return normalizeScalarValues(data, properties, collection, resolvedRelations, {
|
|
4718
|
+
skipRelations: true
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4448
4721
|
class RelationService {
|
|
4449
4722
|
constructor(db, registry) {
|
|
4450
4723
|
this.db = db;
|
|
@@ -4456,9 +4729,10 @@ class RelationService {
|
|
|
4456
4729
|
async fetchRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
|
|
4457
4730
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
4458
4731
|
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
4459
|
-
const relation = resolvedRelations
|
|
4732
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
4460
4733
|
if (!relation) {
|
|
4461
|
-
|
|
4734
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
4735
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
4462
4736
|
}
|
|
4463
4737
|
return this.fetchEntitiesUsingJoins(parentCollection, parentEntityId, relation, options);
|
|
4464
4738
|
}
|
|
@@ -4558,8 +4832,11 @@ class RelationService {
|
|
|
4558
4832
|
async countRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
|
|
4559
4833
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
4560
4834
|
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
4561
|
-
const relation = resolvedRelations
|
|
4562
|
-
if (!relation)
|
|
4835
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
4836
|
+
if (!relation) {
|
|
4837
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
4838
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
4839
|
+
}
|
|
4563
4840
|
const targetCollection = relation.target();
|
|
4564
4841
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
4565
4842
|
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
@@ -4624,16 +4901,59 @@ class RelationService {
|
|
|
4624
4901
|
const results2 = await query2;
|
|
4625
4902
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
4626
4903
|
const resultMap2 = /* @__PURE__ */ new Map();
|
|
4627
|
-
|
|
4904
|
+
for (const row of results2) {
|
|
4628
4905
|
const parentEntity = row[getTableName(parentCollection)] || row;
|
|
4629
4906
|
const targetEntity = row[targetTableName] || row;
|
|
4630
4907
|
const parentId = parentEntity[parentIdInfo.fieldName];
|
|
4631
|
-
|
|
4908
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4909
|
+
resultMap2.set(String(parentId), {
|
|
4632
4910
|
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4633
4911
|
path: targetCollection.slug,
|
|
4634
|
-
values:
|
|
4912
|
+
values: parsedValues
|
|
4635
4913
|
});
|
|
4636
|
-
}
|
|
4914
|
+
}
|
|
4915
|
+
return resultMap2;
|
|
4916
|
+
}
|
|
4917
|
+
if (relation.direction === "owning" && relation.localKey) {
|
|
4918
|
+
const localKeyCol = parentTable[relation.localKey];
|
|
4919
|
+
if (!localKeyCol) {
|
|
4920
|
+
throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
4921
|
+
}
|
|
4922
|
+
const fkRows = await this.db.select({
|
|
4923
|
+
parentId: parentIdCol,
|
|
4924
|
+
fkValue: localKeyCol
|
|
4925
|
+
}).from(parentTable).where(inArray(parentIdCol, parsedParentIds));
|
|
4926
|
+
const parentToFk = /* @__PURE__ */ new Map();
|
|
4927
|
+
const uniqueFkValues = [];
|
|
4928
|
+
const seenFks = /* @__PURE__ */ new Set();
|
|
4929
|
+
for (const row of fkRows) {
|
|
4930
|
+
if (row.fkValue == null) continue;
|
|
4931
|
+
parentToFk.set(String(row.parentId), row.fkValue);
|
|
4932
|
+
const fkStr = String(row.fkValue);
|
|
4933
|
+
if (!seenFks.has(fkStr)) {
|
|
4934
|
+
seenFks.add(fkStr);
|
|
4935
|
+
uniqueFkValues.push(row.fkValue);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
if (uniqueFkValues.length === 0) return /* @__PURE__ */ new Map();
|
|
4939
|
+
const targetResults = await this.db.select().from(targetTable).where(inArray(targetIdField, uniqueFkValues));
|
|
4940
|
+
const targetById = /* @__PURE__ */ new Map();
|
|
4941
|
+
for (const row of targetResults) {
|
|
4942
|
+
const tid = String(row[targetIdInfo.fieldName]);
|
|
4943
|
+
targetById.set(tid, row);
|
|
4944
|
+
}
|
|
4945
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
4946
|
+
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
4947
|
+
const targetEntity = targetById.get(String(fkValue));
|
|
4948
|
+
if (targetEntity) {
|
|
4949
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4950
|
+
resultMap2.set(parentIdStr, {
|
|
4951
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4952
|
+
path: targetCollection.slug,
|
|
4953
|
+
values: parsedValues
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4637
4957
|
return resultMap2;
|
|
4638
4958
|
}
|
|
4639
4959
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
@@ -4651,7 +4971,8 @@ class RelationService {
|
|
|
4651
4971
|
);
|
|
4652
4972
|
const results = await query;
|
|
4653
4973
|
const resultMap = /* @__PURE__ */ new Map();
|
|
4654
|
-
|
|
4974
|
+
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
4975
|
+
for (const row of results) {
|
|
4655
4976
|
const targetEntity = row[getTableName(targetCollection)] || row;
|
|
4656
4977
|
let parentId;
|
|
4657
4978
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
@@ -4659,22 +4980,133 @@ class RelationService {
|
|
|
4659
4980
|
} else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
|
|
4660
4981
|
const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
|
|
4661
4982
|
parentId = targetEntity[inferredForeignKeyName];
|
|
4662
|
-
} else if (relation.direction === "owning" && relation.localKey) {
|
|
4663
|
-
for (const parsedParentId of parsedParentIds) {
|
|
4664
|
-
if (!resultMap.has(parsedParentId)) {
|
|
4665
|
-
parentId = parsedParentId;
|
|
4666
|
-
break;
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
4983
|
}
|
|
4670
|
-
if (parentId !== void 0 &&
|
|
4671
|
-
|
|
4984
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
4985
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4986
|
+
resultMap.set(String(parentId), {
|
|
4672
4987
|
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4673
4988
|
path: targetCollection.slug,
|
|
4674
|
-
values:
|
|
4989
|
+
values: parsedValues
|
|
4675
4990
|
});
|
|
4676
4991
|
}
|
|
4677
|
-
}
|
|
4992
|
+
}
|
|
4993
|
+
return resultMap;
|
|
4994
|
+
}
|
|
4995
|
+
/**
|
|
4996
|
+
* Batch fetch many-cardinality related entities for multiple parent entities.
|
|
4997
|
+
* Returns a Map<parentId, Entity[]> instead of Map<parentId, Entity>.
|
|
4998
|
+
* Uses a single SQL query with IN clause to avoid N+1.
|
|
4999
|
+
*/
|
|
5000
|
+
async batchFetchRelatedEntitiesMany(parentCollectionPath, parentEntityIds, _relationKey, relation) {
|
|
5001
|
+
if (parentEntityIds.length === 0) return /* @__PURE__ */ new Map();
|
|
5002
|
+
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5003
|
+
const targetCollection = relation.target();
|
|
5004
|
+
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5005
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
5006
|
+
const targetIdInfo = targetPks[0];
|
|
5007
|
+
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5008
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5009
|
+
const parentIdInfo = parentPks[0];
|
|
5010
|
+
const parentTable = this.registry.getTable(getTableName(parentCollection));
|
|
5011
|
+
if (!parentTable) throw new Error("Parent table not found");
|
|
5012
|
+
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
5013
|
+
const parsedParentIds = parentEntityIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
5014
|
+
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
5015
|
+
let query2 = this.db.select().from(parentTable).$dynamic();
|
|
5016
|
+
let currentTable = parentTable;
|
|
5017
|
+
for (const join of relation.joinPath) {
|
|
5018
|
+
const joinTable = this.registry.getTable(join.table);
|
|
5019
|
+
if (!joinTable) throw new Error(`Join table not found: ${join.table}`);
|
|
5020
|
+
const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
|
|
5021
|
+
const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
|
|
5022
|
+
const fromColName = fromColumn.split(".").pop();
|
|
5023
|
+
const toColName = toColumn.split(".").pop();
|
|
5024
|
+
const fromCol = currentTable[fromColName];
|
|
5025
|
+
const toCol = joinTable[toColName];
|
|
5026
|
+
if (!fromCol || !toCol) throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
|
|
5027
|
+
query2 = query2.innerJoin(joinTable, eq$3(fromCol, toCol));
|
|
5028
|
+
currentTable = joinTable;
|
|
5029
|
+
}
|
|
5030
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5031
|
+
query2 = query2.where(inArray(parentIdField, parsedParentIds));
|
|
5032
|
+
const results2 = await query2;
|
|
5033
|
+
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5034
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
5035
|
+
for (const row of results2) {
|
|
5036
|
+
const parentEntity = row[getTableName(parentCollection)] || row;
|
|
5037
|
+
const targetEntity = row[targetTableName] || row;
|
|
5038
|
+
const parentId = String(parentEntity[parentIdInfo.fieldName]);
|
|
5039
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
5040
|
+
const arr = resultMap2.get(parentId) || [];
|
|
5041
|
+
arr.push({
|
|
5042
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
5043
|
+
path: targetCollection.slug,
|
|
5044
|
+
values: parsedValues
|
|
5045
|
+
});
|
|
5046
|
+
resultMap2.set(parentId, arr);
|
|
5047
|
+
}
|
|
5048
|
+
return resultMap2;
|
|
5049
|
+
}
|
|
5050
|
+
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
5051
|
+
const junctionTable = this.registry.getTable(relation.through.table);
|
|
5052
|
+
if (!junctionTable) {
|
|
5053
|
+
console.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
5054
|
+
return /* @__PURE__ */ new Map();
|
|
5055
|
+
}
|
|
5056
|
+
const sourceJunctionCol = junctionTable[relation.through.sourceColumn];
|
|
5057
|
+
const targetJunctionCol = junctionTable[relation.through.targetColumn];
|
|
5058
|
+
if (!sourceJunctionCol || !targetJunctionCol) {
|
|
5059
|
+
console.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
|
|
5060
|
+
return /* @__PURE__ */ new Map();
|
|
5061
|
+
}
|
|
5062
|
+
const query2 = this.db.select().from(junctionTable).innerJoin(targetTable, eq$3(targetJunctionCol, targetIdField)).where(inArray(sourceJunctionCol, parsedParentIds));
|
|
5063
|
+
const results2 = await query2;
|
|
5064
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
5065
|
+
const targetTableName = getTableName(targetCollection);
|
|
5066
|
+
for (const row of results2) {
|
|
5067
|
+
const junctionData = row[relation.through.table] || row;
|
|
5068
|
+
const targetData = row[targetTableName] || row;
|
|
5069
|
+
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5070
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
5071
|
+
const arr = resultMap2.get(parentId) || [];
|
|
5072
|
+
arr.push({
|
|
5073
|
+
id: String(targetData[targetIdInfo.fieldName]),
|
|
5074
|
+
path: targetCollection.slug,
|
|
5075
|
+
values: parsedValues
|
|
5076
|
+
});
|
|
5077
|
+
resultMap2.set(parentId, arr);
|
|
5078
|
+
}
|
|
5079
|
+
return resultMap2;
|
|
5080
|
+
}
|
|
5081
|
+
let query = this.db.select().from(targetTable).$dynamic();
|
|
5082
|
+
query = DrizzleConditionBuilder.buildRelationQuery(query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5083
|
+
const results = await query;
|
|
5084
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
5085
|
+
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
5086
|
+
for (const row of results) {
|
|
5087
|
+
const targetEntity = row[getTableName(targetCollection)] || row;
|
|
5088
|
+
let parentId;
|
|
5089
|
+
if (relation.through && relation.direction === "inverse") {
|
|
5090
|
+
const junctionData = row[relation.through.table] || row;
|
|
5091
|
+
parentId = junctionData[relation.through.targetColumn];
|
|
5092
|
+
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5093
|
+
parentId = targetEntity[relation.foreignKeyOnTarget];
|
|
5094
|
+
} else if (relation.direction === "inverse" && relation.inverseRelationName) {
|
|
5095
|
+
const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
|
|
5096
|
+
parentId = targetEntity[inferredForeignKeyName];
|
|
5097
|
+
}
|
|
5098
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5099
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
5100
|
+
const key = String(parentId);
|
|
5101
|
+
const arr = resultMap.get(key) || [];
|
|
5102
|
+
arr.push({
|
|
5103
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
5104
|
+
path: targetCollection.slug,
|
|
5105
|
+
values: parsedValues
|
|
5106
|
+
});
|
|
5107
|
+
resultMap.set(key, arr);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
4678
5110
|
return resultMap;
|
|
4679
5111
|
}
|
|
4680
5112
|
/**
|
|
@@ -4683,7 +5115,7 @@ class RelationService {
|
|
|
4683
5115
|
async updateRelationsUsingJoins(tx, collection, entityId, relationValues) {
|
|
4684
5116
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4685
5117
|
for (const [key, value] of Object.entries(relationValues)) {
|
|
4686
|
-
const relation = resolvedRelations
|
|
5118
|
+
const relation = findRelation(resolvedRelations, key);
|
|
4687
5119
|
if (!relation || relation.cardinality !== "many") continue;
|
|
4688
5120
|
const targetEntityIds = value && Array.isArray(value) ? value.map((rel) => rel.id) : [];
|
|
4689
5121
|
const targetCollection = relation.target();
|
|
@@ -4767,6 +5199,8 @@ class RelationService {
|
|
|
4767
5199
|
await tx.insert(junctionTable).values(newLinks);
|
|
4768
5200
|
}
|
|
4769
5201
|
}
|
|
5202
|
+
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
|
|
5203
|
+
console.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
|
|
4770
5204
|
} else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
4771
5205
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
4772
5206
|
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
@@ -5144,6 +5578,20 @@ class EntityFetchService {
|
|
|
5144
5578
|
// =============================================================
|
|
5145
5579
|
// DRIZZLE QUERY HELPERS
|
|
5146
5580
|
// =============================================================
|
|
5581
|
+
/**
|
|
5582
|
+
* Resolves the correct Drizzle column for sorting.
|
|
5583
|
+
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
5584
|
+
*/
|
|
5585
|
+
resolveOrderByField(table, orderBy, collection) {
|
|
5586
|
+
let orderByField = table[orderBy];
|
|
5587
|
+
if (!orderByField && collection) {
|
|
5588
|
+
const property = collection.properties[orderBy];
|
|
5589
|
+
if (property && property.type === "relation" && "relation" in property && property.relation?.direction === "owning") {
|
|
5590
|
+
orderByField = table[`${orderBy}_id`];
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
return orderByField;
|
|
5594
|
+
}
|
|
5147
5595
|
/**
|
|
5148
5596
|
* Build the `with` config for Drizzle's relational query API.
|
|
5149
5597
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
@@ -5156,12 +5604,10 @@ class EntityFetchService {
|
|
|
5156
5604
|
*/
|
|
5157
5605
|
buildWithConfig(collection, include) {
|
|
5158
5606
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5159
|
-
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5160
5607
|
const withConfig = {};
|
|
5161
5608
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
5162
5609
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
5163
5610
|
if (!shouldInclude(key)) continue;
|
|
5164
|
-
if (!include && !propertyKeys.has(key)) continue;
|
|
5165
5611
|
const drizzleRelName = relation.relationName || key;
|
|
5166
5612
|
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
5167
5613
|
continue;
|
|
@@ -5211,10 +5657,8 @@ class EntityFetchService {
|
|
|
5211
5657
|
*/
|
|
5212
5658
|
drizzleResultToEntity(row, collection, collectionPath, idInfo, databaseId, idInfoArray) {
|
|
5213
5659
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5214
|
-
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5215
5660
|
const normalizedValues = normalizeDbValues(row, collection);
|
|
5216
5661
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
5217
|
-
if (!propertyKeys.has(key)) continue;
|
|
5218
5662
|
const drizzleRelName = relation.relationName || key;
|
|
5219
5663
|
const relData = row[drizzleRelName];
|
|
5220
5664
|
if (relData === void 0 || relData === null) continue;
|
|
@@ -5233,17 +5677,12 @@ class EntityFetchService {
|
|
|
5233
5677
|
}
|
|
5234
5678
|
const relId = String(targetEntity[targetIdField] ?? targetEntity.id ?? targetEntity[Object.keys(targetEntity)[0]]);
|
|
5235
5679
|
const targetValues = normalizeDbValues(targetEntity, targetCollection);
|
|
5236
|
-
return {
|
|
5680
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
5237
5681
|
id: relId,
|
|
5238
5682
|
path: targetPath,
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
path: targetPath,
|
|
5243
|
-
values: targetValues,
|
|
5244
|
-
databaseId
|
|
5245
|
-
}
|
|
5246
|
-
};
|
|
5683
|
+
values: targetValues,
|
|
5684
|
+
databaseId
|
|
5685
|
+
});
|
|
5247
5686
|
});
|
|
5248
5687
|
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
5249
5688
|
const targetCollection = relation.target();
|
|
@@ -5253,17 +5692,12 @@ class EntityFetchService {
|
|
|
5253
5692
|
const relObj = relData;
|
|
5254
5693
|
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
5255
5694
|
const targetValues = normalizeDbValues(relObj, targetCollection);
|
|
5256
|
-
normalizedValues[key] = {
|
|
5695
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
5257
5696
|
id: relId,
|
|
5258
5697
|
path: targetPath,
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
path: targetPath,
|
|
5263
|
-
values: targetValues,
|
|
5264
|
-
databaseId
|
|
5265
|
-
}
|
|
5266
|
-
};
|
|
5698
|
+
values: targetValues,
|
|
5699
|
+
databaseId
|
|
5700
|
+
});
|
|
5267
5701
|
}
|
|
5268
5702
|
}
|
|
5269
5703
|
return {
|
|
@@ -5280,27 +5714,16 @@ class EntityFetchService {
|
|
|
5280
5714
|
*/
|
|
5281
5715
|
async resolveJoinPathRelations(entity, collection, collectionPath, parsedId, databaseId) {
|
|
5282
5716
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5283
|
-
const
|
|
5284
|
-
const promises2 = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
|
|
5717
|
+
const promises2 = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
|
|
5285
5718
|
try {
|
|
5286
5719
|
const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {
|
|
5287
5720
|
limit: relation.cardinality === "one" ? 1 : void 0
|
|
5288
5721
|
});
|
|
5289
5722
|
if (relation.cardinality === "one" && relatedEntities.length > 0) {
|
|
5290
5723
|
const e = relatedEntities[0];
|
|
5291
|
-
entity.values[key] =
|
|
5292
|
-
id: e.id,
|
|
5293
|
-
path: e.path,
|
|
5294
|
-
__type: "relation",
|
|
5295
|
-
data: e
|
|
5296
|
-
};
|
|
5724
|
+
entity.values[key] = createRelationRefWithData(e.id, e.path, e);
|
|
5297
5725
|
} else if (relation.cardinality === "many") {
|
|
5298
|
-
entity.values[key] = relatedEntities.map((e) => (
|
|
5299
|
-
id: e.id,
|
|
5300
|
-
path: e.path,
|
|
5301
|
-
__type: "relation",
|
|
5302
|
-
data: e
|
|
5303
|
-
}));
|
|
5726
|
+
entity.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
|
|
5304
5727
|
}
|
|
5305
5728
|
} catch (e) {
|
|
5306
5729
|
console.warn(`Could not resolve joinPath relation '${key}':`, e);
|
|
@@ -5315,8 +5738,7 @@ class EntityFetchService {
|
|
|
5315
5738
|
async resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, databaseId) {
|
|
5316
5739
|
if (entities.length === 0) return;
|
|
5317
5740
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5318
|
-
const
|
|
5319
|
-
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0);
|
|
5741
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
5320
5742
|
if (joinPathRelations.length === 0) return;
|
|
5321
5743
|
for (const [key, relation] of joinPathRelations) {
|
|
5322
5744
|
try {
|
|
@@ -5328,15 +5750,10 @@ class EntityFetchService {
|
|
|
5328
5750
|
for (const entity of entities) {
|
|
5329
5751
|
const parsed = parseIdValues(entity.id, [idInfo]);
|
|
5330
5752
|
const entityId = parsed[idInfo.fieldName];
|
|
5331
|
-
const relatedEntity = resultMap.get(entityId);
|
|
5753
|
+
const relatedEntity = resultMap.get(String(entityId));
|
|
5332
5754
|
if (relatedEntity) {
|
|
5333
5755
|
if (relation.cardinality === "one") {
|
|
5334
|
-
entity.values[key] =
|
|
5335
|
-
id: relatedEntity.id,
|
|
5336
|
-
path: relatedEntity.path,
|
|
5337
|
-
__type: "relation",
|
|
5338
|
-
data: relatedEntity
|
|
5339
|
-
};
|
|
5756
|
+
entity.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
|
|
5340
5757
|
}
|
|
5341
5758
|
}
|
|
5342
5759
|
}
|
|
@@ -5346,22 +5763,72 @@ class EntityFetchService {
|
|
|
5346
5763
|
}
|
|
5347
5764
|
}
|
|
5348
5765
|
/**
|
|
5349
|
-
*
|
|
5766
|
+
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
5767
|
+
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
5350
5768
|
*/
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
5354
|
-
};
|
|
5769
|
+
async resolveJoinPathRelationsBatchRest(rows, collection, collectionPath, idInfoArray, include) {
|
|
5770
|
+
if (rows.length === 0) return;
|
|
5355
5771
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5772
|
+
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5773
|
+
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
5774
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
5775
|
+
if (joinPathRelations.length === 0) return;
|
|
5776
|
+
const idInfo = idInfoArray[0];
|
|
5777
|
+
for (const [key, relation] of joinPathRelations) {
|
|
5778
|
+
try {
|
|
5779
|
+
const entityIds = rows.map((r) => {
|
|
5780
|
+
const parsed = parseIdValues(String(r.id), idInfoArray);
|
|
5781
|
+
return parsed[idInfo.fieldName];
|
|
5782
|
+
});
|
|
5783
|
+
if (relation.cardinality === "one") {
|
|
5784
|
+
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5785
|
+
for (const row of rows) {
|
|
5786
|
+
const parsed = parseIdValues(String(row.id), idInfoArray);
|
|
5787
|
+
const entityId = parsed[idInfo.fieldName];
|
|
5788
|
+
const relatedEntity = resultMap.get(String(entityId));
|
|
5789
|
+
if (relatedEntity) {
|
|
5790
|
+
row[key] = {
|
|
5791
|
+
id: relatedEntity.id,
|
|
5792
|
+
...relatedEntity.values
|
|
5793
|
+
};
|
|
5794
|
+
} else {
|
|
5795
|
+
row[key] = null;
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
} else if (relation.cardinality === "many") {
|
|
5799
|
+
const resultMap = await this.batchFetchManyRelatedEntities(collectionPath, entityIds, key);
|
|
5800
|
+
for (const row of rows) {
|
|
5801
|
+
const parsed = parseIdValues(String(row.id), idInfoArray);
|
|
5802
|
+
const entityId = parsed[idInfo.fieldName];
|
|
5803
|
+
const relatedList = resultMap.get(String(entityId)) || [];
|
|
5804
|
+
row[key] = relatedList.map((e) => ({
|
|
5805
|
+
id: e.id,
|
|
5806
|
+
...e.values
|
|
5807
|
+
}));
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
} catch (e) {
|
|
5811
|
+
console.warn(`Could not batch resolve joinPath relation '${key}' for REST:`, e);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
/**
|
|
5816
|
+
* Convert a db.query result row to a flat REST-style object with populated relations.
|
|
5817
|
+
*/
|
|
5818
|
+
drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
|
|
5819
|
+
const flat = {
|
|
5820
|
+
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
5821
|
+
};
|
|
5822
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5823
|
+
for (const [k, v] of Object.entries(row)) {
|
|
5824
|
+
if (k === idInfo.fieldName) continue;
|
|
5825
|
+
const relation = findRelation(resolvedRelations, k);
|
|
5826
|
+
if (Array.isArray(v) && relation) {
|
|
5827
|
+
flat[k] = v.map((item) => {
|
|
5828
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
5829
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
5830
|
+
if (nestedKey) {
|
|
5831
|
+
const nested = item[nestedKey];
|
|
5365
5832
|
return {
|
|
5366
5833
|
id: String(nested.id ?? nested[Object.keys(nested)[0]]),
|
|
5367
5834
|
...nested
|
|
@@ -5407,7 +5874,7 @@ class EntityFetchService {
|
|
|
5407
5874
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5408
5875
|
}
|
|
5409
5876
|
if (options.startAfter) {
|
|
5410
|
-
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
|
|
5877
|
+
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
5411
5878
|
if (cursorConditions.length > 0) allConditions.push(...cursorConditions);
|
|
5412
5879
|
}
|
|
5413
5880
|
if (allConditions.length > 0) {
|
|
@@ -5415,7 +5882,8 @@ class EntityFetchService {
|
|
|
5415
5882
|
}
|
|
5416
5883
|
const orderExpressions = [];
|
|
5417
5884
|
if (options.orderBy) {
|
|
5418
|
-
const
|
|
5885
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
5886
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5419
5887
|
if (orderByField) {
|
|
5420
5888
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
5421
5889
|
}
|
|
@@ -5426,16 +5894,18 @@ class EntityFetchService {
|
|
|
5426
5894
|
}
|
|
5427
5895
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5428
5896
|
if (limitValue) queryOpts.limit = limitValue;
|
|
5897
|
+
if (options.offset && options.offset > 0) queryOpts.offset = options.offset;
|
|
5429
5898
|
return queryOpts;
|
|
5430
5899
|
}
|
|
5431
5900
|
/**
|
|
5432
5901
|
* Extract cursor pagination conditions from startAfter options.
|
|
5433
5902
|
*/
|
|
5434
|
-
buildCursorConditions(table, idField, idInfo, options) {
|
|
5903
|
+
buildCursorConditions(table, idField, idInfo, options, collectionPath) {
|
|
5435
5904
|
if (!options.startAfter) return [];
|
|
5436
5905
|
const cursor = options.startAfter;
|
|
5437
5906
|
if (options.orderBy) {
|
|
5438
|
-
const
|
|
5907
|
+
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
|
|
5908
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5439
5909
|
if (orderByField) {
|
|
5440
5910
|
const startAfterOrderValue = cursor.values?.[options.orderBy] ?? cursor[options.orderBy];
|
|
5441
5911
|
const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
|
|
@@ -5497,11 +5967,7 @@ class EntityFetchService {
|
|
|
5497
5967
|
const relationPromises = Object.entries(resolvedRelations).filter(([key]) => propertyKeys.has(key)).map(async ([key, relation]) => {
|
|
5498
5968
|
if (relation.cardinality === "many") {
|
|
5499
5969
|
const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {});
|
|
5500
|
-
values[key] = relatedEntities.map((e) => (
|
|
5501
|
-
id: e.id,
|
|
5502
|
-
path: e.path,
|
|
5503
|
-
__type: "relation"
|
|
5504
|
-
}));
|
|
5970
|
+
values[key] = relatedEntities.map((e) => createRelationRef(e.id, e.path));
|
|
5505
5971
|
} else if (relation.cardinality === "one") {
|
|
5506
5972
|
if (values[key] == null) {
|
|
5507
5973
|
try {
|
|
@@ -5510,11 +5976,7 @@ class EntityFetchService {
|
|
|
5510
5976
|
});
|
|
5511
5977
|
if (relatedEntities.length > 0) {
|
|
5512
5978
|
const e = relatedEntities[0];
|
|
5513
|
-
values[key] =
|
|
5514
|
-
id: e.id,
|
|
5515
|
-
path: e.path,
|
|
5516
|
-
__type: "relation"
|
|
5517
|
-
};
|
|
5979
|
+
values[key] = createRelationRef(e.id, e.path);
|
|
5518
5980
|
}
|
|
5519
5981
|
} catch (e) {
|
|
5520
5982
|
console.warn(`Could not resolve one-to-one relation property: ${key}`, e);
|
|
@@ -5544,13 +6006,13 @@ class EntityFetchService {
|
|
|
5544
6006
|
}
|
|
5545
6007
|
const tableName = getTableName$1(table);
|
|
5546
6008
|
const qb = this.getQueryBuilder(tableName);
|
|
5547
|
-
|
|
6009
|
+
const withConfig = this.buildWithConfig(collection);
|
|
6010
|
+
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6011
|
+
if (qb && !options.searchString && !hasRelations) {
|
|
5548
6012
|
try {
|
|
5549
|
-
const
|
|
5550
|
-
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
6013
|
+
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5551
6014
|
const results2 = await qb.findMany(queryOpts);
|
|
5552
6015
|
const entities = results2.map((row) => this.drizzleResultToEntity(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray));
|
|
5553
|
-
await this.resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, options.databaseId);
|
|
5554
6016
|
return entities;
|
|
5555
6017
|
} catch (e) {
|
|
5556
6018
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
@@ -5573,7 +6035,7 @@ class EntityFetchService {
|
|
|
5573
6035
|
}
|
|
5574
6036
|
const orderExpressions = [];
|
|
5575
6037
|
if (options.orderBy) {
|
|
5576
|
-
const orderByField = table
|
|
6038
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5577
6039
|
if (orderByField) {
|
|
5578
6040
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
5579
6041
|
}
|
|
@@ -5581,7 +6043,7 @@ class EntityFetchService {
|
|
|
5581
6043
|
orderExpressions.push(desc(idField));
|
|
5582
6044
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5583
6045
|
if (options.startAfter) {
|
|
5584
|
-
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
|
|
6046
|
+
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
5585
6047
|
if (cursorConditions.length > 0) {
|
|
5586
6048
|
allConditions.push(...cursorConditions);
|
|
5587
6049
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
@@ -5590,6 +6052,7 @@ class EntityFetchService {
|
|
|
5590
6052
|
}
|
|
5591
6053
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5592
6054
|
if (limitValue) query = query.limit(limitValue);
|
|
6055
|
+
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5593
6056
|
const results = await query;
|
|
5594
6057
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5595
6058
|
}
|
|
@@ -5603,7 +6066,7 @@ class EntityFetchService {
|
|
|
5603
6066
|
async processEntityResults(results, collection, collectionPath, idInfo, databaseId, skipRelations = false, idInfoArray) {
|
|
5604
6067
|
if (results.length === 0) return [];
|
|
5605
6068
|
const entitiesWithValues = await Promise.all(results.map(async (entity) => {
|
|
5606
|
-
const values = await parseDataFromServer(entity, collection
|
|
6069
|
+
const values = await parseDataFromServer(entity, collection);
|
|
5607
6070
|
return {
|
|
5608
6071
|
entity,
|
|
5609
6072
|
values,
|
|
@@ -5628,37 +6091,29 @@ class EntityFetchService {
|
|
|
5628
6091
|
const relationResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5629
6092
|
entitiesMissingRelation.forEach((item) => {
|
|
5630
6093
|
const entityId = item.entity[idInfo.fieldName];
|
|
5631
|
-
const relatedEntity = relationResults.get(entityId);
|
|
6094
|
+
const relatedEntity = relationResults.get(String(entityId));
|
|
5632
6095
|
if (relatedEntity) {
|
|
5633
|
-
item.values[key] =
|
|
5634
|
-
id: relatedEntity.id,
|
|
5635
|
-
path: relatedEntity.path,
|
|
5636
|
-
__type: "relation",
|
|
5637
|
-
data: relatedEntity
|
|
5638
|
-
};
|
|
6096
|
+
item.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
|
|
5639
6097
|
}
|
|
5640
6098
|
});
|
|
5641
6099
|
} catch (e) {
|
|
5642
6100
|
console.warn(`Could not batch load one-to-one relation property: ${key}`, e);
|
|
5643
6101
|
}
|
|
5644
6102
|
}
|
|
5645
|
-
const
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
await Promise.all(manyRelationQueries);
|
|
5660
|
-
});
|
|
5661
|
-
await Promise.all(manyRelationPromises);
|
|
6103
|
+
const manyRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many");
|
|
6104
|
+
for (const [key, relation] of manyRelations) {
|
|
6105
|
+
try {
|
|
6106
|
+
const entityIds = entitiesWithValues.map((item) => item.entity[idInfo.fieldName]);
|
|
6107
|
+
const relationResults = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, entityIds, key, relation);
|
|
6108
|
+
entitiesWithValues.forEach((item) => {
|
|
6109
|
+
const entityId = String(item.entity[idInfo.fieldName]);
|
|
6110
|
+
const relatedEntities = relationResults.get(entityId) || [];
|
|
6111
|
+
item.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
|
|
6112
|
+
});
|
|
6113
|
+
} catch (e) {
|
|
6114
|
+
console.warn(`Could not batch load many relation property: ${key}`, e);
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
5662
6117
|
}
|
|
5663
6118
|
return entitiesWithValues.map((item) => ({
|
|
5664
6119
|
id: item.id,
|
|
@@ -5699,12 +6154,16 @@ class EntityFetchService {
|
|
|
5699
6154
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
5700
6155
|
const relationKey = pathSegments[i];
|
|
5701
6156
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
5702
|
-
const relation = resolvedRelations
|
|
6157
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
5703
6158
|
if (!relation) {
|
|
5704
6159
|
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
5705
6160
|
}
|
|
5706
6161
|
if (i === pathSegments.length - 1) {
|
|
5707
|
-
|
|
6162
|
+
const entities = await this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
|
|
6163
|
+
for (const entity of entities) {
|
|
6164
|
+
entity.path = path2;
|
|
6165
|
+
}
|
|
6166
|
+
return entities;
|
|
5708
6167
|
}
|
|
5709
6168
|
if (i + 1 < pathSegments.length) {
|
|
5710
6169
|
const nextEntityId = pathSegments[i + 1];
|
|
@@ -5726,11 +6185,19 @@ class EntityFetchService {
|
|
|
5726
6185
|
let query = this.db.select({
|
|
5727
6186
|
count: count()
|
|
5728
6187
|
}).from(table).$dynamic();
|
|
6188
|
+
const allConditions = [];
|
|
6189
|
+
if (options.searchString) {
|
|
6190
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
6191
|
+
if (searchConditions.length === 0) return 0;
|
|
6192
|
+
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
6193
|
+
}
|
|
5729
6194
|
if (options.filter) {
|
|
5730
6195
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5731
|
-
if (filterConditions.length > 0)
|
|
5732
|
-
|
|
5733
|
-
|
|
6196
|
+
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
6197
|
+
}
|
|
6198
|
+
if (allConditions.length > 0) {
|
|
6199
|
+
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
6200
|
+
if (finalCondition) query = query.where(finalCondition);
|
|
5734
6201
|
}
|
|
5735
6202
|
const result = await query;
|
|
5736
6203
|
return Number(result[0]?.count || 0);
|
|
@@ -5749,7 +6216,7 @@ class EntityFetchService {
|
|
|
5749
6216
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
5750
6217
|
const relationKey = pathSegments[i];
|
|
5751
6218
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
5752
|
-
const relation = resolvedRelations
|
|
6219
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
5753
6220
|
if (!relation) {
|
|
5754
6221
|
throw new Error(`Relation '${relationKey}' not found`);
|
|
5755
6222
|
}
|
|
@@ -5808,12 +6275,14 @@ class EntityFetchService {
|
|
|
5808
6275
|
const idField = table[idInfo.fieldName];
|
|
5809
6276
|
const tableName = getTableName$1(table);
|
|
5810
6277
|
const qb = this.getQueryBuilder(tableName);
|
|
5811
|
-
if (qb) {
|
|
6278
|
+
if (qb && !options.searchString) {
|
|
5812
6279
|
try {
|
|
5813
6280
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5814
6281
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
5815
6282
|
const results = await qb.findMany(queryOpts);
|
|
5816
|
-
|
|
6283
|
+
const restRows = results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
|
|
6284
|
+
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
6285
|
+
return restRows;
|
|
5817
6286
|
} catch (e) {
|
|
5818
6287
|
console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
|
|
5819
6288
|
}
|
|
@@ -5835,7 +6304,7 @@ class EntityFetchService {
|
|
|
5835
6304
|
const batchResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5836
6305
|
for (const entity of entities) {
|
|
5837
6306
|
const eid = entity[idInfo.fieldName];
|
|
5838
|
-
const related = batchResults.get(eid);
|
|
6307
|
+
const related = batchResults.get(String(eid));
|
|
5839
6308
|
if (related) {
|
|
5840
6309
|
entity[key] = {
|
|
5841
6310
|
id: related.id,
|
|
@@ -5891,7 +6360,9 @@ class EntityFetchService {
|
|
|
5891
6360
|
} : {}
|
|
5892
6361
|
});
|
|
5893
6362
|
if (!row) return null;
|
|
5894
|
-
|
|
6363
|
+
const restRow = this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
|
|
6364
|
+
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
6365
|
+
return restRow;
|
|
5895
6366
|
} catch (e) {
|
|
5896
6367
|
console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
|
|
5897
6368
|
}
|
|
@@ -5959,7 +6430,7 @@ class EntityFetchService {
|
|
|
5959
6430
|
}
|
|
5960
6431
|
const orderExpressions = [];
|
|
5961
6432
|
if (options.orderBy) {
|
|
5962
|
-
const orderByField = table
|
|
6433
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5963
6434
|
if (orderByField) {
|
|
5964
6435
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
5965
6436
|
}
|
|
@@ -5968,6 +6439,7 @@ class EntityFetchService {
|
|
|
5968
6439
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5969
6440
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5970
6441
|
if (limitValue) query = query.limit(limitValue);
|
|
6442
|
+
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5971
6443
|
return await query;
|
|
5972
6444
|
}
|
|
5973
6445
|
/**
|
|
@@ -6016,7 +6488,7 @@ class EntityFetchService {
|
|
|
6016
6488
|
}
|
|
6017
6489
|
}
|
|
6018
6490
|
if (options.orderBy) {
|
|
6019
|
-
const orderByField = table
|
|
6491
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
6020
6492
|
if (orderByField) {
|
|
6021
6493
|
queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
6022
6494
|
}
|
|
@@ -6070,17 +6542,15 @@ class EntityFetchService {
|
|
|
6070
6542
|
* Groups results by parent ID to avoid N+1.
|
|
6071
6543
|
*/
|
|
6072
6544
|
async batchFetchManyRelatedEntities(parentCollectionPath, parentIds, relationKey) {
|
|
6073
|
-
|
|
6074
|
-
const
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
}
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
await Promise.all(batchPromises);
|
|
6083
|
-
return resultMap;
|
|
6545
|
+
if (parentIds.length === 0) return /* @__PURE__ */ new Map();
|
|
6546
|
+
const collection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
6547
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6548
|
+
const relation = resolvedRelations[relationKey];
|
|
6549
|
+
if (!relation) {
|
|
6550
|
+
console.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
|
|
6551
|
+
return /* @__PURE__ */ new Map();
|
|
6552
|
+
}
|
|
6553
|
+
return this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
|
|
6084
6554
|
}
|
|
6085
6555
|
}
|
|
6086
6556
|
class EntityPersistService {
|
|
@@ -6116,6 +6586,7 @@ class EntityPersistService {
|
|
|
6116
6586
|
const effectiveValues = {
|
|
6117
6587
|
...values
|
|
6118
6588
|
};
|
|
6589
|
+
let junctionTableInfo;
|
|
6119
6590
|
if (collectionPath.includes("/")) {
|
|
6120
6591
|
const segments = collectionPath.split("/").filter(Boolean);
|
|
6121
6592
|
if (segments.length >= 3 && segments.length % 2 === 1) {
|
|
@@ -6125,9 +6596,10 @@ class EntityPersistService {
|
|
|
6125
6596
|
for (let i = 2; i < segments.length; i += 2) {
|
|
6126
6597
|
const relationKey = segments[i];
|
|
6127
6598
|
const resolvedRelations2 = resolveCollectionRelations(currentCollection);
|
|
6128
|
-
const relation = resolvedRelations2
|
|
6599
|
+
const relation = findRelation(resolvedRelations2, relationKey);
|
|
6129
6600
|
if (!relation) {
|
|
6130
|
-
|
|
6601
|
+
const available = Object.keys(resolvedRelations2).join(", ") || "(none)";
|
|
6602
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
|
|
6131
6603
|
}
|
|
6132
6604
|
if (i === segments.length - 1) {
|
|
6133
6605
|
const targetCollection = relation.target();
|
|
@@ -6137,7 +6609,7 @@ class EntityPersistService {
|
|
|
6137
6609
|
const parentIdInfo2 = parentIdInfoArray2[0];
|
|
6138
6610
|
const parsedParentIdObj2 = parseIdValues(currentEntityId, parentIdInfoArray2);
|
|
6139
6611
|
const parsedParentId2 = parsedParentIdObj2[parentIdInfo2.fieldName];
|
|
6140
|
-
|
|
6612
|
+
junctionTableInfo = {
|
|
6141
6613
|
parentCollection: currentCollection,
|
|
6142
6614
|
parentId: parsedParentId2,
|
|
6143
6615
|
relation,
|
|
@@ -6206,14 +6678,10 @@ class EntityPersistService {
|
|
|
6206
6678
|
}
|
|
6207
6679
|
}
|
|
6208
6680
|
}
|
|
6209
|
-
const
|
|
6210
|
-
const inverseRelationUpdates =
|
|
6211
|
-
const joinPathRelationUpdates =
|
|
6212
|
-
const
|
|
6213
|
-
delete processedData.__inverseRelationUpdates;
|
|
6214
|
-
delete processedData.__joinPathRelationUpdates;
|
|
6215
|
-
delete processedData.__junction_table_info;
|
|
6216
|
-
const entityData = sanitizeAndConvertDates(processedData);
|
|
6681
|
+
const serializedResult = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
|
|
6682
|
+
const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
|
|
6683
|
+
const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
|
|
6684
|
+
const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
|
|
6217
6685
|
let savedId;
|
|
6218
6686
|
try {
|
|
6219
6687
|
savedId = await this.db.transaction(async (tx) => {
|
|
@@ -6226,7 +6694,7 @@ class EntityPersistService {
|
|
|
6226
6694
|
}
|
|
6227
6695
|
const scalarKeys = Object.keys(entityData);
|
|
6228
6696
|
if (scalarKeys.length > 0) {
|
|
6229
|
-
|
|
6697
|
+
const updateQuery = tx.update(table).set(entityData);
|
|
6230
6698
|
const conditions = [];
|
|
6231
6699
|
for (const info of idInfoArray) {
|
|
6232
6700
|
const field = table[info.fieldName];
|
|
@@ -6590,21 +7058,6 @@ class PostgresBackendDriver {
|
|
|
6590
7058
|
if (poolManager) {
|
|
6591
7059
|
this.branchService = new BranchService(db, poolManager);
|
|
6592
7060
|
}
|
|
6593
|
-
this.admin = {
|
|
6594
|
-
executeSql: this.executeSql.bind(this),
|
|
6595
|
-
fetchAvailableDatabases: this.fetchAvailableDatabases.bind(this),
|
|
6596
|
-
fetchAvailableRoles: this.fetchAvailableRoles.bind(this),
|
|
6597
|
-
fetchCurrentDatabase: this.fetchCurrentDatabase.bind(this),
|
|
6598
|
-
fetchUnmappedTables: this.fetchUnmappedTables.bind(this),
|
|
6599
|
-
fetchTableMetadata: this.fetchTableMetadata.bind(this),
|
|
6600
|
-
// Branch operations (only available when poolManager is configured)
|
|
6601
|
-
...this.branchService ? {
|
|
6602
|
-
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
6603
|
-
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
6604
|
-
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
6605
|
-
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
6606
|
-
} : {}
|
|
6607
|
-
};
|
|
6608
7061
|
}
|
|
6609
7062
|
key = "postgres";
|
|
6610
7063
|
initialised = true;
|
|
@@ -6622,8 +7075,26 @@ class PostgresBackendDriver {
|
|
|
6622
7075
|
_pendingNotifications = [];
|
|
6623
7076
|
/**
|
|
6624
7077
|
* Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin).
|
|
7078
|
+
* Implemented as a getter so method references are resolved at call-time,
|
|
7079
|
+
* allowing test spies applied after construction to take effect.
|
|
6625
7080
|
*/
|
|
6626
|
-
admin
|
|
7081
|
+
get admin() {
|
|
7082
|
+
return {
|
|
7083
|
+
executeSql: (...args) => this.executeSql(...args),
|
|
7084
|
+
fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
|
|
7085
|
+
fetchAvailableRoles: () => this.fetchAvailableRoles(),
|
|
7086
|
+
fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
|
|
7087
|
+
fetchUnmappedTables: (...args) => this.fetchUnmappedTables(...args),
|
|
7088
|
+
fetchTableMetadata: (...args) => this.fetchTableMetadata(...args),
|
|
7089
|
+
// Branch operations (only available when poolManager is configured)
|
|
7090
|
+
...this.branchService ? {
|
|
7091
|
+
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
7092
|
+
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
7093
|
+
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
7094
|
+
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
7095
|
+
} : {}
|
|
7096
|
+
};
|
|
7097
|
+
}
|
|
6627
7098
|
resolveCollectionCallbacks(collection, path2) {
|
|
6628
7099
|
if (!collection && !path2) return {
|
|
6629
7100
|
collection: void 0,
|
|
@@ -6652,6 +7123,7 @@ class PostgresBackendDriver {
|
|
|
6652
7123
|
collection,
|
|
6653
7124
|
filter,
|
|
6654
7125
|
limit,
|
|
7126
|
+
offset,
|
|
6655
7127
|
startAfter,
|
|
6656
7128
|
orderBy,
|
|
6657
7129
|
searchString,
|
|
@@ -6662,6 +7134,7 @@ class PostgresBackendDriver {
|
|
|
6662
7134
|
orderBy,
|
|
6663
7135
|
order,
|
|
6664
7136
|
limit,
|
|
7137
|
+
offset,
|
|
6665
7138
|
startAfter,
|
|
6666
7139
|
databaseId: collection?.databaseId,
|
|
6667
7140
|
searchString
|
|
@@ -6705,6 +7178,7 @@ class PostgresBackendDriver {
|
|
|
6705
7178
|
collection,
|
|
6706
7179
|
filter,
|
|
6707
7180
|
limit,
|
|
7181
|
+
offset,
|
|
6708
7182
|
startAfter,
|
|
6709
7183
|
orderBy,
|
|
6710
7184
|
searchString,
|
|
@@ -6725,6 +7199,7 @@ class PostgresBackendDriver {
|
|
|
6725
7199
|
orderBy,
|
|
6726
7200
|
order,
|
|
6727
7201
|
limit,
|
|
7202
|
+
offset,
|
|
6728
7203
|
startAfter,
|
|
6729
7204
|
databaseId: collection?.databaseId,
|
|
6730
7205
|
searchString
|
|
@@ -6736,6 +7211,7 @@ class PostgresBackendDriver {
|
|
|
6736
7211
|
collection,
|
|
6737
7212
|
filter,
|
|
6738
7213
|
limit,
|
|
7214
|
+
offset,
|
|
6739
7215
|
startAfter,
|
|
6740
7216
|
orderBy,
|
|
6741
7217
|
searchString,
|
|
@@ -6896,7 +7372,7 @@ class PostgresBackendDriver {
|
|
|
6896
7372
|
collection: resolvedCollection,
|
|
6897
7373
|
path: path2,
|
|
6898
7374
|
entityId: savedEntity.id,
|
|
6899
|
-
values:
|
|
7375
|
+
values: savedEntity.values,
|
|
6900
7376
|
previousValues: previousValuesForHistory,
|
|
6901
7377
|
status,
|
|
6902
7378
|
context: contextForCallback
|
|
@@ -6907,7 +7383,7 @@ class PostgresBackendDriver {
|
|
|
6907
7383
|
collection: resolvedCollection,
|
|
6908
7384
|
path: path2,
|
|
6909
7385
|
entityId: savedEntity.id,
|
|
6910
|
-
values:
|
|
7386
|
+
values: savedEntity.values,
|
|
6911
7387
|
previousValues: previousValuesForHistory,
|
|
6912
7388
|
status,
|
|
6913
7389
|
context: contextForCallback
|
|
@@ -7044,10 +7520,12 @@ class PostgresBackendDriver {
|
|
|
7044
7520
|
async countEntities({
|
|
7045
7521
|
path: path2,
|
|
7046
7522
|
collection,
|
|
7047
|
-
filter
|
|
7523
|
+
filter,
|
|
7524
|
+
searchString
|
|
7048
7525
|
}) {
|
|
7049
7526
|
return this.entityService.countEntities(path2, {
|
|
7050
|
-
filter
|
|
7527
|
+
filter,
|
|
7528
|
+
searchString
|
|
7051
7529
|
});
|
|
7052
7530
|
}
|
|
7053
7531
|
getTargetDb(databaseName) {
|
|
@@ -7103,7 +7581,7 @@ class PostgresBackendDriver {
|
|
|
7103
7581
|
return databases;
|
|
7104
7582
|
}
|
|
7105
7583
|
async fetchAvailableRoles() {
|
|
7106
|
-
const result = await this.executeSql(
|
|
7584
|
+
const result = await this.executeSql("SELECT rolname FROM pg_roles;");
|
|
7107
7585
|
return result.map((r) => r.rolname);
|
|
7108
7586
|
}
|
|
7109
7587
|
async fetchCurrentDatabase() {
|
|
@@ -7277,12 +7755,12 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7277
7755
|
const result = await this.delegate.db.transaction(async (tx) => {
|
|
7278
7756
|
let userId = this.user?.uid;
|
|
7279
7757
|
if (!userId) {
|
|
7280
|
-
console.warn(
|
|
7758
|
+
console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
|
|
7281
7759
|
userId = "anonymous";
|
|
7282
7760
|
}
|
|
7283
|
-
|
|
7761
|
+
const userRoles2 = this.user?.roles ?? [];
|
|
7284
7762
|
if (!this.user?.roles) {
|
|
7285
|
-
console.warn(
|
|
7763
|
+
console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
|
|
7286
7764
|
}
|
|
7287
7765
|
const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
|
|
7288
7766
|
const rolesString = normalizedRoles.join(",");
|
|
@@ -7352,29 +7830,6 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7352
7830
|
async countEntities(props) {
|
|
7353
7831
|
return this.withTransaction((delegate) => delegate.countEntities(props));
|
|
7354
7832
|
}
|
|
7355
|
-
/**
|
|
7356
|
-
* Intentionally delegates to the base delegate WITHOUT RLS wrapping.
|
|
7357
|
-
* executeSql is an admin-only feature; access control should be enforced
|
|
7358
|
-
* at the API route level, not via database-level RLS.
|
|
7359
|
-
*/
|
|
7360
|
-
async executeSql(sqlText, options) {
|
|
7361
|
-
return this.delegate.executeSql(sqlText, options);
|
|
7362
|
-
}
|
|
7363
|
-
async fetchAvailableDatabases() {
|
|
7364
|
-
return this.delegate.fetchAvailableDatabases();
|
|
7365
|
-
}
|
|
7366
|
-
async fetchAvailableRoles() {
|
|
7367
|
-
return this.delegate.fetchAvailableRoles();
|
|
7368
|
-
}
|
|
7369
|
-
async fetchCurrentDatabase() {
|
|
7370
|
-
return this.delegate.fetchCurrentDatabase();
|
|
7371
|
-
}
|
|
7372
|
-
async fetchUnmappedTables(mappedPaths) {
|
|
7373
|
-
return this.delegate.fetchUnmappedTables(mappedPaths);
|
|
7374
|
-
}
|
|
7375
|
-
async fetchTableMetadata(tableName) {
|
|
7376
|
-
return this.delegate.fetchTableMetadata(tableName);
|
|
7377
|
-
}
|
|
7378
7833
|
}
|
|
7379
7834
|
class DatabasePoolManager {
|
|
7380
7835
|
pools = /* @__PURE__ */ new Map();
|
|
@@ -7410,7 +7865,10 @@ class DatabasePoolManager {
|
|
|
7410
7865
|
connectionString: url.toString(),
|
|
7411
7866
|
max: 10,
|
|
7412
7867
|
// Default sensible limit, can be tuned later
|
|
7413
|
-
idleTimeoutMillis:
|
|
7868
|
+
idleTimeoutMillis: 1e4,
|
|
7869
|
+
// Reduced from 30000 for aggressive cleanup
|
|
7870
|
+
allowExitOnIdle: true
|
|
7871
|
+
// Prevent idle clients from hanging the Node.js process
|
|
7414
7872
|
});
|
|
7415
7873
|
pool.on("error", (err) => {
|
|
7416
7874
|
console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
|
|
@@ -7461,13 +7919,6 @@ const users = rebaseSchema.table("users", {
|
|
|
7461
7919
|
photoUrl: varchar("photo_url", {
|
|
7462
7920
|
length: 500
|
|
7463
7921
|
}),
|
|
7464
|
-
provider: varchar("provider", {
|
|
7465
|
-
length: 50
|
|
7466
|
-
}).notNull().default("email"),
|
|
7467
|
-
// 'email' | 'google'
|
|
7468
|
-
googleId: varchar("google_id", {
|
|
7469
|
-
length: 255
|
|
7470
|
-
}).unique(),
|
|
7471
7922
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
7472
7923
|
emailVerificationToken: varchar("email_verification_token", {
|
|
7473
7924
|
length: 255
|
|
@@ -7541,12 +7992,31 @@ const appConfig = rebaseSchema.table("app_config", {
|
|
|
7541
7992
|
value: jsonb("value").notNull(),
|
|
7542
7993
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7543
7994
|
});
|
|
7995
|
+
const userIdentities = rebaseSchema.table("user_identities", {
|
|
7996
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7997
|
+
userId: uuid("user_id").notNull().references(() => users.id, {
|
|
7998
|
+
onDelete: "cascade"
|
|
7999
|
+
}),
|
|
8000
|
+
provider: varchar("provider", {
|
|
8001
|
+
length: 50
|
|
8002
|
+
}).notNull(),
|
|
8003
|
+
// e.g. 'google', 'linkedin'
|
|
8004
|
+
providerId: varchar("provider_id", {
|
|
8005
|
+
length: 255
|
|
8006
|
+
}).notNull(),
|
|
8007
|
+
profileData: jsonb("profile_data"),
|
|
8008
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
8009
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
8010
|
+
}, (table) => ({
|
|
8011
|
+
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
8012
|
+
}));
|
|
7544
8013
|
const usersRelations = relations(users, ({
|
|
7545
8014
|
many
|
|
7546
8015
|
}) => ({
|
|
7547
8016
|
userRoles: many(userRoles),
|
|
7548
8017
|
refreshTokens: many(refreshTokens),
|
|
7549
|
-
passwordResetTokens: many(passwordResetTokens)
|
|
8018
|
+
passwordResetTokens: many(passwordResetTokens),
|
|
8019
|
+
userIdentities: many(userIdentities)
|
|
7550
8020
|
}));
|
|
7551
8021
|
const rolesRelations = relations(roles, ({
|
|
7552
8022
|
many
|
|
@@ -7581,13 +8051,24 @@ const passwordResetTokensRelations = relations(passwordResetTokens, ({
|
|
|
7581
8051
|
references: [users.id]
|
|
7582
8052
|
})
|
|
7583
8053
|
}));
|
|
8054
|
+
const userIdentitiesRelations = relations(userIdentities, ({
|
|
8055
|
+
one
|
|
8056
|
+
}) => ({
|
|
8057
|
+
user: one(users, {
|
|
8058
|
+
fields: [userIdentities.userId],
|
|
8059
|
+
references: [users.id]
|
|
8060
|
+
})
|
|
8061
|
+
}));
|
|
7584
8062
|
const getPrimaryKeyProp = (collection) => {
|
|
7585
8063
|
if (collection.properties) {
|
|
7586
8064
|
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
|
|
7587
8065
|
if (idPropEntry) {
|
|
8066
|
+
const prop = idPropEntry[1];
|
|
8067
|
+
const isUuid2 = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
|
|
7588
8068
|
return {
|
|
7589
8069
|
name: idPropEntry[0],
|
|
7590
|
-
type:
|
|
8070
|
+
type: prop.type === "number" ? "number" : "string",
|
|
8071
|
+
isUuid: isUuid2
|
|
7591
8072
|
};
|
|
7592
8073
|
}
|
|
7593
8074
|
}
|
|
@@ -7595,12 +8076,15 @@ const getPrimaryKeyProp = (collection) => {
|
|
|
7595
8076
|
if (idProp?.type === "number") {
|
|
7596
8077
|
return {
|
|
7597
8078
|
name: "id",
|
|
7598
|
-
type: "number"
|
|
8079
|
+
type: "number",
|
|
8080
|
+
isUuid: false
|
|
7599
8081
|
};
|
|
7600
8082
|
}
|
|
8083
|
+
const isUuid = idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid";
|
|
7601
8084
|
return {
|
|
7602
8085
|
name: "id",
|
|
7603
|
-
type: "string"
|
|
8086
|
+
type: "string",
|
|
8087
|
+
isUuid: isUuid ?? false
|
|
7604
8088
|
};
|
|
7605
8089
|
};
|
|
7606
8090
|
const isNumericId = (collection) => {
|
|
@@ -7614,7 +8098,7 @@ const isIdProperty = (propName, prop, collection) => {
|
|
|
7614
8098
|
const hasExplicitId = Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId));
|
|
7615
8099
|
return !hasExplicitId && propName === "id";
|
|
7616
8100
|
};
|
|
7617
|
-
const getDrizzleColumn = (propName, prop, collection) => {
|
|
8101
|
+
const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
7618
8102
|
const colName = toSnakeCase(propName);
|
|
7619
8103
|
let columnDefinition;
|
|
7620
8104
|
switch (prop.type) {
|
|
@@ -7633,20 +8117,20 @@ const getDrizzleColumn = (propName, prop, collection) => {
|
|
|
7633
8117
|
columnDefinition = `varchar("${colName}")`;
|
|
7634
8118
|
}
|
|
7635
8119
|
if (isIdProperty(propName, prop, collection)) {
|
|
7636
|
-
columnDefinition +=
|
|
8120
|
+
columnDefinition += ".primaryKey()";
|
|
7637
8121
|
}
|
|
7638
8122
|
if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
|
|
7639
8123
|
if (stringProp.isId === "uuid") {
|
|
7640
|
-
columnDefinition +=
|
|
8124
|
+
columnDefinition += ".defaultRandom()";
|
|
7641
8125
|
} else if (stringProp.isId === "cuid") {
|
|
7642
|
-
columnDefinition +=
|
|
8126
|
+
columnDefinition += ".default(sql`cuid()`)";
|
|
7643
8127
|
} else if (typeof stringProp.isId === "string") {
|
|
7644
8128
|
const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`") ? stringProp.isId.substring(4, stringProp.isId.length - 1) : stringProp.isId;
|
|
7645
8129
|
columnDefinition += `.default(sql\`${sqlContent}\`)`;
|
|
7646
8130
|
}
|
|
7647
8131
|
}
|
|
7648
8132
|
if (stringProp.validation?.unique) {
|
|
7649
|
-
columnDefinition +=
|
|
8133
|
+
columnDefinition += ".unique()";
|
|
7650
8134
|
}
|
|
7651
8135
|
break;
|
|
7652
8136
|
}
|
|
@@ -7668,10 +8152,10 @@ const getDrizzleColumn = (propName, prop, collection) => {
|
|
|
7668
8152
|
columnDefinition = baseType;
|
|
7669
8153
|
}
|
|
7670
8154
|
if (isId) {
|
|
7671
|
-
columnDefinition +=
|
|
8155
|
+
columnDefinition += ".primaryKey()";
|
|
7672
8156
|
}
|
|
7673
8157
|
if (numProp.validation?.unique) {
|
|
7674
|
-
columnDefinition +=
|
|
8158
|
+
columnDefinition += ".unique()";
|
|
7675
8159
|
}
|
|
7676
8160
|
break;
|
|
7677
8161
|
}
|
|
@@ -7702,7 +8186,7 @@ const getDrizzleColumn = (propName, prop, collection) => {
|
|
|
7702
8186
|
case "relation": {
|
|
7703
8187
|
const refProp = prop;
|
|
7704
8188
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
7705
|
-
const relation = resolvedRelations
|
|
8189
|
+
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
7706
8190
|
if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
|
|
7707
8191
|
return null;
|
|
7708
8192
|
}
|
|
@@ -7721,8 +8205,9 @@ const getDrizzleColumn = (propName, prop, collection) => {
|
|
|
7721
8205
|
}
|
|
7722
8206
|
const fkColumnName = toSnakeCase(relation.localKey);
|
|
7723
8207
|
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
7724
|
-
const
|
|
7725
|
-
const
|
|
8208
|
+
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
8209
|
+
const targetIdField = pkProp.name;
|
|
8210
|
+
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `varchar("${fkColumnName}")`;
|
|
7726
8211
|
const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
|
|
7727
8212
|
const required = prop.validation?.required;
|
|
7728
8213
|
const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
|
|
@@ -7735,6 +8220,26 @@ const getDrizzleColumn = (propName, prop, collection) => {
|
|
|
7735
8220
|
}
|
|
7736
8221
|
return ` ${relation.localKey}: ${columnDef}`;
|
|
7737
8222
|
}
|
|
8223
|
+
case "reference": {
|
|
8224
|
+
const refProp = prop;
|
|
8225
|
+
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
8226
|
+
if (!targetCollection) {
|
|
8227
|
+
columnDefinition = `varchar("${colName}")`;
|
|
8228
|
+
break;
|
|
8229
|
+
}
|
|
8230
|
+
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
8231
|
+
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
8232
|
+
const targetIdField = pkProp.name;
|
|
8233
|
+
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `varchar("${colName}")`;
|
|
8234
|
+
const required = prop.validation?.required;
|
|
8235
|
+
const onDelete = required ? "cascade" : "set null";
|
|
8236
|
+
const refOptions = `{ onDelete: "${onDelete}" }`;
|
|
8237
|
+
columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${refOptions})`;
|
|
8238
|
+
if (required) {
|
|
8239
|
+
columnDefinition += ".notNull()";
|
|
8240
|
+
}
|
|
8241
|
+
return ` ${propName}: ${columnDefinition}`;
|
|
8242
|
+
}
|
|
7738
8243
|
default:
|
|
7739
8244
|
return null;
|
|
7740
8245
|
}
|
|
@@ -7761,7 +8266,7 @@ const buildUsingClause = (rule) => {
|
|
|
7761
8266
|
return resolveRawSql(rule.using);
|
|
7762
8267
|
}
|
|
7763
8268
|
if (rule.access === "public") {
|
|
7764
|
-
return `
|
|
8269
|
+
return "sql`true`";
|
|
7765
8270
|
}
|
|
7766
8271
|
if (rule.ownerField) {
|
|
7767
8272
|
return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
|
|
@@ -7774,16 +8279,31 @@ const buildWithCheckClause = (rule) => {
|
|
|
7774
8279
|
}
|
|
7775
8280
|
return buildUsingClause(rule);
|
|
7776
8281
|
};
|
|
8282
|
+
const getPolicyNameHash = (rule) => {
|
|
8283
|
+
const data = JSON.stringify({
|
|
8284
|
+
a: rule.access,
|
|
8285
|
+
m: rule.mode,
|
|
8286
|
+
op: rule.operation,
|
|
8287
|
+
ops: rule.operations?.slice().sort(),
|
|
8288
|
+
own: rule.ownerField,
|
|
8289
|
+
rol: rule.roles?.slice().sort(),
|
|
8290
|
+
pg: rule.pgRoles?.slice().sort(),
|
|
8291
|
+
u: rule.using,
|
|
8292
|
+
w: rule.withCheck
|
|
8293
|
+
});
|
|
8294
|
+
return createHash("sha1").update(data).digest("hex").substring(0, 7);
|
|
8295
|
+
};
|
|
7777
8296
|
const generatePolicyCode = (tableName, rule, index) => {
|
|
7778
8297
|
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
8298
|
+
const ruleHash = getPolicyNameHash(rule);
|
|
7779
8299
|
return ops.map((op, opIdx) => {
|
|
7780
|
-
const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}
|
|
8300
|
+
const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
7781
8301
|
return generateSinglePolicyCode(tableName, rule, op, policyName);
|
|
7782
8302
|
}).join("");
|
|
7783
8303
|
};
|
|
7784
8304
|
const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
|
|
7785
8305
|
const mode = rule.mode ?? "permissive";
|
|
7786
|
-
const roles2 = rule.roles;
|
|
8306
|
+
const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
|
|
7787
8307
|
const needsUsing = operation !== "insert";
|
|
7788
8308
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
7789
8309
|
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
@@ -7803,22 +8323,57 @@ const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
|
|
|
7803
8323
|
}
|
|
7804
8324
|
}
|
|
7805
8325
|
if (!usingClause && needsUsing) {
|
|
7806
|
-
usingClause = `
|
|
8326
|
+
usingClause = "sql`false`";
|
|
7807
8327
|
}
|
|
7808
8328
|
if (!withCheckClause && needsWithCheck) {
|
|
7809
|
-
withCheckClause = `
|
|
8329
|
+
withCheckClause = "sql`false`";
|
|
7810
8330
|
}
|
|
7811
8331
|
const parts = [];
|
|
7812
8332
|
parts.push(`as: "${mode}"`);
|
|
7813
8333
|
parts.push(`for: "${operation}"`);
|
|
7814
|
-
const toRoles = rule.pgRoles
|
|
8334
|
+
const toRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
|
|
7815
8335
|
parts.push(`to: [${toRoles.map((r) => `"${r}"`).join(", ")}]`);
|
|
7816
8336
|
if (usingClause) parts.push(`using: ${usingClause}`);
|
|
7817
8337
|
if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
|
|
7818
8338
|
return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),
|
|
7819
8339
|
`;
|
|
7820
8340
|
};
|
|
7821
|
-
const
|
|
8341
|
+
const computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
8342
|
+
const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
|
|
8343
|
+
if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
|
|
8344
|
+
return `${getTableName(sourceCollection)}_${rel.localKey}`;
|
|
8345
|
+
}
|
|
8346
|
+
if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
|
|
8347
|
+
try {
|
|
8348
|
+
const targetCollection = rel.target();
|
|
8349
|
+
return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
|
|
8350
|
+
} catch {
|
|
8351
|
+
return fallback;
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
if (rel.direction === "inverse" && rel.cardinality === "one") {
|
|
8355
|
+
if (rel.foreignKeyOnTarget) {
|
|
8356
|
+
try {
|
|
8357
|
+
const targetCollection = rel.target();
|
|
8358
|
+
return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
|
|
8359
|
+
} catch {
|
|
8360
|
+
return fallback;
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
try {
|
|
8364
|
+
const targetCollection = rel.target();
|
|
8365
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8366
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey && targetRel.target().slug === sourceCollection.slug);
|
|
8367
|
+
if (correspondingRelation && correspondingRelation.localKey) {
|
|
8368
|
+
return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
|
|
8369
|
+
}
|
|
8370
|
+
} catch {
|
|
8371
|
+
}
|
|
8372
|
+
return fallback;
|
|
8373
|
+
}
|
|
8374
|
+
return fallback;
|
|
8375
|
+
};
|
|
8376
|
+
const generateSchema = async (collections, stripPolicies = false) => {
|
|
7822
8377
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
7823
8378
|
const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
|
|
7824
8379
|
collections.some((c) => c.properties && Object.values(c.properties).some((p) => (p.type === "map" || p.type === "array") && p.columnType === "json"));
|
|
@@ -7826,9 +8381,7 @@ const generateSchema = async (collections) => {
|
|
|
7826
8381
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
7827
8382
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';
|
|
7828
8383
|
`;
|
|
7829
|
-
schemaContent +=
|
|
7830
|
-
|
|
7831
|
-
`;
|
|
8384
|
+
schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
|
|
7832
8385
|
const exportedTableVars = [];
|
|
7833
8386
|
const exportedEnumVars = [];
|
|
7834
8387
|
const exportedRelationVars = [];
|
|
@@ -7889,8 +8442,8 @@ const generateSchema = async (collections) => {
|
|
|
7889
8442
|
} = relation.through;
|
|
7890
8443
|
const onDelete = relation.onDelete ?? "cascade";
|
|
7891
8444
|
const refOptions = `{ onDelete: "${onDelete}" }`;
|
|
7892
|
-
const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
|
|
7893
|
-
const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
|
|
8445
|
+
const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "varchar";
|
|
8446
|
+
const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar";
|
|
7894
8447
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
7895
8448
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
7896
8449
|
schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
|
|
@@ -7899,31 +8452,28 @@ const generateSchema = async (collections) => {
|
|
|
7899
8452
|
`;
|
|
7900
8453
|
schemaContent += ` ${targetColumn}: ${targetColType}("${toSnakeCase(targetColumn)}").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),
|
|
7901
8454
|
`;
|
|
7902
|
-
schemaContent +=
|
|
7903
|
-
`;
|
|
8455
|
+
schemaContent += "}, (table) => ({\n";
|
|
7904
8456
|
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
|
|
7905
8457
|
`;
|
|
7906
|
-
schemaContent +=
|
|
7907
|
-
|
|
7908
|
-
`;
|
|
8458
|
+
schemaContent += "}));\n\n";
|
|
7909
8459
|
} else if (!isJunction) {
|
|
7910
8460
|
schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
|
|
7911
8461
|
`;
|
|
7912
8462
|
const columns = /* @__PURE__ */ new Set();
|
|
7913
8463
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
7914
|
-
const columnString = getDrizzleColumn(propName, prop, collection);
|
|
8464
|
+
const columnString = getDrizzleColumn(propName, prop, collection, collections);
|
|
7915
8465
|
if (columnString) columns.add(columnString);
|
|
7916
8466
|
});
|
|
7917
8467
|
const hasIdColumn = Array.from(columns).some((col) => col.includes(".primaryKey()"));
|
|
7918
8468
|
if (!hasIdColumn) {
|
|
7919
|
-
columns.add(
|
|
8469
|
+
columns.add(' id: varchar("id").primaryKey()');
|
|
7920
8470
|
}
|
|
7921
8471
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
7922
|
-
const securityRules = collection.securityRules;
|
|
7923
|
-
if (securityRules && securityRules.length > 0) {
|
|
8472
|
+
const securityRules = isPostgresCollection(collection) ? collection.securityRules : void 0;
|
|
8473
|
+
if (!stripPolicies && securityRules && securityRules.length > 0) {
|
|
7924
8474
|
schemaContent += "\n}, (table) => ([\n";
|
|
7925
8475
|
securityRules.forEach((rule, idx) => {
|
|
7926
|
-
schemaContent += generatePolicyCode(tableName, rule
|
|
8476
|
+
schemaContent += generatePolicyCode(tableName, rule);
|
|
7927
8477
|
});
|
|
7928
8478
|
schemaContent += "])).enableRLS();\n\n";
|
|
7929
8479
|
} else {
|
|
@@ -7963,13 +8513,13 @@ const generateSchema = async (collections) => {
|
|
|
7963
8513
|
}
|
|
7964
8514
|
} catch {
|
|
7965
8515
|
}
|
|
7966
|
-
tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {
|
|
8516
|
+
tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {
|
|
7967
8517
|
fields: [${tableVarName}.${relation.through.sourceColumn}],
|
|
7968
8518
|
references: [${sourceTableVar}.${sourceId}],
|
|
7969
8519
|
relationName: "${owningRelationName}"
|
|
7970
8520
|
})`);
|
|
7971
8521
|
const targetRelName = inverseRelationName ?? owningRelationName;
|
|
7972
|
-
tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {
|
|
8522
|
+
tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {
|
|
7973
8523
|
fields: [${tableVarName}.${relation.through.targetColumn}],
|
|
7974
8524
|
references: [${targetTableVar}.${targetId}],
|
|
7975
8525
|
relationName: "${targetRelName}"
|
|
@@ -7977,22 +8527,25 @@ const generateSchema = async (collections) => {
|
|
|
7977
8527
|
}
|
|
7978
8528
|
} else {
|
|
7979
8529
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
8530
|
+
const emittedRelationNames = /* @__PURE__ */ new Set();
|
|
7980
8531
|
for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
|
|
7981
8532
|
try {
|
|
7982
8533
|
const target = rel.target();
|
|
7983
8534
|
const targetTableVar = getTableVarName(getTableName(target));
|
|
7984
|
-
const
|
|
7985
|
-
const
|
|
8535
|
+
const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
|
|
8536
|
+
const deduplicationKey = `${drizzleRelationName}::${rel.direction}`;
|
|
8537
|
+
if (emittedRelationNames.has(deduplicationKey)) continue;
|
|
8538
|
+
emittedRelationNames.add(deduplicationKey);
|
|
7986
8539
|
if (rel.cardinality === "one") {
|
|
7987
8540
|
if (rel.direction === "owning" && rel.localKey) {
|
|
7988
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8541
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
7989
8542
|
fields: [${tableVarName}.${rel.localKey}],
|
|
7990
8543
|
references: [${targetTableVar}.${getPrimaryKeyName(target)}],
|
|
7991
8544
|
relationName: "${drizzleRelationName}"
|
|
7992
8545
|
})`);
|
|
7993
8546
|
} else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
7994
8547
|
const sourceIdField = getPrimaryKeyName(collection);
|
|
7995
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8548
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
7996
8549
|
fields: [${tableVarName}.${sourceIdField}],
|
|
7997
8550
|
references: [${targetTableVar}.${rel.foreignKeyOnTarget}],
|
|
7998
8551
|
relationName: "${drizzleRelationName}"
|
|
@@ -8004,7 +8557,7 @@ const generateSchema = async (collections) => {
|
|
|
8004
8557
|
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.target().slug === collection.slug);
|
|
8005
8558
|
if (correspondingRelation && correspondingRelation.localKey) {
|
|
8006
8559
|
const sourceIdField = getPrimaryKeyName(collection);
|
|
8007
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8560
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8008
8561
|
fields: [${tableVarName}.${sourceIdField}],
|
|
8009
8562
|
references: [${targetTableVar}.${correspondingRelation.localKey}],
|
|
8010
8563
|
relationName: "${drizzleRelationName}"
|
|
@@ -8016,10 +8569,10 @@ const generateSchema = async (collections) => {
|
|
|
8016
8569
|
}
|
|
8017
8570
|
} else if (rel.cardinality === "many") {
|
|
8018
8571
|
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8019
|
-
tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8572
|
+
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8020
8573
|
} else if (rel.through) {
|
|
8021
8574
|
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8022
|
-
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8575
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8023
8576
|
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8024
8577
|
try {
|
|
8025
8578
|
const targetCollection = rel.target();
|
|
@@ -8027,7 +8580,7 @@ const generateSchema = async (collections) => {
|
|
|
8027
8580
|
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8028
8581
|
if (correspondingRelation && correspondingRelation.through) {
|
|
8029
8582
|
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8030
|
-
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8583
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8031
8584
|
} else {
|
|
8032
8585
|
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8033
8586
|
}
|
|
@@ -8331,6 +8884,14 @@ class RealtimeService extends EventEmitter {
|
|
|
8331
8884
|
async handleCollectionSubscription(clientId, request, authContext) {
|
|
8332
8885
|
const subscriptionId = request.subscriptionId;
|
|
8333
8886
|
try {
|
|
8887
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
8888
|
+
if (!collection) {
|
|
8889
|
+
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
8890
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
8891
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
8892
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
8893
|
+
return;
|
|
8894
|
+
}
|
|
8334
8895
|
this._subscriptions.set(subscriptionId, {
|
|
8335
8896
|
clientId,
|
|
8336
8897
|
type: "collection",
|
|
@@ -8348,7 +8909,6 @@ class RealtimeService extends EventEmitter {
|
|
|
8348
8909
|
});
|
|
8349
8910
|
let entities;
|
|
8350
8911
|
if (this.driver) {
|
|
8351
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
8352
8912
|
entities = await this.driver.fetchCollection({
|
|
8353
8913
|
path: request.path,
|
|
8354
8914
|
collection,
|
|
@@ -8378,6 +8938,14 @@ class RealtimeService extends EventEmitter {
|
|
|
8378
8938
|
async handleEntitySubscription(clientId, request, authContext) {
|
|
8379
8939
|
const subscriptionId = request.subscriptionId;
|
|
8380
8940
|
try {
|
|
8941
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
8942
|
+
if (!collection) {
|
|
8943
|
+
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
8944
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
8945
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
8946
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
8947
|
+
return;
|
|
8948
|
+
}
|
|
8381
8949
|
this._subscriptions.set(subscriptionId, {
|
|
8382
8950
|
clientId,
|
|
8383
8951
|
type: "entity",
|
|
@@ -8387,7 +8955,6 @@ class RealtimeService extends EventEmitter {
|
|
|
8387
8955
|
});
|
|
8388
8956
|
let entity;
|
|
8389
8957
|
if (this.driver) {
|
|
8390
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
8391
8958
|
entity = await this.driver.fetchEntity({
|
|
8392
8959
|
path: request.path,
|
|
8393
8960
|
entityId: request.entityId,
|
|
@@ -8460,13 +9027,13 @@ class RealtimeService extends EventEmitter {
|
|
|
8460
9027
|
for (const [subscriptionId, subscription] of webSocketSubscriptions) {
|
|
8461
9028
|
try {
|
|
8462
9029
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
8463
|
-
if (entity && entity.values?._rebase_invalidated) {
|
|
9030
|
+
if (entity && entity.values && entity.values?._rebase_invalidated) {
|
|
8464
9031
|
this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
|
|
8465
9032
|
} else {
|
|
8466
9033
|
this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
|
|
8467
9034
|
}
|
|
8468
9035
|
} else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
8469
|
-
if (!entity || !entity.values?._rebase_invalidated) {
|
|
9036
|
+
if (!entity || !(entity.values && entity.values?._rebase_invalidated)) {
|
|
8470
9037
|
this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
|
|
8471
9038
|
}
|
|
8472
9039
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
@@ -8481,7 +9048,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8481
9048
|
const callback = this.subscriptionCallbacks.get(subscriptionId);
|
|
8482
9049
|
if (!callback) continue;
|
|
8483
9050
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
8484
|
-
if (entity && entity.values?._rebase_invalidated) {
|
|
9051
|
+
if (entity && entity.values && entity.values?._rebase_invalidated) {
|
|
8485
9052
|
this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
|
|
8486
9053
|
} else {
|
|
8487
9054
|
callback(entity);
|
|
@@ -8547,6 +9114,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8547
9114
|
orderBy: collectionRequest.orderBy,
|
|
8548
9115
|
order: collectionRequest.order,
|
|
8549
9116
|
limit: collectionRequest.limit,
|
|
9117
|
+
offset: collectionRequest.offset,
|
|
8550
9118
|
startAfter: collectionRequest.startAfter,
|
|
8551
9119
|
searchString: collectionRequest.searchString
|
|
8552
9120
|
});
|
|
@@ -8574,6 +9142,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8574
9142
|
orderBy: collectionRequest.orderBy,
|
|
8575
9143
|
order: collectionRequest.order,
|
|
8576
9144
|
limit: collectionRequest.limit,
|
|
9145
|
+
offset: collectionRequest.offset,
|
|
8577
9146
|
startAfter: collectionRequest.startAfter,
|
|
8578
9147
|
databaseId: collectionRequest.databaseId
|
|
8579
9148
|
});
|
|
@@ -8634,6 +9203,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8634
9203
|
orderBy: collectionRequest.orderBy,
|
|
8635
9204
|
order: collectionRequest.order,
|
|
8636
9205
|
limit: collectionRequest.limit,
|
|
9206
|
+
offset: collectionRequest.offset,
|
|
8637
9207
|
startAfter: collectionRequest.startAfter,
|
|
8638
9208
|
databaseId: collectionRequest.databaseId
|
|
8639
9209
|
});
|
|
@@ -8939,7 +9509,7 @@ class RealtimeService extends EventEmitter {
|
|
|
8939
9509
|
}
|
|
8940
9510
|
const PostgresRealtimeProvider = RealtimeService;
|
|
8941
9511
|
const clientSessions = /* @__PURE__ */ new Map();
|
|
8942
|
-
const WS_RATE_LIMIT =
|
|
9512
|
+
const WS_RATE_LIMIT = 2e3;
|
|
8943
9513
|
const WS_RATE_WINDOW_MS = 6e4;
|
|
8944
9514
|
const ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set(["EXECUTE_SQL", "FETCH_DATABASES", "FETCH_ROLES", "FETCH_UNMAPPED_TABLES", "FETCH_TABLE_METADATA", "FETCH_CURRENT_DATABASE", "CREATE_BRANCH", "DELETE_BRANCH", "LIST_BRANCHES"]);
|
|
8945
9515
|
function isAdminSession(session) {
|
|
@@ -8959,6 +9529,12 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
8959
9529
|
const wss = new WebSocketServer({
|
|
8960
9530
|
server
|
|
8961
9531
|
});
|
|
9532
|
+
wss.on("error", (err) => {
|
|
9533
|
+
if (err.code === "EADDRINUSE") {
|
|
9534
|
+
return;
|
|
9535
|
+
}
|
|
9536
|
+
console.error("❌ [WebSocket Server] Error:", err);
|
|
9537
|
+
});
|
|
8962
9538
|
const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
|
|
8963
9539
|
wss.on("connection", (ws) => {
|
|
8964
9540
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -9199,7 +9775,12 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9199
9775
|
options
|
|
9200
9776
|
} = payload;
|
|
9201
9777
|
const delegate = await getScopedDelegate();
|
|
9202
|
-
const
|
|
9778
|
+
const admin = delegate.admin;
|
|
9779
|
+
if (!isSQLAdmin(admin)) {
|
|
9780
|
+
sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
|
|
9781
|
+
break;
|
|
9782
|
+
}
|
|
9783
|
+
const result = await admin.executeSql(sql2, options);
|
|
9203
9784
|
if (process.env.NODE_ENV !== "production") {
|
|
9204
9785
|
wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
|
|
9205
9786
|
}
|
|
@@ -9217,9 +9798,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9217
9798
|
{
|
|
9218
9799
|
wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
|
|
9219
9800
|
const delegate = await getScopedDelegate();
|
|
9801
|
+
const admin = delegate.admin;
|
|
9220
9802
|
let databases = [];
|
|
9221
|
-
if (
|
|
9222
|
-
databases = await
|
|
9803
|
+
if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {
|
|
9804
|
+
databases = await admin.fetchAvailableDatabases();
|
|
9223
9805
|
}
|
|
9224
9806
|
wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
|
|
9225
9807
|
const response = {
|
|
@@ -9236,9 +9818,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9236
9818
|
{
|
|
9237
9819
|
wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
|
|
9238
9820
|
const delegate = await getScopedDelegate();
|
|
9821
|
+
const admin = delegate.admin;
|
|
9239
9822
|
let roles2 = [];
|
|
9240
|
-
if (
|
|
9241
|
-
roles2 = await
|
|
9823
|
+
if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
|
|
9824
|
+
roles2 = await admin.fetchAvailableRoles();
|
|
9242
9825
|
}
|
|
9243
9826
|
wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
|
|
9244
9827
|
const response = {
|
|
@@ -9255,9 +9838,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9255
9838
|
{
|
|
9256
9839
|
wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
|
|
9257
9840
|
const delegate = await getScopedDelegate();
|
|
9841
|
+
const admin = delegate.admin;
|
|
9258
9842
|
let database = void 0;
|
|
9259
|
-
if (
|
|
9260
|
-
database = await
|
|
9843
|
+
if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {
|
|
9844
|
+
database = await admin.fetchCurrentDatabase();
|
|
9261
9845
|
}
|
|
9262
9846
|
const response = {
|
|
9263
9847
|
type: "FETCH_CURRENT_DATABASE_SUCCESS",
|
|
@@ -9273,9 +9857,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9273
9857
|
{
|
|
9274
9858
|
wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
|
|
9275
9859
|
const delegate = await getScopedDelegate();
|
|
9860
|
+
const admin = delegate.admin;
|
|
9276
9861
|
let tables = [];
|
|
9277
|
-
if (
|
|
9278
|
-
tables = await
|
|
9862
|
+
if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {
|
|
9863
|
+
tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
|
|
9279
9864
|
}
|
|
9280
9865
|
wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
|
|
9281
9866
|
const response = {
|
|
@@ -9295,9 +9880,10 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig) {
|
|
|
9295
9880
|
tableName
|
|
9296
9881
|
} = payload;
|
|
9297
9882
|
const delegate = await getScopedDelegate();
|
|
9883
|
+
const admin = delegate.admin;
|
|
9298
9884
|
let metadata;
|
|
9299
|
-
if (
|
|
9300
|
-
metadata = await
|
|
9885
|
+
if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {
|
|
9886
|
+
metadata = await admin.fetchTableMetadata(tableName);
|
|
9301
9887
|
}
|
|
9302
9888
|
wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
|
|
9303
9889
|
const response = {
|
|
@@ -9482,7 +10068,7 @@ class PostgresCollectionRegistry extends CollectionRegistry {
|
|
|
9482
10068
|
*/
|
|
9483
10069
|
getRelationKeysForCollection(collectionPath) {
|
|
9484
10070
|
const collection = this.getCollectionByPath(collectionPath);
|
|
9485
|
-
if (!collection
|
|
10071
|
+
if (!collection || !getDataSourceCapabilities(collection.driver).supportsRelations || !collection.relations) return [];
|
|
9486
10072
|
return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
|
|
9487
10073
|
}
|
|
9488
10074
|
}
|
|
@@ -9539,8 +10125,6 @@ async function ensureAuthTablesExist(db) {
|
|
|
9539
10125
|
password_hash TEXT,
|
|
9540
10126
|
display_name TEXT,
|
|
9541
10127
|
photo_url TEXT,
|
|
9542
|
-
provider TEXT DEFAULT 'email',
|
|
9543
|
-
google_id TEXT UNIQUE,
|
|
9544
10128
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
9545
10129
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9546
10130
|
)
|
|
@@ -9550,8 +10134,20 @@ async function ensureAuthTablesExist(db) {
|
|
|
9550
10134
|
ON rebase.users(email)
|
|
9551
10135
|
`);
|
|
9552
10136
|
await db.execute(sql`
|
|
9553
|
-
CREATE
|
|
9554
|
-
|
|
10137
|
+
CREATE TABLE IF NOT EXISTS rebase.user_identities (
|
|
10138
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10139
|
+
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
10140
|
+
provider TEXT NOT NULL,
|
|
10141
|
+
provider_id TEXT NOT NULL,
|
|
10142
|
+
profile_data JSONB,
|
|
10143
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10144
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10145
|
+
UNIQUE(provider, provider_id)
|
|
10146
|
+
)
|
|
10147
|
+
`);
|
|
10148
|
+
await db.execute(sql`
|
|
10149
|
+
CREATE INDEX IF NOT EXISTS idx_user_identities_user
|
|
10150
|
+
ON rebase.user_identities(user_id)
|
|
9555
10151
|
`);
|
|
9556
10152
|
await db.execute(sql`
|
|
9557
10153
|
CREATE TABLE IF NOT EXISTS rebase.roles (
|
|
@@ -9564,10 +10160,6 @@ async function ensureAuthTablesExist(db) {
|
|
|
9564
10160
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9565
10161
|
)
|
|
9566
10162
|
`);
|
|
9567
|
-
await db.execute(sql`
|
|
9568
|
-
ALTER TABLE rebase.roles
|
|
9569
|
-
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
9570
|
-
`);
|
|
9571
10163
|
await db.execute(sql`
|
|
9572
10164
|
CREATE TABLE IF NOT EXISTS rebase.user_roles (
|
|
9573
10165
|
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
@@ -9599,51 +10191,6 @@ async function ensureAuthTablesExist(db) {
|
|
|
9599
10191
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
|
|
9600
10192
|
ON rebase.refresh_tokens(user_id)
|
|
9601
10193
|
`);
|
|
9602
|
-
await db.execute(sql`
|
|
9603
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9604
|
-
ADD COLUMN IF NOT EXISTS user_agent TEXT
|
|
9605
|
-
`);
|
|
9606
|
-
await db.execute(sql`
|
|
9607
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9608
|
-
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
9609
|
-
`);
|
|
9610
|
-
const constraintCheck = await db.execute(sql`
|
|
9611
|
-
SELECT 1 FROM information_schema.table_constraints
|
|
9612
|
-
WHERE constraint_name = 'unique_device_session'
|
|
9613
|
-
AND table_schema = 'rebase'
|
|
9614
|
-
AND table_name = 'refresh_tokens'
|
|
9615
|
-
`);
|
|
9616
|
-
if (constraintCheck.rows.length === 0) {
|
|
9617
|
-
try {
|
|
9618
|
-
await db.execute(sql`
|
|
9619
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9620
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9621
|
-
`);
|
|
9622
|
-
console.log("✅ Added unique_device_session constraint");
|
|
9623
|
-
} catch (e) {
|
|
9624
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
9625
|
-
if (errorMessage.includes("could not create unique index")) {
|
|
9626
|
-
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
9627
|
-
await db.execute(sql`
|
|
9628
|
-
DELETE FROM rebase.refresh_tokens a
|
|
9629
|
-
USING rebase.refresh_tokens b
|
|
9630
|
-
WHERE a.user_id = b.user_id
|
|
9631
|
-
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
9632
|
-
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
9633
|
-
AND a.created_at < b.created_at
|
|
9634
|
-
`);
|
|
9635
|
-
await db.execute(sql`
|
|
9636
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9637
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9638
|
-
`).catch((retryErr) => {
|
|
9639
|
-
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
9640
|
-
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
9641
|
-
});
|
|
9642
|
-
} else {
|
|
9643
|
-
console.error("Constraint migration issue:", errorMessage);
|
|
9644
|
-
}
|
|
9645
|
-
}
|
|
9646
|
-
}
|
|
9647
10194
|
await db.execute(sql`
|
|
9648
10195
|
CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
|
|
9649
10196
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
@@ -9669,18 +10216,7 @@ async function ensureAuthTablesExist(db) {
|
|
|
9669
10216
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9670
10217
|
)
|
|
9671
10218
|
`);
|
|
9672
|
-
await db
|
|
9673
|
-
ALTER TABLE rebase.users
|
|
9674
|
-
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE
|
|
9675
|
-
`);
|
|
9676
|
-
await db.execute(sql`
|
|
9677
|
-
ALTER TABLE rebase.users
|
|
9678
|
-
ADD COLUMN IF NOT EXISTS email_verification_token TEXT
|
|
9679
|
-
`);
|
|
9680
|
-
await db.execute(sql`
|
|
9681
|
-
ALTER TABLE rebase.users
|
|
9682
|
-
ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
|
|
9683
|
-
`);
|
|
10219
|
+
await applyInternalMigrations(db);
|
|
9684
10220
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
9685
10221
|
await db.transaction(async (tx) => {
|
|
9686
10222
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
@@ -9733,6 +10269,99 @@ async function seedDefaultRoles(db) {
|
|
|
9733
10269
|
}
|
|
9734
10270
|
console.log("✅ Default roles created: admin, editor, viewer");
|
|
9735
10271
|
}
|
|
10272
|
+
async function applyInternalMigrations(db) {
|
|
10273
|
+
try {
|
|
10274
|
+
await db.execute(sql`
|
|
10275
|
+
ALTER TABLE rebase.users
|
|
10276
|
+
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
|
|
10277
|
+
ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
|
|
10278
|
+
ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
|
|
10279
|
+
`);
|
|
10280
|
+
const columnsCheck = await db.execute(sql`
|
|
10281
|
+
SELECT column_name
|
|
10282
|
+
FROM information_schema.columns
|
|
10283
|
+
WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
|
|
10284
|
+
`);
|
|
10285
|
+
const existingColumns = columnsCheck.rows.map((r) => r.column_name);
|
|
10286
|
+
if (existingColumns.includes("google_id")) {
|
|
10287
|
+
await db.execute(sql`
|
|
10288
|
+
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
10289
|
+
SELECT id, 'google', google_id
|
|
10290
|
+
FROM rebase.users
|
|
10291
|
+
WHERE google_id IS NOT NULL
|
|
10292
|
+
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
10293
|
+
`);
|
|
10294
|
+
}
|
|
10295
|
+
if (existingColumns.includes("linkedin_id")) {
|
|
10296
|
+
await db.execute(sql`
|
|
10297
|
+
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
10298
|
+
SELECT id, 'linkedin', linkedin_id
|
|
10299
|
+
FROM rebase.users
|
|
10300
|
+
WHERE linkedin_id IS NOT NULL
|
|
10301
|
+
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
10302
|
+
`);
|
|
10303
|
+
}
|
|
10304
|
+
if (existingColumns.length > 0) {
|
|
10305
|
+
await db.execute(sql`
|
|
10306
|
+
ALTER TABLE rebase.users
|
|
10307
|
+
DROP COLUMN IF EXISTS provider,
|
|
10308
|
+
DROP COLUMN IF EXISTS google_id,
|
|
10309
|
+
DROP COLUMN IF EXISTS linkedin_id
|
|
10310
|
+
`);
|
|
10311
|
+
await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
|
|
10312
|
+
await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
|
|
10313
|
+
console.log("✅ Migrated to user_identities and dropped legacy columns.");
|
|
10314
|
+
}
|
|
10315
|
+
await db.execute(sql`
|
|
10316
|
+
ALTER TABLE rebase.roles
|
|
10317
|
+
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
10318
|
+
`);
|
|
10319
|
+
await db.execute(sql`
|
|
10320
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10321
|
+
ADD COLUMN IF NOT EXISTS user_agent TEXT,
|
|
10322
|
+
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
10323
|
+
`);
|
|
10324
|
+
const constraintCheck = await db.execute(sql`
|
|
10325
|
+
SELECT 1 FROM information_schema.table_constraints
|
|
10326
|
+
WHERE constraint_name = 'unique_device_session'
|
|
10327
|
+
AND table_schema = 'rebase'
|
|
10328
|
+
AND table_name = 'refresh_tokens'
|
|
10329
|
+
`);
|
|
10330
|
+
if (constraintCheck.rows.length === 0) {
|
|
10331
|
+
try {
|
|
10332
|
+
await db.execute(sql`
|
|
10333
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10334
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
10335
|
+
`);
|
|
10336
|
+
console.log("✅ Added unique_device_session constraint");
|
|
10337
|
+
} catch (e) {
|
|
10338
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
10339
|
+
if (errorMessage.includes("could not create unique index")) {
|
|
10340
|
+
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
10341
|
+
await db.execute(sql`
|
|
10342
|
+
DELETE FROM rebase.refresh_tokens a
|
|
10343
|
+
USING rebase.refresh_tokens b
|
|
10344
|
+
WHERE a.user_id = b.user_id
|
|
10345
|
+
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
10346
|
+
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
10347
|
+
AND a.created_at < b.created_at
|
|
10348
|
+
`);
|
|
10349
|
+
await db.execute(sql`
|
|
10350
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10351
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
10352
|
+
`).catch((retryErr) => {
|
|
10353
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
10354
|
+
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
10355
|
+
});
|
|
10356
|
+
} else {
|
|
10357
|
+
console.error("Constraint migration issue:", errorMessage);
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
} catch (error) {
|
|
10362
|
+
console.error("❌ Failed to run internal migrations:", error);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
9736
10365
|
class UserService {
|
|
9737
10366
|
constructor(db) {
|
|
9738
10367
|
this.db = db;
|
|
@@ -9749,9 +10378,54 @@ class UserService {
|
|
|
9749
10378
|
const [user] = await this.db.select().from(users).where(eq$3(users.email, email.toLowerCase()));
|
|
9750
10379
|
return user || null;
|
|
9751
10380
|
}
|
|
9752
|
-
async
|
|
9753
|
-
const
|
|
9754
|
-
|
|
10381
|
+
async getUserByIdentity(provider, providerId) {
|
|
10382
|
+
const result = await this.db.execute(sql`
|
|
10383
|
+
SELECT u.*
|
|
10384
|
+
FROM rebase.users u
|
|
10385
|
+
INNER JOIN rebase.user_identities ui ON u.id = ui.user_id
|
|
10386
|
+
WHERE ui.provider = ${provider} AND ui.provider_id = ${providerId}
|
|
10387
|
+
LIMIT 1
|
|
10388
|
+
`);
|
|
10389
|
+
if (result.rows.length === 0) return null;
|
|
10390
|
+
const row = result.rows[0];
|
|
10391
|
+
return {
|
|
10392
|
+
id: row.id,
|
|
10393
|
+
email: row.email,
|
|
10394
|
+
passwordHash: row.password_hash ?? null,
|
|
10395
|
+
displayName: row.display_name ?? null,
|
|
10396
|
+
photoUrl: row.photo_url ?? null,
|
|
10397
|
+
emailVerified: row.email_verified ?? false,
|
|
10398
|
+
emailVerificationToken: row.email_verification_token ?? null,
|
|
10399
|
+
emailVerificationSentAt: row.email_verification_sent_at ?? null,
|
|
10400
|
+
createdAt: row.created_at,
|
|
10401
|
+
updatedAt: row.updated_at
|
|
10402
|
+
};
|
|
10403
|
+
}
|
|
10404
|
+
async getUserIdentities(userId) {
|
|
10405
|
+
const result = await this.db.execute(sql`
|
|
10406
|
+
SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
|
|
10407
|
+
FROM rebase.user_identities
|
|
10408
|
+
WHERE user_id = ${userId}
|
|
10409
|
+
`);
|
|
10410
|
+
return result.rows.map((row) => ({
|
|
10411
|
+
id: row.id,
|
|
10412
|
+
userId: row.user_id,
|
|
10413
|
+
provider: row.provider,
|
|
10414
|
+
providerId: row.provider_id,
|
|
10415
|
+
profileData: row.profile_data ?? null,
|
|
10416
|
+
createdAt: row.created_at,
|
|
10417
|
+
updatedAt: row.updated_at
|
|
10418
|
+
}));
|
|
10419
|
+
}
|
|
10420
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
10421
|
+
await this.db.insert(userIdentities).values({
|
|
10422
|
+
userId,
|
|
10423
|
+
provider,
|
|
10424
|
+
providerId,
|
|
10425
|
+
profileData: profileData || null
|
|
10426
|
+
}).onConflictDoNothing({
|
|
10427
|
+
target: [userIdentities.provider, userIdentities.providerId]
|
|
10428
|
+
});
|
|
9755
10429
|
}
|
|
9756
10430
|
async updateUser(id, data) {
|
|
9757
10431
|
const [user] = await this.db.update(users).set({
|
|
@@ -9772,6 +10446,7 @@ class UserService {
|
|
|
9772
10446
|
const search = options?.search?.trim() || "";
|
|
9773
10447
|
const orderBy = options?.orderBy || "createdAt";
|
|
9774
10448
|
const orderDir = options?.orderDir || "desc";
|
|
10449
|
+
const roleId = options?.roleId;
|
|
9775
10450
|
const columnMap = {
|
|
9776
10451
|
email: "email",
|
|
9777
10452
|
displayName: "display_name",
|
|
@@ -9781,42 +10456,34 @@ class UserService {
|
|
|
9781
10456
|
};
|
|
9782
10457
|
const orderColumn = columnMap[orderBy] || "created_at";
|
|
9783
10458
|
const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
|
|
9784
|
-
|
|
9785
|
-
|
|
10459
|
+
const conditions = [];
|
|
10460
|
+
if (roleId) {
|
|
10461
|
+
conditions.push(sql`EXISTS (SELECT 1 FROM rebase.user_roles ur WHERE ur.user_id = users.id AND ur.role_id = ${roleId})`);
|
|
10462
|
+
}
|
|
9786
10463
|
if (search) {
|
|
9787
10464
|
const pattern = `%${search}%`;
|
|
9788
|
-
|
|
9789
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
9790
|
-
WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
|
|
9791
|
-
`);
|
|
9792
|
-
total = countResult.rows[0].total;
|
|
9793
|
-
const dataResult = await this.db.execute(sql`
|
|
9794
|
-
SELECT * FROM rebase.users
|
|
9795
|
-
WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
|
|
9796
|
-
ORDER BY ${sql.raw(orderColumn)} ${direction}
|
|
9797
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
9798
|
-
`);
|
|
9799
|
-
rows = dataResult.rows;
|
|
9800
|
-
} else {
|
|
9801
|
-
const countResult = await this.db.execute(sql`
|
|
9802
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
9803
|
-
`);
|
|
9804
|
-
total = countResult.rows[0].total;
|
|
9805
|
-
const dataResult = await this.db.execute(sql`
|
|
9806
|
-
SELECT * FROM rebase.users
|
|
9807
|
-
ORDER BY ${sql.raw(orderColumn)} ${direction}
|
|
9808
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
9809
|
-
`);
|
|
9810
|
-
rows = dataResult.rows;
|
|
10465
|
+
conditions.push(sql`(email ILIKE ${pattern} OR display_name ILIKE ${pattern})`);
|
|
9811
10466
|
}
|
|
10467
|
+
const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
|
10468
|
+
const orderByClause = roleId ? sql`ORDER BY ${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (SELECT count(*) FROM rebase.user_roles ur WHERE ur.user_id = users.id) DESC, ${sql.raw(orderColumn)} ${direction}`;
|
|
10469
|
+
const countResult = await this.db.execute(sql`
|
|
10470
|
+
SELECT count(*)::int as total FROM rebase.users
|
|
10471
|
+
${whereClause}
|
|
10472
|
+
`);
|
|
10473
|
+
const total = countResult.rows[0].total;
|
|
10474
|
+
const dataResult = await this.db.execute(sql`
|
|
10475
|
+
SELECT * FROM rebase.users
|
|
10476
|
+
${whereClause}
|
|
10477
|
+
${orderByClause}
|
|
10478
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
10479
|
+
`);
|
|
10480
|
+
const rows = dataResult.rows;
|
|
9812
10481
|
const mappedUsers = rows.map((row) => ({
|
|
9813
10482
|
id: row.id,
|
|
9814
10483
|
email: row.email,
|
|
9815
10484
|
passwordHash: row.password_hash ?? row.passwordHash ?? null,
|
|
9816
10485
|
displayName: row.display_name ?? row.displayName ?? null,
|
|
9817
10486
|
photoUrl: row.photo_url ?? row.photoUrl ?? null,
|
|
9818
|
-
provider: row.provider,
|
|
9819
|
-
googleId: row.google_id ?? row.googleId ?? null,
|
|
9820
10487
|
emailVerified: row.email_verified ?? row.emailVerified ?? false,
|
|
9821
10488
|
emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
|
|
9822
10489
|
emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
|
|
@@ -10190,8 +10857,14 @@ class PostgresAuthRepository {
|
|
|
10190
10857
|
async getUserByEmail(email) {
|
|
10191
10858
|
return this.userService.getUserByEmail(email);
|
|
10192
10859
|
}
|
|
10193
|
-
async
|
|
10194
|
-
return this.userService.
|
|
10860
|
+
async getUserByIdentity(provider, providerId) {
|
|
10861
|
+
return this.userService.getUserByIdentity(provider, providerId);
|
|
10862
|
+
}
|
|
10863
|
+
async getUserIdentities(userId) {
|
|
10864
|
+
return this.userService.getUserIdentities(userId);
|
|
10865
|
+
}
|
|
10866
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
10867
|
+
return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
|
|
10195
10868
|
}
|
|
10196
10869
|
async updateUser(id, data) {
|
|
10197
10870
|
return this.userService.updateUser(id, data);
|
|
@@ -10319,16 +10992,6 @@ class HistoryService {
|
|
|
10319
10992
|
updatedBy
|
|
10320
10993
|
} = params;
|
|
10321
10994
|
const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
|
|
10322
|
-
try {
|
|
10323
|
-
require("fs").appendFileSync("/Users/francesco/rebase/packages/backend/history_diff.log", `[recordHistory: ${tableName}/${entityId} - ${action}]
|
|
10324
|
-
CHANGED FIELDS: ${JSON.stringify(changedFields)}
|
|
10325
|
-
PREVIOUS: ${JSON.stringify(previousValues, null, 2)}
|
|
10326
|
-
NEW: ${JSON.stringify(values, null, 2)}
|
|
10327
|
-
|
|
10328
|
-
`);
|
|
10329
|
-
} catch (e) {
|
|
10330
|
-
console.error("DEBUG FILE WRITE ERROR:", e);
|
|
10331
|
-
}
|
|
10332
10995
|
if (action === "update" && (!changedFields || changedFields.length === 0)) {
|
|
10333
10996
|
return;
|
|
10334
10997
|
}
|
|
@@ -10490,6 +11153,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10490
11153
|
const registry = new PostgresCollectionRegistry();
|
|
10491
11154
|
if (collections) {
|
|
10492
11155
|
registry.registerMultiple(collections);
|
|
11156
|
+
console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
10493
11157
|
}
|
|
10494
11158
|
if (pgConfig.schema?.tables) {
|
|
10495
11159
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -10556,9 +11220,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
10556
11220
|
const internals = driverResult.internals;
|
|
10557
11221
|
const db = internals.db;
|
|
10558
11222
|
await ensureAuthTablesExist(db);
|
|
10559
|
-
if (authConfig.google?.clientId) {
|
|
10560
|
-
configureGoogleOAuth(authConfig.google.clientId);
|
|
10561
|
-
}
|
|
10562
11223
|
let emailService;
|
|
10563
11224
|
if (authConfig.email) {
|
|
10564
11225
|
emailService = createEmailService(authConfig.email);
|
|
@@ -10627,6 +11288,8 @@ export {
|
|
|
10627
11288
|
refreshTokensRelations,
|
|
10628
11289
|
roles,
|
|
10629
11290
|
rolesRelations,
|
|
11291
|
+
userIdentities,
|
|
11292
|
+
userIdentitiesRelations,
|
|
10630
11293
|
userRoles,
|
|
10631
11294
|
userRolesRelations,
|
|
10632
11295
|
users,
|