@rebasepro/server-postgres 0.21.2-canary.g1ea48be → 0.23.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 (83) hide show
  1. package/dist/{BranchService-CucnFcSE.js → BranchService-DGPL6G_C.js} +2 -2
  2. package/dist/{BranchService-CucnFcSE.js.map → BranchService-DGPL6G_C.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +41 -1
  4. package/dist/auth/services.d.ts +27 -8
  5. package/dist/backup/backup-cli.d.ts +22 -0
  6. package/dist/{backup-cli-CkjsJEcu.js → backup-cli-24v4OlSp.js} +17 -14
  7. package/dist/backup-cli-24v4OlSp.js.map +1 -0
  8. package/dist/{backup-service-BNLwvxuy.js → backup-service-HQ9GC4tN.js} +2 -2
  9. package/dist/{backup-service-BNLwvxuy.js.map → backup-service-HQ9GC4tN.js.map} +1 -1
  10. package/dist/{cli-errors-Dka89exj.js → cli-errors-B8qHg02P.js} +6 -1
  11. package/dist/cli-errors-B8qHg02P.js.map +1 -0
  12. package/dist/cli-flags-BglvpjHv.js +138 -0
  13. package/dist/cli-flags-BglvpjHv.js.map +1 -0
  14. package/dist/cli-flags.d.ts +28 -0
  15. package/dist/cli-helpers.d.ts +6 -0
  16. package/dist/cli-scratch-database.d.ts +34 -0
  17. package/dist/cli.js +97 -261
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-DF-8dTVa.js} +16 -20
  20. package/dist/column-plan-helpers-DF-8dTVa.js.map +1 -0
  21. package/dist/{doctor-Gzzd8wTq.js → doctor-Cb2thZ8s.js} +8 -7
  22. package/dist/doctor-Cb2thZ8s.js.map +1 -0
  23. package/dist/{ensure-collection-policies-B4IDFtQ-.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
  24. package/dist/{ensure-collection-policies-B4IDFtQ-.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
  25. package/dist/{ensure-collection-tables-Cr2ye5JC.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
  26. package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
  27. package/dist/{ensure-tables-BnEvEJPr.js → ensure-tables-Dhn9KM3B.js} +2 -2
  28. package/dist/{ensure-tables-BnEvEJPr.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
  29. package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
  30. package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
  31. package/dist/{generate-drizzle-schema-D2MDdJt-.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
  32. package/dist/{generate-drizzle-schema-D2MDdJt-.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
  33. package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
  34. package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
  35. package/dist/index.es.js +1882 -392
  36. package/dist/index.es.js.map +1 -1
  37. package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-WMuAfxvw.js} +323 -4
  38. package/dist/introspect-db-logic-WMuAfxvw.js.map +1 -0
  39. package/dist/{plan-schema-Hgl62S-w.js → plan-schema-C0fxM8dY.js} +170 -38
  40. package/dist/plan-schema-C0fxM8dY.js.map +1 -0
  41. package/dist/{policy-drift-B0GDRh7_.js → policy-drift-DljYdrpW.js} +2 -2
  42. package/dist/{policy-drift-B0GDRh7_.js.map → policy-drift-DljYdrpW.js.map} +1 -1
  43. package/dist/{rls-bootstrap-sql-BHA6jLtj.js → rls-bootstrap-sql-B8EclDyM.js} +320 -17
  44. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +1 -0
  45. package/dist/schema/atlas-argv.d.ts +15 -0
  46. package/dist/schema/column-plan-helpers.d.ts +23 -4
  47. package/dist/schema/destructive-sql.d.ts +52 -1
  48. package/dist/schema/doctor-cli.js +3 -3
  49. package/dist/schema/generate-drizzle-schema.js +1 -1
  50. package/dist/schema/generate-postgres-ddl.js +1 -1
  51. package/dist/schema/introspect-db.js +1 -312
  52. package/dist/schema/introspect-db.js.map +1 -1
  53. package/dist/schema/plan/diff-plan.d.ts +3 -2
  54. package/dist/schema/plan/plan-schema.d.ts +10 -3
  55. package/dist/schema/plan/types.d.ts +6 -1
  56. package/dist/schema/search-column.d.ts +21 -1
  57. package/dist/services/FetchService.d.ts +147 -30
  58. package/dist/services/PersistService.d.ts +56 -5
  59. package/dist/services/RelationService.d.ts +42 -1
  60. package/dist/services/RelationWriteService.d.ts +22 -1
  61. package/dist/services/cdc/CdcListener.d.ts +6 -1
  62. package/dist/services/collection-helpers.d.ts +11 -0
  63. package/dist/services/dataService.d.ts +19 -21
  64. package/dist/services/field-op-sql.d.ts +71 -0
  65. package/dist/services/junction-writes.d.ts +19 -3
  66. package/dist/services/pg-notify-listener.d.ts +7 -0
  67. package/dist/services/read-field-access.d.ts +19 -0
  68. package/dist/services/read-scope.d.ts +100 -0
  69. package/dist/services/realtimeService.d.ts +77 -3
  70. package/dist/services/row-pipeline.d.ts +5 -0
  71. package/dist/services/soft-delete.d.ts +0 -2
  72. package/dist/services/write-transaction-scope.d.ts +39 -0
  73. package/dist/utils/drizzle-conditions.d.ts +63 -8
  74. package/dist/websocket.d.ts +10 -3
  75. package/package.json +9 -9
  76. package/dist/backup-cli-CkjsJEcu.js.map +0 -1
  77. package/dist/cli-errors-Dka89exj.js.map +0 -1
  78. package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
  79. package/dist/doctor-Gzzd8wTq.js.map +0 -1
  80. package/dist/ensure-collection-tables-Cr2ye5JC.js.map +0 -1
  81. package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
  82. package/dist/plan-schema-Hgl62S-w.js.map +0 -1
  83. package/dist/rls-bootstrap-sql-BHA6jLtj.js.map +0 -1
@@ -1,8 +1,37 @@
1
+ import { Properties } from "@rebasepro/types";
1
2
  import { RelationService } from "./RelationService.js";
3
+ import { type ReadCallContextProvider } from "./read-scope.js";
2
4
  import { RelationWriteService } from "./RelationWriteService.js";
3
5
  import { FetchService } from "./FetchService.js";
4
6
  import { DrizzleClient } from "../interfaces.js";
5
7
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
8
+ /**
9
+ * The top-level properties whose value is stamped once, when the row is
10
+ * created: `on_create` dates and `user_on_create` identities.
11
+ *
12
+ * What they hold is a fact about the row's creation, so a write to a row that
13
+ * already exists has nothing to say about them — an update, and the
14
+ * conflict-update of an upsert that met a stored row. Top level only: a stamp
15
+ * nested inside a `map` is part of that map's value, not a column a write can
16
+ * leave out.
17
+ */
18
+ export declare function createStampKeys(properties: Properties | undefined): string[];
19
+ /** How {@link PersistService.save} writes a row that may already be stored. */
20
+ export interface PersistSaveOptions {
21
+ /** INSERT ... ON CONFLICT DO UPDATE. See `SaveProps.upsert`. */
22
+ upsert?: boolean;
23
+ /** The conflict target, instead of the primary key. See `SaveProps.onConflict`. */
24
+ onConflict?: readonly string[];
25
+ /**
26
+ * Keys of `values` that belong to the INSERT alone: what the create
27
+ * pipeline filled in rather than what the caller wrote — a declared
28
+ * `defaultValue`, a tenant stamp. When an upsert's INSERT meets a stored
29
+ * row, those columns keep the value they have. The create-time stamps
30
+ * (`on_create`, `user_on_create`) are left out of the conflict-update
31
+ * without being named here.
32
+ */
33
+ insertOnlyKeys?: readonly string[];
34
+ }
6
35
  /**
7
36
  * Service for handling all row write operations.
8
37
  * Handles saving, deleting, and updating rows.
@@ -10,12 +39,26 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
10
39
  export declare class PersistService {
11
40
  private db;
12
41
  private registry;
42
+ /**
43
+ * How this service's reads reach the identity their `beforeQuery` hooks
44
+ * run as. Passed by the driver that constructed it; absent only in a
45
+ * test, where a collection declaring the hook is refused rather than
46
+ * read unnarrowed. See `read-scope.ts`.
47
+ */
48
+ private callContext?;
13
49
  /** Reads: whether a row is under a parent, the key a link joins on. */
14
50
  private relationService;
15
51
  /** Writes: junction membership, foreign-key stamping, links. */
16
52
  private relationWrites;
17
53
  private fetchService;
18
- constructor(db: DrizzleClient, registry: PostgresCollectionRegistry);
54
+ constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
55
+ /**
56
+ * How this service's reads reach the identity their `beforeQuery` hooks
57
+ * run as. Passed by the driver that constructed it; absent only in a
58
+ * test, where a collection declaring the hook is refused rather than
59
+ * read unnarrowed. See `read-scope.ts`.
60
+ */
61
+ callContext?: ReadCallContextProvider | undefined);
19
62
  /**
20
63
  * Set the columns of one many-to-many link, without touching the membership.
21
64
  *
@@ -67,6 +110,10 @@ export declare class PersistService {
67
110
  * the row already exists — which is what a re-runnable import needs. The
68
111
  * conflict is matched on the primary key unless `options.onConflict` names
69
112
  * other columns; see {@link SaveProps.onConflict} for why that matters.
113
+ * When the INSERT meets a stored row, the conflict-update sets what the
114
+ * caller wrote and nothing the create alone decides — see
115
+ * {@link PersistSaveOptions.insertOnlyKeys} — and only on a row the caller's
116
+ * `beforeQuery` scope reaches.
70
117
  *
71
118
  * `values` may carry field operations (`{ views: { $inc: 1 } }`). They are
72
119
  * split out here rather than at the REST boundary because every request
@@ -74,10 +121,14 @@ export declare class PersistService {
74
121
  * one method, and a rule applied at one door is a rule the other doors do
75
122
  * not have. What they compile to is `field-op-sql.ts`.
76
123
  */
77
- save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: {
78
- upsert?: boolean;
79
- onConflict?: readonly string[];
80
- }): Promise<Record<string, unknown>>;
124
+ save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: PersistSaveOptions): Promise<Record<string, unknown>>;
125
+ /**
126
+ * The caller's `beforeQuery` scope over this table, as one condition: what
127
+ * a stored row must satisfy for a write that meets it to change it. The
128
+ * same scope the update gate and the single get apply; `undefined` when the
129
+ * collection declares no hook.
130
+ */
131
+ private writeScope;
81
132
  /**
82
133
  * Get the RelationService instance for external use
83
134
  */
@@ -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,47 @@ 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;
78
+ /**
79
+ * The target rows a caller may see through `relation`: the target's
80
+ * `beforeQuery` scope and its soft delete, as one condition on the target
81
+ * table, or `undefined` when neither applies.
82
+ *
83
+ * What every loader here narrows a read by, exposed for the membership
84
+ * writes. A write that diffs a new link set against "what is linked now"
85
+ * has to diff against what the caller could have read — otherwise saving
86
+ * back the list they were shown unlinks every row they were not.
87
+ */
88
+ visibleTargetCondition(parentCollection: CollectionConfig, relation: ResolvedRelation, parentId?: string | number): Promise<SQL | undefined>;
48
89
  /**
49
90
  * One target row, as the {@link RelatedRow} everything here returns.
50
91
  *
@@ -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.
@@ -49,6 +64,12 @@ export declare class RelationWriteService {
49
64
  * lost update the membership diff exists to avoid.
50
65
  */
51
66
  updateRelationPivot(tx: DrizzleClient, hop: NestedPathHop, targetId: string | number, pivot: Record<string, unknown>): Promise<void>;
67
+ /**
68
+ * The target rows of `relation` this caller can see, for a junction diff —
69
+ * or `undefined` when that is all of them. See
70
+ * {@link RelationService.visibleTargetCondition}.
71
+ */
72
+ private visibleTargets;
52
73
  /** A collection's id, parsed to the type its primary key column holds. */
53
74
  private parsedId;
54
75
  /** The same, for the membership list a to-many write names. */
@@ -31,7 +31,12 @@ export declare function parseCdcPayload(payload: string): CdcChangeEvent | null;
31
31
  */
32
32
  export declare class CdcListener {
33
33
  private readonly listener;
34
- constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>);
34
+ /**
35
+ * @param onReconnect Called when the connection is listening again after a
36
+ * drop. Every change committed in the gap was notified to nobody, so
37
+ * this is where subscribers are told to look again.
38
+ */
39
+ constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>, onReconnect?: () => void);
35
40
  /**
36
41
  * Connect and begin listening. Idempotent.
37
42
  *
@@ -1,4 +1,5 @@
1
1
  import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
+ import { SQL } from "drizzle-orm";
2
3
  import { CollectionConfig, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
3
4
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
4
5
  import { ApiError } from "@rebasepro/server";
@@ -34,6 +35,16 @@ export declare function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta;
34
35
  * statement into an unrelated-looking `25P02`.
35
36
  */
36
37
  export declare function idCanAddressTable(id: string | number, table: PgTable, idInfoArray: PrimaryKeyInfo[]): boolean;
38
+ /**
39
+ * The condition that names one row: every key column equal to its part of the
40
+ * address.
41
+ *
42
+ * Every key column, not the first. A composite-key read that matched only the
43
+ * leading column answered `p1:::bob` with whichever `p1` row Postgres reached
44
+ * first, and a delete then judged that row's `beforeDelete` while deleting bob —
45
+ * the writes have always matched the whole key, so the reads must too.
46
+ */
47
+ export declare function rowIdentityCondition(table: PgTable, idInfoArray: PrimaryKeyInfo[], id: string | number, collectionPath: string): SQL;
37
48
  export declare function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig;
38
49
  /**
39
50
  * Reject a write naming something that is not a column of the table.
@@ -1,8 +1,9 @@
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
- import { PersistService } from "./PersistService.js";
6
+ import { PersistService, type PersistSaveOptions } from "./PersistService.js";
6
7
  import { RelationService } from "./RelationService.js";
7
8
  import { DataRepository, DrizzleClient } from "../interfaces.js";
8
9
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.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
  */
@@ -37,21 +52,7 @@ export declare class DataService implements DataRepository {
37
52
  /**
38
53
  * Fetch a collection of rows with optional filtering, ordering, and pagination
39
54
  */
40
- fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
41
- filter?: FilterValues<Extract<keyof M, string>>;
42
- /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
43
- logical?: LogicalCondition;
44
- orderBy?: string | OrderByTuple[];
45
- order?: "desc" | "asc";
46
- limit?: number;
47
- offset?: number;
48
- startAfter?: Record<string, unknown>;
49
- searchString?: string;
50
- databaseId?: string;
51
- vectorSearch?: VectorSearchParams;
52
- /** See `FetchCollectionProps.withDeleted`. */
53
- withDeleted?: WithDeleted;
54
- }): Promise<Record<string, unknown>[]>;
55
+ fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: Parameters<FetchService["fetchCollection"]>[1]): Promise<Record<string, unknown>[]>;
55
56
  /**
56
57
  * The REST read pipeline: flat rows, with exactly the relations `include`
57
58
  * names — see {@link FetchService.fetchCollectionForRest}.
@@ -117,10 +118,7 @@ export declare class DataService implements DataRepository {
117
118
  /**
118
119
  * Save an row (create or update)
119
120
  */
