@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,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Dashboard-authored business-invariant rules: CEL (Common Expression
|
|
3
|
+
// Language) expressions evaluated in-process against real Prisma query
|
|
4
|
+
// results (not just the footprint's PKs/counts — actual field values), so
|
|
5
|
+
// bugs that never throw and never return a 5xx (an invoice total that
|
|
6
|
+
// silently drifted from its line items, negative inventory, an order marked
|
|
7
|
+
// shipped with no shipment row) can still produce a reproduction.
|
|
8
|
+
//
|
|
9
|
+
// Replaces an earlier JSON condition-tree evaluator (see git history) — CEL
|
|
10
|
+
// gives the same same-row-comparison-and-relation-aggregate expressiveness
|
|
11
|
+
// with a much friendlier authoring surface (`total ==
|
|
12
|
+
// lineItems.map(x, x.total).sum()` instead of a nested op/field/right tree),
|
|
13
|
+
// using the actively-maintained, zero-dependency @marcbachmann/cel-js.
|
|
14
|
+
//
|
|
15
|
+
// `expr` is the INVARIANT itself — what should always be true — not the
|
|
16
|
+
// violation condition: a rule fires when `expr` evaluates to exactly
|
|
17
|
+
// `false`. This is the opposite polarity from the old JSON tree (which
|
|
18
|
+
// encoded the violation, e.g. `not: eq(...)`), but reads naturally in CEL
|
|
19
|
+
// and matches how an admin would actually describe the rule out loud.
|
|
20
|
+
//
|
|
21
|
+
// A field or relation the app's own query didn't `include` simply isn't a
|
|
22
|
+
// key on the row object, which CEL reports as a runtime "no such attribute"
|
|
23
|
+
// error rather than `false` — evaluateInvariant treats any evaluation error,
|
|
24
|
+
// and any non-boolean result (an author mistake, e.g. forgetting a
|
|
25
|
+
// comparison operator), as "can't evaluate" and skips, never firing a false
|
|
26
|
+
// violation on data the query simply didn't load. Same principle
|
|
27
|
+
// `invariantInference.ts`/`distributionDrift.ts` use: never guess.
|
|
28
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
31
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
32
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(o, k2, desc);
|
|
35
|
+
}) : (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
o[k2] = m[k];
|
|
38
|
+
}));
|
|
39
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
40
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
41
|
+
}) : function(o, v) {
|
|
42
|
+
o["default"] = v;
|
|
43
|
+
});
|
|
44
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
45
|
+
var ownKeys = function(o) {
|
|
46
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
47
|
+
var ar = [];
|
|
48
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
49
|
+
return ar;
|
|
50
|
+
};
|
|
51
|
+
return ownKeys(o);
|
|
52
|
+
};
|
|
53
|
+
return function (mod) {
|
|
54
|
+
if (mod && mod.__esModule) return mod;
|
|
55
|
+
var result = {};
|
|
56
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
57
|
+
__setModuleDefault(result, mod);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.BusinessInvariantViolation = void 0;
|
|
63
|
+
exports.operationMatchesHook = operationMatchesHook;
|
|
64
|
+
exports.compileInvariant = compileInvariant;
|
|
65
|
+
exports.evaluateInvariant = evaluateInvariant;
|
|
66
|
+
let envPromise = null;
|
|
67
|
+
function loadEnv() {
|
|
68
|
+
if (!envPromise) {
|
|
69
|
+
envPromise = Promise.resolve().then(() => __importStar(require('@marcbachmann/cel-js'))).then(({ Environment }) => new Environment({ unlistedVariablesAreDyn: true }).registerFunction({
|
|
70
|
+
name: 'sum',
|
|
71
|
+
receiverType: 'list',
|
|
72
|
+
returnType: 'double',
|
|
73
|
+
handler: (list) => list.reduce((a, b) => a + b, 0),
|
|
74
|
+
params: [],
|
|
75
|
+
}))
|
|
76
|
+
.catch(() => null); // can't load the CEL engine at all — every rule silently never fires, never crashes the host app
|
|
77
|
+
}
|
|
78
|
+
return envPromise;
|
|
79
|
+
}
|
|
80
|
+
const READ_OPS = new Set(['findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findMany']);
|
|
81
|
+
const WRITE_OPS = new Set(['create', 'createMany', 'update', 'updateMany', 'upsert', 'delete', 'deleteMany']);
|
|
82
|
+
// Whether a Prisma operation name belongs to the read/write class a rule's
|
|
83
|
+
// when_hook asks to run on. Anything neither (e.g. $queryRaw, aggregate,
|
|
84
|
+
// count, groupBy) never matches — a rule needs real field values on a real
|
|
85
|
+
// row, which those don't uniformly provide.
|
|
86
|
+
function operationMatchesHook(op, whenHook) {
|
|
87
|
+
return whenHook === 'after_read' ? READ_OPS.has(op) : WRITE_OPS.has(op);
|
|
88
|
+
}
|
|
89
|
+
// Compiled programs are cached by (rule id, expr) so an edited rule
|
|
90
|
+
// recompiles (new expr under the same id misses the cache) while an
|
|
91
|
+
// unchanged one is parsed exactly once for the life of the process — CEL
|
|
92
|
+
// parsing is not free, and this runs on every matching query result.
|
|
93
|
+
// Invalid CEL syntax caches a `null` miss rather than re-attempting the
|
|
94
|
+
// same broken parse on every request. A CEL-engine load failure (see
|
|
95
|
+
// loadEnv's own doc comment) is deliberately NOT cached here — that's a
|
|
96
|
+
// process-wide condition already memoized by envPromise itself, not a
|
|
97
|
+
// per-rule outcome, so a later rule with valid syntax isn't punished by an
|
|
98
|
+
// earlier one's transient load timing.
|
|
99
|
+
const compileCache = new Map();
|
|
100
|
+
async function compileInvariant(rule) {
|
|
101
|
+
const key = `${rule.id}\x00${rule.expr}`;
|
|
102
|
+
if (compileCache.has(key))
|
|
103
|
+
return compileCache.get(key);
|
|
104
|
+
const env = await loadEnv();
|
|
105
|
+
if (!env)
|
|
106
|
+
return null;
|
|
107
|
+
let compiled;
|
|
108
|
+
try {
|
|
109
|
+
compiled = env.parse(rule.expr);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
compiled = null; // invalid CEL — never crash the host app on a bad admin-authored rule
|
|
113
|
+
}
|
|
114
|
+
compileCache.set(key, compiled);
|
|
115
|
+
return compiled;
|
|
116
|
+
}
|
|
117
|
+
// Runs a compiled invariant against one row. Returns `false` only for a
|
|
118
|
+
// genuine, cleanly-evaluated violation; `undefined` for anything else
|
|
119
|
+
// (evaluation error from a missing field/relation, or a non-boolean result
|
|
120
|
+
// from a malformed expression) so the caller never mistakes "couldn't tell"
|
|
121
|
+
// for "violated".
|
|
122
|
+
function evaluateInvariant(compiled, row) {
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = compiled(row);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return typeof result === 'boolean' ? result : undefined;
|
|
131
|
+
}
|
|
132
|
+
// One reusable Error class for every rule violation. Per-rule identity comes
|
|
133
|
+
// from capture()'s extra.errorType override (see sdk.ts), not from a
|
|
134
|
+
// per-rule subclass — many rules can share this class without collapsing
|
|
135
|
+
// into one issue, since fingerprinting keys off errorType, not the class
|
|
136
|
+
// name, once errorType is supplied explicitly.
|
|
137
|
+
class BusinessInvariantViolation extends Error {
|
|
138
|
+
constructor(message) {
|
|
139
|
+
super(message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.BusinessInvariantViolation = BusinessInvariantViolation;
|
package/dist/cjs/knex.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installKnexHooks = installKnexHooks;
|
|
4
|
+
const context_js_1 = require("./context.js");
|
|
5
|
+
const OP_BY_METHOD = {
|
|
6
|
+
select: 'select',
|
|
7
|
+
first: 'select',
|
|
8
|
+
pluck: 'select',
|
|
9
|
+
insert: 'insert',
|
|
10
|
+
update: 'update',
|
|
11
|
+
del: 'delete',
|
|
12
|
+
delete: 'delete',
|
|
13
|
+
};
|
|
14
|
+
function isRowArray(v) {
|
|
15
|
+
return Array.isArray(v) && v.length > 0 && typeof v[0] === 'object' && v[0] !== null;
|
|
16
|
+
}
|
|
17
|
+
function distinct(values) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const v of values) {
|
|
21
|
+
if (!seen.has(v)) {
|
|
22
|
+
seen.add(v);
|
|
23
|
+
out.push(v);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function extractRowPks(rows, pkCols) {
|
|
29
|
+
if (pkCols.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const vals = pkCols.map((c) => row[c]);
|
|
34
|
+
if (vals.some((v) => v === undefined || v === null))
|
|
35
|
+
continue;
|
|
36
|
+
out.push(vals.map((v) => String(v)).join(':'));
|
|
37
|
+
}
|
|
38
|
+
return distinct(out);
|
|
39
|
+
}
|
|
40
|
+
// Installs a footprint listener on a Knex instance. Also covers Objection.js
|
|
41
|
+
// models bound to this same instance.
|
|
42
|
+
function installKnexHooks(knex, opts = {}) {
|
|
43
|
+
const primaryKey = opts.primaryKey ?? (() => ['id']);
|
|
44
|
+
try {
|
|
45
|
+
knex.on('query-response', (response, obj, builder) => {
|
|
46
|
+
try {
|
|
47
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
48
|
+
if (!ctx)
|
|
49
|
+
return;
|
|
50
|
+
const method = obj?.method ?? builder?._method;
|
|
51
|
+
const op = method ? OP_BY_METHOD[method] : undefined;
|
|
52
|
+
if (!op)
|
|
53
|
+
return; // schema/DDL/raw — skip
|
|
54
|
+
const table = builder?._single?.table;
|
|
55
|
+
if (typeof table !== 'string' || !table)
|
|
56
|
+
return;
|
|
57
|
+
let pks = [];
|
|
58
|
+
let count = 0;
|
|
59
|
+
if (isRowArray(response)) {
|
|
60
|
+
pks = extractRowPks(response, primaryKey(table));
|
|
61
|
+
count = response.length;
|
|
62
|
+
}
|
|
63
|
+
else if (Array.isArray(response)) {
|
|
64
|
+
// Primitive array — insert returning ids (partial on sqlite/mysql).
|
|
65
|
+
if (op === 'insert')
|
|
66
|
+
pks = distinct(response.filter((v) => v != null).map((v) => String(v)));
|
|
67
|
+
count = response.length;
|
|
68
|
+
}
|
|
69
|
+
else if (typeof response === 'number') {
|
|
70
|
+
count = response; // affected-row count from update/delete
|
|
71
|
+
}
|
|
72
|
+
const entry = {
|
|
73
|
+
model: table,
|
|
74
|
+
op,
|
|
75
|
+
pks,
|
|
76
|
+
count,
|
|
77
|
+
note: count === 0 ? 'returned 0 rows' : undefined,
|
|
78
|
+
resolved: true,
|
|
79
|
+
};
|
|
80
|
+
ctx.footprint.push(entry);
|
|
81
|
+
}
|
|
82
|
+
catch { /* never break the query pipeline */ }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch { /* never throw from install */ }
|
|
86
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createKyselyPlugin = createKyselyPlugin;
|
|
4
|
+
const context_js_1 = require("./context.js");
|
|
5
|
+
const OP_BY_KIND = {
|
|
6
|
+
SelectQueryNode: 'select',
|
|
7
|
+
InsertQueryNode: 'insert',
|
|
8
|
+
UpdateQueryNode: 'update',
|
|
9
|
+
DeleteQueryNode: 'delete',
|
|
10
|
+
MergeQueryNode: 'merge',
|
|
11
|
+
};
|
|
12
|
+
// TableNode → its identifier name; unwraps an AliasNode (`from('t as x')`).
|
|
13
|
+
function tableName(node) {
|
|
14
|
+
const n = node;
|
|
15
|
+
if (!n || typeof n !== 'object')
|
|
16
|
+
return undefined;
|
|
17
|
+
if (n.kind === 'AliasNode')
|
|
18
|
+
return tableName(n.node);
|
|
19
|
+
if (n.kind === 'TableNode') {
|
|
20
|
+
const table = n.table;
|
|
21
|
+
return table?.identifier?.name;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
// The operation's principal table, read from the op-specific field.
|
|
26
|
+
function primaryTable(node) {
|
|
27
|
+
switch (node.kind) {
|
|
28
|
+
case 'SelectQueryNode':
|
|
29
|
+
case 'DeleteQueryNode': {
|
|
30
|
+
const from = node.from;
|
|
31
|
+
return tableName(from?.froms?.[0]);
|
|
32
|
+
}
|
|
33
|
+
case 'InsertQueryNode':
|
|
34
|
+
return tableName(node.into);
|
|
35
|
+
case 'UpdateQueryNode':
|
|
36
|
+
return tableName(node.table);
|
|
37
|
+
default:
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Every table referenced anywhere in the query (joins, subqueries, CTEs, …),
|
|
42
|
+
// in first-seen order. A bounded deep-walk — instrumentation must not blow the
|
|
43
|
+
// stack or hang on a pathological tree.
|
|
44
|
+
function collectTables(root) {
|
|
45
|
+
const found = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const stack = [root];
|
|
48
|
+
let budget = 5000;
|
|
49
|
+
while (stack.length && budget-- > 0) {
|
|
50
|
+
const cur = stack.pop();
|
|
51
|
+
if (!cur || typeof cur !== 'object')
|
|
52
|
+
continue;
|
|
53
|
+
const name = tableName(cur);
|
|
54
|
+
if (name && !seen.has(name)) {
|
|
55
|
+
seen.add(name);
|
|
56
|
+
found.push(name);
|
|
57
|
+
}
|
|
58
|
+
for (const v of Object.values(cur)) {
|
|
59
|
+
if (v && typeof v === 'object')
|
|
60
|
+
stack.push(v);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
// Column NAMES referenced inside a WhereNode subtree — never values.
|
|
66
|
+
function whereColumns(whereNode) {
|
|
67
|
+
const cols = new Set();
|
|
68
|
+
const stack = [whereNode];
|
|
69
|
+
let budget = 5000;
|
|
70
|
+
while (stack.length && budget-- > 0) {
|
|
71
|
+
const cur = stack.pop();
|
|
72
|
+
if (!cur || typeof cur !== 'object')
|
|
73
|
+
continue;
|
|
74
|
+
const n = cur;
|
|
75
|
+
if (n.kind === 'ColumnNode') {
|
|
76
|
+
const col = n.column;
|
|
77
|
+
if (col?.name)
|
|
78
|
+
cols.add(col.name);
|
|
79
|
+
}
|
|
80
|
+
for (const v of Object.values(n)) {
|
|
81
|
+
if (v && typeof v === 'object')
|
|
82
|
+
stack.push(v);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...cols].sort();
|
|
86
|
+
}
|
|
87
|
+
function extractPks(rows, pkCols) {
|
|
88
|
+
if (pkCols.length === 0)
|
|
89
|
+
return [];
|
|
90
|
+
// A set: a join can return the same primary-table row many times, but the
|
|
91
|
+
// footprint records which DISTINCT rows were touched.
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const pks = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const vals = pkCols.map((c) => row[c]);
|
|
96
|
+
if (vals.some((v) => v === undefined))
|
|
97
|
+
continue; // column not selected — can't form a PK
|
|
98
|
+
const pk = vals.map((v) => String(v)).join(':');
|
|
99
|
+
if (!seen.has(pk)) {
|
|
100
|
+
seen.add(pk);
|
|
101
|
+
pks.push(pk);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return pks;
|
|
105
|
+
}
|
|
106
|
+
// Creates the Reprova Kysely footprint plugin.
|
|
107
|
+
// Usage: new Kysely({ dialect, plugins: [createKyselyPlugin()] })
|
|
108
|
+
function createKyselyPlugin(opts = {}) {
|
|
109
|
+
const primaryKey = opts.primaryKey ?? (() => ['id']);
|
|
110
|
+
const captureWhereShape = opts.captureWhereShape !== false;
|
|
111
|
+
// Keyed by the queryId OBJECT shared across both hooks; a compiled-but-never-
|
|
112
|
+
// executed query is GC'd without leaking.
|
|
113
|
+
const pending = new WeakMap();
|
|
114
|
+
return {
|
|
115
|
+
transformQuery(args) {
|
|
116
|
+
try {
|
|
117
|
+
const node = args.node;
|
|
118
|
+
const op = OP_BY_KIND[node.kind];
|
|
119
|
+
if (op) {
|
|
120
|
+
const whereShape = captureWhereShape && node.where
|
|
121
|
+
? whereColumns(node.where).join(',') || undefined
|
|
122
|
+
: undefined;
|
|
123
|
+
pending.set(args.queryId, {
|
|
124
|
+
op,
|
|
125
|
+
primary: primaryTable(node),
|
|
126
|
+
tables: collectTables(node),
|
|
127
|
+
whereShape,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// A footprint failure must never alter the query — fall through.
|
|
133
|
+
}
|
|
134
|
+
return args.node;
|
|
135
|
+
},
|
|
136
|
+
async transformResult(args) {
|
|
137
|
+
try {
|
|
138
|
+
const meta = pending.get(args.queryId);
|
|
139
|
+
if (meta) {
|
|
140
|
+
pending.delete(args.queryId);
|
|
141
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
142
|
+
if (ctx) {
|
|
143
|
+
const result = args.result;
|
|
144
|
+
const rows = result.rows ?? [];
|
|
145
|
+
const affected = result.numAffectedRows != null ? Number(result.numAffectedRows) : undefined;
|
|
146
|
+
const count = rows.length || affected || 0;
|
|
147
|
+
const primary = meta.primary ?? '$unknown';
|
|
148
|
+
const entry = {
|
|
149
|
+
model: primary,
|
|
150
|
+
op: meta.op,
|
|
151
|
+
pks: extractPks(rows, primaryKey(primary)),
|
|
152
|
+
where_shape: meta.whereShape,
|
|
153
|
+
count,
|
|
154
|
+
note: count === 0 ? 'returned 0 rows' : undefined,
|
|
155
|
+
resolved: true,
|
|
156
|
+
};
|
|
157
|
+
ctx.footprint.push(entry);
|
|
158
|
+
// Joined / subquery tables: recorded so the extractor knows they
|
|
159
|
+
// were touched, but without PKs (result columns can't be reliably
|
|
160
|
+
// attributed across a join).
|
|
161
|
+
for (const t of meta.tables) {
|
|
162
|
+
if (t === primary)
|
|
163
|
+
continue;
|
|
164
|
+
ctx.footprint.push({ model: t, op: meta.op, pks: [], count: 0, note: 'referenced (join/subquery)', resolved: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Never break the result pipeline over a footprint error.
|
|
171
|
+
}
|
|
172
|
+
return args.result;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentLegacyPrismaClient = instrumentLegacyPrismaClient;
|
|
4
|
+
const context_js_1 = require("./context.js");
|
|
5
|
+
const prisma_js_1 = require("./prisma.js");
|
|
6
|
+
// Older Prisma clients have no query-lifecycle hook at all — confirmed live
|
|
7
|
+
// against a real @prisma/client@2.0.0-beta.4 (2020): $extends (introduced
|
|
8
|
+
// 4.16, 2023), $use (Prisma's earlier middleware API, introduced ~2.something
|
|
9
|
+
// and itself later deprecated in favor of $extends), and even $queryRaw are
|
|
10
|
+
// all `undefined` on that client. createPrismaExtension's $allOperations has
|
|
11
|
+
// nothing to attach to on a client like this.
|
|
12
|
+
//
|
|
13
|
+
// What IS there, on every Prisma client generator back to the very first
|
|
14
|
+
// one: a plain per-model delegate object (client.article, client.user, ...)
|
|
15
|
+
// whose CRUD methods (findMany, create, update, ...) are ordinary
|
|
16
|
+
// own-property functions — directly reassignable, the same monkey-patch
|
|
17
|
+
// shape this SDK already uses for TypeORM's QueryRunner.prototype.query and
|
|
18
|
+
// Mongoose's already-compiled models (instrumentAlreadyCompiledModels). This
|
|
19
|
+
// wraps each one directly instead of relying on any official extension
|
|
20
|
+
// point, so footprint capture and every detector built on recordPrismaOperation
|
|
21
|
+
// work identically regardless of how old the installed client is.
|
|
22
|
+
//
|
|
23
|
+
// Also captures DB-rejected writes the same way typeorm.ts does for a
|
|
24
|
+
// TypeORM app: create/update/upsert/delete/updateMany/deleteMany record an
|
|
25
|
+
// `<op>_failed` footprint entry on rejection — column NAMES only (from the
|
|
26
|
+
// call's own `data`/`where` argument keys), never the bound values, per the
|
|
27
|
+
// metadata-only rule. A read failing has no "what would have been written"
|
|
28
|
+
// story, so only write ops get this treatment, matching typeorm.ts's scope.
|
|
29
|
+
const READ_OPS = new Set(['findOne', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findMany', 'count', 'aggregate', 'groupBy']);
|
|
30
|
+
const WRITE_OPS = new Set(['create', 'createMany', 'update', 'updateMany', 'upsert', 'delete', 'deleteMany']);
|
|
31
|
+
// `findOne` is Prisma's own deprecated alias for `findUnique` (renamed
|
|
32
|
+
// well before $extends existed, still present on clients this old) —
|
|
33
|
+
// normalized before recording so the footprint's `op` field and
|
|
34
|
+
// operationMatchesHook's after_read gating (invariantRules.ts's READ_OPS,
|
|
35
|
+
// which only lists modern names) both see the same vocabulary regardless
|
|
36
|
+
// of which Prisma version actually captured it.
|
|
37
|
+
function normalizeOperation(method) {
|
|
38
|
+
return method === 'findOne' ? 'findUnique' : method;
|
|
39
|
+
}
|
|
40
|
+
// Reassigns each detected model delegate's own CRUD methods exactly once —
|
|
41
|
+
// re-instrumenting an already-patched delegate (e.g. instrumentPrisma called
|
|
42
|
+
// twice) would double-record every footprint entry.
|
|
43
|
+
const patchedDelegates = new WeakSet();
|
|
44
|
+
function instrumentLegacyPrismaClient(client, opts = {}) {
|
|
45
|
+
try {
|
|
46
|
+
const dmmf = opts.dmmf ?? client.dmmf;
|
|
47
|
+
const hooks = {
|
|
48
|
+
dmmf,
|
|
49
|
+
getRules: opts.getRules,
|
|
50
|
+
inferenceReporter: opts.inferenceReporter,
|
|
51
|
+
writeCountDriftReporter: opts.writeCountDriftReporter,
|
|
52
|
+
writeCountBaselinePoller: opts.writeCountBaselinePoller,
|
|
53
|
+
};
|
|
54
|
+
const modelNameFor = buildModelNameResolver(dmmf);
|
|
55
|
+
for (const [key, delegate] of Object.entries(client)) {
|
|
56
|
+
if (!isModelDelegate(delegate))
|
|
57
|
+
continue;
|
|
58
|
+
if (patchedDelegates.has(delegate))
|
|
59
|
+
continue;
|
|
60
|
+
patchedDelegates.add(delegate);
|
|
61
|
+
const modelName = modelNameFor(key);
|
|
62
|
+
for (const method of Object.keys(delegate)) {
|
|
63
|
+
if (READ_OPS.has(method)) {
|
|
64
|
+
patchReadMethod(delegate, method, modelName, hooks);
|
|
65
|
+
}
|
|
66
|
+
else if (WRITE_OPS.has(method)) {
|
|
67
|
+
patchWriteMethod(delegate, method, modelName, hooks);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* never throw from install */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isModelDelegate(value) {
|
|
77
|
+
if (!value || typeof value !== 'object')
|
|
78
|
+
return false;
|
|
79
|
+
const d = value;
|
|
80
|
+
return typeof d.findMany === 'function' || typeof d.create === 'function';
|
|
81
|
+
}
|
|
82
|
+
// Prisma's own convention: a model named `Article` gets a delegate property
|
|
83
|
+
// named `article` (first letter lowercased) — true on every version this
|
|
84
|
+
// adapter targets. Falls back to that convention when dmmf isn't available
|
|
85
|
+
// at all; uses the real dmmf model list when it is, so an unconventional
|
|
86
|
+
// name still resolves correctly.
|
|
87
|
+
function buildModelNameResolver(dmmf) {
|
|
88
|
+
if (!dmmf) {
|
|
89
|
+
return (key) => key.charAt(0).toUpperCase() + key.slice(1);
|
|
90
|
+
}
|
|
91
|
+
const byLowerFirst = new Map();
|
|
92
|
+
for (const model of dmmf.datamodel.models) {
|
|
93
|
+
byLowerFirst.set(model.name.charAt(0).toLowerCase() + model.name.slice(1), model.name);
|
|
94
|
+
}
|
|
95
|
+
return (key) => byLowerFirst.get(key) ?? key.charAt(0).toUpperCase() + key.slice(1);
|
|
96
|
+
}
|
|
97
|
+
function patchReadMethod(delegate, method, modelName, hooks) {
|
|
98
|
+
const original = delegate[method];
|
|
99
|
+
const operation = normalizeOperation(method);
|
|
100
|
+
delegate[method] = async function (...args) {
|
|
101
|
+
const result = await original.apply(this, args);
|
|
102
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
103
|
+
if (ctx) {
|
|
104
|
+
const callArgs = args[0] ?? {};
|
|
105
|
+
await (0, prisma_js_1.recordPrismaOperation)(ctx, modelName, operation, callArgs, result, hooks);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function patchWriteMethod(delegate, method, modelName, hooks) {
|
|
111
|
+
const original = delegate[method];
|
|
112
|
+
delegate[method] = async function (...args) {
|
|
113
|
+
const callArgs = args[0] ?? {};
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = await original.apply(this, args);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
120
|
+
if (ctx)
|
|
121
|
+
(0, prisma_js_1.recordFailedWrite)(ctx, modelName, method, callArgs);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
125
|
+
if (ctx) {
|
|
126
|
+
await (0, prisma_js_1.recordPrismaOperation)(ctx, modelName, method, callArgs, result, hooks);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installMikroOrmSubscriber = installMikroOrmSubscriber;
|
|
4
|
+
const context_js_1 = require("./context.js");
|
|
5
|
+
function pkOf(entity, meta) {
|
|
6
|
+
const keys = meta?.primaryKeys ?? [];
|
|
7
|
+
if (!entity || keys.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
const vals = keys.map((k) => entity[k]);
|
|
10
|
+
if (vals.some((v) => v === undefined || v === null))
|
|
11
|
+
return null;
|
|
12
|
+
return vals.map((v) => String(v)).join(':');
|
|
13
|
+
}
|
|
14
|
+
// Find-or-create the (table, op) entry for this request and fold in the PK.
|
|
15
|
+
function recordPk(table, op, pk) {
|
|
16
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
17
|
+
if (!ctx || !table)
|
|
18
|
+
return;
|
|
19
|
+
let entry = ctx.footprint.find((f) => f.model === table && f.op === op);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
entry = { model: table, op, pks: [], count: 0 };
|
|
22
|
+
ctx.footprint.push(entry);
|
|
23
|
+
}
|
|
24
|
+
if (pk !== null && !entry.pks.includes(pk))
|
|
25
|
+
entry.pks.push(pk);
|
|
26
|
+
entry.count = entry.pks.length;
|
|
27
|
+
}
|
|
28
|
+
// Accepts either a MikroORM instance (orm.em.getEventManager()) or an
|
|
29
|
+
// EntityManager directly (em.getEventManager()) — Reprova has no way to
|
|
30
|
+
// know which one a given host app kept a reference to.
|
|
31
|
+
function getEventManager(orm) {
|
|
32
|
+
const o = orm;
|
|
33
|
+
if (typeof o.getEventManager === 'function')
|
|
34
|
+
return o.getEventManager();
|
|
35
|
+
if (o.em && typeof o.em.getEventManager === 'function')
|
|
36
|
+
return o.em.getEventManager();
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// Installs a footprint subscriber on an initialized MikroORM instance (or
|
|
40
|
+
// EntityManager). Call AFTER MikroORM.init() — the EventManager only
|
|
41
|
+
// exists once the ORM is initialized. Registers a plain object, so the
|
|
42
|
+
// host needs no @Subscriber()-decorated class.
|
|
43
|
+
function installMikroOrmSubscriber(orm) {
|
|
44
|
+
const subscriber = {
|
|
45
|
+
onLoad(args) {
|
|
46
|
+
try {
|
|
47
|
+
recordPk(args?.meta?.className, 'select', pkOf(args?.entity, args?.meta));
|
|
48
|
+
}
|
|
49
|
+
catch { /* never break */ }
|
|
50
|
+
},
|
|
51
|
+
afterCreate(args) {
|
|
52
|
+
try {
|
|
53
|
+
recordPk(args?.meta?.className, 'insert', pkOf(args?.entity, args?.meta));
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore */ }
|
|
56
|
+
},
|
|
57
|
+
afterUpdate(args) {
|
|
58
|
+
try {
|
|
59
|
+
recordPk(args?.meta?.className, 'update', pkOf(args?.entity, args?.meta));
|
|
60
|
+
}
|
|
61
|
+
catch { /* ignore */ }
|
|
62
|
+
},
|
|
63
|
+
afterDelete(args) {
|
|
64
|
+
try {
|
|
65
|
+
recordPk(args?.meta?.className, 'delete', pkOf(args?.entity, args?.meta));
|
|
66
|
+
}
|
|
67
|
+
catch { /* ignore */ }
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
getEventManager(orm)?.registerSubscriber(subscriber);
|
|
72
|
+
}
|
|
73
|
+
catch { /* never throw from install */ }
|
|
74
|
+
}
|