@reprova/sdk 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,28 +1,223 @@
1
1
  # @reprova/sdk
2
2
 
3
- Node.js/TypeScript SDK: Express error hook, HTTP 5xx hook, a Prisma client extension
4
- that records each request's data footprint (which tables/rows it touched) via
5
- `AsyncLocalStorage`, outbound HTTP call recording, and a batched, fire-and-forget
6
- transport to the control plane capturing never adds meaningful latency to your
7
- requests, even when the control plane is unreachable (`transport.test.ts` proves this;
8
- `chaos.spec.ts` in the integration suite proves it against the real, running control
9
- plane too).
3
+ Node.js/TypeScript SDK for **any Node app** vanilla `node:http`, Koa, Fastify,
4
+ Express, background workers. `Reprova.init()` instruments Node's own HTTP servers,
5
+ so every request gets capture context (errors, silent 5xxs, headers, trace ids,
6
+ outbound HTTP calls) with zero framework code. Express and Prisma integrations are
7
+ optional enrichments: Express adds async-rejection forwarding and parsed request
8
+ bodies; Prisma adds automatic data footprints (which tables/rows each request
9
+ touched — what makes an error *reproducible*, not just visible). The batched,
10
+ fire-and-forget transport never adds meaningful latency, even when the control
11
+ plane is unreachable (`transport.test.ts`; `chaos.spec.ts` proves it against the
12
+ real running control plane).
13
+
14
+ ## Install
15
+ ```bash
16
+ npm install @reprova/sdk
17
+ ```
18
+
19
+ ## Usage — any Node app
10
20
 
11
- ## Usage
12
21
  ```ts
13
- import { Reprova, createPrismaExtension } from '@reprova/sdk';
22
+ import { Reprova } from '@reprova/sdk';
23
+
24
+ Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
25
+ // That's it. A plain http.createServer / Koa / Fastify app now captures
26
+ // errors, silent 5xx responses, request context, trace ids, outbound calls.
27
+ ```
28
+
29
+ `init` instruments `http.Server`/`https.Server` directly and attaches
30
+ process-level error handlers (opt-outs: `instrumentHttp: false`,
31
+ `processHandlers: false`; `uncaughtException` flushes then exits 1). A missing
32
+ `dsn` puts the SDK in disabled mode: everything mounts, nothing records.
33
+
34
+ ### Background jobs and scripts
35
+
36
+ ```ts
37
+ await sdk.runJob('nightly-sync', async () => { ... }); // failures ingest as job_failure
38
+ await sdk.runWithContext({ name: 'csv-import' }, doImport); // generic wrapper: capture + rethrow
39
+ ```
14
40
 
