@rebasepro/server-postgres 0.22.0 → 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-DqBakiMO.js → backup-cli-24v4OlSp.js} +17 -14
- package/dist/backup-cli-24v4OlSp.js.map +1 -0
- package/dist/{backup-service-CaMOS76G.js → backup-service-HQ9GC4tN.js} +2 -2
- package/dist/{backup-service-CaMOS76G.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-CU9IogdL.js → doctor-Cb2thZ8s.js} +8 -7
- package/dist/doctor-Cb2thZ8s.js.map +1 -0
- package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
- package/dist/{ensure-collection-policies-Dzd-2S81.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
- package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
- package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-Dhn9KM3B.js} +2 -2
- package/dist/{ensure-tables-Cr5B4UmH.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
- package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
- package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
- package/dist/index.es.js +1508 -355
- 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-CboAIwLN.js → plan-schema-C0fxM8dY.js} +158 -31
- package/dist/plan-schema-C0fxM8dY.js.map +1 -0
- package/dist/{policy-drift-xJfy9xG7.js → policy-drift-DljYdrpW.js} +2 -2
- package/dist/{policy-drift-xJfy9xG7.js.map → policy-drift-DljYdrpW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-H3rCFi3F.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/services/FetchService.d.ts +97 -28
- package/dist/services/PersistService.d.ts +41 -5
- package/dist/services/RelationService.d.ts +11 -0
- package/dist/services/RelationWriteService.d.ts +6 -0
- package/dist/services/cdc/CdcListener.d.ts +6 -1
- package/dist/services/collection-helpers.d.ts +11 -0
- package/dist/services/dataService.d.ts +3 -20
- 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/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 +7 -2
- package/dist/websocket.d.ts +10 -3
- package/package.json +9 -9
- package/dist/backup-cli-DqBakiMO.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-CU9IogdL.js.map +0 -1
- package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-CboAIwLN.js.map +0 -1
- package/dist/rls-bootstrap-sql-H3rCFi3F.js.map +0 -1
|
@@ -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.
|
|
@@ -3,7 +3,7 @@ import type { VectorSearchParams } from "@rebasepro/types";
|
|
|
3
3
|
import { FetchService } from "./FetchService.js";
|
|
4
4
|
import type { ReadCallContextProvider } from "./read-scope.js";
|
|
5
5
|
import type { WithDeleted } from "./soft-delete.js";
|
|
6
|
-
import { PersistService } from "./PersistService.js";
|
|
6
|
+
import { PersistService, type PersistSaveOptions } from "./PersistService.js";
|
|
7
7
|
import { RelationService } from "./RelationService.js";
|
|
8
8
|
import { DataRepository, DrizzleClient } from "../interfaces.js";
|
|
9
9
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
@@ -52,21 +52,7 @@ export declare class DataService implements DataRepository {
|
|
|
52
52
|
/**
|
|
53
53
|
* Fetch a collection of rows with optional filtering, ordering, and pagination
|
|
54
54
|
*/
|
|
55
|
-
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?:
|
|
56
|
-
filter?: FilterValues<Extract<keyof M, string>>;
|
|
57
|
-
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
58
|
-
logical?: LogicalCondition;
|
|
59
|
-
orderBy?: string | OrderByTuple[];
|
|
60
|
-
order?: "desc" | "asc";
|
|
61
|
-
limit?: number;
|
|
62
|
-
offset?: number;
|
|
63
|
-
startAfter?: Record<string, unknown>;
|
|
64
|
-
searchString?: string;
|
|
65
|
-
databaseId?: string;
|
|
66
|
-
vectorSearch?: VectorSearchParams;
|
|
67
|
-
/** See `FetchCollectionProps.withDeleted`. */
|
|
68
|
-
withDeleted?: WithDeleted;
|
|
69
|
-
}): Promise<Record<string, unknown>[]>;
|
|
55
|
+
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: Parameters<FetchService["fetchCollection"]>[1]): Promise<Record<string, unknown>[]>;
|
|
70
56
|
/**
|
|
71
57
|
* The REST read pipeline: flat rows, with exactly the relations `include`
|
|
72
58
|
* names — see {@link FetchService.fetchCollectionForRest}.
|
|
@@ -132,10 +118,7 @@ export declare class DataService implements DataRepository {
|
|
|
132
118
|
/**
|
|
133
119
|
* Save an row (create or update)
|
|
134
120
|
*/
|
|
135
|
-
save<M extends Record<string, unknown>>(collectionPath: string, values: Partial<M>, id?: string | number, databaseId?: string, options?:
|
|
136
|
-
upsert?: boolean;
|
|
137
|
-
onConflict?: readonly string[];
|
|
138
|
-
}): 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>>;
|
|
139
122
|
/**
|
|
140
123
|
* Delete an row by ID
|
|
141
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;
|
|
@@ -267,7 +267,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
267
267
|
* and always flow through. Keyed → expiry timestamp (ms).
|
|
268
268
|
*/
|
|
269
269
|
private recentAppEmits;
|
|
270
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* How long an app-emit key suppresses its own CDC echo: the refetch
|
|
272
|
+
* debounce, and no longer.
|
|
273
|
+
*
|
|
274
|
+
* The mark cannot tell an echo from another writer's change to the same
|
|
275
|
+
* row, and a save that touches no row of the table (a to-many relation
|
|
276
|
+
* only, an empty payload) is announced with no echo ever coming to consume
|
|
277
|
+
* it. Held for seconds, it swallowed the next change psql, a cron or
|
|
278
|
+
* another instance made to that row. Inside the debounce nothing is lost:
|
|
279
|
+
* the refetch the app emit scheduled has not started yet — its timer fires
|
|
280
|
+
* no earlier than this — and it reads after any commit whose NOTIFY has
|
|
281
|
+
* already arrived. An echo later than that costs one more refetch.
|
|
282
|
+
*/
|
|
271
283
|
private static readonly CDC_DEDUP_WINDOW_MS;
|
|
272
284
|
constructor(db: NodePgDatabase<any>, registry: PostgresCollectionRegistry);
|
|
273
285
|
/**
|
|
@@ -366,6 +378,25 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
366
378
|
* synchronously, so nothing else can have answered the request yet.
|
|
367
379
|
*/
|
|
368
380
|
private reportSocketFetchFailure;
|
|
381
|
+
/**
|
|
382
|
+
* Re-judge everything a socket holds open as the identity it has now.
|
|
383
|
+
*
|
|
384
|
+
* A socket re-authenticates on every session refresh, and when a role is
|
|
385
|
+
* taken away, a tenant claim removed or a different account signs in, the
|
|
386
|
+
* next token says so. Requests read as the new identity at once; the
|
|
387
|
+
* subscriptions kept refetching as the one they were opened with, so a
|
|
388
|
+
* demoted user went on receiving editor-only or another tenant's rows for
|
|
389
|
+
* as long as the view stayed mounted.
|
|
390
|
+
*
|
|
391
|
+
* Each of this client's subscriptions is replaced by one carrying the new
|
|
392
|
+
* identity — a replacement, so a refetch already running as the old one
|
|
393
|
+
* cannot deliver — and refetched, so the view narrows now rather than at
|
|
394
|
+
* the next write. A subscription whose filter, sort or projection names a
|
|
395
|
+
* field the new roles may not read is ended with the refusal a new
|
|
396
|
+
* subscription would get. Channel memberships are put to the authorizer
|
|
397
|
+
* again, when one is installed, and left when it refuses.
|
|
398
|
+
*/
|
|
399
|
+
rescopeClient(clientId: string, authContext: SubscriptionAuthContext): Promise<void>;
|
|
369
400
|
private handleUnsubscribe;
|
|
370
401
|
/**
|
|
371
402
|
* Enhanced notification method that handles nested relation updates.
|
|
@@ -497,6 +528,23 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
497
528
|
* Extract parent paths from a nested path like "posts/70/tags"
|
|
498
529
|
* Returns ["posts", "posts/70"] for the example above
|
|
499
530
|
*/
|
|
531
|
+
/**
|
|
532
|
+
* The other paths a subscriber can address the row written at `path` by.
|
|
533
|
+
*
|
|
534
|
+
* `authors/1/posts` and `posts` are two addresses for the same rows, and
|
|
535
|
+
* subscriptions were matched by the written path's exact string. So a post
|
|
536
|
+
* saved through `authors/1/posts` never reached a subscriber of `posts` or
|
|
537
|
+
* of `posts/43`, and one saved through `posts` never reached a subscriber
|
|
538
|
+
* of `authors/1/posts`, whose list it may just have joined or left.
|
|
539
|
+
*
|
|
540
|
+
* The aliases are the target collection's root path, and every nested path
|
|
541
|
+
* a live subscription holds that lands on the same collection. Each still
|
|
542
|
+
* refetches under its own scope and its own parent, so naming a path here
|
|
543
|
+
* decides only who is asked to look again — never what they are shown.
|
|
544
|
+
*/
|
|
545
|
+
private aliasPaths;
|
|
546
|
+
/** The slug of the collection a path lands on, or `undefined` for one that names none. */
|
|
547
|
+
private collectionSlugAt;
|
|
500
548
|
private getParentPaths;
|
|
501
549
|
/**
|
|
502
550
|
* Install a channel authorizer — see {@link ChannelAuthorizer}.
|
|
@@ -705,8 +753,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
705
753
|
* 1. All debounced refetch timers are cancelled (prevents queries after pool closes).
|
|
706
754
|
* 2. All subscription state and callbacks are cleared.
|
|
707
755
|
* 3. The dedicated LISTEN client (outside the pool) is disconnected.
|
|
708
|
-
* 4. All WebSocket clients are
|
|
709
|
-
*
|
|
756
|
+
* 4. All WebSocket clients are closed with 1001 ("going away"); one that
|
|
757
|
+
* does not answer the close frame is dropped after a short grace.
|
|
710
758
|
*/
|
|
711
759
|
destroy(): Promise<void>;
|
|
712
760
|
/** Whether database-level change capture is currently the active source. */
|
|
@@ -729,6 +777,18 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
729
777
|
* (bypass PgBouncer — LISTEN needs a session connection).
|
|
730
778
|
*/
|
|
731
779
|
enableCdc(connectionString: string): Promise<void>;
|
|
780
|
+
/**
|
|
781
|
+
* Refetch every live subscription, as a change to its path would.
|
|
782
|
+
*
|
|
783
|
+
* Called when a LISTEN connection (CDC, or the cross-instance broadcast)
|
|
784
|
+
* is listening again after a drop. Postgres does not queue NOTIFY for a
|
|
785
|
+
* session that is not listening, so every change committed while it was
|
|
786
|
+
* down — by another instance, psql, a cron — reached nobody here, and a
|
|
787
|
+
* subscriber held its pre-gap rows until some later change to the same
|
|
788
|
+
* collection happened to arrive. Each refetch runs under the
|
|
789
|
+
* subscription's own scope, so this decides only who looks again.
|
|
790
|
+
*/
|
|
791
|
+
private resyncSubscriptions;
|
|
732
792
|
/** Stop the CDC listener and clear its state. */
|
|
733
793
|
stopCdc(): Promise<void>;
|
|
734
794
|
/**
|
|
@@ -769,6 +829,16 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
769
829
|
private handleJunctionCdcEvent;
|
|
770
830
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
771
831
|
private extractIdFromCdcRow;
|
|
832
|
+
/**
|
|
833
|
+
* A captured row with its key under the key's *field* names.
|
|
834
|
+
*
|
|
835
|
+
* The trigger captures the tuple by column — `user_id` — and an address is
|
|
836
|
+
* built from the key's property names — `userId`. Read by property name
|
|
837
|
+
* alone, a key declared apart from its column was never on the captured
|
|
838
|
+
* row, the address fell back to `*`, and no single-row subscriber heard
|
|
839
|
+
* about any write made outside this server.
|
|
840
|
+
*/
|
|
841
|
+
private cdcRowByKeyFields;
|
|
772
842
|
private dedupKey;
|
|
773
843
|
/** Record that this instance just delivered `key` via the app path. */
|
|
774
844
|
private markAppEmit;
|
|
@@ -796,6 +866,10 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
796
866
|
private broadcastChange;
|
|
797
867
|
/**
|
|
798
868
|
* Create and connect the dedicated LISTEN client with auto-reconnect.
|
|
869
|
+
*
|
|
870
|
+
* @param reconnect Set when the client is coming back from a drop: every
|
|
871
|
+
* notification published in the gap is gone, so the subscriptions
|
|
872
|
+
* are refetched once it is listening again.
|
|
799
873
|
*/
|
|
800
874
|
private connectListenClient;
|
|
801
875
|
/**
|
|
@@ -59,6 +59,11 @@ export declare function toRestValues(row: Record<string, unknown>, collection: C
|
|
|
59
59
|
* an `update` that echoes the row back overwrite the real value with the null it
|
|
60
60
|
* was handed.
|
|
61
61
|
*
|
|
62
|
+
* A withheld to-one relation takes its foreign key with it. `bandId: 7` names
|
|
63
|
+
* the salary band exactly as `band: { id: 7 }` does, and the row carries the
|
|
64
|
+
* column beside the relation — so withholding only the relation withheld
|
|
65
|
+
* nothing.
|
|
66
|
+
*
|
|
62
67
|
* `_matches` is filtered rather than deleted: it is the list of fields a text
|
|
63
68
|
* search hit, and a field the caller cannot read must not appear in it even
|
|
64
69
|
* though the array itself is theirs to see.
|
|
@@ -23,8 +23,6 @@ import type { CollectionConfig } from "@rebasepro/types";
|
|
|
23
23
|
* (`assertCollectionConfigs`), where the answer is a config change, rather than
|
|
24
24
|
* at the first delete, where it would be a 500 for whoever pressed the button.
|
|
25
25
|
*/
|
|
26
|
-
/** The default field, when `softDelete: true` names none. */
|
|
27
|
-
export declare const DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
28
26
|
/** How a caller asks about deleted rows. See `FetchCollectionProps.withDeleted`. */
|
|
29
27
|
export type WithDeleted = boolean | "only" | undefined;
|
|
30
28
|
export interface SoftDeleteField {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AmbientTransaction } from "@rebasepro/server";
|
|
2
|
+
import type { RebaseCallContext } from "@rebasepro/types";
|
|
2
3
|
import type { DrizzleClient } from "../interfaces.js";
|
|
3
4
|
type RunSql = (sqlText: string, params?: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
4
5
|
/**
|
|
@@ -24,6 +25,10 @@ export declare class WriteTransactionScope implements AmbientTransaction {
|
|
|
24
25
|
private open;
|
|
25
26
|
private readonly inflight;
|
|
26
27
|
private readonly commitHooks;
|
|
28
|
+
/** The first tracked statement that failed, whether or not its caller caught it. */
|
|
29
|
+
private failure?;
|
|
30
|
+
private readonly settledHooks;
|
|
31
|
+
private settledContext?;
|
|
27
32
|
/** The transaction handle, for statements in this package that are already written against drizzle. */
|
|
28
33
|
tx?: DrizzleClient;
|
|
29
34
|
/** Attach the transaction, once it has begun. */
|
|
@@ -49,6 +54,40 @@ export declare class WriteTransactionScope implements AmbientTransaction {
|
|
|
49
54
|
* connection, as if outside any write.
|
|
50
55
|
*/
|
|
51
56
|
settle(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Refuse to commit a transaction a failed statement has aborted.
|
|
59
|
+
*
|
|
60
|
+
* Once any statement fails, Postgres aborts the whole transaction, and
|
|
61
|
+
* catching the error in JavaScript does not undo that. The COMMIT that
|
|
62
|
+
* follows is answered with the tag `ROLLBACK` and no error, so the driver
|
|
63
|
+
* resolves it: the write reported success — 200, webhooks, realtime,
|
|
64
|
+
* the idempotency record — while nothing was stored. A callback that
|
|
65
|
+
* wrapped a lookup or a job enqueue in `try/catch` was enough.
|
|
66
|
+
*
|
|
67
|
+
* So one statement is asked of the transaction before its commit. On an
|
|
68
|
+
* aborted one it fails with `25P02`, and the write is refused and rolled
|
|
69
|
+
* back instead. A failed `context.data` write is not caught by this: it
|
|
70
|
+
* runs in a savepoint, is undone on its own, and leaves the transaction
|
|
71
|
+
* usable — which is what makes catching one safe.
|
|
72
|
+
*/
|
|
73
|
+
assertCommittable(): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* What a hook deferred with {@link afterSettled} is handed as `context`:
|
|
76
|
+
* the same caller, on a driver where every call is a transaction of its
|
|
77
|
+
* own. The write's transaction is gone by the time the hook runs, so a
|
|
78
|
+
* context bound to it would be bound to a connection back in the pool.
|
|
79
|
+
*/
|
|
80
|
+
setSettledContext(context: RebaseCallContext): void;
|
|
81
|
+
/**
|
|
82
|
+
* Run `fn` once this write's transaction is over, whichever way it went,
|
|
83
|
+
* outside it — for what a failure hook hands off (a job, a queue message,
|
|
84
|
+
* a webhook), which must not ride the transaction the failure rolls back.
|
|
85
|
+
* `false` when this scope has no context to hand it; the caller runs it
|
|
86
|
+
* itself then.
|
|
87
|
+
*/
|
|
88
|
+
afterSettled(fn: (context: RebaseCallContext) => Promise<void>): boolean;
|
|
89
|
+
/** The transaction committed or rolled back: run what was waiting for either. */
|
|
90
|
+
settled(): Promise<void>;
|
|
52
91
|
/**
|
|
53
92
|
* The transaction is over, whichever way it went. Nothing may reach it now:
|
|
54
93
|
* its client is back in the pool, and a callback's detached promise still
|
|
@@ -632,9 +632,14 @@ export declare class DrizzleConditionBuilder {
|
|
|
632
632
|
*/
|
|
633
633
|
static buildSearchRankExpression(searchString: string, table: PgTable<any>, collection: CollectionConfig): SQL | undefined;
|
|
634
634
|
/**
|
|
635
|
-
* Build a unique field check condition
|
|
635
|
+
* Build a unique field check condition: rows holding `value`, other than
|
|
636
|
+
* the one `excludeRow` names.
|
|
637
|
+
*
|
|
638
|
+
* `excludeRow` is the whole identity of the row being saved — every key
|
|
639
|
+
* column — so excluding one member of a composite-key table does not
|
|
640
|
+
* exclude every row sharing its first key part.
|
|
636
641
|
*/
|
|
637
|
-
static buildUniqueFieldCondition(fieldColumn: AnyPgColumn, value: unknown,
|
|
642
|
+
static buildUniqueFieldCondition(fieldColumn: AnyPgColumn, value: unknown, excludeRow?: SQL): SQL[];
|
|
638
643
|
/**
|
|
639
644
|
* Build relation-based query with joins and conditions
|
|
640
645
|
*/
|
package/dist/websocket.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RealtimeService } from "./services/realtimeService.js";
|
|
2
2
|
import { PostgresBackendDriver } from "./PostgresBackendDriver.js";
|
|
3
|
-
import type { AuthAdapter } from "@rebasepro/types";
|
|
4
|
-
import { Server } from "http";
|
|
3
|
+
import type { AuthAdapter, RealtimeSocketLimits } from "@rebasepro/types";
|
|
4
|
+
import type { Server } from "http";
|
|
5
5
|
/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */
|
|
6
6
|
interface WsAuthConfig {
|
|
7
7
|
requireAuth?: boolean;
|
|
@@ -41,5 +41,12 @@ export declare const ADMIN_ONLY_TYPES: Set<string>;
|
|
|
41
41
|
* adding one without answering it is a test failure.
|
|
42
42
|
*/
|
|
43
43
|
export declare const PUBLIC_TYPES: Set<string>;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* `limits` are the data API's, for the same rows reached through this door:
|
|
46
|
+
* frames larger than its body limit close the socket with 1009 before they are
|
|
47
|
+
* read, and every data frame counts in the caller's data-API bucket — the one
|
|
48
|
+
* their HTTP requests count in — rather than only in a budget per connection,
|
|
49
|
+
* which a caller multiplied by opening more.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createPostgresWebSocket(server: Server, realtimeService: RealtimeService, driver: PostgresBackendDriver, authConfig?: WsAuthConfig, authAdapter?: AuthAdapter, limits?: RealtimeSocketLimits): void;
|
|
45
52
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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.
|
|
54
|
-
"@rebasepro/
|
|
55
|
-
"@rebasepro/server": "0.
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/utils": "0.
|
|
53
|
+
"@rebasepro/codegen": "0.23.0",
|
|
54
|
+
"@rebasepro/types": "0.23.0",
|
|
55
|
+
"@rebasepro/server": "0.23.0",
|
|
56
|
+
"@rebasepro/common": "0.23.0",
|
|
57
|
+
"@rebasepro/utils": "0.23.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@electric-sql/pglite": "0.5.6",
|
|
@@ -64,13 +64,13 @@
|
|
|
64
64
|
"@types/node": "^26.1.2",
|
|
65
65
|
"@types/pg": "^8.20.0",
|
|
66
66
|
"@types/ws": "^8.18.1",
|
|
67
|
-
"hono": "^4.13.
|
|
67
|
+
"hono": "^4.13.5",
|
|
68
68
|
"jest": "^30.4.2",
|
|
69
69
|
"ts-jest": "^29.4.12",
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"vite": "^8.1.5",
|
|
72
|
-
"vitest": "^4.1.
|
|
73
|
-
"@rebasepro/client": "0.
|
|
72
|
+
"vitest": "^4.1.11",
|
|
73
|
+
"@rebasepro/client": "0.23.0"
|
|
74
74
|
},
|
|
75
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
76
76
|
"publishConfig": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"backup-cli-DqBakiMO.js","names":[],"sources":["../src/backup/backup-cli.ts"],"sourcesContent":["/**\n * CLI handlers for `rebase db backup`, `rebase db restore`, and\n * `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the\n * backup surface stays self-contained.\n */\nimport arg from \"arg\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport readline from \"readline\";\nimport chalk from \"chalk\";\n// Aliased: `out` is already a local in two of the commands below (the `--out`\n// backup destination).\nimport { out as print, outWarn, outError } from \"../cli-output\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport {\n BackupDestination,\n globalsFileForDump,\n parseBackupDestination,\n parseDbNameFromUrl,\n resolveConnectionString,\n withDatabaseName\n} from \"./pg-tools\";\nimport {\n applyGlobals,\n BackupToolError,\n createDump,\n discardPartialDump,\n ensureDatabaseExists,\n listBackups,\n preflight,\n restoreDump,\n uploadBackup,\n validateDump\n} from \"./backup-service\";\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\n/**\n * Build a StorageController for an object-storage destination from the same\n * `S3_*` env vars the backend uses. Returns `null` for local destinations.\n */\nasync function resolveStorageForDestination(\n dest: BackupDestination,\n env: Record<string, string | undefined>\n): Promise<StorageController | null> {\n if (dest.kind === \"local\") return null;\n if (dest.kind === \"gcs\") {\n const { GCSStorageController } = await import(\"@rebasepro/server\");\n return new GCSStorageController({ type: \"gcs\", bucket: dest.bucket });\n }\n // s3 (also covers R2/MinIO/Hetzner/GCS-interop via S3_ENDPOINT)\n if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {\n throw new BackupToolError(\n \"S3 destination requires S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment.\",\n \"Set the same S3_* variables your backend uses for storage.\"\n );\n }\n const { S3StorageController } = await import(\"@rebasepro/server\");\n return new S3StorageController({\n type: \"s3\",\n bucket: dest.bucket,\n region: env.S3_REGION || \"auto\",\n accessKeyId: env.S3_ACCESS_KEY_ID,\n secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n endpoint: env.S3_ENDPOINT,\n // Unset stays unset, so the controller decides from the endpoint the\n // way it does for the runtime's own storage. `=== \"true\"` made it an\n // explicit `false`, and a MinIO backup addressed the bucket as a host.\n forcePathStyle: parseEnvBoolean(env.S3_FORCE_PATH_STYLE)\n });\n}\n\nfunction requireConnection(): string {\n const conn = resolveConnectionString(process.env);\n if (!conn) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n return conn;\n}\n\nasync function promptConfirm(question: string): Promise<boolean> {\n // Non-interactive shells (CI, pipes) can't answer — treat as \"no\".\n if (!process.stdin.isTTY) return false;\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer: string = await new Promise((resolve) => rl.question(question, resolve));\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backup\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupCommand(rawArgs: string[]): Promise<void> {\n const args = arg(\n {\n \"--out\": String,\n // The same alias `build` and `cloud env pull` carry. `--output` is\n // the canonical spelling in this CLI and this command took only\n // `--out`, so `rebase db backup --output ./backups` wrote to\n // ./backups *and* to the default directory — the flag was ignored\n // and its value became a positional.\n \"--output\": \"--out\",\n \"--exclude-schema\": [String],\n \"--no-owner\": Boolean,\n \"--enable-row-security\": Boolean,\n \"--row-security-role\": String,\n \"-o\": \"--out\"\n },\n { argv: rawArgs.slice(2), permissive: true }\n );\n\n if (args._.includes(\"--help\") || rawArgs.includes(\"--help\")) {\n printBackupHelp();\n return;\n }\n\n const connectionString = requireConnection();\n const dbName = parseDbNameFromUrl(connectionString) ?? \"database\";\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n print(\"\");\n print(chalk.bold(\" 💾 Rebase DB Backup\"));\n print(chalk.gray(` Database: ${dbName}`));\n print(chalk.gray(` Destination: ${out}`));\n print(\"\");\n\n // Version pre-flight (doctor-style).\n const pf = await preflight(\"pg_dump\", connectionString);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n print(chalk.gray(` Using pg_dump ${pf.toolMajor} against server ${pf.serverMajor}.`));\n\n // Opt-in, and loud. With row security on, pg_dump stops refusing to read\n // rows it cannot see and starts leaving them out — an exit-0 backup that\n // is quietly short. Anyone choosing that should know they chose it.\n const rowSecurity = args[\"--enable-row-security\"]\n ? { uid: \"rebase-db-backup\", roles: [args[\"--row-security-role\"] || \"admin\"] }\n : undefined;\n\n if (rowSecurity) {\n outWarn(\"\");\n outWarn(chalk.yellow(\" ⚠ Dumping with row-level security ON.\"));\n outWarn(chalk.gray(` Reading as roles [${rowSecurity.roles.join(\", \")}], which satisfies the generated`));\n outWarn(chalk.gray(\" `admin_full_access` policy. This backup contains exactly the rows those\"));\n outWarn(chalk.gray(\" policies admit — a table whose policies have no admin rule comes out short,\"));\n outWarn(chalk.gray(\" and pg_dump will not say so. Prefer granting the dumping role BYPASSRLS.\"));\n outWarn(\"\");\n }\n\n try {\n if (dest.kind === \"local\") {\n // Honour an explicit `…/name.dump` path; otherwise treat it as a\n // directory and auto-name the file.\n const explicitFile = dest.path.endsWith(\".dump\");\n const dump = await createDump({\n connectionString,\n dbName,\n outDir: explicitFile ? path.dirname(dest.path) : dest.path,\n fileName: explicitFile ? path.basename(dest.path) : undefined,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n await assertDumpValid(dump.localFile);\n print(\"\");\n print(chalk.green(` ✓ Backup written to ${dump.localFile} (${formatBytes(dump.sizeBytes)})`));\n if (dump.globalsFile) {\n print(chalk.gray(` ✓ Roles captured to ${dump.globalsFile} (needed so RLS survives a restore).`));\n }\n } else {\n const storage = await resolveStorageForDestination(dest, process.env);\n const dump = await createDump({\n connectionString,\n dbName,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n try {\n await assertDumpValid(dump.localFile);\n const uploaded = await uploadBackup(storage!, dump.localFile, dest);\n print(\"\");\n print(chalk.green(` ✓ Backup uploaded to ${uploaded.storageUrl} (${formatBytes(dump.sizeBytes)})`));\n // Upload the roles sidecar next to the dump so a restore can\n // recreate roles the dump's GRANT/RLS statements depend on.\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {\n const globalsUpload = await uploadBackup(storage!, dump.globalsFile, dest);\n print(chalk.gray(` ✓ Roles uploaded to ${globalsUpload.storageUrl} (needed so RLS survives a restore).`));\n }\n print(chalk.gray(\" Ensure this bucket is private — backups may contain secrets and PII.\"));\n } finally {\n if (fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db restore <backup>\n// ─────────────────────────────────────────────────────────────────────────\nexport async function restoreCommand(rawArgs: string[]): Promise<void> {\n const args = arg(\n {\n \"--target-db\": String,\n \"--create-db\": Boolean,\n \"--clean\": Boolean,\n \"--no-owner\": Boolean,\n \"--continue-on-error\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\"\n },\n { argv: rawArgs.slice(2), permissive: true }\n );\n\n const backupArg = args._[0];\n if (!backupArg || rawArgs.includes(\"--help\")) {\n printRestoreHelp();\n if (!backupArg && !rawArgs.includes(\"--help\")) process.exit(1);\n return;\n }\n\n const baseConnection = requireConnection();\n\n // Choose the target connection: an explicit --target-db (or --create-db's\n // implied fresh db) swaps the database name so the live one isn't clobbered.\n const targetDb = args[\"--target-db\"] ?? parseDbNameFromUrl(baseConnection) ?? undefined;\n const targetConnection = args[\"--target-db\"]\n ? withDatabaseName(baseConnection, args[\"--target-db\"])\n : baseConnection;\n\n print(\"\");\n print(chalk.bold(\" ♻️ Rebase DB Restore\"));\n print(chalk.gray(` Source: ${backupArg}`));\n print(chalk.gray(` Target: ${targetDb ?? \"(from DATABASE_URL)\"}`));\n print(\"\");\n\n // Resolve the local file to restore from (download object-storage keys).\n // Also resolve the `.globals.sql` roles sidecar so cluster roles can be\n // recreated before the restore — without them the dump's GRANT/RLS\n // statements fail and RLS is silently lost.\n let localFile: string;\n let globalsSql: string | null = null;\n let cleanupTemp = false;\n try {\n if (/^(s3|gs):\\/\\//.test(backupArg)) {\n const dest = parseBackupDestination(backupArg.replace(/\\/[^/]+$/, \"\"));\n const storage = await resolveStorageForDestination(dest, process.env);\n if (!storage) throw new BackupToolError(\"Could not resolve storage for the given URL.\");\n const key = backupArg.replace(/^(s3|gs):\\/\\/[^/]+\\//, \"\");\n const bucket = backupArg.replace(/^(s3|gs):\\/\\/([^/]+)\\/.*$/, \"$2\");\n const file = await storage.getObject(key, bucket);\n if (!file) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-restore-\"));\n localFile = path.join(tmpDir, path.basename(key));\n fs.writeFileSync(localFile, Buffer.from(await file.arrayBuffer()));\n cleanupTemp = true;\n const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);\n if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString(\"utf-8\");\n } else {\n localFile = path.resolve(backupArg);\n if (!fs.existsSync(localFile)) {\n outError(chalk.red(` ✗ Backup file not found: ${localFile}`));\n process.exit(1);\n }\n const globalsPath = globalsFileForDump(localFile);\n if (fs.existsSync(globalsPath)) globalsSql = fs.readFileSync(globalsPath, \"utf-8\");\n }\n\n // Version pre-flight. Check against the base connection — the server\n // version is identical for every database, and the target may not\n // exist yet when --create-db is used.\n const pf = await preflight(\"pg_restore\", baseConnection);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n\n // `--create-db` needs a name before anything else can be decided.\n if (args[\"--create-db\"] && !targetDb) {\n outError(chalk.red(\" ✗ --create-db requires a resolvable target database name (use --target-db).\"));\n process.exit(1);\n }\n\n // Destructive-action gate. Restores overwrite data; never run without\n // an explicit yes (interactive confirmation or --yes).\n //\n // Ahead of `--create-db`, not after it. Creating the database first\n // meant an aborted run had already changed the cluster, while printing\n // \"No changes were made\" — and it left an empty database behind that a\n // second, confirmed run then reported as \"already exists\". The gate is\n // now the first thing that can stop the command, so its own message is\n // true whichever way the answer goes.\n if (!args[\"--yes\"]) {\n outWarn(chalk.yellow(\n ` ⚠️ This will restore into \"${targetDb ?? \"the target database\"}\" and may overwrite existing data.`\n ));\n const confirmed = await promptConfirm(chalk.yellow(\" Type 'yes' to continue: \"));\n if (!confirmed) {\n print(chalk.gray(\" Aborted. No changes were made.\"));\n process.exit(1);\n }\n }\n\n // Create the target database when requested.\n if (args[\"--create-db\"]) {\n const created = await ensureDatabaseExists(baseConnection, targetDb!);\n print(chalk.gray(created ? ` ✓ Created database \"${targetDb}\".` : ` • Database \"${targetDb}\" already exists.`));\n }\n\n // Recreate cluster roles before restoring so GRANT/RLS statements in\n // the dump apply. Best-effort and idempotent (see applyGlobals).\n if (globalsSql) {\n print(chalk.gray(\" Recreating cluster roles from the backup's roles sidecar…\"));\n const { applied, skipped } = await applyGlobals(\n targetConnection,\n globalsSql,\n (m) => print(chalk.gray(m))\n );\n print(chalk.gray(` ✓ Roles: ${applied} applied, ${skipped} skipped (already present or not permitted).`));\n } else {\n outWarn(chalk.yellow(\" ⚠️ No roles sidecar (.globals.sql) accompanies this backup.\"));\n outWarn(chalk.yellow(\" If the dump grants to roles that don't exist (e.g. rebase_user), the restore\"));\n outWarn(chalk.yellow(\" will fail — recreate those roles first, or use a backup that includes its globals.\"));\n }\n\n await restoreDump({\n connectionString: targetConnection,\n inputFile: localFile,\n clean: args[\"--clean\"],\n noOwner: args[\"--no-owner\"],\n // Fail loudly by default so a skipped GRANT never leaves RLS off.\n exitOnError: !args[\"--continue-on-error\"],\n inheritStdio: true\n });\n\n print(\"\");\n print(chalk.green(` ✓ Restore completed into \"${targetDb ?? \"the target database\"}\".`));\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n } finally {\n if (cleanupTemp && typeof localFile! === \"string\" && fs.existsSync(localFile!)) {\n fs.rmSync(path.dirname(localFile!), { recursive: true, force: true });\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backups list\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupsCommand(rawArgs: string[]): Promise<void> {\n const action = rawArgs[2];\n if (!action || action === \"--help\") {\n printBackupsHelp();\n return;\n }\n if (action !== \"list\") {\n outError(chalk.red(`Unknown backups action: \"${action}\". Valid: list`));\n process.exit(1);\n }\n\n const args = arg({ \"--out\": String, \"--output\": \"--out\", \"-o\": \"--out\" }, { argv: rawArgs.slice(3), permissive: true });\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n try {\n const storage = await resolveStorageForDestination(dest, process.env);\n const backups = await listBackups(dest, storage ?? undefined);\n print(\"\");\n if (backups.length === 0) {\n print(chalk.gray(` No backups found at ${out}.`));\n } else {\n print(chalk.bold(` 💾 ${backups.length} backup(s) at ${out}:`));\n print(\"\");\n for (const b of backups) {\n const when = b.createdAt ? b.createdAt.toISOString() : \"unknown date\";\n const name = dest.kind === \"local\" ? path.basename(b.key) : b.key;\n // An empty file is called out rather than listed as a peer of\n // the real ones. Older failures could leave a 0-byte dump here\n // (fixed at the source now), and retention protects the newest\n // by date whatever they contain — so a corpse left in place can\n // hold a `keepMinimum` slot against a backup that matters.\n const empty = b.sizeBytes === 0;\n const size = b.sizeBytes === undefined ? \"\" : ` — ${formatBytes(b.sizeBytes)}`;\n if (empty) {\n print(` ${chalk.red(\"○\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}`)}${chalk.red(\" — EMPTY, not restorable\")}`);\n } else {\n print(` ${chalk.green(\"●\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}${size}`)}`);\n }\n }\n if (backups.some(b => b.sizeBytes === 0)) {\n print(\"\");\n print(chalk.yellow(\" ⚠ Empty files above are leftovers from a failed backup. Delete them:\"));\n print(chalk.gray(\" they count as recent backups for retention but restore nothing.\"));\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n/**\n * Verify a freshly written dump; abort the command if it looks corrupt.\n *\n * Discards the artifact on the way out. Refusing to *report* success was never\n * enough on its own: the file stayed on disk, `rebase db backups list` showed\n * it as an ordinary entry, and retention — which ranks by timestamp and never\n * looks at size — would protect it as one of the `keepMinimum` newest while\n * pruning a real backup underneath it. The roles sidecar goes too; the two are\n * uploaded and pruned as a pair, so half a pair is not a backup either.\n */\nasync function assertDumpValid(localFile: string): Promise<void> {\n const check = await validateDump(localFile);\n if (!check.ok) {\n discardPartialDump(globalsFileForDump(localFile));\n discardPartialDump(localFile);\n throw new BackupToolError(\n `The backup failed validation and was discarded: ${check.reason}`,\n \"The dump was corrupt or truncated, so nothing was kept. Investigate before relying on this destination.\"\n );\n }\n}\n\nfunction reportError(err: unknown): void {\n if (err instanceof BackupToolError) {\n outError(chalk.red(` ✗ ${err.message}`));\n if (err.hint) outError(chalk.gray(` ${err.hint}`));\n } else {\n outError(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));\n }\n}\n\nfunction printBackupHelp(): void {\n print(`\n${chalk.bold(\"rebase db backup\")} — Create a database backup (pg_dump, custom format)\n\n${chalk.green.bold(\"Usage\")}\n rebase db backup [--out <path|s3://bucket/prefix>] [options]\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--out, -o\")} <dest> Local path or s3://…/gs://… URL (default: ./backups)\n ${chalk.blue(\"--exclude-schema\")} <s> Exclude a schema (repeatable)\n ${chalk.blue(\"--no-owner\")} Omit ownership commands from the dump\n ${chalk.blue(\"--enable-row-security\")} Dump as an admin subject instead of failing on RLS\n ${chalk.red(\"(may produce a partial dump — see below)\")}\n ${chalk.blue(\"--row-security-role\")} <r> Role to read as with the flag above (default: admin)\n\n${chalk.green.bold(\"Row-level security\")}\n On a managed Postgres the dumping role usually owns nothing and has no\n BYPASSRLS, so pg_dump refuses:\n\n ERROR: query would be affected by row-level security policy for table \"...\"\n\n That refusal is the safe behaviour. --enable-row-security replaces it by\n reading as an admin subject: Rebase sets app.uid/app.user_roles so the\n generated admin_full_access policy admits the dump. The dump then contains\n exactly what those policies admit ${chalk.red(\"and no error is raised for what they do not\")} —\n a table whose policies lack an admin rule comes out short, silently.\n\n Granting the dumping role BYPASSRLS is the option that keeps a backup\n meaning \"every row\".\n\n${chalk.green.bold(\"Notes\")}\n Backups may contain secrets and PII. Use private storage destinations and\n enable encryption-at-rest. See docs/backups.md.\n`);\n}\n\nfunction printRestoreHelp(): void {\n print(`\n${chalk.bold(\"rebase db restore\")} — Restore a database from a backup (pg_restore)\n\n${chalk.green.bold(\"Usage\")}\n rebase db restore <backup> [options]\n\n${chalk.green.bold(\"Arguments\")}\n <backup> Local .dump file, or s3://…/gs://… object key\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--target-db\")} <name> Restore into this database instead of DATABASE_URL's\n ${chalk.blue(\"--create-db\")} Create the target database first if it doesn't exist\n ${chalk.blue(\"--clean\")} Drop existing objects before recreating them\n ${chalk.blue(\"--no-owner\")} Ignore ownership from the dump\n ${chalk.blue(\"--continue-on-error\")} Log and continue past errors ${chalk.red(\"(may leave RLS un-enforced!)\")}\n ${chalk.blue(\"--yes, -y\")} Skip the interactive confirmation ${chalk.red(\"(destructive!)\")}\n\n${chalk.red.bold(\"Warning\")}\n Restore is destructive and never runs automatically. Without --yes it\n requires an interactive 'yes'. Prefer --create-db/--target-db to restore\n into a fresh database rather than overwriting a live one.\n\n By default the restore aborts on the first error (--exit-on-error) so a\n skipped GRANT never silently leaves RLS un-enforced. Roles are recreated\n from the backup's .globals.sql sidecar first; keep that file next to the\n dump. Use --continue-on-error only when you understand the consequences.\n`);\n}\n\nfunction printBackupsHelp(): void {\n print(`\n${chalk.bold(\"rebase db backups\")} — Manage stored backups\n\n${chalk.green.bold(\"Usage\")}\n rebase db backups list [--out <path|s3://bucket/prefix>]\n rebase db backup list [--out <path|s3://bucket/prefix>] ${chalk.gray(\"(the same thing)\")}\n`);\n}\n"],"mappings":"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"}
|