@rebasepro/server-postgres 0.21.2-canary.g1ea48be → 0.22.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.
Files changed (40) hide show
  1. package/dist/{backup-cli-CkjsJEcu.js → backup-cli-DqBakiMO.js} +2 -2
  2. package/dist/{backup-cli-CkjsJEcu.js.map → backup-cli-DqBakiMO.js.map} +1 -1
  3. package/dist/{backup-service-BNLwvxuy.js → backup-service-CaMOS76G.js} +2 -2
  4. package/dist/{backup-service-BNLwvxuy.js.map → backup-service-CaMOS76G.js.map} +1 -1
  5. package/dist/cli.js +9 -9
  6. package/dist/{doctor-Gzzd8wTq.js → doctor-CU9IogdL.js} +3 -3
  7. package/dist/{doctor-Gzzd8wTq.js.map → doctor-CU9IogdL.js.map} +1 -1
  8. package/dist/{ensure-collection-policies-B4IDFtQ-.js → ensure-collection-policies-Dzd-2S81.js} +4 -4
  9. package/dist/{ensure-collection-policies-B4IDFtQ-.js.map → ensure-collection-policies-Dzd-2S81.js.map} +1 -1
  10. package/dist/{ensure-collection-tables-Cr2ye5JC.js → ensure-collection-tables-CpAgy51F.js} +3 -3
  11. package/dist/{ensure-collection-tables-Cr2ye5JC.js.map → ensure-collection-tables-CpAgy51F.js.map} +1 -1
  12. package/dist/{ensure-tables-BnEvEJPr.js → ensure-tables-Cr5B4UmH.js} +2 -2
  13. package/dist/{ensure-tables-BnEvEJPr.js.map → ensure-tables-Cr5B4UmH.js.map} +1 -1
  14. package/dist/{generate-drizzle-schema-D2MDdJt-.js → generate-drizzle-schema-B537GIvz.js} +2 -2
  15. package/dist/{generate-drizzle-schema-D2MDdJt-.js.map → generate-drizzle-schema-B537GIvz.js.map} +1 -1
  16. package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js → generate-drizzle-schema-logic-BcMl7VSy.js} +2 -2
  17. package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js.map → generate-drizzle-schema-logic-BcMl7VSy.js.map} +1 -1
  18. package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js → generate-postgres-ddl-logic-D7imhYV8.js} +2 -2
  19. package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js.map → generate-postgres-ddl-logic-D7imhYV8.js.map} +1 -1
  20. package/dist/index.es.js +391 -54
  21. package/dist/index.es.js.map +1 -1
  22. package/dist/{plan-schema-Hgl62S-w.js → plan-schema-CboAIwLN.js} +13 -8
  23. package/dist/plan-schema-CboAIwLN.js.map +1 -0
  24. package/dist/{policy-drift-B0GDRh7_.js → policy-drift-xJfy9xG7.js} +2 -2
  25. package/dist/{policy-drift-B0GDRh7_.js.map → policy-drift-xJfy9xG7.js.map} +1 -1
  26. package/dist/{rls-bootstrap-sql-BHA6jLtj.js → rls-bootstrap-sql-H3rCFi3F.js} +5 -5
  27. package/dist/{rls-bootstrap-sql-BHA6jLtj.js.map → rls-bootstrap-sql-H3rCFi3F.js.map} +1 -1
  28. package/dist/schema/doctor-cli.js +2 -2
  29. package/dist/schema/generate-drizzle-schema.js +1 -1
  30. package/dist/schema/generate-postgres-ddl.js +1 -1
  31. package/dist/schema/search-column.d.ts +21 -1
  32. package/dist/services/FetchService.d.ts +50 -2
  33. package/dist/services/PersistService.d.ts +16 -1
  34. package/dist/services/RelationService.d.ts +31 -1
  35. package/dist/services/RelationWriteService.d.ts +16 -1
  36. package/dist/services/dataService.d.ts +16 -1
  37. package/dist/services/read-scope.d.ts +100 -0
  38. package/dist/utils/drizzle-conditions.d.ts +56 -6
  39. package/package.json +7 -7
  40. package/dist/plan-schema-Hgl62S-w.js.map +0 -1
@@ -4,8 +4,8 @@ __createRequire(import.meta.url);
4
4
  import { i as diagnoseDbError } from "../cli-errors-Dka89exj.js";
