@rebasepro/server-postgres 0.10.1-canary.31c773c → 0.10.1-canary.3d09338

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 (69) hide show
  1. package/dist/PostgresBootstrapper.d.ts +7 -3
  2. package/dist/auth/services.d.ts +43 -4
  3. package/dist/backup/backup-logic.d.ts +23 -0
  4. package/dist/backup/backup-service.d.ts +44 -2
  5. package/dist/backup/pg-tools.d.ts +41 -1
  6. package/dist/chunk-DSJWtz9O.js +40 -0
  7. package/dist/cli-helpers.d.ts +33 -1
  8. package/dist/ensure-collection-tables-BVvtkkRm.js +304 -0
  9. package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.es.js +1854 -4745
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/schema/auth-schema.d.ts +170 -0
  14. package/dist/schema/destructive-sql.d.ts +49 -0
  15. package/dist/schema/ensure-collection-tables.d.ts +79 -0
  16. package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
  17. package/dist/services/FetchService.d.ts +21 -8
  18. package/dist/services/PersistService.d.ts +12 -0
  19. package/dist/services/RelationService.d.ts +30 -0
  20. package/dist/services/cdc/CdcListener.d.ts +7 -14
  21. package/dist/services/cdc/junction-tables.d.ts +38 -0
  22. package/dist/services/channel-bus/ChannelBus.d.ts +29 -0
  23. package/dist/services/channel-bus/PostgresChannelBus.d.ts +111 -0
  24. package/dist/services/channel-bus/index.d.ts +55 -0
  25. package/dist/services/channel-history.d.ts +11 -0
  26. package/dist/services/channel-presence.d.ts +66 -0
  27. package/dist/services/nested-path.d.ts +59 -0
  28. package/dist/services/pg-notify-listener.d.ts +47 -0
  29. package/dist/services/realtimeService.d.ts +133 -6
  30. package/dist/src-CBgtrPhJ.js +336 -0
  31. package/dist/src-CBgtrPhJ.js.map +1 -0
  32. package/dist/src-lcfUP4xg.js +4106 -0
  33. package/dist/src-lcfUP4xg.js.map +1 -0
  34. package/dist/utils/drizzle-conditions.d.ts +41 -0
  35. package/package.json +8 -9
  36. package/src/PostgresBootstrapper.ts +82 -4
  37. package/src/auth/ensure-tables.ts +70 -56
  38. package/src/auth/services.ts +186 -48
  39. package/src/backup/backup-cli.ts +60 -1
  40. package/src/backup/backup-cron.ts +24 -1
  41. package/src/backup/backup-logic.ts +62 -0
  42. package/src/backup/backup-service.ts +132 -13
  43. package/src/backup/pg-tools.ts +70 -2
  44. package/src/cli-helpers.ts +82 -27
  45. package/src/cli.ts +152 -6
  46. package/src/index.ts +4 -0
  47. package/src/schema/auth-schema.ts +41 -3
  48. package/src/schema/destructive-sql.ts +94 -0
  49. package/src/schema/doctor.ts +6 -6
  50. package/src/schema/ensure-collection-tables.test.ts +156 -0
  51. package/src/schema/ensure-collection-tables.ts +297 -0
  52. package/src/schema/generate-drizzle-schema-logic.ts +13 -9
  53. package/src/schema/generate-postgres-ddl-logic.ts +22 -15
  54. package/src/schema/introspect-db-inference.ts +13 -13
  55. package/src/schema/introspect-db-logic.ts +6 -6
  56. package/src/services/FetchService.ts +104 -114
  57. package/src/services/PersistService.ts +127 -85
  58. package/src/services/RelationService.ts +95 -6
  59. package/src/services/cdc/CdcListener.ts +27 -91
  60. package/src/services/cdc/junction-tables.ts +91 -0
  61. package/src/services/channel-bus/ChannelBus.ts +44 -0
  62. package/src/services/channel-bus/PostgresChannelBus.ts +299 -0
  63. package/src/services/channel-bus/index.ts +123 -0
  64. package/src/services/channel-history.ts +35 -0
  65. package/src/services/channel-presence.ts +148 -0
  66. package/src/services/nested-path.ts +130 -0
  67. package/src/services/pg-notify-listener.ts +137 -0
  68. package/src/services/realtimeService.ts +443 -14
  69. package/src/utils/drizzle-conditions.ts +154 -0
