@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g06dbe5b
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 +49 -2
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +40 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-CBEOeYqa.js} +1118 -220
- package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-Bww-Lg0s.js +8999 -0
- package/dist/backup-service-Bww-Lg0s.js.map +1 -0
- package/dist/cli-helpers.d.ts +96 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js +57 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js.map +1 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js +800 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
- package/dist/index.es.js +2460 -10233
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +185 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +23 -25
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +99 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +185 -3
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +38 -8
- package/dist/services/RelationService.d.ts +54 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +62 -2
- package/dist/services/dataService.d.ts +8 -1
- package/dist/services/realtimeService.d.ts +131 -21
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +129 -6
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D1qbmLZ2.js +587 -0
- package/dist/websocket-D1qbmLZ2.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +176 -6
- package/src/PostgresBootstrapper.ts +245 -37
- package/src/auth/ensure-tables.ts +357 -81
- package/src/auth/services.ts +93 -7
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +199 -10
- package/src/cli.ts +276 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +129 -20
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +46 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +12 -63
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +135 -76
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +572 -42
- package/src/schema/generate-drizzle-schema-logic.ts +169 -43
- package/src/schema/generate-postgres-ddl-logic.ts +670 -29
- package/src/schema/generate-postgres-ddl.ts +25 -2
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +446 -100
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +11 -3
- package/src/security/rls-enforcement.ts +149 -6
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +265 -122
- package/src/services/PersistService.ts +57 -7
- package/src/services/RelationService.ts +358 -114
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +123 -4
- package/src/services/dataService.ts +8 -1
- package/src/services/realtimeService.ts +346 -81
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +396 -29
- package/src/utils/pg-error-utils.ts +73 -3
- package/src/websocket.ts +130 -24
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
1
|
+
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
6
|
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
-
import { DrizzleConditionBuilder, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
|
+
import { DrizzleConditionBuilder, getUnknownFilterFieldsMode, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
8
8
|
import {
|
|
9
9
|
getCollectionByPath,
|
|
10
10
|
getTableForCollection,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
requirePrimaryKeys,
|
|
13
13
|
deriveRowAddress,
|
|
14
14
|
parseIdValues,
|
|
15
|
+
idCanAddressTable,
|
|
15
16
|
buildCompositeId,
|
|
16
17
|
COMPOSITE_ID_SEPARATOR
|
|
17
18
|
} from "./collection-helpers";
|
|
@@ -20,9 +21,11 @@ import { RelationService } from "./RelationService";
|
|
|
20
21
|
import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
|
|
21
22
|
import { DrizzleClient } from "../interfaces";
|
|
22
23
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
23
|
-
import {
|
|
24
|
+
import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
25
|
+
import { visibleColumnProjection, hiddenColumnsOption } from "../schema/search-column";
|
|
24
26
|
import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
|
|
25
|
-
import { logger } from "@rebasepro/server";
|
|
27
|
+
import { ApiError, logger } from "@rebasepro/server";
|
|
28
|
+
import { reachedDatabase } from "../utils/pg-error-utils";
|
|
26
29
|
|
|
27
30
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
28
31
|
type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
|
|
@@ -117,6 +120,31 @@ export class FetchService {
|
|
|
117
120
|
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
118
121
|
* caller that hands over no collection to resolve against.
|
|
119
122
|
*/
|
|
123
|
+
/**
|
|
124
|
+
* The ORDER BY target, which may be relevance rather than a column.
|
|
125
|
+
*
|
|
126
|
+
* `_score` is only meaningful for a collection that declared a `search`
|
|
127
|
+
* block *and* for a request that carried a search string — ranking rows
|
|
128
|
+
* against no query ranks them all at zero. Outside those two conditions it
|
|
129
|
+
* is an unknown field and gets the same 400 as any other typo, which is the
|
|
130
|
+
* behaviour that matters: a sort that is silently dropped returns 200 with
|
|
131
|
+
* rows in arbitrary order, and paging over that repeats and skips rows.
|
|
132
|
+
*/
|
|
133
|
+
static readonly SCORE_FIELD = "_score";
|
|
134
|
+
|
|
135
|
+
private resolveOrderTarget(
|
|
136
|
+
table: PgTable<any>,
|
|
137
|
+
orderBy: string,
|
|
138
|
+
collection?: CollectionConfig,
|
|
139
|
+
searchString?: string
|
|
140
|
+
): AnyPgColumn | SQL | undefined {
|
|
141
|
+
if (orderBy === FetchService.SCORE_FIELD && collection && searchString) {
|
|
142
|
+
const rank = DrizzleConditionBuilder.buildSearchRankExpression(searchString, table, collection);
|
|
143
|
+
if (rank) return rank;
|
|
144
|
+
}
|
|
145
|
+
return this.resolveOrderByField(table, orderBy, collection);
|
|
146
|
+
}
|
|
147
|
+
|
|
120
148
|
private resolveOrderByField(
|
|
121
149
|
table: PgTable<any>,
|
|
122
150
|
orderBy: string,
|
|
@@ -129,12 +157,10 @@ export class FetchService {
|
|
|
129
157
|
if (direct) return direct;
|
|
130
158
|
|
|
131
159
|
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (foreignKey) return foreignKey;
|
|
137
|
-
}
|
|
160
|
+
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : undefined;
|
|
161
|
+
if (declaredRelation?.kind === "belongsTo") {
|
|
162
|
+
const foreignKey = columnAt(declaredRelation.localKey);
|
|
163
|
+
if (foreignKey) return foreignKey;
|
|
138
164
|
}
|
|
139
165
|
|
|
140
166
|
// No collection in hand — the two shapes an owning relation's key takes
|
|
@@ -145,7 +171,61 @@ export class FetchService {
|
|
|
145
171
|
if (foreignKey) return foreignKey;
|
|
146
172
|
}
|
|
147
173
|
|
|
148
|
-
|
|
174
|
+
// Nothing resolved. Returning `undefined` is exactly what the docblock
|
|
175
|
+
// above describes: the caller drops the ORDER BY and hands back rows in
|
|
176
|
+
// whatever order Postgres pleases, while the requester believes they
|
|
177
|
+
// are sorted. `?orderBy=titel` answered 200 with unsorted data and no
|
|
178
|
+
// hint that the sort had been ignored.
|
|
179
|
+
//
|
|
180
|
+
// A *filter* naming a field that does not exist is already refused for
|
|
181
|
+
// precisely this reason — it "used to widen results silently". An
|
|
182
|
+
// unresolvable sort field is the same drift between a query and the
|
|
183
|
+
// schema, so it answers the same way and honours the same switch: one
|
|
184
|
+
// knob, because a deployment that wants the lenient behaviour wants it
|
|
185
|
+
// for both.
|
|
186
|
+
const collectionName = collection?.slug ?? collection?.name;
|
|
187
|
+
const onCollection = collectionName ? ` on collection '${collectionName}'` : "";
|
|
188
|
+
|
|
189
|
+
// A declared to-many relation is a different mistake from a typo, and
|
|
190
|
+
// saying "unknown field" about a field the collection plainly declares
|
|
191
|
+
// sends the reader looking for a spelling error that is not there.
|
|
192
|
+
// There is simply no single value per row to order by — `posts.tags` is
|
|
193
|
+
// a set — so no ORDER BY exists to write, with or without a typo.
|
|
194
|
+
if (declaredRelation && declaredRelation.kind !== "belongsTo") {
|
|
195
|
+
throw ApiError.badRequest(
|
|
196
|
+
`Cannot sort by '${orderBy}'${onCollection}: it is a to-many relation ` +
|
|
197
|
+
`(${declaredRelation.kind}), which has no single value per row to order by.`,
|
|
198
|
+
"ORDER_BY_FIELD_NOT_SORTABLE",
|
|
199
|
+
{ field: orderBy, kind: declaredRelation.kind, ...(collectionName && { collection: collectionName }) }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (getUnknownFilterFieldsMode() === "warn") {
|
|
204
|
+
logger.warn(
|
|
205
|
+
`Sorting by field '${orderBy}'${onCollection}, but it does not exist in the table — ` +
|
|
206
|
+
"the ORDER BY was dropped and these rows are unsorted."
|
|
207
|
+
);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let validFields: string[] = [];
|
|
212
|
+
try {
|
|
213
|
+
validFields = Object.keys(getTableColumns(table)).sort();
|
|
214
|
+
} catch {
|
|
215
|
+
// A table stand-in without Drizzle's column symbols — the message
|
|
216
|
+
// is worth less without the list, but not worth failing over.
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw ApiError.badRequest(
|
|
220
|
+
`Unknown orderBy field '${orderBy}'${onCollection}` +
|
|
221
|
+
(validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""),
|
|
222
|
+
"UNKNOWN_ORDER_BY_FIELD",
|
|
223
|
+
{
|
|
224
|
+
field: orderBy,
|
|
225
|
+
...(collectionName && { collection: collectionName }),
|
|
226
|
+
...(validFields.length > 0 && { validFields })
|
|
227
|
+
}
|
|
228
|
+
);
|
|
149
229
|
}
|
|
150
230
|
|
|
151
231
|
/**
|
|
@@ -353,6 +433,15 @@ export class FetchService {
|
|
|
353
433
|
): Record<string, unknown> {
|
|
354
434
|
const queryOpts: Record<string, unknown> = {};
|
|
355
435
|
|
|
436
|
+
// Same exclusion the `db.select` fallback applies, in the shape the
|
|
437
|
+
// relational query builder takes. Both paths serve the same request, so
|
|
438
|
+
// a row must not carry the search column down one and not the other.
|
|
439
|
+
const hidden = hiddenColumnsOption(
|
|
440
|
+
getTableColumns(table),
|
|
441
|
+
this.registry.getCollectionByPath(collectionPath) ?? undefined
|
|
442
|
+
);
|
|
443
|
+
if (hidden) queryOpts.columns = hidden;
|
|
444
|
+
|
|
356
445
|
if (withConfig) queryOpts.with = withConfig;
|
|
357
446
|
|
|
358
447
|
// Build where conditions
|
|
@@ -363,7 +452,7 @@ export class FetchService {
|
|
|
363
452
|
if (options.searchString) {
|
|
364
453
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
365
454
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
366
|
-
options.searchString, collection.properties, table
|
|
455
|
+
options.searchString, collection.properties, table, collection
|
|
367
456
|
);
|
|
368
457
|
if (searchConditions.length === 0) {
|
|
369
458
|
// Return options that will produce empty results
|
|
@@ -397,7 +486,7 @@ export class FetchService {
|
|
|
397
486
|
const orderExpressions: unknown[] = [];
|
|
398
487
|
if (options.orderBy) {
|
|
399
488
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
400
|
-
const orderByField = this.
|
|
489
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
401
490
|
if (orderByField) {
|
|
402
491
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
403
492
|
}
|
|
@@ -431,6 +520,21 @@ export class FetchService {
|
|
|
431
520
|
const cursor = options.startAfter;
|
|
432
521
|
|
|
433
522
|
if (options.orderBy) {
|
|
523
|
+
// Relevance is computed per query, not stored, so there is no value
|
|
524
|
+
// on the cursor row to compare a later page against — and two
|
|
525
|
+
// requests with different search strings would produce scores that
|
|
526
|
+
// are not on the same scale at all. Refusing is the only honest
|
|
527
|
+
// answer: a dropped cursor condition silently repeats and skips
|
|
528
|
+
// rows, which is precisely what paging exists to prevent.
|
|
529
|
+
if (options.orderBy === FetchService.SCORE_FIELD) {
|
|
530
|
+
throw ApiError.badRequest(
|
|
531
|
+
"Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. " +
|
|
532
|
+
"Relevance is computed per query rather than stored, so it cannot key a cursor. " +
|
|
533
|
+
"Use `limit`/`offset` for relevance-ordered pages, or order by a column.",
|
|
534
|
+
"SCORE_CURSOR_UNSUPPORTED",
|
|
535
|
+
{ field: FetchService.SCORE_FIELD }
|
|
536
|
+
);
|
|
537
|
+
}
|
|
434
538
|
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : undefined;
|
|
435
539
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
436
540
|
if (orderByField) {
|
|
@@ -536,6 +640,11 @@ idColumn };
|
|
|
536
640
|
throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
537
641
|
}
|
|
538
642
|
|
|
643
|
+
// An address the key columns cannot hold names no row — the same answer
|
|
644
|
+
// as a well-formed id nobody has. Asking Postgres instead raises 22P02
|
|
645
|
+
// and aborts the transaction around this read.
|
|
646
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return undefined;
|
|
647
|
+
|
|
539
648
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
540
649
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
541
650
|
|
|
@@ -548,14 +657,17 @@ idColumn };
|
|
|
548
657
|
try {
|
|
549
658
|
const withConfig = this.buildWithConfig(collection);
|
|
550
659
|
|
|
660
|
+
const hidden = hiddenColumnsOption(getTableColumns(table), collection);
|
|
661
|
+
|
|
551
662
|
const row = await qb.findFirst({
|
|
552
663
|
where: eq(idField, parsedId),
|
|
553
|
-
with: withConfig
|
|
664
|
+
with: withConfig,
|
|
665
|
+
...(hidden ? { columns: hidden } : {})
|
|
554
666
|
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
555
667
|
|
|
556
668
|
if (!row) return undefined;
|
|
557
669
|
|
|
558
|
-
const flatRow =
|
|
670
|
+
const flatRow = toFlatRow(row, collection, this.registry);
|
|
559
671
|
|
|
560
672
|
// Post-fetch joinPath relations that Drizzle's `with` can't express
|
|
561
673
|
await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
@@ -566,13 +678,15 @@ idColumn };
|
|
|
566
678
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
567
679
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
568
680
|
}
|
|
681
|
+
if (reachedDatabase(e)) throw e;
|
|
569
682
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
570
683
|
}
|
|
571
684
|
}
|
|
572
685
|
|
|
573
686
|
// Fallback: db.select + N+1 relation loading
|
|
687
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
574
688
|
const result = await this.db
|
|
575
|
-
.select()
|
|
689
|
+
.select(visibleOne as never)
|
|
576
690
|
.from(table)
|
|
577
691
|
.where(eq(idField, parsedId))
|
|
578
692
|
.limit(1);
|
|
@@ -640,6 +754,7 @@ idColumn };
|
|
|
640
754
|
offset?: number;
|
|
641
755
|
startAfter?: Record<string, unknown>;
|
|
642
756
|
searchString?: string;
|
|
757
|
+
searchExplain?: boolean;
|
|
643
758
|
databaseId?: string;
|
|
644
759
|
vectorSearch?: VectorSearchParams;
|
|
645
760
|
logical?: LogicalCondition;
|
|
@@ -682,7 +797,7 @@ idColumn };
|
|
|
682
797
|
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
683
798
|
|
|
684
799
|
const rows = (results as Record<string, unknown>[]).map(row =>
|
|
685
|
-
|
|
800
|
+
toFlatRow(row, collection, this.registry)
|
|
686
801
|
);
|
|
687
802
|
|
|
688
803
|
return rows;
|
|
@@ -691,6 +806,7 @@ idColumn };
|
|
|
691
806
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
692
807
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
693
808
|
}
|
|
809
|
+
if (reachedDatabase(e)) throw e;
|
|
694
810
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
695
811
|
}
|
|
696
812
|
}
|
|
@@ -702,17 +818,41 @@ idColumn };
|
|
|
702
818
|
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
703
819
|
}
|
|
704
820
|
|
|
821
|
+
// A generated search column is an index in column form; `SELECT *`
|
|
822
|
+
// would ship it to every caller. The projection is undefined — and the
|
|
823
|
+
// SQL therefore unchanged — for any table without one.
|
|
824
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
825
|
+
|
|
826
|
+
// Relevance, alongside the row, exactly as `_distance` rides along with
|
|
827
|
+
// a vector search. Present only when the collection opted in and the
|
|
828
|
+
// request carried a search string, so a caller can order by it, show
|
|
829
|
+
// it, or blend it with a score of their own.
|
|
830
|
+
const rankSelect = options.searchString
|
|
831
|
+
? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection)
|
|
832
|
+
: undefined;
|
|
833
|
+
|
|
834
|
+
// Only when asked: a `ts_headline` per declared field per row.
|
|
835
|
+
const matchesSelect = options.searchString && options.searchExplain
|
|
836
|
+
? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection)
|
|
837
|
+
: undefined;
|
|
838
|
+
|
|
705
839
|
let query = vectorMeta
|
|
706
|
-
? this.db.select({ table_row: table,
|
|
840
|
+
? this.db.select({ table_row: (visible ?? table) as never,
|
|
707
841
|
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
708
|
-
:
|
|
842
|
+
: rankSelect
|
|
843
|
+
? this.db.select({
|
|
844
|
+
table_row: (visible ?? table) as never,
|
|
845
|
+
_score: rankSelect,
|
|
846
|
+
...(matchesSelect ? { _matches: matchesSelect } : {})
|
|
847
|
+
}).from(table).$dynamic()
|
|
848
|
+
: (visible ? this.db.select(visible as never).from(table).$dynamic() : this.db.select().from(table).$dynamic());
|
|
709
849
|
const allConditions: SQL[] = [];
|
|
710
850
|
|
|
711
851
|
if (scopeCondition) allConditions.push(scopeCondition);
|
|
712
852
|
|
|
713
853
|
if (options.searchString) {
|
|
714
854
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
715
|
-
options.searchString, collection.properties, table
|
|
855
|
+
options.searchString, collection.properties, table, collection
|
|
716
856
|
);
|
|
717
857
|
if (searchConditions.length === 0) return [];
|
|
718
858
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -743,7 +883,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
743
883
|
if (vectorMeta) {
|
|
744
884
|
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
745
885
|
} else if (options.orderBy) {
|
|
746
|
-
const orderByField = this.
|
|
886
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
747
887
|
if (orderByField) {
|
|
748
888
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
749
889
|
}
|
|
@@ -777,7 +917,15 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
777
917
|
...r.table_row,
|
|
778
918
|
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
779
919
|
}))
|
|
780
|
-
|
|
920
|
+
// Same nested shape, unwrapped the same way, when a relevance
|
|
921
|
+
// score was selected instead.
|
|
922
|
+
: rankSelect
|
|
923
|
+
? (rawResults as { table_row: Record<string, unknown>; _score: unknown; _matches?: unknown }[]).map(r => ({
|
|
924
|
+
...r.table_row,
|
|
925
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
926
|
+
...(matchesSelect ? { _matches: r._matches ?? [] } : {})
|
|
927
|
+
}))
|
|
928
|
+
: rawResults as Record<string, unknown>[];
|
|
781
929
|
|
|
782
930
|
return this.processRowResults<M>(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
783
931
|
}
|
|
@@ -785,7 +933,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
785
933
|
/**
|
|
786
934
|
* Fallback path used when db.query is unavailable.
|
|
787
935
|
*
|
|
788
|
-
* The primary path runs the results through `
|
|
936
|
+
* The primary path runs the results through `toFlatRow`, which maps
|
|
789
937
|
* relations from what drizzle already nested — no query per row. This one
|
|
790
938
|
* has no nesting to read, so it resolves relations itself, in batches.
|
|
791
939
|
*
|
|
@@ -893,6 +1041,16 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
893
1041
|
collectionPath: string,
|
|
894
1042
|
options: {
|
|
895
1043
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1044
|
+
/**
|
|
1045
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
1046
|
+
*
|
|
1047
|
+
* `fetchRowsWithConditions` below has always applied this; it was
|
|
1048
|
+
* simply absent from this signature, so the only callers that could
|
|
1049
|
+
* pass one were the ones that went around this method. Realtime
|
|
1050
|
+
* came through here, which is why a subscription filtered by a
|
|
1051
|
+
* logical group was pushed every row in the table.
|
|
1052
|
+
*/
|
|
1053
|
+
logical?: LogicalCondition;
|
|
896
1054
|
orderBy?: string;
|
|
897
1055
|
order?: "desc" | "asc";
|
|
898
1056
|
limit?: number;
|
|
@@ -923,10 +1081,21 @@ relatedTo: hop });
|
|
|
923
1081
|
searchString: string,
|
|
924
1082
|
options: {
|
|
925
1083
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1084
|
+
/**
|
|
1085
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
1086
|
+
*
|
|
1087
|
+
* `fetchRowsWithConditions` has always applied one; it was missing
|
|
1088
|
+
* from this signature, so a realtime search subscription carrying a
|
|
1089
|
+
* group could not pass it on and served every row matching the text
|
|
1090
|
+
* that RLS allowed.
|
|
1091
|
+
*/
|
|
1092
|
+
logical?: LogicalCondition;
|
|
926
1093
|
orderBy?: string;
|
|
927
1094
|
order?: "desc" | "asc";
|
|
928
1095
|
limit?: number;
|
|
929
1096
|
databaseId?: string;
|
|
1097
|
+
/** Ask each row which declared search field matched. */
|
|
1098
|
+
searchExplain?: boolean;
|
|
930
1099
|
} = {}
|
|
931
1100
|
): Promise<Record<string, unknown>[]> {
|
|
932
1101
|
return this.fetchRowsWithConditions<M>(collectionPath, {
|
|
@@ -942,6 +1111,7 @@ relatedTo: hop });
|
|
|
942
1111
|
collectionPath: string,
|
|
943
1112
|
options: {
|
|
944
1113
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1114
|
+
logical?: LogicalCondition;
|
|
945
1115
|
searchString?: string;
|
|
946
1116
|
databaseId?: string;
|
|
947
1117
|
} = {}
|
|
@@ -962,7 +1132,7 @@ relatedTo: hop });
|
|
|
962
1132
|
|
|
963
1133
|
if (options.searchString) {
|
|
964
1134
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
965
|
-
options.searchString, collection.properties, table
|
|
1135
|
+
options.searchString, collection.properties, table, collection
|
|
966
1136
|
);
|
|
967
1137
|
if (searchConditions.length === 0) return 0;
|
|
968
1138
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -973,6 +1143,13 @@ relatedTo: hop });
|
|
|
973
1143
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
974
1144
|
}
|
|
975
1145
|
|
|
1146
|
+
if (options.logical) {
|
|
1147
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(
|
|
1148
|
+
options.logical, table, effectivePath, this.filterContext(effectivePath, table)
|
|
1149
|
+
);
|
|
1150
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
976
1153
|
if (allConditions.length > 0) {
|
|
977
1154
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
978
1155
|
if (finalCondition) query = query.where(finalCondition);
|
|
@@ -1043,6 +1220,8 @@ relatedTo: hop });
|
|
|
1043
1220
|
collectionPath: string,
|
|
1044
1221
|
options: {
|
|
1045
1222
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1223
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
1224
|
+
logical?: LogicalCondition;
|
|
1046
1225
|
orderBy?: string;
|
|
1047
1226
|
order?: "desc" | "asc";
|
|
1048
1227
|
limit?: number;
|
|
@@ -1111,6 +1290,7 @@ relatedTo: hop }, include
|
|
|
1111
1290
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1112
1291
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
1113
1292
|
}
|
|
1293
|
+
if (reachedDatabase(e)) throw e;
|
|
1114
1294
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
1115
1295
|
}
|
|
1116
1296
|
}
|
|
@@ -1184,6 +1364,9 @@ relatedTo: hop }, include
|
|
|
1184
1364
|
const idInfo = idInfoArray[0];
|
|
1185
1365
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1186
1366
|
|
|
1367
|
+
// See `fetchOne`: an unaddressable id is a 404, not a database error.
|
|
1368
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return null;
|
|
1369
|
+
|
|
1187
1370
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
1188
1371
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
1189
1372
|
|
|
@@ -1217,13 +1400,15 @@ relatedTo: hop }, include
|
|
|
1217
1400
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1218
1401
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
1219
1402
|
}
|
|
1403
|
+
if (reachedDatabase(e)) throw e;
|
|
1220
1404
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
1221
1405
|
}
|
|
1222
1406
|
}
|
|
1223
1407
|
|
|
1224
1408
|
// Fallback: db.select + N+1 relation loading
|
|
1409
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
1225
1410
|
const result = await this.db
|
|
1226
|
-
.select()
|
|
1411
|
+
.select(visibleOne as never)
|
|
1227
1412
|
.from(table)
|
|
1228
1413
|
.where(eq(idField, parsedId))
|
|
1229
1414
|
.limit(1);
|
|
@@ -1277,12 +1462,26 @@ relatedTo: hop }, include
|
|
|
1277
1462
|
collectionPath: string,
|
|
1278
1463
|
options: {
|
|
1279
1464
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1465
|
+
/**
|
|
1466
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
1467
|
+
*
|
|
1468
|
+
* Declared *and applied*, because this is the path every REST search
|
|
1469
|
+
* and vector read takes: `fetchCollectionForRest` skips `db.query`
|
|
1470
|
+
* whenever a `searchString` or a `vectorSearch` is present. The
|
|
1471
|
+
* group arrived here on `options` from the very beginning and was
|
|
1472
|
+
* simply never read, so `?searchString=x&or=(...)` served every row
|
|
1473
|
+
* matching `x` that RLS allowed — while `count` (which does apply
|
|
1474
|
+
* it) reported the narrowed total, so `meta.total` and `data`
|
|
1475
|
+
* described different sets of rows.
|
|
1476
|
+
*/
|
|
1477
|
+
logical?: LogicalCondition;
|
|
1280
1478
|
orderBy?: string;
|
|
1281
1479
|
order?: "desc" | "asc";
|
|
1282
1480
|
limit?: number;
|
|
1283
1481
|
offset?: number;
|
|
1284
1482
|
startAfter?: Record<string, unknown>;
|
|
1285
1483
|
searchString?: string;
|
|
1484
|
+
searchExplain?: boolean;
|
|
1286
1485
|
vectorSearch?: VectorSearchParams;
|
|
1287
1486
|
relatedTo?: NestedPathHop;
|
|
1288
1487
|
} = {}
|
|
@@ -1298,17 +1497,41 @@ relatedTo: hop }, include
|
|
|
1298
1497
|
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
1299
1498
|
}
|
|
1300
1499
|
|
|
1500
|
+
// A generated search column is an index in column form; `SELECT *`
|
|
1501
|
+
// would ship it to every caller. The projection is undefined — and the
|
|
1502
|
+
// SQL therefore unchanged — for any table without one.
|
|
1503
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
1504
|
+
|
|
1505
|
+
// Relevance, alongside the row, exactly as `_distance` rides along with
|
|
1506
|
+
// a vector search. Present only when the collection opted in and the
|
|
1507
|
+
// request carried a search string, so a caller can order by it, show
|
|
1508
|
+
// it, or blend it with a score of their own.
|
|
1509
|
+
const rankSelect = options.searchString
|
|
1510
|
+
? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection)
|
|
1511
|
+
: undefined;
|
|
1512
|
+
|
|
1513
|
+
// Only when asked: a `ts_headline` per declared field per row.
|
|
1514
|
+
const matchesSelect = options.searchString && options.searchExplain
|
|
1515
|
+
? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection)
|
|
1516
|
+
: undefined;
|
|
1517
|
+
|
|
1301
1518
|
let query = vectorMeta
|
|
1302
|
-
? this.db.select({ table_row: table,
|
|
1519
|
+
? this.db.select({ table_row: (visible ?? table) as never,
|
|
1303
1520
|
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
1304
|
-
:
|
|
1521
|
+
: rankSelect
|
|
1522
|
+
? this.db.select({
|
|
1523
|
+
table_row: (visible ?? table) as never,
|
|
1524
|
+
_score: rankSelect,
|
|
1525
|
+
...(matchesSelect ? { _matches: matchesSelect } : {})
|
|
1526
|
+
}).from(table).$dynamic()
|
|
1527
|
+
: (visible ? this.db.select(visible as never).from(table).$dynamic() : this.db.select().from(table).$dynamic());
|
|
1305
1528
|
const allConditions: SQL[] = [];
|
|
1306
1529
|
|
|
1307
1530
|
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
1308
1531
|
|
|
1309
1532
|
if (options.searchString) {
|
|
1310
1533
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
1311
|
-
options.searchString, collection.properties, table
|
|
1534
|
+
options.searchString, collection.properties, table, collection
|
|
1312
1535
|
);
|
|
1313
1536
|
if (searchConditions.length === 0) return [];
|
|
1314
1537
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -1319,6 +1542,11 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1319
1542
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
1320
1543
|
}
|
|
1321
1544
|
|
|
1545
|
+
if (options.logical) {
|
|
1546
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
1547
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1322
1550
|
if (vectorMeta?.filter) {
|
|
1323
1551
|
allConditions.push(vectorMeta.filter);
|
|
1324
1552
|
}
|
|
@@ -1332,7 +1560,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1332
1560
|
if (vectorMeta) {
|
|
1333
1561
|
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
1334
1562
|
} else if (options.orderBy) {
|
|
1335
|
-
const orderByField = this.
|
|
1563
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
1336
1564
|
if (orderByField) {
|
|
1337
1565
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
1338
1566
|
}
|
|
@@ -1357,6 +1585,14 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1357
1585
|
}));
|
|
1358
1586
|
}
|
|
1359
1587
|
|
|
1588
|
+
if (rankSelect) {
|
|
1589
|
+
return (rawResults as { table_row: Record<string, unknown>; _score: unknown; _matches?: unknown }[]).map(r => ({
|
|
1590
|
+
...r.table_row,
|
|
1591
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
1592
|
+
...(matchesSelect ? { _matches: r._matches ?? [] } : {})
|
|
1593
|
+
}));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1360
1596
|
return rawResults as Record<string, unknown>[];
|
|
1361
1597
|
}
|
|
1362
1598
|
|
|
@@ -1378,99 +1614,6 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1378
1614
|
return !!this.getQueryBuilder(tableName);
|
|
1379
1615
|
}
|
|
1380
1616
|
|
|
1381
|
-
/**
|
|
1382
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
1383
|
-
* for efficient JOIN-based relation loading.
|
|
1384
|
-
* Returns null if the API is not available or the query fails.
|
|
1385
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
1386
|
-
*/
|
|
1387
|
-
private async fetchWithDrizzleQuery<M extends Record<string, unknown>>(
|
|
1388
|
-
collectionPath: string,
|
|
1389
|
-
collection: CollectionConfig,
|
|
1390
|
-
options: {
|
|
1391
|
-
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1392
|
-
orderBy?: string;
|
|
1393
|
-
order?: "desc" | "asc";
|
|
1394
|
-
limit?: number;
|
|
1395
|
-
},
|
|
1396
|
-
include: string[],
|
|
1397
|
-
idInfo: { fieldName: string; type: "string" | "number" },
|
|
1398
|
-
idInfoArray?: { fieldName: string; type: "string" | "number" }[]
|
|
1399
|
-
): Promise<Record<string, unknown>[] | null> {
|
|
1400
|
-
try {
|
|
1401
|
-
|
|
1402
|
-
const table = getTableForCollection(collection, this.registry);
|
|
1403
|
-
const tableName = getTableName(table);
|
|
1404
|
-
const queryTarget = this.getQueryBuilder(tableName);
|
|
1405
|
-
|
|
1406
|
-
if (!queryTarget?.findMany) return null;
|
|
1407
|
-
|
|
1408
|
-
// Build the `with` config from include array
|
|
1409
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
1410
|
-
const withConfig: Record<string, boolean> = {};
|
|
1411
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
1412
|
-
if (include[0] === "*" || include.includes(key)) {
|
|
1413
|
-
// Use the Drizzle relation name (from the schema)
|
|
1414
|
-
const drizzleRelName = relation.relationName || key;
|
|
1415
|
-
withConfig[drizzleRelName] = true;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Build query options
|
|
1420
|
-
const queryOpts: Record<string, unknown> = { with: withConfig };
|
|
1421
|
-
if (options.limit) queryOpts.limit = options.limit;
|
|
1422
|
-
|
|
1423
|
-
// Build where clause
|
|
1424
|
-
if (options.filter) {
|
|
1425
|
-
const filterConditions = this.buildFilterConditions(
|
|
1426
|
-
options.filter, table, collectionPath
|
|
1427
|
-
);
|
|
1428
|
-
if (filterConditions.length > 0) {
|
|
1429
|
-
queryOpts.where = and(...filterConditions);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Build orderBy
|
|
1434
|
-
if (options.orderBy) {
|
|
1435
|
-
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
1436
|
-
if (orderByField) {
|
|
1437
|
-
queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const results = await queryTarget.findMany(queryOpts as Parameters<NonNullable<typeof queryTarget>["findMany"]>[0]);
|
|
1443
|
-
|
|
1444
|
-
// Inline the nested Drizzle results, columns only — no synthesized id.
|
|
1445
|
-
return results.map((row: Record<string, unknown>) => {
|
|
1446
|
-
const flat: Record<string, unknown> = {};
|
|
1447
|
-
for (const [k, v] of Object.entries(row)) {
|
|
1448
|
-
if (Array.isArray(v)) {
|
|
1449
|
-
// Many relation — inline each nested row
|
|
1450
|
-
flat[k] = v.map((item: Record<string, unknown>) => {
|
|
1451
|
-
// Junction table rows may have the target nested, unwrap those
|
|
1452
|
-
const keys = Object.keys(item);
|
|
1453
|
-
const nestedObj = keys.find(nk => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
1454
|
-
if (nestedObj && keys.length <= 3) {
|
|
1455
|
-
return { ...(item[nestedObj] as Record<string, unknown>) };
|
|
1456
|
-
}
|
|
1457
|
-
return { ...item };
|
|
1458
|
-
});
|
|
1459
|
-
} else if (typeof v === "object" && v !== null) {
|
|
1460
|
-
// One-to-one relation — inline the target's columns
|
|
1461
|
-
flat[k] = { ...(v as Record<string, unknown>) };
|
|
1462
|
-
} else {
|
|
1463
|
-
flat[k] = v;
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
return flat;
|
|
1467
|
-
});
|
|
1468
|
-
} catch (e) {
|
|
1469
|
-
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
1470
|
-
return null;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
1617
|
/**
|
|
1475
1618
|
* Fallback path used when db.query is unavailable.
|
|
1476
1619
|
* The primary path uses db.query.findMany with `with` config, which
|