@reprova/sdk 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +75 -0
  2. package/dist/cjs/context.js +28 -0
  3. package/dist/cjs/distributionBaselinePoller.js +87 -0
  4. package/dist/cjs/distributionDrift.js +170 -0
  5. package/dist/cjs/drizzle.js +204 -0
  6. package/dist/cjs/frameworkAdapter.js +2 -0
  7. package/dist/cjs/httpFetch.js +59 -0
  8. package/dist/cjs/idempotency.js +132 -0
  9. package/dist/cjs/idempotencySettingsPoller.js +71 -0
  10. package/dist/cjs/index.js +101 -0
  11. package/dist/cjs/invariantInference.js +90 -0
  12. package/dist/cjs/invariantRules.js +142 -0
  13. package/dist/cjs/knex.js +105 -0
  14. package/dist/cjs/kysely.js +201 -0
  15. package/dist/cjs/legacyPrisma.js +130 -0
  16. package/dist/cjs/mikroorm.js +107 -0
  17. package/dist/cjs/mongoSchemaReporter.js +85 -0
  18. package/dist/cjs/mongoose.js +335 -0
  19. package/dist/cjs/nPlusOne.js +0 -0
  20. package/dist/cjs/nest.js +136 -0
  21. package/dist/cjs/nextjs.js +180 -0
  22. package/dist/cjs/package.json +1 -0
  23. package/dist/cjs/prisma.js +363 -0
  24. package/dist/cjs/proto.gen.js +4 -0
  25. package/dist/cjs/registry.js +100 -0
  26. package/dist/cjs/retryStorm.js +70 -0
  27. package/dist/cjs/rulesPoller.js +77 -0
  28. package/dist/cjs/sdk.js +1231 -0
  29. package/dist/cjs/sequelize.js +146 -0
  30. package/dist/cjs/shape.js +45 -0
  31. package/dist/cjs/stack.js +51 -0
  32. package/dist/cjs/transport.js +71 -0
  33. package/dist/cjs/typeorm.js +231 -0
  34. package/dist/context.d.ts +18 -0
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/context.js.map +1 -1
  37. package/dist/distributionBaselinePoller.d.ts +36 -0
  38. package/dist/distributionBaselinePoller.d.ts.map +1 -0
  39. package/dist/distributionBaselinePoller.js +84 -0
  40. package/dist/distributionBaselinePoller.js.map +1 -0
  41. package/dist/distributionDrift.d.ts +41 -0
  42. package/dist/distributionDrift.d.ts.map +1 -0
  43. package/dist/distributionDrift.js +161 -0
  44. package/dist/distributionDrift.js.map +1 -0
  45. package/dist/drizzle.d.ts.map +1 -1
  46. package/dist/drizzle.js +10 -4
  47. package/dist/drizzle.js.map +1 -1
  48. package/dist/frameworkAdapter.d.ts +5 -0
  49. package/dist/frameworkAdapter.d.ts.map +1 -0
  50. package/dist/frameworkAdapter.js +2 -0
  51. package/dist/frameworkAdapter.js.map +1 -0
  52. package/dist/httpFetch.d.ts +13 -0
  53. package/dist/httpFetch.d.ts.map +1 -0
  54. package/dist/httpFetch.js +54 -0
  55. package/dist/httpFetch.js.map +1 -0
  56. package/dist/idempotency.d.ts +26 -0
  57. package/dist/idempotency.d.ts.map +1 -0
  58. package/dist/idempotency.js +124 -0
  59. package/dist/idempotency.js.map +1 -0
  60. package/dist/idempotencySettingsPoller.d.ts +23 -0
  61. package/dist/idempotencySettingsPoller.d.ts.map +1 -0
  62. package/dist/idempotencySettingsPoller.js +68 -0
  63. package/dist/idempotencySettingsPoller.js.map +1 -0
  64. package/dist/index.d.ts +33 -8
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +20 -6
  67. package/dist/index.js.map +1 -1
  68. package/dist/invariantInference.d.ts +28 -0
  69. package/dist/invariantInference.d.ts.map +1 -0
  70. package/dist/invariantInference.js +87 -0
  71. package/dist/invariantInference.js.map +1 -0
  72. package/dist/invariantRules.d.ts +16 -0
  73. package/dist/invariantRules.d.ts.map +1 -0
  74. package/dist/invariantRules.js +104 -0
  75. package/dist/invariantRules.js.map +1 -0
  76. package/dist/knex.d.ts +1 -0
  77. package/dist/knex.d.ts.map +1 -1
  78. package/dist/knex.js +19 -0
  79. package/dist/knex.js.map +1 -1
  80. package/dist/kysely.d.ts +1 -0
  81. package/dist/kysely.d.ts.map +1 -1
  82. package/dist/kysely.js +27 -1
  83. package/dist/kysely.js.map +1 -1
  84. package/dist/legacyPrisma.d.ts +6 -0
  85. package/dist/legacyPrisma.d.ts.map +1 -0
  86. package/dist/legacyPrisma.js +128 -0
  87. package/dist/legacyPrisma.js.map +1 -0
  88. package/dist/mikroorm.d.ts +3 -0
  89. package/dist/mikroorm.d.ts.map +1 -0
  90. package/dist/mikroorm.js +104 -0
  91. package/dist/mikroorm.js.map +1 -0
  92. package/dist/mongoSchemaReporter.d.ts +23 -0
  93. package/dist/mongoSchemaReporter.d.ts.map +1 -0
  94. package/dist/mongoSchemaReporter.js +82 -0
  95. package/dist/mongoSchemaReporter.js.map +1 -0
  96. package/dist/mongoose.d.ts +48 -0
  97. package/dist/mongoose.d.ts.map +1 -0
  98. package/dist/mongoose.js +331 -0
  99. package/dist/mongoose.js.map +1 -0
  100. package/dist/nPlusOne.d.ts +52 -0
  101. package/dist/nPlusOne.d.ts.map +1 -0
  102. package/dist/nPlusOne.js +0 -0
  103. package/dist/nPlusOne.js.map +1 -0
  104. package/dist/nest.d.ts +13 -0
  105. package/dist/nest.d.ts.map +1 -1
  106. package/dist/nest.js +39 -0
  107. package/dist/nest.js.map +1 -1
  108. package/dist/nextjs.d.ts +27 -0
  109. package/dist/nextjs.d.ts.map +1 -0
  110. package/dist/nextjs.js +178 -0
  111. package/dist/nextjs.js.map +1 -0
  112. package/dist/prisma.d.ts +21 -5
  113. package/dist/prisma.d.ts.map +1 -1
  114. package/dist/prisma.js +247 -22
  115. package/dist/prisma.js.map +1 -1
  116. package/dist/proto.gen.d.ts +10 -2
  117. package/dist/proto.gen.d.ts.map +1 -1
  118. package/dist/registry.d.ts +1 -0
  119. package/dist/registry.d.ts.map +1 -1
  120. package/dist/registry.js +24 -0
  121. package/dist/registry.js.map +1 -1
  122. package/dist/retryStorm.d.ts +42 -0
  123. package/dist/retryStorm.d.ts.map +1 -0
  124. package/dist/retryStorm.js +65 -0
  125. package/dist/retryStorm.js.map +1 -0
  126. package/dist/rulesPoller.d.ts +26 -0
  127. package/dist/rulesPoller.d.ts.map +1 -0
  128. package/dist/rulesPoller.js +74 -0
  129. package/dist/rulesPoller.js.map +1 -0
  130. package/dist/sdk.d.ts +48 -7
  131. package/dist/sdk.d.ts.map +1 -1
  132. package/dist/sdk.js +619 -47
  133. package/dist/sdk.js.map +1 -1
  134. package/dist/sequelize.d.ts +1 -0
  135. package/dist/sequelize.d.ts.map +1 -1
  136. package/dist/sequelize.js +41 -3
  137. package/dist/sequelize.js.map +1 -1
  138. package/dist/stack.d.ts.map +1 -1
  139. package/dist/stack.js +6 -3
  140. package/dist/stack.js.map +1 -1
  141. package/dist/transport.d.ts.map +1 -1
  142. package/dist/transport.js +2 -1
  143. package/dist/transport.js.map +1 -1
  144. package/dist/typeorm.d.ts +2 -0
  145. package/dist/typeorm.d.ts.map +1 -1
  146. package/dist/typeorm.js +163 -1
  147. package/dist/typeorm.js.map +1 -1
  148. package/package.json +14 -5
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installSequelizeHooks = installSequelizeHooks;
4
+ exports.readSequelizeMigrationId = readSequelizeMigrationId;
5
+ const context_js_1 = require("./context.js");
6
+ function readAttr(instance, attr) {
7
+ if (instance && typeof instance.get === 'function') {
8
+ try {
9
+ const v = instance.get(attr);
10
+ if (v !== undefined)
11
+ return v;
12
+ }
13
+ catch {
14
+ // fall through to direct property access
15
+ }
16
+ }
17
+ return instance?.[attr];
18
+ }
19
+ function extractPks(instances, pkAttrs) {
20
+ if (pkAttrs.length === 0)
21
+ return [];
22
+ const seen = new Set();
23
+ const pks = [];
24
+ for (const inst of instances) {
25
+ const vals = pkAttrs.map((a) => readAttr(inst, a));
26
+ if (vals.some((v) => v === undefined || v === null))
27
+ continue;
28
+ const pk = vals.map((v) => String(v)).join(':');
29
+ if (!seen.has(pk)) {
30
+ seen.add(pk);
31
+ pks.push(pk);
32
+ }
33
+ }
34
+ return pks;
35
+ }
36
+ function whereShape(options, capture) {
37
+ if (!capture || !options?.where)
38
+ return undefined;
39
+ const cols = Object.keys(options.where).sort(); // symbol Op keys are skipped
40
+ return cols.length ? cols.join(',') : undefined;
41
+ }
42
+ function push(entry) {
43
+ const ctx = context_js_1.contextStorage.getStore();
44
+ if (ctx)
45
+ ctx.footprint.push(entry);
46
+ }
47
+ function installOnModel(model, capture) {
48
+ const pkAttrs = () => model.primaryKeyAttributes ?? ['id'];
49
+ model.addHook('afterFind', (result, options) => {
50
+ try {
51
+ const items = (Array.isArray(result) ? result : result ? [result] : []);
52
+ const pks = extractPks(items, pkAttrs());
53
+ push({
54
+ model: model.tableName,
55
+ op: 'select',
56
+ pks,
57
+ where_shape: whereShape(options, capture),
58
+ count: items.length,
59
+ note: items.length === 0 ? 'returned 0 rows' : undefined,
60
+ resolved: true,
61
+ });
62
+ }
63
+ catch { /* never break a query */ }
64
+ });
65
+ const recordWrite = (op) => (instance) => {
66
+ try {
67
+ const pks = extractPks([instance], pkAttrs());
68
+ push({ model: model.tableName, op, pks, count: 1, resolved: true });
69
+ }
70
+ catch { /* ignore */ }
71
+ };
72
+ model.addHook('afterCreate', recordWrite('insert'));
73
+ model.addHook('afterUpdate', recordWrite('update'));
74
+ model.addHook('afterDestroy', recordWrite('delete'));
75
+ model.addHook('afterBulkCreate', (instances) => {
76
+ try {
77
+ const items = (Array.isArray(instances) ? instances : []);
78
+ push({ model: model.tableName, op: 'insert', pks: extractPks(items, pkAttrs()), count: items.length, resolved: true });
79
+ }
80
+ catch { /* ignore */ }
81
+ });
82
+ // Bulk update/destroy operate by WHERE without materializing instances, so
83
+ // we record the touch + where_shape but no PKs.
84
+ const recordBulk = (op) => (options) => {
85
+ try {
86
+ push({ model: model.tableName, op, pks: [], where_shape: whereShape(options, capture), count: 0, note: `bulk ${op}`, resolved: true });
87
+ }
88
+ catch { /* ignore */ }
89
+ };
90
+ model.addHook('afterBulkUpdate', recordBulk('update'));
91
+ model.addHook('afterBulkDestroy', recordBulk('delete'));
92
+ }
93
+ // Installs footprint hooks on every model of a Sequelize instance (and any
94
+ // defined later). Call once after your models are defined.
95
+ function installSequelizeHooks(sequelize, opts = {}) {
96
+ const capture = opts.captureWhereShape !== false;
97
+ const sq = sequelize;
98
+ try {
99
+ for (const model of Object.values(sq.models ?? {}))
100
+ installOnModel(model, capture);
101
+ sq.addHook('afterDefine', (model) => {
102
+ try {
103
+ installOnModel(model, capture);
104
+ }
105
+ catch { /* ignore */ }
106
+ });
107
+ }
108
+ catch { /* ignore — never throw from install */ }
109
+ }
110
+ // Reads the latest applied migration name from sequelize-cli's own tracking
111
+ // table (created automatically by `sequelize-cli db:migrate`). Unlike
112
+ // Prisma/Kysely/Knex/TypeORM's tracking tables, SequelizeMeta has no
113
+ // timestamp column — just a single 'name' column (the migration filename) —
114
+ // so ordering relies on sequelize-cli's own convention of timestamp-prefixed
115
+ // filenames (e.g. "20240101120000-create-users.js") sorting correctly as
116
+ // plain text; migrationTableName mirrors sequelize-cli's --migrations-path-
117
+ // adjacent config for apps that renamed it from the 'SequelizeMeta' default.
118
+ // Two tries, same reasoning as instrumentPrisma's dialect fallback: LIMIT
119
+ // (Postgres/MySQL) then TOP (SQL Server). Best-effort: an absent table or
120
+ // any query error resolves to 'unknown', never throws. Wire it up with:
121
+ // sdk.registerFrameworkAdapter({ resolveMigrationId: () => readSequelizeMigrationId(sequelize) })
122
+ async function readSequelizeMigrationId(sequelize, migrationTableName = 'SequelizeMeta') {
123
+ const sq = sequelize;
124
+ try {
125
+ const rows = (await sq.query(`SELECT name FROM ${migrationTableName} ORDER BY name DESC LIMIT 1`, {
126
+ type: 'SELECT',
127
+ raw: true,
128
+ }));
129
+ if (rows?.[0]?.name)
130
+ return rows[0].name;
131
+ }
132
+ catch {
133
+ try {
134
+ const rows = (await sq.query(`SELECT TOP 1 name FROM ${migrationTableName} ORDER BY name DESC`, {
135
+ type: 'SELECT',
136
+ raw: true,
137
+ }));
138
+ if (rows?.[0]?.name)
139
+ return rows[0].name;
140
+ }
141
+ catch {
142
+ // migration id stays 'unknown'
143
+ }
144
+ }
145
+ return 'unknown';
146
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // Shapes a value by replacing every scalar leaf with "[type:length]".
3
+ // This prevents PII from leaking into where_shape / args_shape fields.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.shapeValue = shapeValue;
6
+ exports.shapeArgs = shapeArgs;
7
+ function shapeValue(val, depth = 0) {
8
+ if (val === null || val === undefined)
9
+ return 'null';
10
+ if (depth > 3)
11
+ return '[...]';
12
+ const t = typeof val;
13
+ if (t === 'string')
14
+ return `[string:${val.length}]`;
15
+ if (t === 'number')
16
+ return `[number:${String(val).length}]`;
17
+ if (t === 'boolean')
18
+ return `[boolean:1]`;
19
+ if (t === 'bigint')
20
+ return `[bigint:${String(val).length}]`;
21
+ if (Array.isArray(val)) {
22
+ if (val.length === 0)
23
+ return '[]';
24
+ return `[array:${val.length}]`;
25
+ }
26
+ if (t === 'object') {
27
+ const shaped = {};
28
+ for (const [k, v] of Object.entries(val)) {
29
+ shaped[k] = shapeValue(v, depth + 1);
30
+ }
31
+ return JSON.stringify(shaped);
32
+ }
33
+ return `[${t}]`;
34
+ }
35
+ function shapeArgs(args) {
36
+ if (args === null || args === undefined)
37
+ return 'null';
38
+ if (typeof args !== 'object')
39
+ return shapeValue(args);
40
+ const shaped = {};
41
+ for (const [k, v] of Object.entries(args)) {
42
+ shaped[k] = shapeValue(v);
43
+ }
44
+ return JSON.stringify(shaped);
45
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStack = parseStack;
4
+ exports.generateTraceId = generateTraceId;
5
+ exports.stripHeaders = stripHeaders;
6
+ exports.captureHeaders = captureHeaders;
7
+ const node_crypto_1 = require("node:crypto");
8
+ // Converts an Error stack trace into an array of frame strings.
9
+ function parseStack(err) {
10
+ if (!err.stack)
11
+ return [];
12
+ const lines = err.stack.split('\n');
13
+ // Skip first line (error message), return the rest trimmed.
14
+ return lines
15
+ .slice(1)
16
+ .map(l => l.trim())
17
+ .filter(l => l.startsWith('at '));
18
+ }
19
+ // Generates a W3C traceparent header value.
20
+ function generateTraceId() {
21
+ const traceId = randomHex(32);
22
+ const spanId = randomHex(16);
23
+ return `00-${traceId}-${spanId}-01`;
24
+ }
25
+ // Node's own crypto module (not the WebCrypto `crypto` global, which only
26
+ // became a stable global in Node 19+) — randomBytes has existed since
27
+ // Node's earliest crypto module, so this works on any Node version this SDK
28
+ // targets, not just modern ones.
29
+ function randomHex(chars) {
30
+ const bytes = Math.ceil(chars / 2);
31
+ return (0, node_crypto_1.randomBytes)(bytes).toString('hex').slice(0, chars);
32
+ }
33
+ // Strips sensitive HTTP headers, replacing values with '[stripped]'.
34
+ const SENSITIVE = /^(authorization|cookie|token|secret)/i;
35
+ function stripHeaders(headers) {
36
+ const out = {};
37
+ for (const [k, v] of Object.entries(headers)) {
38
+ out[k] = SENSITIVE.test(k) ? '[stripped]' : v;
39
+ }
40
+ return out;
41
+ }
42
+ // Captures HTTP headers from Express-style IncomingMessage header object.
43
+ function captureHeaders(raw) {
44
+ const out = {};
45
+ for (const [k, v] of Object.entries(raw)) {
46
+ if (v === undefined)
47
+ continue;
48
+ out[k] = Array.isArray(v) ? v.join(', ') : v;
49
+ }
50
+ return out;
51
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BatchTransport = void 0;
4
+ const httpFetch_js_1 = require("./httpFetch.js");
5
+ const WARN_INTERVAL_MS = 60_000;
6
+ class BatchTransport {
7
+ queue = [];
8
+ timer = null;
9
+ lastWarnAt = 0;
10
+ endpoint;
11
+ apiKey;
12
+ maxBatch;
13
+ flushIntervalMs;
14
+ constructor(opts) {
15
+ this.endpoint = opts.endpoint;
16
+ this.apiKey = opts.apiKey;
17
+ this.maxBatch = opts.maxBatch ?? 10;
18
+ this.flushIntervalMs = opts.flushIntervalMs ?? 2_000;
19
+ }
20
+ start() {
21
+ this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
22
+ if (this.timer.unref)
23
+ this.timer.unref(); // don't block process exit
24
+ }
25
+ stop() {
26
+ if (this.timer) {
27
+ clearInterval(this.timer);
28
+ this.timer = null;
29
+ }
30
+ }
31
+ enqueue(payload) {
32
+ this.queue.push(payload);
33
+ if (this.queue.length >= this.maxBatch) {
34
+ void this.flush();
35
+ }
36
+ }
37
+ async flush() {
38
+ if (this.queue.length === 0)
39
+ return;
40
+ const batch = this.queue.splice(0, this.maxBatch);
41
+ for (const payload of batch) {
42
+ await this.sendOne(payload);
43
+ }
44
+ }
45
+ async sendOne(payload) {
46
+ try {
47
+ const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'X-Reprova-Key': this.apiKey,
52
+ },
53
+ body: JSON.stringify(payload),
54
+ });
55
+ if (!res.ok) {
56
+ this.warnOnce(`reprova: ingest returned ${res.status}`);
57
+ }
58
+ }
59
+ catch (err) {
60
+ this.warnOnce(`reprova: transport error: ${err.message}`);
61
+ }
62
+ }
63
+ warnOnce(msg) {
64
+ const now = Date.now();
65
+ if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
66
+ this.lastWarnAt = now;
67
+ console.warn(msg);
68
+ }
69
+ }
70
+ }
71
+ exports.BatchTransport = BatchTransport;
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installTypeOrmSubscriber = installTypeOrmSubscriber;
4
+ exports.installTypeOrmQueryLogger = installTypeOrmQueryLogger;
5
+ exports.readTypeOrmMigrationId = readTypeOrmMigrationId;
6
+ const context_js_1 = require("./context.js");
7
+ function pkOf(entity, metadata) {
8
+ const cols = metadata?.primaryColumns ?? [];
9
+ if (!entity || cols.length === 0)
10
+ return null;
11
+ const vals = cols.map((c) => entity[c.propertyName]);
12
+ if (vals.some((v) => v === undefined || v === null))
13
+ return null;
14
+ return vals.map((v) => String(v)).join(':');
15
+ }
16
+ // Find-or-create the (table, op) entry for this request and fold in the PK.
17
+ function recordPk(table, op, pk) {
18
+ const ctx = context_js_1.contextStorage.getStore();
19
+ if (!ctx || !table)
20
+ return;
21
+ let entry = ctx.footprint.find((f) => f.model === table && f.op === op);
22
+ if (!entry) {
23
+ entry = { model: table, op, pks: [], count: 0, resolved: true };
24
+ ctx.footprint.push(entry);
25
+ }
26
+ if (pk !== null && !entry.pks.includes(pk))
27
+ entry.pks.push(pk);
28
+ entry.count = entry.pks.length;
29
+ }
30
+ // Installs a footprint subscriber on an initialized TypeORM DataSource. Call
31
+ // AFTER dataSource.initialize() — the subscribers array exists once the source
32
+ // is initialized. Attaches an instance directly, so the host needs no
33
+ // @EventSubscriber()-decorated class.
34
+ function installTypeOrmSubscriber(dataSource) {
35
+ const subscriber = {
36
+ afterLoad(entity, event) {
37
+ try {
38
+ recordPk(event?.metadata?.tableName, 'select', pkOf(entity, event?.metadata));
39
+ }
40
+ catch { /* never break */ }
41
+ },
42
+ afterInsert(event) {
43
+ try {
44
+ recordPk(event?.metadata?.tableName, 'insert', pkOf(event?.entity, event?.metadata));
45
+ }
46
+ catch { /* ignore */ }
47
+ },
48
+ afterUpdate(event) {
49
+ try {
50
+ recordPk(event?.metadata?.tableName, 'update', pkOf(event?.entity, event?.metadata));
51
+ }
52
+ catch { /* ignore */ }
53
+ },
54
+ afterRemove(event) {
55
+ try {
56
+ recordPk(event?.metadata?.tableName, 'delete', pkOf(event?.databaseEntity ?? event?.entity, event?.metadata));
57
+ }
58
+ catch { /* ignore */ }
59
+ },
60
+ };
61
+ try {
62
+ const ds = dataSource;
63
+ if (Array.isArray(ds.subscribers))
64
+ ds.subscribers.push(subscriber);
65
+ else
66
+ ds.subscribers = [subscriber];
67
+ }
68
+ catch { /* never throw from install */ }
69
+ installTypeOrmQueryLogger(dataSource);
70
+ }
71
+ // Entity subscribers only fire on SUCCESS (afterInsert/afterUpdate/etc. never
72
+ // broadcast for a query the database rejected), so a failed write — a
73
+ // constraint violation, a too-long value, a deadlock — leaves no trace in the
74
+ // footprint at all: the request just carries the thrown error with nothing
75
+ // showing what was attempted. This records what the write attempt LOOKED
76
+ // LIKE — table, operation, and the column *names* involved — never the bound
77
+ // parameter values, which are row data (the too-long username, the duplicate
78
+ // email) and must never enter a footprint per the metadata-only rule. Only
79
+ // parses SQL for INSERT/UPDATE/DELETE; a SELECT that fails (e.g. a raw query
80
+ // builder error) has no row-level meaning to capture and is left alone.
81
+ const WRITE_VERBS = new Set(['insert', 'update', 'delete']);
82
+ function parseFailedWrite(sql) {
83
+ const trimmed = sql.trim();
84
+ const verb = trimmed.match(/^(insert|update|delete)\b/i)?.[1]?.toLowerCase();
85
+ if (!verb || !WRITE_VERBS.has(verb))
86
+ return null;
87
+ const stripIdent = (s) => s.trim().replace(/^[`"[]|[`"\]]$/g, '');
88
+ if (verb === 'insert') {
89
+ // INSERT INTO `table` (`a`, `b`) VALUES (?, ?) / INSERT INTO "table" ("a","b") VALUES ($1,$2)
90
+ const m = trimmed.match(/^insert\s+into\s+([`"[\]\w.]+)\s*\(([^)]*)\)/i);
91
+ if (!m)
92
+ return null;
93
+ return {
94
+ op: 'insert',
95
+ table: stripIdent(m[1].split('.').pop() ?? m[1]),
96
+ columns: m[2].split(',').map(stripIdent).filter(Boolean),
97
+ };
98
+ }
99
+ if (verb === 'update') {
100
+ // UPDATE `table` SET `a` = ?, `b` = ? [WHERE ...]
101
+ const m = trimmed.match(/^update\s+([`"[\]\w.]+)\s+set\s+([\s\S]*?)(?:\s+where\s[\s\S]*)?$/i);
102
+ if (!m)
103
+ return null;
104
+ return {
105
+ op: 'update',
106
+ table: stripIdent(m[1].split('.').pop() ?? m[1]),
107
+ columns: m[2].split(',').map((assign) => stripIdent(assign.split('=')[0] ?? '')).filter(Boolean),
108
+ };
109
+ }
110
+ // DELETE FROM `table` [WHERE ...] — no column shape, just the table.
111
+ const m = trimmed.match(/^delete\s+from\s+([`"[\]\w.]+)/i);
112
+ if (!m)
113
+ return null;
114
+ return { op: 'delete', table: stripIdent(m[1].split('.').pop() ?? m[1]), columns: [] };
115
+ }
116
+ function recordFailedWrite(ctx, sql) {
117
+ const parsed = parseFailedWrite(sql);
118
+ if (!parsed || !parsed.table)
119
+ return;
120
+ ctx.footprint.push({
121
+ model: parsed.table,
122
+ op: `${parsed.op}_failed`,
123
+ pks: [],
124
+ count: 0,
125
+ where_shape: parsed.columns.length ? parsed.columns.join(',') : undefined,
126
+ note: 'rejected by database — see error for reason',
127
+ resolved: true,
128
+ });
129
+ }
130
+ // Prototypes already patched, keyed by the prototype object itself (TypeORM
131
+ // shares one QueryRunner class per driver/dialect, not per DataSource
132
+ // instance, so this correctly dedupes even if install is called more than
133
+ // once, or against more than one DataSource using the same driver).
134
+ const patchedQueryRunnerProtos = new WeakSet();
135
+ // TypeORM's own driver (at least the "mysql" — not mysql2 — npm package's,
136
+ // which pre-dates async_hooks-aware pooling) acquires its actual DB
137
+ // connection from a pool INSIDE QueryRunner.query()'s own promise executor,
138
+ // and older callback-based drivers like this one are exactly the class of
139
+ // library that has historically NOT reliably propagated Node's
140
+ // AsyncLocalStorage context across that kind of internal async boundary —
141
+ // the same class of bug already fixed for Mongoose's pooled connections in
142
+ // mongoose.ts (context frozen to whatever was ambient when a pooled
143
+ // connection was first used, not when a later query is issued). This patch
144
+ // defends against that regardless of the exact internal cause, by
145
+ // re-asserting the CALLER's context explicitly around every query rather
146
+ // than trusting the driver's own continuation to carry it.
147
+ //
148
+ // One important limitation found while validating this against a real
149
+ // NestJS + TypeORM + MySQL app: if context is ALREADY gone by the time
150
+ // repository.save()/find() is even called — observed happening upstream of
151
+ // this file entirely, in Nest's own ValidationPipe/@Body() parameter
152
+ // resolution for that app's TypeORM/Nest version combination — this patch
153
+ // (and installTypeOrmSubscriber) has nothing left to capture. That is a
154
+ // separate, framework-level gap this file cannot fix from here.
155
+ //
156
+ // The fix: monkey-patch QueryRunner.prototype.query (once per driver, since
157
+ // TypeORM shares that class across DataSources of the same dialect) to
158
+ // capture the CALLER's context synchronously — before any of the driver's
159
+ // own internal pool/socket work runs — then re-run the original call inside
160
+ // contextStorage.run() with that captured value. Per Node's own
161
+ // AsyncLocalStorage contract, any async operation "created within" a run()
162
+ // callback inherits its store; since query()'s returned Promise (and
163
+ // whatever pool acquisition happens inside it) is created there, this
164
+ // restores continuity for both this file's own failed-write capture AND the
165
+ // pre-existing entity-subscriber path, which shares the same underlying
166
+ // QueryRunner.query() call.
167
+ function installTypeOrmQueryLogger(dataSource) {
168
+ try {
169
+ const ds = dataSource;
170
+ if (typeof ds.createQueryRunner !== 'function')
171
+ return;
172
+ const sample = ds.createQueryRunner();
173
+ const proto = Object.getPrototypeOf(sample);
174
+ if (typeof proto.query !== 'function' || patchedQueryRunnerProtos.has(proto)) {
175
+ void sample.release?.().catch(() => { });
176
+ return;
177
+ }
178
+ patchedQueryRunnerProtos.add(proto);
179
+ const originalQuery = proto.query;
180
+ proto.query = function (...args) {
181
+ const capturedCtx = context_js_1.contextStorage.getStore();
182
+ if (!capturedCtx)
183
+ return originalQuery.apply(this, args);
184
+ const sql = typeof args[0] === 'string' ? args[0] : '';
185
+ const result = context_js_1.contextStorage.run(capturedCtx, () => originalQuery.apply(this, args));
186
+ result.catch(() => {
187
+ try {
188
+ recordFailedWrite(capturedCtx, sql);
189
+ }
190
+ catch {
191
+ /* never break the app's own query */
192
+ }
193
+ });
194
+ return result;
195
+ };
196
+ void sample.release?.().catch(() => { });
197
+ }
198
+ catch {
199
+ /* never throw from install */
200
+ }
201
+ }
202
+ // Reads the latest applied migration name from TypeORM's own migrations
203
+ // table (created automatically the first time `migration:run` — or a
204
+ // DataSource with migrationsRun: true — executes). 'name' and 'timestamp'
205
+ // are TypeORM's own fixed column names; migrationsTableName mirrors the one
206
+ // DataSource option apps can change it with, default 'migrations'. Two
207
+ // tries, same reasoning as instrumentPrisma's dialect fallback: Postgres/
208
+ // MySQL's LIMIT, then SQL Server's TOP (one of the three dialects this
209
+ // table could live in, per the agent's own SQL-dialect support). Best-
210
+ // effort: an absent table or any query error resolves to 'unknown', never
211
+ // throws. Wire it up with:
212
+ // sdk.registerFrameworkAdapter({ resolveMigrationId: () => readTypeOrmMigrationId(dataSource) })
213
+ async function readTypeOrmMigrationId(dataSource, migrationsTableName = 'migrations') {
214
+ const ds = dataSource;
215
+ try {
216
+ const rows = await ds.query(`SELECT name FROM ${migrationsTableName} ORDER BY timestamp DESC LIMIT 1`);
217
+ if (rows?.[0]?.name)
218
+ return rows[0].name;
219
+ }
220
+ catch {
221
+ try {
222
+ const rows = await ds.query(`SELECT TOP 1 name FROM ${migrationsTableName} ORDER BY timestamp DESC`);
223
+ if (rows?.[0]?.name)
224
+ return rows[0].name;
225
+ }
226
+ catch {
227
+ // migration id stays 'unknown'
228
+ }
229
+ }
230
+ return 'unknown';
231
+ }
package/dist/context.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface FootprintQuery {
13
13
  where_shape?: string;
14
14
  count: number;
15
15
  note?: string;
16
+ resolved?: boolean;
16
17
  }
17
18
  export interface OutboundCall {
18
19
  host: string;
@@ -24,14 +25,31 @@ export interface OutboundCall {
24
25
  duration_ms?: number;
25
26
  outcome?: 'success' | 'timeout' | 'network_error';
26
27
  }
28
+ export interface BusinessInvariantHit {
29
+ ruleId: string;
30
+ message: string;
31
+ pks: string[];
32
+ }
33
+ export interface WriteCountDriftHit {
34
+ route: string;
35
+ model: string;
36
+ operation: string;
37
+ }
27
38
  export interface ReprovaContext {
28
39
  traceId: string;
29
40
  capturedAt: Date;
30
41
  request: RequestSnapshot;
31
42
  footprint: FootprintQuery[];
32
43
  outboundCalls: OutboundCall[];
44
+ businessInvariantHits: BusinessInvariantHit[];
45
+ writeCountDriftHits: WriteCountDriftHit[];
33
46
  captured?: boolean;
34
47
  rawRequest?: unknown;
48
+ replayDetection?: {
49
+ trigger: string;
50
+ type: string;
51
+ message: string;
52
+ };
35
53
  }
36
54
  export declare const contextStorage: AsyncLocalStorage<ReprovaContext>;
37
55
  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;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;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAGnE"}
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;IAQd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;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,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;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,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;IAC9C,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IAKnB,UAAU,CAAC,EAAE,OAAO,CAAC;IAOrB,eAAe,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE;AAED,eAAO,MAAM,cAAc,mCAA0C,CAAC;AAEtE,wBAAgB,cAAc,IAAI,cAAc,GAAG,SAAS,CAE3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAGnE"}
@@ -1 +1 @@
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;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,MAA+B;IAC3D,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;IAC7B,IAAI,GAAG;QAAE,GAAG,CAAC,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC;AAC3C,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA2EhD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAEtE,MAAM,UAAU,cAAc;IAC5B,OAAO,cAAc,CAAC,QAAQ,EAAE,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,MAA+B;IAC3D,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;IAC7B,IAAI,GAAG;QAAE,GAAG,CAAC,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { DistributionBaseline } from './distributionDrift.js';
2
+ interface DistributionBaselinePollerOptions {
3
+ endpoint: string;
4
+ apiKey: string;
5
+ intervalMs?: number;
6
+ }
7
+ export interface DistributionBaselinesResponse {
8
+ enabled?: boolean;
9
+ baselines?: Array<{
10
+ route: string;
11
+ field: string;
12
+ sample_count: number;
13
+ mean: number;
14
+ stddev: number;
15
+ }>;
16
+ }
17
+ export declare class DistributionBaselinePoller {
18
+ private enabled;
19
+ private baselines;
20
+ private timer;
21
+ private lastWarnAt;
22
+ private readonly endpoint;
23
+ private readonly apiKey;
24
+ private readonly intervalMs;
25
+ constructor(opts: DistributionBaselinePollerOptions);
26
+ start(): void;
27
+ stop(): void;
28
+ isEnabled(): boolean;
29
+ getBaseline(route: string, field: string): DistributionBaseline | undefined;
30
+ refresh(): Promise<void>;
31
+ seed(body: DistributionBaselinesResponse): void;
32
+ private applyResponse;
33
+ private warnOnce;
34
+ }
35
+ export {};
36
+ //# sourceMappingURL=distributionBaselinePoller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributionBaselinePoller.d.ts","sourceRoot":"","sources":["../src/distributionBaselinePoller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,UAAU,iCAAiC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzG;AAaD,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA2C;IAC5D,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,IAAI,EAAE,iCAAiC;IAMnD,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAOZ,SAAS,IAAI,OAAO;IAIpB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAIrE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB9B,IAAI,CAAC,IAAI,EAAE,6BAA6B,GAAG,IAAI;IAI/C,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,QAAQ;CAOjB"}