41
+ ### Data footprints without Prisma (knex, raw SQL, any DAO)
42
+
43
+ ```ts
44
+ const rows = await knex('invoices').where({ id }).select();
45
+ sdk.recordFootprint({ model: 'Invoice', op: 'select', pks: rows.map(r => r.id), count: rows.length });
46
+ ```
47
+
48
+ Reproductions need footprints. Six ORMs record them automatically (below);
49
+ anything else records them with one `recordFootprint` call per query site.
50
+
51
+ | ORM | Helper | Auto PKs | where_shape |
52
+ | --- | --- | --- | --- |
53
+ | Prisma | `instrumentPrisma` / `createPrismaExtension` | ✅ exact (DMMF) | ✅ |
54
+ | Kysely | `createKyselyPlugin` | ✅ (`primaryKey` opt for composite) | ✅ |
55
+ | Sequelize | `installSequelizeHooks` | ✅ exact (model metadata) | ✅ |
56
+ | TypeORM | `installTypeOrmSubscriber` | ✅ exact (entity metadata) | ➖ |
57
+ | Knex / Objection | `installKnexHooks` | ✅ SELECT; INSERT depends on driver RETURNING | ➖ |
58
+ | Drizzle | `createDrizzleLogger` | ❌ shape-only (no result hook) | ✅ |
59
+
60
+ ## Express (optional enrichment)
61
+
62
+ ```ts
15
63
  const sdk = Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
64
+ sdk.setupExpress(app); // async-rejection forwarding + error-middleware capture + parsed bodies
65
+ ```
66
+
67
+ ## Prisma (optional enrichment — automatic footprints)
68
+
69
+ One call wires both automatic footprints and `migration_id`:
70
+
71
+ ```ts
72
+ const sdk = Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
73
+ await sdk.instrumentPrisma(prisma, { dmmf: Prisma.dmmf });
74
+ ```
75
+
76
+ `instrumentPrisma` splices the footprint extension onto your existing client
77
+ in place (so the shared singleton keeps recording) and best-effort reads the
78
+ latest row of `_prisma_migrations` into `migration_id`. It's a no-op when the
79
+ SDK is disabled (no `dsn` / replay), so no `if (dsn)` guard is needed, and it
80
+ never imports `@prisma/client` — Prisma stays an optional peer. A missing or
81
+ unreadable migrations table is non-fatal: `migration_id` stays `'unknown'`.
82
+ Pass `{ readMigrationId: false }` to set it yourself via `sdk.setMigrationId`.
83
+
84
+ Prefer to own the wiring? The extension is still exported directly:
85
+
86
+ ```ts
87
+ import { createPrismaExtension } from '@reprova/sdk';
88
+ const prisma = new PrismaClient().$extends(createPrismaExtension(Prisma.dmmf));
89
+ ```
90
+
91
+ ## Kysely (optional enrichment — automatic footprints)
92
+
93
+ Add the plugin at construction; every query then records a footprint:
94
+
95
+ ```ts
96
+ import { createKyselyPlugin } from '@reprova/sdk';
97
+ const db = new Kysely<DB>({ dialect, plugins: [createKyselyPlugin()] });
98
+ ```
99
+
100
+ The plugin reads the compiled AST for the table(s) and operation, the WHERE
101
+ clause's referenced **column names** (never values — PII-safe by
102
+ construction), and the returned rows for primary keys. Joined/subquery tables
103
+ are recorded as referenced (without PKs). It's a no-op outside a captured
104
+ request, never imports `kysely`, and can never break a query (every path is
105
+ guarded).
106
+
107
+ Kysely carries no schema metadata at runtime, so PK detection defaults to
108
+ `['id']`. Override for composite or non-`id` keys:
109
+
110
+ ```ts
111
+ createKyselyPlugin({ primaryKey: (table) => table === 'membership' ? ['tenantId', 'userId'] : ['id'] });
112
+ ```
113
+
114
+ `migration_id` is independent of the ORM: read your migration tool's version
115
+ table (Kysely's default is `kysely_migration`) and call
116
+ `sdk.setMigrationId(latest)`, or leave it `'unknown'`.
117
+
118
+ ## Sequelize (optional enrichment — automatic footprints)
119
+
120
+ Install once after your models are defined; every query records a footprint:
121
+
122
+ ```ts
123
+ import { installSequelizeHooks } from '@reprova/sdk';
124
+ installSequelizeHooks(sequelize);
125
+ ```
126
+
127
+ Uses Sequelize's lifecycle hooks (`afterFind`, `afterCreate`, …) plus each
128
+ model's `tableName` / `primaryKeyAttributes`, so PKs are exact. Hooks are
129
+ registered per model (and on `afterDefine` for later ones), so the table is
130
+ known even when a query returns 0 rows. `where_shape` = the WHERE object's
131
+ top-level keys (names only).
132
+
133
+ ## TypeORM (optional enrichment — automatic footprints)
134
+
135
+ Attach the subscriber after `dataSource.initialize()`:
136
+
137
+ ```ts
138
+ import { installTypeOrmSubscriber } from '@reprova/sdk';
139
+ await dataSource.initialize();
140
+ installTypeOrmSubscriber(dataSource);
141
+ ```
142
+
143
+ Uses entity lifecycle events (`afterLoad`, `afterInsert`, …) and each entity's
144
+ metadata for exact PKs. TypeORM fires these per entity with no WHERE context,
145
+ so entries are **merged by (table, op)** within a request (union of distinct
146
+ PKs), and there is no `where_shape`.
147
+
148
+ ## Knex / Objection (optional enrichment — automatic footprints)
149
+
150
+ One listener covers raw Knex **and** Objection.js (which runs through the same
151
+ Knex instance):
152
+
153
+ ```ts
154
+ import { installKnexHooks } from '@reprova/sdk';
155
+ installKnexHooks(knex);
156
+ ```
157
+
158
+ Reads the table and operation from Knex's `query-response` event. SELECT rows
159
+ yield PKs (via `primaryKey(table)`, default `['id']`); UPDATE/DELETE record an
160
+ affected-row count; INSERT PK fidelity depends on the driver's RETURNING
161
+ support (full on Postgres, last-id only on sqlite/mysql). No `where_shape`
162
+ (Knex has no query AST).
163
+
164
+ ## Drizzle (optional enrichment — shape-only)
165
+
166
+ Drizzle has no result-aware hook, only a logger — so this adapter records the
167
+ table, operation, and WHERE column names, but **cannot extract PKs**:
168
+
169
+ ```ts
170
+ import { createDrizzleLogger } from '@reprova/sdk';
171
+ const db = drizzle(client, { logger: createDrizzleLogger() });
172
+ ```
173
+
174
+ For PK-level fidelity with Drizzle, add `sdk.recordFootprint(...)` at the query
175
+ sites that matter; the logger gives cheap table-level coverage on top.
176
+
177
+ ## Express (continued)
178
+
179
+ `setupExpress(app)` can be called before or after your routes; it wires the
180
+ request-context middleware (repositioned to run first), error capture, the
181
+ HTTP-5xx hook, and async rejection forwarding in one call. A missing `dsn`
182
+ puts the SDK in disabled mode: nothing is recorded or sent, but Express-5
183
+ rejection semantics still apply — so apps can call `init` unconditionally.
184
+
185
+ Prefer manual control? `sdk.requestHandler()` / `sdk.errorHandler()` /
186
+ `wrapResponse` are still exported and behave exactly as before:
187
+
188
+ ```ts
16
189
  app.use(sdk.requestHandler());
17
190
  // ... your routes ...
18
191
  app.use(sdk.errorHandler());
19
-
20
- const prisma = new PrismaClient().$extends(createPrismaExtension(Prisma.dmmf));
21
192
  ```
22
193
 
23
194
  Every capture carries a W3C `trace_id`, the git `release` sha, and the latest applied
24
195
  Prisma `migration_id` — non-negotiable schema fields the rest of the pipeline depends on.
25
196
 
197
+ ## Async handlers just work
198
+
199
+ Existing route code needs **no changes** — no `asyncHandler` wrapper, no
200
+ `try/catch → next(err)`:
201
+
202
+ ```ts
203
+ app.get('/invoices/:id', async (req, res) => {
204
+ const invoice = await db.invoice.find(req.params.id); // a rejection here is
205
+ res.json(invoice); // captured automatically
206
+ });
207
+ ```
208
+
209
+ Express 4 normally discards the promise an async handler returns, so a rejection
210
+ never reaches error middleware. On the first request, the SDK transparently patches
211
+ the router's dispatch so rejected handler promises are forwarded down the error
212
+ chain — the exact semantics Express 5 ships natively — with the full request
213
+ context (headers, body, data footprint, trace id) intact on the capture. Your own
214
+ error middleware still runs and the client still gets its response; nothing is
215
+ swallowed. On Express 5 the SDK detects native forwarding and does nothing.
216
+
217
+ One limitation: a router imported from a *different* physical copy of `express`
218
+ in `node_modules` (rare — npm normally dedupes to one) isn't covered by the outer
219
+ app's patch; mount `sdk.requestHandler()` inside that sub-app to cover its copy too.
220
+
26
221
  ## Commands
27
222
  ```bash
28
223
  npm run build
package/dist/context.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface OutboundCall {
21
21
  status: number;
22
22
  response_body?: string;
23
23
  recorded_for_replay: boolean;
24
+ duration_ms?: number;
25
+ outcome?: 'success' | 'timeout' | 'network_error';
24
26
  }
25
27
  export interface ReprovaContext {
26
28
  traceId: string;
@@ -29,6 +31,7 @@ export interface ReprovaContext {
29
31
  footprint: FootprintQuery[];
30
32
  outboundCalls: OutboundCall[];
31
33
  captured?: boolean;
34
+ rawRequest?: unknown;
32
35
  }
33
36
  export declare const contextStorage: AsyncLocalStorage<ReprovaContext>;
34
37
  export declare function currentContext(): ReprovaContext | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,mCAA0C,CAAC;AAEtE,wBAAgB,cAAc,IAAI,cAAc,GAAG,SAAS,CAE3D"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAG7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,eAAe,CAAC;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IAKnB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,cAAc,mCAA0C,CAAC;AAEtE,wBAAgB,cAAc,IAAI,cAAc,GAAG,SAAS,CAE3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqChD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAEtE,MAAM,UAAU,cAAc;IAC5B,OAAO,cAAc,CAAC,QAAQ,EAAE,CAAC;AACnC,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA8ChD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAEtE,MAAM,UAAU,cAAc;IAC5B,OAAO,cAAc,CAAC,QAAQ,EAAE,CAAC;AACnC,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface DrizzleLoggerOptions {
2
+ captureWhereShape?: boolean;
3
+ forward?: {
4
+ logQuery(query: string, params: unknown[]): void;
5
+ };
6
+ }
7
+ export interface DrizzleLogger {
8
+ logQuery(query: string, params: unknown[]): void;
9
+ }
10
+ export declare function createDrizzleLogger(opts?: DrizzleLoggerOptions): DrizzleLogger;
11
+ //# sourceMappingURL=drizzle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,oBAAoB;IAEnC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CAChE;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAuCD,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB,GAAG,aAAa,CA2BlF"}
@@ -0,0 +1,72 @@
1
+ import { contextStorage } from './context.js';
2
+ const OP_BY_KEYWORD = {
3
+ select: 'select',
4
+ insert: 'insert',
5
+ update: 'update',
6
+ delete: 'delete',
7
+ };
8
+ // First SQL keyword → operation.
9
+ function opOf(sql) {
10
+ const m = /^\s*(select|insert|update|delete)\b/i.exec(sql);
11
+ return m ? OP_BY_KEYWORD[m[1].toLowerCase()] : undefined;
12
+ }
13
+ // The principal table, from the op-appropriate clause.
14
+ function tableOf(sql, op) {
15
+ let m = null;
16
+ if (op === 'select' || op === 'delete')
17
+ m = /\bfrom\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
18
+ else if (op === 'insert')
19
+ m = /\binto\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
20
+ else if (op === 'update')
21
+ m = /\bupdate\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
22
+ return m ? m[1] : undefined;
23
+ }
24
+ // Column names referenced in the WHERE clause — the right-hand identifier of
25
+ // each `"table"."col"` (or a bare `"col"`). Names only; values are `?`.
26
+ function whereColumns(sql) {
27
+ const wIdx = sql.search(/\bwhere\b/i);
28
+ if (wIdx < 0)
29
+ return [];
30
+ const tail = sql.slice(wIdx + 5);
31
+ const cols = new Set();
32
+ const re = /"([A-Za-z0-9_]+)"(?:\s*\.\s*"([A-Za-z0-9_]+)")?/g;
33
+ let m;
34
+ while ((m = re.exec(tail)) !== null)
35
+ cols.add(m[2] ?? m[1]);
36
+ return [...cols].sort();
37
+ }
38
+ // Creates a Drizzle-compatible logger that records footprints.
39
+ // Usage: drizzle(client, { logger: createDrizzleLogger() })
40
+ export function createDrizzleLogger(opts = {}) {
41
+ const captureWhereShape = opts.captureWhereShape !== false;
42
+ const forward = opts.forward;
43
+ return {
44
+ logQuery(query, params) {
45
+ try {
46
+ const ctx = contextStorage.getStore();
47
+ if (ctx) {
48
+ const op = opOf(query);
49
+ const table = op ? tableOf(query, op) : undefined;
50
+ if (op && table) {
51
+ const where = captureWhereShape ? whereColumns(query).join(',') || undefined : undefined;
52
+ const entry = {
53
+ model: table,
54
+ op,
55
+ pks: [],
56
+ where_shape: where,
57
+ count: 0,
58
+ note: 'drizzle: shape-only (no result access — PKs/count unavailable)',
59
+ };
60
+ ctx.footprint.push(entry);
61
+ }
62
+ }
63
+ }
64
+ catch { /* never break a query */ }
65
+ try {
66
+ forward?.logQuery(query, params);
67
+ }
68
+ catch { /* ignore */ }
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,15 @@
1
- export { Reprova, wrapResponse } from './sdk.js';
1
+ export { Reprova, wrapResponse, forwardAsyncErrors, replayErrorHandler } from './sdk.js';
2
2
  export { contextStorage, currentContext } from './context.js';
3
3
  export { createPrismaExtension } from './prisma.js';
4
+ export { createKyselyPlugin } from './kysely.js';
5
+ export type { KyselyPluginOptions, KyselyFootprintPlugin } from './kysely.js';
6
+ export { installSequelizeHooks } from './sequelize.js';
7
+ export type { SequelizeAdapterOptions } from './sequelize.js';
8
+ export { installTypeOrmSubscriber } from './typeorm.js';
9
+ export { installKnexHooks } from './knex.js';
10
+ export type { KnexAdapterOptions } from './knex.js';
11
+ export { createDrizzleLogger } from './drizzle.js';
12
+ export type { DrizzleLoggerOptions, DrizzleLogger } from './drizzle.js';
4
13
  export type { ReprovaOptions } from './sdk.js';
5
14
  export type { ReprovaContext, FootprintQuery, OutboundCall, RequestSnapshot } from './context.js';
6
15
  export * from './proto.gen.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAClG,cAAc,gBAAgB,CAAC;AAE/B,eAAO,MAAM,WAAW,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAClG,cAAc,gBAAgB,CAAC;AAE/B,eAAO,MAAM,WAAW,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,11 @@
1
- export { Reprova, wrapResponse } from './sdk.js';
1
+ export { Reprova, wrapResponse, forwardAsyncErrors, replayErrorHandler } from './sdk.js';
2
2
  export { contextStorage, currentContext } from './context.js';
3
3
  export { createPrismaExtension } from './prisma.js';
4
+ export { createKyselyPlugin } from './kysely.js';
5
+ export { installSequelizeHooks } from './sequelize.js';
6
+ export { installTypeOrmSubscriber } from './typeorm.js';
7
+ export { installKnexHooks } from './knex.js';
8
+ export { createDrizzleLogger } from './drizzle.js';
4
9
  export * from './proto.gen.js';
5
10
  export const SDK_VERSION = '0.1.0';
6
11
  //# 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,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGpD,cAAc,gBAAgB,CAAC;AAE/B,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAInD,cAAc,gBAAgB,CAAC;AAE/B,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC"}
package/dist/knex.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface KnexAdapterOptions {
2
+ primaryKey?: (table: string) => string[];
3
+ }
4
+ export declare function installKnexHooks(knex: unknown, opts?: KnexAdapterOptions): void;
5
+ //# sourceMappingURL=knex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knex.d.ts","sourceRoot":"","sources":["../src/knex.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,kBAAkB;IAEjC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CAC1C;AAsCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,kBAAuB,GAAG,IAAI,CAqCnF"}
package/dist/knex.js ADDED
@@ -0,0 +1,83 @@
1
+ import { contextStorage } from './context.js';
2
+ const OP_BY_METHOD = {
3
+ select: 'select',
4
+ first: 'select',
5
+ pluck: 'select',
6
+ insert: 'insert',
7
+ update: 'update',
8
+ del: 'delete',
9
+ delete: 'delete',
10
+ };
11
+ function isRowArray(v) {
12
+ return Array.isArray(v) && v.length > 0 && typeof v[0] === 'object' && v[0] !== null;
13
+ }
14
+ function distinct(values) {
15
+ const seen = new Set();
16
+ const out = [];
17
+ for (const v of values) {
18
+ if (!seen.has(v)) {
19
+ seen.add(v);
20
+ out.push(v);
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ function extractRowPks(rows, pkCols) {
26
+ if (pkCols.length === 0)
27
+ return [];
28
+ const out = [];
29
+ for (const row of rows) {
30
+ const vals = pkCols.map((c) => row[c]);
31
+ if (vals.some((v) => v === undefined || v === null))
32
+ continue;
33
+ out.push(vals.map((v) => String(v)).join(':'));
34
+ }
35
+ return distinct(out);
36
+ }
37
+ // Installs a footprint listener on a Knex instance. Also covers Objection.js
38
+ // models bound to this same instance.
39
+ export function installKnexHooks(knex, opts = {}) {
40
+ const primaryKey = opts.primaryKey ?? (() => ['id']);
41
+ try {
42
+ knex.on('query-response', (response, obj, builder) => {
43
+ try {
44
+ const ctx = contextStorage.getStore();
45
+ if (!ctx)
46
+ return;
47
+ const method = obj?.method ?? builder?._method;
48
+ const op = method ? OP_BY_METHOD[method] : undefined;
49
+ if (!op)
50
+ return; // schema/DDL/raw — skip
51
+ const table = builder?._single?.table;
52
+ if (typeof table !== 'string' || !table)
53
+ return;
54
+ let pks = [];
55
+ let count = 0;
56
+ if (isRowArray(response)) {
57
+ pks = extractRowPks(response, primaryKey(table));
58
+ count = response.length;
59
+ }
60
+ else if (Array.isArray(response)) {
61
+ // Primitive array — insert returning ids (partial on sqlite/mysql).
62
+ if (op === 'insert')
63
+ pks = distinct(response.filter((v) => v != null).map((v) => String(v)));
64
+ count = response.length;
65
+ }
66
+ else if (typeof response === 'number') {
67
+ count = response; // affected-row count from update/delete
68
+ }
69
+ const entry = {
70
+ model: table,
71
+ op,
72
+ pks,
73
+ count,
74
+ note: count === 0 ? 'returned 0 rows' : undefined,
75
+ };
76
+ ctx.footprint.push(entry);
77
+ }
78
+ catch { /* never break the query pipeline */ }
79
+ });
80
+ }
81
+ catch { /* never throw from install */ }
82
+ }
83
+ //# sourceMappingURL=knex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knex.js","sourceRoot":"","sources":["../src/knex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ interface KyselyQueryId {
2
+ readonly queryId: string;
3
+ }
4
+ export interface KyselyPluginOptions {
5
+ primaryKey?: (table: string) => string[];
6
+ captureWhereShape?: boolean;
7
+ }
8
+ export interface KyselyFootprintPlugin {
9
+ transformQuery<N extends {
10
+ kind: string;
11
+ }>(args: {
12
+ node: N;
13
+ queryId: KyselyQueryId;
14
+ }): N;
15
+ transformResult<R>(args: {
16
+ result: R;
17
+ queryId: KyselyQueryId;
18
+ }): Promise<R>;
19
+ }
20
+ export declare function createKyselyPlugin(opts?: KyselyPluginOptions): KyselyFootprintPlugin;
21
+ export {};
22
+ //# sourceMappingURL=kysely.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely.d.ts","sourceRoot":"","sources":["../src/kysely.ts"],"names":[],"mappings":"AAmBA,UAAU,aAAa;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAOD,MAAM,WAAW,mBAAmB;IAKlC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAIzC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAKD,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,CAAC,SAAS;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,CAAC,CAAC;QAAC,OAAO,EAAE,aAAa,CAAA;KAAE,GAAG,CAAC,CAAC;IACzF,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,OAAO,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7E;AA6GD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,mBAAwB,GAAG,qBAAqB,CAmExF"}