@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.
Files changed (81) hide show
  1. package/dist/{BranchService-CucnFcSE.js → BranchService-DGPL6G_C.js} +2 -2
  2. package/dist/{BranchService-CucnFcSE.js.map → BranchService-DGPL6G_C.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +41 -1
  4. package/dist/auth/services.d.ts +27 -8
  5. package/dist/backup/backup-cli.d.ts +22 -0
  6. package/dist/{backup-cli-DqBakiMO.js → backup-cli-24v4OlSp.js} +17 -14
  7. package/dist/backup-cli-24v4OlSp.js.map +1 -0
  8. package/dist/{backup-service-CaMOS76G.js → backup-service-HQ9GC4tN.js} +2 -2
  9. package/dist/{backup-service-CaMOS76G.js.map → backup-service-HQ9GC4tN.js.map} +1 -1
  10. package/dist/{cli-errors-Dka89exj.js → cli-errors-B8qHg02P.js} +6 -1
  11. package/dist/cli-errors-B8qHg02P.js.map +1 -0
  12. package/dist/cli-flags-BglvpjHv.js +138 -0
  13. package/dist/cli-flags-BglvpjHv.js.map +1 -0
  14. package/dist/cli-flags.d.ts +28 -0
  15. package/dist/cli-helpers.d.ts +6 -0
  16. package/dist/cli-scratch-database.d.ts +34 -0
  17. package/dist/cli.js +97 -261
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-DF-8dTVa.js} +16 -20
  20. package/dist/column-plan-helpers-DF-8dTVa.js.map +1 -0
  21. package/dist/{doctor-CU9IogdL.js → doctor-Cb2thZ8s.js} +8 -7
  22. package/dist/doctor-Cb2thZ8s.js.map +1 -0
  23. package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
  24. package/dist/{ensure-collection-policies-Dzd-2S81.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
  25. package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
  26. package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
  27. package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-Dhn9KM3B.js} +2 -2
  28. package/dist/{ensure-tables-Cr5B4UmH.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
  29. package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
  30. package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
  31. package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
  32. package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
  33. package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
  34. package/dist/{generate-postgres-ddl-logic-D7imhYV8.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
  35. package/dist/index.es.js +1508 -355
  36. package/dist/index.es.js.map +1 -1
  37. package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-WMuAfxvw.js} +323 -4
  38. package/dist/introspect-db-logic-WMuAfxvw.js.map +1 -0
  39. package/dist/{plan-schema-CboAIwLN.js → plan-schema-C0fxM8dY.js} +158 -31
  40. package/dist/plan-schema-C0fxM8dY.js.map +1 -0
  41. package/dist/{policy-drift-xJfy9xG7.js → policy-drift-DljYdrpW.js} +2 -2
  42. package/dist/{policy-drift-xJfy9xG7.js.map → policy-drift-DljYdrpW.js.map} +1 -1
  43. package/dist/{rls-bootstrap-sql-H3rCFi3F.js → rls-bootstrap-sql-B8EclDyM.js} +320 -17
  44. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +1 -0
  45. package/dist/schema/atlas-argv.d.ts +15 -0
  46. package/dist/schema/column-plan-helpers.d.ts +23 -4
  47. package/dist/schema/destructive-sql.d.ts +52 -1
  48. package/dist/schema/doctor-cli.js +3 -3
  49. package/dist/schema/generate-drizzle-schema.js +1 -1
  50. package/dist/schema/generate-postgres-ddl.js +1 -1
  51. package/dist/schema/introspect-db.js +1 -312
  52. package/dist/schema/introspect-db.js.map +1 -1
  53. package/dist/schema/plan/diff-plan.d.ts +3 -2
  54. package/dist/schema/plan/plan-schema.d.ts +10 -3
  55. package/dist/schema/plan/types.d.ts +6 -1
  56. package/dist/services/FetchService.d.ts +97 -28
  57. package/dist/services/PersistService.d.ts +41 -5
  58. package/dist/services/RelationService.d.ts +11 -0
  59. package/dist/services/RelationWriteService.d.ts +6 -0
  60. package/dist/services/cdc/CdcListener.d.ts +6 -1
  61. package/dist/services/collection-helpers.d.ts +11 -0
  62. package/dist/services/dataService.d.ts +3 -20
  63. package/dist/services/field-op-sql.d.ts +71 -0
  64. package/dist/services/junction-writes.d.ts +19 -3
  65. package/dist/services/pg-notify-listener.d.ts +7 -0
  66. package/dist/services/read-field-access.d.ts +19 -0
  67. package/dist/services/realtimeService.d.ts +77 -3
  68. package/dist/services/row-pipeline.d.ts +5 -0
  69. package/dist/services/soft-delete.d.ts +0 -2
  70. package/dist/services/write-transaction-scope.d.ts +39 -0
  71. package/dist/utils/drizzle-conditions.d.ts +7 -2
  72. package/dist/websocket.d.ts +10 -3
  73. package/package.json +9 -9
  74. package/dist/backup-cli-DqBakiMO.js.map +0 -1
  75. package/dist/cli-errors-Dka89exj.js.map +0 -1
  76. package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
  77. package/dist/doctor-CU9IogdL.js.map +0 -1
  78. package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
  79. package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
  80. package/dist/plan-schema-CboAIwLN.js.map +0 -1
  81. 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, so a link the caller cannot
95
- * read is in neither list and survives the save.
108
+ * this same transaction under the same policies, and through
109
+ * `visibleTargets` — the target's `beforeQuery` scope and soft delete, which
110
+ * RLS knows nothing of — so a link the caller cannot read is in neither list
111
+ * and survives the save.
96
112
  * - **Junction payload columns.** A junction carrying its own columns
97
113
  * (`position`, `role`, `created_at`) lost them on every save, because every
98
114
  * row was re-inserted with only the two keys. Untouched links are left alone.
@@ -115,7 +131,7 @@ export declare function removeJunctionLink(tx: DrizzleClient, binding: JunctionB
115
131
  * that is what keeps a plain `[1, 2, 3]` membership write from wiping the
116
132
  * `role`s off the links it did not mention.
117
133
  */
118
- export declare function applyJunctionMembership(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, links: JunctionLinkWrite[]): Promise<void>;
134
+ export declare function applyJunctionMembership(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, links: JunctionLinkWrite[], visibleTargets?: VisibleTargets): Promise<void>;
119
135
  /**
120
136
  * Set the columns of ONE existing link, without touching the membership.
121
137
  *
@@ -23,6 +23,13 @@ export interface PgNotifyListenerOptions {
23
23
  logLabel: string;
24
24
  /** Delay before a reconnect attempt. */
25
25
  reconnectDelayMs?: number;
26
+ /**
27
+ * Called once the connection is listening again after a drop — never after
28
+ * the first connect. Postgres keeps no NOTIFY for a session that is not
29
+ * listening, so whatever was published while the connection was down is
30
+ * gone for good; a caller that mirrors state from the channel resyncs here.
31
+ */
32
+ onReconnect?: () => void;
26
33
  }
27
34
  export declare class PgNotifyListener {
28
35
  private readonly options;
@@ -0,0 +1,19 @@
1
+ import type { CollectionConfig, FetchCollectionProps } from "@rebasepro/types";
2
+ import type { FieldViewer } from "@rebasepro/common";
3
+ /**
4
+ * Refuse a read that names a field its caller may not read, for the doors that
5
+ * take a driver-shaped request rather than a query string: the socket's
6
+ * `FETCH_COLLECTION`, `COUNT` and `CHECK_UNIQUE_FIELD`, and `subscribe_collection`.
7
+ * Callers hand it the request itself, not a re-listed copy of some of its keys.
8
+ *
9
+ * The row strip keeps a withheld value off the wire; this is the other half, and
10
+ * without it the value is still readable one predicate at a time — a `COUNT`
11
+ * filtered on `passwordHash like '$2b$10$A%'` answers 1 or 0. `GET /api/data`
12
+ * refuses the same request with `FIELD_NOT_READABLE`, and this is that rule, not
13
+ * a copy of it: only the request's shape is translated here.
14
+ *
15
+ * The shape arrives as whatever JSON the client sent, so it is read defensively.
16
+ * A malformed `orderBy` or `fields` is not this check's to reject; the entries
17
+ * that name a field are checked and the rest are left for the driver to refuse.
18
+ */
19
+ export declare function assertReadRequestReadable(request: Pick<FetchCollectionProps, "filter" | "logical" | "orderBy" | "fields" | "include" | "vectorSearch">, collection: CollectionConfig, viewer: FieldViewer): void;
@@ -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
- /** How long an app-emit key suppresses its own CDC echo. Covers NOTIFY round-trip latency. */
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 removed (but not forcefully closed — the
709
- * HTTP server close will handle that).
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, idColumn?: AnyPgColumn, excludeId?: string | number): SQL[];
642
+ static buildUniqueFieldCondition(fieldColumn: AnyPgColumn, value: unknown, excludeRow?: SQL): SQL[];
638
643
  /**
639
644
  * Build relation-based query with joins and conditions
640
645
  */
@@ -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
- export declare function createPostgresWebSocket(server: Server, realtimeService: RealtimeService, driver: PostgresBackendDriver, authConfig?: WsAuthConfig, authAdapter?: AuthAdapter): void;
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.22.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.22.0",
54
- "@rebasepro/common": "0.22.0",
55
- "@rebasepro/server": "0.22.0",
56
- "@rebasepro/types": "0.22.0",
57
- "@rebasepro/utils": "0.22.0"
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.0",
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.10",
73
- "@rebasepro/client": "0.22.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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"}