@rebasepro/server-postgres 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- 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
|
-
}
|