@descryy/runtime-database-observation 0.4.8 → 0.5.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.
package/dist/index.d.ts CHANGED
@@ -11,4 +11,12 @@ export type { PostgresQueryCollectorOptions } from "./postgres-query-collector.t
11
11
  export { createPostgresQueryCollector } from "./postgres-query-collector.ts";
12
12
  export type { PostgresStatementLogSource } from "./postgres-log-source.ts";
13
13
  export { createPostgresLogSource } from "./postgres-log-source.ts";
14
+ export type { ParsedRowChange, RowChangeOp } from "./postgres-decoding-parser.ts";
15
+ export { parseTestDecodingLine } from "./postgres-decoding-parser.ts";
16
+ export type { PgRowClient, PostgresRowChangeCollectorOptions } from "./postgres-row-collector.ts";
17
+ export { createPostgresRowChangeCollector } from "./postgres-row-collector.ts";
18
+ export type { SqliteRowSource, SqliteRowOp, SqliteRowDiff, TableSnapshot, SqliteRowChangeCollectorOptions } from "./sqlite-row-snapshot.ts";
19
+ export { createSqliteRowChangeCollector, diffTableSnapshot } from "./sqlite-row-snapshot.ts";
20
+ export type { MysqlRowChangeCollectorOptions } from "./mysql-row-collector.ts";
21
+ export { createMysqlRowChangeCollector, MYSQL_ROW_CAPTURE_REFUSAL_REASON } from "./mysql-row-collector.ts";
14
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEhG,YAAY,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,YAAY,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEhG,YAAY,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,YAAY,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,YAAY,EAAE,WAAW,EAAE,iCAAiC,EAAE,MAAM,6BAA6B,CAAC;AAClG,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AAE/E,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAC5I,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7F,YAAY,EAAE,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -6,4 +6,8 @@ export { resolveObservedDatabaseQuery } from "./graph-mapping.js";
6
6
  export { parsePostgresLogLine } from "./postgres-log-parser.js";
7
7
  export { createPostgresQueryCollector } from "./postgres-query-collector.js";
8
8
  export { createPostgresLogSource } from "./postgres-log-source.js";