5
5
  import { o as validatePolicyPgRoles, s as warnOnAnonymousGrants } from "../rls-enforcement-C6Xk0lA6.js";
6
6
  import { t as chalk } from "../source-Br7L7GOI.js";
7
- import { n as loadCollections, r as runDoctor } from "../doctor-Gzzd8wTq.js";
8
- import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-B0GDRh7_.js";
7
+ import { n as loadCollections, r as runDoctor } from "../doctor-CU9IogdL.js";
8
+ import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-xJfy9xG7.js";
9
9
  import path from "path";
10
10
  //#region src/schema/doctor-policy-checks.ts
11
11
  /**
@@ -1,6 +1,6 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { t as main } from "../generate-drizzle-schema-D2MDdJt-.js";
3
+ import { t as main } from "../generate-drizzle-schema-B537GIvz.js";
4
4
  //#region src/schema/bin/generate-drizzle-schema.ts
5
5
  /**
6
6
  * The built entry point for `generate-drizzle-schema`.
@@ -1,6 +1,6 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "../generate-postgres-ddl-logic-CR2xcS7e.js";
3
+ import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "../generate-postgres-ddl-logic-D7imhYV8.js";
4
4
  import { n as outError, t as out } from "../cli-output-CNdMql-L.js";
5
5
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
6
6
  import { declaredDatabaseExtensions } from "@rebasepro/types";
@@ -43,7 +43,7 @@
43
43
  * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
44
44
  * typed without accents misses every row that carries them.
45
45
  */
46
- import { CollectionConfig, SearchConfig, SearchWeight } from "@rebasepro/types";
46
+ import { CollectionConfig, SearchConfig, SearchMode, SearchWeight } from "@rebasepro/types";
47
47
  /**
48
48
  * Names of the helper functions. Frozen: they are recorded in the stored
49
49
  * generation expression of every search column ever created, so renaming one
@@ -67,6 +67,20 @@ export interface ResolvedSearchField {
67
67
  sql: string;
68
68
  /** The plain-text term this field contributes, for the fuzzy column. */
69
69
  textSql: string;
70
+ /**
71
+ * The same text with accents folded **unconditionally**, for the substring
72
+ * half of {@link SearchMode} `"hybrid"`.
73
+ *
74
+ * Separate from {@link textSql} rather than replacing it, and that
75
+ * separation is the whole migration story for `mode`. `textSql` feeds the
76
+ * *stored* generated columns, so changing it changes their generation
77
+ * expression, which changes the fingerprint, which makes the next boot
78
+ * refuse (see `searchStampGuards`). This one is only ever interpolated into
79
+ * a WHERE clause, so a collection can switch to `"hybrid"` — and gain
80
+ * accent folding on the substring half — without rebuilding a column or
81
+ * taking an ACCESS EXCLUSIVE lock.
82
+ */
83
+ foldedTextSql: string;
70
84
  }
71
85
  /** Everything the generators need to render one collection's search column. */