@@ -232,6 +232,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
232
232
  }, {}, {
233
233
  $type: Record<string, unknown>;
234
234
  }>;
235
+ tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
236
+ name: "tokens_valid_after";
237
+ tableName: "users";
238
+ dataType: "date";
239
+ columnType: "PgTimestamp";
240
+ data: Date;
241
+ driverParam: string;
242
+ notNull: false;
243
+ hasDefault: false;
244
+ isPrimaryKey: false;
245
+ isAutoincrement: false;
246
+ hasRuntimeDefault: false;
247
+ enumValues: undefined;
248
+ baseColumn: never;
249
+ identity: undefined;
250
+ generated: undefined;
251
+ }, {}, {}>;
235
252
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
236
253
  name: "created_at";
237
254
  tableName: "users";
@@ -307,6 +324,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
307
324
  identity: undefined;
308
325
  generated: undefined;
309
326
  }, {}, {}>;
327
+ sessionId: import("drizzle-orm/pg-core").PgColumn<{
328
+ name: "session_id";
329
+ tableName: "refresh_tokens";
330
+ dataType: "string";
331
+ columnType: "PgUUID";
332
+ data: string;
333
+ driverParam: string;
334
+ notNull: true;
335
+ hasDefault: true;
336
+ isPrimaryKey: false;
337
+ isAutoincrement: false;
338
+ hasRuntimeDefault: false;
339
+ enumValues: undefined;
340
+ baseColumn: never;
341
+ identity: undefined;
342
+ generated: undefined;
343
+ }, {}, {}>;
310
344
  tokenHash: import("drizzle-orm/pg-core").PgColumn<{
311
345
  name: "token_hash";
312
346
  tableName: "refresh_tokens";
@@ -343,6 +377,57 @@ export declare function createAuthSchema(usersSchemaName?: string): {
343
377
  identity: undefined;
344
378
  generated: undefined;
345
379
  }, {}, {}>;
380
+ revoked: import("drizzle-orm/pg-core").PgColumn<{
381
+ name: "revoked";
382
+ tableName: "refresh_tokens";
383
+ dataType: "boolean";
384
+ columnType: "PgBoolean";
385
+ data: boolean;
386
+ driverParam: boolean;
387
+ notNull: true;
388
+ hasDefault: true;
389
+ isPrimaryKey: false;
390
+ isAutoincrement: false;
391
+ hasRuntimeDefault: false;
392
+ enumValues: undefined;
393
+ baseColumn: never;
394
+ identity: undefined;
395
+ generated: undefined;
396
+ }, {}, {}>;
397
+ rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
398
+ name: "rotated_at";
399
+ tableName: "refresh_tokens";
400
+ dataType: "date";
401
+ columnType: "PgTimestamp";
402
+ data: Date;
403
+ driverParam: string;
404
+ notNull: false;
405
+ hasDefault: false;
406
+ isPrimaryKey: false;
407
+ isAutoincrement: false;
408
+ hasRuntimeDefault: false;
409
+ enumValues: undefined;
410
+ baseColumn: never;
411
+ identity: undefined;
412
+ generated: undefined;
413
+ }, {}, {}>;
414
+ sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
415
+ name: "session_started_at";
416
+ tableName: "refresh_tokens";
417
+ dataType: "date";
418
+ columnType: "PgTimestamp";
419
+ data: Date;
420
+ driverParam: string;
421
+ notNull: true;
422
+ hasDefault: true;
423
+ isPrimaryKey: false;
424
+ isAutoincrement: false;
425
+ hasRuntimeDefault: false;
426
+ enumValues: undefined;
427
+ baseColumn: never;
428
+ identity: undefined;
429
+ generated: undefined;
430
+ }, {}, {}>;
346
431
  userAgent: import("drizzle-orm/pg-core").PgColumn<{
347
432
  name: "user_agent";
348
433
  tableName: "refresh_tokens";
@@ -1398,6 +1483,23 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
1398
1483
  }, {}, {
1399
1484
  $type: Record<string, unknown>;
1400
1485
  }>;
