@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.
Files changed (147) hide show
  1. package/README.md +170 -8
  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 +96 -0
  11. package/dist/cjs/invariantInference.js +90 -0
  12. package/dist/cjs/invariantRules.js +142 -0
  13. package/dist/cjs/knex.js +86 -0
  14. package/dist/cjs/kysely.js +175 -0
  15. package/dist/cjs/legacyPrisma.js +130 -0
  16. package/dist/cjs/mikroorm.js +74 -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 +108 -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 +200 -0
  34. package/dist/context.d.ts +32 -0
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/context.js +18 -0
  37. package/dist/context.js.map +1 -1
  38. package/dist/distributionBaselinePoller.d.ts +36 -0
  39. package/dist/distributionBaselinePoller.d.ts.map +1 -0
  40. package/dist/distributionBaselinePoller.js +84 -0
  41. package/dist/distributionBaselinePoller.js.map +1 -0
  42. package/dist/distributionDrift.d.ts +41 -0
  43. package/dist/distributionDrift.d.ts.map +1 -0
  44. package/dist/distributionDrift.js +161 -0
  45. package/dist/distributionDrift.js.map +1 -0
  46. package/dist/drizzle.d.ts +22 -0
  47. package/dist/drizzle.d.ts.map +1 -1
  48. package/dist/drizzle.js +129 -1
  49. package/dist/drizzle.js.map +1 -1
  50. package/dist/frameworkAdapter.d.ts +5 -0
  51. package/dist/frameworkAdapter.d.ts.map +1 -0
  52. package/dist/frameworkAdapter.js +2 -0
  53. package/dist/frameworkAdapter.js.map +1 -0
  54. package/dist/httpFetch.d.ts +13 -0
  55. package/dist/httpFetch.d.ts.map +1 -0
  56. package/dist/httpFetch.js +54 -0
  57. package/dist/httpFetch.js.map +1 -0
  58. package/dist/idempotency.d.ts +26 -0
  59. package/dist/idempotency.d.ts.map +1 -0
  60. package/dist/idempotency.js +124 -0
  61. package/dist/idempotency.js.map +1 -0
  62. package/dist/idempotencySettingsPoller.d.ts +23 -0
  63. package/dist/idempotencySettingsPoller.d.ts.map +1 -0
  64. package/dist/idempotencySettingsPoller.js +68 -0
  65. package/dist/idempotencySettingsPoller.js.map +1 -0
  66. package/dist/index.d.ts +33 -6
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +19 -4
  69. package/dist/index.js.map +1 -1
  70. package/dist/invariantInference.d.ts +28 -0
  71. package/dist/invariantInference.d.ts.map +1 -0
  72. package/dist/invariantInference.js +87 -0
  73. package/dist/invariantInference.js.map +1 -0
  74. package/dist/invariantRules.d.ts +16 -0
  75. package/dist/invariantRules.d.ts.map +1 -0
  76. package/dist/invariantRules.js +104 -0
  77. package/dist/invariantRules.js.map +1 -0
  78. package/dist/knex.d.ts.map +1 -1
  79. package/dist/knex.js +1 -0
  80. package/dist/knex.js.map +1 -1
  81. package/dist/kysely.d.ts.map +1 -1
  82. package/dist/kysely.js +2 -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 +2 -0
  89. package/dist/mikroorm.d.ts.map +1 -0
  90. package/dist/mikroorm.js +72 -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 +31 -0
  105. package/dist/nest.d.ts.map +1 -0
  106. package/dist/nest.js +133 -0
  107. package/dist/nest.js.map +1 -0
  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 +19 -1
  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 +53 -6
  131. package/dist/sdk.d.ts.map +1 -1
  132. package/dist/sdk.js +663 -34
  133. package/dist/sdk.js.map +1 -1
  134. package/dist/sequelize.d.ts.map +1 -1
  135. package/dist/sequelize.js +4 -3
  136. package/dist/sequelize.js.map +1 -1
  137. package/dist/stack.d.ts.map +1 -1
  138. package/dist/stack.js +6 -3
  139. package/dist/stack.js.map +1 -1
  140. package/dist/transport.d.ts.map +1 -1
  141. package/dist/transport.js +2 -1
  142. package/dist/transport.js.map +1 -1
  143. package/dist/typeorm.d.ts +1 -0
  144. package/dist/typeorm.d.ts.map +1 -1
  145. package/dist/typeorm.js +133 -1
  146. package/dist/typeorm.js.map +1 -1
  147. 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;
@@ -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
+ }