@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.umd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(function(global2, factory) {
|
|
2
|
-
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("pg"), require("drizzle-orm/node-postgres"), require("drizzle-orm"), require("drizzle-orm/pg-core"), require("
|
|
3
|
-
})(this, function(exports2, pg, nodePostgres, drizzleOrm, pgCore, fs, path, url, chokidar, ws, events,
|
|
2
|
+
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("pg"), require("drizzle-orm/node-postgres"), require("drizzle-orm"), require("drizzle-orm/pg-core"), require("crypto"), require("fs"), require("path"), require("url"), require("chokidar"), require("ws"), require("events"), require("util"), require("@rebasepro/server-core")) : typeof define === "function" && define.amd ? define(["exports", "pg", "drizzle-orm/node-postgres", "drizzle-orm", "drizzle-orm/pg-core", "crypto", "fs", "path", "url", "chokidar", "ws", "events", "util", "@rebasepro/server-core"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2["Rebase Backend"] = {}, global2.pg, global2.nodePostgres, global2.drizzleOrm, global2.pgCore, global2.crypto, global2.fs, global2.path, global2.url, global2.chokidar, global2.ws, global2.events, global2.util, global2.serverCore));
|
|
3
|
+
})(this, function(exports2, pg, nodePostgres, drizzleOrm, pgCore, crypto, fs, path, url, chokidar, ws, events, util, serverCore) {
|
|
4
4
|
"use strict";
|
|
5
5
|
var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
6
6
|
function _interopNamespaceDefault(e) {
|
|
@@ -20,64 +20,118 @@
|
|
|
20
20
|
return Object.freeze(n);
|
|
21
21
|
}
|
|
22
22
|
const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const DEFAULT_POOL = {
|
|
24
|
+
max: 20,
|
|
25
|
+
idleTimeoutMillis: 3e4,
|
|
26
|
+
connectionTimeoutMillis: 1e4,
|
|
27
|
+
queryTimeout: 3e4,
|
|
28
|
+
statementTimeout: 3e4,
|
|
29
|
+
keepAlive: true
|
|
30
|
+
};
|
|
31
|
+
function createPostgresDatabaseConnection(connectionString, schema, poolConfig) {
|
|
32
|
+
const opts = {
|
|
33
|
+
...DEFAULT_POOL,
|
|
34
|
+
...poolConfig
|
|
35
|
+
};
|
|
36
|
+
const pgPoolConfig = {
|
|
25
37
|
connectionString,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Timeout for new connections
|
|
33
|
-
// Retry configuration
|
|
34
|
-
query_timeout: 3e4,
|
|
35
|
-
// Query timeout
|
|
36
|
-
statement_timeout: 3e4,
|
|
37
|
-
// Statement timeout
|
|
38
|
-
// Keep connections alive
|
|
39
|
-
keepAlive: true,
|
|
38
|
+
max: opts.max,
|
|
39
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
40
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
41
|
+
query_timeout: opts.queryTimeout,
|
|
42
|
+
statement_timeout: opts.statementTimeout,
|
|
43
|
+
keepAlive: opts.keepAlive,
|
|
40
44
|
keepAliveInitialDelayMillis: 0
|
|
41
|
-
}
|
|
45
|
+
};
|
|
46
|
+
const pool = new pg.Pool(pgPoolConfig);
|
|
42
47
|
pool.on("error", (err) => {
|
|
43
|
-
console.error("
|
|
48
|
+
console.error("[pg-pool] Unexpected pool error:", err.message);
|
|
44
49
|
if (err.message.includes("ETIMEDOUT")) {
|
|
45
|
-
console.warn("Connection timeout detected
|
|
50
|
+
console.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
|
|
46
51
|
}
|
|
47
52
|
});
|
|
48
|
-
pool.on("connect", (client) => {
|
|
49
|
-
console.debug("Database client connected");
|
|
50
|
-
client.on("error", (err) => {
|
|
51
|
-
console.error("Database client error:", err);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
pool.on("remove", (client) => {
|
|
55
|
-
console.debug("Database client removed from pool");
|
|
56
|
-
});
|
|
57
53
|
const db = schema ? nodePostgres.drizzle(pool, {
|
|
58
54
|
schema
|
|
59
55
|
}) : nodePostgres.drizzle(pool);
|
|
60
|
-
process.on("SIGINT", async () => {
|
|
61
|
-
console.log("SIGINT: Closing database pool...");
|
|
62
|
-
await pool.end();
|
|
63
|
-
process.exit(0);
|
|
64
|
-
});
|
|
65
|
-
process.on("SIGTERM", async () => {
|
|
66
|
-
console.log("SIGTERM: Closing database pool...");
|
|
67
|
-
await pool.end();
|
|
68
|
-
process.exit(0);
|
|
69
|
-
});
|
|
70
56
|
return {
|
|
71
57
|
db,
|
|
58
|
+
pool,
|
|
72
59
|
connectionString
|
|
73
60
|
};
|
|
74
61
|
}
|
|
75
62
|
function isPostgresCollection(collection) {
|
|
76
63
|
return !collection.driver || collection.driver === "postgres";
|
|
77
64
|
}
|
|
78
|
-
function
|
|
79
|
-
return
|
|
65
|
+
function isSQLAdmin(admin) {
|
|
66
|
+
return !!admin && typeof admin.executeSql === "function";
|
|
67
|
+
}
|
|
68
|
+
function isSchemaAdmin(admin) {
|
|
69
|
+
return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
|
|
70
|
+
}
|
|
71
|
+
const POSTGRES_CAPABILITIES = {
|
|
72
|
+
key: "postgres",
|
|
73
|
+
label: "PostgreSQL",
|
|
74
|
+
supportsRelations: true,
|
|
75
|
+
supportsSubcollections: false,
|
|
76
|
+
supportsRLS: true,
|
|
77
|
+
supportsReferences: false,
|
|
78
|
+
supportsColumnTypes: true,
|
|
79
|
+
supportsRealtime: true,
|
|
80
|
+
supportsSQLAdmin: true,
|
|
81
|
+
supportsDocumentAdmin: false,
|
|
82
|
+
supportsSchemaAdmin: true
|
|
83
|
+
};
|
|
84
|
+
const FIREBASE_CAPABILITIES = {
|
|
85
|
+
key: "firestore",
|
|
86
|
+
label: "Firebase / Firestore",
|
|
87
|
+
supportsRelations: false,
|
|
88
|
+
supportsSubcollections: true,
|
|
89
|
+
supportsRLS: false,
|
|
90
|
+
supportsReferences: true,
|
|
91
|
+
supportsColumnTypes: false,
|
|
92
|
+
supportsRealtime: true,
|
|
93
|
+
supportsSQLAdmin: false,
|
|
94
|
+
supportsDocumentAdmin: false,
|
|
95
|
+
supportsSchemaAdmin: false
|
|
96
|
+
};
|
|
97
|
+
const MONGODB_CAPABILITIES = {
|
|
98
|
+
key: "mongodb",
|
|
99
|
+
label: "MongoDB",
|
|
100
|
+
supportsRelations: false,
|
|
101
|
+
supportsSubcollections: true,
|
|
102
|
+
supportsRLS: false,
|
|
103
|
+
supportsReferences: true,
|
|
104
|
+
supportsColumnTypes: false,
|
|
105
|
+
supportsRealtime: false,
|
|
106
|
+
supportsSQLAdmin: false,
|
|
107
|
+
supportsDocumentAdmin: true,
|
|
108
|
+
supportsSchemaAdmin: true
|
|
109
|
+
};
|
|
110
|
+
const DEFAULT_CAPABILITIES = {
|
|
111
|
+
key: "(default)",
|
|
112
|
+
label: "Default",
|
|
113
|
+
supportsRelations: true,
|
|
114
|
+
supportsSubcollections: true,
|
|
115
|
+
supportsRLS: true,
|
|
116
|
+
supportsReferences: true,
|
|
117
|
+
supportsColumnTypes: true,
|
|
118
|
+
supportsRealtime: true,
|
|
119
|
+
supportsSQLAdmin: true,
|
|
120
|
+
supportsDocumentAdmin: true,
|
|
121
|
+
supportsSchemaAdmin: true
|
|
122
|
+
};
|
|
123
|
+
const CAPABILITIES_REGISTRY = {
|
|
124
|
+
postgres: POSTGRES_CAPABILITIES,
|
|
125
|
+
firestore: FIREBASE_CAPABILITIES,
|
|
126
|
+
mongodb: MONGODB_CAPABILITIES,
|
|
127
|
+
"(default)": DEFAULT_CAPABILITIES
|
|
128
|
+
};
|
|
129
|
+
function getDataSourceCapabilities(driver) {
|
|
130
|
+
if (!driver) return POSTGRES_CAPABILITIES;
|
|
131
|
+
return CAPABILITIES_REGISTRY[driver] ?? DEFAULT_CAPABILITIES;
|
|
80
132
|
}
|
|
133
|
+
const DEFAULT_ONE_OF_TYPE = "type";
|
|
134
|
+
const DEFAULT_ONE_OF_VALUE = "value";
|
|
81
135
|
const snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g;
|
|
82
136
|
const toSnakeCase = (str) => {
|
|
83
137
|
const regExpMatchArray = str.match(snakeCaseRegex);
|
|
@@ -975,6 +1029,21 @@
|
|
|
975
1029
|
const singularName = snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName;
|
|
976
1030
|
return `${singularName}_id`;
|
|
977
1031
|
}
|
|
1032
|
+
function createRelationRef(id, path2) {
|
|
1033
|
+
return {
|
|
1034
|
+
id,
|
|
1035
|
+
path: path2,
|
|
1036
|
+
__type: "relation"
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function createRelationRefWithData(id, path2, data) {
|
|
1040
|
+
return {
|
|
1041
|
+
id,
|
|
1042
|
+
path: path2,
|
|
1043
|
+
__type: "relation",
|
|
1044
|
+
data
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
978
1047
|
function enumToObjectEntries(enumValues) {
|
|
979
1048
|
if (Array.isArray(enumValues)) {
|
|
980
1049
|
return enumValues;
|
|
@@ -998,15 +1067,35 @@
|
|
|
998
1067
|
if (collection.childCollections) {
|
|
999
1068
|
return collection.childCollections() ?? [];
|
|
1000
1069
|
}
|
|
1001
|
-
if (
|
|
1070
|
+
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
1002
1071
|
return collection.subcollections() ?? [];
|
|
1003
1072
|
}
|
|
1004
|
-
if (
|
|
1073
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
|
|
1005
1074
|
const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
|
|
1006
1075
|
return manyRelations.map((r) => {
|
|
1007
1076
|
const target = r.target();
|
|
1008
|
-
|
|
1009
|
-
|
|
1077
|
+
if (!target) return void 0;
|
|
1078
|
+
const relationKey = r.relationName || target.slug;
|
|
1079
|
+
let customName;
|
|
1080
|
+
if (collection.properties) {
|
|
1081
|
+
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
1082
|
+
if (prop && prop[1].name) {
|
|
1083
|
+
customName = prop[1].name;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const baseOverrides = {
|
|
1087
|
+
slug: relationKey
|
|
1088
|
+
};
|
|
1089
|
+
if (customName) {
|
|
1090
|
+
baseOverrides.name = customName;
|
|
1091
|
+
baseOverrides.singularName = customName;
|
|
1092
|
+
}
|
|
1093
|
+
const targetWithOverrides = {
|
|
1094
|
+
...target,
|
|
1095
|
+
...baseOverrides
|
|
1096
|
+
};
|
|
1097
|
+
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
1098
|
+
}).filter((c) => Boolean(c));
|
|
1010
1099
|
}
|
|
1011
1100
|
return [];
|
|
1012
1101
|
}
|
|
@@ -1113,7 +1202,7 @@
|
|
|
1113
1202
|
if (!newRelation.foreignKeyOnTarget) {
|
|
1114
1203
|
let foundForeignKey = false;
|
|
1115
1204
|
try {
|
|
1116
|
-
const targetRelations = targetCollection.relations || [];
|
|
1205
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1117
1206
|
for (const targetRel of targetRelations) {
|
|
1118
1207
|
if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) {
|
|
1119
1208
|
try {
|
|
@@ -1139,7 +1228,7 @@
|
|
|
1139
1228
|
let isManyToManyInverse = false;
|
|
1140
1229
|
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
|
|
1141
1230
|
try {
|
|
1142
|
-
const targetRelations = targetCollection.relations || [];
|
|
1231
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1143
1232
|
for (const targetRel of targetRelations) {
|
|
1144
1233
|
if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
|
|
1145
1234
|
isManyToManyInverse = true;
|
|
@@ -1173,14 +1262,21 @@
|
|
|
1173
1262
|
}
|
|
1174
1263
|
return newRelation;
|
|
1175
1264
|
}
|
|
1265
|
+
const _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
1176
1266
|
function resolveCollectionRelations(collection) {
|
|
1267
|
+
const cached = _resolvedRelationsCache.get(collection);
|
|
1268
|
+
if (cached) return cached;
|
|
1269
|
+
if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
|
|
1270
|
+
const relCollection = collection;
|
|
1177
1271
|
const relations = {};
|
|
1178
|
-
|
|
1179
|
-
|
|
1272
|
+
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
1273
|
+
if (relCollection.relations) {
|
|
1274
|
+
relCollection.relations.forEach((relation) => {
|
|
1180
1275
|
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1181
1276
|
const relationKey = normalizedRelation.relationName;
|
|
1182
1277
|
if (relationKey) {
|
|
1183
1278
|
relations[relationKey] = normalizedRelation;
|
|
1279
|
+
registeredRelationNames.add(relationKey);
|
|
1184
1280
|
}
|
|
1185
1281
|
});
|
|
1186
1282
|
}
|
|
@@ -1192,15 +1288,17 @@
|
|
|
1192
1288
|
sourceCollection: collection
|
|
1193
1289
|
});
|
|
1194
1290
|
if (relation) {
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
relations[propKey] = sanitizeRelation(relation, collection);
|
|
1291
|
+
if (relations[propKey]) return;
|
|
1292
|
+
if (!relation.relationName) {
|
|
1293
|
+
relation.relationName = propKey;
|
|
1200
1294
|
}
|
|
1295
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1296
|
+
relations[propKey] = normalizedRelation;
|
|
1297
|
+
registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
|
|
1201
1298
|
}
|
|
1202
1299
|
});
|
|
1203
1300
|
}
|
|
1301
|
+
_resolvedRelationsCache.set(collection, relations);
|
|
1204
1302
|
return relations;
|
|
1205
1303
|
}
|
|
1206
1304
|
function resolvePropertyRelation({
|
|
@@ -1209,7 +1307,24 @@
|
|
|
1209
1307
|
sourceCollection
|
|
1210
1308
|
}) {
|
|
1211
1309
|
if (property.type !== "relation") return void 0;
|
|
1212
|
-
const
|
|
1310
|
+
const relProp = property;
|
|
1311
|
+
if (relProp.target) {
|
|
1312
|
+
return {
|
|
1313
|
+
relationName: relProp.relationName || propertyKey,
|
|
1314
|
+
target: relProp.target,
|
|
1315
|
+
cardinality: relProp.cardinality || "one",
|
|
1316
|
+
direction: relProp.direction || "owning",
|
|
1317
|
+
inverseRelationName: relProp.inverseRelationName,
|
|
1318
|
+
localKey: relProp.localKey,
|
|
1319
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget,
|
|
1320
|
+
through: relProp.through,
|
|
1321
|
+
joinPath: relProp.joinPath,
|
|
1322
|
+
onUpdate: relProp.onUpdate,
|
|
1323
|
+
onDelete: relProp.onDelete,
|
|
1324
|
+
overrides: relProp.overrides
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
|
|
1213
1328
|
if (!relation) {
|
|
1214
1329
|
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1215
1330
|
return void 0;
|
|
@@ -1217,7 +1332,7 @@
|
|
|
1217
1332
|
return relation;
|
|
1218
1333
|
}
|
|
1219
1334
|
function getTableName(collection) {
|
|
1220
|
-
if (
|
|
1335
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
1221
1336
|
return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1222
1337
|
}
|
|
1223
1338
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -1233,6 +1348,14 @@
|
|
|
1233
1348
|
function getColumnName(fullColumn) {
|
|
1234
1349
|
return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
|
|
1235
1350
|
}
|
|
1351
|
+
function findRelation(resolvedRelations, key) {
|
|
1352
|
+
if (resolvedRelations[key]) return resolvedRelations[key];
|
|
1353
|
+
const slugKey = key.replace(/_/g, "-");
|
|
1354
|
+
if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
|
|
1355
|
+
const snakeKey = key.replace(/-/g, "_");
|
|
1356
|
+
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
1357
|
+
return void 0;
|
|
1358
|
+
}
|
|
1236
1359
|
var logic = { exports: {} };
|
|
1237
1360
|
(function(module2, exports$1) {
|
|
1238
1361
|
(function(root2, factory) {
|
|
@@ -3005,10 +3128,12 @@
|
|
|
3005
3128
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
3006
3129
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
3007
3130
|
rootCollections = [];
|
|
3131
|
+
cachedCollectionsList = null;
|
|
3008
3132
|
// Raw configuration layer (used by Collection Editor AST generator)
|
|
3009
3133
|
rawCollectionsByTableName = /* @__PURE__ */ new Map();
|
|
3010
3134
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
3011
3135
|
rawRootCollections = [];
|
|
3136
|
+
cachedRawCollectionsList = null;
|
|
3012
3137
|
// Snapshot of raw input for idempotency check — compared BEFORE normalization
|
|
3013
3138
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
3014
3139
|
lastRawInputSnapshot = null;
|
|
@@ -3021,9 +3146,11 @@
|
|
|
3021
3146
|
this.collectionsByTableName.clear();
|
|
3022
3147
|
this.collectionsBySlug.clear();
|
|
3023
3148
|
this.rootCollections = [];
|
|
3149
|
+
this.cachedCollectionsList = null;
|
|
3024
3150
|
this.rawCollectionsByTableName.clear();
|
|
3025
3151
|
this.rawCollectionsBySlug.clear();
|
|
3026
3152
|
this.rawRootCollections = [];
|
|
3153
|
+
this.cachedRawCollectionsList = null;
|
|
3027
3154
|
}
|
|
3028
3155
|
/**
|
|
3029
3156
|
* Registers a collection and its subcollections recursively.
|
|
@@ -3056,12 +3183,14 @@
|
|
|
3056
3183
|
this.rawCollectionsBySlug.set(raw.slug, raw);
|
|
3057
3184
|
}
|
|
3058
3185
|
});
|
|
3059
|
-
normalizedCollections.forEach((c
|
|
3186
|
+
normalizedCollections.forEach((c) => {
|
|
3060
3187
|
const subcollections = getSubcollections(c);
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
this._registerRecursively(this.normalizeCollection(
|
|
3188
|
+
if (subcollections && subcollections.length > 0) {
|
|
3189
|
+
subcollections.forEach((subCollection) => {
|
|
3190
|
+
if (!subCollection) return;
|
|
3191
|
+
this._registerRecursively(this.normalizeCollection({
|
|
3192
|
+
...subCollection
|
|
3193
|
+
}), cloneDeep(subCollection));
|
|
3065
3194
|
});
|
|
3066
3195
|
}
|
|
3067
3196
|
});
|
|
@@ -3087,41 +3216,100 @@
|
|
|
3087
3216
|
if (rawCollection.slug) {
|
|
3088
3217
|
this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
|
|
3089
3218
|
}
|
|
3090
|
-
const subcollections = getSubcollections(
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
this._registerRecursively(this.normalizeCollection(
|
|
3219
|
+
const subcollections = getSubcollections(normalizedCollection);
|
|
3220
|
+
if (subcollections && subcollections.length > 0) {
|
|
3221
|
+
subcollections.forEach((subCollection) => {
|
|
3222
|
+
if (!subCollection) return;
|
|
3223
|
+
this._registerRecursively(this.normalizeCollection({
|
|
3224
|
+
...subCollection
|
|
3225
|
+
}), cloneDeep(subCollection));
|
|
3095
3226
|
});
|
|
3096
3227
|
}
|
|
3097
3228
|
}
|
|
3098
3229
|
normalizeCollection(collection) {
|
|
3099
|
-
const
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3230
|
+
const result = {
|
|
3231
|
+
...collection
|
|
3232
|
+
};
|
|
3233
|
+
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
3234
|
+
const relResult = result;
|
|
3235
|
+
const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
|
|
3236
|
+
const mergedRelationsRaw = [...extractedRelations];
|
|
3237
|
+
for (const manual of manualRelations) {
|
|
3238
|
+
const name = manual.relationName;
|
|
3239
|
+
if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
|
|
3240
|
+
mergedRelationsRaw.push(manual);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
let mergedRelations = mergedRelationsRaw;
|
|
3244
|
+
if (getDataSourceCapabilities(result.driver).supportsRelations) {
|
|
3245
|
+
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
3246
|
+
try {
|
|
3247
|
+
return sanitizeRelation(r, result);
|
|
3248
|
+
} catch {
|
|
3249
|
+
return r;
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
relResult.relations = mergedRelations;
|
|
3253
|
+
}
|
|
3254
|
+
const properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
3255
|
+
result.properties = properties;
|
|
3256
|
+
if (!result.childCollections) {
|
|
3257
|
+
if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) {
|
|
3258
|
+
result.childCollections = result.subcollections;
|
|
3259
|
+
} else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
|
|
3260
|
+
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
3107
3261
|
if (manyRelations.length > 0) {
|
|
3108
|
-
|
|
3262
|
+
result.childCollections = () => manyRelations.map((r) => {
|
|
3109
3263
|
const target = r.target();
|
|
3110
3264
|
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
3111
3265
|
});
|
|
3112
3266
|
}
|
|
3113
3267
|
}
|
|
3114
3268
|
}
|
|
3115
|
-
return
|
|
3269
|
+
return result;
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
3273
|
+
* This allows developers to define relations directly on properties without a separate
|
|
3274
|
+
* `relations[]` entry on the collection.
|
|
3275
|
+
*/
|
|
3276
|
+
extractRelationsFromProperties(properties) {
|
|
3277
|
+
const relations = [];
|
|
3278
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
3279
|
+
if (property.type === "relation") {
|
|
3280
|
+
const relProp = property;
|
|
3281
|
+
const target = relProp.target ?? relProp.relation?.target;
|
|
3282
|
+
if (target) {
|
|
3283
|
+
const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
|
|
3284
|
+
relations.push({
|
|
3285
|
+
relationName,
|
|
3286
|
+
target,
|
|
3287
|
+
cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
|
|
3288
|
+
direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
|
|
3289
|
+
inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
|
|
3290
|
+
localKey: relProp.localKey ?? relProp.relation?.localKey,
|
|
3291
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
|
|
3292
|
+
through: relProp.through ?? relProp.relation?.through,
|
|
3293
|
+
joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
|
|
3294
|
+
onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
|
|
3295
|
+
onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
|
|
3296
|
+
overrides: relProp.overrides ?? relProp.relation?.overrides
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
} else if (property.type === "map" && property.properties) {
|
|
3300
|
+
relations.push(...this.extractRelationsFromProperties(property.properties));
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return relations;
|
|
3116
3304
|
}
|
|
3117
3305
|
normalizeProperties(properties, relations) {
|
|
3118
3306
|
const newProperties = {};
|
|
3119
3307
|
for (const key in properties) {
|
|
3120
|
-
newProperties[key] = this.normalizeProperty(properties[key], relations);
|
|
3308
|
+
newProperties[key] = this.normalizeProperty(key, properties[key], relations);
|
|
3121
3309
|
}
|
|
3122
3310
|
return newProperties;
|
|
3123
3311
|
}
|
|
3124
|
-
normalizeProperty(property, relations) {
|
|
3312
|
+
normalizeProperty(key, property, relations) {
|
|
3125
3313
|
const newProperty = {
|
|
3126
3314
|
...property
|
|
3127
3315
|
};
|
|
@@ -3131,9 +3319,9 @@
|
|
|
3131
3319
|
const arrayProp = newProperty;
|
|
3132
3320
|
if (arrayProp.of) {
|
|
3133
3321
|
if (Array.isArray(arrayProp.of)) {
|
|
3134
|
-
arrayProp.of = arrayProp.of.map((p) => this.normalizeProperty(p, relations));
|
|
3322
|
+
arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations));
|
|
3135
3323
|
} else {
|
|
3136
|
-
arrayProp.of = this.normalizeProperty(arrayProp.of, relations);
|
|
3324
|
+
arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations);
|
|
3137
3325
|
}
|
|
3138
3326
|
} else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
|
|
3139
3327
|
arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations);
|
|
@@ -3145,11 +3333,12 @@
|
|
|
3145
3333
|
}
|
|
3146
3334
|
} else if (newProperty.type === "relation") {
|
|
3147
3335
|
const relationProperty = newProperty;
|
|
3148
|
-
const
|
|
3336
|
+
const name = relationProperty.relationName || key;
|
|
3337
|
+
const relation = relations.find((r) => r.relationName === name);
|
|
3149
3338
|
if (relation) {
|
|
3150
3339
|
relationProperty.relation = relation;
|
|
3151
3340
|
} else {
|
|
3152
|
-
console.warn(`Could not find relation for property with relationName: ${
|
|
3341
|
+
console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
|
|
3153
3342
|
}
|
|
3154
3343
|
}
|
|
3155
3344
|
return newProperty;
|
|
@@ -3157,6 +3346,11 @@
|
|
|
3157
3346
|
get(path2) {
|
|
3158
3347
|
const bySlug = this.collectionsBySlug.get(path2);
|
|
3159
3348
|
if (bySlug) return bySlug;
|
|
3349
|
+
if (path2.includes("-")) {
|
|
3350
|
+
const normalized = path2.replace(/-/g, "_");
|
|
3351
|
+
const byNormalized = this.collectionsBySlug.get(normalized);
|
|
3352
|
+
if (byNormalized) return byNormalized;
|
|
3353
|
+
}
|
|
3160
3354
|
return this.collectionsByTableName.get(path2);
|
|
3161
3355
|
}
|
|
3162
3356
|
/**
|
|
@@ -3166,6 +3360,11 @@
|
|
|
3166
3360
|
getRaw(path2) {
|
|
3167
3361
|
const bySlug = this.rawCollectionsBySlug.get(path2);
|
|
3168
3362
|
if (bySlug) return bySlug;
|
|
3363
|
+
if (path2.includes("-")) {
|
|
3364
|
+
const normalized = path2.replace(/-/g, "_");
|
|
3365
|
+
const byNormalized = this.rawCollectionsBySlug.get(normalized);
|
|
3366
|
+
if (byNormalized) return byNormalized;
|
|
3367
|
+
}
|
|
3169
3368
|
return this.rawCollectionsByTableName.get(path2);
|
|
3170
3369
|
}
|
|
3171
3370
|
/**
|
|
@@ -3187,11 +3386,11 @@
|
|
|
3187
3386
|
}
|
|
3188
3387
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
3189
3388
|
const relationKey = pathSegments[i];
|
|
3190
|
-
if (!
|
|
3191
|
-
throw new Error(`Relation path navigation requires a
|
|
3389
|
+
if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) {
|
|
3390
|
+
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
|
|
3192
3391
|
}
|
|
3193
3392
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
3194
|
-
const relation = resolvedRelations
|
|
3393
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
3195
3394
|
if (!relation) {
|
|
3196
3395
|
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
3197
3396
|
}
|
|
@@ -3201,10 +3400,16 @@
|
|
|
3201
3400
|
return currentCollection;
|
|
3202
3401
|
}
|
|
3203
3402
|
getCollections() {
|
|
3204
|
-
|
|
3403
|
+
if (!this.cachedCollectionsList) {
|
|
3404
|
+
this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
3405
|
+
}
|
|
3406
|
+
return this.cachedCollectionsList;
|
|
3205
3407
|
}
|
|
3206
3408
|
getRawCollections() {
|
|
3207
|
-
|
|
3409
|
+
if (!this.cachedRawCollectionsList) {
|
|
3410
|
+
this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
3411
|
+
}
|
|
3412
|
+
return this.cachedRawCollectionsList;
|
|
3208
3413
|
}
|
|
3209
3414
|
/**
|
|
3210
3415
|
* Resolves a multi-segment path like "products/123/locales" and returns
|
|
@@ -3260,24 +3465,62 @@
|
|
|
3260
3465
|
"lte": "<=",
|
|
3261
3466
|
"in": "in",
|
|
3262
3467
|
"nin": "not-in",
|
|
3468
|
+
"not-in": "not-in",
|
|
3263
3469
|
"cs": "array-contains",
|
|
3264
|
-
"csa": "array-contains-any"
|
|
3470
|
+
"csa": "array-contains-any",
|
|
3471
|
+
"==": "==",
|
|
3472
|
+
"!=": "!=",
|
|
3473
|
+
">": ">",
|
|
3474
|
+
">=": ">=",
|
|
3475
|
+
"<": "<",
|
|
3476
|
+
"<=": "<=",
|
|
3477
|
+
"array-contains": "array-contains",
|
|
3478
|
+
"array-contains-any": "array-contains-any"
|
|
3265
3479
|
};
|
|
3266
3480
|
const filter = {};
|
|
3267
3481
|
for (const [field, rawValue] of Object.entries(where)) {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
if (typeof
|
|
3273
|
-
|
|
3482
|
+
if (rawValue === null) {
|
|
3483
|
+
filter[field] = ["==", null];
|
|
3484
|
+
continue;
|
|
3485
|
+
}
|
|
3486
|
+
if (typeof rawValue === "boolean") {
|
|
3487
|
+
filter[field] = ["==", rawValue];
|
|
3488
|
+
continue;
|
|
3489
|
+
}
|
|
3490
|
+
if (typeof rawValue === "number") {
|
|
3491
|
+
filter[field] = ["==", rawValue];
|
|
3492
|
+
continue;
|
|
3274
3493
|
}
|
|
3275
|
-
if (
|
|
3276
|
-
|
|
3494
|
+
if (Array.isArray(rawValue) && rawValue.length === 2) {
|
|
3495
|
+
const [rawOp, val] = rawValue;
|
|
3496
|
+
const mappedOp = operatorMap[rawOp] ?? "==";
|
|
3497
|
+
filter[field] = [mappedOp, val];
|
|
3498
|
+
continue;
|
|
3277
3499
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3500
|
+
if (typeof rawValue === "string") {
|
|
3501
|
+
const dotIndex = rawValue.indexOf(".");
|
|
3502
|
+
if (dotIndex === -1) {
|
|
3503
|
+
filter[field] = ["==", rawValue];
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
const op = rawValue.substring(0, dotIndex);
|
|
3507
|
+
let value = rawValue.substring(dotIndex + 1);
|
|
3508
|
+
if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
|
|
3509
|
+
value = value.slice(1, -1).split(",").map((v) => v.trim());
|
|
3510
|
+
}
|
|
3511
|
+
if (value === "null") {
|
|
3512
|
+
value = null;
|
|
3513
|
+
} else if (value === "true") {
|
|
3514
|
+
value = true;
|
|
3515
|
+
} else if (value === "false") {
|
|
3516
|
+
value = false;
|
|
3517
|
+
} else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
|
|
3518
|
+
value = Number(value);
|
|
3519
|
+
}
|
|
3520
|
+
const mappedOp = operatorMap[op];
|
|
3521
|
+
if (mappedOp) {
|
|
3522
|
+
filter[field] = [mappedOp, value];
|
|
3523
|
+
}
|
|
3281
3524
|
}
|
|
3282
3525
|
}
|
|
3283
3526
|
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
@@ -3296,7 +3539,7 @@
|
|
|
3296
3539
|
const entities = await driver.fetchCollection({
|
|
3297
3540
|
path: slug,
|
|
3298
3541
|
limit: params?.limit,
|
|
3299
|
-
|
|
3542
|
+
offset: params?.offset,
|
|
3300
3543
|
filter: convertWhereToFilter(params?.where),
|
|
3301
3544
|
orderBy: orderParsed?.[0],
|
|
3302
3545
|
order: orderParsed?.[1],
|
|
@@ -3358,7 +3601,7 @@
|
|
|
3358
3601
|
return driver.listenCollection({
|
|
3359
3602
|
path: slug,
|
|
3360
3603
|
limit: params?.limit,
|
|
3361
|
-
|
|
3604
|
+
offset: params?.offset,
|
|
3362
3605
|
filter: convertWhereToFilter(params?.where),
|
|
3363
3606
|
orderBy: orderParsed?.[0],
|
|
3364
3607
|
order: orderParsed?.[1],
|
|
@@ -3405,7 +3648,8 @@
|
|
|
3405
3648
|
if (prop === "collection") return getAccessor;
|
|
3406
3649
|
if (typeof prop === "symbol") return void 0;
|
|
3407
3650
|
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3408
|
-
|
|
3651
|
+
const slug = toSnakeCase(prop);
|
|
3652
|
+
return getAccessor(slug);
|
|
3409
3653
|
}
|
|
3410
3654
|
});
|
|
3411
3655
|
}
|
|
@@ -3463,7 +3707,7 @@
|
|
|
3463
3707
|
* Build relation-based conditions for different relation types
|
|
3464
3708
|
*/
|
|
3465
3709
|
static buildRelationConditions(relation, parentEntityId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry) {
|
|
3466
|
-
console.debug(
|
|
3710
|
+
console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
|
|
3467
3711
|
relationName: relation.relationName,
|
|
3468
3712
|
cardinality: relation.cardinality,
|
|
3469
3713
|
direction: relation.direction,
|
|
@@ -3475,7 +3719,7 @@
|
|
|
3475
3719
|
const joinConditions = [];
|
|
3476
3720
|
const whereConditions = [];
|
|
3477
3721
|
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
3478
|
-
console.debug(
|
|
3722
|
+
console.debug("🔍 [buildRelationConditions] Using joinPath logic");
|
|
3479
3723
|
const {
|
|
3480
3724
|
joins,
|
|
3481
3725
|
finalCondition
|
|
@@ -3483,37 +3727,37 @@
|
|
|
3483
3727
|
joinConditions.push(...joins);
|
|
3484
3728
|
whereConditions.push(finalCondition);
|
|
3485
3729
|
} else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
3486
|
-
console.debug(
|
|
3730
|
+
console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
|
|
3487
3731
|
const junctionResult = this.buildJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
|
|
3488
3732
|
joinConditions.push(junctionResult.join);
|
|
3489
3733
|
whereConditions.push(junctionResult.condition);
|
|
3490
3734
|
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
|
|
3491
|
-
console.debug(
|
|
3735
|
+
console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
|
|
3492
3736
|
const junctionResult = this.buildInverseJunctionTableConditions(relation.through, targetIdColumn, parentEntityId, registry);
|
|
3493
3737
|
joinConditions.push(junctionResult.join);
|
|
3494
3738
|
whereConditions.push(junctionResult.condition);
|
|
3495
3739
|
} else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
|
|
3496
|
-
console.debug(
|
|
3740
|
+
console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
|
|
3497
3741
|
const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
|
|
3498
3742
|
if (junctionInfo) {
|
|
3499
|
-
console.debug(
|
|
3743
|
+
console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
|
|
3500
3744
|
const junctionResult = this.buildInverseJunctionTableConditions(junctionInfo, targetIdColumn, parentEntityId, registry);
|
|
3501
3745
|
joinConditions.push(junctionResult.join);
|
|
3502
3746
|
whereConditions.push(junctionResult.condition);
|
|
3503
3747
|
} else if (relation.foreignKeyOnTarget) {
|
|
3504
|
-
console.debug(
|
|
3748
|
+
console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
|
|
3505
3749
|
const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
|
|
3506
3750
|
whereConditions.push(simpleCondition);
|
|
3507
3751
|
} else {
|
|
3508
|
-
console.error(
|
|
3752
|
+
console.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
|
|
3509
3753
|
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.`);
|
|
3510
3754
|
}
|
|
3511
3755
|
} else {
|
|
3512
|
-
console.debug(
|
|
3756
|
+
console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
|
|
3513
3757
|
const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentEntityId);
|
|
3514
3758
|
whereConditions.push(simpleCondition);
|
|
3515
3759
|
}
|
|
3516
|
-
console.debug(
|
|
3760
|
+
console.debug("🔍 [buildRelationConditions] Final result:", {
|
|
3517
3761
|
joinConditionsCount: joinConditions.length,
|
|
3518
3762
|
whereConditionsCount: whereConditions.length
|
|
3519
3763
|
});
|
|
@@ -3764,7 +4008,8 @@
|
|
|
3764
4008
|
static buildSearchConditions(searchString, properties, table) {
|
|
3765
4009
|
const searchConditions = [];
|
|
3766
4010
|
for (const [key, prop] of Object.entries(properties)) {
|
|
3767
|
-
|
|
4011
|
+
const p = prop;
|
|
4012
|
+
if (p.type === "string" && !p.enum && p.isId !== "uuid") {
|
|
3768
4013
|
const fieldColumn = table[key];
|
|
3769
4014
|
if (fieldColumn) {
|
|
3770
4015
|
searchConditions.push(drizzleOrm.ilike(fieldColumn, `%${searchString}%`));
|
|
@@ -3927,29 +4172,29 @@
|
|
|
3927
4172
|
try {
|
|
3928
4173
|
console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
|
|
3929
4174
|
if (!relation.inverseRelationName) {
|
|
3930
|
-
console.debug(
|
|
4175
|
+
console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
|
|
3931
4176
|
return null;
|
|
3932
4177
|
}
|
|
3933
4178
|
const targetCollection = relation.target();
|
|
3934
4179
|
console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
|
|
3935
4180
|
const targetCollectionRelations = resolveCollectionRelations(targetCollection);
|
|
3936
|
-
console.debug(
|
|
4181
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
|
|
3937
4182
|
const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
|
|
3938
4183
|
if (!correspondingRelation) {
|
|
3939
4184
|
console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
|
|
3940
4185
|
return null;
|
|
3941
4186
|
}
|
|
3942
|
-
console.debug(
|
|
4187
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
|
|
3943
4188
|
relationName: correspondingRelation.relationName,
|
|
3944
4189
|
cardinality: correspondingRelation.cardinality,
|
|
3945
4190
|
direction: correspondingRelation.direction,
|
|
3946
4191
|
hasThrough: !!correspondingRelation.through
|
|
3947
4192
|
});
|
|
3948
4193
|
if (correspondingRelation.cardinality !== "many" || correspondingRelation.direction !== "owning" || !correspondingRelation.through) {
|
|
3949
|
-
console.debug(
|
|
4194
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
|
|
3950
4195
|
return null;
|
|
3951
4196
|
}
|
|
3952
|
-
console.debug(
|
|
4197
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
|
|
3953
4198
|
const through = correspondingRelation.through;
|
|
3954
4199
|
const result = {
|
|
3955
4200
|
table: through.table,
|
|
@@ -3958,7 +4203,7 @@
|
|
|
3958
4203
|
targetColumn: through.sourceColumn
|
|
3959
4204
|
// Swapped for inverse relation
|
|
3960
4205
|
};
|
|
3961
|
-
console.debug(
|
|
4206
|
+
console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
|
|
3962
4207
|
return result;
|
|
3963
4208
|
} catch (error) {
|
|
3964
4209
|
console.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}':`, error);
|
|
@@ -3967,10 +4212,19 @@
|
|
|
3967
4212
|
}
|
|
3968
4213
|
}
|
|
3969
4214
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
4215
|
+
function getColumnMeta(col) {
|
|
4216
|
+
const raw = col;
|
|
4217
|
+
return {
|
|
4218
|
+
columnType: typeof raw.columnType === "string" ? raw.columnType : void 0,
|
|
4219
|
+
dataType: typeof raw.dataType === "string" ? raw.dataType : void 0,
|
|
4220
|
+
primary: typeof raw.primary === "boolean" ? raw.primary : void 0
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
3970
4223
|
function getCollectionByPath(collectionPath, registry) {
|
|
3971
4224
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
3972
4225
|
if (!collection) {
|
|
3973
|
-
|
|
4226
|
+
const registered = registry.getCollections().map((c) => c.slug).join(", ");
|
|
4227
|
+
throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
|
|
3974
4228
|
}
|
|
3975
4229
|
return collection;
|
|
3976
4230
|
}
|
|
@@ -3987,7 +4241,8 @@
|
|
|
3987
4241
|
if (collection.properties) {
|
|
3988
4242
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
3989
4243
|
fieldName: key,
|
|
3990
|
-
type: prop.type === "number" ? "number" : "string"
|
|
4244
|
+
type: prop.type === "number" ? "number" : "string",
|
|
4245
|
+
isUUID: prop.isId === "uuid"
|
|
3991
4246
|
}));
|
|
3992
4247
|
if (idProps.length > 0) {
|
|
3993
4248
|
return idProps;
|
|
@@ -3997,19 +4252,25 @@
|
|
|
3997
4252
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
3998
4253
|
const col = colRaw;
|
|
3999
4254
|
if (col && typeof col === "object" && "primary" in col && col.primary) {
|
|
4000
|
-
const
|
|
4255
|
+
const meta = getColumnMeta(col);
|
|
4256
|
+
const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
|
|
4257
|
+
const isUUID = meta.columnType === "PgUUID";
|
|
4001
4258
|
keys2.push({
|
|
4002
4259
|
fieldName: key,
|
|
4003
|
-
type
|
|
4260
|
+
type,
|
|
4261
|
+
isUUID
|
|
4004
4262
|
});
|
|
4005
4263
|
}
|
|
4006
4264
|
}
|
|
4007
4265
|
if (keys2.length === 0 && "id" in table) {
|
|
4008
4266
|
const idCol = table["id"];
|
|
4009
|
-
const
|
|
4267
|
+
const idMeta = getColumnMeta(idCol);
|
|
4268
|
+
const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
|
|
4269
|
+
const isUUID = idMeta.columnType === "PgUUID";
|
|
4010
4270
|
keys2.push({
|
|
4011
4271
|
fieldName: "id",
|
|
4012
|
-
type
|
|
4272
|
+
type,
|
|
4273
|
+
isUUID
|
|
4013
4274
|
});
|
|
4014
4275
|
}
|
|
4015
4276
|
return keys2;
|
|
@@ -4021,7 +4282,7 @@
|
|
|
4021
4282
|
}
|
|
4022
4283
|
if (primaryKeys.length === 1) {
|
|
4023
4284
|
const pk = primaryKeys[0];
|
|
4024
|
-
if (pk.type === "number") {
|
|
4285
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4025
4286
|
const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
|
|
4026
4287
|
if (isNaN(parsed)) {
|
|
4027
4288
|
throw new Error(`Invalid numeric ID: ${idValue}`);
|
|
@@ -4039,7 +4300,7 @@
|
|
|
4039
4300
|
for (let i = 0; i < primaryKeys.length; i++) {
|
|
4040
4301
|
const pk = primaryKeys[i];
|
|
4041
4302
|
const val = parts[i];
|
|
4042
|
-
if (pk.type === "number") {
|
|
4303
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4043
4304
|
const parsed = parseInt(val, 10);
|
|
4044
4305
|
if (isNaN(parsed)) {
|
|
4045
4306
|
throw new Error(`Invalid numeric ID component: ${val}`);
|
|
@@ -4098,28 +4359,37 @@
|
|
|
4098
4359
|
return obj;
|
|
4099
4360
|
}
|
|
4100
4361
|
function serializeDataToServer(entity, properties, collection, registry) {
|
|
4101
|
-
if (!entity || !properties) return
|
|
4362
|
+
if (!entity || !properties) return {
|
|
4363
|
+
scalarData: entity ?? {},
|
|
4364
|
+
inverseRelationUpdates: [],
|
|
4365
|
+
joinPathRelationUpdates: []
|
|
4366
|
+
};
|
|
4102
4367
|
const result = {};
|
|
4103
4368
|
const resolvedRelations = collection ? resolveCollectionRelations(collection) : {};
|
|
4104
4369
|
const inverseRelationUpdates = [];
|
|
4105
4370
|
const joinPathRelationUpdates = [];
|
|
4371
|
+
const foreignKeys = /* @__PURE__ */ new Set();
|
|
4372
|
+
Object.values(resolvedRelations).forEach((relation) => {
|
|
4373
|
+
if (relation.localKey) foreignKeys.add(relation.localKey);
|
|
4374
|
+
});
|
|
4106
4375
|
for (const [key, value] of Object.entries(entity)) {
|
|
4107
4376
|
const property = properties[key];
|
|
4377
|
+
const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
|
|
4108
4378
|
if (!property) {
|
|
4109
|
-
result[key] =
|
|
4379
|
+
result[key] = effectiveValue;
|
|
4110
4380
|
continue;
|
|
4111
4381
|
}
|
|
4112
4382
|
if (property.type === "relation" && collection) {
|
|
4113
|
-
const relation = resolvedRelations
|
|
4383
|
+
const relation = findRelation(resolvedRelations, key);
|
|
4114
4384
|
if (relation) {
|
|
4115
4385
|
if (relation.direction === "owning" && relation.localKey) {
|
|
4116
|
-
const serializedValue = serializePropertyToServer(
|
|
4117
|
-
if (serializedValue !==
|
|
4386
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4387
|
+
if (serializedValue !== void 0) {
|
|
4118
4388
|
result[relation.localKey] = serializedValue;
|
|
4119
4389
|
}
|
|
4120
4390
|
continue;
|
|
4121
4391
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
4122
|
-
const serializedValue = serializePropertyToServer(
|
|
4392
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4123
4393
|
const pks = getPrimaryKeys(collection, registry);
|
|
4124
4394
|
inverseRelationUpdates.push({
|
|
4125
4395
|
relationKey: key,
|
|
@@ -4129,7 +4399,7 @@
|
|
|
4129
4399
|
});
|
|
4130
4400
|
continue;
|
|
4131
4401
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
4132
|
-
const serializedValue = serializePropertyToServer(
|
|
4402
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4133
4403
|
if (relation.cardinality === "one") {
|
|
4134
4404
|
joinPathRelationUpdates.push({
|
|
4135
4405
|
relationKey: key,
|
|
@@ -4147,7 +4417,7 @@
|
|
|
4147
4417
|
}
|
|
4148
4418
|
continue;
|
|
4149
4419
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
4150
|
-
const serializedValue = serializePropertyToServer(
|
|
4420
|
+
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4151
4421
|
joinPathRelationUpdates.push({
|
|
4152
4422
|
relationKey: key,
|
|
4153
4423
|
relation,
|
|
@@ -4157,15 +4427,13 @@
|
|
|
4157
4427
|
}
|
|
4158
4428
|
}
|
|
4159
4429
|
}
|
|
4160
|
-
result[key] = serializePropertyToServer(
|
|
4430
|
+
result[key] = serializePropertyToServer(effectiveValue, property);
|
|
4161
4431
|
}
|
|
4162
|
-
|
|
4163
|
-
result
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
}
|
|
4168
|
-
return result;
|
|
4432
|
+
return {
|
|
4433
|
+
scalarData: result,
|
|
4434
|
+
inverseRelationUpdates,
|
|
4435
|
+
joinPathRelationUpdates
|
|
4436
|
+
};
|
|
4169
4437
|
}
|
|
4170
4438
|
function serializePropertyToServer(value, property) {
|
|
4171
4439
|
if (value === null || value === void 0) {
|
|
@@ -4179,10 +4447,28 @@
|
|
|
4179
4447
|
} else if (typeof value === "object" && value !== null && "id" in value) {
|
|
4180
4448
|
return value.id;
|
|
4181
4449
|
}
|
|
4450
|
+
if (value === "") return null;
|
|
4182
4451
|
return value;
|
|
4183
4452
|
case "array":
|
|
4184
|
-
if (Array.isArray(value)
|
|
4185
|
-
|
|
4453
|
+
if (Array.isArray(value)) {
|
|
4454
|
+
if (property.of) {
|
|
4455
|
+
return value.map((item) => serializePropertyToServer(item, property.of));
|
|
4456
|
+
} else if (property.oneOf) {
|
|
4457
|
+
const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
|
|
4458
|
+
const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
|
|
4459
|
+
return value.map((e) => {
|
|
4460
|
+
if (e === null) return null;
|
|
4461
|
+
if (typeof e !== "object") return e;
|
|
4462
|
+
const rec = e;
|
|
4463
|
+
const type = rec[typeField];
|
|
4464
|
+
const childProperty = property.oneOf?.properties[type];
|
|
4465
|
+
if (!type || !childProperty) return e;
|
|
4466
|
+
return {
|
|
4467
|
+
[typeField]: type,
|
|
4468
|
+
[valueField]: serializePropertyToServer(rec[valueField], childProperty)
|
|
4469
|
+
};
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4186
4472
|
}
|
|
4187
4473
|
return value;
|
|
4188
4474
|
case "map":
|
|
@@ -4206,38 +4492,20 @@
|
|
|
4206
4492
|
async function parseDataFromServer(data, collection, db, registry) {
|
|
4207
4493
|
const properties = collection.properties;
|
|
4208
4494
|
if (!data || !properties) return data;
|
|
4209
|
-
const result = {};
|
|
4210
4495
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4211
|
-
const
|
|
4212
|
-
|
|
4213
|
-
if (relation.localKey && !properties[relation.localKey]) {
|
|
4214
|
-
internalFKColumns.add(relation.localKey);
|
|
4215
|
-
}
|
|
4496
|
+
const result = normalizeScalarValues(data, properties, collection, resolvedRelations, {
|
|
4497
|
+
skipRelations: false
|
|
4216
4498
|
});
|
|
4217
|
-
for (const [key, value] of Object.entries(data)) {
|
|
4218
|
-
if (internalFKColumns.has(key)) {
|
|
4219
|
-
continue;
|
|
4220
|
-
}
|
|
4221
|
-
const property = properties[key];
|
|
4222
|
-
if (!property) {
|
|
4223
|
-
continue;
|
|
4224
|
-
}
|
|
4225
|
-
result[key] = parsePropertyFromServer(value, property, collection, key);
|
|
4226
|
-
}
|
|
4227
4499
|
for (const [propKey, property] of Object.entries(properties)) {
|
|
4228
4500
|
if (property.type === "relation" && !(propKey in result)) {
|
|
4229
|
-
const relation = resolvedRelations
|
|
4501
|
+
const relation = findRelation(resolvedRelations, propKey);
|
|
4230
4502
|
if (relation) {
|
|
4231
4503
|
if (relation.direction === "owning" && relation.localKey && relation.localKey in data) {
|
|
4232
4504
|
const fkValue = data[relation.localKey];
|
|
4233
4505
|
if (fkValue !== null && fkValue !== void 0) {
|
|
4234
4506
|
try {
|
|
4235
4507
|
const targetCollection = relation.target();
|
|
4236
|
-
result[propKey] =
|
|
4237
|
-
id: fkValue.toString(),
|
|
4238
|
-
path: targetCollection.slug,
|
|
4239
|
-
__type: "relation"
|
|
4240
|
-
};
|
|
4508
|
+
result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
|
|
4241
4509
|
} catch (e) {
|
|
4242
4510
|
console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
|
|
4243
4511
|
}
|
|
@@ -4256,18 +4524,10 @@
|
|
|
4256
4524
|
if (relation.cardinality === "one") {
|
|
4257
4525
|
const targetPks = getPrimaryKeys(targetCollection, registry);
|
|
4258
4526
|
const relatedEntity = relatedEntities[0];
|
|
4259
|
-
result[propKey] =
|
|
4260
|
-
id: buildCompositeId(relatedEntity, targetPks),
|
|
4261
|
-
path: targetCollection.slug,
|
|
4262
|
-
__type: "relation"
|
|
4263
|
-
};
|
|
4527
|
+
result[propKey] = createRelationRef(buildCompositeId(relatedEntity, targetPks), targetCollection.slug);
|
|
4264
4528
|
} else {
|
|
4265
4529
|
const targetPks = getPrimaryKeys(targetCollection, registry);
|
|
4266
|
-
result[propKey] = relatedEntities.map((entity) => (
|
|
4267
|
-
id: buildCompositeId(entity, targetPks),
|
|
4268
|
-
path: targetCollection.slug,
|
|
4269
|
-
__type: "relation"
|
|
4270
|
-
}));
|
|
4530
|
+
result[propKey] = relatedEntities.map((entity) => createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug));
|
|
4271
4531
|
}
|
|
4272
4532
|
}
|
|
4273
4533
|
}
|
|
@@ -4328,19 +4588,11 @@
|
|
|
4328
4588
|
if (relation.cardinality === "one") {
|
|
4329
4589
|
const joinResult = joinResults[0];
|
|
4330
4590
|
const targetEntity = joinResult[targetTableName] || joinResult;
|
|
4331
|
-
result[propKey] =
|
|
4332
|
-
id: buildCompositeId(targetEntity, targetPks),
|
|
4333
|
-
path: targetCollection.slug,
|
|
4334
|
-
__type: "relation"
|
|
4335
|
-
};
|
|
4591
|
+
result[propKey] = createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
|
|
4336
4592
|
} else {
|
|
4337
4593
|
result[propKey] = joinResults.map((joinResult) => {
|
|
4338
4594
|
const targetEntity = joinResult[targetTableName] || joinResult;
|
|
4339
|
-
return
|
|
4340
|
-
id: buildCompositeId(targetEntity, targetPks),
|
|
4341
|
-
path: targetCollection.slug,
|
|
4342
|
-
__type: "relation"
|
|
4343
|
-
};
|
|
4595
|
+
return createRelationRef(buildCompositeId(targetEntity, targetPks), targetCollection.slug);
|
|
4344
4596
|
});
|
|
4345
4597
|
}
|
|
4346
4598
|
}
|
|
@@ -4364,7 +4616,7 @@
|
|
|
4364
4616
|
let relationDef = property.relation;
|
|
4365
4617
|
if (!relationDef && propertyKey) {
|
|
4366
4618
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4367
|
-
relationDef = resolvedRelations
|
|
4619
|
+
relationDef = findRelation(resolvedRelations, propertyKey);
|
|
4368
4620
|
}
|
|
4369
4621
|
if (!relationDef) {
|
|
4370
4622
|
relationDef = collection.relations?.find((rel) => rel.relationName === property.relationName);
|
|
@@ -4375,11 +4627,7 @@
|
|
|
4375
4627
|
}
|
|
4376
4628
|
try {
|
|
4377
4629
|
const targetCollection = relationDef.target();
|
|
4378
|
-
return
|
|
4379
|
-
id: value.toString(),
|
|
4380
|
-
path: targetCollection.slug,
|
|
4381
|
-
__type: "relation"
|
|
4382
|
-
};
|
|
4630
|
+
return createRelationRef(value.toString(), targetCollection.slug);
|
|
4383
4631
|
} catch (e) {
|
|
4384
4632
|
console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
|
|
4385
4633
|
return value;
|
|
@@ -4387,8 +4635,25 @@
|
|
|
4387
4635
|
}
|
|
4388
4636
|
return value;
|
|
4389
4637
|
case "array":
|
|
4390
|
-
if (Array.isArray(value)
|
|
4391
|
-
|
|
4638
|
+
if (Array.isArray(value)) {
|
|
4639
|
+
if (property.of) {
|
|
4640
|
+
return value.map((item) => parsePropertyFromServer(item, property.of, collection));
|
|
4641
|
+
} else if (property.oneOf) {
|
|
4642
|
+
const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
|
|
4643
|
+
const valueField = property.oneOf.valueField ?? DEFAULT_ONE_OF_VALUE;
|
|
4644
|
+
return value.map((e) => {
|
|
4645
|
+
if (e === null) return null;
|
|
4646
|
+
if (typeof e !== "object") return e;
|
|
4647
|
+
const rec = e;
|
|
4648
|
+
const type = rec[typeField];
|
|
4649
|
+
const childProperty = property.oneOf?.properties[type];
|
|
4650
|
+
if (!type || !childProperty) return e;
|
|
4651
|
+
return {
|
|
4652
|
+
[typeField]: type,
|
|
4653
|
+
[valueField]: parsePropertyFromServer(rec[valueField], childProperty, collection)
|
|
4654
|
+
};
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4392
4657
|
}
|
|
4393
4658
|
return value;
|
|
4394
4659
|
case "map":
|
|
@@ -4433,11 +4698,8 @@
|
|
|
4433
4698
|
return value;
|
|
4434
4699
|
}
|
|
4435
4700
|
}
|
|
4436
|
-
function
|
|
4437
|
-
const properties = collection.properties;
|
|
4438
|
-
if (!data || !properties) return data;
|
|
4701
|
+
function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
|
|
4439
4702
|
const result = {};
|
|
4440
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4441
4703
|
const internalFKColumns = /* @__PURE__ */ new Set();
|
|
4442
4704
|
Object.values(resolvedRelations).forEach((relation) => {
|
|
4443
4705
|
if (relation.localKey && !properties[relation.localKey]) {
|
|
@@ -4445,14 +4707,25 @@
|
|
|
4445
4707
|
}
|
|
4446
4708
|
});
|
|
4447
4709
|
for (const [key, value] of Object.entries(data)) {
|
|
4448
|
-
if (internalFKColumns.has(key))
|
|
4710
|
+
if (internalFKColumns.has(key)) {
|
|
4711
|
+
result[key] = value === null ? null : typeof value === "number" ? value : String(value);
|
|
4712
|
+
continue;
|
|
4713
|
+
}
|
|
4449
4714
|
const property = properties[key];
|
|
4450
4715
|
if (!property) continue;
|
|
4451
|
-
if (property.type === "relation") continue;
|
|
4716
|
+
if (options.skipRelations && property.type === "relation") continue;
|
|
4452
4717
|
result[key] = parsePropertyFromServer(value, property, collection, key);
|
|
4453
4718
|
}
|
|
4454
4719
|
return result;
|
|
4455
4720
|
}
|
|
4721
|
+
function normalizeDbValues(data, collection) {
|
|
4722
|
+
const properties = collection.properties;
|
|
4723
|
+
if (!data || !properties) return data;
|
|
4724
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4725
|
+
return normalizeScalarValues(data, properties, collection, resolvedRelations, {
|
|
4726
|
+
skipRelations: true
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4456
4729
|
class RelationService {
|
|
4457
4730
|
constructor(db, registry) {
|
|
4458
4731
|
this.db = db;
|
|
@@ -4464,9 +4737,10 @@
|
|
|
4464
4737
|
async fetchRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
|
|
4465
4738
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
4466
4739
|
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
4467
|
-
const relation = resolvedRelations
|
|
4740
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
4468
4741
|
if (!relation) {
|
|
4469
|
-
|
|
4742
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
4743
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
4470
4744
|
}
|
|
4471
4745
|
return this.fetchEntitiesUsingJoins(parentCollection, parentEntityId, relation, options);
|
|
4472
4746
|
}
|
|
@@ -4566,8 +4840,11 @@
|
|
|
4566
4840
|
async countRelatedEntities(parentCollectionPath, parentEntityId, relationKey, options = {}) {
|
|
4567
4841
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
4568
4842
|
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
4569
|
-
const relation = resolvedRelations
|
|
4570
|
-
if (!relation)
|
|
4843
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
4844
|
+
if (!relation) {
|
|
4845
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
4846
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
4847
|
+
}
|
|
4571
4848
|
const targetCollection = relation.target();
|
|
4572
4849
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
4573
4850
|
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
@@ -4632,16 +4909,59 @@
|
|
|
4632
4909
|
const results2 = await query2;
|
|
4633
4910
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
4634
4911
|
const resultMap2 = /* @__PURE__ */ new Map();
|
|
4635
|
-
|
|
4912
|
+
for (const row of results2) {
|
|
4636
4913
|
const parentEntity = row[getTableName(parentCollection)] || row;
|
|
4637
4914
|
const targetEntity = row[targetTableName] || row;
|
|
4638
4915
|
const parentId = parentEntity[parentIdInfo.fieldName];
|
|
4639
|
-
|
|
4916
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4917
|
+
resultMap2.set(String(parentId), {
|
|
4640
4918
|
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4641
4919
|
path: targetCollection.slug,
|
|
4642
|
-
values:
|
|
4920
|
+
values: parsedValues
|
|
4643
4921
|
});
|
|
4644
|
-
}
|
|
4922
|
+
}
|
|
4923
|
+
return resultMap2;
|
|
4924
|
+
}
|
|
4925
|
+
if (relation.direction === "owning" && relation.localKey) {
|
|
4926
|
+
const localKeyCol = parentTable[relation.localKey];
|
|
4927
|
+
if (!localKeyCol) {
|
|
4928
|
+
throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
4929
|
+
}
|
|
4930
|
+
const fkRows = await this.db.select({
|
|
4931
|
+
parentId: parentIdCol,
|
|
4932
|
+
fkValue: localKeyCol
|
|
4933
|
+
}).from(parentTable).where(drizzleOrm.inArray(parentIdCol, parsedParentIds));
|
|
4934
|
+
const parentToFk = /* @__PURE__ */ new Map();
|
|
4935
|
+
const uniqueFkValues = [];
|
|
4936
|
+
const seenFks = /* @__PURE__ */ new Set();
|
|
4937
|
+
for (const row of fkRows) {
|
|
4938
|
+
if (row.fkValue == null) continue;
|
|
4939
|
+
parentToFk.set(String(row.parentId), row.fkValue);
|
|
4940
|
+
const fkStr = String(row.fkValue);
|
|
4941
|
+
if (!seenFks.has(fkStr)) {
|
|
4942
|
+
seenFks.add(fkStr);
|
|
4943
|
+
uniqueFkValues.push(row.fkValue);
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
if (uniqueFkValues.length === 0) return /* @__PURE__ */ new Map();
|
|
4947
|
+
const targetResults = await this.db.select().from(targetTable).where(drizzleOrm.inArray(targetIdField, uniqueFkValues));
|
|
4948
|
+
const targetById = /* @__PURE__ */ new Map();
|
|
4949
|
+
for (const row of targetResults) {
|
|
4950
|
+
const tid = String(row[targetIdInfo.fieldName]);
|
|
4951
|
+
targetById.set(tid, row);
|
|
4952
|
+
}
|
|
4953
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
4954
|
+
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
4955
|
+
const targetEntity = targetById.get(String(fkValue));
|
|
4956
|
+
if (targetEntity) {
|
|
4957
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4958
|
+
resultMap2.set(parentIdStr, {
|
|
4959
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4960
|
+
path: targetCollection.slug,
|
|
4961
|
+
values: parsedValues
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4645
4965
|
return resultMap2;
|
|
4646
4966
|
}
|
|
4647
4967
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
@@ -4659,7 +4979,8 @@
|
|
|
4659
4979
|
);
|
|
4660
4980
|
const results = await query;
|
|
4661
4981
|
const resultMap = /* @__PURE__ */ new Map();
|
|
4662
|
-
|
|
4982
|
+
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
4983
|
+
for (const row of results) {
|
|
4663
4984
|
const targetEntity = row[getTableName(targetCollection)] || row;
|
|
4664
4985
|
let parentId;
|
|
4665
4986
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
@@ -4667,22 +4988,133 @@
|
|
|
4667
4988
|
} else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
|
|
4668
4989
|
const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
|
|
4669
4990
|
parentId = targetEntity[inferredForeignKeyName];
|
|
4670
|
-
} else if (relation.direction === "owning" && relation.localKey) {
|
|
4671
|
-
for (const parsedParentId of parsedParentIds) {
|
|
4672
|
-
if (!resultMap.has(parsedParentId)) {
|
|
4673
|
-
parentId = parsedParentId;
|
|
4674
|
-
break;
|
|
4675
|
-
}
|
|
4676
|
-
}
|
|
4677
4991
|
}
|
|
4678
|
-
if (parentId !== void 0 &&
|
|
4679
|
-
|
|
4992
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
4993
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
4994
|
+
resultMap.set(String(parentId), {
|
|
4680
4995
|
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
4681
4996
|
path: targetCollection.slug,
|
|
4682
|
-
values:
|
|
4997
|
+
values: parsedValues
|
|
4683
4998
|
});
|
|
4684
4999
|
}
|
|
4685
|
-
}
|
|
5000
|
+
}
|
|
5001
|
+
return resultMap;
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* Batch fetch many-cardinality related entities for multiple parent entities.
|
|
5005
|
+
* Returns a Map<parentId, Entity[]> instead of Map<parentId, Entity>.
|
|
5006
|
+
* Uses a single SQL query with IN clause to avoid N+1.
|
|
5007
|
+
*/
|
|
5008
|
+
async batchFetchRelatedEntitiesMany(parentCollectionPath, parentEntityIds, _relationKey, relation) {
|
|
5009
|
+
if (parentEntityIds.length === 0) return /* @__PURE__ */ new Map();
|
|
5010
|
+
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5011
|
+
const targetCollection = relation.target();
|
|
5012
|
+
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5013
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
5014
|
+
const targetIdInfo = targetPks[0];
|
|
5015
|
+
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5016
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5017
|
+
const parentIdInfo = parentPks[0];
|
|
5018
|
+
const parentTable = this.registry.getTable(getTableName(parentCollection));
|
|
5019
|
+
if (!parentTable) throw new Error("Parent table not found");
|
|
5020
|
+
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
5021
|
+
const parsedParentIds = parentEntityIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
5022
|
+
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
5023
|
+
let query2 = this.db.select().from(parentTable).$dynamic();
|
|
5024
|
+
let currentTable = parentTable;
|
|
5025
|
+
for (const join of relation.joinPath) {
|
|
5026
|
+
const joinTable = this.registry.getTable(join.table);
|
|
5027
|
+
if (!joinTable) throw new Error(`Join table not found: ${join.table}`);
|
|
5028
|
+
const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
|
|
5029
|
+
const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
|
|
5030
|
+
const fromColName = fromColumn.split(".").pop();
|
|
5031
|
+
const toColName = toColumn.split(".").pop();
|
|
5032
|
+
const fromCol = currentTable[fromColName];
|
|
5033
|
+
const toCol = joinTable[toColName];
|
|
5034
|
+
if (!fromCol || !toCol) throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
|
|
5035
|
+
query2 = query2.innerJoin(joinTable, drizzleOrm.eq(fromCol, toCol));
|
|
5036
|
+
currentTable = joinTable;
|
|
5037
|
+
}
|
|
5038
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5039
|
+
query2 = query2.where(drizzleOrm.inArray(parentIdField, parsedParentIds));
|
|
5040
|
+
const results2 = await query2;
|
|
5041
|
+
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5042
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
5043
|
+
for (const row of results2) {
|
|
5044
|
+
const parentEntity = row[getTableName(parentCollection)] || row;
|
|
5045
|
+
const targetEntity = row[targetTableName] || row;
|
|
5046
|
+
const parentId = String(parentEntity[parentIdInfo.fieldName]);
|
|
5047
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
5048
|
+
const arr = resultMap2.get(parentId) || [];
|
|
5049
|
+
arr.push({
|
|
5050
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
5051
|
+
path: targetCollection.slug,
|
|
5052
|
+
values: parsedValues
|
|
5053
|
+
});
|
|
5054
|
+
resultMap2.set(parentId, arr);
|
|
5055
|
+
}
|
|
5056
|
+
return resultMap2;
|
|
5057
|
+
}
|
|
5058
|
+
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
5059
|
+
const junctionTable = this.registry.getTable(relation.through.table);
|
|
5060
|
+
if (!junctionTable) {
|
|
5061
|
+
console.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
5062
|
+
return /* @__PURE__ */ new Map();
|
|
5063
|
+
}
|
|
5064
|
+
const sourceJunctionCol = junctionTable[relation.through.sourceColumn];
|
|
5065
|
+
const targetJunctionCol = junctionTable[relation.through.targetColumn];
|
|
5066
|
+
if (!sourceJunctionCol || !targetJunctionCol) {
|
|
5067
|
+
console.warn(`[batchFetchRelatedEntitiesMany] Junction columns not found in '${relation.through.table}'`);
|
|
5068
|
+
return /* @__PURE__ */ new Map();
|
|
5069
|
+
}
|
|
5070
|
+
const query2 = this.db.select().from(junctionTable).innerJoin(targetTable, drizzleOrm.eq(targetJunctionCol, targetIdField)).where(drizzleOrm.inArray(sourceJunctionCol, parsedParentIds));
|
|
5071
|
+
const results2 = await query2;
|
|
5072
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
5073
|
+
const targetTableName = getTableName(targetCollection);
|
|
5074
|
+
for (const row of results2) {
|
|
5075
|
+
const junctionData = row[relation.through.table] || row;
|
|
5076
|
+
const targetData = row[targetTableName] || row;
|
|
5077
|
+
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5078
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
5079
|
+
const arr = resultMap2.get(parentId) || [];
|
|
5080
|
+
arr.push({
|
|
5081
|
+
id: String(targetData[targetIdInfo.fieldName]),
|
|
5082
|
+
path: targetCollection.slug,
|
|
5083
|
+
values: parsedValues
|
|
5084
|
+
});
|
|
5085
|
+
resultMap2.set(parentId, arr);
|
|
5086
|
+
}
|
|
5087
|
+
return resultMap2;
|
|
5088
|
+
}
|
|
5089
|
+
let query = this.db.select().from(targetTable).$dynamic();
|
|
5090
|
+
query = DrizzleConditionBuilder.buildRelationQuery(query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5091
|
+
const results = await query;
|
|
5092
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
5093
|
+
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
5094
|
+
for (const row of results) {
|
|
5095
|
+
const targetEntity = row[getTableName(targetCollection)] || row;
|
|
5096
|
+
let parentId;
|
|
5097
|
+
if (relation.through && relation.direction === "inverse") {
|
|
5098
|
+
const junctionData = row[relation.through.table] || row;
|
|
5099
|
+
parentId = junctionData[relation.through.targetColumn];
|
|
5100
|
+
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5101
|
+
parentId = targetEntity[relation.foreignKeyOnTarget];
|
|
5102
|
+
} else if (relation.direction === "inverse" && relation.inverseRelationName) {
|
|
5103
|
+
const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
|
|
5104
|
+
parentId = targetEntity[inferredForeignKeyName];
|
|
5105
|
+
}
|
|
5106
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5107
|
+
const parsedValues = await parseDataFromServer(targetEntity, targetCollection);
|
|
5108
|
+
const key = String(parentId);
|
|
5109
|
+
const arr = resultMap.get(key) || [];
|
|
5110
|
+
arr.push({
|
|
5111
|
+
id: String(targetEntity[targetIdInfo.fieldName]),
|
|
5112
|
+
path: targetCollection.slug,
|
|
5113
|
+
values: parsedValues
|
|
5114
|
+
});
|
|
5115
|
+
resultMap.set(key, arr);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
4686
5118
|
return resultMap;
|
|
4687
5119
|
}
|
|
4688
5120
|
/**
|
|
@@ -4691,7 +5123,7 @@
|
|
|
4691
5123
|
async updateRelationsUsingJoins(tx, collection, entityId, relationValues) {
|
|
4692
5124
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
4693
5125
|
for (const [key, value] of Object.entries(relationValues)) {
|
|
4694
|
-
const relation = resolvedRelations
|
|
5126
|
+
const relation = findRelation(resolvedRelations, key);
|
|
4695
5127
|
if (!relation || relation.cardinality !== "many") continue;
|
|
4696
5128
|
const targetEntityIds = value && Array.isArray(value) ? value.map((rel) => rel.id) : [];
|
|
4697
5129
|
const targetCollection = relation.target();
|
|
@@ -4775,6 +5207,8 @@
|
|
|
4775
5207
|
await tx.insert(junctionTable).values(newLinks);
|
|
4776
5208
|
}
|
|
4777
5209
|
}
|
|
5210
|
+
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
|
|
5211
|
+
console.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
|
|
4778
5212
|
} else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
4779
5213
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
4780
5214
|
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
@@ -5152,6 +5586,20 @@
|
|
|
5152
5586
|
// =============================================================
|
|
5153
5587
|
// DRIZZLE QUERY HELPERS
|
|
5154
5588
|
// =============================================================
|
|
5589
|
+
/**
|
|
5590
|
+
* Resolves the correct Drizzle column for sorting.
|
|
5591
|
+
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
5592
|
+
*/
|
|
5593
|
+
resolveOrderByField(table, orderBy, collection) {
|
|
5594
|
+
let orderByField = table[orderBy];
|
|
5595
|
+
if (!orderByField && collection) {
|
|
5596
|
+
const property = collection.properties[orderBy];
|
|
5597
|
+
if (property && property.type === "relation" && "relation" in property && property.relation?.direction === "owning") {
|
|
5598
|
+
orderByField = table[`${orderBy}_id`];
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
return orderByField;
|
|
5602
|
+
}
|
|
5155
5603
|
/**
|
|
5156
5604
|
* Build the `with` config for Drizzle's relational query API.
|
|
5157
5605
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
@@ -5164,12 +5612,10 @@
|
|
|
5164
5612
|
*/
|
|
5165
5613
|
buildWithConfig(collection, include) {
|
|
5166
5614
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5167
|
-
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5168
5615
|
const withConfig = {};
|
|
5169
5616
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
5170
5617
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
5171
5618
|
if (!shouldInclude(key)) continue;
|
|
5172
|
-
if (!include && !propertyKeys.has(key)) continue;
|
|
5173
5619
|
const drizzleRelName = relation.relationName || key;
|
|
5174
5620
|
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
5175
5621
|
continue;
|
|
@@ -5219,10 +5665,8 @@
|
|
|
5219
5665
|
*/
|
|
5220
5666
|
drizzleResultToEntity(row, collection, collectionPath, idInfo, databaseId, idInfoArray) {
|
|
5221
5667
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5222
|
-
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5223
5668
|
const normalizedValues = normalizeDbValues(row, collection);
|
|
5224
5669
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
5225
|
-
if (!propertyKeys.has(key)) continue;
|
|
5226
5670
|
const drizzleRelName = relation.relationName || key;
|
|
5227
5671
|
const relData = row[drizzleRelName];
|
|
5228
5672
|
if (relData === void 0 || relData === null) continue;
|
|
@@ -5241,17 +5685,12 @@
|
|
|
5241
5685
|
}
|
|
5242
5686
|
const relId = String(targetEntity[targetIdField] ?? targetEntity.id ?? targetEntity[Object.keys(targetEntity)[0]]);
|
|
5243
5687
|
const targetValues = normalizeDbValues(targetEntity, targetCollection);
|
|
5244
|
-
return {
|
|
5688
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
5245
5689
|
id: relId,
|
|
5246
5690
|
path: targetPath,
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
path: targetPath,
|
|
5251
|
-
values: targetValues,
|
|
5252
|
-
databaseId
|
|
5253
|
-
}
|
|
5254
|
-
};
|
|
5691
|
+
values: targetValues,
|
|
5692
|
+
databaseId
|
|
5693
|
+
});
|
|
5255
5694
|
});
|
|
5256
5695
|
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
5257
5696
|
const targetCollection = relation.target();
|
|
@@ -5261,17 +5700,12 @@
|
|
|
5261
5700
|
const relObj = relData;
|
|
5262
5701
|
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
5263
5702
|
const targetValues = normalizeDbValues(relObj, targetCollection);
|
|
5264
|
-
normalizedValues[key] = {
|
|
5703
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
5265
5704
|
id: relId,
|
|
5266
5705
|
path: targetPath,
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
path: targetPath,
|
|
5271
|
-
values: targetValues,
|
|
5272
|
-
databaseId
|
|
5273
|
-
}
|
|
5274
|
-
};
|
|
5706
|
+
values: targetValues,
|
|
5707
|
+
databaseId
|
|
5708
|
+
});
|
|
5275
5709
|
}
|
|
5276
5710
|
}
|
|
5277
5711
|
return {
|
|
@@ -5288,27 +5722,16 @@
|
|
|
5288
5722
|
*/
|
|
5289
5723
|
async resolveJoinPathRelations(entity, collection, collectionPath, parsedId, databaseId) {
|
|
5290
5724
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5291
|
-
const
|
|
5292
|
-
const promises = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
|
|
5725
|
+
const promises = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0).map(async ([key, relation]) => {
|
|
5293
5726
|
try {
|
|
5294
5727
|
const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {
|
|
5295
5728
|
limit: relation.cardinality === "one" ? 1 : void 0
|
|
5296
5729
|
});
|
|
5297
5730
|
if (relation.cardinality === "one" && relatedEntities.length > 0) {
|
|
5298
5731
|
const e = relatedEntities[0];
|
|
5299
|
-
entity.values[key] =
|
|
5300
|
-
id: e.id,
|
|
5301
|
-
path: e.path,
|
|
5302
|
-
__type: "relation",
|
|
5303
|
-
data: e
|
|
5304
|
-
};
|
|
5732
|
+
entity.values[key] = createRelationRefWithData(e.id, e.path, e);
|
|
5305
5733
|
} else if (relation.cardinality === "many") {
|
|
5306
|
-
entity.values[key] = relatedEntities.map((e) => (
|
|
5307
|
-
id: e.id,
|
|
5308
|
-
path: e.path,
|
|
5309
|
-
__type: "relation",
|
|
5310
|
-
data: e
|
|
5311
|
-
}));
|
|
5734
|
+
entity.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
|
|
5312
5735
|
}
|
|
5313
5736
|
} catch (e) {
|
|
5314
5737
|
console.warn(`Could not resolve joinPath relation '${key}':`, e);
|
|
@@ -5323,8 +5746,7 @@
|
|
|
5323
5746
|
async resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, databaseId) {
|
|
5324
5747
|
if (entities.length === 0) return;
|
|
5325
5748
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5326
|
-
const
|
|
5327
|
-
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.joinPath && relation.joinPath.length > 0);
|
|
5749
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
5328
5750
|
if (joinPathRelations.length === 0) return;
|
|
5329
5751
|
for (const [key, relation] of joinPathRelations) {
|
|
5330
5752
|
try {
|
|
@@ -5336,15 +5758,10 @@
|
|
|
5336
5758
|
for (const entity of entities) {
|
|
5337
5759
|
const parsed = parseIdValues(entity.id, [idInfo]);
|
|
5338
5760
|
const entityId = parsed[idInfo.fieldName];
|
|
5339
|
-
const relatedEntity = resultMap.get(entityId);
|
|
5761
|
+
const relatedEntity = resultMap.get(String(entityId));
|
|
5340
5762
|
if (relatedEntity) {
|
|
5341
5763
|
if (relation.cardinality === "one") {
|
|
5342
|
-
entity.values[key] =
|
|
5343
|
-
id: relatedEntity.id,
|
|
5344
|
-
path: relatedEntity.path,
|
|
5345
|
-
__type: "relation",
|
|
5346
|
-
data: relatedEntity
|
|
5347
|
-
};
|
|
5764
|
+
entity.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
|
|
5348
5765
|
}
|
|
5349
5766
|
}
|
|
5350
5767
|
}
|
|
@@ -5354,22 +5771,72 @@
|
|
|
5354
5771
|
}
|
|
5355
5772
|
}
|
|
5356
5773
|
/**
|
|
5357
|
-
*
|
|
5774
|
+
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
5775
|
+
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
5358
5776
|
*/
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
5362
|
-
};
|
|
5777
|
+
async resolveJoinPathRelationsBatchRest(rows, collection, collectionPath, idInfoArray, include) {
|
|
5778
|
+
if (rows.length === 0) return;
|
|
5363
5779
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5780
|
+
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
5781
|
+
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
5782
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
5783
|
+
if (joinPathRelations.length === 0) return;
|
|
5784
|
+
const idInfo = idInfoArray[0];
|
|
5785
|
+
for (const [key, relation] of joinPathRelations) {
|
|
5786
|
+
try {
|
|
5787
|
+
const entityIds = rows.map((r) => {
|
|
5788
|
+
const parsed = parseIdValues(String(r.id), idInfoArray);
|
|
5789
|
+
return parsed[idInfo.fieldName];
|
|
5790
|
+
});
|
|
5791
|
+
if (relation.cardinality === "one") {
|
|
5792
|
+
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5793
|
+
for (const row of rows) {
|
|
5794
|
+
const parsed = parseIdValues(String(row.id), idInfoArray);
|
|
5795
|
+
const entityId = parsed[idInfo.fieldName];
|
|
5796
|
+
const relatedEntity = resultMap.get(String(entityId));
|
|
5797
|
+
if (relatedEntity) {
|
|
5798
|
+
row[key] = {
|
|
5799
|
+
id: relatedEntity.id,
|
|
5800
|
+
...relatedEntity.values
|
|
5801
|
+
};
|
|
5802
|
+
} else {
|
|
5803
|
+
row[key] = null;
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
} else if (relation.cardinality === "many") {
|
|
5807
|
+
const resultMap = await this.batchFetchManyRelatedEntities(collectionPath, entityIds, key);
|
|
5808
|
+
for (const row of rows) {
|
|
5809
|
+
const parsed = parseIdValues(String(row.id), idInfoArray);
|
|
5810
|
+
const entityId = parsed[idInfo.fieldName];
|
|
5811
|
+
const relatedList = resultMap.get(String(entityId)) || [];
|
|
5812
|
+
row[key] = relatedList.map((e) => ({
|
|
5813
|
+
id: e.id,
|
|
5814
|
+
...e.values
|
|
5815
|
+
}));
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
} catch (e) {
|
|
5819
|
+
console.warn(`Could not batch resolve joinPath relation '${key}' for REST:`, e);
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
/**
|
|
5824
|
+
* Convert a db.query result row to a flat REST-style object with populated relations.
|
|
5825
|
+
*/
|
|
5826
|
+
drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
|
|
5827
|
+
const flat = {
|
|
5828
|
+
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
5829
|
+
};
|
|
5830
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5831
|
+
for (const [k, v] of Object.entries(row)) {
|
|
5832
|
+
if (k === idInfo.fieldName) continue;
|
|
5833
|
+
const relation = findRelation(resolvedRelations, k);
|
|
5834
|
+
if (Array.isArray(v) && relation) {
|
|
5835
|
+
flat[k] = v.map((item) => {
|
|
5836
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
5837
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
5838
|
+
if (nestedKey) {
|
|
5839
|
+
const nested = item[nestedKey];
|
|
5373
5840
|
return {
|
|
5374
5841
|
id: String(nested.id ?? nested[Object.keys(nested)[0]]),
|
|
5375
5842
|
...nested
|
|
@@ -5415,7 +5882,7 @@
|
|
|
5415
5882
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5416
5883
|
}
|
|
5417
5884
|
if (options.startAfter) {
|
|
5418
|
-
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
|
|
5885
|
+
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
5419
5886
|
if (cursorConditions.length > 0) allConditions.push(...cursorConditions);
|
|
5420
5887
|
}
|
|
5421
5888
|
if (allConditions.length > 0) {
|
|
@@ -5423,7 +5890,8 @@
|
|
|
5423
5890
|
}
|
|
5424
5891
|
const orderExpressions = [];
|
|
5425
5892
|
if (options.orderBy) {
|
|
5426
|
-
const
|
|
5893
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
5894
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5427
5895
|
if (orderByField) {
|
|
5428
5896
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
5429
5897
|
}
|
|
@@ -5434,16 +5902,18 @@
|
|
|
5434
5902
|
}
|
|
5435
5903
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5436
5904
|
if (limitValue) queryOpts.limit = limitValue;
|
|
5905
|
+
if (options.offset && options.offset > 0) queryOpts.offset = options.offset;
|
|
5437
5906
|
return queryOpts;
|
|
5438
5907
|
}
|
|
5439
5908
|
/**
|
|
5440
5909
|
* Extract cursor pagination conditions from startAfter options.
|
|
5441
5910
|
*/
|
|
5442
|
-
buildCursorConditions(table, idField, idInfo, options) {
|
|
5911
|
+
buildCursorConditions(table, idField, idInfo, options, collectionPath) {
|
|
5443
5912
|
if (!options.startAfter) return [];
|
|
5444
5913
|
const cursor = options.startAfter;
|
|
5445
5914
|
if (options.orderBy) {
|
|
5446
|
-
const
|
|
5915
|
+
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
|
|
5916
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5447
5917
|
if (orderByField) {
|
|
5448
5918
|
const startAfterOrderValue = cursor.values?.[options.orderBy] ?? cursor[options.orderBy];
|
|
5449
5919
|
const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
|
|
@@ -5505,11 +5975,7 @@
|
|
|
5505
5975
|
const relationPromises = Object.entries(resolvedRelations).filter(([key]) => propertyKeys.has(key)).map(async ([key, relation]) => {
|
|
5506
5976
|
if (relation.cardinality === "many") {
|
|
5507
5977
|
const relatedEntities = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, {});
|
|
5508
|
-
values[key] = relatedEntities.map((e) => (
|
|
5509
|
-
id: e.id,
|
|
5510
|
-
path: e.path,
|
|
5511
|
-
__type: "relation"
|
|
5512
|
-
}));
|
|
5978
|
+
values[key] = relatedEntities.map((e) => createRelationRef(e.id, e.path));
|
|
5513
5979
|
} else if (relation.cardinality === "one") {
|
|
5514
5980
|
if (values[key] == null) {
|
|
5515
5981
|
try {
|
|
@@ -5518,11 +5984,7 @@
|
|
|
5518
5984
|
});
|
|
5519
5985
|
if (relatedEntities.length > 0) {
|
|
5520
5986
|
const e = relatedEntities[0];
|
|
5521
|
-
values[key] =
|
|
5522
|
-
id: e.id,
|
|
5523
|
-
path: e.path,
|
|
5524
|
-
__type: "relation"
|
|
5525
|
-
};
|
|
5987
|
+
values[key] = createRelationRef(e.id, e.path);
|
|
5526
5988
|
}
|
|
5527
5989
|
} catch (e) {
|
|
5528
5990
|
console.warn(`Could not resolve one-to-one relation property: ${key}`, e);
|
|
@@ -5552,13 +6014,13 @@
|
|
|
5552
6014
|
}
|
|
5553
6015
|
const tableName = drizzleOrm.getTableName(table);
|
|
5554
6016
|
const qb = this.getQueryBuilder(tableName);
|
|
5555
|
-
|
|
6017
|
+
const withConfig = this.buildWithConfig(collection);
|
|
6018
|
+
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6019
|
+
if (qb && !options.searchString && !hasRelations) {
|
|
5556
6020
|
try {
|
|
5557
|
-
const
|
|
5558
|
-
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
6021
|
+
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5559
6022
|
const results2 = await qb.findMany(queryOpts);
|
|
5560
6023
|
const entities = results2.map((row) => this.drizzleResultToEntity(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray));
|
|
5561
|
-
await this.resolveJoinPathRelationsBatch(entities, collection, collectionPath, idInfo, options.databaseId);
|
|
5562
6024
|
return entities;
|
|
5563
6025
|
} catch (e) {
|
|
5564
6026
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
@@ -5581,7 +6043,7 @@
|
|
|
5581
6043
|
}
|
|
5582
6044
|
const orderExpressions = [];
|
|
5583
6045
|
if (options.orderBy) {
|
|
5584
|
-
const orderByField = table
|
|
6046
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5585
6047
|
if (orderByField) {
|
|
5586
6048
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
5587
6049
|
}
|
|
@@ -5589,7 +6051,7 @@
|
|
|
5589
6051
|
orderExpressions.push(drizzleOrm.desc(idField));
|
|
5590
6052
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5591
6053
|
if (options.startAfter) {
|
|
5592
|
-
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options);
|
|
6054
|
+
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
5593
6055
|
if (cursorConditions.length > 0) {
|
|
5594
6056
|
allConditions.push(...cursorConditions);
|
|
5595
6057
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
@@ -5598,6 +6060,7 @@
|
|
|
5598
6060
|
}
|
|
5599
6061
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5600
6062
|
if (limitValue) query = query.limit(limitValue);
|
|
6063
|
+
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5601
6064
|
const results = await query;
|
|
5602
6065
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5603
6066
|
}
|
|
@@ -5611,7 +6074,7 @@
|
|
|
5611
6074
|
async processEntityResults(results, collection, collectionPath, idInfo, databaseId, skipRelations = false, idInfoArray) {
|
|
5612
6075
|
if (results.length === 0) return [];
|
|
5613
6076
|
const entitiesWithValues = await Promise.all(results.map(async (entity) => {
|
|
5614
|
-
const values = await parseDataFromServer(entity, collection
|
|
6077
|
+
const values = await parseDataFromServer(entity, collection);
|
|
5615
6078
|
return {
|
|
5616
6079
|
entity,
|
|
5617
6080
|
values,
|
|
@@ -5636,37 +6099,29 @@
|
|
|
5636
6099
|
const relationResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5637
6100
|
entitiesMissingRelation.forEach((item) => {
|
|
5638
6101
|
const entityId = item.entity[idInfo.fieldName];
|
|
5639
|
-
const relatedEntity = relationResults.get(entityId);
|
|
6102
|
+
const relatedEntity = relationResults.get(String(entityId));
|
|
5640
6103
|
if (relatedEntity) {
|
|
5641
|
-
item.values[key] =
|
|
5642
|
-
id: relatedEntity.id,
|
|
5643
|
-
path: relatedEntity.path,
|
|
5644
|
-
__type: "relation",
|
|
5645
|
-
data: relatedEntity
|
|
5646
|
-
};
|
|
6104
|
+
item.values[key] = createRelationRefWithData(relatedEntity.id, relatedEntity.path, relatedEntity);
|
|
5647
6105
|
}
|
|
5648
6106
|
});
|
|
5649
6107
|
} catch (e) {
|
|
5650
6108
|
console.warn(`Could not batch load one-to-one relation property: ${key}`, e);
|
|
5651
6109
|
}
|
|
5652
6110
|
}
|
|
5653
|
-
const
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
await Promise.all(manyRelationQueries);
|
|
5668
|
-
});
|
|
5669
|
-
await Promise.all(manyRelationPromises);
|
|
6111
|
+
const manyRelations = Object.entries(resolvedRelations).filter(([key, relation]) => propertyKeys.has(key) && relation.cardinality === "many");
|
|
6112
|
+
for (const [key, relation] of manyRelations) {
|
|
6113
|
+
try {
|
|
6114
|
+
const entityIds = entitiesWithValues.map((item) => item.entity[idInfo.fieldName]);
|
|
6115
|
+
const relationResults = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, entityIds, key, relation);
|
|
6116
|
+
entitiesWithValues.forEach((item) => {
|
|
6117
|
+
const entityId = String(item.entity[idInfo.fieldName]);
|
|
6118
|
+
const relatedEntities = relationResults.get(entityId) || [];
|
|
6119
|
+
item.values[key] = relatedEntities.map((e) => createRelationRefWithData(e.id, e.path, e));
|
|
6120
|
+
});
|
|
6121
|
+
} catch (e) {
|
|
6122
|
+
console.warn(`Could not batch load many relation property: ${key}`, e);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
5670
6125
|
}
|
|
5671
6126
|
return entitiesWithValues.map((item) => ({
|
|
5672
6127
|
id: item.id,
|
|
@@ -5707,12 +6162,16 @@
|
|
|
5707
6162
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
5708
6163
|
const relationKey = pathSegments[i];
|
|
5709
6164
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
5710
|
-
const relation = resolvedRelations
|
|
6165
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
5711
6166
|
if (!relation) {
|
|
5712
6167
|
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
5713
6168
|
}
|
|
5714
6169
|
if (i === pathSegments.length - 1) {
|
|
5715
|
-
|
|
6170
|
+
const entities = await this.relationService.fetchRelatedEntities(currentCollection.slug, currentEntityId, relationKey, options);
|
|
6171
|
+
for (const entity of entities) {
|
|
6172
|
+
entity.path = path2;
|
|
6173
|
+
}
|
|
6174
|
+
return entities;
|
|
5716
6175
|
}
|
|
5717
6176
|
if (i + 1 < pathSegments.length) {
|
|
5718
6177
|
const nextEntityId = pathSegments[i + 1];
|
|
@@ -5734,11 +6193,19 @@
|
|
|
5734
6193
|
let query = this.db.select({
|
|
5735
6194
|
count: drizzleOrm.count()
|
|
5736
6195
|
}).from(table).$dynamic();
|
|
6196
|
+
const allConditions = [];
|
|
6197
|
+
if (options.searchString) {
|
|
6198
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
6199
|
+
if (searchConditions.length === 0) return 0;
|
|
6200
|
+
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
6201
|
+
}
|
|
5737
6202
|
if (options.filter) {
|
|
5738
6203
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5739
|
-
if (filterConditions.length > 0)
|
|
5740
|
-
|
|
5741
|
-
|
|
6204
|
+
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
6205
|
+
}
|
|
6206
|
+
if (allConditions.length > 0) {
|
|
6207
|
+
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
6208
|
+
if (finalCondition) query = query.where(finalCondition);
|
|
5742
6209
|
}
|
|
5743
6210
|
const result = await query;
|
|
5744
6211
|
return Number(result[0]?.count || 0);
|
|
@@ -5757,7 +6224,7 @@
|
|
|
5757
6224
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
5758
6225
|
const relationKey = pathSegments[i];
|
|
5759
6226
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
5760
|
-
const relation = resolvedRelations
|
|
6227
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
5761
6228
|
if (!relation) {
|
|
5762
6229
|
throw new Error(`Relation '${relationKey}' not found`);
|
|
5763
6230
|
}
|
|
@@ -5816,12 +6283,14 @@
|
|
|
5816
6283
|
const idField = table[idInfo.fieldName];
|
|
5817
6284
|
const tableName = drizzleOrm.getTableName(table);
|
|
5818
6285
|
const qb = this.getQueryBuilder(tableName);
|
|
5819
|
-
if (qb) {
|
|
6286
|
+
if (qb && !options.searchString) {
|
|
5820
6287
|
try {
|
|
5821
6288
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5822
6289
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
5823
6290
|
const results = await qb.findMany(queryOpts);
|
|
5824
|
-
|
|
6291
|
+
const restRows = results.map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
|
|
6292
|
+
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
6293
|
+
return restRows;
|
|
5825
6294
|
} catch (e) {
|
|
5826
6295
|
console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
|
|
5827
6296
|
}
|
|
@@ -5843,7 +6312,7 @@
|
|
|
5843
6312
|
const batchResults = await this.relationService.batchFetchRelatedEntities(collectionPath, entityIds, key, relation);
|
|
5844
6313
|
for (const entity of entities) {
|
|
5845
6314
|
const eid = entity[idInfo.fieldName];
|
|
5846
|
-
const related = batchResults.get(eid);
|
|
6315
|
+
const related = batchResults.get(String(eid));
|
|
5847
6316
|
if (related) {
|
|
5848
6317
|
entity[key] = {
|
|
5849
6318
|
id: related.id,
|
|
@@ -5899,7 +6368,9 @@
|
|
|
5899
6368
|
} : {}
|
|
5900
6369
|
});
|
|
5901
6370
|
if (!row) return null;
|
|
5902
|
-
|
|
6371
|
+
const restRow = this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
|
|
6372
|
+
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
6373
|
+
return restRow;
|
|
5903
6374
|
} catch (e) {
|
|
5904
6375
|
console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
|
|
5905
6376
|
}
|
|
@@ -5967,7 +6438,7 @@
|
|
|
5967
6438
|
}
|
|
5968
6439
|
const orderExpressions = [];
|
|
5969
6440
|
if (options.orderBy) {
|
|
5970
|
-
const orderByField = table
|
|
6441
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5971
6442
|
if (orderByField) {
|
|
5972
6443
|
orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
|
|
5973
6444
|
}
|
|
@@ -5976,6 +6447,7 @@
|
|
|
5976
6447
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5977
6448
|
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5978
6449
|
if (limitValue) query = query.limit(limitValue);
|
|
6450
|
+
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5979
6451
|
return await query;
|
|
5980
6452
|
}
|
|
5981
6453
|
/**
|
|
@@ -6024,7 +6496,7 @@
|
|
|
6024
6496
|
}
|
|
6025
6497
|
}
|
|
6026
6498
|
if (options.orderBy) {
|
|
6027
|
-
const orderByField = table
|
|
6499
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
6028
6500
|
if (orderByField) {
|
|
6029
6501
|
queryOpts.orderBy = options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField);
|
|
6030
6502
|
}
|
|
@@ -6078,17 +6550,15 @@
|
|
|
6078
6550
|
* Groups results by parent ID to avoid N+1.
|
|
6079
6551
|
*/
|
|
6080
6552
|
async batchFetchManyRelatedEntities(parentCollectionPath, parentIds, relationKey) {
|
|
6081
|
-
|
|
6082
|
-
const
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
}
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
await Promise.all(batchPromises);
|
|
6091
|
-
return resultMap;
|
|
6553
|
+
if (parentIds.length === 0) return /* @__PURE__ */ new Map();
|
|
6554
|
+
const collection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
6555
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6556
|
+
const relation = resolvedRelations[relationKey];
|
|
6557
|
+
if (!relation) {
|
|
6558
|
+
console.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
|
|
6559
|
+
return /* @__PURE__ */ new Map();
|
|
6560
|
+
}
|
|
6561
|
+
return this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
|
|
6092
6562
|
}
|
|
6093
6563
|
}
|
|
6094
6564
|
class EntityPersistService {
|
|
@@ -6124,6 +6594,7 @@
|
|
|
6124
6594
|
const effectiveValues = {
|
|
6125
6595
|
...values
|
|
6126
6596
|
};
|
|
6597
|
+
let junctionTableInfo;
|
|
6127
6598
|
if (collectionPath.includes("/")) {
|
|
6128
6599
|
const segments = collectionPath.split("/").filter(Boolean);
|
|
6129
6600
|
if (segments.length >= 3 && segments.length % 2 === 1) {
|
|
@@ -6133,9 +6604,10 @@
|
|
|
6133
6604
|
for (let i = 2; i < segments.length; i += 2) {
|
|
6134
6605
|
const relationKey = segments[i];
|
|
6135
6606
|
const resolvedRelations2 = resolveCollectionRelations(currentCollection);
|
|
6136
|
-
const relation = resolvedRelations2
|
|
6607
|
+
const relation = findRelation(resolvedRelations2, relationKey);
|
|
6137
6608
|
if (!relation) {
|
|
6138
|
-
|
|
6609
|
+
const available = Object.keys(resolvedRelations2).join(", ") || "(none)";
|
|
6610
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
|
|
6139
6611
|
}
|
|
6140
6612
|
if (i === segments.length - 1) {
|
|
6141
6613
|
const targetCollection = relation.target();
|
|
@@ -6145,7 +6617,7 @@
|
|
|
6145
6617
|
const parentIdInfo2 = parentIdInfoArray2[0];
|
|
6146
6618
|
const parsedParentIdObj2 = parseIdValues(currentEntityId, parentIdInfoArray2);
|
|
6147
6619
|
const parsedParentId2 = parsedParentIdObj2[parentIdInfo2.fieldName];
|
|
6148
|
-
|
|
6620
|
+
junctionTableInfo = {
|
|
6149
6621
|
parentCollection: currentCollection,
|
|
6150
6622
|
parentId: parsedParentId2,
|
|
6151
6623
|
relation,
|
|
@@ -6214,14 +6686,10 @@
|
|
|
6214
6686
|
}
|
|
6215
6687
|
}
|
|
6216
6688
|
}
|
|
6217
|
-
const
|
|
6218
|
-
const inverseRelationUpdates =
|
|
6219
|
-
const joinPathRelationUpdates =
|
|
6220
|
-
const
|
|
6221
|
-
delete processedData.__inverseRelationUpdates;
|
|
6222
|
-
delete processedData.__joinPathRelationUpdates;
|
|
6223
|
-
delete processedData.__junction_table_info;
|
|
6224
|
-
const entityData = sanitizeAndConvertDates(processedData);
|
|
6689
|
+
const serializedResult = serializeDataToServer(otherValues, collection.properties, collection, this.registry);
|
|
6690
|
+
const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
|
|
6691
|
+
const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
|
|
6692
|
+
const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
|
|
6225
6693
|
let savedId;
|
|
6226
6694
|
try {
|
|
6227
6695
|
savedId = await this.db.transaction(async (tx) => {
|
|
@@ -6234,7 +6702,7 @@
|
|
|
6234
6702
|
}
|
|
6235
6703
|
const scalarKeys = Object.keys(entityData);
|
|
6236
6704
|
if (scalarKeys.length > 0) {
|
|
6237
|
-
|
|
6705
|
+
const updateQuery = tx.update(table).set(entityData);
|
|
6238
6706
|
const conditions = [];
|
|
6239
6707
|
for (const info of idInfoArray) {
|
|
6240
6708
|
const field = table[info.fieldName];
|
|
@@ -6598,21 +7066,6 @@
|
|
|
6598
7066
|
if (poolManager) {
|
|
6599
7067
|
this.branchService = new BranchService(db, poolManager);
|
|
6600
7068
|
}
|
|
6601
|
-
this.admin = {
|
|
6602
|
-
executeSql: this.executeSql.bind(this),
|
|
6603
|
-
fetchAvailableDatabases: this.fetchAvailableDatabases.bind(this),
|
|
6604
|
-
fetchAvailableRoles: this.fetchAvailableRoles.bind(this),
|
|
6605
|
-
fetchCurrentDatabase: this.fetchCurrentDatabase.bind(this),
|
|
6606
|
-
fetchUnmappedTables: this.fetchUnmappedTables.bind(this),
|
|
6607
|
-
fetchTableMetadata: this.fetchTableMetadata.bind(this),
|
|
6608
|
-
// Branch operations (only available when poolManager is configured)
|
|
6609
|
-
...this.branchService ? {
|
|
6610
|
-
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
6611
|
-
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
6612
|
-
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
6613
|
-
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
6614
|
-
} : {}
|
|
6615
|
-
};
|
|
6616
7069
|
}
|
|
6617
7070
|
key = "postgres";
|
|
6618
7071
|
initialised = true;
|
|
@@ -6630,8 +7083,26 @@
|
|
|
6630
7083
|
_pendingNotifications = [];
|
|
6631
7084
|
/**
|
|
6632
7085
|
* Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin).
|
|
7086
|
+
* Implemented as a getter so method references are resolved at call-time,
|
|
7087
|
+
* allowing test spies applied after construction to take effect.
|
|
6633
7088
|
*/
|
|
6634
|
-
admin
|
|
7089
|
+
get admin() {
|
|
7090
|
+
return {
|
|
7091
|
+
executeSql: (...args) => this.executeSql(...args),
|
|
7092
|
+
fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
|
|
7093
|
+
fetchAvailableRoles: () => this.fetchAvailableRoles(),
|
|
7094
|
+
fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
|
|
7095
|
+
fetchUnmappedTables: (...args) => this.fetchUnmappedTables(...args),
|
|
7096
|
+
fetchTableMetadata: (...args) => this.fetchTableMetadata(...args),
|
|
7097
|
+
// Branch operations (only available when poolManager is configured)
|
|
7098
|
+
...this.branchService ? {
|
|
7099
|
+
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
7100
|
+
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
7101
|
+
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
7102
|
+
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
7103
|
+
} : {}
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
6635
7106
|
resolveCollectionCallbacks(collection, path2) {
|
|
6636
7107
|
if (!collection && !path2) return {
|
|
6637
7108
|
collection: void 0,
|
|
@@ -6660,6 +7131,7 @@
|
|
|
6660
7131
|
collection,
|
|
6661
7132
|
filter,
|
|
6662
7133
|
limit,
|
|
7134
|
+
offset,
|
|
6663
7135
|
startAfter,
|
|
6664
7136
|
orderBy,
|
|
6665
7137
|
searchString,
|
|
@@ -6670,6 +7142,7 @@
|
|
|
6670
7142
|
orderBy,
|
|
6671
7143
|
order,
|
|
6672
7144
|
limit,
|
|
7145
|
+
offset,
|
|
6673
7146
|
startAfter,
|
|
6674
7147
|
databaseId: collection?.databaseId,
|
|
6675
7148
|
searchString
|
|
@@ -6713,6 +7186,7 @@
|
|
|
6713
7186
|
collection,
|
|
6714
7187
|
filter,
|
|
6715
7188
|
limit,
|
|
7189
|
+
offset,
|
|
6716
7190
|
startAfter,
|
|
6717
7191
|
orderBy,
|
|
6718
7192
|
searchString,
|
|
@@ -6733,6 +7207,7 @@
|
|
|
6733
7207
|
orderBy,
|
|
6734
7208
|
order,
|
|
6735
7209
|
limit,
|
|
7210
|
+
offset,
|
|
6736
7211
|
startAfter,
|
|
6737
7212
|
databaseId: collection?.databaseId,
|
|
6738
7213
|
searchString
|
|
@@ -6744,6 +7219,7 @@
|
|
|
6744
7219
|
collection,
|
|
6745
7220
|
filter,
|
|
6746
7221
|
limit,
|
|
7222
|
+
offset,
|
|
6747
7223
|
startAfter,
|
|
6748
7224
|
orderBy,
|
|
6749
7225
|
searchString,
|
|
@@ -6904,7 +7380,7 @@
|
|
|
6904
7380
|
collection: resolvedCollection,
|
|
6905
7381
|
path: path2,
|
|
6906
7382
|
entityId: savedEntity.id,
|
|
6907
|
-
values:
|
|
7383
|
+
values: savedEntity.values,
|
|
6908
7384
|
previousValues: previousValuesForHistory,
|
|
6909
7385
|
status,
|
|
6910
7386
|
context: contextForCallback
|
|
@@ -6915,7 +7391,7 @@
|
|
|
6915
7391
|
collection: resolvedCollection,
|
|
6916
7392
|
path: path2,
|
|
6917
7393
|
entityId: savedEntity.id,
|
|
6918
|
-
values:
|
|
7394
|
+
values: savedEntity.values,
|
|
6919
7395
|
previousValues: previousValuesForHistory,
|
|
6920
7396
|
status,
|
|
6921
7397
|
context: contextForCallback
|
|
@@ -7052,10 +7528,12 @@
|
|
|
7052
7528
|
async countEntities({
|
|
7053
7529
|
path: path2,
|
|
7054
7530
|
collection,
|
|
7055
|
-
filter
|
|
7531
|
+
filter,
|
|
7532
|
+
searchString
|
|
7056
7533
|
}) {
|
|
7057
7534
|
return this.entityService.countEntities(path2, {
|
|
7058
|
-
filter
|
|
7535
|
+
filter,
|
|
7536
|
+
searchString
|
|
7059
7537
|
});
|
|
7060
7538
|
}
|
|
7061
7539
|
getTargetDb(databaseName) {
|
|
@@ -7111,7 +7589,7 @@
|
|
|
7111
7589
|
return databases;
|
|
7112
7590
|
}
|
|
7113
7591
|
async fetchAvailableRoles() {
|
|
7114
|
-
const result = await this.executeSql(
|
|
7592
|
+
const result = await this.executeSql("SELECT rolname FROM pg_roles;");
|
|
7115
7593
|
return result.map((r) => r.rolname);
|
|
7116
7594
|
}
|
|
7117
7595
|
async fetchCurrentDatabase() {
|
|
@@ -7285,12 +7763,12 @@
|
|
|
7285
7763
|
const result = await this.delegate.db.transaction(async (tx) => {
|
|
7286
7764
|
let userId = this.user?.uid;
|
|
7287
7765
|
if (!userId) {
|
|
7288
|
-
console.warn(
|
|
7766
|
+
console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
|
|
7289
7767
|
userId = "anonymous";
|
|
7290
7768
|
}
|
|
7291
|
-
|
|
7769
|
+
const userRoles2 = this.user?.roles ?? [];
|
|
7292
7770
|
if (!this.user?.roles) {
|
|
7293
|
-
console.warn(
|
|
7771
|
+
console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
|
|
7294
7772
|
}
|
|
7295
7773
|
const normalizedRoles = userRoles2.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
|
|
7296
7774
|
const rolesString = normalizedRoles.join(",");
|
|
@@ -7360,29 +7838,6 @@
|
|
|
7360
7838
|
async countEntities(props) {
|
|
7361
7839
|
return this.withTransaction((delegate) => delegate.countEntities(props));
|
|
7362
7840
|
}
|
|
7363
|
-
/**
|
|
7364
|
-
* Intentionally delegates to the base delegate WITHOUT RLS wrapping.
|
|
7365
|
-
* executeSql is an admin-only feature; access control should be enforced
|
|
7366
|
-
* at the API route level, not via database-level RLS.
|
|
7367
|
-
*/
|
|
7368
|
-
async executeSql(sqlText, options) {
|
|
7369
|
-
return this.delegate.executeSql(sqlText, options);
|
|
7370
|
-
}
|
|
7371
|
-
async fetchAvailableDatabases() {
|
|
7372
|
-
return this.delegate.fetchAvailableDatabases();
|
|
7373
|
-
}
|
|
7374
|
-
async fetchAvailableRoles() {
|
|
7375
|
-
return this.delegate.fetchAvailableRoles();
|
|
7376
|
-
}
|
|
7377
|
-
async fetchCurrentDatabase() {
|
|
7378
|
-
return this.delegate.fetchCurrentDatabase();
|
|
7379
|
-
}
|
|
7380
|
-
async fetchUnmappedTables(mappedPaths) {
|
|
7381
|
-
return this.delegate.fetchUnmappedTables(mappedPaths);
|
|
7382
|
-
}
|
|
7383
|
-
async fetchTableMetadata(tableName) {
|
|
7384
|
-
return this.delegate.fetchTableMetadata(tableName);
|
|
7385
|
-
}
|
|
7386
7841
|
}
|
|
7387
7842
|
class DatabasePoolManager {
|
|
7388
7843
|
pools = /* @__PURE__ */ new Map();
|
|
@@ -7418,7 +7873,10 @@
|
|
|
7418
7873
|
connectionString: url2.toString(),
|
|
7419
7874
|
max: 10,
|
|
7420
7875
|
// Default sensible limit, can be tuned later
|
|
7421
|
-
idleTimeoutMillis:
|
|
7876
|
+
idleTimeoutMillis: 1e4,
|
|
7877
|
+
// Reduced from 30000 for aggressive cleanup
|
|
7878
|
+
allowExitOnIdle: true
|
|
7879
|
+
// Prevent idle clients from hanging the Node.js process
|
|
7422
7880
|
});
|
|
7423
7881
|
pool.on("error", (err) => {
|
|
7424
7882
|
console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
|
|
@@ -7469,13 +7927,6 @@
|
|
|
7469
7927
|
photoUrl: pgCore.varchar("photo_url", {
|
|
7470
7928
|
length: 500
|
|
7471
7929
|
}),
|
|
7472
|
-
provider: pgCore.varchar("provider", {
|
|
7473
|
-
length: 50
|
|
7474
|
-
}).notNull().default("email"),
|
|
7475
|
-
// 'email' | 'google'
|
|
7476
|
-
googleId: pgCore.varchar("google_id", {
|
|
7477
|
-
length: 255
|
|
7478
|
-
}).unique(),
|
|
7479
7930
|
emailVerified: pgCore.boolean("email_verified").default(false).notNull(),
|
|
7480
7931
|
emailVerificationToken: pgCore.varchar("email_verification_token", {
|
|
7481
7932
|
length: 255
|
|
@@ -7549,12 +8000,31 @@
|
|
|
7549
8000
|
value: pgCore.jsonb("value").notNull(),
|
|
7550
8001
|
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
7551
8002
|
});
|
|
8003
|
+
const userIdentities = rebaseSchema.table("user_identities", {
|
|
8004
|
+
id: pgCore.uuid("id").defaultRandom().primaryKey(),
|
|
8005
|
+
userId: pgCore.uuid("user_id").notNull().references(() => users.id, {
|
|
8006
|
+
onDelete: "cascade"
|
|
8007
|
+
}),
|
|
8008
|
+
provider: pgCore.varchar("provider", {
|
|
8009
|
+
length: 50
|
|
8010
|
+
}).notNull(),
|
|
8011
|
+
// e.g. 'google', 'linkedin'
|
|
8012
|
+
providerId: pgCore.varchar("provider_id", {
|
|
8013
|
+
length: 255
|
|
8014
|
+
}).notNull(),
|
|
8015
|
+
profileData: pgCore.jsonb("profile_data"),
|
|
8016
|
+
createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
|
|
8017
|
+
updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
|
|
8018
|
+
}, (table) => ({
|
|
8019
|
+
uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
|
|
8020
|
+
}));
|
|
7552
8021
|
const usersRelations = drizzleOrm.relations(users, ({
|
|
7553
8022
|
many
|
|
7554
8023
|
}) => ({
|
|
7555
8024
|
userRoles: many(userRoles),
|
|
7556
8025
|
refreshTokens: many(refreshTokens),
|
|
7557
|
-
passwordResetTokens: many(passwordResetTokens)
|
|
8026
|
+
passwordResetTokens: many(passwordResetTokens),
|
|
8027
|
+
userIdentities: many(userIdentities)
|
|
7558
8028
|
}));
|
|
7559
8029
|
const rolesRelations = drizzleOrm.relations(roles, ({
|
|
7560
8030
|
many
|
|
@@ -7589,13 +8059,24 @@
|
|
|
7589
8059
|
references: [users.id]
|
|
7590
8060
|
})
|
|
7591
8061
|
}));
|
|
8062
|
+
const userIdentitiesRelations = drizzleOrm.relations(userIdentities, ({
|
|
8063
|
+
one
|
|
8064
|
+
}) => ({
|
|
8065
|
+
user: one(users, {
|
|
8066
|
+
fields: [userIdentities.userId],
|
|
8067
|
+
references: [users.id]
|
|
8068
|
+
})
|
|
8069
|
+
}));
|
|
7592
8070
|
const getPrimaryKeyProp = (collection) => {
|
|
7593
8071
|
if (collection.properties) {
|
|
7594
8072
|
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
|
|
7595
8073
|
if (idPropEntry) {
|
|
8074
|
+
const prop = idPropEntry[1];
|
|
8075
|
+
const isUuid2 = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
|
|
7596
8076
|
return {
|
|
7597
8077
|
name: idPropEntry[0],
|
|
7598
|
-
type:
|
|
8078
|
+
type: prop.type === "number" ? "number" : "string",
|
|
8079
|
+
isUuid: isUuid2
|
|
7599
8080
|
};
|
|
7600
8081
|
}
|
|
7601
8082
|
}
|
|
@@ -7603,12 +8084,15 @@
|
|
|
7603
8084
|
if (idProp?.type === "number") {
|
|
7604
8085
|
return {
|
|
7605
8086
|
name: "id",
|
|
7606
|
-
type: "number"
|
|
8087
|
+
type: "number",
|
|
8088
|
+
isUuid: false
|
|
7607
8089
|
};
|
|
7608
8090
|
}
|
|
8091
|
+
const isUuid = idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid";
|
|
7609
8092
|
return {
|
|
7610
8093
|
name: "id",
|
|
7611
|
-
type: "string"
|
|
8094
|
+
type: "string",
|
|
8095
|
+
isUuid: isUuid ?? false
|
|
7612
8096
|
};
|
|
7613
8097
|
};
|
|
7614
8098
|
const isNumericId = (collection) => {
|
|
@@ -7622,7 +8106,7 @@
|
|
|
7622
8106
|
const hasExplicitId = Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId));
|
|
7623
8107
|
return !hasExplicitId && propName === "id";
|
|
7624
8108
|
};
|
|
7625
|
-
const getDrizzleColumn = (propName, prop, collection) => {
|
|
8109
|
+
const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
7626
8110
|
const colName = toSnakeCase(propName);
|
|
7627
8111
|
let columnDefinition;
|
|
7628
8112
|
switch (prop.type) {
|
|
@@ -7641,20 +8125,20 @@
|
|
|
7641
8125
|
columnDefinition = `varchar("${colName}")`;
|
|
7642
8126
|
}
|
|
7643
8127
|
if (isIdProperty(propName, prop, collection)) {
|
|
7644
|
-
columnDefinition +=
|
|
8128
|
+
columnDefinition += ".primaryKey()";
|
|
7645
8129
|
}
|
|
7646
8130
|
if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
|
|
7647
8131
|
if (stringProp.isId === "uuid") {
|
|
7648
|
-
columnDefinition +=
|
|
8132
|
+
columnDefinition += ".defaultRandom()";
|
|
7649
8133
|
} else if (stringProp.isId === "cuid") {
|
|
7650
|
-
columnDefinition +=
|
|
8134
|
+
columnDefinition += ".default(sql`cuid()`)";
|
|
7651
8135
|
} else if (typeof stringProp.isId === "string") {
|
|
7652
8136
|
const sqlContent = stringProp.isId.startsWith("sql`") && stringProp.isId.endsWith("`") ? stringProp.isId.substring(4, stringProp.isId.length - 1) : stringProp.isId;
|
|
7653
8137
|
columnDefinition += `.default(sql\`${sqlContent}\`)`;
|
|
7654
8138
|
}
|
|
7655
8139
|
}
|
|
7656
8140
|
if (stringProp.validation?.unique) {
|
|
7657
|
-
columnDefinition +=
|
|
8141
|
+
columnDefinition += ".unique()";
|
|
7658
8142
|
}
|
|
7659
8143
|
break;
|
|
7660
8144
|
}
|
|
@@ -7676,10 +8160,10 @@
|
|
|
7676
8160
|
columnDefinition = baseType;
|
|
7677
8161
|
}
|
|
7678
8162
|
if (isId) {
|
|
7679
|
-
columnDefinition +=
|
|
8163
|
+
columnDefinition += ".primaryKey()";
|
|
7680
8164
|
}
|
|
7681
8165
|
if (numProp.validation?.unique) {
|
|
7682
|
-
columnDefinition +=
|
|
8166
|
+
columnDefinition += ".unique()";
|
|
7683
8167
|
}
|
|
7684
8168
|
break;
|
|
7685
8169
|
}
|
|
@@ -7710,7 +8194,7 @@
|
|
|
7710
8194
|
case "relation": {
|
|
7711
8195
|
const refProp = prop;
|
|
7712
8196
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
7713
|
-
const relation = resolvedRelations
|
|
8197
|
+
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
7714
8198
|
if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
|
|
7715
8199
|
return null;
|
|
7716
8200
|
}
|
|
@@ -7729,8 +8213,9 @@
|
|
|
7729
8213
|
}
|
|
7730
8214
|
const fkColumnName = toSnakeCase(relation.localKey);
|
|
7731
8215
|
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
7732
|
-
const
|
|
7733
|
-
const
|
|
8216
|
+
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
8217
|
+
const targetIdField = pkProp.name;
|
|
8218
|
+
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `varchar("${fkColumnName}")`;
|
|
7734
8219
|
const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
|
|
7735
8220
|
const required = prop.validation?.required;
|
|
7736
8221
|
const onDeleteVal = relation.onDelete ?? (required ? "cascade" : "set null");
|
|
@@ -7743,6 +8228,26 @@
|
|
|
7743
8228
|
}
|
|
7744
8229
|
return ` ${relation.localKey}: ${columnDef}`;
|
|
7745
8230
|
}
|
|
8231
|
+
case "reference": {
|
|
8232
|
+
const refProp = prop;
|
|
8233
|
+
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
8234
|
+
if (!targetCollection) {
|
|
8235
|
+
columnDefinition = `varchar("${colName}")`;
|
|
8236
|
+
break;
|
|
8237
|
+
}
|
|
8238
|
+
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
8239
|
+
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
8240
|
+
const targetIdField = pkProp.name;
|
|
8241
|
+
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `varchar("${colName}")`;
|
|
8242
|
+
const required = prop.validation?.required;
|
|
8243
|
+
const onDelete = required ? "cascade" : "set null";
|
|
8244
|
+
const refOptions = `{ onDelete: "${onDelete}" }`;
|
|
8245
|
+
columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${refOptions})`;
|
|
8246
|
+
if (required) {
|
|
8247
|
+
columnDefinition += ".notNull()";
|
|
8248
|
+
}
|
|
8249
|
+
return ` ${propName}: ${columnDefinition}`;
|
|
8250
|
+
}
|
|
7746
8251
|
default:
|
|
7747
8252
|
return null;
|
|
7748
8253
|
}
|
|
@@ -7769,7 +8274,7 @@
|
|
|
7769
8274
|
return resolveRawSql(rule.using);
|
|
7770
8275
|
}
|
|
7771
8276
|
if (rule.access === "public") {
|
|
7772
|
-
return `
|
|
8277
|
+
return "sql`true`";
|
|
7773
8278
|
}
|
|
7774
8279
|
if (rule.ownerField) {
|
|
7775
8280
|
return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
|
|
@@ -7782,16 +8287,31 @@
|
|
|
7782
8287
|
}
|
|
7783
8288
|
return buildUsingClause(rule);
|
|
7784
8289
|
};
|
|
8290
|
+
const getPolicyNameHash = (rule) => {
|
|
8291
|
+
const data = JSON.stringify({
|
|
8292
|
+
a: rule.access,
|
|
8293
|
+
m: rule.mode,
|
|
8294
|
+
op: rule.operation,
|
|
8295
|
+
ops: rule.operations?.slice().sort(),
|
|
8296
|
+
own: rule.ownerField,
|
|
8297
|
+
rol: rule.roles?.slice().sort(),
|
|
8298
|
+
pg: rule.pgRoles?.slice().sort(),
|
|
8299
|
+
u: rule.using,
|
|
8300
|
+
w: rule.withCheck
|
|
8301
|
+
});
|
|
8302
|
+
return crypto.createHash("sha1").update(data).digest("hex").substring(0, 7);
|
|
8303
|
+
};
|
|
7785
8304
|
const generatePolicyCode = (tableName, rule, index) => {
|
|
7786
8305
|
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
8306
|
+
const ruleHash = getPolicyNameHash(rule);
|
|
7787
8307
|
return ops.map((op, opIdx) => {
|
|
7788
|
-
const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}
|
|
8308
|
+
const policyName = rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
7789
8309
|
return generateSinglePolicyCode(tableName, rule, op, policyName);
|
|
7790
8310
|
}).join("");
|
|
7791
8311
|
};
|
|
7792
8312
|
const generateSinglePolicyCode = (tableName, rule, operation, policyName) => {
|
|
7793
8313
|
const mode = rule.mode ?? "permissive";
|
|
7794
|
-
const roles2 = rule.roles;
|
|
8314
|
+
const roles2 = rule.roles ? [...rule.roles].sort() : void 0;
|
|
7795
8315
|
const needsUsing = operation !== "insert";
|
|
7796
8316
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
7797
8317
|
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
@@ -7811,22 +8331,57 @@
|
|
|
7811
8331
|
}
|
|
7812
8332
|
}
|
|
7813
8333
|
if (!usingClause && needsUsing) {
|
|
7814
|
-
usingClause = `
|
|
8334
|
+
usingClause = "sql`false`";
|
|
7815
8335
|
}
|
|
7816
8336
|
if (!withCheckClause && needsWithCheck) {
|
|
7817
|
-
withCheckClause = `
|
|
8337
|
+
withCheckClause = "sql`false`";
|
|
7818
8338
|
}
|
|
7819
8339
|
const parts = [];
|
|
7820
8340
|
parts.push(`as: "${mode}"`);
|
|
7821
8341
|
parts.push(`for: "${operation}"`);
|
|
7822
|
-
const toRoles = rule.pgRoles
|
|
8342
|
+
const toRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
|
|
7823
8343
|
parts.push(`to: [${toRoles.map((r) => `"${r}"`).join(", ")}]`);
|
|
7824
8344
|
if (usingClause) parts.push(`using: ${usingClause}`);
|
|
7825
8345
|
if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
|
|
7826
8346
|
return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),
|
|
7827
8347
|
`;
|
|
7828
8348
|
};
|
|
7829
|
-
const
|
|
8349
|
+
const computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
8350
|
+
const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
|
|
8351
|
+
if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
|
|
8352
|
+
return `${getTableName(sourceCollection)}_${rel.localKey}`;
|
|
8353
|
+
}
|
|
8354
|
+
if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
|
|
8355
|
+
try {
|
|
8356
|
+
const targetCollection = rel.target();
|
|
8357
|
+
return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
|
|
8358
|
+
} catch {
|
|
8359
|
+
return fallback;
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
if (rel.direction === "inverse" && rel.cardinality === "one") {
|
|
8363
|
+
if (rel.foreignKeyOnTarget) {
|
|
8364
|
+
try {
|
|
8365
|
+
const targetCollection = rel.target();
|
|
8366
|
+
return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
|
|
8367
|
+
} catch {
|
|
8368
|
+
return fallback;
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
try {
|
|
8372
|
+
const targetCollection = rel.target();
|
|
8373
|
+
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
8374
|
+
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey && targetRel.target().slug === sourceCollection.slug);
|
|
8375
|
+
if (correspondingRelation && correspondingRelation.localKey) {
|
|
8376
|
+
return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
|
|
8377
|
+
}
|
|
8378
|
+
} catch {
|
|
8379
|
+
}
|
|
8380
|
+
return fallback;
|
|
8381
|
+
}
|
|
8382
|
+
return fallback;
|
|
8383
|
+
};
|
|
8384
|
+
const generateSchema = async (collections, stripPolicies = false) => {
|
|
7830
8385
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
7831
8386
|
const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
|
|
7832
8387
|
collections.some((c) => c.properties && Object.values(c.properties).some((p) => (p.type === "map" || p.type === "array") && p.columnType === "json"));
|
|
@@ -7834,9 +8389,7 @@
|
|
|
7834
8389
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
7835
8390
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';
|
|
7836
8391
|
`;
|
|
7837
|
-
schemaContent +=
|
|
7838
|
-
|
|
7839
|
-
`;
|
|
8392
|
+
schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
|
|
7840
8393
|
const exportedTableVars = [];
|
|
7841
8394
|
const exportedEnumVars = [];
|
|
7842
8395
|
const exportedRelationVars = [];
|
|
@@ -7897,8 +8450,8 @@
|
|
|
7897
8450
|
} = relation.through;
|
|
7898
8451
|
const onDelete = relation.onDelete ?? "cascade";
|
|
7899
8452
|
const refOptions = `{ onDelete: "${onDelete}" }`;
|
|
7900
|
-
const sourceColType = isNumericId(sourceCollection) ? "integer" : "varchar";
|
|
7901
|
-
const targetColType = isNumericId(targetCollection) ? "integer" : "varchar";
|
|
8453
|
+
const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "varchar";
|
|
8454
|
+
const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar";
|
|
7902
8455
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
7903
8456
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
7904
8457
|
schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
|
|
@@ -7907,31 +8460,28 @@
|
|
|
7907
8460
|
`;
|
|
7908
8461
|
schemaContent += ` ${targetColumn}: ${targetColType}("${toSnakeCase(targetColumn)}").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),
|
|
7909
8462
|
`;
|
|
7910
|
-
schemaContent +=
|
|
7911
|
-
`;
|
|
8463
|
+
schemaContent += "}, (table) => ({\n";
|
|
7912
8464
|
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
|
|
7913
8465
|
`;
|
|
7914
|
-
schemaContent +=
|
|
7915
|
-
|
|
7916
|
-
`;
|
|
8466
|
+
schemaContent += "}));\n\n";
|
|
7917
8467
|
} else if (!isJunction) {
|
|
7918
8468
|
schemaContent += `export const ${tableVarName} = pgTable("${tableName}", {
|
|
7919
8469
|
`;
|
|
7920
8470
|
const columns = /* @__PURE__ */ new Set();
|
|
7921
8471
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
7922
|
-
const columnString = getDrizzleColumn(propName, prop, collection);
|
|
8472
|
+
const columnString = getDrizzleColumn(propName, prop, collection, collections);
|
|
7923
8473
|
if (columnString) columns.add(columnString);
|
|
7924
8474
|
});
|
|
7925
8475
|
const hasIdColumn = Array.from(columns).some((col) => col.includes(".primaryKey()"));
|
|
7926
8476
|
if (!hasIdColumn) {
|
|
7927
|
-
columns.add(
|
|
8477
|
+
columns.add(' id: varchar("id").primaryKey()');
|
|
7928
8478
|
}
|
|
7929
8479
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
7930
|
-
const securityRules = collection.securityRules;
|
|
7931
|
-
if (securityRules && securityRules.length > 0) {
|
|
8480
|
+
const securityRules = isPostgresCollection(collection) ? collection.securityRules : void 0;
|
|
8481
|
+
if (!stripPolicies && securityRules && securityRules.length > 0) {
|
|
7932
8482
|
schemaContent += "\n}, (table) => ([\n";
|
|
7933
8483
|
securityRules.forEach((rule, idx) => {
|
|
7934
|
-
schemaContent += generatePolicyCode(tableName, rule
|
|
8484
|
+
schemaContent += generatePolicyCode(tableName, rule);
|
|
7935
8485
|
});
|
|
7936
8486
|
schemaContent += "])).enableRLS();\n\n";
|
|
7937
8487
|
} else {
|
|
@@ -7971,13 +8521,13 @@
|
|
|
7971
8521
|
}
|
|
7972
8522
|
} catch {
|
|
7973
8523
|
}
|
|
7974
|
-
tableRelations.push(` ${relation.through.sourceColumn}: one(${sourceTableVar}, {
|
|
8524
|
+
tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {
|
|
7975
8525
|
fields: [${tableVarName}.${relation.through.sourceColumn}],
|
|
7976
8526
|
references: [${sourceTableVar}.${sourceId}],
|
|
7977
8527
|
relationName: "${owningRelationName}"
|
|
7978
8528
|
})`);
|
|
7979
8529
|
const targetRelName = inverseRelationName ?? owningRelationName;
|
|
7980
|
-
tableRelations.push(` ${relation.through.targetColumn}: one(${targetTableVar}, {
|
|
8530
|
+
tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {
|
|
7981
8531
|
fields: [${tableVarName}.${relation.through.targetColumn}],
|
|
7982
8532
|
references: [${targetTableVar}.${targetId}],
|
|
7983
8533
|
relationName: "${targetRelName}"
|
|
@@ -7985,22 +8535,25 @@
|
|
|
7985
8535
|
}
|
|
7986
8536
|
} else {
|
|
7987
8537
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
8538
|
+
const emittedRelationNames = /* @__PURE__ */ new Set();
|
|
7988
8539
|
for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
|
|
7989
8540
|
try {
|
|
7990
8541
|
const target = rel.target();
|
|
7991
8542
|
const targetTableVar = getTableVarName(getTableName(target));
|
|
7992
|
-
const
|
|
7993
|
-
const
|
|
8543
|
+
const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
|
|
8544
|
+
const deduplicationKey = `${drizzleRelationName}::${rel.direction}`;
|
|
8545
|
+
if (emittedRelationNames.has(deduplicationKey)) continue;
|
|
8546
|
+
emittedRelationNames.add(deduplicationKey);
|
|
7994
8547
|
if (rel.cardinality === "one") {
|
|
7995
8548
|
if (rel.direction === "owning" && rel.localKey) {
|
|
7996
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8549
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
7997
8550
|
fields: [${tableVarName}.${rel.localKey}],
|
|
7998
8551
|
references: [${targetTableVar}.${getPrimaryKeyName(target)}],
|
|
7999
8552
|
relationName: "${drizzleRelationName}"
|
|
8000
8553
|
})`);
|
|
8001
8554
|
} else if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8002
8555
|
const sourceIdField = getPrimaryKeyName(collection);
|
|
8003
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8556
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8004
8557
|
fields: [${tableVarName}.${sourceIdField}],
|
|
8005
8558
|
references: [${targetTableVar}.${rel.foreignKeyOnTarget}],
|
|
8006
8559
|
relationName: "${drizzleRelationName}"
|
|
@@ -8012,7 +8565,7 @@
|
|
|
8012
8565
|
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.target().slug === collection.slug);
|
|
8013
8566
|
if (correspondingRelation && correspondingRelation.localKey) {
|
|
8014
8567
|
const sourceIdField = getPrimaryKeyName(collection);
|
|
8015
|
-
tableRelations.push(` ${relationKey}: one(${targetTableVar}, {
|
|
8568
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {
|
|
8016
8569
|
fields: [${tableVarName}.${sourceIdField}],
|
|
8017
8570
|
references: [${targetTableVar}.${correspondingRelation.localKey}],
|
|
8018
8571
|
relationName: "${drizzleRelationName}"
|
|
@@ -8024,10 +8577,10 @@
|
|
|
8024
8577
|
}
|
|
8025
8578
|
} else if (rel.cardinality === "many") {
|
|
8026
8579
|
if (rel.direction === "inverse" && rel.foreignKeyOnTarget) {
|
|
8027
|
-
tableRelations.push(` ${relationKey}: many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8580
|
+
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8028
8581
|
} else if (rel.through) {
|
|
8029
8582
|
const junctionTableVar = getTableVarName(rel.through.table);
|
|
8030
|
-
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8583
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8031
8584
|
} else if (rel.direction === "inverse" && rel.inverseRelationName) {
|
|
8032
8585
|
try {
|
|
8033
8586
|
const targetCollection = rel.target();
|
|
@@ -8035,7 +8588,7 @@
|
|
|
8035
8588
|
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
8036
8589
|
if (correspondingRelation && correspondingRelation.through) {
|
|
8037
8590
|
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
8038
|
-
tableRelations.push(` ${relationKey}: many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8591
|
+
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: "${drizzleRelationName}" })`);
|
|
8039
8592
|
} else {
|
|
8040
8593
|
console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
8041
8594
|
}
|
|
@@ -8339,6 +8892,14 @@ ${tableRelations.join(",\n")}
|
|
|
8339
8892
|
async handleCollectionSubscription(clientId, request, authContext) {
|
|
8340
8893
|
const subscriptionId = request.subscriptionId;
|
|
8341
8894
|
try {
|
|
8895
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
8896
|
+
if (!collection) {
|
|
8897
|
+
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
8898
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
8899
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
8900
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
8901
|
+
return;
|
|
8902
|
+
}
|
|
8342
8903
|
this._subscriptions.set(subscriptionId, {
|
|
8343
8904
|
clientId,
|
|
8344
8905
|
type: "collection",
|
|
@@ -8356,7 +8917,6 @@ ${tableRelations.join(",\n")}
|
|
|
8356
8917
|
});
|
|
8357
8918
|
let entities;
|
|
8358
8919
|
if (this.driver) {
|
|
8359
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
8360
8920
|
entities = await this.driver.fetchCollection({
|
|
8361
8921
|
path: request.path,
|
|
8362
8922
|
collection,
|
|
@@ -8386,6 +8946,14 @@ ${tableRelations.join(",\n")}
|
|
|
8386
8946
|
async handleEntitySubscription(clientId, request, authContext) {
|
|
8387
8947
|
const subscriptionId = request.subscriptionId;
|
|
8388
8948
|
try {
|
|
8949
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
8950
|
+
if (!collection) {
|
|
8951
|
+
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
8952
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
8953
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
8954
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
8955
|
+
return;
|
|
8956
|
+
}
|
|
8389
8957
|
this._subscriptions.set(subscriptionId, {
|
|
8390
8958
|
clientId,
|
|
8391
8959
|
type: "entity",
|
|
@@ -8395,7 +8963,6 @@ ${tableRelations.join(",\n")}
|
|
|
8395
8963
|
});
|
|
8396
8964
|
let entity;
|
|
8397
8965
|
if (this.driver) {
|
|
8398
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
8399
8966
|
entity = await this.driver.fetchEntity({
|
|
8400
8967
|
path: request.path,
|
|
8401
8968
|
entityId: request.entityId,
|
|
@@ -8468,13 +9035,13 @@ ${tableRelations.join(",\n")}
|
|
|
8468
9035
|
for (const [subscriptionId, subscription] of webSocketSubscriptions) {
|
|
8469
9036
|
try {
|
|
8470
9037
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
8471
|
-
if (entity && entity.values?._rebase_invalidated) {
|
|
9038
|
+
if (entity && entity.values && entity.values?._rebase_invalidated) {
|
|
8472
9039
|
this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
|
|
8473
9040
|
} else {
|
|
8474
9041
|
this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
|
|
8475
9042
|
}
|
|
8476
9043
|
} else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
8477
|
-
if (!entity || !entity.values?._rebase_invalidated) {
|
|
9044
|
+
if (!entity || !(entity.values && entity.values?._rebase_invalidated)) {
|
|
8478
9045
|
this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
|
|
8479
9046
|
}
|
|
8480
9047
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
@@ -8489,7 +9056,7 @@ ${tableRelations.join(",\n")}
|
|
|
8489
9056
|
const callback = this.subscriptionCallbacks.get(subscriptionId);
|
|
8490
9057
|
if (!callback) continue;
|
|
8491
9058
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
8492
|
-
if (entity && entity.values?._rebase_invalidated) {
|
|
9059
|
+
if (entity && entity.values && entity.values?._rebase_invalidated) {
|
|
8493
9060
|
this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
|
|
8494
9061
|
} else {
|
|
8495
9062
|
callback(entity);
|
|
@@ -8555,6 +9122,7 @@ ${tableRelations.join(",\n")}
|
|
|
8555
9122
|
orderBy: collectionRequest.orderBy,
|
|
8556
9123
|
order: collectionRequest.order,
|
|
8557
9124
|
limit: collectionRequest.limit,
|
|
9125
|
+
offset: collectionRequest.offset,
|
|
8558
9126
|
startAfter: collectionRequest.startAfter,
|
|
8559
9127
|
searchString: collectionRequest.searchString
|
|
8560
9128
|
});
|
|
@@ -8582,6 +9150,7 @@ ${tableRelations.join(",\n")}
|
|
|
8582
9150
|
orderBy: collectionRequest.orderBy,
|
|
8583
9151
|
order: collectionRequest.order,
|
|
8584
9152
|
limit: collectionRequest.limit,
|
|
9153
|
+
offset: collectionRequest.offset,
|
|
8585
9154
|
startAfter: collectionRequest.startAfter,
|
|
8586
9155
|
databaseId: collectionRequest.databaseId
|
|
8587
9156
|
});
|
|
@@ -8642,6 +9211,7 @@ ${tableRelations.join(",\n")}
|
|
|
8642
9211
|
orderBy: collectionRequest.orderBy,
|
|
8643
9212
|
order: collectionRequest.order,
|
|
8644
9213
|
limit: collectionRequest.limit,
|
|
9214
|
+
offset: collectionRequest.offset,
|
|
8645
9215
|
startAfter: collectionRequest.startAfter,
|
|
8646
9216
|
databaseId: collectionRequest.databaseId
|
|
8647
9217
|
});
|
|
@@ -8947,7 +9517,7 @@ ${tableRelations.join(",\n")}
|
|
|
8947
9517
|
}
|
|
8948
9518
|
const PostgresRealtimeProvider = RealtimeService;
|
|
8949
9519
|
const clientSessions = /* @__PURE__ */ new Map();
|
|
8950
|
-
const WS_RATE_LIMIT =
|
|
9520
|
+
const WS_RATE_LIMIT = 2e3;
|
|
8951
9521
|
const WS_RATE_WINDOW_MS = 6e4;
|
|
8952
9522
|
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"]);
|
|
8953
9523
|
function isAdminSession(session) {
|
|
@@ -8967,6 +9537,12 @@ ${tableRelations.join(",\n")}
|
|
|
8967
9537
|
const wss = new ws.WebSocketServer({
|
|
8968
9538
|
server
|
|
8969
9539
|
});
|
|
9540
|
+
wss.on("error", (err) => {
|
|
9541
|
+
if (err.code === "EADDRINUSE") {
|
|
9542
|
+
return;
|
|
9543
|
+
}
|
|
9544
|
+
console.error("❌ [WebSocket Server] Error:", err);
|
|
9545
|
+
});
|
|
8970
9546
|
const requireAuth = authConfig?.requireAuth !== false && authConfig?.jwtSecret;
|
|
8971
9547
|
wss.on("connection", (ws2) => {
|
|
8972
9548
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -9207,7 +9783,12 @@ ${tableRelations.join(",\n")}
|
|
|
9207
9783
|
options
|
|
9208
9784
|
} = payload;
|
|
9209
9785
|
const delegate = await getScopedDelegate();
|
|
9210
|
-
const
|
|
9786
|
+
const admin = delegate.admin;
|
|
9787
|
+
if (!isSQLAdmin(admin)) {
|
|
9788
|
+
sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
|
|
9789
|
+
break;
|
|
9790
|
+
}
|
|
9791
|
+
const result = await admin.executeSql(sql, options);
|
|
9211
9792
|
if (process.env.NODE_ENV !== "production") {
|
|
9212
9793
|
wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
|
|
9213
9794
|
}
|
|
@@ -9225,9 +9806,10 @@ ${tableRelations.join(",\n")}
|
|
|
9225
9806
|
{
|
|
9226
9807
|
wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
|
|
9227
9808
|
const delegate = await getScopedDelegate();
|
|
9809
|
+
const admin = delegate.admin;
|
|
9228
9810
|
let databases = [];
|
|
9229
|
-
if (
|
|
9230
|
-
databases = await
|
|
9811
|
+
if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {
|
|
9812
|
+
databases = await admin.fetchAvailableDatabases();
|
|
9231
9813
|
}
|
|
9232
9814
|
wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
|
|
9233
9815
|
const response = {
|
|
@@ -9244,9 +9826,10 @@ ${tableRelations.join(",\n")}
|
|
|
9244
9826
|
{
|
|
9245
9827
|
wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
|
|
9246
9828
|
const delegate = await getScopedDelegate();
|
|
9829
|
+
const admin = delegate.admin;
|
|
9247
9830
|
let roles2 = [];
|
|
9248
|
-
if (
|
|
9249
|
-
roles2 = await
|
|
9831
|
+
if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
|
|
9832
|
+
roles2 = await admin.fetchAvailableRoles();
|
|
9250
9833
|
}
|
|
9251
9834
|
wsDebug(`👤 [WebSocket Server] Fetched ${roles2.length} roles.`);
|
|
9252
9835
|
const response = {
|
|
@@ -9263,9 +9846,10 @@ ${tableRelations.join(",\n")}
|
|
|
9263
9846
|
{
|
|
9264
9847
|
wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
|
|
9265
9848
|
const delegate = await getScopedDelegate();
|
|
9849
|
+
const admin = delegate.admin;
|
|
9266
9850
|
let database = void 0;
|
|
9267
|
-
if (
|
|
9268
|
-
database = await
|
|
9851
|
+
if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {
|
|
9852
|
+
database = await admin.fetchCurrentDatabase();
|
|
9269
9853
|
}
|
|
9270
9854
|
const response = {
|
|
9271
9855
|
type: "FETCH_CURRENT_DATABASE_SUCCESS",
|
|
@@ -9281,9 +9865,10 @@ ${tableRelations.join(",\n")}
|
|
|
9281
9865
|
{
|
|
9282
9866
|
wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
|
|
9283
9867
|
const delegate = await getScopedDelegate();
|
|
9868
|
+
const admin = delegate.admin;
|
|
9284
9869
|
let tables = [];
|
|
9285
|
-
if (
|
|
9286
|
-
tables = await
|
|
9870
|
+
if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {
|
|
9871
|
+
tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
|
|
9287
9872
|
}
|
|
9288
9873
|
wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
|
|
9289
9874
|
const response = {
|
|
@@ -9303,9 +9888,10 @@ ${tableRelations.join(",\n")}
|
|
|
9303
9888
|
tableName
|
|
9304
9889
|
} = payload;
|
|
9305
9890
|
const delegate = await getScopedDelegate();
|
|
9891
|
+
const admin = delegate.admin;
|
|
9306
9892
|
let metadata;
|
|
9307
|
-
if (
|
|
9308
|
-
metadata = await
|
|
9893
|
+
if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {
|
|
9894
|
+
metadata = await admin.fetchTableMetadata(tableName);
|
|
9309
9895
|
}
|
|
9310
9896
|
wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
|
|
9311
9897
|
const response = {
|
|
@@ -9490,7 +10076,7 @@ ${tableRelations.join(",\n")}
|
|
|
9490
10076
|
*/
|
|
9491
10077
|
getRelationKeysForCollection(collectionPath) {
|
|
9492
10078
|
const collection = this.getCollectionByPath(collectionPath);
|
|
9493
|
-
if (!collection
|
|
10079
|
+
if (!collection || !getDataSourceCapabilities(collection.driver).supportsRelations || !collection.relations) return [];
|
|
9494
10080
|
return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
|
|
9495
10081
|
}
|
|
9496
10082
|
}
|
|
@@ -9547,8 +10133,6 @@ ${tableRelations.join(",\n")}
|
|
|
9547
10133
|
password_hash TEXT,
|
|
9548
10134
|
display_name TEXT,
|
|
9549
10135
|
photo_url TEXT,
|
|
9550
|
-
provider TEXT DEFAULT 'email',
|
|
9551
|
-
google_id TEXT UNIQUE,
|
|
9552
10136
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
9553
10137
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9554
10138
|
)
|
|
@@ -9558,8 +10142,20 @@ ${tableRelations.join(",\n")}
|
|
|
9558
10142
|
ON rebase.users(email)
|
|
9559
10143
|
`);
|
|
9560
10144
|
await db.execute(drizzleOrm.sql`
|
|
9561
|
-
CREATE
|
|
9562
|
-
|
|
10145
|
+
CREATE TABLE IF NOT EXISTS rebase.user_identities (
|
|
10146
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10147
|
+
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
10148
|
+
provider TEXT NOT NULL,
|
|
10149
|
+
provider_id TEXT NOT NULL,
|
|
10150
|
+
profile_data JSONB,
|
|
10151
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10152
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10153
|
+
UNIQUE(provider, provider_id)
|
|
10154
|
+
)
|
|
10155
|
+
`);
|
|
10156
|
+
await db.execute(drizzleOrm.sql`
|
|
10157
|
+
CREATE INDEX IF NOT EXISTS idx_user_identities_user
|
|
10158
|
+
ON rebase.user_identities(user_id)
|
|
9563
10159
|
`);
|
|
9564
10160
|
await db.execute(drizzleOrm.sql`
|
|
9565
10161
|
CREATE TABLE IF NOT EXISTS rebase.roles (
|
|
@@ -9572,10 +10168,6 @@ ${tableRelations.join(",\n")}
|
|
|
9572
10168
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9573
10169
|
)
|
|
9574
10170
|
`);
|
|
9575
|
-
await db.execute(drizzleOrm.sql`
|
|
9576
|
-
ALTER TABLE rebase.roles
|
|
9577
|
-
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
9578
|
-
`);
|
|
9579
10171
|
await db.execute(drizzleOrm.sql`
|
|
9580
10172
|
CREATE TABLE IF NOT EXISTS rebase.user_roles (
|
|
9581
10173
|
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
@@ -9607,51 +10199,6 @@ ${tableRelations.join(",\n")}
|
|
|
9607
10199
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
|
|
9608
10200
|
ON rebase.refresh_tokens(user_id)
|
|
9609
10201
|
`);
|
|
9610
|
-
await db.execute(drizzleOrm.sql`
|
|
9611
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9612
|
-
ADD COLUMN IF NOT EXISTS user_agent TEXT
|
|
9613
|
-
`);
|
|
9614
|
-
await db.execute(drizzleOrm.sql`
|
|
9615
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9616
|
-
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
9617
|
-
`);
|
|
9618
|
-
const constraintCheck = await db.execute(drizzleOrm.sql`
|
|
9619
|
-
SELECT 1 FROM information_schema.table_constraints
|
|
9620
|
-
WHERE constraint_name = 'unique_device_session'
|
|
9621
|
-
AND table_schema = 'rebase'
|
|
9622
|
-
AND table_name = 'refresh_tokens'
|
|
9623
|
-
`);
|
|
9624
|
-
if (constraintCheck.rows.length === 0) {
|
|
9625
|
-
try {
|
|
9626
|
-
await db.execute(drizzleOrm.sql`
|
|
9627
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9628
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9629
|
-
`);
|
|
9630
|
-
console.log("✅ Added unique_device_session constraint");
|
|
9631
|
-
} catch (e) {
|
|
9632
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
9633
|
-
if (errorMessage.includes("could not create unique index")) {
|
|
9634
|
-
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
9635
|
-
await db.execute(drizzleOrm.sql`
|
|
9636
|
-
DELETE FROM rebase.refresh_tokens a
|
|
9637
|
-
USING rebase.refresh_tokens b
|
|
9638
|
-
WHERE a.user_id = b.user_id
|
|
9639
|
-
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
9640
|
-
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
9641
|
-
AND a.created_at < b.created_at
|
|
9642
|
-
`);
|
|
9643
|
-
await db.execute(drizzleOrm.sql`
|
|
9644
|
-
ALTER TABLE rebase.refresh_tokens
|
|
9645
|
-
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
9646
|
-
`).catch((retryErr) => {
|
|
9647
|
-
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
9648
|
-
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
9649
|
-
});
|
|
9650
|
-
} else {
|
|
9651
|
-
console.error("Constraint migration issue:", errorMessage);
|
|
9652
|
-
}
|
|
9653
|
-
}
|
|
9654
|
-
}
|
|
9655
10202
|
await db.execute(drizzleOrm.sql`
|
|
9656
10203
|
CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
|
|
9657
10204
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
@@ -9677,18 +10224,7 @@ ${tableRelations.join(",\n")}
|
|
|
9677
10224
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9678
10225
|
)
|
|
9679
10226
|
`);
|
|
9680
|
-
await db
|
|
9681
|
-
ALTER TABLE rebase.users
|
|
9682
|
-
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE
|
|
9683
|
-
`);
|
|
9684
|
-
await db.execute(drizzleOrm.sql`
|
|
9685
|
-
ALTER TABLE rebase.users
|
|
9686
|
-
ADD COLUMN IF NOT EXISTS email_verification_token TEXT
|
|
9687
|
-
`);
|
|
9688
|
-
await db.execute(drizzleOrm.sql`
|
|
9689
|
-
ALTER TABLE rebase.users
|
|
9690
|
-
ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
|
|
9691
|
-
`);
|
|
10227
|
+
await applyInternalMigrations(db);
|
|
9692
10228
|
await db.execute(drizzleOrm.sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
9693
10229
|
await db.transaction(async (tx) => {
|
|
9694
10230
|
await tx.execute(drizzleOrm.sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
@@ -9741,6 +10277,99 @@ ${tableRelations.join(",\n")}
|
|
|
9741
10277
|
}
|
|
9742
10278
|
console.log("✅ Default roles created: admin, editor, viewer");
|
|
9743
10279
|
}
|
|
10280
|
+
async function applyInternalMigrations(db) {
|
|
10281
|
+
try {
|
|
10282
|
+
await db.execute(drizzleOrm.sql`
|
|
10283
|
+
ALTER TABLE rebase.users
|
|
10284
|
+
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
|
|
10285
|
+
ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
|
|
10286
|
+
ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
|
|
10287
|
+
`);
|
|
10288
|
+
const columnsCheck = await db.execute(drizzleOrm.sql`
|
|
10289
|
+
SELECT column_name
|
|
10290
|
+
FROM information_schema.columns
|
|
10291
|
+
WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
|
|
10292
|
+
`);
|
|
10293
|
+
const existingColumns = columnsCheck.rows.map((r) => r.column_name);
|
|
10294
|
+
if (existingColumns.includes("google_id")) {
|
|
10295
|
+
await db.execute(drizzleOrm.sql`
|
|
10296
|
+
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
10297
|
+
SELECT id, 'google', google_id
|
|
10298
|
+
FROM rebase.users
|
|
10299
|
+
WHERE google_id IS NOT NULL
|
|
10300
|
+
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
10301
|
+
`);
|
|
10302
|
+
}
|
|
10303
|
+
if (existingColumns.includes("linkedin_id")) {
|
|
10304
|
+
await db.execute(drizzleOrm.sql`
|
|
10305
|
+
INSERT INTO rebase.user_identities (user_id, provider, provider_id)
|
|
10306
|
+
SELECT id, 'linkedin', linkedin_id
|
|
10307
|
+
FROM rebase.users
|
|
10308
|
+
WHERE linkedin_id IS NOT NULL
|
|
10309
|
+
ON CONFLICT (provider, provider_id) DO NOTHING
|
|
10310
|
+
`);
|
|
10311
|
+
}
|
|
10312
|
+
if (existingColumns.length > 0) {
|
|
10313
|
+
await db.execute(drizzleOrm.sql`
|
|
10314
|
+
ALTER TABLE rebase.users
|
|
10315
|
+
DROP COLUMN IF EXISTS provider,
|
|
10316
|
+
DROP COLUMN IF EXISTS google_id,
|
|
10317
|
+
DROP COLUMN IF EXISTS linkedin_id
|
|
10318
|
+
`);
|
|
10319
|
+
await db.execute(drizzleOrm.sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
|
|
10320
|
+
await db.execute(drizzleOrm.sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
|
|
10321
|
+
console.log("✅ Migrated to user_identities and dropped legacy columns.");
|
|
10322
|
+
}
|
|
10323
|
+
await db.execute(drizzleOrm.sql`
|
|
10324
|
+
ALTER TABLE rebase.roles
|
|
10325
|
+
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
10326
|
+
`);
|
|
10327
|
+
await db.execute(drizzleOrm.sql`
|
|
10328
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10329
|
+
ADD COLUMN IF NOT EXISTS user_agent TEXT,
|
|
10330
|
+
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
10331
|
+
`);
|
|
10332
|
+
const constraintCheck = await db.execute(drizzleOrm.sql`
|
|
10333
|
+
SELECT 1 FROM information_schema.table_constraints
|
|
10334
|
+
WHERE constraint_name = 'unique_device_session'
|
|
10335
|
+
AND table_schema = 'rebase'
|
|
10336
|
+
AND table_name = 'refresh_tokens'
|
|
10337
|
+
`);
|
|
10338
|
+
if (constraintCheck.rows.length === 0) {
|
|
10339
|
+
try {
|
|
10340
|
+
await db.execute(drizzleOrm.sql`
|
|
10341
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10342
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
10343
|
+
`);
|
|
10344
|
+
console.log("✅ Added unique_device_session constraint");
|
|
10345
|
+
} catch (e) {
|
|
10346
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
10347
|
+
if (errorMessage.includes("could not create unique index")) {
|
|
10348
|
+
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
10349
|
+
await db.execute(drizzleOrm.sql`
|
|
10350
|
+
DELETE FROM rebase.refresh_tokens a
|
|
10351
|
+
USING rebase.refresh_tokens b
|
|
10352
|
+
WHERE a.user_id = b.user_id
|
|
10353
|
+
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
10354
|
+
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
10355
|
+
AND a.created_at < b.created_at
|
|
10356
|
+
`);
|
|
10357
|
+
await db.execute(drizzleOrm.sql`
|
|
10358
|
+
ALTER TABLE rebase.refresh_tokens
|
|
10359
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
10360
|
+
`).catch((retryErr) => {
|
|
10361
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
10362
|
+
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
10363
|
+
});
|
|
10364
|
+
} else {
|
|
10365
|
+
console.error("Constraint migration issue:", errorMessage);
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
10369
|
+
} catch (error) {
|
|
10370
|
+
console.error("❌ Failed to run internal migrations:", error);
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
9744
10373
|
class UserService {
|
|
9745
10374
|
constructor(db) {
|
|
9746
10375
|
this.db = db;
|
|
@@ -9757,9 +10386,54 @@ ${tableRelations.join(",\n")}
|
|
|
9757
10386
|
const [user] = await this.db.select().from(users).where(drizzleOrm.eq(users.email, email.toLowerCase()));
|
|
9758
10387
|
return user || null;
|
|
9759
10388
|
}
|
|
9760
|
-
async
|
|
9761
|
-
const
|
|
9762
|
-
|
|
10389
|
+
async getUserByIdentity(provider, providerId) {
|
|
10390
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
10391
|
+
SELECT u.*
|
|
10392
|
+
FROM rebase.users u
|
|
10393
|
+
INNER JOIN rebase.user_identities ui ON u.id = ui.user_id
|
|
10394
|
+
WHERE ui.provider = ${provider} AND ui.provider_id = ${providerId}
|
|
10395
|
+
LIMIT 1
|
|
10396
|
+
`);
|
|
10397
|
+
if (result.rows.length === 0) return null;
|
|
10398
|
+
const row = result.rows[0];
|
|
10399
|
+
return {
|
|
10400
|
+
id: row.id,
|
|
10401
|
+
email: row.email,
|
|
10402
|
+
passwordHash: row.password_hash ?? null,
|
|
10403
|
+
displayName: row.display_name ?? null,
|
|
10404
|
+
photoUrl: row.photo_url ?? null,
|
|
10405
|
+
emailVerified: row.email_verified ?? false,
|
|
10406
|
+
emailVerificationToken: row.email_verification_token ?? null,
|
|
10407
|
+
emailVerificationSentAt: row.email_verification_sent_at ?? null,
|
|
10408
|
+
createdAt: row.created_at,
|
|
10409
|
+
updatedAt: row.updated_at
|
|
10410
|
+
};
|
|
10411
|
+
}
|
|
10412
|
+
async getUserIdentities(userId) {
|
|
10413
|
+
const result = await this.db.execute(drizzleOrm.sql`
|
|
10414
|
+
SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
|
|
10415
|
+
FROM rebase.user_identities
|
|
10416
|
+
WHERE user_id = ${userId}
|
|
10417
|
+
`);
|
|
10418
|
+
return result.rows.map((row) => ({
|
|
10419
|
+
id: row.id,
|
|
10420
|
+
userId: row.user_id,
|
|
10421
|
+
provider: row.provider,
|
|
10422
|
+
providerId: row.provider_id,
|
|
10423
|
+
profileData: row.profile_data ?? null,
|
|
10424
|
+
createdAt: row.created_at,
|
|
10425
|
+
updatedAt: row.updated_at
|
|
10426
|
+
}));
|
|
10427
|
+
}
|
|
10428
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
10429
|
+
await this.db.insert(userIdentities).values({
|
|
10430
|
+
userId,
|
|
10431
|
+
provider,
|
|
10432
|
+
providerId,
|
|
10433
|
+
profileData: profileData || null
|
|
10434
|
+
}).onConflictDoNothing({
|
|
10435
|
+
target: [userIdentities.provider, userIdentities.providerId]
|
|
10436
|
+
});
|
|
9763
10437
|
}
|
|
9764
10438
|
async updateUser(id, data) {
|
|
9765
10439
|
const [user] = await this.db.update(users).set({
|
|
@@ -9780,6 +10454,7 @@ ${tableRelations.join(",\n")}
|
|
|
9780
10454
|
const search = options?.search?.trim() || "";
|
|
9781
10455
|
const orderBy = options?.orderBy || "createdAt";
|
|
9782
10456
|
const orderDir = options?.orderDir || "desc";
|
|
10457
|
+
const roleId = options?.roleId;
|
|
9783
10458
|
const columnMap = {
|
|
9784
10459
|
email: "email",
|
|
9785
10460
|
displayName: "display_name",
|
|
@@ -9789,42 +10464,34 @@ ${tableRelations.join(",\n")}
|
|
|
9789
10464
|
};
|
|
9790
10465
|
const orderColumn = columnMap[orderBy] || "created_at";
|
|
9791
10466
|
const direction = orderDir === "asc" ? drizzleOrm.sql`ASC` : drizzleOrm.sql`DESC`;
|
|
9792
|
-
|
|
9793
|
-
|
|
10467
|
+
const conditions = [];
|
|
10468
|
+
if (roleId) {
|
|
10469
|
+
conditions.push(drizzleOrm.sql`EXISTS (SELECT 1 FROM rebase.user_roles ur WHERE ur.user_id = users.id AND ur.role_id = ${roleId})`);
|
|
10470
|
+
}
|
|
9794
10471
|
if (search) {
|
|
9795
10472
|
const pattern = `%${search}%`;
|
|
9796
|
-
|
|
9797
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
9798
|
-
WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
|
|
9799
|
-
`);
|
|
9800
|
-
total = countResult.rows[0].total;
|
|
9801
|
-
const dataResult = await this.db.execute(drizzleOrm.sql`
|
|
9802
|
-
SELECT * FROM rebase.users
|
|
9803
|
-
WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
|
|
9804
|
-
ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}
|
|
9805
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
9806
|
-
`);
|
|
9807
|
-
rows = dataResult.rows;
|
|
9808
|
-
} else {
|
|
9809
|
-
const countResult = await this.db.execute(drizzleOrm.sql`
|
|
9810
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
9811
|
-
`);
|
|
9812
|
-
total = countResult.rows[0].total;
|
|
9813
|
-
const dataResult = await this.db.execute(drizzleOrm.sql`
|
|
9814
|
-
SELECT * FROM rebase.users
|
|
9815
|
-
ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}
|
|
9816
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
9817
|
-
`);
|
|
9818
|
-
rows = dataResult.rows;
|
|
10473
|
+
conditions.push(drizzleOrm.sql`(email ILIKE ${pattern} OR display_name ILIKE ${pattern})`);
|
|
9819
10474
|
}
|
|
10475
|
+
const whereClause = conditions.length > 0 ? drizzleOrm.sql`WHERE ${drizzleOrm.sql.join(conditions, drizzleOrm.sql` AND `)}` : drizzleOrm.sql``;
|
|
10476
|
+
const orderByClause = roleId ? drizzleOrm.sql`ORDER BY ${drizzleOrm.sql.raw(orderColumn)} ${direction}` : drizzleOrm.sql`ORDER BY (SELECT count(*) FROM rebase.user_roles ur WHERE ur.user_id = users.id) DESC, ${drizzleOrm.sql.raw(orderColumn)} ${direction}`;
|
|
10477
|
+
const countResult = await this.db.execute(drizzleOrm.sql`
|
|
10478
|
+
SELECT count(*)::int as total FROM rebase.users
|
|
10479
|
+
${whereClause}
|
|
10480
|
+
`);
|
|
10481
|
+
const total = countResult.rows[0].total;
|
|
10482
|
+
const dataResult = await this.db.execute(drizzleOrm.sql`
|
|
10483
|
+
SELECT * FROM rebase.users
|
|
10484
|
+
${whereClause}
|
|
10485
|
+
${orderByClause}
|
|
10486
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
10487
|
+
`);
|
|
10488
|
+
const rows = dataResult.rows;
|
|
9820
10489
|
const mappedUsers = rows.map((row) => ({
|
|
9821
10490
|
id: row.id,
|
|
9822
10491
|
email: row.email,
|
|
9823
10492
|
passwordHash: row.password_hash ?? row.passwordHash ?? null,
|
|
9824
10493
|
displayName: row.display_name ?? row.displayName ?? null,
|
|
9825
10494
|
photoUrl: row.photo_url ?? row.photoUrl ?? null,
|
|
9826
|
-
provider: row.provider,
|
|
9827
|
-
googleId: row.google_id ?? row.googleId ?? null,
|
|
9828
10495
|
emailVerified: row.email_verified ?? row.emailVerified ?? false,
|
|
9829
10496
|
emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
|
|
9830
10497
|
emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
|
|
@@ -10198,8 +10865,14 @@ ${tableRelations.join(",\n")}
|
|
|
10198
10865
|
async getUserByEmail(email) {
|
|
10199
10866
|
return this.userService.getUserByEmail(email);
|
|
10200
10867
|
}
|
|
10201
|
-
async
|
|
10202
|
-
return this.userService.
|
|
10868
|
+
async getUserByIdentity(provider, providerId) {
|
|
10869
|
+
return this.userService.getUserByIdentity(provider, providerId);
|
|
10870
|
+
}
|
|
10871
|
+
async getUserIdentities(userId) {
|
|
10872
|
+
return this.userService.getUserIdentities(userId);
|
|
10873
|
+
}
|
|
10874
|
+
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
10875
|
+
return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
|
|
10203
10876
|
}
|
|
10204
10877
|
async updateUser(id, data) {
|
|
10205
10878
|
return this.userService.updateUser(id, data);
|
|
@@ -10327,16 +11000,6 @@ ${tableRelations.join(",\n")}
|
|
|
10327
11000
|
updatedBy
|
|
10328
11001
|
} = params;
|
|
10329
11002
|
const changedFields = previousValues && values ? findChangedFields(previousValues, values) : null;
|
|
10330
|
-
try {
|
|
10331
|
-
require("fs").appendFileSync("/Users/francesco/rebase/packages/backend/history_diff.log", `[recordHistory: ${tableName}/${entityId} - ${action}]
|
|
10332
|
-
CHANGED FIELDS: ${JSON.stringify(changedFields)}
|
|
10333
|
-
PREVIOUS: ${JSON.stringify(previousValues, null, 2)}
|
|
10334
|
-
NEW: ${JSON.stringify(values, null, 2)}
|
|
10335
|
-
|
|
10336
|
-
`);
|
|
10337
|
-
} catch (e) {
|
|
10338
|
-
console.error("DEBUG FILE WRITE ERROR:", e);
|
|
10339
|
-
}
|
|
10340
11003
|
if (action === "update" && (!changedFields || changedFields.length === 0)) {
|
|
10341
11004
|
return;
|
|
10342
11005
|
}
|
|
@@ -10498,6 +11161,7 @@ NEW: ${JSON.stringify(values, null, 2)}
|
|
|
10498
11161
|
const registry = new PostgresCollectionRegistry();
|
|
10499
11162
|
if (collections) {
|
|
10500
11163
|
registry.registerMultiple(collections);
|
|
11164
|
+
console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
10501
11165
|
}
|
|
10502
11166
|
if (pgConfig.schema?.tables) {
|
|
10503
11167
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -10564,9 +11228,6 @@ NEW: ${JSON.stringify(values, null, 2)}
|
|
|
10564
11228
|
const internals = driverResult.internals;
|
|
10565
11229
|
const db = internals.db;
|
|
10566
11230
|
await ensureAuthTablesExist(db);
|
|
10567
|
-
if (authConfig.google?.clientId) {
|
|
10568
|
-
serverCore.configureGoogleOAuth(authConfig.google.clientId);
|
|
10569
|
-
}
|
|
10570
11231
|
let emailService;
|
|
10571
11232
|
if (authConfig.email) {
|
|
10572
11233
|
emailService = serverCore.createEmailService(authConfig.email);
|
|
@@ -10634,6 +11295,8 @@ NEW: ${JSON.stringify(values, null, 2)}
|
|
|
10634
11295
|
exports2.refreshTokensRelations = refreshTokensRelations;
|
|
10635
11296
|
exports2.roles = roles;
|
|
10636
11297
|
exports2.rolesRelations = rolesRelations;
|
|
11298
|
+
exports2.userIdentities = userIdentities;
|
|
11299
|
+
exports2.userIdentitiesRelations = userIdentitiesRelations;
|
|
10637
11300
|
exports2.userRoles = userRoles;
|
|
10638
11301
|
exports2.userRolesRelations = userRolesRelations;
|
|
10639
11302
|
exports2.users = users;
|