1486
+ tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
1487
+ name: "tokens_valid_after";
1488
+ tableName: "users";
1489
+ dataType: "date";
1490
+ columnType: "PgTimestamp";
1491
+ data: Date;
1492
+ driverParam: string;
1493
+ notNull: false;
1494
+ hasDefault: false;
1495
+ isPrimaryKey: false;
1496
+ isAutoincrement: false;
1497
+ hasRuntimeDefault: false;
1498
+ enumValues: undefined;
1499
+ baseColumn: never;
1500
+ identity: undefined;
1501
+ generated: undefined;
1502
+ }, {}, {}>;
1401
1503
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
1402
1504
  name: "created_at";
1403
1505
  tableName: "users";
@@ -1473,6 +1575,23 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
1473
1575
  identity: undefined;
1474
1576
  generated: undefined;
1475
1577
  }, {}, {}>;
1578
+ sessionId: import("drizzle-orm/pg-core").PgColumn<{
1579
+ name: "session_id";
1580
+ tableName: "refresh_tokens";
1581
+ dataType: "string";
1582
+ columnType: "PgUUID";
1583
+ data: string;
1584
+ driverParam: string;
1585
+ notNull: true;
1586
+ hasDefault: true;
1587
+ isPrimaryKey: false;
1588
+ isAutoincrement: false;
1589
+ hasRuntimeDefault: false;
1590
+ enumValues: undefined;
1591
+ baseColumn: never;
1592
+ identity: undefined;
1593
+ generated: undefined;
1594
+ }, {}, {}>;
1476
1595
  tokenHash: import("drizzle-orm/pg-core").PgColumn<{
1477
1596
  name: "token_hash";
1478
1597
  tableName: "refresh_tokens";
@@ -1509,6 +1628,57 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
1509
1628
  identity: undefined;
1510
1629
  generated: undefined;
1511
1630
  }, {}, {}>;