72
86
  export interface SearchColumnSpec {
@@ -76,6 +90,12 @@ export interface SearchColumnSpec {
76
90
  column: string;
77
91
  language: string;
78
92
  unaccent: boolean;
93
+ /**
94
+ * How the query side matches. Deliberately absent from
95
+ * {@link SearchColumnSpec.expression} and from every fingerprint: it
96
+ * describes the WHERE clause, not the column.
97
+ */
98
+ mode: SearchMode;
79
99
  fields: ResolvedSearchField[];
80
100
  /** Body of `GENERATED ALWAYS AS ( … ) STORED` for the tsvector column. */
81
101
  expression: string;
@@ -7,6 +7,7 @@ import { DrizzleClient } from "../interfaces.js";
7
7
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
8
8
  import { type NestedPathHop } from "./nested-path.js";
9
9
  import { type WithDeleted } from "./soft-delete.js";
10
+ import { type ReadCallContextProvider } from "./read-scope.js";
10
11
  /**
11
12
  * Service for handling all row read operations.
12
13
  * Handles fetching, searching, counting, and filtering rows.
@@ -39,8 +40,22 @@ export declare function numericAggregateAliases(aggregates: {
39
40
  export declare class FetchService {
40
41
  private db;
41
42
  private registry;
43
+ /**
44
+ * How this service's reads reach the identity their `beforeQuery` hooks
45
+ * run as. Passed by the driver that constructed it; absent only in a
46
+ * test, where a collection declaring the hook is refused rather than
47
+ * read unnarrowed. See `read-scope.ts`.
48
+ */
49
+ private callContext?;
42
50
  private relationService;
43
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
51
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
52
+ /**
53
+ * How this service's reads reach the identity their `beforeQuery` hooks
54
+ * run as. Passed by the driver that constructed it; absent only in a
55
+ * test, where a collection declaring the hook is refused rather than
56
+ * read unnarrowed. See `read-scope.ts`.
57
+ */
58
+ callContext?: ReadCallContextProvider | undefined);
44
59
  /**
45
60
  * Get the relational query builder for a given table name.
46
61
  * Safely narrows the DrizzleClient union type to access db.query[tableName].
@@ -63,6 +78,31 @@ export declare class FetchService {
63
78
  * compile stays unresolvable and so fails closed.
64
79
  */
65
80
  private filterContext;
81
+ /**
82
+ * The conditions this collection's `beforeQuery` hooks ask to add, for a
83
+ * read that is about to be compiled.
84
+ *
85
+ * Resolved here, from the registry this service already holds, rather than
86
+ * handed in by a caller — so a read path that builds a WHERE cannot serve
87
+ * one of these collections without the hook. `[]` when nothing is declared,
88
+ * which leaves the compiled SQL byte-identical.
89
+ *
90
+ * Every caller pushes the result onto the same `allConditions` array its
91
+ * own filter goes into, and every one of those arrays is AND-ed. That is
92
+ * what keeps a hook additive: it is not trusted to narrow, it is only ever
93
+ * given a way to.
94
+ */
95
+ private narrowRead;
96
+ /** {@link narrowRead}, pre-combined, for the reads that hold one `SQL`. */
97
+ private narrowReadCondition;
98
+ /**
99
+ * The read the hooks are shown, from the options a read path was given.
100
+ *
101
+ * Transport is left out on purpose — a `databaseId`, an `include` tree, a
102
+ * `withDeleted` flag are not part of "which rows". What is in is what a
103
+ * scope might reasonably branch on.
104
+ */
105
+ private static describeRead;
66
106
  /**
67
107
  * The table column this collection's rows are keyed by, or `undefined`.
68
108
  *
@@ -457,7 +497,15 @@ export declare class FetchService {
457
497
  withDeleted?: WithDeleted;
458
498
  }): Promise<Record<string, unknown>[]>;
459
499
  /**
460
- * Check if a field value is unique
500
+ * Check if a field value is unique.
501
+ *
502
+ * Deliberately **not** narrowed by `beforeQuery`, and it is the only read
503
+ * here that is not. The question is whether the value exists anywhere in
504
+ * the table, not whether it exists among the rows this caller can see:
505
+ * narrowed, it would answer "unique" for a value a hidden row already
506
+ * holds, and the insert would then be rejected by the unique constraint
507
+ * with a message about a row the caller has no way to find. Uniqueness is a
508
+ * property of the table.
461
509
  */
462
510
  checkUniqueField(collectionPath: string, fieldName: string, value: unknown, excludeEntityId?: string, _databaseId?: string): Promise<boolean>;
463
511
  /**
@@ -1,4 +1,5 @@
1
1
  import { RelationService } from "./RelationService.js";
2
+ import type { ReadCallContextProvider } from "./read-scope.js";
2
3
  import { RelationWriteService } from "./RelationWriteService.js";
3
4
  import { FetchService } from "./FetchService.js";
4
5
  import { DrizzleClient } from "../interfaces.js";
@@ -10,12 +11,26 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
10
11
  export declare class PersistService {
11
12
  private db;
12
13
  private registry;
14
+ /**
15
+ * How this service's reads reach the identity their `beforeQuery` hooks
16
+ * run as. Passed by the driver that constructed it; absent only in a
17
+ * test, where a collection declaring the hook is refused rather than
18
+ * read unnarrowed. See `read-scope.ts`.
19
+ */
20
+ private callContext?;
13
21
  /** Reads: whether a row is under a parent, the key a link joins on. */
14
22
  private relationService;
15
23
  /** Writes: junction membership, foreign-key stamping, links. */
16
24
  private relationWrites;
17
25
  private fetchService;
18
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
26
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
27
+ /**
28
+ * How this service's reads reach the identity their `beforeQuery` hooks
29
+ * run as. Passed by the driver that constructed it; absent only in a
30
+ * test, where a collection declaring the hook is refused rather than
31
+ * read unnarrowed. See `read-scope.ts`.
32
+ */
33
+ callContext?: ReadCallContextProvider | undefined);
19
34
  /**
20
35
  * Set the columns of one many-to-many link, without touching the membership.
21
36
  *
@@ -4,6 +4,7 @@ import { DrizzleClient } from "../interfaces.js";
4
4
  import { CollectionConfig, FilterValues, OrderByTuple, ResolvedRelation, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
6
6
  import type { NestedPathHop } from "./nested-path.js";
7
+ import { type ReadCallContextProvider } from "./read-scope.js";
7
8
  /**
8
9
  * Typed wrapper for Drizzle dynamic query innerJoin.
9
10
  * Drizzle's `$dynamic()` queries lose the `innerJoin` method from
@@ -44,7 +45,36 @@ export interface RelatedRow<M extends Record<string, unknown> = Record<string, u
44
45
  export declare class RelationService {
45
46
  private db;
46
47
  private registry;
47
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
48
+ /**
49
+ * How this service's reads reach the identity their `beforeQuery` hooks
50
+ * run as. Passed by the driver that constructed it; absent only in a
51
+ * test, where a collection declaring the hook is refused rather than
52
+ * read unnarrowed. See `read-scope.ts`.
53
+ */
54
+ private callContext?;
55
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
56
+ /**
57
+ * How this service's reads reach the identity their `beforeQuery` hooks
58
+ * run as. Passed by the driver that constructed it; absent only in a
59
+ * test, where a collection declaring the hook is refused rather than
60
+ * read unnarrowed. See `read-scope.ts`.
61
+ */
62
+ callContext?: ReadCallContextProvider | undefined);
63
+ /**
64
+ * The **target** collection's `beforeQuery` narrowing, for rows reached
65
+ * through a relation.
66
+ *
67
+ * The target's, not the parent's, because these are the target's rows: a
68
+ * scope on `comments` has to hold whether they are listed at
69
+ * `/comments` or loaded as `post.comments`. Without this an `include` is a
70
+ * way around every row filter a collection declares — the exact hole
71
+ * `stripUnreadable` was once missing from the relation-ref branch.
72
+ *
73
+ * Folded into the `narrow` condition the loaders already thread through
74
+ * every relation kind, so a `via` join, a junction, a foreign key and the
75
+ * dynamic relation builder all apply it or none of them do.
76
+ */
77
+ private narrowTargetRead;
48
78
  /**
49
79
  * One target row, as the {@link RelatedRow} everything here returns.
50
80
  *
@@ -2,6 +2,7 @@ import { CollectionConfig, Properties, ResolvedManyToMany, ResolvedRelation, Res
2
2
  import { DrizzleClient } from "../interfaces.js";
3
3
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
4
4
  import type { NestedPathHop } from "./nested-path.js";
5
+ import type { ReadCallContextProvider } from "./read-scope.js";
5
6
  /**
6
7
  * A `_pivot` as the junction table's columns.
7
8
  *
@@ -28,8 +29,22 @@ export declare function serializePivot(pivot: Record<string, unknown>, payload:
28
29
  export declare class RelationWriteService {
29
30
  private db;
30
31
  private registry;
32
+ /**
33
+ * How this service's reads reach the identity their `beforeQuery` hooks
34
+ * run as. Passed by the driver that constructed it; absent only in a
35
+ * test, where a collection declaring the hook is refused rather than
36
+ * read unnarrowed. See `read-scope.ts`.
37
+ */
38
+ private callContext?;
31
39
  private reads;