9
+ export { parseTestDecodingLine } from "./postgres-decoding-parser.js";
10
+ export { createPostgresRowChangeCollector } from "./postgres-row-collector.js";
11
+ export { createSqliteRowChangeCollector, diffTableSnapshot } from "./sqlite-row-snapshot.js";
12
+ export { createMysqlRowChangeCollector, MYSQL_ROW_CAPTURE_REFUSAL_REASON } from "./mysql-row-collector.js";
9
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAA2C,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGhG,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAA2C,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGhG,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AAG/E,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7F,OAAO,EAAE,6BAA6B,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MySQL/MariaDB row-change capture: a named, remediable refusal, not a
3
+ * binlog implementation. See DEC-NEXT-mysql-row-and-schema-capture-refused-no-driver
4
+ * (descry-core decisions inbox) for the full reasoning — in short, this
5
+ * repo's `pnpm-lock.yaml` carries no MySQL driver (`mysql2` or otherwise)
6
+ * today, and this lane cannot install one, so writing binlog-protocol
7
+ * parsing code that has never run against a real driver would be a worse
8
+ * violation of honest degradation (rule 7) than declining to write it.
9
+ *
10
+ * Shaped as a `Collector` for the same reason `createPostgresRowChangeCollector`
11
+ * is: a caller wiring up database observation for a run can try to start
12
+ * this collector unconditionally, exactly like the others, and get back a
13
+ * refusal it can disclose rather than a branch it has to special-case.
14
+ *
15
+ * The moment `mysql2` (with `binlog_format=ROW` and full row images) becomes
16
+ * an installed dependency, this refusal should be replaced with a real
17
+ * binlog client, following `postgres-row-collector.ts`'s dynamic-import
18
+ * pattern -- optional dependency, loaded lazily, refuse-by-name if absent.
19
+ */
20
+ import type { Collector } from "@descryy/runtime-contracts";
21
+ export interface MysqlRowChangeCollectorOptions {
22
+ readonly connectionString: string;
23
+ readonly service?: string;
24
+ readonly processId?: string;
25
+ }
26
+ export declare const MYSQL_ROW_CAPTURE_REFUSAL_REASON: string;
27
+ export declare function createMysqlRowChangeCollector(options: MysqlRowChangeCollectorOptions): Collector;
28
+ //# sourceMappingURL=mysql-row-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql-row-collector.d.ts","sourceRoot":"","sources":["../src/mysql-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAiE,MAAM,4BAA4B,CAAC;AAE3H,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,gCAAgC,QAEkC,CAAC;AAEhF,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,8BAA8B,GAAG,SAAS,CAmChG"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MySQL/MariaDB row-change capture: a named, remediable refusal, not a
3
+ * binlog implementation. See DEC-NEXT-mysql-row-and-schema-capture-refused-no-driver
4
+ * (descry-core decisions inbox) for the full reasoning — in short, this
5
+ * repo's `pnpm-lock.yaml` carries no MySQL driver (`mysql2` or otherwise)
6
+ * today, and this lane cannot install one, so writing binlog-protocol
7
+ * parsing code that has never run against a real driver would be a worse
8
+ * violation of honest degradation (rule 7) than declining to write it.
9
+ *
10
+ * Shaped as a `Collector` for the same reason `createPostgresRowChangeCollector`
11
+ * is: a caller wiring up database observation for a run can try to start
12
+ * this collector unconditionally, exactly like the others, and get back a
13
+ * refusal it can disclose rather than a branch it has to special-case.
14
+ *
15
+ * The moment `mysql2` (with `binlog_format=ROW` and full row images) becomes
16
+ * an installed dependency, this refusal should be replaced with a real
17
+ * binlog client, following `postgres-row-collector.ts`'s dynamic-import
18
+ * pattern -- optional dependency, loaded lazily, refuse-by-name if absent.
19
+ */
20
+ export const MYSQL_ROW_CAPTURE_REFUSAL_REASON = "MySQL row-change capture needs the optional dependency \"mysql2\" (with binlog_format=ROW and full row images), " +
21
+ "which is not installed in this build; the query-text channel is unaffected.";
22
+ export function createMysqlRowChangeCollector(options) {
23
+ const processId = options.processId ?? "mysql-row-changes";
24
+ function capabilities() {
25
+ const unrelated = {
26
+ availability: "unavailable",
27
+ reason: "MysqlRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
28
+ };
29
+ return {
30
+ domObservation: unrelated,
31
+ consoleObservation: unrelated,
32
+ networkObservation: unrelated,
33
+ backendLogAccess: unrelated,
34
+ distributedTrace: unrelated,
35
+ sourceMapping: unrelated,
36
+ stackCapture: unrelated,
37
+ processLifecycle: unrelated,
38
+ databaseObservation: { availability: "unavailable", reason: MYSQL_ROW_CAPTURE_REFUSAL_REASON },
39
+ externalServiceObservation: unrelated,
40
+ };
41
+ }
42
+ return {
43
+ collectorId: `mysql-row-collector:${processId}`,
44
+ start(_context) {
45
+ return Promise.resolve({ available: false, reason: MYSQL_ROW_CAPTURE_REFUSAL_REASON });
46
+ },
47
+ stop() {
48
+ return Promise.resolve();
49
+ },
50
+ capabilities,
51
+ };
52
+ }
53
+ //# sourceMappingURL=mysql-row-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql-row-collector.js","sourceRoot":"","sources":["../src/mysql-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,kHAAkH;IAClH,6EAA6E,CAAC;AAEhF,MAAM,UAAU,6BAA6B,CAAC,OAAuC;IACnF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,mBAAmB,CAAC;IAE3D,SAAS,YAAY;QACnB,MAAM,SAAS,GAAG;YAChB,YAAY,EAAE,aAAsB;YACpC,MAAM,EAAE,qFAAqF;SAC9F,CAAC;QACF,OAAO;YACL,cAAc,EAAE,SAAS;YACzB,kBAAkB,EAAE,SAAS;YAC7B,kBAAkB,EAAE,SAAS;YAC7B,gBAAgB,EAAE,SAAS;YAC3B,gBAAgB,EAAE,SAAS;YAC3B,aAAa,EAAE,SAAS;YACxB,YAAY,EAAE,SAAS;YACvB,gBAAgB,EAAE,SAAS;YAC3B,mBAAmB,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,gCAAgC,EAAE;YAC9F,0BAA0B,EAAE,SAAS;SACtC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,WAAW,EAAE,uBAAuB,SAAS,EAAE;QAE/C,KAAK,CAAC,QAA0B;YAC9B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC,CAAC;QACzF,CAAC;QAED,IAAI;YACF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,YAAY;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Bounded reader over `test_decoding`'s documented output format (the
3
+ * built-in logical-decoding output plugin every Postgres ships — no
4
+ * extension install needed). `postgres-row-collector.ts` reads each row's
5
+ * `data` column back from `pg_logical_slot_get_changes` and hands it here,
6
+ * one line at a time; `xid` arrives as its own column on that same result
7
+ * row, so this parser does not need to track BEGIN/COMMIT for transaction
8
+ * identity — those lines simply refuse (`null`), same as this package's
9
+ * other parsers (`postgres-log-parser.ts`, `table-extraction.ts`) refuse
10
+ * rather than guess at a shape they don't recognise.
11
+ *
12
+ * Recognised shapes (per https://www.postgresql.org/docs/current/test-decoding.html):
13
+ *
14
+ * - `table <schema>.<table>: INSERT: <col>[<type>]:<value> ...`
15
+ * - `table <schema>.<table>: UPDATE: old-key: <col>[<type>]:<value> ... new-tuple: <col>[<type>]:<value> ...`
16
+ * — present only when the table's `REPLICA IDENTITY` makes an old key
17
+ * decodable at all (FULL, or DEFAULT when the key columns changed).
18
+ * - `table <schema>.<table>: UPDATE: <col>[<type>]:<value> ...` — no
19
+ * `old-key:` segment; the new row only, no before-state in this line.
20
+ * - `table <schema>.<table>: DELETE: <col>[<type>]:<value> ...` — full or
21
+ * key-only, depending on identity.
22
+ * - `table <schema>.<table>: DELETE: (no-tuple-data)` — `REPLICA IDENTITY
23
+ * NOTHING`; no columns at all.
24
+ *
25
+ * Column values are captured as raw strings, never coerced to a JS number/
26
+ * boolean/null — `name[text]:'it''s here'` unescapes SQL-style doubled
27
+ * quotes, but a bareword value (`1`, `null`, `t`, `unchanged-toast-datum`)
28
+ * is reported exactly as `test_decoding` printed it. Coercing to a native
29
+ * type would need the column's Postgres type, which the `[type]` tag names
30
+ * but this parser deliberately does not interpret — same "don't guess past
31
+ * what's declared" stance as `live-schema.ts`'s column reader.
32
+ *
33
+ * What this parser does NOT decide: whether a given row's before-value is
34
+ * "full", "key-only" or "none" for a *table* — that is a property of
35
+ * `REPLICA IDENTITY`, looked up once from `pg_class.relreplident` by
36
+ * `postgres-row-collector.ts` and attached to every emitted event, not
37
+ * inferred from which of the three UPDATE/DELETE shapes above matched.
38
+ */
39
+ export type RowChangeOp = "INSERT" | "UPDATE" | "DELETE";
40
+ export interface ParsedRowChange {
41
+ readonly schema: string;
42
+ readonly table: string;
43
+ readonly op: RowChangeOp;
44
+ readonly before: Record<string, string> | null;
45
+ readonly after: Record<string, string> | null;
46
+ }
47
+ /**
48
+ * `null` for anything not one of the recognised `test_decoding` shapes —
49
+ * including `BEGIN <xid>` / `COMMIT <xid>` lines, which this parser is not
50
+ * asked to interpret (xid comes from the calling row's own column).
51
+ */
52
+ export declare function parseTestDecodingLine(data: string): ParsedRowChange | null;
53
+ //# sourceMappingURL=postgres-decoding-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-decoding-parser.d.ts","sourceRoot":"","sources":["../src/postgres-decoding-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC/C;AA0BD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CA6B1E"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Bounded reader over `test_decoding`'s documented output format (the
3
+ * built-in logical-decoding output plugin every Postgres ships — no
4
+ * extension install needed). `postgres-row-collector.ts` reads each row's
5
+ * `data` column back from `pg_logical_slot_get_changes` and hands it here,
6
+ * one line at a time; `xid` arrives as its own column on that same result
7
+ * row, so this parser does not need to track BEGIN/COMMIT for transaction
8
+ * identity — those lines simply refuse (`null`), same as this package's
9
+ * other parsers (`postgres-log-parser.ts`, `table-extraction.ts`) refuse
10
+ * rather than guess at a shape they don't recognise.
11
+ *
12
+ * Recognised shapes (per https://www.postgresql.org/docs/current/test-decoding.html):
13
+ *
14
+ * - `table <schema>.<table>: INSERT: <col>[<type>]:<value> ...`
15
+ * - `table <schema>.<table>: UPDATE: old-key: <col>[<type>]:<value> ... new-tuple: <col>[<type>]:<value> ...`
16
+ * — present only when the table's `REPLICA IDENTITY` makes an old key
17
+ * decodable at all (FULL, or DEFAULT when the key columns changed).
18
+ * - `table <schema>.<table>: UPDATE: <col>[<type>]:<value> ...` — no
19
+ * `old-key:` segment; the new row only, no before-state in this line.
20
+ * - `table <schema>.<table>: DELETE: <col>[<type>]:<value> ...` — full or
21
+ * key-only, depending on identity.
22
+ * - `table <schema>.<table>: DELETE: (no-tuple-data)` — `REPLICA IDENTITY
23
+ * NOTHING`; no columns at all.
24
+ *
25
+ * Column values are captured as raw strings, never coerced to a JS number/
26
+ * boolean/null — `name[text]:'it''s here'` unescapes SQL-style doubled
27
+ * quotes, but a bareword value (`1`, `null`, `t`, `unchanged-toast-datum`)
28
+ * is reported exactly as `test_decoding` printed it. Coercing to a native
29
+ * type would need the column's Postgres type, which the `[type]` tag names
30
+ * but this parser deliberately does not interpret — same "don't guess past
31
+ * what's declared" stance as `live-schema.ts`'s column reader.
32
+ *
33
+ * What this parser does NOT decide: whether a given row's before-value is
34
+ * "full", "key-only" or "none" for a *table* — that is a property of
35
+ * `REPLICA IDENTITY`, looked up once from `pg_class.relreplident` by
36
+ * `postgres-row-collector.ts` and attached to every emitted event, not
37
+ * inferred from which of the three UPDATE/DELETE shapes above matched.
38
+ */
39
+ const LINE_PATTERN = /^table\s+([^.\s]+)\.([^:\s]+):\s+(INSERT|UPDATE|DELETE):\s*(.*)$/;
40
+ const OLD_KEY_NEW_TUPLE = /^old-key:\s*(.*?)\s+new-tuple:\s*(.*)$/;
41
+ const NO_TUPLE_DATA = "(no-tuple-data)";
42
+ /** `col[type]:'quoted value'` or `col[type]:bareword` pairs, in appearance order. */
43
+ const COLUMN_PATTERN = /([A-Za-z_][A-Za-z0-9_]*)\[[^\]]*\]:('(?:[^']|'')*'|\S+)/g;
44
+ function unquote(rawValue) {
45
+ if (rawValue.length >= 2 && rawValue.startsWith("'") && rawValue.endsWith("'")) {
46
+ return rawValue.slice(1, -1).replace(/''/g, "'");
47
+ }
48
+ return rawValue;
49
+ }
50
+ function parseColumns(segment) {
51
+ const columns = {};
52
+ for (const match of segment.matchAll(COLUMN_PATTERN)) {
53
+ const name = match[1];
54
+ const rawValue = match[2];
55
+ columns[name] = unquote(rawValue);
56
+ }
57
+ return columns;
58
+ }
59
+ /**
60
+ * `null` for anything not one of the recognised `test_decoding` shapes —
61
+ * including `BEGIN <xid>` / `COMMIT <xid>` lines, which this parser is not
62
+ * asked to interpret (xid comes from the calling row's own column).
63
+ */
64
+ export function parseTestDecodingLine(data) {
65
+ const match = LINE_PATTERN.exec(data.trim());
66
+ if (match === null)
67
+ return null;
68
+ const [, schema, table, opRaw, rest] = match;
69
+ const op = opRaw;
70
+ if (op === "INSERT") {
71
+ return { schema: schema, table: table, op, before: null, after: parseColumns(rest) };
72
+ }
73
+ if (op === "DELETE") {
74
+ if (rest.trim() === NO_TUPLE_DATA) {
75
+ return { schema: schema, table: table, op, before: null, after: null };
76
+ }
77
+ return { schema: schema, table: table, op, before: parseColumns(rest), after: null };
78
+ }
79
+ // UPDATE
80
+ const oldKeyMatch = OLD_KEY_NEW_TUPLE.exec(rest);
81
+ if (oldKeyMatch !== null) {
82
+ return {
83
+ schema: schema,
84
+ table: table,
85
+ op,
86
+ before: parseColumns(oldKeyMatch[1]),
87
+ after: parseColumns(oldKeyMatch[2]),
88
+ };
89
+ }
90
+ return { schema: schema, table: table, op, before: null, after: parseColumns(rest) };
91
+ }
92
+ //# sourceMappingURL=postgres-decoding-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-decoding-parser.js","sourceRoot":"","sources":["../src/postgres-decoding-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAYH,MAAM,YAAY,GAAG,kEAAkE,CAAC;AACxF,MAAM,iBAAiB,GAAG,wCAAwC,CAAC;AACnE,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAExC,qFAAqF;AACrF,MAAM,cAAc,GAAG,0DAA0D,CAAC;AAElF,SAAS,OAAO,CAAC,QAAgB;IAC/B,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/E,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;IAC7C,MAAM,EAAE,GAAG,KAAoB,CAAC;IAEhC,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,CAAC;IAC1F,CAAC;IAED,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,IAAI,IAAK,CAAC,IAAI,EAAE,KAAK,aAAa,EAAE,CAAC;YACnC,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC1F,CAAC;IAED,SAAS;IACT,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAK,CAAC,CAAC;IAClD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,OAAO;YACL,MAAM,EAAE,MAAO;YACf,KAAK,EAAE,KAAM;YACb,EAAE;YACF,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;YACrC,KAAK,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;SACrC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,CAAC;AAC1F,CAAC"}
@@ -12,9 +12,12 @@
12
12
  * this observes query *text* — `extractTargetTable` still resolves the
