@reprova/sdk 0.5.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 +75 -0
- 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 +18 -0
- package/dist/context.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/drizzle.js +10 -4
- 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 +30 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -3
- 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 +13 -0
- package/dist/nest.d.ts.map +1 -1
- package/dist/nest.js +39 -0
- package/dist/nest.js.map +1 -1
- 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 +10 -2
- 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 +48 -7
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +619 -47
- 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 +14 -5
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoSchemaReporter = void 0;
|
|
4
|
+
const mongoose_js_1 = require("./mongoose.js");
|
|
5
|
+
const httpFetch_js_1 = require("./httpFetch.js");
|
|
6
|
+
// Periodically re-scans a live Mongoose instance's model registry and
|
|
7
|
+
// reports the discovered model->collection map plus every ref: edge found,
|
|
8
|
+
// so the control plane can hand the agent a reference-edge map at
|
|
9
|
+
// plan-build time (MongoDB has no FK catalog for the agent to introspect
|
|
10
|
+
// itself — see mongo_schema.go's handler doc comment). Unlike
|
|
11
|
+
// DistributionDriftReporter (accumulates external .record() deltas since
|
|
12
|
+
// its last flush), this reporter is self-driven: it re-derives the current
|
|
13
|
+
// schema shape itself on each tick and only POSTs when that shape actually
|
|
14
|
+
// changed, since schema shape is near-static and re-sending an unchanged
|
|
15
|
+
// report every tick for the life of a long-running process would be pure
|
|
16
|
+
// waste.
|
|
17
|
+
const WARN_INTERVAL_MS = 60_000;
|
|
18
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
19
|
+
class MongoSchemaReporter {
|
|
20
|
+
timer = null;
|
|
21
|
+
lastSentJSON = null;
|
|
22
|
+
lastWarnAt = 0;
|
|
23
|
+
endpoint;
|
|
24
|
+
apiKey;
|
|
25
|
+
intervalMs;
|
|
26
|
+
mongooseInstance;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.endpoint = opts.endpoint;
|
|
29
|
+
this.apiKey = opts.apiKey;
|
|
30
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31
|
+
this.mongooseInstance = opts.mongooseInstance;
|
|
32
|
+
}
|
|
33
|
+
start() {
|
|
34
|
+
void this.tick(); // report the current shape immediately, don't wait a full interval for the first report
|
|
35
|
+
this.timer = setInterval(() => void this.tick(), this.intervalMs);
|
|
36
|
+
if (this.timer.unref)
|
|
37
|
+
this.timer.unref();
|
|
38
|
+
}
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.timer) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Exposed for tests and for an explicit "report now" call after a
|
|
46
|
+
// dynamic model registration; start()/the timer call this on their own.
|
|
47
|
+
async tick() {
|
|
48
|
+
let discovered;
|
|
49
|
+
try {
|
|
50
|
+
discovered = (0, mongoose_js_1.discoverMongooseSchema)(this.mongooseInstance);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // never let introspection failure break the host app
|
|
54
|
+
}
|
|
55
|
+
if (discovered.models.length === 0 && discovered.edges.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const json = JSON.stringify(discovered);
|
|
58
|
+
if (json === this.lastSentJSON)
|
|
59
|
+
return; // schema shape unchanged since last report
|
|
60
|
+
try {
|
|
61
|
+
const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json', 'X-Reprova-Key': this.apiKey },
|
|
64
|
+
body: JSON.stringify({ models: discovered.models, edges: discovered.edges }),
|
|
65
|
+
});
|
|
66
|
+
if (res.ok) {
|
|
67
|
+
this.lastSentJSON = json;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.warnOnce(`reprova: mongo schema report returned ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.warnOnce(`reprova: mongo schema report error: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
warnOnce(msg) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
|
|
80
|
+
this.lastWarnAt = now;
|
|
81
|
+
console.warn(msg);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.MongoSchemaReporter = MongoSchemaReporter;
|