32
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
40
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
41
+ /**
42
+ * How this service's reads reach the identity their `beforeQuery` hooks
43
+ * run as. Passed by the driver that constructed it; absent only in a
44
+ * test, where a collection declaring the hook is refused rather than
45
+ * read unnarrowed. See `read-scope.ts`.
46
+ */
47
+ callContext?: ReadCallContextProvider | undefined);
33
48
  /**
34
49
  * Remove the junction row linking a parent to `targetId`, leaving the target
35
50
  * row itself alone.
@@ -1,6 +1,7 @@
1
1
  import { FilterValues, IncludeSpec, LogicalCondition, OrderByTuple } from "@rebasepro/types";
2
2
  import type { VectorSearchParams } from "@rebasepro/types";
3
3
  import { FetchService } from "./FetchService.js";
4
+ import type { ReadCallContextProvider } from "./read-scope.js";
4
5
  import type { WithDeleted } from "./soft-delete.js";
5
6
  import { PersistService } from "./PersistService.js";
6
7
  import { RelationService } from "./RelationService.js";
@@ -25,9 +26,23 @@ export * from "../interfaces.js";
25
26
  export declare class DataService implements DataRepository {
26
27
  private db;
27
28
  private registry;
29
+ /**
30
+ * How this service's reads reach the identity their `beforeQuery` hooks
31
+ * run as. Passed by the driver that constructed it; absent only in a
32
+ * test, where a collection declaring the hook is refused rather than
33
+ * read unnarrowed. See `read-scope.ts`.
34
+ */
35
+ private callContext?;
28
36
  private fetchService;