1631
+ revoked: import("drizzle-orm/pg-core").PgColumn<{
1632
+ name: "revoked";
1633
+ tableName: "refresh_tokens";
1634
+ dataType: "boolean";
1635
+ columnType: "PgBoolean";
1636
+ data: boolean;
1637
+ driverParam: boolean;
1638
+ notNull: true;
1639
+ hasDefault: true;
1640
+ isPrimaryKey: false;
1641
+ isAutoincrement: false;
1642
+ hasRuntimeDefault: false;
1643
+ enumValues: undefined;
1644
+ baseColumn: never;
1645
+ identity: undefined;
1646
+ generated: undefined;
1647
+ }, {}, {}>;
1648
+ rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
1649
+ name: "rotated_at";
1650
+ tableName: "refresh_tokens";
1651
+ dataType: "date";
1652
+ columnType: "PgTimestamp";
1653
+ data: Date;
1654
+ driverParam: string;
1655
+ notNull: false;
1656
+ hasDefault: false;
1657
+ isPrimaryKey: false;
1658
+ isAutoincrement: false;
1659
+ hasRuntimeDefault: false;
1660
+ enumValues: undefined;
1661
+ baseColumn: never;
1662
+ identity: undefined;
1663
+ generated: undefined;
1664
+ }, {}, {}>;
1665
+ sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
1666
+ name: "session_started_at";
1667
+ tableName: "refresh_tokens";
1668
+ dataType: "date";
1669
+ columnType: "PgTimestamp";
1670
+ data: Date;
1671
+ driverParam: string;
1672
+ notNull: true;
1673
+ hasDefault: true;
1674
+ isPrimaryKey: false;
1675
+ isAutoincrement: false;
1676
+ hasRuntimeDefault: false;
1677
+ enumValues: undefined;
1678
+ baseColumn: never;
1679
+ identity: undefined;
1680
+ generated: undefined;
1681
+ }, {}, {}>;
1512
1682
  userAgent: import("drizzle-orm/pg-core").PgColumn<{
1513
1683
  name: "user_agent";
1514
1684
  tableName: "refresh_tokens";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pure helpers that classify an Atlas declarative-apply plan as destructive
3
+ * or not, and decide what `rebase db push` should do about it.
4
+ *
5
+ * `db push` runs `atlas schema apply` to make the live database match the
6
+ * generated `schema.sql`. Removing a collection field compiles to
7
+ * `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
8
+ * We first run the apply with `--dry-run` to obtain the planned SQL, scan it
9
+ * here, and refuse to auto-approve anything destructive without an explicit
10
+ * opt-in.
11
+ *
12
+ * Everything in this file is side-effect free so it can be unit-tested
13
+ * without Atlas or a database.
14
+ */
15
+ /**
16
+ * Split a SQL script into individual statements, dropping blank lines and
17
+ * `--` comment lines. Deliberately simple: Atlas emits one plain statement
18
+ * per `;`, without string literals that contain semicolons in a schema DDL
19
+ * plan, so a naive split is safe and keeps this dependency-free.
20
+ */
21
+ export declare function splitSqlStatements(sql: string): string[];
22
+ export interface DestructiveStatement {
23
+ /** The offending statement (trimmed, without the trailing `;`). */
24
+ statement: string;
25
+ /** Which destructive operation it was flagged for, e.g. "DROP COLUMN". */
26
+ kind: string;
27
+ }
28
+ /**
29
+ * Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
30
+ * the statements that would destroy data. An empty array means the plan is
31
+ * safe to auto-approve.
32
+ */
33
+ export declare function detectDestructiveStatements(planSql: string): DestructiveStatement[];
34
+ export type PushDecision = "apply" | "confirm" | "refuse";
35
+ /**
36
+ * Decide how `db push` should proceed given the plan's destructiveness and
37
+ * the invocation context.
38
+ *
39
+ * - No destructive statements → `apply` (safe to auto-approve).
40
+ * - Destructive + `--allow-destructive` → `apply` (operator opted in).
41
+ * - Destructive + interactive TTY → `confirm` (prompt before applying).
42
+ * - Destructive + non-interactive → `refuse` (never silently drop data in
43
+ * CI / scripts / agents).
44
+ */
45
+ export declare function decidePushSafety(opts: {
46
+ destructiveCount: number;
47
+ allowDestructive: boolean;
48
+ interactive: boolean;
49
+ }): PushDecision;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Bringing a database up to date with a bundle's collections, additively.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * A managed runtime boots someone else's compiled project against a database it
7
+ * has never seen. Auth tables are ensured at boot already, but collection tables
8
+ * were not created by anything: the platform ran the app and every `/api/data/*`
9
+ * request answered 500 on a missing relation. `rebase db push` cannot help — it
10
+ * is an Atlas-driven CLI command, and the runtime image ships no CLI.
11
+ *
12
+ * ## Why additive-only, forever
13
+ *
14
+ * This runs unattended, against a database with customers' data in it, with no
15
+ * human reading a diff. So it may only ever do things that cannot lose data:
16
+ * create a missing table, add a missing column, create a missing enum type.
17
+ *
18
+ * It will **never** drop a table or a column, narrow a type, or alter a
19
+ * constraint. A removed field leaves its column behind; a renamed field looks
20
+ * like an addition and the old column stays. That is the correct trade for an
21
+ * automated path — the alternative is an unattended process that can silently
22
+ * destroy a column, which is precisely the failure `db push` was hardened
23
+ * against. Destructive changes stay a deliberate, human-reviewed migration.
24
+ *
25
+ * Because of that, this is safe to run on every boot, and re-running it is a
26
+ * no-op.
27
+ */
28
+ import { type CollectionConfig } from "@rebasepro/types";
29
+ /**
30
+ * The subset of a database handle this needs: run a statement, get rows back.
31
+ *
32
+ * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by
33
+ * schema name, and schema names are identifiers — they cannot be bound as
34
+ * parameters anyway. They are validated against {@link SAFE_IDENTIFIER} before
35
+ * they reach a statement, so a config that somehow carried a quote is refused
36
+ * rather than concatenated.
37
+ */
38
+ export interface Queryable {
39
+ query<T = unknown>(sql: string): Promise<{
40
+ rows: T[];
41
+ }>;
42
+ }
43
+ /** What the database currently has, as the planner needs it. */
44
+ export interface ExistingSchema {
45
+ /** `schema.table` → set of column names. */
46
+ tables: Map<string, Set<string>>;
47
+ /** `schema.typename` of every enum type that already exists. */
48
+ enums: Set<string>;
49
+ }
50
+ export interface EnsureAction {
51
+ kind: "create-enum" | "create-table" | "add-column";
52
+ /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
53
+ target: string;
54
+ sql: string;
55
+ }
56
+ export interface EnsurePlan {
57
+ actions: EnsureAction[];
58
+ /** Every statement, in dependency order. Empty when the schema is current. */
59
+ statements: string[];
60
+ }
61
+ /**
62
+ * Decide what to add. Pure — the caller supplies what exists and runs the result.
63
+ *
64
+ * Ordering matters and is deliberate: enum types before the tables and columns
65
+ * that reference them, tables before the columns added to other tables (a new
66
+ * table may be the target of a relation), and nothing is emitted twice.
67
+ */
68
+ export declare function planCollectionSchemaEnsure(collections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
69
+ /** Read what the database has, for the schemas the collections live in. */
70
+ export declare function readExistingSchema(client: Queryable, schemas: string[]): Promise<ExistingSchema>;
71
+ /**
72
+ * Bring the database up to date. Returns what it did.
73
+ *
74
+ * Each statement runs on its own rather than in one transaction: they are all
75
+ * independently safe and idempotent, and a single failure (an enum label that
76
+ * cannot be added, say) should not roll back the tables that were created fine.
77
+ * The error is surfaced with the statement that caused it.
78
+ */
79
+ export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsurePlan>;
@@ -1,4 +1,7 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { CollectionConfig, Property } from "@rebasepro/types";
2
+ export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
3
+ export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
4
+ export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
2
5
  export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
3
6
  includePolicies?: boolean;
4
7
  }) => Promise<string>;