120
- save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: {
121
- upsert?: boolean;
122
- onConflict?: readonly string[];
123
- }): Promise<Record<string, unknown>>;
121
+ save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?: PersistSaveOptions): Promise<Record<string, unknown>>;
124
122
  /**
125
123
  * Delete an row by ID
126
124
  */
@@ -1,6 +1,9 @@
1
1
  import { SQL } from "drizzle-orm";
2
2
  import { PgTable } from "drizzle-orm/pg-core";
3
+ import { ApiError } from "@rebasepro/server";
3
4
  import type { ParsedFieldOp } from "@rebasepro/server";
5
+ import type { Properties } from "@rebasepro/types";
6
+ import type { DrizzleClient } from "../interfaces.js";
4
7
  /**
5
8
  * The expression one operation compiles to, or a 400 saying why it cannot.
6
9
  *
@@ -14,3 +17,71 @@ export declare function compileFieldOp(table: PgTable, field: string, op: Parsed
14
17
  export declare function compileFieldOps(table: PgTable, fieldOps: Record<string, ParsedFieldOp>, context: {
15
18
  collectionPath: string;
16
19
  }): Record<string, SQL>;
20
+ /**
21
+ * One declared bound an operation's result has to stay inside.
22
+ *
23
+ * `code` and `message` are what a plain value breaking the same rule reports
24
+ * (see `write-validation.ts`), so `{ stock: { $inc: -1000 } }` and
25
+ * `{ stock: -5 }` are refused with one answer.
26
+ */
27
+ export interface FieldOpBound {
28
+ /** The property key. */
29
+ field: string;
30
+ code: "min" | "max" | "more_than" | "less_than" | "positive" | "negative" | "min_items" | "max_items";
31
+ /**
32
+ * TRUE when the value the operation writes keeps to the rule, evaluated
33
+ * against the row the statement is changing. Never NULL: an unset column
34
+ * reads as `0` or empty wherever the operation itself reads it so, and an
35
+ * array the operation leaves unset has no size for a bound to judge.
36
+ */
37
+ holds: SQL;
38
+ message: string;
39
+ }
40
+ /**
41
+ * The declared bounds each operation's *result* must satisfy, as conditions
42
+ * for the UPDATE that applies it.
43
+ *
44
+ * `{ stock: -5 }` on `stock: { validation: { min: 0 } }` is refused before the
45
+ * statement is built, because the value is in the request. What
46
+ * `{ stock: { $inc: -1000 } }` produces depends on the stored row, which the
47
+ * request never sees, so it can only be judged where the row is: in the WHERE
48
+ * of the UPDATE that does the arithmetic, over the row that statement holds
49
+ * locked. Reading the row first and judging the sum would reopen the race the
50
+ * operation exists to close — two decrements of a stock of 1 would each read
51
+ * `1`, each find room for one more, and both go through. A condition on the
52
+ * UPDATE cannot be raced that way: under READ COMMITTED an UPDATE blocked on a
53
+ * concurrent writer re-evaluates its WHERE against the row that writer
54
+ * committed, so the second decrement sees `0 - 1` and matches nothing.
55
+ *
56
+ * The bound is the whole declared range, applied to the result the way it
57
+ * applies to a value: a number's `min`, `max`, `moreThan`, `lessThan`,
58
+ * `positive` and `negative`, an array's `min` and `max` items. `integer` is not
59
+ * here: a whole increment of a whole number stays whole, and the request
60
+ * already refuses a fractional one. `$merge` has no bounds to hold.
61
+ */
62
+ export declare function compileFieldOpBounds(table: PgTable, fieldOps: Record<string, ParsedFieldOp>, properties: Properties, context: {
63
+ collectionPath: string;
64
+ }): FieldOpBound[];
65
+ /**
66
+ * Which bounds the row, as it now stands, would break — for an UPDATE that
67
+ * carried bounds and matched nothing.
68
+ *
69
+ * A guarded UPDATE matching zero rows means one of three things: the row is not
70
+ * there for this caller (404), a row-level security policy refused the write
71
+ * (403), or the operation's result broke a bound (400). The first two are
72
+ * `explainZeroRowWrite`'s; this answers the third, by evaluating each bound
73
+ * against the row over the same handle and the same key conditions. An empty
74
+ * answer — the row is not visible, or every bound holds — leaves the decision to
75
+ * `explainZeroRowWrite`.
76
+ *
77
+ * It is a second statement, so the row can change in between. A row deleted in
78
+ * between is a 404 here as it would have been anyway. A row changed so that the
79
+ * operation now fits reads as "every bound holds" and falls through to the
80
+ * row-level-security answer; the write was refused either way, and the
81
+ * refusal is never turned into a write.
82
+ *
83
+ * Only booleans come back — whether each bound holds — never the stored value.
84
+ */
85
+ export declare function brokenFieldOpBounds(handle: DrizzleClient, table: PgTable, conditions: SQL[], bounds: FieldOpBound[]): Promise<FieldOpBound[]>;
86
+ /** The 400 a write gets when an operation's result breaks a bound. */
87
+ export declare function fieldOpBoundsError(collectionSlug: string, broken: FieldOpBound[]): ApiError;
@@ -1,3 +1,4 @@
1
+ import { type SQL } from "drizzle-orm";
1
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
2
3
  import type { Properties, ResolvedVia } from "@rebasepro/types";