13
13
  * touched table — never a captured before/after row. Postgres's statement
14
14
  * log is a text stream, not a row-change feed; reading one would need a
15
- * separate mechanism (logical replication or a trigger-based audit table)
16
- * this collector does not add. See `packages/database-observation`'s
17
- * package description for the same disclosure on the `node:sqlite` path.
15
+ * separate mechanism this collector does not add. That mechanism is now a
16
+ * separate collector in this package — `postgres-row-collector.ts`, via a
17
+ * temporary logical replication slot — opted into independently rather than
18
+ * folded into this one, since it needs its own connection, its own
19
+ * preconditions (`wal_level`, replication privilege) and its own refusal
20
+ * path when either is absent.
18
21
  *
19
22
  * **Live-tested against a real, disposable `postgres:16` Docker container**
20
23
  * (`test/postgres-live.test.ts`): a real `INSERT`/`SELECT` issued via
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-query-collector.d.ts","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAC7I,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAMvG,MAAM,WAAW,6BAA6B;IAC5C,+HAA+H;IAC/H,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,mHAAmH;IACnH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,SAAS,CA6F9F"}
1
+ {"version":3,"file":"postgres-query-collector.d.ts","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAC7I,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAMvG,MAAM,WAAW,6BAA6B;IAC5C,+HAA+H;IAC/H,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,mHAAmH;IACnH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,SAAS,CA6F9F"}
@@ -12,9 +12,12 @@
12
12
  * this observes query *text* — `extractTargetTable` still resolves the