29
37
  private persistService;
30
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
38
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
39
+ /**
40
+ * How this service's reads reach the identity their `beforeQuery` hooks
41
+ * run as. Passed by the driver that constructed it; absent only in a
42
+ * test, where a collection declaring the hook is refused rather than
43
+ * read unnarrowed. See `read-scope.ts`.
44
+ */
45
+ callContext?: ReadCallContextProvider | undefined);
31
46
  /**
32
47
  * Fetch a single row by ID
33
48
  */
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `beforeQuery`: the one place a collection callback can change *which* rows a
3
+ * read asks for.
4
+ *
5
+ * `afterRead` runs over rows that have already been fetched, so until this
6
+ * existed an application developer who needed the read itself narrowed — a
7
+ * tenant scope, a visibility window, a per-role row filter — had two options:
8
+ * patch this driver, or `rebase eject`.
9
+ *
10
+ * Two properties are load-bearing, and both belong to the *shape* of the thing
11
+ * rather than to how carefully each call site was written.
12
+ *
13
+ * ## It can only narrow
14
+ *
15
+ * A hook returns a {@link QueryNarrowing} — a declarative filter, optionally a
16
+ * logical group — which is AND-ed into the query the caller sent. `AND(q, c)`
17
+ * is a subset of `q` for every `c`, so there is no `c` a hook can return that
18
+ * widens a read. The obvious alternative shape, `(query) => query`, can: a
19
+ * hook that drops a condition on the way through runs the query without it,
20
+ * and on an RLS data plane a dropped condition returns everything the policies
21
+ * happen to allow. That is the same reasoning behind
22
+ * `UnknownFilterFieldsMode` defaulting to `"error"`, and it is why that mode is
23
+ * **forced** to `"error"` for a hook's own filter here whatever the
24
+ * process-wide setting is: a scope condition naming a renamed column has to
25
+ * refuse the request, never be dropped from it.
26
+ *
27
+ * ## It cannot be skipped
28
+ *
29
+ * The hooks are resolved *by the read path itself*, out of the registry that
30
+ * read already holds — not handed to it by a caller who might forget. So a
31
+ * read that compiles a WHERE compiles this one too, or it throws.
32
+ *
33
+ * The one thing a read path cannot derive is the identity the hook runs as:
34
+ * `FetchService`, `RelationService` and `DataService` carry no user. That
35
+ * arrives as a {@link ReadCallContextProvider}, threaded from the driver that
36
+ * constructed them — the driver is the only object that knows both the caller
37
+ * and the connection, and it constructs a service per transaction anyway, so
38
+ * there is exactly one place per instance to pass it.
39
+ *
40
+ * Absence is the interesting case, and it fails closed: a service built with no
41
+ * provider refuses any read of a collection that declares `beforeQuery`, with
42
+ * a message naming the collection. So a future construction site that forgets
43
+ * the provider breaks loudly on the first scoped collection instead of quietly
44
+ * serving every row — which is the failure mode this whole module exists to
45
+ * rule out. A deployment with no `beforeQuery` anywhere never reaches any of
46
+ * this: the check is one map lookup and the compiled SQL is byte-identical.
47
+ *
48
+ * @module
49
+ */
50
+ import { SQL } from "drizzle-orm";
51
+ import { PgTable } from "drizzle-orm/pg-core";
52
+ import type { CollectionConfig, ReadOperation, ReadQuery, RebaseCallContext } from "@rebasepro/types";
53
+ import type { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
54
+ import { type FilterCompilationOptions } from "../utils/drizzle-conditions.js";
55
+ /**
56
+ * How a read path reaches the context its `beforeQuery` hooks run with.
57
+ *
58
+ * A function rather than the context itself, because the driver's transaction
59
+ * handle and its `data` plane are established *after* it constructs the
60
+ * services that read through them — and because building a context costs an
61
+ * object that a collection with no hooks should never pay for.
62
+ */
63
+ export type ReadCallContextProvider = () => RebaseCallContext;
64
+ /** Whether anything at all would run for this collection. */
65
+ export declare function hasBeforeQuery(registry: PostgresCollectionRegistry | undefined, collection: CollectionConfig | undefined): boolean;
66
+ /**
67
+ * What a read path is about to compile, in the form a hook is shown it.
68
+ *
69
+ * Built by the caller rather than derived here, because only the caller knows
70
+ * which of its options are part of the query and which are transport — a
71
+ * `databaseId`, an `include` tree, a `withDeleted` flag are not "which rows".
72
+ */
73
+ export interface ReadQueryDescription {
74
+ operation: ReadOperation;
75
+ query: ReadQuery;
76
+ }
77
+ /** Everything `beforeQueryConditions` needs that is not about this one read. */
78
+ export interface BeforeQueryEnv {
79
+ registry: PostgresCollectionRegistry | undefined;
80
+ /**
81
+ * The context provider the driver handed down. `undefined` means no driver
82
+ * built this service, which is a refusal rather than a bypass — see the
83
+ * module comment.
84
+ */
85
+ callContext: ReadCallContextProvider | undefined;
86
+ }
87
+ /**
88
+ * The conditions every `beforeQuery` hook for this collection asks to add.
89
+ *
90
+ * Returned as a list rather than pre-combined so a caller pushes them onto the
91
+ * same `allConditions` array its own filter goes into. Every caller AND-es
92
+ * that array, which is what makes these additive — the hook is not trusted to
93
+ * narrow, it is only ever given a way to.
94
+ */
95
+ export declare function beforeQueryConditions(env: BeforeQueryEnv, collection: CollectionConfig | undefined, path: string, table: PgTable<never>, description: ReadQueryDescription, filterContext: Omit<FilterCompilationOptions, "unknownFields">): Promise<SQL[]>;
96
+ /**
97
+ * {@link beforeQueryConditions}, pre-combined, for the read paths that hold a
98
+ * single `SQL` rather than a list of them — a single get, a relation loader.
99
+ */
100
+ export declare function beforeQueryCondition(env: BeforeQueryEnv, collection: CollectionConfig | undefined, path: string, table: PgTable<never>, description: ReadQueryDescription, filterContext: Omit<FilterCompilationOptions, "unknownFields">): Promise<SQL | undefined>;
@@ -516,13 +516,20 @@ export declare class DrizzleConditionBuilder {
516
516
  * - **Declared** — one `@@ websearch_to_tsquery` against the generated
517
517
  * `tsvector` column. Stems, drops stopwords, AND-es the terms, reaches
518
518
  * inside JSONB and arrays, and uses the GIN index.
519
- * - **Not declared** — the original `ILIKE '%term%'` OR-ed across top-level
520
- * string properties, with the term escaped (see {@link escapeLikePattern})
521
- * so it is matched as the literal text the user typed.
519
+ * - **Not declared** — `ILIKE '%term%'` per term, OR-ed across top-level
520
+ * string properties and AND-ed across the terms, with each term escaped
521
+ * (see {@link escapeLikePattern}) so it is matched as the literal text
522
+ * the user typed.
522
523
  *
523
- * The second is the default and stays the default. A collection that has
524
- * not opted in compiles to exactly the SQL it compiled to before this
525
- * branch existed, which is the only reason it is safe to have added it.
524
+ * The second is the default and stays the default, and it AND-es terms for
525
+ * the same reason `websearch_to_tsquery` does: a person typing two words
526
+ * into a search box means both, and a name lives in two columns. Matching
527
+ * the whole string per column instead — which is what this did — cannot
528
+ * find `sebastian melendez` on a row with `first_name`/`last_name`, and
529
+ * could not find `sebastian ` at all.
530
+ *
531
+ * Returns at most one condition. Every caller ORs what it gets back, which
532
+ * is right across columns and wrong across terms, so the AND is built here.
526
533
  *
527
534
  * `collection` is optional so that the callers which genuinely have no
528
535
  * collection in hand — nested paths, derived views — keep working; without
@@ -544,6 +551,49 @@ export declare class DrizzleConditionBuilder {
544
551
  * on user input, which `to_tsquery` does on so much as a stray parenthesis.
545
552
  */
546
553
  static buildFullTextCondition(searchString: string, table: PgTable<any>, collection: CollectionConfig): SQL | undefined;
554
+ /**
555
+ * The substring half of {@link SearchMode} `"hybrid"`: every term found
556
+ * somewhere in the declared fields, with accents folded on both sides.
557
+ *
558
+ * Terms are AND-ed and fields OR-ed, exactly as the ILIKE default does and
559
+ * for the same reason — a person typing two words means both, and a name
560
+ * lives in two columns (`splitSearchTerms`). What it does *not* share with
561
+ * the default is its reach: these are the declared `search.fields`, so it
562
+ * folds accents, joins `text[]` and walks into JSONB through the same
563
+ * IMMUTABLE helpers the generated column uses.
564
+ *
565
+ * The needle is folded with the same function as the document, so
566
+ * `munoz` and `Muñoz` meet in the middle rather than one side being
567
+ * normalized and the other not — the subtle way to build a search that
568
+ * matches nothing.
569
+ *
570
+ * Interpolated as raw SQL because the field expressions are generated (they
571
+ * name columns and helper functions, which are identifiers, not values);
572
+ * the *term* is a bind parameter, escaped for LIKE first — see
573
+ * {@link escapeLikePattern} for what that escape is for.
574
+ */
575
+ static buildFoldedSubstringCondition(searchString: string, spec: SearchColumnSpec): SQL | undefined;
576
+ /**
577
+ * Whether one text expression holds **any** term of the search string.
578
+ *
579
+ * The per-field question, where {@link buildFoldedSubstringCondition} asks
580
+ * the per-row one: `OR` across terms rather than `AND`, because a two-word
581
+ * query satisfied across two fields has each field carrying one term. Used
582
+ * by `_matches` to name the fields that caused a substring hit.
583
+ */
584
+ static foldedSubstringOf(searchString: string, text: SQL): SQL;
585
+ /**
586
+ * One accent-folded `%term%` bind parameter per search term.
587
+ *
588
+ * The needle is folded with the same function as the document, so `munoz`
589
+ * and `Muñoz` meet in the middle instead of one side being normalized and
590
+ * the other not — the subtle way to build a search that matches nothing.
591
+ *
592
+ * A string with no terms at all is only whitespace, and stays the single
593
+ * literal pattern it is on the ILIKE path rather than becoming "no rows",
594
+ * so both paths answer a stray space the same way.
595
+ */
596
+ private static foldedNeedles;
547
597
  /**
548
598
  * `websearch_to_tsquery(<config>, <normalized search string>)`.
549
599
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgres",
3
- "version": "0.21.2-canary.g1ea48be",
3
+ "version": "0.22.0",
4
4
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
5
5
  "keywords": [
6
6
  "cms",
@@ -50,11 +50,11 @@
50
50
  "execa": "^9.6.1",
51
51
  "pg": "^8.22.0",
52
52
  "ws": "^8.21.1",
53
- "@rebasepro/codegen": "0.21.2-canary.g1ea48be",
54
- "@rebasepro/common": "0.21.2-canary.g1ea48be",
55
- "@rebasepro/server": "0.21.2-canary.g1ea48be",
56
- "@rebasepro/types": "0.21.2-canary.g1ea48be",
57
- "@rebasepro/utils": "0.21.2-canary.g1ea48be"
53
+ "@rebasepro/codegen": "0.22.0",
54
+ "@rebasepro/common": "0.22.0",
55
+ "@rebasepro/server": "0.22.0",
56
+ "@rebasepro/types": "0.22.0",
57
+ "@rebasepro/utils": "0.22.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@electric-sql/pglite": "0.5.6",
@@ -70,7 +70,7 @@
70
70
  "typescript": "^6.0.3",
71
71
  "vite": "^8.1.5",
72
72
  "vitest": "^4.1.10",
73
- "@rebasepro/client": "0.21.2-canary.g1ea48be"
73
+ "@rebasepro/client": "0.22.0"
74
74
  },
75
75
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
76
76
  "publishConfig": {