@@ -5,6 +5,7 @@ import type { VectorSearchParams } from "@rebasepro/types";
5
5
  import { RelationService } from "./RelationService";
6
6
  import { DrizzleClient } from "../interfaces";
7
7
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
8
+ import { type NestedPathHop } from "./nested-path";
8
9
  /**
9
10
  * Service for handling all row read operations.
10
11
  * Handles fetching, searching, counting, and filtering rows.
@@ -65,6 +66,22 @@ export declare class FetchService {
65
66
  * Extract cursor pagination conditions from startAfter options.
66
67
  */
67
68
  private buildCursorConditions;
69
+ /**
70
+ * Compile "rows reachable from this parent" into a `WHERE` condition on the
71
+ * target table, so a nested listing can run as an ordinary collection query.
72
+ */
73
+ private buildRelationScope;
74
+ /**
75
+ * Whether `id` is actually reachable at `collectionPath`.
76
+ *
77
+ * Trivially true for a root path. For a nested one it is a real question:
78
+ * the path resolves to the target collection, and matching on the primary
79
+ * key alone made the parent segment decorative — `authors/1/posts/43`
80
+ * returned post 43 whoever wrote it, and the REST layer's delete then
81
+ * deleted it. A row that is not under this parent is reported as absent,
82
+ * which is what a caller addressing it through the parent should see.
83
+ */
84
+ private isAddressableUnder;
68
85
  /**
69
86
  * Fetch a single row by ID
70
87
  */
@@ -83,6 +100,8 @@ export declare class FetchService {
83
100
  databaseId?: string;
84
101
  vectorSearch?: VectorSearchParams;
85
102
  logical?: LogicalCondition;
103
+ /** Narrow to the rows reachable from a parent through a relation. */
104
+ relatedTo?: NestedPathHop;
86
105
  }): Promise<Record<string, unknown>[]>;
87
106
  /**
88
107
  * Fallback path used when db.query is unavailable.
@@ -118,10 +137,6 @@ export declare class FetchService {
118
137
  limit?: number;
119
138
  databaseId?: string;
120
139
  }): Promise<Record<string, unknown>[]>;
121
- /**
122
- * Fetch collection from multi-segment path
123
- */
124
- private fetchCollectionFromPath;
125
140
  /**
126
141
  * Count rows in a collection
127
142
  */
@@ -130,10 +145,6 @@ export declare class FetchService {
130
145
  searchString?: string;
131
146
  databaseId?: string;
132
147
  }): Promise<number>;
133
- /**
134
- * Count rows from multi-segment path
135
- */
136
- private countEntitiesFromPath;
137
148
  /**
138
149
  * Check if a field value is unique
139
150
  */
@@ -160,6 +171,8 @@ export declare class FetchService {
160
171
  searchString?: string;
161
172
  databaseId?: string;
162
173
  vectorSearch?: VectorSearchParams;
174
+ /** Narrow to the rows reachable from a parent through a relation. */
175
+ relatedTo?: NestedPathHop;
163
176
  }, include?: string[]): Promise<Record<string, unknown>[]>;