13
13
  * touched table — never a captured before/after row. Postgres's statement
14
14
  * log is a text stream, not a row-change feed; reading one would need a
15
- * separate mechanism (logical replication or a trigger-based audit table)
16
- * this collector does not add. See `packages/database-observation`'s
17
- * package description for the same disclosure on the `node:sqlite` path.
15
+ * separate mechanism this collector does not add. That mechanism is now a
16
+ * separate collector in this package — `postgres-row-collector.ts`, via a
17
+ * temporary logical replication slot — opted into independently rather than
18
+ * folded into this one, since it needs its own connection, its own
19
+ * preconditions (`wal_level`, replication privilege) and its own refusal
20
+ * path when either is absent.
18
21
  *
19
22
  * **Live-tested against a real, disposable `postgres:16` Docker container**
20
23
  * (`test/postgres-live.test.ts`): a real `INSERT`/`SELECT` issued via
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-query-collector.js","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"postgres-query-collector.js","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Closes the gap `postgres-query-collector.ts` discloses: "queries only ...
3
+ * no before/after row state." Postgres's own logical decoding is the
4
+ * mechanism — no extension install, no schema change, no trigger/audit
5
+ * table the target application doesn't already have.
6
+ *
7
+ * Lifecycle, deliberately shaped around `Collector.start()`/`stop()` rather
8
+ * than a continuous stream (unlike `PostgresQueryCollector`, which tails a
9
+ * log line-by-line as it's written):
10
+ *
11
+ * - `start()`: opens ONE `pg` connection, checks two preconditions
12
+ * (`wal_level = 'logical'`, and the role has `REPLICATION` or superuser),
13
+ * refuses by name (never throws) if either fails, then creates a
14
+ * **temporary** logical replication slot (`pg_create_logical_replication_slot`
15
+ * with `test_decoding` and `temporary = true`). Temporary slots drop on
16
+ * disconnect — a crash between create and read cannot leave WAL retained
17
+ * on the server, which a permanent slot could. The `CollectorContext`
18
+ * passed in is retained (closed over) for use at `stop()`, since
19
+ * `Collector.stop()` itself takes none.
20
+ * - `stop()`: reads `pg_logical_slot_get_changes` once on that same
21
+ * connection, parses each row via `postgres-decoding-parser.ts`, emits one
22
+ * `DATABASE_QUERY` evidence item per recognised row change, then closes
23
+ * the connection (which also drops the temporary slot).
24
+ *
25
+ * Before-value completeness is a table-level fact (`REPLICA IDENTITY`), read
26
+ * once via `pg_class.relreplident` at slot-creation time and attached to
27
+ * every emitted row as `beforeCompleteness` — disclosed per DEC-NEXT
28
+ * row-change-evidence-reuses-database-query-type, never inferred from which
29
+ * of `postgres-decoding-parser.ts`'s three UPDATE/DELETE shapes matched.
30
+ *
31
+ * `pg` is an *optional* dependency here, loaded via dynamic `import()`
32
+ * exactly like `adapter-sql`'s `live-schema.ts` (see that file's own header
33
+ * for why: keeps every consumer that never wants row capture from needing a
34
+ * Postgres client library, and keeps `tsc` from requiring `pg`'s types to
35
+ * check this file). Not installed → the import throws → this collector
36
+ * refuses by name, same as a failed precondition check.
37
+ */
38
+ import type { Collector } from "@descryy/runtime-contracts";
39
+ /** The narrow slice of `pg`'s `Client` this module calls — same shape `live-schema.ts` uses, small enough to be satisfied by a test double with no real database involved. */
40
+ export interface PgRowClient {
41
+ connect(): Promise<void>;
42
+ query(text: string, params?: readonly unknown[]): Promise<{
43
+ rows: readonly Record<string, unknown>[];
44
+ }>;
45
+ end(): Promise<void>;
46
+ }
47
+ export interface PostgresRowChangeCollectorOptions {
48
+ readonly connectionString: string;
49
+ /** Which owned system this collector's evidence should be attributed to (plan §16.7). */
50
+ readonly service?: string;
51
+ readonly processId?: string;
52
+ /** Test/advanced seam — defaults to a real `pg.Client` loaded dynamically. */
53
+ readonly clientFactory?: (connectionString: string) => Promise<PgRowClient> | PgRowClient;
54
+ }
55
+ type PgClientConstructor = new (config: unknown) => PgRowClient;
56
+ /**
57
+ * The `Client` constructor from a dynamically imported `pg`. `pg` is CommonJS,
58
+ * so under `import()` its exports sit on `default` and Node's named-export
59
+ * detection does not surface `Client` at the top level; the first release read
60
+ * only the top level and every real connection failed with "pgModule.Client is
61
+ * not a constructor". Both shapes are read, and a module exposing neither is a
62
+ * loud error rather than a silent no-op.
63
+ */
64
+ export declare function pgClientConstructor(loaded: unknown): PgClientConstructor;
65
+ export declare function createPostgresRowChangeCollector(options: PostgresRowChangeCollectorOptions): Collector;
66
+ export {};
67
+ //# sourceMappingURL=postgres-row-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-row-collector.d.ts","sourceRoot":"","sources":["../src/postgres-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAK7I,8KAA8K;AAC9K,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACxG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC;CAC3F;AAMD,KAAK,mBAAmB,GAAG,KAAK,MAAM,EAAE,OAAO,KAAK,WAAW,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,mBAAmB,CAOxE;AAsBD,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,iCAAiC,GAAG,SAAS,CAsMtG"}
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Closes the gap `postgres-query-collector.ts` discloses: "queries only ...
3
+ * no before/after row state." Postgres's own logical decoding is the
4
+ * mechanism — no extension install, no schema change, no trigger/audit
5
+ * table the target application doesn't already have.
6
+ *
7
+ * Lifecycle, deliberately shaped around `Collector.start()`/`stop()` rather
8
+ * than a continuous stream (unlike `PostgresQueryCollector`, which tails a
9
+ * log line-by-line as it's written):
10
+ *
11
+ * - `start()`: opens ONE `pg` connection, checks two preconditions
12
+ * (`wal_level = 'logical'`, and the role has `REPLICATION` or superuser),
13
+ * refuses by name (never throws) if either fails, then creates a
14
+ * **temporary** logical replication slot (`pg_create_logical_replication_slot`
15
+ * with `test_decoding` and `temporary = true`). Temporary slots drop on
16
+ * disconnect — a crash between create and read cannot leave WAL retained
17
+ * on the server, which a permanent slot could. The `CollectorContext`
18
+ * passed in is retained (closed over) for use at `stop()`, since
19
+ * `Collector.stop()` itself takes none.
20
+ * - `stop()`: reads `pg_logical_slot_get_changes` once on that same
21
+ * connection, parses each row via `postgres-decoding-parser.ts`, emits one
22
+ * `DATABASE_QUERY` evidence item per recognised row change, then closes
23
+ * the connection (which also drops the temporary slot).
24
+ *
25
+ * Before-value completeness is a table-level fact (`REPLICA IDENTITY`), read
26
+ * once via `pg_class.relreplident` at slot-creation time and attached to
27
+ * every emitted row as `beforeCompleteness` — disclosed per DEC-NEXT
28
+ * row-change-evidence-reuses-database-query-type, never inferred from which
29
+ * of `postgres-decoding-parser.ts`'s three UPDATE/DELETE shapes matched.
30
+ *
31
+ * `pg` is an *optional* dependency here, loaded via dynamic `import()`
32
+ * exactly like `adapter-sql`'s `live-schema.ts` (see that file's own header
33
+ * for why: keeps every consumer that never wants row capture from needing a
34
+ * Postgres client library, and keeps `tsc` from requiring `pg`'s types to
35
+ * check this file). Not installed → the import throws → this collector
36
+ * refuses by name, same as a failed precondition check.
37
+ */
38
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
39
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
40
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
41
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
42
+ });
43
+ }
44
+ return path;
45
+ };
46
+ import { parseTestDecodingLine } from "./postgres-decoding-parser.js";
47
+ import { COLLECTOR_VERSION } from "./collector-version.js";
48
+ const EVENT_TYPE = "DATABASE_QUERY";
49
+ /**
50
+ * The `Client` constructor from a dynamically imported `pg`. `pg` is CommonJS,
51
+ * so under `import()` its exports sit on `default` and Node's named-export
52
+ * detection does not surface `Client` at the top level; the first release read
53
+ * only the top level and every real connection failed with "pgModule.Client is
54
+ * not a constructor". Both shapes are read, and a module exposing neither is a
55
+ * loud error rather than a silent no-op.
56
+ */
57
+ export function pgClientConstructor(loaded) {
58
+ const module = loaded;
59
+ const candidate = module?.Client ?? module?.default?.Client;
60
+ if (typeof candidate !== "function") {
61
+ throw new Error('"pg" was loaded but exposes no Client constructor, at its top level or on its default export');
62
+ }
63
+ return candidate;
64
+ }
65
+ async function defaultClientFactory(connectionString) {
66
+ // Read from a variable, not a string literal -- see the module header:
67
+ // keeps `tsc` from requiring `pg`'s type declarations to check this file,
68
+ // an ordinary dynamic import at runtime either way.
69
+ const moduleName = "pg";
70
+ const Client = pgClientConstructor(await import(__rewriteRelativeImportExtension(moduleName)));
71
+ return new Client({ connectionString });
72
+ }
73
+ function makeSlotName(processId) {
74
+ const safe = processId.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 40);
75
+ return `descry_rowcap_${safe}_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
76
+ }
77
+ function replidentToCompleteness(code) {
78
+ if (code === "f")
79
+ return "full";
80
+ if (code === "n" || code === "i")
81
+ return "none";
82
+ return "key-only"; // "d" (default, PK-only) and anything unrecognised -- assume the narrower claim, never the wider one
83
+ }
84
+ export function createPostgresRowChangeCollector(options) {
85
+ const processId = options.processId ?? "postgres-row-changes";
86
+ const clientFactory = options.clientFactory ?? defaultClientFactory;
87
+ let client = null;
88
+ let slot = null;
89
+ let savedContext = null;
90
+ let refusalReason = null;
91
+ let identityByTable = new Map();
92
+ function computeCapabilities() {
93
+ const unrelated = {
94
+ availability: "unavailable",
95
+ reason: "PostgresRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
96
+ };
97
+ const databaseObservation = refusalReason !== null
98
+ ? { availability: "unavailable", reason: refusalReason }
99
+ : {
100
+ availability: "available",
101
+ reason: "row-level INSERT/UPDATE/DELETE via a temporary logical replication slot (test_decoding); " +
102
+ "before-values are full only for tables with REPLICA IDENTITY FULL -- key-only or absent " +
103
+ "otherwise, disclosed per row as beforeCompleteness.",
104
+ };
105
+ return {
106
+ domObservation: unrelated,
107
+ consoleObservation: unrelated,
108
+ networkObservation: unrelated,
109
+ backendLogAccess: unrelated,
110
+ distributedTrace: unrelated,
111
+ sourceMapping: unrelated,
112
+ stackCapture: unrelated,
113
+ processLifecycle: unrelated,
114
+ databaseObservation,
115
+ externalServiceObservation: unrelated,
116
+ };
117
+ }
118
+ function emitRowChange(context, parsed, xid) {
119
+ const completeness = identityByTable.get(`${parsed.schema}.${parsed.table}`) ?? "key-only";
120
+ context.emit({
121
+ timestamp: new Date().toISOString(),
122
+ source: "backend-process",
123
+ service: options.service ?? null,
124
+ process: processId,
125
+ eventType: EVENT_TYPE,
126
+ payload: {
127
+ kind: "row-change",
128
+ schema: parsed.schema,
129
+ table: parsed.table,
130
+ op: parsed.op,
131
+ before: parsed.before,
132
+ after: parsed.after,
133
+ xid: xid === undefined || xid === null ? null : String(xid),
134
+ beforeCompleteness: completeness,
135
+ },
136
+ traceId: null,
137
+ requestId: null,
138
+ correlationId: null,
139
+ graphNodeId: null,
140
+ sourceLocation: null,
141
+ stackTrace: null,
142
+ confidence: 1,
143
+ redactionStatus: "pending-redaction",
144
+ collectorVersion: COLLECTOR_VERSION,
145
+ });
146
+ }
147
+ function emitCollectorError(context, detail) {
148
+ try {
149
+ context.emit({
150
+ timestamp: new Date().toISOString(),
151
+ source: "backend-process",
152
+ service: options.service ?? null,
153
+ process: processId,
154
+ eventType: "COLLECTOR_ERROR",
155
+ payload: {
156
+ raw: `postgres row-change collector for process "${processId}" failed: ${detail}`,
157
+ collectorId: `postgres-row-collector:${processId}`,
158
+ error: detail,
159
+ },
160
+ traceId: null,
161
+ requestId: null,
162
+ correlationId: null,
163
+ graphNodeId: null,
164
+ sourceLocation: null,
165
+ stackTrace: null,
166
+ confidence: 1,
167
+ redactionStatus: "pending-redaction",
168
+ collectorVersion: COLLECTOR_VERSION,
169
+ });
170
+ }
171
+ catch (emitError) {
172
+ console.error(`[postgres-row-collector:${processId}] failed (${detail}) AND the failure could not be emitted: ${String(emitError)}`);
173
+ }
174
+ }
175
+ async function closeClient() {
176
+ if (client === null)
177
+ return;
178
+ try {
179
+ await client.end();
180
+ }
181
+ catch {
182
+ // Best-effort close -- the temporary slot drops on disconnect regardless.
183
+ }
184
+ client = null;
185
+ slot = null;
186
+ }
187
+ return {
188
+ collectorId: `postgres-row-collector:${processId}`,
189
+ async start(context) {
190
+ savedContext = context;
191
+ let candidate;
192
+ try {
193
+ candidate = await clientFactory(options.connectionString);
194
+ await candidate.connect();
195
+ }
196
+ catch (error) {
197
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
198
+ refusalReason = `could not connect for row-level capture (${detail}); the query-text channel is unaffected.`;
199
+ return { available: false, reason: refusalReason };
200
+ }
201
+ client = candidate;
202
+ try {
203
+ const walLevelResult = await client.query("SHOW wal_level");
204
+ const walLevel = String(walLevelResult.rows[0]?.wal_level ?? "").toLowerCase();
205
+ if (walLevel !== "logical") {
206
+ refusalReason =
207
+ `wal_level is "${walLevel || "unknown"}", not "logical" -- row-level capture needs logical replication ` +
208
+ "enabled on the server (a superuser-only setting); the query-text channel is unaffected.";
209
+ await closeClient();
210
+ return { available: false, reason: refusalReason };
211
+ }
212
+ const roleResult = await client.query("SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user");
213
+ const role = roleResult.rows[0];
214
+ if (role === undefined || (role.rolreplication !== true && role.rolsuper !== true)) {
215
+ refusalReason =
216
+ "the connection's role has neither REPLICATION nor superuser privilege -- row-level capture needs one " +
217
+ "of them to create a logical replication slot; the query-text channel is unaffected.";
218
+ await closeClient();
219
+ return { available: false, reason: refusalReason };
220
+ }
221
+ const identityResult = await client.query("SELECT n.nspname AS schema, c.relname AS table, c.relreplident AS identity " +
222
+ "FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " +
223
+ "WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema')");
224
+ identityByTable = new Map();
225
+ for (const row of identityResult.rows) {
226
+ const r = row;
227
+ identityByTable.set(`${String(r.schema)}.${String(r.table)}`, replidentToCompleteness(r.identity));
228
+ }
229
+ slot = makeSlotName(processId);
230
+ await client.query("SELECT * FROM pg_create_logical_replication_slot($1, 'test_decoding', false, true)", [slot]);
231
+ return { available: true };
232
+ }
233
+ catch (error) {
234
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
235
+ refusalReason = `could not start row-level capture: ${detail}`;
236
+ await closeClient();
237
+ return { available: false, reason: refusalReason };
238
+ }
239
+ },
240
+ async stop() {
241
+ if (client === null || slot === null)
242
+ return;
243
+ const activeClient = client;
244
+ const activeSlot = slot;
245
+ const context = savedContext;
246
+ try {
247
+ const changes = await activeClient.query("SELECT xid, data FROM pg_logical_slot_get_changes($1, NULL, NULL)", [activeSlot]);
248
+ for (const row of changes.rows) {
249
+ const data = String(row.data ?? "");
250
+ const xid = row.xid;
251
+ const parsed = parseTestDecodingLine(data);
252
+ if (parsed === null)
253
+ continue; // BEGIN/COMMIT or unrecognised -- refused, not guessed
254
+ if (context !== null)
255
+ emitRowChange(context, parsed, xid);
256
+ }
257
+ }
258
+ catch (error) {
259
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
260
+ refusalReason = `could not read the replication slot: ${detail}`;
261
+ if (context !== null)
262
+ emitCollectorError(context, detail);
263
+ }
264
+ finally {
265
+ await closeClient();
266
+ }
267
+ },
268
+ capabilities() {
269
+ return computeCapabilities();
270
+ },
271
+ };
272
+ }
273
+ //# sourceMappingURL=postgres-row-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-row-collector.js","sourceRoot":"","sources":["../src/postgres-row-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SQLite row-change capture: before/after snapshot-and-diff, not a
3
+ * change-feed — SQLite has no logical-decoding equivalent, so this observes
4
+ * by re-reading, not by tailing. `sqlite-instrumentation.ts`'s preload
5
+ * already captures query *text*; this closes the same before/after-row gap
6
+ * `postgres-row-collector.ts` closes for Postgres, by a different mechanism
7
+ * because SQLite offers a different one.
8
+ *
9
+ * Lifecycle: `start()` opens the target database **read-only**
10
+ * (`new DatabaseSync(path, { readOnly: true })` — this collector never
11
+ * writes) and snapshots every user table (`sqlite_master`, excluding
12
+ * `sqlite_%` internal tables) up to `rowCap` rows each, keyed by `rowid`.
13
+ * `stop()` re-reads the same table set and diffs by `rowid`: present only
14
+ * before → DELETE, present only after → INSERT, present in both with
15
+ * different column values → UPDATE. See
16
+ * DEC-NEXT-sqlite-row-snapshot-scope-and-cap for why scope is "every table
17
+ * in the database" rather than a query-derived subset, and why a
18
+ * `WITHOUT ROWID` table (no `rowid` to key by) is skipped and disclosed by
19
+ * name rather than guessed at with a substitute key.
20
+ *
21
+ * `truncated: true` on an emitted row's table means that table had more
22
+ * than `rowCap` rows at one of the two snapshots — a change outside the
23
+ * sampled window could exist and go unreported. Disclosed per row, not
24
+ * buried in a capability string alone.
25
+ */
26
+ import type { Collector } from "@descryy/runtime-contracts";
27
+ /** The narrow slice of `node:sqlite` this module needs — swappable in tests for a fake with no real file on disk. */
28
+ export interface SqliteRowSource {
29
+ listTables(): readonly string[];
30
+ /** Reads up to `limit + 1` rows (the `+1` lets the caller detect truncation without a second COUNT query), keyed by `rowid`. Throws for a table this source cannot key by rowid (e.g. `WITHOUT ROWID`). */
31
+ readRows(table: string, limit: number): readonly Record<string, unknown>[];
32
+ close(): void;
33
+ }
34
+ export interface TableSnapshot {
35
+ readonly rowsByRowid: Map<string, Record<string, unknown>>;
36
+ readonly truncated: boolean;
37
+ }
38
+ export type SqliteRowOp = "INSERT" | "UPDATE" | "DELETE";
39
+ export interface SqliteRowDiff {
40
+ readonly table: string;
41
+ readonly op: SqliteRowOp;
42
+ readonly before: Record<string, unknown> | null;
43
+ readonly after: Record<string, unknown> | null;
44
+ readonly truncated: boolean;
45
+ }
46
+ /** Pure, independently testable: diffs one table's before/after snapshot by rowid. Exported for direct unit testing without a real database file. */
47
+ export declare function diffTableSnapshot(table: string, before: TableSnapshot, after: TableSnapshot): SqliteRowDiff[];
48
+ export interface SqliteRowChangeCollectorOptions {
49
+ readonly databasePath: string;
50
+ readonly rowCap?: number;
51
+ readonly service?: string;
52
+ readonly processId?: string;
53
+ /** Test/advanced seam — defaults to a real read-only `node:sqlite` handle. */
54
+ readonly sourceFactory?: (databasePath: string) => SqliteRowSource;
55
+ }
56
+ export declare function createSqliteRowChangeCollector(options: SqliteRowChangeCollectorOptions): Collector;
57
+ //# sourceMappingURL=sqlite-row-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-row-snapshot.d.ts","sourceRoot":"","sources":["../src/sqlite-row-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAM7I,qHAAqH;AACrH,MAAM,WAAW,eAAe;IAC9B,UAAU,IAAI,SAAS,MAAM,EAAE,CAAC;IAChC,2MAA2M;IAC3M,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC3E,KAAK,IAAI,IAAI,CAAC;CACf;AAwBD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAqBD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qJAAqJ;AACrJ,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,aAAa,EAAE,CAgB7G;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,eAAe,CAAC;CACpE;AAID,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,SAAS,CA4HlG"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * SQLite row-change capture: before/after snapshot-and-diff, not a
3
+ * change-feed — SQLite has no logical-decoding equivalent, so this observes
4
+ * by re-reading, not by tailing. `sqlite-instrumentation.ts`'s preload
5
+ * already captures query *text*; this closes the same before/after-row gap
6
+ * `postgres-row-collector.ts` closes for Postgres, by a different mechanism
7
+ * because SQLite offers a different one.
8
+ *
9
+ * Lifecycle: `start()` opens the target database **read-only**
10
+ * (`new DatabaseSync(path, { readOnly: true })` — this collector never
11
+ * writes) and snapshots every user table (`sqlite_master`, excluding
12
+ * `sqlite_%` internal tables) up to `rowCap` rows each, keyed by `rowid`.
13
+ * `stop()` re-reads the same table set and diffs by `rowid`: present only
14
+ * before → DELETE, present only after → INSERT, present in both with
15
+ * different column values → UPDATE. See
16
+ * DEC-NEXT-sqlite-row-snapshot-scope-and-cap for why scope is "every table
17
+ * in the database" rather than a query-derived subset, and why a
18
+ * `WITHOUT ROWID` table (no `rowid` to key by) is skipped and disclosed by
19
+ * name rather than guessed at with a substitute key.
20
+ *
21
+ * `truncated: true` on an emitted row's table means that table had more
22
+ * than `rowCap` rows at one of the two snapshots — a change outside the
23
+ * sampled window could exist and go unreported. Disclosed per row, not
24
+ * buried in a capability string alone.
25
+ */
26
+ import { DatabaseSync } from "node:sqlite";
27
+ import { COLLECTOR_VERSION } from "./collector-version.js";
28
+ const DEFAULT_ROW_CAP = 10_000;
29
+ function quoteIdentifier(name) {
30
+ return `"${name.replace(/"/g, '""')}"`;
31
+ }
32
+ function defaultSourceFactory(databasePath) {
33
+ const db = new DatabaseSync(databasePath, { readOnly: true });
34
+ return {
35
+ listTables() {
36
+ const rows = db
37
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'")
38
+ .all();
39
+ return rows.map((row) => row.name);
40
+ },
41
+ readRows(table, limit) {
42
+ return db.prepare(`SELECT rowid AS __rowid, * FROM ${quoteIdentifier(table)} LIMIT ?`).all(limit);
43
+ },
44
+ close() {
45
+ db.close();
46
+ },
47
+ };
48
+ }
49
+ function snapshotTable(source, table, rowCap) {
50
+ const rows = source.readRows(table, rowCap + 1);
51
+ const truncated = rows.length > rowCap;
52
+ const bounded = truncated ? rows.slice(0, rowCap) : rows;
53
+ const rowsByRowid = new Map();
54
+ for (const row of bounded) {
55
+ const { __rowid, ...rest } = row;
56
+ rowsByRowid.set(String(__rowid), rest);
57
+ }
58
+ return { rowsByRowid, truncated };
59
+ }
60
+ function rowsEqual(a, b) {
61
+ const aKeys = Object.keys(a);
62
+ const bKeys = Object.keys(b);
63
+ if (aKeys.length !== bKeys.length)
64
+ return false;
65
+ return aKeys.every((key) => Object.is(a[key], b[key]) || String(a[key]) === String(b[key]));
66
+ }
67
+ /** Pure, independently testable: diffs one table's before/after snapshot by rowid. Exported for direct unit testing without a real database file. */
68
+ export function diffTableSnapshot(table, before, after) {
69
+ const diffs = [];
70
+ const truncated = before.truncated || after.truncated;
71
+ const allRowids = new Set([...before.rowsByRowid.keys(), ...after.rowsByRowid.keys()]);
72
+ for (const rowid of allRowids) {
73
+ const beforeRow = before.rowsByRowid.get(rowid) ?? null;
74
+ const afterRow = after.rowsByRowid.get(rowid) ?? null;
75
+ if (beforeRow === null && afterRow !== null) {
76
+ diffs.push({ table, op: "INSERT", before: null, after: afterRow, truncated });
77
+ }
78
+ else if (beforeRow !== null && afterRow === null) {
79
+ diffs.push({ table, op: "DELETE", before: beforeRow, after: null, truncated });
80
+ }
81
+ else if (beforeRow !== null && afterRow !== null && !rowsEqual(beforeRow, afterRow)) {
82
+ diffs.push({ table, op: "UPDATE", before: beforeRow, after: afterRow, truncated });
83
+ }
84
+ }
85
+ return diffs;
86
+ }
87
+ const EVENT_TYPE = "DATABASE_QUERY";
88
+ export function createSqliteRowChangeCollector(options) {
89
+ const processId = options.processId ?? "sqlite-row-changes";
90
+ const rowCap = options.rowCap ?? DEFAULT_ROW_CAP;
91
+ const sourceFactory = options.sourceFactory ?? defaultSourceFactory;
92
+ let source = null;
93
+ let tables = [];
94
+ let beforeSnapshots = new Map();
95
+ let skippedTables = [];
96
+ let refusalReason = null;
97
+ function capabilities() {
98
+ const unrelated = {
99
+ availability: "unavailable",
100
+ reason: "SqliteRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
101
+ };
102
+ const databaseObservation = refusalReason !== null
103
+ ? { availability: "unavailable", reason: refusalReason }
104
+ : {
105
+ availability: "available",
106
+ reason: `row-level INSERT/UPDATE/DELETE via before/after snapshot-and-diff, capped at ${rowCap} rows per table ` +
107
+ "(truncated tables are disclosed per row as truncated: true)." +
108
+ (skippedTables.length > 0
109
+ ? ` Skipped (no rowid to key by, likely WITHOUT ROWID): ${skippedTables.join(", ")}.`
110
+ : ""),
111
+ };
112
+ return {
113
+ domObservation: unrelated,
114
+ consoleObservation: unrelated,
115
+ networkObservation: unrelated,
116
+ backendLogAccess: unrelated,
117
+ distributedTrace: unrelated,
118
+ sourceMapping: unrelated,
119
+ stackCapture: unrelated,
120
+ processLifecycle: unrelated,
121
+ databaseObservation,
122
+ externalServiceObservation: unrelated,
123
+ };
124
+ }
125
+ function emitDiff(context, diff) {
126
+ context.emit({
127
+ timestamp: new Date().toISOString(),
128
+ source: "backend-process",
129
+ service: options.service ?? null,
130
+ process: processId,
131
+ eventType: EVENT_TYPE,
132
+ payload: {
133
+ kind: "row-change",
134
+ table: diff.table,
135
+ op: diff.op,
136
+ before: diff.before,
137
+ after: diff.after,
138
+ truncated: diff.truncated,
139
+ },
140
+ traceId: null,
141
+ requestId: null,
142
+ correlationId: null,
143
+ graphNodeId: null,
144
+ sourceLocation: null,
145
+ stackTrace: null,
146
+ confidence: 1,
147
+ redactionStatus: "pending-redaction",
148
+ collectorVersion: COLLECTOR_VERSION,
149
+ });
150
+ }
151
+ let savedContext = null;
152
+ return {
153
+ collectorId: `sqlite-row-collector:${processId}`,
154
+ start(context) {
155
+ savedContext = context;
156
+ try {
157
+ source = sourceFactory(options.databasePath);
158
+ tables = source.listTables();
159
+ beforeSnapshots = new Map();
160
+ skippedTables = [];
161
+ for (const table of tables) {
162
+ try {
163
+ beforeSnapshots.set(table, snapshotTable(source, table, rowCap));
164
+ }
165
+ catch {
166
+ skippedTables.push(table);
167
+ }
168
+ }
169
+ return Promise.resolve({ available: true });
170
+ }
171
+ catch (error) {
172
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
173
+ refusalReason = `could not open "${options.databasePath}" read-only for row-level capture: ${detail}`;
174
+ source = null;
175
+ return Promise.resolve({ available: false, reason: refusalReason });
176
+ }
177
+ },
178
+ async stop() {
179
+ if (source === null)
180
+ return;
181
+ const activeSource = source;
182
+ const context = savedContext;
183
+ try {
184
+ for (const table of tables) {
185
+ const before = beforeSnapshots.get(table);
186
+ if (before === undefined)
187
+ continue; // skipped at start() -- stays skipped, disclosed via capabilities
188
+ let after;
189
+ try {
190
+ after = snapshotTable(activeSource, table, rowCap);
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ const diffs = diffTableSnapshot(table, before, after);
196
+ if (context !== null) {
197
+ for (const diff of diffs)
198
+ emitDiff(context, diff);
199
+ }
200
+ }
201
+ }
202
+ finally {
203
+ activeSource.close();
204
+ source = null;
205
+ }
206
+ },
207
+ capabilities,
208
+ };
209
+ }
210
+ //# sourceMappingURL=sqlite-row-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-row-snapshot.js","sourceRoot":"","sources":["../src/sqlite-row-snapshot.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@descryy/runtime-database-observation",
3
- "version": "0.4.8",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
- "description": "Database query observation (plan §18, checklist §14): real DATABASE_QUERY capture via driver instrumentation, never parsed application logs. Driver #1 is node:sqlite.",
5
+ "description": "Database observation (plan §18, checklist §14): real DATABASE_QUERY capture via driver instrumentation, never parsed application logs, plus row-level before/after state where the driver honestly supports it (Postgres logical decoding, SQLite before/after snapshot; MySQL refuses by name pending an installed driver). Driver #1 is node:sqlite.",
6
6
  "license": "UNLICENSED",
7
7
  "repository": {
8
8
  "type": "git",
@@ -25,16 +25,20 @@
25
25
  "registry": "https://registry.npmjs.org",
26
26
  "access": "public"
27
27
  },
28
- "scripts": {
29
- "build": "tsc -b"
30
- },
31
28
  "dependencies": {
32
29
  "@descryy/runtime-contracts": "0.3.3",
33
30
  "@descryy/runtime-backend-observation": "0.3.6",
34
- "@descryy/runtime-graph-correlator": "0.4.8"
31
+ "@descryy/runtime-graph-correlator": "0.5.0"
35
32
  },
36
33
  "devDependencies": {
37
- "@descryy/runtime-controller": "0.3.7",
38
- "@descryy/runtime-orchestrator": "0.4.8"
34
+ "@descryy/ir": "0.11.0",
35
+ "@descryy/runtime-controller": "0.3.9",
36
+ "@descryy/runtime-orchestrator": "0.4.12"
37
+ },
38
+ "optionalDependencies": {
39
+ "pg": "^8.13.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -b"
39
43
  }
40
- }
44
+ }