@rebasepro/server-postgres 0.17.3 → 0.18.1

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 (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,103 +0,0 @@
1
- import { logger } from "@rebasepro/server";
2
- import { CDC_CHANNEL } from "./trigger-cdc";
3
- import { PgNotifyListener } from "../pg-notify-listener";
4
-
5
- /**
6
- * A single database change captured by the CDC triggers and delivered over the
7
- * `rebase_cdc` NOTIFY channel.
8
- */
9
- export interface CdcChangeEvent {
10
- schema: string;
11
- table: string;
12
- op: "INSERT" | "UPDATE" | "DELETE";
13
- /**
14
- * The changed tuple (NEW for insert/update, OLD for delete). May be a
15
- * partial identity-only object when the full row overflowed the pg_notify
16
- * size cap — see {@link truncated}.
17
- */
18
- row: Record<string, unknown>;
19
- /** True when the row was reduced to its identity because it was too large to notify. */
20
- truncated?: boolean;
21
- }
22
-
23
- /**
24
- * Parse a `rebase_cdc` NOTIFY payload. Returns `null` for anything malformed so
25
- * a single bad message can never crash the listener.
26
- */
27
- export function parseCdcPayload(payload: string): CdcChangeEvent | null {
28
- let parsed: unknown;
29
- try {
30
- parsed = JSON.parse(payload);
31
- } catch {
32
- return null;
33
- }
34
- if (!parsed || typeof parsed !== "object") return null;
35
-
36
- const obj = parsed as Record<string, unknown>;
37
- const schema = typeof obj.schema === "string" ? obj.schema : undefined;
38
- const table = typeof obj.table === "string" ? obj.table : undefined;
39
- const op = obj.op;
40
- if (!schema || !table) return null;
41
- if (op !== "INSERT" && op !== "UPDATE" && op !== "DELETE") return null;
42
-
43
- const row = obj.row && typeof obj.row === "object" ? (obj.row as Record<string, unknown>) : {};
44
-
45
- return {
46
- schema,
47
- table,
48
- op,
49
- row,
50
- truncated: obj.truncated === true
51
- };
52
- }
53
-
54
- /**
55
- * Dedicated Postgres LISTEN client for database-level CDC.
56
- *
57
- * A {@link PgNotifyListener} — a connection outside the Drizzle pool that stays
58
- * open and repairs itself — plus the parsing that turns a `rebase_cdc` payload
59
- * into a change event. Each backend instance runs one, so every instance
60
- * observes every committed change regardless of which instance (or external
61
- * process) made the write.
62
- */
63
- export class CdcListener {
64
- private readonly listener: PgNotifyListener;
65
-
66
- constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>) {
67
- this.listener = new PgNotifyListener({
68
- connectionString,
69
- channel: CDC_CHANNEL,
70
- logLabel: "[CDC]",
71
- onPayload: (payload) => {
72
- const event = parseCdcPayload(payload);
73
- if (!event) {
74
- logger.warn("⚠️ [CDC] Dropping unparseable change notification.");
75
- return;
76
- }
77
- return onEvent(event);
78
- }
79
- });
80
- }
81
-
82
- /**
83
- * Connect and begin listening. Idempotent.
84
- *
85
- * The **initial** connection is validated synchronously: if it cannot be
86
- * established (or `LISTEN` is refused), this rejects so callers — notably
87
- * `REALTIME_CDC=auto` — can detect an unusable connection and fall back to
88
- * app-level realtime. Once the initial connection succeeds, later drops
89
- * self-heal in the background.
90
- */
91
- async start(): Promise<void> {
92
- if (this.listener.active) {
93
- logger.warn("⚠️ [CDC] CdcListener.start() called but already running. Ignoring.");
94
- return;
95
- }
96
- await this.listener.start();
97
- }
98
-
99
- /** Stop listening and release the connection. */
100
- async stop(): Promise<void> {
101
- await this.listener.stop();
102
- }
103
- }
@@ -1,91 +0,0 @@
1
- import { CollectionConfig, ResolvedRelation, isManyToMany } from "@rebasepro/types";
2
- import { resolveCollectionRelations } from "@rebasepro/common";
3
-
4
- import { PostgresCollectionRegistry } from "../../collections/PostgresCollectionRegistry";
5
-
6
- /**
7
- * One end of a many-to-many, as seen from the junction table.
8
- *
9
- * A junction table is not a collection, so nothing in the registry maps it to
10
- * one — which is why a change to it was invisible to change capture. But its
11
- * rows are exactly the contents of a parent's child list, so a write to it is a
12
- * change to `<parentSlug>/<sourceId>/<relationKey>` and to nothing else.
13
- */
14
- export interface JunctionLink {
15
- schema: string;
16
- /** The junction table itself, e.g. `posts_tags`. */
17
- table: string;
18
- /** The collection whose relation this is, e.g. `posts`. */
19
- parentCollection: CollectionConfig;
20
- /** The relation's key — the path segment a child list is addressed by. */
21
- relationKey: string;
22
- /** Junction column holding the parent's id. */
23
- sourceColumn: string;
24
- /** Junction column holding the target's id. */
25
- targetColumn: string;
26
- }
27
-
28
- /**
29
- * Every junction table reachable from a registered collection, once per
30
- * relation that uses it.
31
- *
32
- * A junction is listed once per *direction* when both sides declare it, because
33
- * each direction addresses a different child list: `posts/1/tags` and
34
- * `tags/t/posts` both change when one link is written.
35
- */
36
- export function collectJunctionLinks(registry: PostgresCollectionRegistry): JunctionLink[] {
37
- const links: JunctionLink[] = [];
38
- const seen = new Set<string>();
39
-
40
- for (const collection of registry.getCollections()) {
41
- let relations: Record<string, ResolvedRelation>;
42
- try {
43
- relations = resolveCollectionRelations(collection);
44
- } catch {
45
- // A collection whose relations cannot be resolved (an unresolvable
46
- // target, typically mid-migration) simply contributes none.
47
- continue;
48
- }
49
-
50
- for (const [relationKey, relation] of Object.entries(relations)) {
51
- if (!isManyToMany(relation)) continue;
52
- const through = relation.through;
53
-
54
- // Same relation registered under both its canonical name and the
55
- // declaring property key would otherwise notify the same path twice.
56
- const key = `${collection.slug}::${relationKey}::${through.table}`;
57
- if (seen.has(key)) continue;
58
- seen.add(key);
59
-
60
- links.push({
61
- schema: (collection as { schema?: string }).schema ?? "public",
62
- table: through.table,
63
- parentCollection: collection,
64
- relationKey,
65
- sourceColumn: through.sourceColumn,
66
- targetColumn: through.targetColumn
67
- });
68
- }
69
- }
70
-
71
- return links;
72
- }
73
-
74
- /**
75
- * Index {@link collectJunctionLinks} by table, under both the qualified and the
76
- * bare name — a change event carries whatever the trigger reports, and a
77
- * collection need not declare a schema.
78
- */
79
- export function buildJunctionLinkMap(registry: PostgresCollectionRegistry): Map<string, JunctionLink[]> {
80
- const map = new Map<string, JunctionLink[]>();
81
-
82
- for (const link of collectJunctionLinks(registry)) {
83
- for (const key of [`${link.schema}.${link.table}`, link.table]) {
84
- const existing = map.get(key);
85
- if (existing) existing.push(link);
86
- else map.set(key, [link]);
87
- }
88
- }
89
-
90
- return map;
91
- }
@@ -1,173 +0,0 @@
1
- import { logger } from "@rebasepro/server";
2
- import type { RawSqlRunner } from "../../security/rls-enforcement";
3
-
4
- /**
5
- * Trigger-based Change Data Capture (CDC).
6
- *
7
- * The preferred CDC source is the write-ahead log (logical replication), which
8
- * — like Supabase Realtime — sees *every* commit regardless of how it was made.
9
- * When logical replication is unavailable (managed Postgres without
10
- * `wal_level=logical`, no replication privilege, no `REPLICA IDENTITY`), this
11
- * trigger-based fallback provides the same guarantee at the row level:
12
- *
13
- * AFTER INSERT/UPDATE/DELETE trigger → pg_notify('rebase_cdc', payload)
14
- *
15
- * A single dedicated LISTEN client per backend instance consumes the channel
16
- * (see {@link CdcListener}) and feeds the change into the existing
17
- * `RealtimeService.notifyUpdate` pipeline, so subscribers see the change no
18
- * matter what wrote it — psql, a cron in another service, raw Drizzle/SQL, or
19
- * the Studio SQL editor.
20
- *
21
- * Provisioning runs from the framework's own bootstrap as the owner (server)
22
- * context, alongside the RLS role provisioning. It is idempotent.
23
- */
24
-
25
- /** Postgres NOTIFY channel carrying database-level change events. */
26
- export const CDC_CHANNEL = "rebase_cdc";
27
-
28
- /** Schema-qualified name of the generic trigger function. */
29
- export const CDC_TRIGGER_FUNCTION = "rebase.rebase_cdc_notify";
30
-
31
- /** Name of the per-table trigger (unqualified — triggers are namespaced by table). */
32
- export const CDC_TRIGGER_NAME = "rebase_cdc_trigger";
33
-
34
- /**
35
- * pg_notify hard-caps payloads at 8000 bytes and *aborts the triggering
36
- * statement* if the limit is exceeded. We stay comfortably under it and, for
37
- * wide rows, fall back to an identity-only payload so CDC can never break a
38
- * write. 7900 leaves headroom for the JSON envelope keys.
39
- */
40
- const MAX_NOTIFY_BYTES = 7900;
41
-
42
- const quoteIdent = (name: string): string => `"${name.replace(/"/g, "\"\"")}"`;
43
- const quoteLiteral = (value: string): string => `'${value.replace(/'/g, "''")}'`;
44
-
45
- /**
46
- * SQL that (re)creates the generic CDC trigger function. Safe to run repeatedly:
47
- * `CREATE OR REPLACE` updates in place without dropping dependent triggers.
48
- *
49
- * The function emits `{ schema, table, op, row }`. The `row` is the full changed
50
- * tuple (NEW for insert/update, OLD for delete) so the consumer can route it to
51
- * a collection and extract the primary key. It is *not* trusted for delivery:
52
- * the consumer marks the row invalidated and each subscriber re-reads it under
53
- * its own RLS context, so a subscriber never receives a row it cannot read.
54
- */
55
- export function buildCdcFunctionSql(): string {
56
- return `
57
- CREATE SCHEMA IF NOT EXISTS rebase;
58
-
59
- CREATE OR REPLACE FUNCTION ${CDC_TRIGGER_FUNCTION}() RETURNS trigger
60
- LANGUAGE plpgsql AS $rebase_cdc$
61
- DECLARE
62
- rec jsonb;
63
- payload text;
64
- BEGIN
65
- IF (TG_OP = 'DELETE') THEN
66
- rec := to_jsonb(OLD);
67
- ELSE
68
- rec := to_jsonb(NEW);
69
- END IF;
70
-
71
- payload := json_build_object(
72
- 'schema', TG_TABLE_SCHEMA,
73
- 'table', TG_TABLE_NAME,
74
- 'op', TG_OP,
75
- 'row', rec
76
- )::text;
77
-
78
- -- Never let CDC abort the write: if the full row overflows the pg_notify
79
- -- 8000-byte cap, emit an identity-only payload the consumer can still route
80
- -- (and refetch the authoritative row from).
81
- IF (octet_length(payload) > ${MAX_NOTIFY_BYTES}) THEN
82
- payload := json_build_object(
83
- 'schema', TG_TABLE_SCHEMA,
84
- 'table', TG_TABLE_NAME,
85
- 'op', TG_OP,
86
- 'row', CASE WHEN rec ? 'id' THEN jsonb_build_object('id', rec->'id') ELSE '{}'::jsonb END,
87
- 'truncated', true
88
- )::text;
89
- END IF;
90
-
91
- PERFORM pg_notify(${quoteLiteral(CDC_CHANNEL)}, payload);
92
- RETURN NULL;
93
- END;
94
- $rebase_cdc$;
95
- `.trim();
96
- }
97
-
98
- /**
99
- * SQL that (re)attaches the CDC trigger to a single table. `DROP ... IF EXISTS`
100
- * before `CREATE` keeps it idempotent and picks up any function signature change.
101
- */
102
- export function buildCdcTriggerSql(schema: string, table: string): string {
103
- const qualified = `${quoteIdent(schema)}.${quoteIdent(table)}`;
104
- return (
105
- `DROP TRIGGER IF EXISTS ${quoteIdent(CDC_TRIGGER_NAME)} ON ${qualified};\n` +
106
- `CREATE TRIGGER ${quoteIdent(CDC_TRIGGER_NAME)} ` +
107
- `AFTER INSERT OR UPDATE OR DELETE ON ${qualified} ` +
108
- `FOR EACH ROW EXECUTE FUNCTION ${CDC_TRIGGER_FUNCTION}();`
109
- );
110
- }
111
-
112
- export interface CdcTableRef {
113
- schema: string;
114
- table: string;
115
- }
116
-
117
- export interface ProvisionResult {
118
- /** Tables the trigger was successfully attached to. */
119
- installed: CdcTableRef[];
120
- /** Tables that could not be provisioned (e.g. not yet migrated), with the error. */
121
- skipped: Array<CdcTableRef & { reason: string }>;
122
- }
123
-
124
- /**
125
- * Idempotently install the CDC trigger function and per-table triggers.
126
- *
127
- * Runs as the owner (server) connection at bootstrap. A table that does not yet
128
- * exist in the database (schema drift) is skipped with a warning rather than
129
- * aborting the whole install, so one un-migrated collection cannot disable CDC
130
- * for the rest.
131
- */
132
- export async function provisionTriggerCdc(
133
- run: RawSqlRunner,
134
- tables: CdcTableRef[]
135
- ): Promise<ProvisionResult> {
136
- // 1. The shared trigger function (once).
137
- await run(buildCdcFunctionSql());
138
-
139
- // 2. One trigger per managed table. De-duplicate identical refs.
140
- const seen = new Set<string>();
141
- const installed: CdcTableRef[] = [];
142
- const skipped: ProvisionResult["skipped"] = [];
143
-
144
- for (const ref of tables) {
145
- const key = `${ref.schema}.${ref.table}`;
146
- if (seen.has(key)) continue;
147
- seen.add(key);
148
-
149
- try {
150
- await run(buildCdcTriggerSql(ref.schema, ref.table));
151
- installed.push(ref);
152
- } catch (err) {
153
- const reason = err instanceof Error ? err.message : String(err);
154
- skipped.push({ ...ref, reason });
155
- logger.warn(
156
- `⚠️ [CDC] Could not attach change-capture trigger to "${key}" — ` +
157
- `is the table migrated? Writes to it won't emit database-level events.`,
158
- { detail: reason }
159
- );
160
- }
161
- }
162
-
163
- // Wiring detail. The single `Realtime source = …` line in the
164
- // bootstrapper is the fact a developer acts on; how many triggers it
165
- // took is for a diagnosis, and the skipped-table warning above still
166
- // fires on its own.
167
- logger.debug(
168
- `📡 [CDC] Trigger-based change capture provisioned on ${installed.length} table(s)` +
169
- (skipped.length ? ` (${skipped.length} skipped)` : "") + "."
170
- );
171
-
172
- return { installed, skipped };
173
- }
@@ -1,44 +0,0 @@
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
-
11
- import type { ChannelBusFrame } from "@rebasepro/types";
12
-
13
- export type {
14
- ChannelBus,
15
- ChannelBusFrame,
16
- ChannelBusHandler,
17
- ChannelBusConfig,
18
- ChannelBusSetting
19
- } from "@rebasepro/types";
20
- export { isChannelBusInstance } from "@rebasepro/types";
21
-
22
- /**
23
- * The default: no cross-instance delivery at all.
24
- *
25
- * This is what every deployment ran before the bus existed, and what a
26
- * single-instance deployment should keep running — `publish` resolves without
27
- * touching the network, so the broadcast path is the same handful of `ws.send`
28
- * calls it always was.
29
- */
30
- export class MemoryChannelBus {
31
- readonly kind = "memory" as const;
32
- readonly maxFrameBytes = Infinity;
33
-
34
- async start(): Promise<void> { /* nothing to connect */ }
35
-
36
- async publish(): Promise<void> { /* nowhere to publish to */ }
37
-
38
- async stop(): Promise<void> { /* nothing to release */ }
39
- }
40
-
41
- /** Encoded size of a frame, for a transport's size check. */
42
- export function frameByteLength(frame: ChannelBusFrame): number {
43
- return Buffer.byteLength(JSON.stringify(frame), "utf8");
44
- }