164
177
  /**
165
178
  * Fetch a single row with optional relation includes for REST API.
@@ -38,6 +38,18 @@ export declare class PersistService {
38
38
  * Delete all rows from a collection
39
39
  */
40
40
  deleteAll(collectionPath: string, _databaseId?: string): Promise<void>;
41
+ /**
42
+ * The column on the *target* table that records the parent, for a create
43
+ * under a nested one-to-many path.
44
+ *
45
+ * Returns `undefined` when the link is not a column at all (a multi-hop
46
+ * `joinPath`), so the caller writes the row without stamping anything.
47
+ *
48
+ * `relation.localKey` is deliberately not consulted: it names a column on
49
+ * the *source* table. Falling back to it here — which is what this used to
50
+ * do, and first — stamped the parent's own foreign key onto the child row.
51
+ */
52
+ private resolveParentForeignKeyColumn;
41
53
  /**
42
54
  * Save an row (create or update)
43
55
  *
@@ -1,6 +1,7 @@
1
1
  import { DrizzleClient } from "../interfaces";
2
2
  import { CollectionConfig, FilterValues, Relation } from "@rebasepro/types";
3
3
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
4
+ import type { NestedPathHop } from "./nested-path";
4
5
  /**
5
6
  * Service for handling all relation-related operations.
6
7
  * Handles fetching, updating, and managing row relations.
@@ -85,6 +86,35 @@ export declare class RelationService {
85
86
  filter?: FilterValues<Extract<keyof M, string>>;
86
87
  databaseId?: string;
87
88
  }): Promise<number>;
89
+ /**
90
+ * Count the target rows a parent reaches through `relation`, narrowed by
91
+ * `additionalFilters` (conditions on the target table).
92
+ *
93
+ * Shared by the public count and by {@link isRelated}, so "how many children
94
+ * does this parent have" and "is this row one of them" are answered by the
95
+ * same join — a membership test that reconstructed the join separately would
96
+ * be free to disagree with the listing it is supposed to gate.
97
+ */
98
+ private countRelatedRows;
99
+ /**
100
+ * Whether `targetId` is actually reachable from the parent named in `hop`.
101
+ *
102
+ * A nested address like `authors/1/posts/43` used to resolve to the target
103
+ * collection and then match on the primary key alone, so the parent segment
104
+ * decided nothing: the row came back, and was updated or deleted, whoever it
105
+ * belonged to. Reads, updates and deletes now all gate on this.
106
+ */
107
+ isRelated(hop: NestedPathHop, targetId: string | number): Promise<boolean>;
108
+ /**
109
+ * Remove the junction row linking a parent to `targetId`, leaving the target
110
+ * row itself alone.
111
+ *
112
+ * This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the
113
+ * target is shared, so deleting the row would remove the tag from every other
114
+ * post that uses it. It used to do exactly that — resolve the path to the
115
+ * `tags` table and delete by primary key.
116
+ */
117
+ unlinkRelatedEntity(tx: DrizzleClient, hop: NestedPathHop, targetId: string | number): Promise<void>;
88
118
  /**
89
119
  * Batch fetch related rows for multiple parent rows to avoid N+1 queries
90
120
  */
@@ -23,19 +23,14 @@ export declare function parseCdcPayload(payload: string): CdcChangeEvent | null;
23
23
  /**
24
24
  * Dedicated Postgres LISTEN client for database-level CDC.
25
25
  *
26
- * Mirrors the resilience of the RealtimeService cross-instance LISTEN client: a
27
- * standalone `pg.Client` (outside the Drizzle pool) that stays connected, and
28
- * transparently reconnects on error/disconnect. Each backend instance runs one,
29
- * so every instance observes every committed change regardless of which
30
- * instance (or external process) made the write.
26
+ * A {@link PgNotifyListener} — a connection outside the Drizzle pool that stays
27
+ * open and repairs itself — plus the parsing that turns a `rebase_cdc` payload
28
+ * into a change event. Each backend instance runs one, so every instance
29
+ * observes every committed change regardless of which instance (or external
30
+ * process) made the write.
31
31
  */
32
32
  export declare class CdcListener {
33
- private readonly connectionString;
34
- private readonly onEvent;
35
- private client?;
36
- private running;
37
- private reconnectTimer?;
38
- private static readonly RECONNECT_DELAY_MS;
33
+ private readonly listener;
39
34
  constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>);
