@rebasepro/server-postgres 0.0.1-canary.fc811d7 → 0.9.1-canary.0fce67c
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/PostgresBackendDriver.d.ts +25 -2
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/auth/services.d.ts +16 -0
- package/dist/collections/buildRegistry.d.ts +27 -0
- package/dist/connection.d.ts +21 -0
- package/dist/data-transformer.d.ts +9 -2
- package/dist/index.es.js +2005 -2583
- package/dist/index.es.js.map +1 -1
- package/dist/module-dir.d.ts +1 -0
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/security/policy-drift.d.ts +16 -5
- package/dist/security/rls-enforcement.d.ts +27 -2
- package/dist/services/FetchService.d.ts +4 -24
- package/dist/services/PersistService.d.ts +27 -1
- package/dist/services/RelationService.d.ts +34 -1
- package/dist/services/collection-helpers.d.ts +79 -14
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +7 -0
- package/dist/services/row-pipeline.d.ts +63 -0
- package/package.json +15 -17
- package/src/PostgresBackendDriver.ts +127 -13
- package/src/PostgresBootstrapper.ts +68 -26
- package/src/auth/ensure-tables.ts +73 -11
- package/src/auth/services.ts +49 -19
- package/src/cli-helpers.ts +2 -20
- package/src/collections/buildRegistry.ts +59 -0
- package/src/connection.ts +61 -1
- package/src/data-transformer.ts +11 -9
- package/src/databasePoolManager.ts +2 -0
- package/src/module-dir.ts +7 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +45 -20
- package/src/schema/generate-drizzle-schema-logic.ts +24 -29
- package/src/schema/generate-postgres-ddl-logic.ts +76 -28
- package/src/schema/introspect-db.ts +19 -2
- package/src/security/anonymous-grants.test.ts +71 -0
- package/src/security/policy-drift.test.ts +48 -14
- package/src/security/policy-drift.ts +72 -10
- package/src/security/rls-enforcement.ts +64 -3
- package/src/services/BranchService.ts +42 -10
- package/src/services/FetchService.ts +65 -270
- package/src/services/PersistService.ts +130 -14
- package/src/services/RelationService.ts +153 -94
- package/src/services/collection-helpers.ts +164 -47
- package/src/services/dataService.ts +3 -2
- package/src/services/index.ts +1 -0
- package/src/services/realtimeService.ts +40 -19
- package/src/services/row-pipeline.ts +239 -0
- package/src/utils/drizzle-conditions.ts +13 -0
- package/src/websocket.ts +4 -1
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/schema/auth-default-policies.d.ts +0 -10
- package/dist/src-Eh-CZosp.js +0 -595
- package/dist/src-Eh-CZosp.js.map +0 -1
- package/src/schema/auth-default-policies.ts +0 -125
|
@@ -11,10 +11,45 @@ import { sql } from "drizzle-orm";
|
|
|
11
11
|
import { BranchInfo } from "@rebasepro/types";
|
|
12
12
|
import { DrizzleClient } from "../interfaces";
|
|
13
13
|
import { DatabasePoolManager } from "../databasePoolManager";
|
|
14
|
+
import { extractPgError, extractCauseMessage } from "../utils/pg-error-utils";
|
|
14
15
|
|
|
15
16
|
/** Internal prefix applied to branch database names to avoid collisions. */
|
|
16
17
|
const BRANCH_DB_PREFIX = "rb_";
|
|
17
18
|
|
|
19
|
+
/** `duplicate_database` — the target database name is already taken. */
|
|
20
|
+
const PG_DUPLICATE_DATABASE = "42P04";
|
|
21
|
+
|
|
22
|
+
/** `object_in_use` — the database still has connections attached. */
|
|
23
|
+
const PG_OBJECT_IN_USE = "55006";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Describe a failed branch DDL statement in terms a user can act on.
|
|
27
|
+
*
|
|
28
|
+
* Drizzle reports failures as `Failed query: <sql> params:` and hides the real
|
|
29
|
+
* PostgreSQL error in the `cause` chain, so matching on `err.message` never sees
|
|
30
|
+
* the actual problem. Match on the PG error code instead — it survives wrapping
|
|
31
|
+
* and, unlike the message text, is not locale-dependent.
|
|
32
|
+
*/
|
|
33
|
+
function describeBranchDdlError(err: unknown, fallbackContext: string): Error {
|
|
34
|
+
const pgError = extractPgError(err);
|
|
35
|
+
|
|
36
|
+
if (pgError?.code === PG_DUPLICATE_DATABASE) {
|
|
37
|
+
return new Error(`Database "${fallbackContext}" already exists on the server. Choose a different branch name.`);
|
|
38
|
+
}
|
|
39
|
+
if (pgError?.code === PG_OBJECT_IN_USE) {
|
|
40
|
+
return new Error(
|
|
41
|
+
`Cannot complete the operation: the database "${fallbackContext}" has active connections. ` +
|
|
42
|
+
"Close other clients or connections and try again."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Unknown failure: surface the real PG message rather than the Drizzle
|
|
47
|
+
// wrapper, which would otherwise show the raw SQL and no reason at all.
|
|
48
|
+
const detail = pgError?.message ?? extractCauseMessage(err);
|
|
49
|
+
if (detail) return new Error(detail);
|
|
50
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
/** Fully-qualified metadata table in the rebase schema. */
|
|
19
54
|
const BRANCHES_TABLE = "rebase.branches";
|
|
20
55
|
|
|
@@ -109,18 +144,15 @@ export class BranchService {
|
|
|
109
144
|
sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`)
|
|
110
145
|
);
|
|
111
146
|
} catch (err) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
// If template fails due to active connections, provide a helpful error
|
|
117
|
-
if (msg.includes("being accessed by other users")) {
|
|
147
|
+
const pgError = extractPgError(err);
|
|
148
|
+
if (pgError?.code === PG_OBJECT_IN_USE) {
|
|
149
|
+
// The template — not the new database — is the one still in use.
|
|
118
150
|
throw new Error(
|
|
119
151
|
`Cannot create branch: the source database "${sourceDb}" has active connections. ` +
|
|
120
152
|
"Close other clients or connections and try again."
|
|
121
153
|
);
|
|
122
154
|
}
|
|
123
|
-
throw err;
|
|
155
|
+
throw describeBranchDdlError(err, dbName);
|
|
124
156
|
}
|
|
125
157
|
|
|
126
158
|
// Record metadata in the default database
|
|
@@ -166,14 +198,14 @@ export class BranchService {
|
|
|
166
198
|
try {
|
|
167
199
|
await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
|
|
168
200
|
} catch (err) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
201
|
+
const pgError = extractPgError(err);
|
|
202
|
+
if (pgError?.code === PG_OBJECT_IN_USE) {
|
|
171
203
|
throw new Error(
|
|
172
204
|
`Cannot delete branch "${sanitizedName}": the database has active connections. ` +
|
|
173
205
|
"Close other clients and try again."
|
|
174
206
|
);
|
|
175
207
|
}
|
|
176
|
-
throw err;
|
|
208
|
+
throw describeBranchDdlError(err, dbName);
|
|
177
209
|
}
|
|
178
210
|
|
|
179
211
|
// Remove metadata
|
|
@@ -7,15 +7,18 @@ import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
|
7
7
|
import {
|
|
8
8
|
getCollectionByPath,
|
|
9
9
|
getTableForCollection,
|
|
10
|
-
|
|
10
|
+
requirePrimaryKeys,
|
|
11
|
+
deriveRowAddress,
|
|
11
12
|
parseIdValues,
|
|
12
|
-
buildCompositeId
|
|
13
|
+
buildCompositeId,
|
|
14
|
+
COMPOSITE_ID_SEPARATOR
|
|
13
15
|
} from "./collection-helpers";
|
|
14
16
|
import { parseDataFromServer, normalizeDbValues } from "../data-transformer";
|
|
15
17
|
import { RelationService } from "./RelationService";
|
|
16
18
|
import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
|
|
17
19
|
import { DrizzleClient } from "../interfaces";
|
|
18
20
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
21
|
+
import { toCmsRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
19
22
|
import { logger } from "@rebasepro/server";
|
|
20
23
|
|
|
21
24
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
@@ -109,7 +112,7 @@ export class FetchService {
|
|
|
109
112
|
// Detect many-to-many junction tables:
|
|
110
113
|
// If the relation goes through a junction table (relation.through exists or
|
|
111
114
|
// the Drizzle schema maps to a junction table), we need two-level with.
|
|
112
|
-
if (relation.cardinality === "many" &&
|
|
115
|
+
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
113
116
|
// The Drizzle relation points to the junction table.
|
|
114
117
|
// We need: { [junctionRelName]: { with: { [targetFkName]: true } } }
|
|
115
118
|
// The target FK name is the relation on the junction table that points to the actual target.
|
|
@@ -127,17 +130,6 @@ export class FetchService {
|
|
|
127
130
|
return withConfig;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
/**
|
|
131
|
-
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
132
|
-
*/
|
|
133
|
-
private isJunctionRelation(relation: Relation, _collection: CollectionConfig): boolean {
|
|
134
|
-
// If `through` is defined, it's explicitly a junction relation
|
|
135
|
-
if (relation.through) return true;
|
|
136
|
-
// If joinPath has an intermediate table, it's likely junction-based
|
|
137
|
-
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
133
|
/**
|
|
142
134
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
143
135
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
@@ -151,88 +143,6 @@ export class FetchService {
|
|
|
151
143
|
return null;
|
|
152
144
|
}
|
|
153
145
|
|
|
154
|
-
/**
|
|
155
|
-
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
156
|
-
* Handles:
|
|
157
|
-
* - Placing `id` at the top level as a string
|
|
158
|
-
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
159
|
-
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
160
|
-
* - Flattening junction-table many-to-many results
|
|
161
|
-
*/
|
|
162
|
-
private drizzleResultToRow<M extends Record<string, unknown>>(
|
|
163
|
-
row: Record<string, unknown>,
|
|
164
|
-
collection: CollectionConfig,
|
|
165
|
-
_collectionPath: string,
|
|
166
|
-
idInfo: { fieldName: string; type: "string" | "number" },
|
|
167
|
-
_databaseId?: string,
|
|
168
|
-
idInfoArray?: { fieldName: string; type: "string" | "number" }[]
|
|
169
|
-
): Record<string, unknown> {
|
|
170
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
171
|
-
|
|
172
|
-
// Normalize non-relation values (dates, numbers, etc.)
|
|
173
|
-
const normalizedValues = normalizeDbValues(row as M, collection) as Record<string, unknown>;
|
|
174
|
-
|
|
175
|
-
// Convert nested relation objects to CMS-style { id, path, __type: "relation" }
|
|
176
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
177
|
-
const drizzleRelName = relation.relationName || key;
|
|
178
|
-
const relData = row[drizzleRelName];
|
|
179
|
-
|
|
180
|
-
if (relData === undefined || relData === null) continue;
|
|
181
|
-
|
|
182
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
183
|
-
const targetCollection = relation.target();
|
|
184
|
-
const targetPath = targetCollection.slug;
|
|
185
|
-
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
186
|
-
const targetIdField = targetPks[0].fieldName;
|
|
187
|
-
|
|
188
|
-
normalizedValues[key] = relData.map((item: Record<string, unknown>) => {
|
|
189
|
-
// Handle junction table flattening:
|
|
190
|
-
// Junction rows look like { post_id: 1, tag_id: { id: 5, name: "ts" } }
|
|
191
|
-
let targetRow = item;
|
|
192
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
193
|
-
// Find the nested target object in the junction row
|
|
194
|
-
const nestedKey = Object.keys(item).find(
|
|
195
|
-
nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk])
|
|
196
|
-
);
|
|
197
|
-
if (nestedKey) {
|
|
198
|
-
targetRow = item[nestedKey] as Record<string, unknown>;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
203
|
-
const targetValues = normalizeDbValues(targetRow, targetCollection);
|
|
204
|
-
|
|
205
|
-
return createRelationRefWithData(relId, targetPath, {
|
|
206
|
-
id: relId,
|
|
207
|
-
path: targetPath,
|
|
208
|
-
values: targetValues
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
212
|
-
const targetCollection = relation.target();
|
|
213
|
-
const targetPath = targetCollection.slug;
|
|
214
|
-
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
215
|
-
const targetIdField = targetPks[0].fieldName;
|
|
216
|
-
const relObj = relData as Record<string, unknown>;
|
|
217
|
-
|
|
218
|
-
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
219
|
-
const targetValues = normalizeDbValues(relObj, targetCollection);
|
|
220
|
-
|
|
221
|
-
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
222
|
-
id: relId,
|
|
223
|
-
path: targetPath,
|
|
224
|
-
values: targetValues
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
...normalizedValues,
|
|
231
|
-
// Spread the canonical id last so it wins over a raw `id` column
|
|
232
|
-
id: (idInfoArray && idInfoArray.length > 1) ? buildCompositeId(row as Record<string, unknown>, idInfoArray) : String(row[idInfo.fieldName])
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
146
|
/**
|
|
237
147
|
* Post-fetch joinPath relations for a single flat row.
|
|
238
148
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
@@ -274,57 +184,6 @@ export class FetchService {
|
|
|
274
184
|
await Promise.all(promises);
|
|
275
185
|
}
|
|
276
186
|
|
|
277
|
-
/**
|
|
278
|
-
* Post-fetch joinPath relations for a batch of flat rows.
|
|
279
|
-
* Uses batch fetching to avoid N+1 queries for list views.
|
|
280
|
-
*/
|
|
281
|
-
private async resolveJoinPathRelationsBatch<M extends Record<string, unknown>>(
|
|
282
|
-
rows: Record<string, unknown>[],
|
|
283
|
-
collection: CollectionConfig,
|
|
284
|
-
collectionPath: string,
|
|
285
|
-
idInfo: { fieldName: string; type: "string" | "number" },
|
|
286
|
-
_databaseId?: string
|
|
287
|
-
): Promise<void> {
|
|
288
|
-
if (rows.length === 0) return;
|
|
289
|
-
|
|
290
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
291
|
-
|
|
292
|
-
const joinPathRelations = Object.entries(resolvedRelations)
|
|
293
|
-
.filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
294
|
-
|
|
295
|
-
if (joinPathRelations.length === 0) return;
|
|
296
|
-
|
|
297
|
-
for (const [key, relation] of joinPathRelations) {
|
|
298
|
-
try {
|
|
299
|
-
const rowIds = rows.map(r => {
|
|
300
|
-
const parsed = parseIdValues(String(r.id), [idInfo]);
|
|
301
|
-
return parsed[idInfo.fieldName] as string | number;
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const resultMap = await this.relationService.batchFetchRelatedEntities(
|
|
305
|
-
collectionPath,
|
|
306
|
-
rowIds,
|
|
307
|
-
key,
|
|
308
|
-
relation
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
for (const row of rows) {
|
|
312
|
-
const parsed = parseIdValues(String(row.id), [idInfo]);
|
|
313
|
-
const id = parsed[idInfo.fieldName] as string | number;
|
|
314
|
-
const relatedRow = resultMap.get(String(id));
|
|
315
|
-
|
|
316
|
-
if (relatedRow) {
|
|
317
|
-
if (relation.cardinality === "one") {
|
|
318
|
-
row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} catch (e) {
|
|
323
|
-
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
187
|
/**
|
|
329
188
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
330
189
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
@@ -348,15 +207,25 @@ export class FetchService {
|
|
|
348
207
|
|
|
349
208
|
if (joinPathRelations.length === 0) return;
|
|
350
209
|
|
|
351
|
-
|
|
210
|
+
// These rows carry their key columns verbatim, so the parent's address
|
|
211
|
+
// is derived from them. It used to be parsed back out of a synthesized
|
|
212
|
+
// `id` — which no longer exists on a row, and threw (composite: parts
|
|
213
|
+
// mismatch; numeric: NaN) into the catch below, where a warning is all
|
|
214
|
+
// that separates "no relations" from "relations dropped".
|
|
215
|
+
//
|
|
216
|
+
// The whole key, because that is what the batch groups its results by:
|
|
217
|
+
// both sides derive the token the same way, so they agree by
|
|
218
|
+
// construction rather than by both happening to pick column zero.
|
|
219
|
+
const parentIdOf = (row: Record<string, unknown>): string | undefined => {
|
|
220
|
+
const address = buildCompositeId(row, idInfoArray);
|
|
221
|
+
return address && address.split(COMPOSITE_ID_SEPARATOR).some(part => part !== "") ? address : undefined;
|
|
222
|
+
};
|
|
352
223
|
|
|
353
224
|
for (const [key, relation] of joinPathRelations) {
|
|
354
225
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return parsed[idInfo.fieldName] as string | number;
|
|
359
|
-
});
|
|
226
|
+
const addressable = rows.filter(r => parentIdOf(r) !== undefined && parentIdOf(r) !== null);
|
|
227
|
+
if (addressable.length === 0) continue;
|
|
228
|
+
const rowIds = addressable.map(r => parentIdOf(r) as string | number);
|
|
360
229
|
|
|
361
230
|
if (relation.cardinality === "one") {
|
|
362
231
|
const resultMap = await this.relationService.batchFetchRelatedEntities(
|
|
@@ -366,19 +235,11 @@ export class FetchService {
|
|
|
366
235
|
relation
|
|
367
236
|
);
|
|
368
237
|
|
|
369
|
-
for (const row of
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (relatedRow) {
|
|
375
|
-
row[key] = {
|
|
376
|
-
...relatedRow.values,
|
|
377
|
-
id: relatedRow.id
|
|
378
|
-
};
|
|
379
|
-
} else {
|
|
380
|
-
row[key] = null;
|
|
381
|
-
}
|
|
238
|
+
for (const row of addressable) {
|
|
239
|
+
const relatedRow = resultMap.get(String(parentIdOf(row)));
|
|
240
|
+
// Columns only: the target's address is the consumer's to
|
|
241
|
+
// derive, and merging it last overwrote a real `id` column.
|
|
242
|
+
row[key] = relatedRow ? { ...relatedRow.values } : null;
|
|
382
243
|
}
|
|
383
244
|
} else if (relation.cardinality === "many") {
|
|
384
245
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(
|
|
@@ -388,14 +249,9 @@ export class FetchService {
|
|
|
388
249
|
relation
|
|
389
250
|
);
|
|
390
251
|
|
|
391
|
-
for (const row of
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
const relatedList = resultMap.get(String(id)) || [];
|
|
395
|
-
row[key] = relatedList.map(e => ({
|
|
396
|
-
...e.values,
|
|
397
|
-
id: e.id
|
|
398
|
-
}));
|
|
252
|
+
for (const row of addressable) {
|
|
253
|
+
const relatedList = resultMap.get(String(parentIdOf(row))) || [];
|
|
254
|
+
row[key] = relatedList.map(e => ({ ...e.values }));
|
|
399
255
|
}
|
|
400
256
|
}
|
|
401
257
|
} catch (e) {
|
|
@@ -404,50 +260,6 @@ export class FetchService {
|
|
|
404
260
|
}
|
|
405
261
|
}
|
|
406
262
|
|
|
407
|
-
/**
|
|
408
|
-
* Convert a db.query result row to a flat REST-style object with populated relations.
|
|
409
|
-
*/
|
|
410
|
-
private drizzleResultToRestRow(
|
|
411
|
-
row: Record<string, unknown>,
|
|
412
|
-
collection: CollectionConfig,
|
|
413
|
-
idInfo: { fieldName: string; type: "string" | "number" },
|
|
414
|
-
idInfoArray?: { fieldName: string; type: "string" | "number" }[]
|
|
415
|
-
): Record<string, unknown> {
|
|
416
|
-
const flat: Record<string, unknown> = { id: (idInfoArray && idInfoArray.length > 1) ? buildCompositeId(row as Record<string, unknown>, idInfoArray) : String(row[idInfo.fieldName]) };
|
|
417
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
418
|
-
|
|
419
|
-
for (const [k, v] of Object.entries(row)) {
|
|
420
|
-
if (k === idInfo.fieldName) continue;
|
|
421
|
-
|
|
422
|
-
const relation = findRelation(resolvedRelations, k);
|
|
423
|
-
if (Array.isArray(v) && relation) {
|
|
424
|
-
// Many relation — flatten each nested row, handling junction tables
|
|
425
|
-
flat[k] = v.map((item: Record<string, unknown>) => {
|
|
426
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
427
|
-
const nestedKey = Object.keys(item).find(
|
|
428
|
-
nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk])
|
|
429
|
-
);
|
|
430
|
-
if (nestedKey) {
|
|
431
|
-
const nested = item[nestedKey] as Record<string, unknown>;
|
|
432
|
-
return { ...nested,
|
|
433
|
-
id: String(nested.id ?? nested[Object.keys(nested)[0]]) };
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return { ...item,
|
|
437
|
-
id: String(item.id ?? item[Object.keys(item)[0]]) };
|
|
438
|
-
});
|
|
439
|
-
} else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) {
|
|
440
|
-
// One-to-one relation — inline the object
|
|
441
|
-
const relObj = v as Record<string, unknown>;
|
|
442
|
-
flat[k] = { ...relObj,
|
|
443
|
-
id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
444
|
-
} else {
|
|
445
|
-
flat[k] = v;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return flat;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
263
|
/**
|
|
452
264
|
* Build db.query-compatible options from standard fetch options.
|
|
453
265
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
@@ -589,7 +401,7 @@ id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
|
589
401
|
): Promise<Record<string, unknown> | undefined> {
|
|
590
402
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
591
403
|
const table = getTableForCollection(collection, this.registry);
|
|
592
|
-
const idInfoArray =
|
|
404
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
593
405
|
const idInfo = idInfoArray[0];
|
|
594
406
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
595
407
|
|
|
@@ -616,7 +428,7 @@ id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
|
616
428
|
|
|
617
429
|
if (!row) return undefined;
|
|
618
430
|
|
|
619
|
-
const flatRow =
|
|
431
|
+
const flatRow = toCmsRow(row, collection, this.registry);
|
|
620
432
|
|
|
621
433
|
// Post-fetch joinPath relations that Drizzle's `with` can't express
|
|
622
434
|
await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
@@ -708,7 +520,7 @@ id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
|
708
520
|
): Promise<Record<string, unknown>[]> {
|
|
709
521
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
710
522
|
const table = getTableForCollection(collection, this.registry);
|
|
711
|
-
const idInfoArray =
|
|
523
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
712
524
|
const idInfo = idInfoArray[0];
|
|
713
525
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
714
526
|
|
|
@@ -740,7 +552,7 @@ id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
|
740
552
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
741
553
|
|
|
742
554
|
const rows = (results as Record<string, unknown>[]).map(row =>
|
|
743
|
-
|
|
555
|
+
toCmsRow(row, collection, this.registry)
|
|
744
556
|
);
|
|
745
557
|
|
|
746
558
|
return rows;
|
|
@@ -840,8 +652,10 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
840
652
|
|
|
841
653
|
/**
|
|
842
654
|
* Fallback path used when db.query is unavailable.
|
|
843
|
-
*
|
|
844
|
-
*
|
|
655
|
+
*
|
|
656
|
+
* The primary path runs the results through `toCmsRow`, which maps
|
|
657
|
+
* relations from what drizzle already nested — no query per row. This one
|
|
658
|
+
* has no nesting to read, so it resolves relations itself, in batches.
|
|
845
659
|
*
|
|
846
660
|
* Process raw database results into flat rows with relations.
|
|
847
661
|
*/
|
|
@@ -864,11 +678,9 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
864
678
|
// relation type, avoiding the N+1 that plagued the old path.
|
|
865
679
|
const parsedRows = await Promise.all(results.map(async (rawRow: Record<string, unknown>) => {
|
|
866
680
|
const values = await parseDataFromServer(rawRow as M, collection) as Record<string, unknown>;
|
|
867
|
-
const id = (idInfoArray && idInfoArray.length > 1) ? buildCompositeId(rawRow as Record<string, unknown>, idInfoArray!) : String(rawRow[idInfo.fieldName]);
|
|
868
681
|
return {
|
|
869
682
|
rawRow,
|
|
870
|
-
values
|
|
871
|
-
id
|
|
683
|
+
values
|
|
872
684
|
};
|
|
873
685
|
}));
|
|
874
686
|
|
|
@@ -938,10 +750,8 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
938
750
|
}
|
|
939
751
|
}
|
|
940
752
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
id: item.id
|
|
944
|
-
}));
|
|
753
|
+
// Columns only — the address is the consumer's to derive.
|
|
754
|
+
return parsedRows.map(item => item.values);
|
|
945
755
|
}
|
|
946
756
|
|
|
947
757
|
/**
|
|
@@ -1030,11 +840,12 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1030
840
|
relationKey,
|
|
1031
841
|
options
|
|
1032
842
|
);
|
|
1033
|
-
//
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
843
|
+
// Flatten RelatedRow[] to rows: the target's columns, and only
|
|
844
|
+
// those. Merging `row.id` in last overwrote a real `id` column,
|
|
845
|
+
// and the address is derivable without it — a consumer resolves
|
|
846
|
+
// this path ("posts/1/comments") to the target collection and
|
|
847
|
+
// reads the key columns, which `values` carries.
|
|
848
|
+
return rows.map(row => ({ ...row.values as Record<string, unknown> }));
|
|
1038
849
|
}
|
|
1039
850
|
|
|
1040
851
|
if (i + 1 < pathSegments.length) {
|
|
@@ -1148,7 +959,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1148
959
|
|
|
1149
960
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1150
961
|
const table = getTableForCollection(collection, this.registry);
|
|
1151
|
-
const idInfoArray =
|
|
962
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1152
963
|
const idInfo = idInfoArray[0];
|
|
1153
964
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1154
965
|
const field = table[fieldName as keyof typeof table] as AnyPgColumn;
|
|
@@ -1208,7 +1019,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1208
1019
|
): Promise<Record<string, unknown>[]> {
|
|
1209
1020
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1210
1021
|
const table = getTableForCollection(collection, this.registry);
|
|
1211
|
-
const idInfoArray =
|
|
1022
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1212
1023
|
const idInfo = idInfoArray[0];
|
|
1213
1024
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1214
1025
|
|
|
@@ -1235,7 +1046,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1235
1046
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
1236
1047
|
|
|
1237
1048
|
const restRows = (results as Record<string, unknown>[]).map(row =>
|
|
1238
|
-
|
|
1049
|
+
toRestRow(row, collection, this.registry)
|
|
1239
1050
|
);
|
|
1240
1051
|
|
|
1241
1052
|
// Drizzle relational query API doesn't resolve joinPath relations, fetch manually
|
|
@@ -1255,10 +1066,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1255
1066
|
const rows = await this.fetchRowsWithConditionsRaw<M>(collectionPath, options);
|
|
1256
1067
|
|
|
1257
1068
|
if (!include || include.length === 0) {
|
|
1258
|
-
return rows
|
|
1259
|
-
...row,
|
|
1260
|
-
id: (idInfoArray.length > 1) ? buildCompositeId(row as Record<string, unknown>, idInfoArray) : String(row[idInfo.fieldName])
|
|
1261
|
-
}));
|
|
1069
|
+
return rows;
|
|
1262
1070
|
}
|
|
1263
1071
|
|
|
1264
1072
|
// Fallback relation loading via batch
|
|
@@ -1279,8 +1087,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1279
1087
|
const eid = row[idInfo.fieldName] as string | number;
|
|
1280
1088
|
const related = batchResults.get(String(eid));
|
|
1281
1089
|
if (related) {
|
|
1282
|
-
(row as Record<string, unknown>)[key] = { ...related.values
|
|
1283
|
-
id: related.id };
|
|
1090
|
+
(row as Record<string, unknown>)[key] = { ...related.values };
|
|
1284
1091
|
}
|
|
1285
1092
|
}
|
|
1286
1093
|
} catch (e) {
|
|
@@ -1304,10 +1111,7 @@ id: related.id };
|
|
|
1304
1111
|
}
|
|
1305
1112
|
}
|
|
1306
1113
|
|
|
1307
|
-
return rows
|
|
1308
|
-
...row,
|
|
1309
|
-
id: (idInfoArray.length > 1) ? buildCompositeId(row as Record<string, unknown>, idInfoArray) : String(row[idInfo.fieldName])
|
|
1310
|
-
}));
|
|
1114
|
+
return rows;
|
|
1311
1115
|
}
|
|
1312
1116
|
|
|
1313
1117
|
/**
|
|
@@ -1321,7 +1125,7 @@ id: related.id };
|
|
|
1321
1125
|
): Promise<Record<string, unknown> | null> {
|
|
1322
1126
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1323
1127
|
const table = getTableForCollection(collection, this.registry);
|
|
1324
|
-
const idInfoArray =
|
|
1128
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1325
1129
|
const idInfo = idInfoArray[0];
|
|
1326
1130
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1327
1131
|
|
|
@@ -1347,7 +1151,7 @@ id: related.id };
|
|
|
1347
1151
|
|
|
1348
1152
|
if (!row) return null;
|
|
1349
1153
|
|
|
1350
|
-
const restRow =
|
|
1154
|
+
const restRow = toRestRow(row, collection, this.registry);
|
|
1351
1155
|
|
|
1352
1156
|
// Drizzle relational query API doesn't resolve joinPath relations, fetch manually
|
|
1353
1157
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
@@ -1371,9 +1175,7 @@ id: related.id };
|
|
|
1371
1175
|
|
|
1372
1176
|
if (result.length === 0) return null;
|
|
1373
1177
|
|
|
1374
|
-
const
|
|
1375
|
-
const flatEntity: Record<string, unknown> = { ...raw,
|
|
1376
|
-
id: (idInfoArray.length > 1) ? buildCompositeId(raw as Record<string, unknown>, idInfoArray) : String(raw[idInfo.fieldName]) };
|
|
1178
|
+
const flatEntity: Record<string, unknown> = { ...(result[0] as Record<string, unknown>) };
|
|
1377
1179
|
|
|
1378
1180
|
if (!include || include.length === 0) {
|
|
1379
1181
|
return flatEntity;
|
|
@@ -1431,7 +1233,7 @@ id: (idInfoArray.length > 1) ? buildCompositeId(raw as Record<string, unknown>,
|
|
|
1431
1233
|
): Promise<Record<string, unknown>[]> {
|
|
1432
1234
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1433
1235
|
const table = getTableForCollection(collection, this.registry);
|
|
1434
|
-
const idInfoArray =
|
|
1236
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1435
1237
|
const idInfo = idInfoArray[0];
|
|
1436
1238
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1437
1239
|
|
|
@@ -1581,31 +1383,24 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1581
1383
|
|
|
1582
1384
|
const results = await queryTarget.findMany(queryOpts as Parameters<NonNullable<typeof queryTarget>["findMany"]>[0]);
|
|
1583
1385
|
|
|
1584
|
-
//
|
|
1386
|
+
// Inline the nested Drizzle results, columns only — no synthesized id.
|
|
1585
1387
|
return results.map((row: Record<string, unknown>) => {
|
|
1586
|
-
const flat: Record<string, unknown> = {
|
|
1388
|
+
const flat: Record<string, unknown> = {};
|
|
1587
1389
|
for (const [k, v] of Object.entries(row)) {
|
|
1588
|
-
if (k === idInfo.fieldName) continue;
|
|
1589
1390
|
if (Array.isArray(v)) {
|
|
1590
|
-
// Many relation —
|
|
1391
|
+
// Many relation — inline each nested row
|
|
1591
1392
|
flat[k] = v.map((item: Record<string, unknown>) => {
|
|
1592
|
-
// Junction table rows may have the target nested,
|
|
1393
|
+
// Junction table rows may have the target nested, unwrap those
|
|
1593
1394
|
const keys = Object.keys(item);
|
|
1594
|
-
// If it looks like a junction row (only FKs + nested objects), extract nested
|
|
1595
1395
|
const nestedObj = keys.find(nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
1596
1396
|
if (nestedObj && keys.length <= 3) {
|
|
1597
|
-
|
|
1598
|
-
return { ...nested,
|
|
1599
|
-
id: String(nested.id ?? nested[Object.keys(nested)[0]]) };
|
|
1397
|
+
return { ...(item[nestedObj] as Record<string, unknown>) };
|
|
1600
1398
|
}
|
|
1601
|
-
return { ...item
|
|
1602
|
-
id: String(item.id ?? item[Object.keys(item)[0]]) };
|
|
1399
|
+
return { ...item };
|
|
1603
1400
|
});
|
|
1604
1401
|
} else if (typeof v === "object" && v !== null) {
|
|
1605
|
-
// One-to-one relation — inline the
|
|
1606
|
-
|
|
1607
|
-
flat[k] = { ...relObj,
|
|
1608
|
-
id: String(relObj.id ?? relObj[Object.keys(relObj)[0]]) };
|
|
1402
|
+
// One-to-one relation — inline the target's columns
|
|
1403
|
+
flat[k] = { ...(v as Record<string, unknown>) };
|
|
1609
1404
|
} else {
|
|
1610
1405
|
flat[k] = v;
|
|
1611
1406
|
}
|