@reprova/sdk 0.4.0 → 0.6.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 +170 -8
- package/dist/cjs/context.js +28 -0
- package/dist/cjs/distributionBaselinePoller.js +87 -0
- package/dist/cjs/distributionDrift.js +170 -0
- package/dist/cjs/drizzle.js +204 -0
- package/dist/cjs/frameworkAdapter.js +2 -0
- package/dist/cjs/httpFetch.js +59 -0
- package/dist/cjs/idempotency.js +132 -0
- package/dist/cjs/idempotencySettingsPoller.js +71 -0
- package/dist/cjs/index.js +96 -0
- package/dist/cjs/invariantInference.js +90 -0
- package/dist/cjs/invariantRules.js +142 -0
- package/dist/cjs/knex.js +86 -0
- package/dist/cjs/kysely.js +175 -0
- package/dist/cjs/legacyPrisma.js +130 -0
- package/dist/cjs/mikroorm.js +74 -0
- package/dist/cjs/mongoSchemaReporter.js +85 -0
- package/dist/cjs/mongoose.js +335 -0
- package/dist/cjs/nPlusOne.js +0 -0
- package/dist/cjs/nest.js +136 -0
- package/dist/cjs/nextjs.js +180 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/prisma.js +363 -0
- package/dist/cjs/proto.gen.js +4 -0
- package/dist/cjs/registry.js +100 -0
- package/dist/cjs/retryStorm.js +70 -0
- package/dist/cjs/rulesPoller.js +77 -0
- package/dist/cjs/sdk.js +1231 -0
- package/dist/cjs/sequelize.js +108 -0
- package/dist/cjs/shape.js +45 -0
- package/dist/cjs/stack.js +51 -0
- package/dist/cjs/transport.js +71 -0
- package/dist/cjs/typeorm.js +200 -0
- package/dist/context.d.ts +32 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +18 -0
- package/dist/context.js.map +1 -1
- package/dist/distributionBaselinePoller.d.ts +36 -0
- package/dist/distributionBaselinePoller.d.ts.map +1 -0
- package/dist/distributionBaselinePoller.js +84 -0
- package/dist/distributionBaselinePoller.js.map +1 -0
- package/dist/distributionDrift.d.ts +41 -0
- package/dist/distributionDrift.d.ts.map +1 -0
- package/dist/distributionDrift.js +161 -0
- package/dist/distributionDrift.js.map +1 -0
- package/dist/drizzle.d.ts +22 -0
- package/dist/drizzle.d.ts.map +1 -1
- package/dist/drizzle.js +129 -1
- package/dist/drizzle.js.map +1 -1
- package/dist/frameworkAdapter.d.ts +5 -0
- package/dist/frameworkAdapter.d.ts.map +1 -0
- package/dist/frameworkAdapter.js +2 -0
- package/dist/frameworkAdapter.js.map +1 -0
- package/dist/httpFetch.d.ts +13 -0
- package/dist/httpFetch.d.ts.map +1 -0
- package/dist/httpFetch.js +54 -0
- package/dist/httpFetch.js.map +1 -0
- package/dist/idempotency.d.ts +26 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +124 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/idempotencySettingsPoller.d.ts +23 -0
- package/dist/idempotencySettingsPoller.d.ts.map +1 -0
- package/dist/idempotencySettingsPoller.js +68 -0
- package/dist/idempotencySettingsPoller.js.map +1 -0
- package/dist/index.d.ts +33 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -4
- package/dist/index.js.map +1 -1
- package/dist/invariantInference.d.ts +28 -0
- package/dist/invariantInference.d.ts.map +1 -0
- package/dist/invariantInference.js +87 -0
- package/dist/invariantInference.js.map +1 -0
- package/dist/invariantRules.d.ts +16 -0
- package/dist/invariantRules.d.ts.map +1 -0
- package/dist/invariantRules.js +104 -0
- package/dist/invariantRules.js.map +1 -0
- package/dist/knex.d.ts.map +1 -1
- package/dist/knex.js +1 -0
- package/dist/knex.js.map +1 -1
- package/dist/kysely.d.ts.map +1 -1
- package/dist/kysely.js +2 -1
- package/dist/kysely.js.map +1 -1
- package/dist/legacyPrisma.d.ts +6 -0
- package/dist/legacyPrisma.d.ts.map +1 -0
- package/dist/legacyPrisma.js +128 -0
- package/dist/legacyPrisma.js.map +1 -0
- package/dist/mikroorm.d.ts +2 -0
- package/dist/mikroorm.d.ts.map +1 -0
- package/dist/mikroorm.js +72 -0
- package/dist/mikroorm.js.map +1 -0
- package/dist/mongoSchemaReporter.d.ts +23 -0
- package/dist/mongoSchemaReporter.d.ts.map +1 -0
- package/dist/mongoSchemaReporter.js +82 -0
- package/dist/mongoSchemaReporter.js.map +1 -0
- package/dist/mongoose.d.ts +48 -0
- package/dist/mongoose.d.ts.map +1 -0
- package/dist/mongoose.js +331 -0
- package/dist/mongoose.js.map +1 -0
- package/dist/nPlusOne.d.ts +52 -0
- package/dist/nPlusOne.d.ts.map +1 -0
- package/dist/nPlusOne.js +0 -0
- package/dist/nPlusOne.js.map +1 -0
- package/dist/nest.d.ts +31 -0
- package/dist/nest.d.ts.map +1 -0
- package/dist/nest.js +133 -0
- package/dist/nest.js.map +1 -0
- package/dist/nextjs.d.ts +27 -0
- package/dist/nextjs.d.ts.map +1 -0
- package/dist/nextjs.js +178 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/prisma.d.ts +21 -5
- package/dist/prisma.d.ts.map +1 -1
- package/dist/prisma.js +247 -22
- package/dist/prisma.js.map +1 -1
- package/dist/proto.gen.d.ts +19 -1
- package/dist/proto.gen.d.ts.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +24 -0
- package/dist/registry.js.map +1 -1
- package/dist/retryStorm.d.ts +42 -0
- package/dist/retryStorm.d.ts.map +1 -0
- package/dist/retryStorm.js +65 -0
- package/dist/retryStorm.js.map +1 -0
- package/dist/rulesPoller.d.ts +26 -0
- package/dist/rulesPoller.d.ts.map +1 -0
- package/dist/rulesPoller.js +74 -0
- package/dist/rulesPoller.js.map +1 -0
- package/dist/sdk.d.ts +53 -6
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +663 -34
- package/dist/sdk.js.map +1 -1
- package/dist/sequelize.d.ts.map +1 -1
- package/dist/sequelize.js +4 -3
- package/dist/sequelize.js.map +1 -1
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +6 -3
- package/dist/stack.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +2 -1
- package/dist/transport.js.map +1 -1
- package/dist/typeorm.d.ts +1 -0
- package/dist/typeorm.d.ts.map +1 -1
- package/dist/typeorm.js +133 -1
- package/dist/typeorm.js.map +1 -1
- package/package.json +20 -5
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDrizzleLogger = createDrizzleLogger;
|
|
4
|
+
exports.createDrizzleCache = createDrizzleCache;
|
|
5
|
+
exports.createDrizzleFootprint = createDrizzleFootprint;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const context_js_1 = require("./context.js");
|
|
8
|
+
const OP_BY_KEYWORD = {
|
|
9
|
+
select: 'select',
|
|
10
|
+
insert: 'insert',
|
|
11
|
+
update: 'update',
|
|
12
|
+
delete: 'delete',
|
|
13
|
+
};
|
|
14
|
+
// First SQL keyword → operation.
|
|
15
|
+
function opOf(sql) {
|
|
16
|
+
const m = /^\s*(select|insert|update|delete)\b/i.exec(sql);
|
|
17
|
+
return m ? OP_BY_KEYWORD[m[1].toLowerCase()] : undefined;
|
|
18
|
+
}
|
|
19
|
+
// The principal table, from the op-appropriate clause.
|
|
20
|
+
function tableOf(sql, op) {
|
|
21
|
+
let m = null;
|
|
22
|
+
if (op === 'select' || op === 'delete')
|
|
23
|
+
m = /\bfrom\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
|
|
24
|
+
else if (op === 'insert')
|
|
25
|
+
m = /\binto\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
|
|
26
|
+
else if (op === 'update')
|
|
27
|
+
m = /\bupdate\s+"?([A-Za-z0-9_]+)"?/i.exec(sql);
|
|
28
|
+
return m ? m[1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
// Column names referenced in the WHERE clause — the right-hand identifier of
|
|
31
|
+
// each `"table"."col"` (or a bare `"col"`). Names only; values are `?`.
|
|
32
|
+
function whereColumns(sql) {
|
|
33
|
+
const wIdx = sql.search(/\bwhere\b/i);
|
|
34
|
+
if (wIdx < 0)
|
|
35
|
+
return [];
|
|
36
|
+
const tail = sql.slice(wIdx + 5);
|
|
37
|
+
const cols = new Set();
|
|
38
|
+
const re = /"([A-Za-z0-9_]+)"(?:\s*\.\s*"([A-Za-z0-9_]+)")?/g;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(tail)) !== null)
|
|
41
|
+
cols.add(m[2] ?? m[1]);
|
|
42
|
+
return [...cols].sort();
|
|
43
|
+
}
|
|
44
|
+
function extractPks(rows, pkCols) {
|
|
45
|
+
if (pkCols.length === 0)
|
|
46
|
+
return [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const pks = [];
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const vals = pkCols.map((c) => row[c]);
|
|
51
|
+
if (vals.some((v) => v === undefined))
|
|
52
|
+
continue; // column not selected — can't form a PK
|
|
53
|
+
const pk = vals.map((v) => String(v)).join(':');
|
|
54
|
+
if (!seen.has(pk)) {
|
|
55
|
+
seen.add(pk);
|
|
56
|
+
pks.push(pk);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return pks;
|
|
60
|
+
}
|
|
61
|
+
// Drizzle's own cache key: SHA-256(sql + '-' + JSON.stringify(params)) — see
|
|
62
|
+
// drizzle-orm/cache/core/cache.js. Reimplemented (not imported) so a query's
|
|
63
|
+
// hash computed here from `logger.logQuery`'s (sql, params) matches the hash
|
|
64
|
+
// Drizzle later passes to `cache.put()` for the SAME query, letting the two
|
|
65
|
+
// hooks correlate without ever importing 'drizzle-orm'.
|
|
66
|
+
// Node's own crypto module (not WebCrypto's `crypto.subtle`, which only
|
|
67
|
+
// became a stable global in Node 19+) — createHash has existed since
|
|
68
|
+
// Node's earliest crypto module, so this works on any Node version this
|
|
69
|
+
// SDK targets. Stays async (a plain value from an async function resolves
|
|
70
|
+
// immediately) so callers don't need to change.
|
|
71
|
+
async function hashQuery(sql, params) {
|
|
72
|
+
const data = `${sql}-${JSON.stringify(params)}`;
|
|
73
|
+
return (0, node_crypto_1.createHash)('sha256').update(data).digest('hex');
|
|
74
|
+
}
|
|
75
|
+
// A pending SELECT footprint entry, keyed by query hash, waiting to be
|
|
76
|
+
// upgraded by a paired cache's put(). Linked to its logger via WeakMap so the
|
|
77
|
+
// map is GC'd along with the logger if the app never pairs a cache to it.
|
|
78
|
+
const pendingByLogger = new WeakMap();
|
|
79
|
+
// Creates a Drizzle-compatible logger that records footprints.
|
|
80
|
+
// Usage: drizzle(client, { logger: createDrizzleLogger() })
|
|
81
|
+
function createDrizzleLogger(opts = {}) {
|
|
82
|
+
const captureWhereShape = opts.captureWhereShape !== false;
|
|
83
|
+
const forward = opts.forward;
|
|
84
|
+
const pending = new Map();
|
|
85
|
+
const logger = {
|
|
86
|
+
logQuery(query, params) {
|
|
87
|
+
try {
|
|
88
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
89
|
+
if (ctx) {
|
|
90
|
+
const op = opOf(query);
|
|
91
|
+
const table = op ? tableOf(query, op) : undefined;
|
|
92
|
+
if (op && table) {
|
|
93
|
+
const where = captureWhereShape ? whereColumns(query).join(',') || undefined : undefined;
|
|
94
|
+
const entry = {
|
|
95
|
+
model: table,
|
|
96
|
+
op,
|
|
97
|
+
pks: [],
|
|
98
|
+
where_shape: where,
|
|
99
|
+
count: 0,
|
|
100
|
+
note: 'drizzle: shape-only (no result access — PKs/count unavailable)',
|
|
101
|
+
resolved: true,
|
|
102
|
+
};
|
|
103
|
+
ctx.footprint.push(entry);
|
|
104
|
+
// A paired cache can upgrade this SAME entry once the real rows
|
|
105
|
+
// are known — see createDrizzleCache(). Mutations get nothing
|
|
106
|
+
// more from Drizzle's cache hook, so they stay as pushed above.
|
|
107
|
+
if (op === 'select') {
|
|
108
|
+
hashQuery(query, params)
|
|
109
|
+
.then((h) => pending.set(h, entry))
|
|
110
|
+
.catch(() => { });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { /* never break a query */ }
|
|
116
|
+
try {
|
|
117
|
+
forward?.logQuery(query, params);
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
pendingByLogger.set(logger, pending);
|
|
123
|
+
return logger;
|
|
124
|
+
}
|
|
125
|
+
// node-postgres (`pg`) QueryResult vs. mysql2's `[rows, fields]` tuple — the
|
|
126
|
+
// two response shapes `cache.put()` actually sees for Reprova's two real
|
|
127
|
+
// customer-database dialects (Postgres, MySQL; see agent/internal/dialect).
|
|
128
|
+
// Unrecognized shapes degrade to "no rows found" rather than guessing.
|
|
129
|
+
function rowsFromResponse(response) {
|
|
130
|
+
if (Array.isArray(response)) {
|
|
131
|
+
const [rows] = response; // mysql2: client.query() resolves [rows, fields]
|
|
132
|
+
return Array.isArray(rows) ? rows : undefined;
|
|
133
|
+
}
|
|
134
|
+
const rows = response?.rows;
|
|
135
|
+
return Array.isArray(rows) ? rows : undefined;
|
|
136
|
+
}
|
|
137
|
+
// Creates a Drizzle Cache that observes (never serves) query results to
|
|
138
|
+
// upgrade SELECT footprints with exact primary keys and row counts.
|
|
139
|
+
// Usage: drizzle(client, { cache: createDrizzleCache({ logger }) })
|
|
140
|
+
function createDrizzleCache(opts = {}) {
|
|
141
|
+
const primaryKey = opts.primaryKey ?? (() => ['id']);
|
|
142
|
+
const pending = opts.logger ? pendingByLogger.get(opts.logger) : undefined;
|
|
143
|
+
return {
|
|
144
|
+
// 'all' — every SELECT is observed automatically; apps never need to
|
|
145
|
+
// call Drizzle's own `.$withCache()` for this to work.
|
|
146
|
+
strategy() {
|
|
147
|
+
return 'all';
|
|
148
|
+
},
|
|
149
|
+
// Always a miss: this cache exists to OBSERVE results, never to REPLACE a
|
|
150
|
+
// real query with a stale one.
|
|
151
|
+
async get() {
|
|
152
|
+
return undefined;
|
|
153
|
+
},
|
|
154
|
+
async put(hashedQuery, response, tables) {
|
|
155
|
+
try {
|
|
156
|
+
if (!tables || tables.length === 0)
|
|
157
|
+
return;
|
|
158
|
+
const rows = rowsFromResponse(response);
|
|
159
|
+
if (!rows)
|
|
160
|
+
return;
|
|
161
|
+
const primary = tables[0];
|
|
162
|
+
const pks = extractPks(rows, primaryKey(primary));
|
|
163
|
+
const count = rows.length;
|
|
164
|
+
const note = count === 0 ? 'returned 0 rows' : undefined;
|
|
165
|
+
const linked = pending?.get(hashedQuery);
|
|
166
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
167
|
+
if (linked) {
|
|
168
|
+
pending.delete(hashedQuery);
|
|
169
|
+
linked.model = primary;
|
|
170
|
+
linked.pks = pks;
|
|
171
|
+
linked.count = count;
|
|
172
|
+
linked.note = note;
|
|
173
|
+
}
|
|
174
|
+
else if (ctx) {
|
|
175
|
+
ctx.footprint.push({ model: primary, op: 'select', pks, count, note, resolved: true });
|
|
176
|
+
}
|
|
177
|
+
// Joined/subquery tables: recorded as referenced, without PKs — same
|
|
178
|
+
// convention as the Kysely adapter (result columns can't be reliably
|
|
179
|
+
// attributed to a joined table's own primary key).
|
|
180
|
+
if (ctx) {
|
|
181
|
+
for (const t of tables.slice(1)) {
|
|
182
|
+
ctx.footprint.push({ model: t, op: 'select', pks: [], count: 0, note: 'referenced (join/subquery)', resolved: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* never break a query */ }
|
|
187
|
+
},
|
|
188
|
+
// Drizzle's mutation hook gets table names only — no rows, no count, and
|
|
189
|
+
// it races the real query (Promise.all internally). Nothing usable to
|
|
190
|
+
// record here; mutations are already captured (shape-only) by the paired
|
|
191
|
+
// logger.
|
|
192
|
+
async onMutate() { },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Convenience: builds a linked logger+cache pair in one call, the
|
|
196
|
+
// recommended way to get exact SELECT pks/count without double-counting.
|
|
197
|
+
// Usage:
|
|
198
|
+
// const { logger, cache } = createDrizzleFootprint();
|
|
199
|
+
// const db = drizzle(client, { logger, cache });
|
|
200
|
+
function createDrizzleFootprint(opts = {}) {
|
|
201
|
+
const logger = createDrizzleLogger({ captureWhereShape: opts.captureWhereShape });
|
|
202
|
+
const cache = createDrizzleCache({ primaryKey: opts.primaryKey, logger });
|
|
203
|
+
return { logger, cache };
|
|
204
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.httpFetch = httpFetch;
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
9
|
+
// Node 18+ ships a global fetch; older Node (found live: Node 14, still a
|
|
10
|
+
// real LTS for legacy apps) has none at all. node-fetch isn't a safe
|
|
11
|
+
// replacement here — v3+ is ESM-only, the exact same require(ESM) trap
|
|
12
|
+
// @marcbachmann/cel-js caused (see invariantRules.ts's own doc comment on
|
|
13
|
+
// loadEnv). Every internal caller in this SDK only ever needs a small,
|
|
14
|
+
// stable subset of the Fetch API (GET/POST, headers, a string body,
|
|
15
|
+
// .ok/.status/.json()/.text()), so this implements just that on top of
|
|
16
|
+
// Node's own always-available http/https modules, and delegates to a real
|
|
17
|
+
// global fetch when one exists (the common case — zero overhead, zero
|
|
18
|
+
// behavior change on any Node version that already has one).
|
|
19
|
+
function httpFetch(url, init) {
|
|
20
|
+
const globalFetch = globalThis.fetch;
|
|
21
|
+
if (typeof globalFetch === 'function') {
|
|
22
|
+
return globalFetch(url, init);
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(url);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
reject(err);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const lib = parsed.protocol === 'https:' ? node_https_1.default : node_http_1.default;
|
|
34
|
+
const req = lib.request({
|
|
35
|
+
hostname: parsed.hostname,
|
|
36
|
+
port: parsed.port || undefined,
|
|
37
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
38
|
+
method: init?.method ?? 'GET',
|
|
39
|
+
headers: init?.headers,
|
|
40
|
+
}, (res) => {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
45
|
+
const status = res.statusCode ?? 0;
|
|
46
|
+
resolve({
|
|
47
|
+
ok: status >= 200 && status < 300,
|
|
48
|
+
status,
|
|
49
|
+
json: async () => JSON.parse(text),
|
|
50
|
+
text: async () => text,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
req.on('error', reject);
|
|
55
|
+
if (init?.body)
|
|
56
|
+
req.write(init.body);
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Idempotency/consistency checks: two independent, domain-agnostic
|
|
3
|
+
// detectors for "silent 200" bugs, unlike business-invariant rules — neither
|
|
4
|
+
// needs a dashboard-authored condition tree, since neither depends on
|
|
5
|
+
// knowing anything about the app's data model.
|
|
6
|
+
//
|
|
7
|
+
// 1. Idempotency-key mismatch: the app itself asserts two requests are "the
|
|
8
|
+
// same operation" by sending an identical value in a known header (e.g.
|
|
9
|
+
// Stripe-style Idempotency-Key). If the response differs between them,
|
|
10
|
+
// that's unambiguously a bug — no heuristic tuning needed, the app told
|
|
11
|
+
// us these should match.
|
|
12
|
+
// 2. Duplicate-read mismatch: the SAME GET (route + query + caller
|
|
13
|
+
// identity) repeated within a short window returning a different
|
|
14
|
+
// result. Real false-positive risk exists here (data can legitimately
|
|
15
|
+
// change between two reads), so it's scoped tight: a short window turns
|
|
16
|
+
// it into "did two near-simultaneous reads of what should be the same
|
|
17
|
+
// snapshot disagree" (a race-condition/double-fetch signal), not general
|
|
18
|
+
// staleness detection — and it's skipped entirely when no caller
|
|
19
|
+
// identity is available, since an identity-less GET trivially disagrees
|
|
20
|
+
// across different callers.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.HashMismatchCache = exports.DuplicateReadMismatch = exports.IdempotencyKeyMismatch = exports.DEFAULT_IDEMPOTENCY_SETTINGS = void 0;
|
|
23
|
+
exports.hashResponseBody = hashResponseBody;
|
|
24
|
+
exports.findIdempotencyKey = findIdempotencyKey;
|
|
25
|
+
exports.extractIdentity = extractIdentity;
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
// Mirrors the control plane's idempotency_settings table defaults — used
|
|
28
|
+
// before the first successful poll, and if polling never succeeds at all
|
|
29
|
+
// (e.g. an older control plane without this endpoint yet).
|
|
30
|
+
exports.DEFAULT_IDEMPOTENCY_SETTINGS = {
|
|
31
|
+
key_check_enabled: true,
|
|
32
|
+
header_names: ['Idempotency-Key', 'X-Idempotency-Key'],
|
|
33
|
+
ttl_ms: 86_400_000,
|
|
34
|
+
max_keys: 100_000,
|
|
35
|
+
duplicate_read_enabled: false,
|
|
36
|
+
duplicate_read_window_ms: 500,
|
|
37
|
+
};
|
|
38
|
+
// One reusable Error class per detector, mirroring BusinessInvariantViolation
|
|
39
|
+
// — per-endpoint identity comes from capture()'s errorType override, not a
|
|
40
|
+
// per-instance subclass, so many distinct endpoints share one class without
|
|
41
|
+
// collapsing into a single issue.
|
|
42
|
+
class IdempotencyKeyMismatch extends Error {
|
|
43
|
+
}
|
|
44
|
+
exports.IdempotencyKeyMismatch = IdempotencyKeyMismatch;
|
|
45
|
+
class DuplicateReadMismatch extends Error {
|
|
46
|
+
}
|
|
47
|
+
exports.DuplicateReadMismatch = DuplicateReadMismatch;
|
|
48
|
+
// Deterministic regardless of key insertion order, so { a:1, b:2 } and
|
|
49
|
+
// { b:2, a:1 } hash identically — otherwise two genuinely-equivalent
|
|
50
|
+
// responses serialized in a different key order would look like a mismatch
|
|
51
|
+
// that isn't one.
|
|
52
|
+
function stableStringify(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object')
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
57
|
+
const obj = value;
|
|
58
|
+
const keys = Object.keys(obj).sort();
|
|
59
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
|
|
60
|
+
}
|
|
61
|
+
function hashResponseBody(body) {
|
|
62
|
+
return (0, node_crypto_1.createHash)('sha256').update(stableStringify(body)).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
// Bounded signature -> last-seen-hash cache with TTL + LRU eviction, shared
|
|
65
|
+
// by both detectors — the underlying mechanism ("has this signature's hash
|
|
66
|
+
// changed within its window") is identical for both; only what goes into
|
|
67
|
+
// the signature and how long it's remembered differs. Map iteration order
|
|
68
|
+
// is insertion order, and delete-then-reinsert on every access keeps it in
|
|
69
|
+
// least-recently-used order, so eviction always drops the LRU entry.
|
|
70
|
+
class HashMismatchCache {
|
|
71
|
+
maxKeys;
|
|
72
|
+
ttlMs;
|
|
73
|
+
map = new Map();
|
|
74
|
+
constructor(maxKeys, ttlMs) {
|
|
75
|
+
this.maxKeys = maxKeys;
|
|
76
|
+
this.ttlMs = ttlMs;
|
|
77
|
+
}
|
|
78
|
+
// Settings can change on every poll — reconfigure in place rather than
|
|
79
|
+
// recreating the cache, so a TTL/cap edit doesn't silently drop everything
|
|
80
|
+
// currently tracked.
|
|
81
|
+
configure(maxKeys, ttlMs) {
|
|
82
|
+
this.maxKeys = maxKeys;
|
|
83
|
+
this.ttlMs = ttlMs;
|
|
84
|
+
}
|
|
85
|
+
// Records `hash` for `signature`, returning true if a live (non-expired)
|
|
86
|
+
// prior hash for the same signature exists and differs from this one.
|
|
87
|
+
checkAndRecord(signature, hash, now = Date.now()) {
|
|
88
|
+
const existing = this.map.get(signature);
|
|
89
|
+
this.map.delete(signature); // re-inserted below either way, refreshing LRU order
|
|
90
|
+
const mismatch = !!existing && existing.expiresAt > now && existing.hash !== hash;
|
|
91
|
+
this.map.set(signature, { hash, expiresAt: now + this.ttlMs });
|
|
92
|
+
while (this.map.size > this.maxKeys) {
|
|
93
|
+
const oldest = this.map.keys().next().value;
|
|
94
|
+
if (oldest === undefined)
|
|
95
|
+
break;
|
|
96
|
+
this.map.delete(oldest);
|
|
97
|
+
}
|
|
98
|
+
return mismatch;
|
|
99
|
+
}
|
|
100
|
+
get size() {
|
|
101
|
+
return this.map.size;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.HashMismatchCache = HashMismatchCache;
|
|
105
|
+
// Checks each configured header name case-insensitively — Node normalizes
|
|
106
|
+
// incoming header names to lowercase, and captureHeaders (stack.ts) passes
|
|
107
|
+
// that through unchanged, so ctx.request.headers keys are already lowercase.
|
|
108
|
+
function findIdempotencyKey(headers, headerNames) {
|
|
109
|
+
for (const name of headerNames) {
|
|
110
|
+
const value = headers[name.toLowerCase()];
|
|
111
|
+
if (value)
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
// A small, conventional set of claim keys checked in order — not exhaustive,
|
|
117
|
+
// just enough to cover common shapes (Auth0/JWT `sub`, a plain `id`,
|
|
118
|
+
// Passport-style `userId`/`user_id`). Returns undefined, never a guess, when
|
|
119
|
+
// none match: duplicate-read detection is skipped entirely without an
|
|
120
|
+
// identity, rather than risk comparing two different callers' data and
|
|
121
|
+
// calling the difference a bug.
|
|
122
|
+
const IDENTITY_CLAIM_KEYS = ['sub', 'id', 'userId', 'user_id'];
|
|
123
|
+
function extractIdentity(authClaims) {
|
|
124
|
+
if (!authClaims)
|
|
125
|
+
return undefined;
|
|
126
|
+
for (const key of IDENTITY_CLAIM_KEYS) {
|
|
127
|
+
const value = authClaims[key];
|
|
128
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
129
|
+
return String(value);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdempotencySettingsPoller = void 0;
|
|
4
|
+
const idempotency_js_1 = require("./idempotency.js");
|
|
5
|
+
const httpFetch_js_1 = require("./httpFetch.js");
|
|
6
|
+
const WARN_INTERVAL_MS = 60_000;
|
|
7
|
+
// Same start()/stop()/setInterval+unref shape as RulesPoller — periodically
|
|
8
|
+
// fetches the tenant's dashboard-configured idempotency-check settings so a
|
|
9
|
+
// toggle flipped in the dashboard reaches a running process without a
|
|
10
|
+
// redeploy. Falls back to DEFAULT_IDEMPOTENCY_SETTINGS before the first
|
|
11
|
+
// successful poll and keeps the last-known-good settings on any failure,
|
|
12
|
+
// never throwing and never leaving the detectors unconfigured.
|
|
13
|
+
class IdempotencySettingsPoller {
|
|
14
|
+
settings = idempotency_js_1.DEFAULT_IDEMPOTENCY_SETTINGS;
|
|
15
|
+
timer = null;
|
|
16
|
+
lastWarnAt = 0;
|
|
17
|
+
endpoint;
|
|
18
|
+
apiKey;
|
|
19
|
+
intervalMs;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.endpoint = opts.endpoint;
|
|
22
|
+
this.apiKey = opts.apiKey;
|
|
23
|
+
this.intervalMs = opts.intervalMs ?? 60_000;
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
void this.refresh(); // don't wait a full interval before settings are usable
|
|
27
|
+
this.timer = setInterval(() => void this.refresh(), this.intervalMs);
|
|
28
|
+
if (this.timer.unref)
|
|
29
|
+
this.timer.unref(); // don't block process exit
|
|
30
|
+
}
|
|
31
|
+
stop() {
|
|
32
|
+
if (this.timer) {
|
|
33
|
+
clearInterval(this.timer);
|
|
34
|
+
this.timer = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getSettings() {
|
|
38
|
+
return this.settings;
|
|
39
|
+
}
|
|
40
|
+
async refresh() {
|
|
41
|
+
try {
|
|
42
|
+
const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
|
|
43
|
+
headers: { 'X-Reprova-Key': this.apiKey },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
this.warnOnce(`reprova: idempotency settings fetch returned ${res.status}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.seed((await res.json()));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
this.warnOnce(`reprova: idempotency settings fetch error: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Applies an already-fetched settings object directly, with no network
|
|
56
|
+
// call — used by replay mode to seed a one-time snapshot the CLI fetched
|
|
57
|
+
// ahead of time (there's no live control plane to poll from inside a
|
|
58
|
+
// replayed app), so checkIdempotencyIfAny (sdk.ts) sees exactly the same
|
|
59
|
+
// shape it would from a real refresh().
|
|
60
|
+
seed(settings) {
|
|
61
|
+
this.settings = settings;
|
|
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.IdempotencySettingsPoller = IdempotencySettingsPoller;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.isDrift = exports.updateWelfordState = exports.newWelfordState = exports.WriteCountDrift = exports.ValueDistributionDrift = exports.InferenceReporter = exports.IdempotencySettingsPoller = exports.hashResponseBody = exports.extractIdentity = exports.findIdempotencyKey = exports.DuplicateReadMismatch = exports.IdempotencyKeyMismatch = exports.HashMismatchCache = exports.DEFAULT_IDEMPOTENCY_SETTINGS = exports.RulesPoller = exports.BusinessInvariantViolation = exports.operationMatchesHook = exports.evaluateInvariant = exports.compileInvariant = exports.PotentialRetryStorm = exports.describeRetryStorm = exports.detectRetryStorm = exports.PotentialNPlusOneQuery = exports.describeNPlusOne = exports.detectNPlusOne = exports.createDrizzleFootprint = exports.createDrizzleCache = exports.createDrizzleLogger = exports.createNextAdapter = exports.createNestContextInterceptor = exports.createNestExceptionFilter = exports.installKnexHooks = exports.MongoSchemaReporter = exports.discoverMongooseSchema = exports.reprovaMongoosePlugin = exports.installMikroOrmSubscriber = exports.installTypeOrmQueryLogger = exports.installTypeOrmSubscriber = exports.installSequelizeHooks = exports.createKyselyPlugin = exports.instrumentLegacyPrismaClient = exports.recordPrismaOperation = exports.createPrismaExtension = exports.setAuthClaims = exports.currentContext = exports.contextStorage = exports.replayErrorHandler = exports.forwardAsyncErrors = exports.wrapResponse = exports.Reprova = void 0;
|
|
18
|
+
exports.SDK_VERSION = exports.DistributionBaselinePoller = exports.DistributionDriftReporter = exports.extractTopLevelNumericFields = void 0;
|
|
19
|
+
var sdk_js_1 = require("./sdk.js");
|
|
20
|
+
Object.defineProperty(exports, "Reprova", { enumerable: true, get: function () { return sdk_js_1.Reprova; } });
|
|
21
|
+
Object.defineProperty(exports, "wrapResponse", { enumerable: true, get: function () { return sdk_js_1.wrapResponse; } });
|
|
22
|
+
Object.defineProperty(exports, "forwardAsyncErrors", { enumerable: true, get: function () { return sdk_js_1.forwardAsyncErrors; } });
|
|
23
|
+
Object.defineProperty(exports, "replayErrorHandler", { enumerable: true, get: function () { return sdk_js_1.replayErrorHandler; } });
|
|
24
|
+
var context_js_1 = require("./context.js");
|
|
25
|
+
Object.defineProperty(exports, "contextStorage", { enumerable: true, get: function () { return context_js_1.contextStorage; } });
|
|
26
|
+
Object.defineProperty(exports, "currentContext", { enumerable: true, get: function () { return context_js_1.currentContext; } });
|
|
27
|
+
Object.defineProperty(exports, "setAuthClaims", { enumerable: true, get: function () { return context_js_1.setAuthClaims; } });
|
|
28
|
+
var prisma_js_1 = require("./prisma.js");
|
|
29
|
+
Object.defineProperty(exports, "createPrismaExtension", { enumerable: true, get: function () { return prisma_js_1.createPrismaExtension; } });
|
|
30
|
+
Object.defineProperty(exports, "recordPrismaOperation", { enumerable: true, get: function () { return prisma_js_1.recordPrismaOperation; } });
|
|
31
|
+
var legacyPrisma_js_1 = require("./legacyPrisma.js");
|
|
32
|
+
Object.defineProperty(exports, "instrumentLegacyPrismaClient", { enumerable: true, get: function () { return legacyPrisma_js_1.instrumentLegacyPrismaClient; } });
|
|
33
|
+
var kysely_js_1 = require("./kysely.js");
|
|
34
|
+
Object.defineProperty(exports, "createKyselyPlugin", { enumerable: true, get: function () { return kysely_js_1.createKyselyPlugin; } });
|
|
35
|
+
var sequelize_js_1 = require("./sequelize.js");
|
|
36
|
+
Object.defineProperty(exports, "installSequelizeHooks", { enumerable: true, get: function () { return sequelize_js_1.installSequelizeHooks; } });
|
|
37
|
+
var typeorm_js_1 = require("./typeorm.js");
|
|
38
|
+
Object.defineProperty(exports, "installTypeOrmSubscriber", { enumerable: true, get: function () { return typeorm_js_1.installTypeOrmSubscriber; } });
|
|
39
|
+
Object.defineProperty(exports, "installTypeOrmQueryLogger", { enumerable: true, get: function () { return typeorm_js_1.installTypeOrmQueryLogger; } });
|
|
40
|
+
var mikroorm_js_1 = require("./mikroorm.js");
|
|
41
|
+
Object.defineProperty(exports, "installMikroOrmSubscriber", { enumerable: true, get: function () { return mikroorm_js_1.installMikroOrmSubscriber; } });
|
|
42
|
+
var mongoose_js_1 = require("./mongoose.js");
|
|
43
|
+
Object.defineProperty(exports, "reprovaMongoosePlugin", { enumerable: true, get: function () { return mongoose_js_1.reprovaMongoosePlugin; } });
|
|
44
|
+
Object.defineProperty(exports, "discoverMongooseSchema", { enumerable: true, get: function () { return mongoose_js_1.discoverMongooseSchema; } });
|
|
45
|
+
var mongoSchemaReporter_js_1 = require("./mongoSchemaReporter.js");
|
|
46
|
+
Object.defineProperty(exports, "MongoSchemaReporter", { enumerable: true, get: function () { return mongoSchemaReporter_js_1.MongoSchemaReporter; } });
|
|
47
|
+
var knex_js_1 = require("./knex.js");
|
|
48
|
+
Object.defineProperty(exports, "installKnexHooks", { enumerable: true, get: function () { return knex_js_1.installKnexHooks; } });
|
|
49
|
+
var nest_js_1 = require("./nest.js");
|
|
50
|
+
Object.defineProperty(exports, "createNestExceptionFilter", { enumerable: true, get: function () { return nest_js_1.createNestExceptionFilter; } });
|
|
51
|
+
Object.defineProperty(exports, "createNestContextInterceptor", { enumerable: true, get: function () { return nest_js_1.createNestContextInterceptor; } });
|
|
52
|
+
var nextjs_js_1 = require("./nextjs.js");
|
|
53
|
+
Object.defineProperty(exports, "createNextAdapter", { enumerable: true, get: function () { return nextjs_js_1.createNextAdapter; } });
|
|
54
|
+
var drizzle_js_1 = require("./drizzle.js");
|
|
55
|
+
Object.defineProperty(exports, "createDrizzleLogger", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleLogger; } });
|
|
56
|
+
Object.defineProperty(exports, "createDrizzleCache", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleCache; } });
|
|
57
|
+
Object.defineProperty(exports, "createDrizzleFootprint", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleFootprint; } });
|
|
58
|
+
var nPlusOne_js_1 = require("./nPlusOne.js");
|
|
59
|
+
Object.defineProperty(exports, "detectNPlusOne", { enumerable: true, get: function () { return nPlusOne_js_1.detectNPlusOne; } });
|
|
60
|
+
Object.defineProperty(exports, "describeNPlusOne", { enumerable: true, get: function () { return nPlusOne_js_1.describeNPlusOne; } });
|
|
61
|
+
Object.defineProperty(exports, "PotentialNPlusOneQuery", { enumerable: true, get: function () { return nPlusOne_js_1.PotentialNPlusOneQuery; } });
|
|
62
|
+
var retryStorm_js_1 = require("./retryStorm.js");
|
|
63
|
+
Object.defineProperty(exports, "detectRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.detectRetryStorm; } });
|
|
64
|
+
Object.defineProperty(exports, "describeRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.describeRetryStorm; } });
|
|
65
|
+
Object.defineProperty(exports, "PotentialRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.PotentialRetryStorm; } });
|
|
66
|
+
var invariantRules_js_1 = require("./invariantRules.js");
|
|
67
|
+
Object.defineProperty(exports, "compileInvariant", { enumerable: true, get: function () { return invariantRules_js_1.compileInvariant; } });
|
|
68
|
+
Object.defineProperty(exports, "evaluateInvariant", { enumerable: true, get: function () { return invariantRules_js_1.evaluateInvariant; } });
|
|
69
|
+
Object.defineProperty(exports, "operationMatchesHook", { enumerable: true, get: function () { return invariantRules_js_1.operationMatchesHook; } });
|
|
70
|
+
Object.defineProperty(exports, "BusinessInvariantViolation", { enumerable: true, get: function () { return invariantRules_js_1.BusinessInvariantViolation; } });
|
|
71
|
+
var rulesPoller_js_1 = require("./rulesPoller.js");
|
|
72
|
+
Object.defineProperty(exports, "RulesPoller", { enumerable: true, get: function () { return rulesPoller_js_1.RulesPoller; } });
|
|
73
|
+
var idempotency_js_1 = require("./idempotency.js");
|
|
74
|
+
Object.defineProperty(exports, "DEFAULT_IDEMPOTENCY_SETTINGS", { enumerable: true, get: function () { return idempotency_js_1.DEFAULT_IDEMPOTENCY_SETTINGS; } });
|
|
75
|
+
Object.defineProperty(exports, "HashMismatchCache", { enumerable: true, get: function () { return idempotency_js_1.HashMismatchCache; } });
|
|
76
|
+
Object.defineProperty(exports, "IdempotencyKeyMismatch", { enumerable: true, get: function () { return idempotency_js_1.IdempotencyKeyMismatch; } });
|
|
77
|
+
Object.defineProperty(exports, "DuplicateReadMismatch", { enumerable: true, get: function () { return idempotency_js_1.DuplicateReadMismatch; } });
|
|
78
|
+
Object.defineProperty(exports, "findIdempotencyKey", { enumerable: true, get: function () { return idempotency_js_1.findIdempotencyKey; } });
|
|
79
|
+
Object.defineProperty(exports, "extractIdentity", { enumerable: true, get: function () { return idempotency_js_1.extractIdentity; } });
|
|
80
|
+
Object.defineProperty(exports, "hashResponseBody", { enumerable: true, get: function () { return idempotency_js_1.hashResponseBody; } });
|
|
81
|
+
var idempotencySettingsPoller_js_1 = require("./idempotencySettingsPoller.js");
|
|
82
|
+
Object.defineProperty(exports, "IdempotencySettingsPoller", { enumerable: true, get: function () { return idempotencySettingsPoller_js_1.IdempotencySettingsPoller; } });
|
|
83
|
+
var invariantInference_js_1 = require("./invariantInference.js");
|
|
84
|
+
Object.defineProperty(exports, "InferenceReporter", { enumerable: true, get: function () { return invariantInference_js_1.InferenceReporter; } });
|
|
85
|
+
var distributionDrift_js_1 = require("./distributionDrift.js");
|
|
86
|
+
Object.defineProperty(exports, "ValueDistributionDrift", { enumerable: true, get: function () { return distributionDrift_js_1.ValueDistributionDrift; } });
|
|
87
|
+
Object.defineProperty(exports, "WriteCountDrift", { enumerable: true, get: function () { return distributionDrift_js_1.WriteCountDrift; } });
|
|
88
|
+
Object.defineProperty(exports, "newWelfordState", { enumerable: true, get: function () { return distributionDrift_js_1.newWelfordState; } });
|
|
89
|
+
Object.defineProperty(exports, "updateWelfordState", { enumerable: true, get: function () { return distributionDrift_js_1.updateWelfordState; } });
|
|
90
|
+
Object.defineProperty(exports, "isDrift", { enumerable: true, get: function () { return distributionDrift_js_1.isDrift; } });
|
|
91
|
+
Object.defineProperty(exports, "extractTopLevelNumericFields", { enumerable: true, get: function () { return distributionDrift_js_1.extractTopLevelNumericFields; } });
|
|
92
|
+
Object.defineProperty(exports, "DistributionDriftReporter", { enumerable: true, get: function () { return distributionDrift_js_1.DistributionDriftReporter; } });
|
|
93
|
+
var distributionBaselinePoller_js_1 = require("./distributionBaselinePoller.js");
|
|
94
|
+
Object.defineProperty(exports, "DistributionBaselinePoller", { enumerable: true, get: function () { return distributionBaselinePoller_js_1.DistributionBaselinePoller; } });
|
|
95
|
+
__exportStar(require("./proto.gen.js"), exports);
|
|
96
|
+
exports.SDK_VERSION = '0.1.0';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Invariant inference, v1 slice: instead of hand-authoring a
|
|
3
|
+
// business-invariant-rule condition tree, passively observe whether a
|
|
4
|
+
// same-named numeric field on a parent row equals the sum of that same
|
|
5
|
+
// field across an already-included to-many relation (Invoice.total vs
|
|
6
|
+
// sum(lineItems.total)) — and report pass/fail counts to the control plane.
|
|
7
|
+
// Once a candidate has been observed enough times at a high enough pass
|
|
8
|
+
// rate, the dashboard offers it as a one-click "confirm" that creates a
|
|
9
|
+
// normal business_invariant_rule — no new evaluation mechanism needed once
|
|
10
|
+
// confirmed, it becomes an ordinary dashboard-authored rule.
|
|
11
|
+
//
|
|
12
|
+
// Deliberately scoped down from the general idea: only the same-field-name
|
|
13
|
+
// shape (not every numeric-field-pair combination, which would be
|
|
14
|
+
// combinatorial and noisy), only `sum` (no `count`), and no dashboard-
|
|
15
|
+
// configurable thresholds yet — see PROGRESS.md for the fuller design this
|
|
16
|
+
// is a first slice of.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.InferenceReporter = void 0;
|
|
19
|
+
const httpFetch_js_1 = require("./httpFetch.js");
|
|
20
|
+
const WARN_INTERVAL_MS = 60_000;
|
|
21
|
+
// Reports observation deltas, not cumulative totals — the control plane
|
|
22
|
+
// owns the running (checked_count, held_count) per candidate; the SDK only
|
|
23
|
+
// ever adds to it. That makes a dropped/failed flush harmless to correctness
|
|
24
|
+
// (the next flush's delta is unaffected by a prior one failing) at the cost
|
|
25
|
+
// of only ever undercounting on a genuine send failure, never overcounting.
|
|
26
|
+
class InferenceReporter {
|
|
27
|
+
counts = new Map();
|
|
28
|
+
timer = null;
|
|
29
|
+
lastWarnAt = 0;
|
|
30
|
+
endpoint;
|
|
31
|
+
apiKey;
|
|
32
|
+
flushIntervalMs;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.endpoint = opts.endpoint;
|
|
35
|
+
this.apiKey = opts.apiKey;
|
|
36
|
+
this.flushIntervalMs = opts.flushIntervalMs ?? 30_000;
|
|
37
|
+
}
|
|
38
|
+
start() {
|
|
39
|
+
this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
|
|
40
|
+
if (this.timer.unref)
|
|
41
|
+
this.timer.unref(); // don't block process exit
|
|
42
|
+
}
|
|
43
|
+
stop() {
|
|
44
|
+
if (this.timer) {
|
|
45
|
+
clearInterval(this.timer);
|
|
46
|
+
this.timer = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
record(model, field, relation, held) {
|
|
50
|
+
const key = `${model}\x00${field}\x00${relation}`;
|
|
51
|
+
let c = this.counts.get(key);
|
|
52
|
+
if (!c) {
|
|
53
|
+
c = { model, field, relation, checked: 0, held: 0 };
|
|
54
|
+
this.counts.set(key, c);
|
|
55
|
+
}
|
|
56
|
+
c.checked++;
|
|
57
|
+
if (held)
|
|
58
|
+
c.held++;
|
|
59
|
+
}
|
|
60
|
+
async flush() {
|
|
61
|
+
if (this.counts.size === 0)
|
|
62
|
+
return;
|
|
63
|
+
const observations = [...this.counts.values()];
|
|
64
|
+
this.counts.clear();
|
|
65
|
+
try {
|
|
66
|
+
const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'X-Reprova-Key': this.apiKey,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ observations }),
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
this.warnOnce(`reprova: inferred-invariant observation report returned ${res.status}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
this.warnOnce(`reprova: inferred-invariant observation report error: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
warnOnce(msg) {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
|
|
85
|
+
this.lastWarnAt = now;
|
|
86
|
+
console.warn(msg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.InferenceReporter = InferenceReporter;
|