40
35
  /**
41
36
  * Connect and begin listening. Idempotent.
@@ -44,11 +39,9 @@ export declare class CdcListener {
44
39
  * established (or `LISTEN` is refused), this rejects so callers — notably
45
40
  * `REALTIME_CDC=auto` — can detect an unusable connection and fall back to
46
41
  * app-level realtime. Once the initial connection succeeds, later drops
47
- * self-heal via {@link scheduleReconnect}.
42
+ * self-heal in the background.
48
43
  */
49
44
  start(): Promise<void>;
50
45
  /** Stop listening and release the connection. */
51
46
  stop(): Promise<void>;
52
- private connect;
53
- private scheduleReconnect;
54
47
  }
@@ -0,0 +1,38 @@
1
+ import { CollectionConfig } from "@rebasepro/types";
2
+ import { PostgresCollectionRegistry } from "../../collections/PostgresCollectionRegistry";
3
+ /**
4
+ * One end of a many-to-many, as seen from the junction table.
5
+ *
6
+ * A junction table is not a collection, so nothing in the registry maps it to
7
+ * one — which is why a change to it was invisible to change capture. But its
8
+ * rows are exactly the contents of a parent's child list, so a write to it is a
9
+ * change to `<parentSlug>/<sourceId>/<relationKey>` and to nothing else.
10
+ */
11
+ export interface JunctionLink {
12
+ schema: string;
13
+ /** The junction table itself, e.g. `posts_tags`. */
14
+ table: string;
15
+ /** The collection whose relation this is, e.g. `posts`. */
16
+ parentCollection: CollectionConfig;
17
+ /** The relation's key — the path segment a child list is addressed by. */
18
+ relationKey: string;
19
+ /** Junction column holding the parent's id. */
20
+ sourceColumn: string;
21
+ /** Junction column holding the target's id. */
22
+ targetColumn: string;
23
+ }
24
+ /**
25
+ * Every junction table reachable from a registered collection, once per
26
+ * relation that uses it.
27
+ *
28
+ * A junction is listed once per *direction* when both sides declare it, because
29
+ * each direction addresses a different child list: `posts/1/tags` and
30
+ * `tags/t/posts` both change when one link is written.
31
+ */
32
+ export declare function collectJunctionLinks(registry: PostgresCollectionRegistry): JunctionLink[];
33
+ /**
34
+ * Index {@link collectJunctionLinks} by table, under both the qualified and the
35
+ * bare name — a change event carries whatever the trigger reports, and a
36
+ * collection need not declare a schema.
37
+ */
38
+ export declare function buildJunctionLinkMap(registry: PostgresCollectionRegistry): Map<string, JunctionLink[]>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Runtime pieces of the channel bus that are not the contract itself.
3
+ *
4
+ * The interface, the frame shape and the implementer's contract live in
5
+ * `@rebasepro/types` (`types/channel_bus.ts`), so a transport shipped as its own
6
+ * package — a Redis one, say — depends on the contract and not on this database
7
+ * adapter. They are re-exported here for convenience: code already importing
8
+ * from the adapter should not have to know where the types are declared.
9
+ */
10
+ import type { ChannelBusFrame } from "@rebasepro/types";
11
+ export type { ChannelBus, ChannelBusFrame, ChannelBusHandler, ChannelBusConfig, ChannelBusSetting } from "@rebasepro/types";
12
+ export { isChannelBusInstance } from "@rebasepro/types";
13
+ /**
14
+ * The default: no cross-instance delivery at all.
15
+ *
16
+ * This is what every deployment ran before the bus existed, and what a
17
+ * single-instance deployment should keep running — `publish` resolves without
18
+ * touching the network, so the broadcast path is the same handful of `ws.send`
19
+ * calls it always was.
20
+ */
21
+ export declare class MemoryChannelBus {
22
+ readonly kind: "memory";
23
+ readonly maxFrameBytes: number;
24
+ start(): Promise<void>;
25
+ publish(): Promise<void>;
26
+ stop(): Promise<void>;
27
+ }
28
+ /** Encoded size of a frame, for a transport's size check. */
29
+ export declare function frameByteLength(frame: ChannelBusFrame): number;