3
4
  import { DrizzleClient } from "../interfaces.js";
@@ -49,6 +50,19 @@ export interface JunctionLinkWrite {
49
50
  /** Payload values keyed by property key, already serialized for the driver. */
50
51
  pivot?: Record<string, unknown>;
51
52
  }
53
+ /**
54
+ * The target rows a membership write can see, when that is not all of them —
55
+ * the target's `beforeQuery` scope and its soft delete.
56
+ *
57
+ * The diff reads "what is linked now" through it, so a link to a row the caller
58
+ * was never shown is in neither list and is left alone rather than unlinked.
59
+ */
60
+ export interface VisibleTargets {
61
+ table: PgTable;
62
+ /** The target column the junction's `targetColumn` holds values of. */
63
+ idColumn: AnyPgColumn;
64
+ condition: SQL;
65
+ }
52
66
  /** The junction a `manyToMany` names outright. */
53
67
  export declare function bindThroughJunction(registry: PostgresCollectionRegistry, through: {
54
68
  table: string;
@@ -91,8 +105,10 @@ export declare function removeJunctionLink(tx: DrizzleClient, binding: JunctionB
91
105
  * the form runs under RLS, so a user who may edit the parent but cannot see
92
106
  * some of the linked rows gets a shorter list — and writing it back deleted
93
107
  * the links they were never shown. The select that drives the diff runs in
94
- * this same transaction under the same policies, so a link the caller cannot
95
- * read is in neither list and survives the save.
108
+ * this same transaction under the same policies, and through
109
+ * `visibleTargets` — the target's `beforeQuery` scope and soft delete, which
110
+ * RLS knows nothing of — so a link the caller cannot read is in neither list
111
+ * and survives the save.
96
112
  * - **Junction payload columns.** A junction carrying its own columns
97
113
  * (`position`, `role`, `created_at`) lost them on every save, because every
98
114
  * row was re-inserted with only the two keys. Untouched links are left alone.
@@ -115,7 +131,7 @@ export declare function removeJunctionLink(tx: DrizzleClient, binding: JunctionB
115
131
  * that is what keeps a plain `[1, 2, 3]` membership write from wiping the
116
132
  * `role`s off the links it did not mention.
117
133
  */
118
- export declare function applyJunctionMembership(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, links: JunctionLinkWrite[]): Promise<void>;
134
+ export declare function applyJunctionMembership(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, links: JunctionLinkWrite[], visibleTargets?: VisibleTargets): Promise<void>;
119
135
  /**
120
136
  * Set the columns of ONE existing link, without touching the membership.
121
137
  *
@@ -23,6 +23,13 @@ export interface PgNotifyListenerOptions {
23
23
  logLabel: string;
24
24
  /** Delay before a reconnect attempt. */
25
25
  reconnectDelayMs?: number;
26
+ /**
27
+ * Called once the connection is listening again after a drop — never after
28
+ * the first connect. Postgres keeps no NOTIFY for a session that is not
29
+ * listening, so whatever was published while the connection was down is
30
+ * gone for good; a caller that mirrors state from the channel resyncs here.
31
+ */
32
+ onReconnect?: () => void;
26
33
  }
27
34
  export declare class PgNotifyListener {
28
35
  private readonly options;
@@ -0,0 +1,19 @@
1
+ import type { CollectionConfig, FetchCollectionProps } from "@rebasepro/types";
2
+ import type { FieldViewer } from "@rebasepro/common";
3
+ /**
4
+ * Refuse a read that names a field its caller may not read, for the doors that
5
+ * take a driver-shaped request rather than a query string: the socket's
6
+ * `FETCH_COLLECTION`, `COUNT` and `CHECK_UNIQUE_FIELD`, and `subscribe_collection`.
7
+ * Callers hand it the request itself, not a re-listed copy of some of its keys.
8
+ *
9
+ * The row strip keeps a withheld value off the wire; this is the other half, and
10
+ * without it the value is still readable one predicate at a time — a `COUNT`
11
+ * filtered on `passwordHash like '$2b$10$A%'` answers 1 or 0. `GET /api/data`
12
+ * refuses the same request with `FIELD_NOT_READABLE`, and this is that rule, not
13
+ * a copy of it: only the request's shape is translated here.
14
+ *
15
+ * The shape arrives as whatever JSON the client sent, so it is read defensively.
16
+ * A malformed `orderBy` or `fields` is not this check's to reject; the entries
17
+ * that name a field are checked and the rest are left for the driver to refuse.
18
+ */
19
+ export declare function assertReadRequestReadable(request: Pick<FetchCollectionProps, "filter" | "logical" | "orderBy" | "fields" | "include" | "vectorSearch">, collection: CollectionConfig, viewer: FieldViewer): void;
@@ -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>;