@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.
- package/dist/{BranchService-CucnFcSE.js → BranchService-DGPL6G_C.js} +2 -2
- package/dist/{BranchService-CucnFcSE.js.map → BranchService-DGPL6G_C.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +41 -1
- package/dist/auth/services.d.ts +27 -8
- package/dist/backup/backup-cli.d.ts +22 -0
- package/dist/{backup-cli-CkjsJEcu.js → backup-cli-24v4OlSp.js} +17 -14
- package/dist/backup-cli-24v4OlSp.js.map +1 -0
- package/dist/{backup-service-BNLwvxuy.js → backup-service-HQ9GC4tN.js} +2 -2
- package/dist/{backup-service-BNLwvxuy.js.map → backup-service-HQ9GC4tN.js.map} +1 -1
- package/dist/{cli-errors-Dka89exj.js → cli-errors-B8qHg02P.js} +6 -1
- package/dist/cli-errors-B8qHg02P.js.map +1 -0
- package/dist/cli-flags-BglvpjHv.js +138 -0
- package/dist/cli-flags-BglvpjHv.js.map +1 -0
- package/dist/cli-flags.d.ts +28 -0
- package/dist/cli-helpers.d.ts +6 -0
- package/dist/cli-scratch-database.d.ts +34 -0
- package/dist/cli.js +97 -261
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-DF-8dTVa.js} +16 -20
- package/dist/column-plan-helpers-DF-8dTVa.js.map +1 -0
- package/dist/{doctor-Gzzd8wTq.js → doctor-Cb2thZ8s.js} +8 -7
- package/dist/doctor-Cb2thZ8s.js.map +1 -0
- package/dist/{ensure-collection-policies-B4IDFtQ-.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
- package/dist/{ensure-collection-policies-B4IDFtQ-.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cr2ye5JC.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
- package/dist/{ensure-tables-BnEvEJPr.js → ensure-tables-Dhn9KM3B.js} +2 -2
- package/dist/{ensure-tables-BnEvEJPr.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
- package/dist/{generate-drizzle-schema-D2MDdJt-.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
- package/dist/{generate-drizzle-schema-D2MDdJt-.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
- package/dist/index.es.js +1882 -392
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-WMuAfxvw.js} +323 -4
- package/dist/introspect-db-logic-WMuAfxvw.js.map +1 -0
- package/dist/{plan-schema-Hgl62S-w.js → plan-schema-C0fxM8dY.js} +170 -38
- package/dist/plan-schema-C0fxM8dY.js.map +1 -0
- package/dist/{policy-drift-B0GDRh7_.js → policy-drift-DljYdrpW.js} +2 -2
- package/dist/{policy-drift-B0GDRh7_.js.map → policy-drift-DljYdrpW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-BHA6jLtj.js → rls-bootstrap-sql-B8EclDyM.js} +320 -17
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +1 -0
- package/dist/schema/atlas-argv.d.ts +15 -0
- package/dist/schema/column-plan-helpers.d.ts +23 -4
- package/dist/schema/destructive-sql.d.ts +52 -1
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/introspect-db.js +1 -312
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +3 -2
- package/dist/schema/plan/plan-schema.d.ts +10 -3
- package/dist/schema/plan/types.d.ts +6 -1
- package/dist/schema/search-column.d.ts +21 -1
- package/dist/services/FetchService.d.ts +147 -30
- package/dist/services/PersistService.d.ts +56 -5
- package/dist/services/RelationService.d.ts +42 -1
- package/dist/services/RelationWriteService.d.ts +22 -1
- package/dist/services/cdc/CdcListener.d.ts +6 -1
- package/dist/services/collection-helpers.d.ts +11 -0
- package/dist/services/dataService.d.ts +19 -21
- package/dist/services/field-op-sql.d.ts +71 -0
- package/dist/services/junction-writes.d.ts +19 -3
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/read-field-access.d.ts +19 -0
- package/dist/services/read-scope.d.ts +100 -0
- package/dist/services/realtimeService.d.ts +77 -3
- package/dist/services/row-pipeline.d.ts +5 -0
- package/dist/services/soft-delete.d.ts +0 -2
- package/dist/services/write-transaction-scope.d.ts +39 -0
- package/dist/utils/drizzle-conditions.d.ts +63 -8
- package/dist/websocket.d.ts +10 -3
- package/package.json +9 -9
- package/dist/backup-cli-CkjsJEcu.js.map +0 -1
- package/dist/cli-errors-Dka89exj.js.map +0 -1
- package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
- package/dist/doctor-Gzzd8wTq.js.map +0 -1
- package/dist/ensure-collection-tables-Cr2ye5JC.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-Hgl62S-w.js.map +0 -1
- 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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
95
|
-
*
|
|
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>;
|