@rebasepro/server-postgres 0.13.1-canary.gef9608c → 0.14.0
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 +48 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +56 -0
- package/dist/cli-output.d.ts +34 -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-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1720 -946
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- 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 +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +48 -0
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +24 -0
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +3 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +139 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +8 -7
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +124 -11
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +299 -168
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +129 -25
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +366 -30
- package/src/schema/generate-drizzle-schema-logic.ts +146 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +277 -10
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +14 -7
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- 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 +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +243 -22
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +3 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +347 -86
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
- package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
- package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-CU6WZGYV.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -2,8 +2,8 @@ import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, lt, or, S
|
|
|
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
|
-
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
|
-
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
5
|
+
import { resolveCollectionRelations, findRelation, fieldKeyForColumn, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
|
+
import { generateForeignKeyName, toWireKey } from "@rebasepro/utils";
|
|
7
7
|
import { DrizzleConditionBuilder, getUnknownFilterFieldsMode, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
8
8
|
import {
|
|
9
9
|
getCollectionByPath,
|
|
@@ -22,6 +22,7 @@ import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query
|
|
|
22
22
|
import { DrizzleClient } from "../interfaces";
|
|
23
23
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
24
24
|
import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
25
|
+
import { visibleColumnProjection, hiddenColumnsOption } from "../schema/search-column";
|
|
25
26
|
import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
|
|
26
27
|
import { ApiError, logger } from "@rebasepro/server";
|
|
27
28
|
import { reachedDatabase } from "../utils/pg-error-utils";
|
|
@@ -119,6 +120,31 @@ export class FetchService {
|
|
|
119
120
|
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
120
121
|
* caller that hands over no collection to resolve against.
|
|
121
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
|
+
|
|
122
148
|
private resolveOrderByField(
|
|
123
149
|
table: PgTable<any>,
|
|
124
150
|
orderBy: string,
|
|
@@ -133,14 +159,21 @@ export class FetchService {
|
|
|
133
159
|
// Owning relation, resolved: the relation names its own local key.
|
|
134
160
|
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : undefined;
|
|
135
161
|
if (declaredRelation?.kind === "belongsTo") {
|
|
136
|
-
|
|
162
|
+
// `localKey` is the column; the table is keyed by the wire name.
|
|
163
|
+
const foreignKey = columnAt(fieldKeyForColumn(collection, declaredRelation.localKey));
|
|
137
164
|
if (foreignKey) return foreignKey;
|
|
138
165
|
}
|
|
139
166
|
|
|
140
|
-
// No collection in hand — the
|
|
141
|
-
//
|
|
142
|
-
// `
|
|
143
|
-
|
|
167
|
+
// No collection in hand — the shapes an owning relation's key takes by
|
|
168
|
+
// default (e.g. `project` → `projectId`, `userProfile` →
|
|
169
|
+
// `userProfileId`), then the snake forms for a project that authored the
|
|
170
|
+
// property under its column name.
|
|
171
|
+
for (const guess of [
|
|
172
|
+
`${orderBy}Id`,
|
|
173
|
+
toWireKey(generateForeignKeyName(orderBy)),
|
|
174
|
+
`${orderBy}_id`,
|
|
175
|
+
generateForeignKeyName(orderBy)
|
|
176
|
+
]) {
|
|
144
177
|
const foreignKey = columnAt(guess);
|
|
145
178
|
if (foreignKey) return foreignKey;
|
|
146
179
|
}
|
|
@@ -300,6 +333,14 @@ export class FetchService {
|
|
|
300
333
|
);
|
|
301
334
|
}
|
|
302
335
|
} catch (e) {
|
|
336
|
+
// A relation that failed to load is not a relation that is absent.
|
|
337
|
+
// Without this the request answers 200 with the field quietly
|
|
338
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
339
|
+
// transaction, every later relation in the same request is
|
|
340
|
+
// swallowed too, so one failure becomes a response missing
|
|
341
|
+
// several fields. Same guard the four other catches in this file
|
|
342
|
+
// already use.
|
|
343
|
+
if (reachedDatabase(e)) throw e;
|
|
303
344
|
logger.warn(`Could not resolve joinPath relation '${key}'`, { error: e });
|
|
304
345
|
}
|
|
305
346
|
});
|
|
@@ -378,6 +419,14 @@ export class FetchService {
|
|
|
378
419
|
}
|
|
379
420
|
}
|
|
380
421
|
} catch (e) {
|
|
422
|
+
// A relation that failed to load is not a relation that is absent.
|
|
423
|
+
// Without this the request answers 200 with the field quietly
|
|
424
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
425
|
+
// transaction, every later relation in the same request is
|
|
426
|
+
// swallowed too, so one failure becomes a response missing
|
|
427
|
+
// several fields. Same guard the four other catches in this file
|
|
428
|
+
// already use.
|
|
429
|
+
if (reachedDatabase(e)) throw e;
|
|
381
430
|
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
382
431
|
}
|
|
383
432
|
}
|
|
@@ -407,6 +456,15 @@ export class FetchService {
|
|
|
407
456
|
): Record<string, unknown> {
|
|
408
457
|
const queryOpts: Record<string, unknown> = {};
|
|
409
458
|
|
|
459
|
+
// Same exclusion the `db.select` fallback applies, in the shape the
|
|
460
|
+
// relational query builder takes. Both paths serve the same request, so
|
|
461
|
+
// a row must not carry the search column down one and not the other.
|
|
462
|
+
const hidden = hiddenColumnsOption(
|
|
463
|
+
getTableColumns(table),
|
|
464
|
+
this.registry.getCollectionByPath(collectionPath) ?? undefined
|
|
465
|
+
);
|
|
466
|
+
if (hidden) queryOpts.columns = hidden;
|
|
467
|
+
|
|
410
468
|
if (withConfig) queryOpts.with = withConfig;
|
|
411
469
|
|
|
412
470
|
// Build where conditions
|
|
@@ -417,7 +475,7 @@ export class FetchService {
|
|
|
417
475
|
if (options.searchString) {
|
|
418
476
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
419
477
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
420
|
-
options.searchString, collection.properties, table
|
|
478
|
+
options.searchString, collection.properties, table, collection
|
|
421
479
|
);
|
|
422
480
|
if (searchConditions.length === 0) {
|
|
423
481
|
// Return options that will produce empty results
|
|
@@ -451,7 +509,7 @@ export class FetchService {
|
|
|
451
509
|
const orderExpressions: unknown[] = [];
|
|
452
510
|
if (options.orderBy) {
|
|
453
511
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
454
|
-
const orderByField = this.
|
|
512
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
455
513
|
if (orderByField) {
|
|
456
514
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
457
515
|
}
|
|
@@ -485,6 +543,21 @@ export class FetchService {
|
|
|
485
543
|
const cursor = options.startAfter;
|
|
486
544
|
|
|
487
545
|
if (options.orderBy) {
|
|
546
|
+
// Relevance is computed per query, not stored, so there is no value
|
|
547
|
+
// on the cursor row to compare a later page against — and two
|
|
548
|
+
// requests with different search strings would produce scores that
|
|
549
|
+
// are not on the same scale at all. Refusing is the only honest
|
|
550
|
+
// answer: a dropped cursor condition silently repeats and skips
|
|
551
|
+
// rows, which is precisely what paging exists to prevent.
|
|
552
|
+
if (options.orderBy === FetchService.SCORE_FIELD) {
|
|
553
|
+
throw ApiError.badRequest(
|
|
554
|
+
"Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. " +
|
|
555
|
+
"Relevance is computed per query rather than stored, so it cannot key a cursor. " +
|
|
556
|
+
"Use `limit`/`offset` for relevance-ordered pages, or order by a column.",
|
|
557
|
+
"SCORE_CURSOR_UNSUPPORTED",
|
|
558
|
+
{ field: FetchService.SCORE_FIELD }
|
|
559
|
+
);
|
|
560
|
+
}
|
|
488
561
|
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : undefined;
|
|
489
562
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
490
563
|
if (orderByField) {
|
|
@@ -607,9 +680,12 @@ idColumn };
|
|
|
607
680
|
try {
|
|
608
681
|
const withConfig = this.buildWithConfig(collection);
|
|
609
682
|
|
|
683
|
+
const hidden = hiddenColumnsOption(getTableColumns(table), collection);
|
|
684
|
+
|
|
610
685
|
const row = await qb.findFirst({
|
|
611
686
|
where: eq(idField, parsedId),
|
|
612
|
-
with: withConfig
|
|
687
|
+
with: withConfig,
|
|
688
|
+
...(hidden ? { columns: hidden } : {})
|
|
613
689
|
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
614
690
|
|
|
615
691
|
if (!row) return undefined;
|
|
@@ -631,8 +707,9 @@ idColumn };
|
|
|
631
707
|
}
|
|
632
708
|
|
|
633
709
|
// Fallback: db.select + N+1 relation loading
|
|
710
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
634
711
|
const result = await this.db
|
|
635
|
-
.select()
|
|
712
|
+
.select(visibleOne as never)
|
|
636
713
|
.from(table)
|
|
637
714
|
.where(eq(idField, parsedId))
|
|
638
715
|
.limit(1);
|
|
@@ -673,6 +750,14 @@ idColumn };
|
|
|
673
750
|
values[key] = createRelationRef(e.id, e.path);
|
|
674
751
|
}
|
|
675
752
|
} catch (e) {
|
|
753
|
+
// A relation that failed to load is not a relation that is absent.
|
|
754
|
+
// Without this the request answers 200 with the field quietly
|
|
755
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
756
|
+
// transaction, every later relation in the same request is
|
|
757
|
+
// swallowed too, so one failure becomes a response missing
|
|
758
|
+
// several fields. Same guard the four other catches in this file
|
|
759
|
+
// already use.
|
|
760
|
+
if (reachedDatabase(e)) throw e;
|
|
676
761
|
logger.warn(`Could not resolve one-to-one relation property: ${key}`, { error: e });
|
|
677
762
|
}
|
|
678
763
|
}
|
|
@@ -700,6 +785,7 @@ idColumn };
|
|
|
700
785
|
offset?: number;
|
|
701
786
|
startAfter?: Record<string, unknown>;
|
|
702
787
|
searchString?: string;
|
|
788
|
+
searchExplain?: boolean;
|
|
703
789
|
databaseId?: string;
|
|
704
790
|
vectorSearch?: VectorSearchParams;
|
|
705
791
|
logical?: LogicalCondition;
|
|
@@ -763,17 +849,41 @@ idColumn };
|
|
|
763
849
|
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
764
850
|
}
|
|
765
851
|
|
|
852
|
+
// A generated search column is an index in column form; `SELECT *`
|
|
853
|
+
// would ship it to every caller. The projection is undefined — and the
|
|
854
|
+
// SQL therefore unchanged — for any table without one.
|
|
855
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
856
|
+
|
|
857
|
+
// Relevance, alongside the row, exactly as `_distance` rides along with
|
|
858
|
+
// a vector search. Present only when the collection opted in and the
|
|
859
|
+
// request carried a search string, so a caller can order by it, show
|
|
860
|
+
// it, or blend it with a score of their own.
|
|
861
|
+
const rankSelect = options.searchString
|
|
862
|
+
? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection)
|
|
863
|
+
: undefined;
|
|
864
|
+
|
|
865
|
+
// Only when asked: a `ts_headline` per declared field per row.
|
|
866
|
+
const matchesSelect = options.searchString && options.searchExplain
|
|
867
|
+
? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection)
|
|
868
|
+
: undefined;
|
|
869
|
+
|
|
766
870
|
let query = vectorMeta
|
|
767
|
-
? this.db.select({ table_row: table,
|
|
871
|
+
? this.db.select({ table_row: (visible ?? table) as never,
|
|
768
872
|
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
769
|
-
:
|
|
873
|
+
: rankSelect
|
|
874
|
+
? this.db.select({
|
|
875
|
+
table_row: (visible ?? table) as never,
|
|
876
|
+
_score: rankSelect,
|
|
877
|
+
...(matchesSelect ? { _matches: matchesSelect } : {})
|
|
878
|
+
}).from(table).$dynamic()
|
|
879
|
+
: (visible ? this.db.select(visible as never).from(table).$dynamic() : this.db.select().from(table).$dynamic());
|
|
770
880
|
const allConditions: SQL[] = [];
|
|
771
881
|
|
|
772
882
|
if (scopeCondition) allConditions.push(scopeCondition);
|
|
773
883
|
|
|
774
884
|
if (options.searchString) {
|
|
775
885
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
776
|
-
options.searchString, collection.properties, table
|
|
886
|
+
options.searchString, collection.properties, table, collection
|
|
777
887
|
);
|
|
778
888
|
if (searchConditions.length === 0) return [];
|
|
779
889
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -804,7 +914,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
804
914
|
if (vectorMeta) {
|
|
805
915
|
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
806
916
|
} else if (options.orderBy) {
|
|
807
|
-
const orderByField = this.
|
|
917
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
808
918
|
if (orderByField) {
|
|
809
919
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
810
920
|
}
|
|
@@ -838,7 +948,15 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
838
948
|
...r.table_row,
|
|
839
949
|
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
840
950
|
}))
|
|
841
|
-
|
|
951
|
+
// Same nested shape, unwrapped the same way, when a relevance
|
|
952
|
+
// score was selected instead.
|
|
953
|
+
: rankSelect
|
|
954
|
+
? (rawResults as { table_row: Record<string, unknown>; _score: unknown; _matches?: unknown }[]).map(r => ({
|
|
955
|
+
...r.table_row,
|
|
956
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
957
|
+
...(matchesSelect ? { _matches: r._matches ?? [] } : {})
|
|
958
|
+
}))
|
|
959
|
+
: rawResults as Record<string, unknown>[];
|
|
842
960
|
|
|
843
961
|
return this.processRowResults<M>(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
844
962
|
}
|
|
@@ -911,6 +1029,14 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
911
1029
|
}
|
|
912
1030
|
});
|
|
913
1031
|
} catch (e) {
|
|
1032
|
+
// A relation that failed to load is not a relation that is absent.
|
|
1033
|
+
// Without this the request answers 200 with the field quietly
|
|
1034
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
1035
|
+
// transaction, every later relation in the same request is
|
|
1036
|
+
// swallowed too, so one failure becomes a response missing
|
|
1037
|
+
// several fields. Same guard the four other catches in this file
|
|
1038
|
+
// already use.
|
|
1039
|
+
if (reachedDatabase(e)) throw e;
|
|
914
1040
|
logger.warn(`Could not batch load one-to-one relation property: ${key}`, { error: e });
|
|
915
1041
|
}
|
|
916
1042
|
}
|
|
@@ -938,6 +1064,14 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
938
1064
|
);
|
|
939
1065
|
});
|
|
940
1066
|
} catch (e) {
|
|
1067
|
+
// A relation that failed to load is not a relation that is absent.
|
|
1068
|
+
// Without this the request answers 200 with the field quietly
|
|
1069
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
1070
|
+
// transaction, every later relation in the same request is
|
|
1071
|
+
// swallowed too, so one failure becomes a response missing
|
|
1072
|
+
// several fields. Same guard the four other catches in this file
|
|
1073
|
+
// already use.
|
|
1074
|
+
if (reachedDatabase(e)) throw e;
|
|
941
1075
|
logger.warn(`Could not batch load many relation property: ${key}`, { error: e });
|
|
942
1076
|
}
|
|
943
1077
|
}
|
|
@@ -994,10 +1128,21 @@ relatedTo: hop });
|
|
|
994
1128
|
searchString: string,
|
|
995
1129
|
options: {
|
|
996
1130
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1131
|
+
/**
|
|
1132
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
1133
|
+
*
|
|
1134
|
+
* `fetchRowsWithConditions` has always applied one; it was missing
|
|
1135
|
+
* from this signature, so a realtime search subscription carrying a
|
|
1136
|
+
* group could not pass it on and served every row matching the text
|
|
1137
|
+
* that RLS allowed.
|
|
1138
|
+
*/
|
|
1139
|
+
logical?: LogicalCondition;
|
|
997
1140
|
orderBy?: string;
|
|
998
1141
|
order?: "desc" | "asc";
|
|
999
1142
|
limit?: number;
|
|
1000
1143
|
databaseId?: string;
|
|
1144
|
+
/** Ask each row which declared search field matched. */
|
|
1145
|
+
searchExplain?: boolean;
|
|
1001
1146
|
} = {}
|
|
1002
1147
|
): Promise<Record<string, unknown>[]> {
|
|
1003
1148
|
return this.fetchRowsWithConditions<M>(collectionPath, {
|
|
@@ -1034,7 +1179,7 @@ relatedTo: hop });
|
|
|
1034
1179
|
|
|
1035
1180
|
if (options.searchString) {
|
|
1036
1181
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
1037
|
-
options.searchString, collection.properties, table
|
|
1182
|
+
options.searchString, collection.properties, table, collection
|
|
1038
1183
|
);
|
|
1039
1184
|
if (searchConditions.length === 0) return 0;
|
|
1040
1185
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -1226,6 +1371,14 @@ relatedTo: hop }, include
|
|
|
1226
1371
|
}
|
|
1227
1372
|
}
|
|
1228
1373
|
} catch (e) {
|
|
1374
|
+
// A relation that failed to load is not a relation that is absent.
|
|
1375
|
+
// Without this the request answers 200 with the field quietly
|
|
1376
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
1377
|
+
// transaction, every later relation in the same request is
|
|
1378
|
+
// swallowed too, so one failure becomes a response missing
|
|
1379
|
+
// several fields. Same guard the four other catches in this file
|
|
1380
|
+
// already use.
|
|
1381
|
+
if (reachedDatabase(e)) throw e;
|
|
1229
1382
|
logger.warn(`[include] Failed to batch load one-to-one '${key}'`, { error: e });
|
|
1230
1383
|
}
|
|
1231
1384
|
}
|
|
@@ -1242,6 +1395,14 @@ relatedTo: hop }, include
|
|
|
1242
1395
|
(row as Record<string, unknown>)[key] = relatedList;
|
|
1243
1396
|
}
|
|
1244
1397
|
} catch (e) {
|
|
1398
|
+
// A relation that failed to load is not a relation that is absent.
|
|
1399
|
+
// Without this the request answers 200 with the field quietly
|
|
1400
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
1401
|
+
// transaction, every later relation in the same request is
|
|
1402
|
+
// swallowed too, so one failure becomes a response missing
|
|
1403
|
+
// several fields. Same guard the four other catches in this file
|
|
1404
|
+
// already use.
|
|
1405
|
+
if (reachedDatabase(e)) throw e;
|
|
1245
1406
|
logger.warn(`[include] Failed to batch load many '${key}'`, { error: e });
|
|
1246
1407
|
}
|
|
1247
1408
|
}
|
|
@@ -1308,8 +1469,9 @@ relatedTo: hop }, include
|
|
|
1308
1469
|
}
|
|
1309
1470
|
|
|
1310
1471
|
// Fallback: db.select + N+1 relation loading
|
|
1472
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
1311
1473
|
const result = await this.db
|
|
1312
|
-
.select()
|
|
1474
|
+
.select(visibleOne as never)
|
|
1313
1475
|
.from(table)
|
|
1314
1476
|
.where(eq(idField, parsedId))
|
|
1315
1477
|
.limit(1);
|
|
@@ -1349,6 +1511,14 @@ relatedTo: hop }, include
|
|
|
1349
1511
|
}));
|
|
1350
1512
|
}
|
|
1351
1513
|
} catch (e) {
|
|
1514
|
+
// A relation that failed to load is not a relation that is absent.
|
|
1515
|
+
// Without this the request answers 200 with the field quietly
|
|
1516
|
+
// missing — and because a Postgres error poisons the surrounding
|
|
1517
|
+
// transaction, every later relation in the same request is
|
|
1518
|
+
// swallowed too, so one failure becomes a response missing
|
|
1519
|
+
// several fields. Same guard the four other catches in this file
|
|
1520
|
+
// already use.
|
|
1521
|
+
if (reachedDatabase(e)) throw e;
|
|
1352
1522
|
logger.warn(`[include] Failed to load relation '${key}'`, { error: e });
|
|
1353
1523
|
}
|
|
1354
1524
|
}
|
|
@@ -1363,12 +1533,26 @@ relatedTo: hop }, include
|
|
|
1363
1533
|
collectionPath: string,
|
|
1364
1534
|
options: {
|
|
1365
1535
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
1536
|
+
/**
|
|
1537
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
1538
|
+
*
|
|
1539
|
+
* Declared *and applied*, because this is the path every REST search
|
|
1540
|
+
* and vector read takes: `fetchCollectionForRest` skips `db.query`
|
|
1541
|
+
* whenever a `searchString` or a `vectorSearch` is present. The
|
|
1542
|
+
* group arrived here on `options` from the very beginning and was
|
|
1543
|
+
* simply never read, so `?searchString=x&or=(...)` served every row
|
|
1544
|
+
* matching `x` that RLS allowed — while `count` (which does apply
|
|
1545
|
+
* it) reported the narrowed total, so `meta.total` and `data`
|
|
1546
|
+
* described different sets of rows.
|
|
1547
|
+
*/
|
|
1548
|
+
logical?: LogicalCondition;
|
|
1366
1549
|
orderBy?: string;
|
|
1367
1550
|
order?: "desc" | "asc";
|
|
1368
1551
|
limit?: number;
|
|
1369
1552
|
offset?: number;
|
|
1370
1553
|
startAfter?: Record<string, unknown>;
|
|
1371
1554
|
searchString?: string;
|
|
1555
|
+
searchExplain?: boolean;
|
|
1372
1556
|
vectorSearch?: VectorSearchParams;
|
|
1373
1557
|
relatedTo?: NestedPathHop;
|
|
1374
1558
|
} = {}
|
|
@@ -1384,17 +1568,41 @@ relatedTo: hop }, include
|
|
|
1384
1568
|
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
1385
1569
|
}
|
|
1386
1570
|
|
|
1571
|
+
// A generated search column is an index in column form; `SELECT *`
|
|
1572
|
+
// would ship it to every caller. The projection is undefined — and the
|
|
1573
|
+
// SQL therefore unchanged — for any table without one.
|
|
1574
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
1575
|
+
|
|
1576
|
+
// Relevance, alongside the row, exactly as `_distance` rides along with
|
|
1577
|
+
// a vector search. Present only when the collection opted in and the
|
|
1578
|
+
// request carried a search string, so a caller can order by it, show
|
|
1579
|
+
// it, or blend it with a score of their own.
|
|
1580
|
+
const rankSelect = options.searchString
|
|
1581
|
+
? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection)
|
|
1582
|
+
: undefined;
|
|
1583
|
+
|
|
1584
|
+
// Only when asked: a `ts_headline` per declared field per row.
|
|
1585
|
+
const matchesSelect = options.searchString && options.searchExplain
|
|
1586
|
+
? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection)
|
|
1587
|
+
: undefined;
|
|
1588
|
+
|
|
1387
1589
|
let query = vectorMeta
|
|
1388
|
-
? this.db.select({ table_row: table,
|
|
1590
|
+
? this.db.select({ table_row: (visible ?? table) as never,
|
|
1389
1591
|
_distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
1390
|
-
:
|
|
1592
|
+
: rankSelect
|
|
1593
|
+
? this.db.select({
|
|
1594
|
+
table_row: (visible ?? table) as never,
|
|
1595
|
+
_score: rankSelect,
|
|
1596
|
+
...(matchesSelect ? { _matches: matchesSelect } : {})
|
|
1597
|
+
}).from(table).$dynamic()
|
|
1598
|
+
: (visible ? this.db.select(visible as never).from(table).$dynamic() : this.db.select().from(table).$dynamic());
|
|
1391
1599
|
const allConditions: SQL[] = [];
|
|
1392
1600
|
|
|
1393
1601
|
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
1394
1602
|
|
|
1395
1603
|
if (options.searchString) {
|
|
1396
1604
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
|
|
1397
|
-
options.searchString, collection.properties, table
|
|
1605
|
+
options.searchString, collection.properties, table, collection
|
|
1398
1606
|
);
|
|
1399
1607
|
if (searchConditions.length === 0) return [];
|
|
1400
1608
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions)!);
|
|
@@ -1405,6 +1613,11 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1405
1613
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
1406
1614
|
}
|
|
1407
1615
|
|
|
1616
|
+
if (options.logical) {
|
|
1617
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
1618
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1408
1621
|
if (vectorMeta?.filter) {
|
|
1409
1622
|
allConditions.push(vectorMeta.filter);
|
|
1410
1623
|
}
|
|
@@ -1418,7 +1631,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1418
1631
|
if (vectorMeta) {
|
|
1419
1632
|
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
1420
1633
|
} else if (options.orderBy) {
|
|
1421
|
-
const orderByField = this.
|
|
1634
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
1422
1635
|
if (orderByField) {
|
|
1423
1636
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
1424
1637
|
}
|
|
@@ -1443,6 +1656,14 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
1443
1656
|
}));
|
|
1444
1657
|
}
|
|
1445
1658
|
|
|
1659
|
+
if (rankSelect) {
|
|
1660
|
+
return (rawResults as { table_row: Record<string, unknown>; _score: unknown; _matches?: unknown }[]).map(r => ({
|
|
1661
|
+
...r.table_row,
|
|
1662
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
1663
|
+
...(matchesSelect ? { _matches: r._matches ?? [] } : {})
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1446
1667
|
return rawResults as Record<string, unknown>[];
|
|
1447
1668
|
}
|
|
1448
1669
|
|