@rebasepro/server-postgres 0.9.1-canary.ad25bc0 → 0.9.1-canary.b10dcdf
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/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 +822 -329
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +4 -32
- package/dist/services/RelationService.d.ts +34 -1
- package/dist/services/collection-helpers.d.ts +76 -0
- 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 +10 -7
- package/src/PostgresBackendDriver.ts +39 -11
- package/src/PostgresBootstrapper.ts +62 -25
- package/src/auth/ensure-tables.ts +73 -11
- package/src/auth/services.ts +49 -19
- 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/schema/generate-drizzle-schema-logic.ts +21 -4
- package/src/schema/generate-postgres-ddl-logic.ts +63 -3
- package/src/schema/introspect-db.ts +19 -2
- package/src/services/FetchService.ts +50 -229
- package/src/services/PersistService.ts +9 -2
- package/src/services/RelationService.ts +153 -94
- package/src/services/collection-helpers.ts +166 -3
- package/src/services/index.ts +1 -0
- package/src/services/realtimeService.ts +40 -19
- package/src/services/row-pipeline.ts +215 -0
- package/src/utils/drizzle-conditions.ts +13 -0
|
@@ -30,6 +30,7 @@ async function main() {
|
|
|
30
30
|
"--force": Boolean,
|
|
31
31
|
"--schema": String,
|
|
32
32
|
"--data-inference": Boolean,
|
|
33
|
+
"--no-data-inference": Boolean,
|
|
33
34
|
"-o": "--output",
|
|
34
35
|
"-c": "--collections",
|
|
35
36
|
"-f": "--force"
|
|
@@ -166,8 +167,14 @@ async function main() {
|
|
|
166
167
|
logger.info(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
|
|
167
168
|
|
|
168
169
|
let runDataInference = false;
|
|
169
|
-
if (args["--data-inference"]
|
|
170
|
+
if (args["--no-data-inference"]) {
|
|
171
|
+
runDataInference = false;
|
|
172
|
+
} else if (args["--data-inference"] !== undefined) {
|
|
170
173
|
runDataInference = args["--data-inference"];
|
|
174
|
+
} else if (!process.stdin.isTTY) {
|
|
175
|
+
// No terminal to answer the question below (scaffolding scripts, CI,
|
|
176
|
+
// `rebase init --introspect`) — asking would hang forever.
|
|
177
|
+
logger.info(chalk.gray("Skipping data inference (non-interactive run; pass --data-inference to enable)."));
|
|
171
178
|
} else {
|
|
172
179
|
const rl = readline.createInterface({
|
|
173
180
|
input: process.stdin,
|
|
@@ -236,7 +243,17 @@ async function main() {
|
|
|
236
243
|
const merged = mergeIndexContent(existing, generatedFiles);
|
|
237
244
|
fs.writeFileSync(indexPath, merged, "utf-8");
|
|
238
245
|
} else {
|
|
239
|
-
|
|
246
|
+
// --force replaces collections derived from the database, but the
|
|
247
|
+
// directory can also hold hand-written ones with no table in the
|
|
248
|
+
// introspected schema (the auth users collection lives in
|
|
249
|
+
// "rebase"). The backend discovers the whole directory, so an
|
|
250
|
+
// index listing only introspected tables would silently drop them
|
|
251
|
+
// from the admin UI while the API still served them.
|
|
252
|
+
const siblings = fs.readdirSync(outDir)
|
|
253
|
+
.filter(f => f.endsWith(".ts") && f !== "index.ts")
|
|
254
|
+
.map(f => f.replace(/\.ts$/, ""));
|
|
255
|
+
const allFiles = [...new Set([...generatedFiles, ...siblings])];
|
|
256
|
+
const indexContent = generateIndexContent(allFiles);
|
|
240
257
|
fs.writeFileSync(indexPath, indexContent, "utf-8");
|
|
241
258
|
}
|
|
242
259
|
logger.info(chalk.green(` ✓ ${indexPath}`));
|
|
@@ -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,85 +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
|
-
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
158
|
-
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
159
|
-
* - Flattening junction-table many-to-many results
|
|
160
|
-
*
|
|
161
|
-
* The row's own address is not among them: it is derived by the consumer
|
|
162
|
-
* from the collection's primary keys.
|
|
163
|
-
*/
|
|
164
|
-
private drizzleResultToRow<M extends Record<string, unknown>>(
|
|
165
|
-
row: Record<string, unknown>,
|
|
166
|
-
collection: CollectionConfig
|
|
167
|
-
): Record<string, unknown> {
|
|
168
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
169
|
-
|
|
170
|
-
// Normalize non-relation values (dates, numbers, etc.)
|
|
171
|
-
const normalizedValues = normalizeDbValues(row as M, collection) as Record<string, unknown>;
|
|
172
|
-
|
|
173
|
-
// Convert nested relation objects to CMS-style { id, path, __type: "relation" }
|
|
174
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
175
|
-
const drizzleRelName = relation.relationName || key;
|
|
176
|
-
const relData = row[drizzleRelName];
|
|
177
|
-
|
|
178
|
-
if (relData === undefined || relData === null) continue;
|
|
179
|
-
|
|
180
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
181
|
-
const targetCollection = relation.target();
|
|
182
|
-
const targetPath = targetCollection.slug;
|
|
183
|
-
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
184
|
-
const targetIdField = targetPks[0].fieldName;
|
|
185
|
-
|
|
186
|
-
normalizedValues[key] = relData.map((item: Record<string, unknown>) => {
|
|
187
|
-
// Handle junction table flattening:
|
|
188
|
-
// Junction rows look like { post_id: 1, tag_id: { id: 5, name: "ts" } }
|
|
189
|
-
let targetRow = item;
|
|
190
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
191
|
-
// Find the nested target object in the junction row
|
|
192
|
-
const nestedKey = Object.keys(item).find(
|
|
193
|
-
nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk])
|
|
194
|
-
);
|
|
195
|
-
if (nestedKey) {
|
|
196
|
-
targetRow = item[nestedKey] as Record<string, unknown>;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
201
|
-
const targetValues = normalizeDbValues(targetRow, targetCollection);
|
|
202
|
-
|
|
203
|
-
return createRelationRefWithData(relId, targetPath, {
|
|
204
|
-
id: relId,
|
|
205
|
-
path: targetPath,
|
|
206
|
-
values: targetValues
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
210
|
-
const targetCollection = relation.target();
|
|
211
|
-
const targetPath = targetCollection.slug;
|
|
212
|
-
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
213
|
-
const targetIdField = targetPks[0].fieldName;
|
|
214
|
-
const relObj = relData as Record<string, unknown>;
|
|
215
|
-
|
|
216
|
-
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
217
|
-
const targetValues = normalizeDbValues(relObj, targetCollection);
|
|
218
|
-
|
|
219
|
-
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
220
|
-
id: relId,
|
|
221
|
-
path: targetPath,
|
|
222
|
-
values: targetValues
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// A row is exactly its columns. The address is derived by the consumer
|
|
228
|
-
// from the collection's primary keys — writing it here would rename the
|
|
229
|
-
// key column (`sku` → `id`) and restringify it (`42` → `"42"`).
|
|
230
|
-
return normalizedValues;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
146
|
/**
|
|
234
147
|
* Post-fetch joinPath relations for a single flat row.
|
|
235
148
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
@@ -271,57 +184,6 @@ export class FetchService {
|
|
|
271
184
|
await Promise.all(promises);
|
|
272
185
|
}
|
|
273
186
|
|
|
274
|
-
/**
|
|
275
|
-
* Post-fetch joinPath relations for a batch of flat rows.
|
|
276
|
-
* Uses batch fetching to avoid N+1 queries for list views.
|
|
277
|
-
*/
|
|
278
|
-
private async resolveJoinPathRelationsBatch<M extends Record<string, unknown>>(
|
|
279
|
-
rows: Record<string, unknown>[],
|
|
280
|
-
collection: CollectionConfig,
|
|
281
|
-
collectionPath: string,
|
|
282
|
-
idInfo: { fieldName: string; type: "string" | "number" },
|
|
283
|
-
_databaseId?: string
|
|
284
|
-
): Promise<void> {
|
|
285
|
-
if (rows.length === 0) return;
|
|
286
|
-
|
|
287
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
288
|
-
|
|
289
|
-
const joinPathRelations = Object.entries(resolvedRelations)
|
|
290
|
-
.filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
291
|
-
|
|
292
|
-
if (joinPathRelations.length === 0) return;
|
|
293
|
-
|
|
294
|
-
for (const [key, relation] of joinPathRelations) {
|
|
295
|
-
try {
|
|
296
|
-
const rowIds = rows.map(r => {
|
|
297
|
-
const parsed = parseIdValues(String(r.id), [idInfo]);
|
|
298
|
-
return parsed[idInfo.fieldName] as string | number;
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
const resultMap = await this.relationService.batchFetchRelatedEntities(
|
|
302
|
-
collectionPath,
|
|
303
|
-
rowIds,
|
|
304
|
-
key,
|
|
305
|
-
relation
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
for (const row of rows) {
|
|
309
|
-
const parsed = parseIdValues(String(row.id), [idInfo]);
|
|
310
|
-
const id = parsed[idInfo.fieldName] as string | number;
|
|
311
|
-
const relatedRow = resultMap.get(String(id));
|
|
312
|
-
|
|
313
|
-
if (relatedRow) {
|
|
314
|
-
if (relation.cardinality === "one") {
|
|
315
|
-
row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} catch (e) {
|
|
320
|
-
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
187
|
/**
|
|
326
188
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
327
189
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
@@ -345,15 +207,25 @@ export class FetchService {
|
|
|
345
207
|
|
|
346
208
|
if (joinPathRelations.length === 0) return;
|
|
347
209
|
|
|
348
|
-
|
|
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
|
+
};
|
|
349
223
|
|
|
350
224
|
for (const [key, relation] of joinPathRelations) {
|
|
351
225
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return parsed[idInfo.fieldName] as string | number;
|
|
356
|
-
});
|
|
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);
|
|
357
229
|
|
|
358
230
|
if (relation.cardinality === "one") {
|
|
359
231
|
const resultMap = await this.relationService.batchFetchRelatedEntities(
|
|
@@ -363,19 +235,11 @@ export class FetchService {
|
|
|
363
235
|
relation
|
|
364
236
|
);
|
|
365
237
|
|
|
366
|
-
for (const row of
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (relatedRow) {
|
|
372
|
-
row[key] = {
|
|
373
|
-
...relatedRow.values,
|
|
374
|
-
id: relatedRow.id
|
|
375
|
-
};
|
|
376
|
-
} else {
|
|
377
|
-
row[key] = null;
|
|
378
|
-
}
|
|
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;
|
|
379
243
|
}
|
|
380
244
|
} else if (relation.cardinality === "many") {
|
|
381
245
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(
|
|
@@ -385,14 +249,9 @@ export class FetchService {
|
|
|
385
249
|
relation
|
|
386
250
|
);
|
|
387
251
|
|
|
388
|
-
for (const row of
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
const relatedList = resultMap.get(String(id)) || [];
|
|
392
|
-
row[key] = relatedList.map(e => ({
|
|
393
|
-
...e.values,
|
|
394
|
-
id: e.id
|
|
395
|
-
}));
|
|
252
|
+
for (const row of addressable) {
|
|
253
|
+
const relatedList = resultMap.get(String(parentIdOf(row))) || [];
|
|
254
|
+
row[key] = relatedList.map(e => ({ ...e.values }));
|
|
396
255
|
}
|
|
397
256
|
}
|
|
398
257
|
} catch (e) {
|
|
@@ -401,47 +260,6 @@ export class FetchService {
|
|
|
401
260
|
}
|
|
402
261
|
}
|
|
403
262
|
|
|
404
|
-
/**
|
|
405
|
-
* Convert a db.query result row to a flat REST-style row with populated relations.
|
|
406
|
-
*
|
|
407
|
-
* Every column is copied through under its own name, with the value Postgres
|
|
408
|
-
* returned. This used to open with a synthesized `id` and then skip the key
|
|
409
|
-
* column, which renamed it (a `sku` primary key was served as `id`, and `sku`
|
|
410
|
-
* did not appear at all) and restringified it (`42` → `"42"`). Consumers that
|
|
411
|
-
* need an address derive it from the collection's primary keys.
|
|
412
|
-
*/
|
|
413
|
-
private drizzleResultToRestRow(
|
|
414
|
-
row: Record<string, unknown>,
|
|
415
|
-
collection: CollectionConfig
|
|
416
|
-
): Record<string, unknown> {
|
|
417
|
-
const flat: Record<string, unknown> = {};
|
|
418
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
419
|
-
|
|
420
|
-
for (const [k, v] of Object.entries(row)) {
|
|
421
|
-
const relation = findRelation(resolvedRelations, k);
|
|
422
|
-
if (Array.isArray(v) && relation) {
|
|
423
|
-
// Many relation — inline each nested row, unwrapping junction rows
|
|
424
|
-
flat[k] = v.map((item: Record<string, unknown>) => {
|
|
425
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
426
|
-
const nestedKey = Object.keys(item).find(
|
|
427
|
-
nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk])
|
|
428
|
-
);
|
|
429
|
-
if (nestedKey) {
|
|
430
|
-
return { ...(item[nestedKey] as Record<string, unknown>) };
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return { ...item };
|
|
434
|
-
});
|
|
435
|
-
} else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) {
|
|
436
|
-
// One-to-one relation — inline the target's columns
|
|
437
|
-
flat[k] = { ...(v as Record<string, unknown>) };
|
|
438
|
-
} else {
|
|
439
|
-
flat[k] = v;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return flat;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
263
|
/**
|
|
446
264
|
* Build db.query-compatible options from standard fetch options.
|
|
447
265
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
@@ -583,7 +401,7 @@ export class FetchService {
|
|
|
583
401
|
): Promise<Record<string, unknown> | undefined> {
|
|
584
402
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
585
403
|
const table = getTableForCollection(collection, this.registry);
|
|
586
|
-
const idInfoArray =
|
|
404
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
587
405
|
const idInfo = idInfoArray[0];
|
|
588
406
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
589
407
|
|
|
@@ -610,7 +428,7 @@ export class FetchService {
|
|
|
610
428
|
|
|
611
429
|
if (!row) return undefined;
|
|
612
430
|
|
|
613
|
-
const flatRow =
|
|
431
|
+
const flatRow = toCmsRow(row, collection, this.registry);
|
|
614
432
|
|
|
615
433
|
// Post-fetch joinPath relations that Drizzle's `with` can't express
|
|
616
434
|
await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
@@ -702,7 +520,7 @@ export class FetchService {
|
|
|
702
520
|
): Promise<Record<string, unknown>[]> {
|
|
703
521
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
704
522
|
const table = getTableForCollection(collection, this.registry);
|
|
705
|
-
const idInfoArray =
|
|
523
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
706
524
|
const idInfo = idInfoArray[0];
|
|
707
525
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
708
526
|
|
|
@@ -734,7 +552,7 @@ export class FetchService {
|
|
|
734
552
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
735
553
|
|
|
736
554
|
const rows = (results as Record<string, unknown>[]).map(row =>
|
|
737
|
-
|
|
555
|
+
toCmsRow(row, collection, this.registry)
|
|
738
556
|
);
|
|
739
557
|
|
|
740
558
|
return rows;
|
|
@@ -834,8 +652,10 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
834
652
|
|
|
835
653
|
/**
|
|
836
654
|
* Fallback path used when db.query is unavailable.
|
|
837
|
-
*
|
|
838
|
-
*
|
|
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.
|
|
839
659
|
*
|
|
840
660
|
* Process raw database results into flat rows with relations.
|
|
841
661
|
*/
|
|
@@ -1020,11 +840,12 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1020
840
|
relationKey,
|
|
1021
841
|
options
|
|
1022
842
|
);
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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> }));
|
|
1028
849
|
}
|
|
1029
850
|
|
|
1030
851
|
if (i + 1 < pathSegments.length) {
|
|
@@ -1138,7 +959,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1138
959
|
|
|
1139
960
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1140
961
|
const table = getTableForCollection(collection, this.registry);
|
|
1141
|
-
const idInfoArray =
|
|
962
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1142
963
|
const idInfo = idInfoArray[0];
|
|
1143
964
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1144
965
|
const field = table[fieldName as keyof typeof table] as AnyPgColumn;
|
|
@@ -1198,7 +1019,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1198
1019
|
): Promise<Record<string, unknown>[]> {
|
|
1199
1020
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1200
1021
|
const table = getTableForCollection(collection, this.registry);
|
|
1201
|
-
const idInfoArray =
|
|
1022
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1202
1023
|
const idInfo = idInfoArray[0];
|
|
1203
1024
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1204
1025
|
|
|
@@ -1225,7 +1046,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1225
1046
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
1226
1047
|
|
|
1227
1048
|
const restRows = (results as Record<string, unknown>[]).map(row =>
|
|
1228
|
-
|
|
1049
|
+
toRestRow(row, collection, this.registry)
|
|
1229
1050
|
);
|
|
1230
1051
|
|
|
1231
1052
|
// Drizzle relational query API doesn't resolve joinPath relations, fetch manually
|
|
@@ -1304,7 +1125,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1304
1125
|
): Promise<Record<string, unknown> | null> {
|
|
1305
1126
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1306
1127
|
const table = getTableForCollection(collection, this.registry);
|
|
1307
|
-
const idInfoArray =
|
|
1128
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1308
1129
|
const idInfo = idInfoArray[0];
|
|
1309
1130
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1310
1131
|
|
|
@@ -1330,7 +1151,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1330
1151
|
|
|
1331
1152
|
if (!row) return null;
|
|
1332
1153
|
|
|
1333
|
-
const restRow =
|
|
1154
|
+
const restRow = toRestRow(row, collection, this.registry);
|
|
1334
1155
|
|
|
1335
1156
|
// Drizzle relational query API doesn't resolve joinPath relations, fetch manually
|
|
1336
1157
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
@@ -1412,7 +1233,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1412
1233
|
): Promise<Record<string, unknown>[]> {
|
|
1413
1234
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
1414
1235
|
const table = getTableForCollection(collection, this.registry);
|
|
1415
|
-
const idInfoArray =
|
|
1236
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
1416
1237
|
const idInfo = idInfoArray[0];
|
|
1417
1238
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1418
1239
|
|
|
@@ -383,8 +383,15 @@ export class PersistService {
|
|
|
383
383
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
// Fetch the
|
|
387
|
-
|
|
386
|
+
// Fetch the saved row back through the same walk `GET /:id` serves, so a
|
|
387
|
+
// write's answer is the read that follows it. This used to be `fetchOne`
|
|
388
|
+
// — the admin view-model walk — whose `__type: "relation"` refs then
|
|
389
|
+
// leaked into REST responses, afterSave callbacks, history records and
|
|
390
|
+
// app-level realtime payloads, none of which see that shape from any
|
|
391
|
+
// read path. The admin does not need them here either: its rows arrive
|
|
392
|
+
// over the realtime subscription refetch, which still serves the
|
|
393
|
+
// view-model walk.
|
|
394
|
+
const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, undefined, databaseId);
|
|
388
395
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
389
396
|
return finalEntity;
|
|
390
397
|
}
|