@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,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reprovaMongoosePlugin = reprovaMongoosePlugin;
|
|
4
|
+
exports.instrumentAlreadyCompiledModels = instrumentAlreadyCompiledModels;
|
|
5
|
+
exports.discoverMongooseSchema = discoverMongooseSchema;
|
|
6
|
+
const context_js_1 = require("./context.js");
|
|
7
|
+
const shape_js_1 = require("./shape.js");
|
|
8
|
+
const SELECT_QUERY_OPS = ['find', 'findOne', 'count', 'countDocuments', 'distinct', 'estimatedDocumentCount'];
|
|
9
|
+
const UPDATE_QUERY_OPS = ['updateOne', 'updateMany', 'findOneAndUpdate', 'findOneAndReplace'];
|
|
10
|
+
const DELETE_QUERY_OPS = ['deleteOne', 'deleteMany', 'findOneAndDelete', 'findOneAndRemove', 'remove'];
|
|
11
|
+
// _id(s) from a query/document result, whatever shape it happens to be
|
|
12
|
+
// (array of docs, single doc, null, or a driver-level count/ack object with
|
|
13
|
+
// no docs at all — idsOf simply yields nothing for the last case).
|
|
14
|
+
function idsOf(value) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return [];
|
|
17
|
+
const docs = Array.isArray(value) ? value : [value];
|
|
18
|
+
const ids = [];
|
|
19
|
+
for (const d of docs) {
|
|
20
|
+
const id = d?._id;
|
|
21
|
+
if (id !== undefined && id !== null)
|
|
22
|
+
ids.push(String(id));
|
|
23
|
+
}
|
|
24
|
+
return ids;
|
|
25
|
+
}
|
|
26
|
+
// updateMany/deleteMany/findOneAndUpdate-family results carry no document
|
|
27
|
+
// ids at all (just {acknowledged, modifiedCount|deletedCount|matchedCount}
|
|
28
|
+
// or a plain number for count()) — this is the bulk-op analog of
|
|
29
|
+
// FootprintQuery.where_shape existing for "no concrete rows, but here's how
|
|
30
|
+
// many and what shape of filter."
|
|
31
|
+
function affectedCountOf(result) {
|
|
32
|
+
if (typeof result === 'number')
|
|
33
|
+
return result;
|
|
34
|
+
const r = result;
|
|
35
|
+
return r?.modifiedCount ?? r?.deletedCount ?? r?.matchedCount ?? 0;
|
|
36
|
+
}
|
|
37
|
+
// The MongoDB driver connection pool that Mongoose 5.x bundles dispatches a
|
|
38
|
+
// query's response from inside the pooled connection's own socket callback
|
|
39
|
+
// — whose AsyncLocalStorage context is fixed to whatever was ambient when
|
|
40
|
+
// that connection was first used, not the context of whichever query is
|
|
41
|
+
// currently awaiting a result on it. Confirmed empirically: a Mongoose
|
|
42
|
+
// post-hook loses ALS context as soon as a single prior query has run
|
|
43
|
+
// outside a captured request, which every real app does (background jobs,
|
|
44
|
+
// startup queries, connection warmup) — so relying on contextStorage in the
|
|
45
|
+
// post-hook alone silently drops footprints in production, not just in
|
|
46
|
+
// contrived tests. Pre-hooks, by contrast, run synchronously on the
|
|
47
|
+
// caller's own stack before any driver I/O, so they still see the correct
|
|
48
|
+
// context. Capture it there and thread it through to the post-hook via a
|
|
49
|
+
// WeakMap keyed on the query/document instance — Kareem (Mongoose's
|
|
50
|
+
// middleware runner) preserves that same instance as `this` across pre and
|
|
51
|
+
// post for one operation, so the lookup is exact, not a guess.
|
|
52
|
+
const ctxByHost = new WeakMap();
|
|
53
|
+
function captureCtx(host) {
|
|
54
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
55
|
+
if (ctx && host && typeof host === 'object')
|
|
56
|
+
ctxByHost.set(host, ctx);
|
|
57
|
+
}
|
|
58
|
+
function resolveCtx(host) {
|
|
59
|
+
return context_js_1.contextStorage.getStore() ?? (host && typeof host === 'object' ? ctxByHost.get(host) : undefined);
|
|
60
|
+
}
|
|
61
|
+
// Find-or-create the (collection, op) entry for this request and fold in
|
|
62
|
+
// whatever this call learned — same merge-by-(table,op) pattern mikroorm.ts
|
|
63
|
+
// uses, extended with a where_shape/count fallback for bulk ops that never
|
|
64
|
+
// expose which specific documents they touched.
|
|
65
|
+
function recordFootprint(ctx, collection, op, pks, affectedCount, whereShape) {
|
|
66
|
+
if (!ctx || !collection)
|
|
67
|
+
return;
|
|
68
|
+
let entry = ctx.footprint.find((f) => f.model === collection && f.op === op);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
entry = { model: collection, op, pks: [], count: 0 };
|
|
71
|
+
ctx.footprint.push(entry);
|
|
72
|
+
}
|
|
73
|
+
for (const pk of pks)
|
|
74
|
+
if (!entry.pks.includes(pk))
|
|
75
|
+
entry.pks.push(pk);
|
|
76
|
+
entry.count = entry.pks.length > 0 ? entry.pks.length : Math.max(entry.count, affectedCount);
|
|
77
|
+
if (whereShape && !entry.where_shape)
|
|
78
|
+
entry.where_shape = whereShape;
|
|
79
|
+
}
|
|
80
|
+
function recordFromQuery(query, op, result) {
|
|
81
|
+
const ctx = resolveCtx(query);
|
|
82
|
+
const collection = query.model?.collection?.collectionName;
|
|
83
|
+
let whereShape;
|
|
84
|
+
try {
|
|
85
|
+
whereShape = query.getFilter ? (0, shape_js_1.shapeArgs)(query.getFilter()) : undefined;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* filter introspection best-effort */
|
|
89
|
+
}
|
|
90
|
+
recordFootprint(ctx, collection, op, idsOf(result), affectedCountOf(result), whereShape);
|
|
91
|
+
}
|
|
92
|
+
// Whether a document was new AT THE TIME save() was called — snapshotted in
|
|
93
|
+
// pre('save') because by the time post('save') runs, Mongoose has already
|
|
94
|
+
// reset doc.isNew to false regardless of which case this was, so isNew
|
|
95
|
+
// alone can't distinguish insert from update from inside the post hook.
|
|
96
|
+
const wasNewByDoc = new WeakMap();
|
|
97
|
+
// Installs Reprova's footprint hooks on a schema. Call BEFORE compiling any
|
|
98
|
+
// model from it — e.g. `mongoose.plugin(reprovaMongoosePlugin)` once,
|
|
99
|
+
// before any `mongoose.model(...)` calls, so every subsequently-compiled
|
|
100
|
+
// model gets it. Registers plain functions, no decorators/classes needed.
|
|
101
|
+
function reprovaMongoosePlugin(schema) {
|
|
102
|
+
for (const opName of SELECT_QUERY_OPS) {
|
|
103
|
+
schema.pre(opName, function () {
|
|
104
|
+
captureCtx(this);
|
|
105
|
+
});
|
|
106
|
+
schema.post(opName, function (result) {
|
|
107
|
+
try {
|
|
108
|
+
recordFromQuery(this, 'select', result);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* never break the host query */
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const opName of UPDATE_QUERY_OPS) {
|
|
116
|
+
schema.pre(opName, function () {
|
|
117
|
+
captureCtx(this);
|
|
118
|
+
});
|
|
119
|
+
schema.post(opName, function (result) {
|
|
120
|
+
try {
|
|
121
|
+
recordFromQuery(this, 'update', result);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* never break the host query */
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
for (const opName of DELETE_QUERY_OPS) {
|
|
129
|
+
schema.pre(opName, function () {
|
|
130
|
+
captureCtx(this);
|
|
131
|
+
});
|
|
132
|
+
schema.post(opName, function (result) {
|
|
133
|
+
try {
|
|
134
|
+
recordFromQuery(this, 'delete', result);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* never break the host query */
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
schema.pre('save', function () {
|
|
142
|
+
try {
|
|
143
|
+
wasNewByDoc.set(this, !!this.isNew);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* never break the host save */
|
|
147
|
+
}
|
|
148
|
+
captureCtx(this);
|
|
149
|
+
});
|
|
150
|
+
schema.post('save', function () {
|
|
151
|
+
try {
|
|
152
|
+
const wasNew = wasNewByDoc.get(this) ?? !!this.isNew;
|
|
153
|
+
wasNewByDoc.delete(this);
|
|
154
|
+
recordFootprint(resolveCtx(this), this.collection?.collectionName, wasNew ? 'insert' : 'update', idsOf(this), 1);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* never break the host save */
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// insertMany's `this` is the shared Model, not a per-call instance, so it
|
|
161
|
+
// can't be used as the WeakMap key (concurrent calls would clobber each
|
|
162
|
+
// other's captured context) — the `docs` array IS per-call and Kareem
|
|
163
|
+
// passes the same reference to both hooks, so key on that instead.
|
|
164
|
+
schema.pre('insertMany', function (docs) {
|
|
165
|
+
captureCtx(docs);
|
|
166
|
+
});
|
|
167
|
+
schema.post('insertMany', function (docs) {
|
|
168
|
+
try {
|
|
169
|
+
const ids = idsOf(docs);
|
|
170
|
+
recordFootprint(resolveCtx(docs), this.collection?.collectionName, 'insert', ids, ids.length);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* never break the host insertMany */
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// --- Retroactive instrumentation for already-compiled models -------------
|
|
178
|
+
//
|
|
179
|
+
// `mongoose.plugin(reprovaMongoosePlugin)` only reaches schemas compiled
|
|
180
|
+
// AFTER the call — confirmed empirically that Mongoose bakes its Kareem
|
|
181
|
+
// hook chain in at `mongoose.model(...)` time, so `schema.pre/post` added
|
|
182
|
+
// to an already-compiled model's schema afterward are simply never
|
|
183
|
+
// consulted again. That ordering requirement forces apps whose own
|
|
184
|
+
// bootstrap already compiles a model before Reprova can be wired in (e.g.
|
|
185
|
+
// a multi-file loader architecture that connects Mongoose, compiles
|
|
186
|
+
// models, THEN sets up the rest) to route `Reprova.init()` to wherever
|
|
187
|
+
// runs earliest, rather than wherever's most natural.
|
|
188
|
+
//
|
|
189
|
+
// This covers that case by wrapping each already-compiled model's own
|
|
190
|
+
// query/document methods directly instead of relying on schema hooks — a
|
|
191
|
+
// plain method override works regardless of compile order. It also
|
|
192
|
+
// sidesteps the pooled-connection ALS-loss problem the schema-hook path
|
|
193
|
+
// needs the WeakMap for (see captureCtx/resolveCtx above): the context is
|
|
194
|
+
// captured synchronously into a closure the instant the wrapped method is
|
|
195
|
+
// called, before any driver I/O, so the `.then()` continuation always has
|
|
196
|
+
// the right value on hand regardless of what AsyncLocalStorage does.
|
|
197
|
+
//
|
|
198
|
+
// Idempotent and safe to call repeatedly (e.g. instrumentMongoose() called
|
|
199
|
+
// more than once) — already-patched models are skipped via `patchedModels`.
|
|
200
|
+
const patchedModels = new WeakSet();
|
|
201
|
+
function wrapQueryReturningMethod(model, methodName, op) {
|
|
202
|
+
const original = model[methodName];
|
|
203
|
+
if (typeof original !== 'function')
|
|
204
|
+
return;
|
|
205
|
+
model[methodName] = function (...args) {
|
|
206
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
207
|
+
const query = original.apply(this, args);
|
|
208
|
+
if (!query || typeof query.exec !== 'function')
|
|
209
|
+
return query;
|
|
210
|
+
const originalExec = query.exec.bind(query);
|
|
211
|
+
query.exec = (...execArgs) => originalExec(...execArgs).then((result) => {
|
|
212
|
+
try {
|
|
213
|
+
const collection = model.collection?.collectionName;
|
|
214
|
+
let whereShape;
|
|
215
|
+
try {
|
|
216
|
+
whereShape = query.getFilter ? (0, shape_js_1.shapeArgs)(query.getFilter()) : undefined;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* filter introspection best-effort */
|
|
220
|
+
}
|
|
221
|
+
recordFootprint(ctx, collection, op, idsOf(result), affectedCountOf(result), whereShape);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* never break the host query */
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
});
|
|
228
|
+
return query;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function patchCompiledModel(model) {
|
|
232
|
+
if (patchedModels.has(model))
|
|
233
|
+
return;
|
|
234
|
+
patchedModels.add(model);
|
|
235
|
+
for (const methodName of SELECT_QUERY_OPS)
|
|
236
|
+
wrapQueryReturningMethod(model, methodName, 'select');
|
|
237
|
+
for (const methodName of UPDATE_QUERY_OPS)
|
|
238
|
+
wrapQueryReturningMethod(model, methodName, 'update');
|
|
239
|
+
for (const methodName of DELETE_QUERY_OPS)
|
|
240
|
+
wrapQueryReturningMethod(model, methodName, 'delete');
|
|
241
|
+
const proto = model.prototype;
|
|
242
|
+
const originalSave = proto?.save;
|
|
243
|
+
if (proto && typeof originalSave === 'function') {
|
|
244
|
+
proto.save = function (...args) {
|
|
245
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
246
|
+
const wasNew = !!this.isNew;
|
|
247
|
+
return originalSave.apply(this, args).then((result) => {
|
|
248
|
+
try {
|
|
249
|
+
recordFootprint(ctx, this.collection?.collectionName, wasNew ? 'insert' : 'update', idsOf(this), 1);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
/* never break the host save */
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const originalInsertMany = model.insertMany;
|
|
259
|
+
if (typeof originalInsertMany === 'function') {
|
|
260
|
+
model.insertMany = function (...args) {
|
|
261
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
262
|
+
return originalInsertMany.apply(this, args).then((docs) => {
|
|
263
|
+
try {
|
|
264
|
+
const ids = idsOf(docs);
|
|
265
|
+
recordFootprint(ctx, model.collection?.collectionName, 'insert', ids, ids.length);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* never break the host insertMany */
|
|
269
|
+
}
|
|
270
|
+
return docs;
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Called once from Reprova.instrumentMongoose() alongside the plugin
|
|
276
|
+
// registration above — covers every model that already exists on this
|
|
277
|
+
// mongoose instance at that moment. Any model compiled AFTER this call is
|
|
278
|
+
// still handled by reprovaMongoosePlugin via mongoose.plugin() as before.
|
|
279
|
+
function instrumentAlreadyCompiledModels(mongooseInstance) {
|
|
280
|
+
let names = [];
|
|
281
|
+
try {
|
|
282
|
+
names = mongooseInstance.modelNames();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (const name of names) {
|
|
288
|
+
try {
|
|
289
|
+
patchCompiledModel(mongooseInstance.model(name));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
/* one bad model shouldn't block instrumenting the rest */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function discoverMongooseSchema(mongooseInstance) {
|
|
297
|
+
const models = [];
|
|
298
|
+
const edges = [];
|
|
299
|
+
let names = [];
|
|
300
|
+
try {
|
|
301
|
+
names = mongooseInstance.modelNames();
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return { models, edges };
|
|
305
|
+
}
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
let model;
|
|
308
|
+
try {
|
|
309
|
+
model = mongooseInstance.model(name);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const collection = model.collection?.collectionName;
|
|
315
|
+
if (!collection)
|
|
316
|
+
continue;
|
|
317
|
+
models.push({ model: name, collection });
|
|
318
|
+
try {
|
|
319
|
+
model.schema.eachPath((pathName, schemaType) => {
|
|
320
|
+
// Direct ref: `{ type: Schema.Types.ObjectId, ref: 'X' }`. Array ref:
|
|
321
|
+
// `[{ type: Schema.Types.ObjectId, ref: 'X' }]` — Mongoose exposes
|
|
322
|
+
// the array element's own SchemaType (with its own `.options.ref`)
|
|
323
|
+
// via `.caster` on the array path's SchemaType.
|
|
324
|
+
const ref = schemaType?.options?.ref ?? schemaType?.caster?.options?.ref;
|
|
325
|
+
if (ref) {
|
|
326
|
+
edges.push({ from_collection: collection, from_field: pathName, to_model: ref, array: !!schemaType?.caster });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* schema introspection best-effort — one bad path shouldn't drop the whole model */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { models, edges };
|
|
335
|
+
}
|
|
Binary file
|
package/dist/cjs/nest.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNestExceptionFilter = createNestExceptionFilter;
|
|
4
|
+
exports.createNestContextInterceptor = createNestContextInterceptor;
|
|
5
|
+
const stack_js_1 = require("./stack.js");
|
|
6
|
+
// Nest's HttpException contract, detected structurally: anything with
|
|
7
|
+
// getStatus() and getResponse() is treated as an intentional HTTP response
|
|
8
|
+
// (a 404, a validation 400, ...) exactly like Nest's own default filter does.
|
|
9
|
+
function isHttpExceptionLike(e) {
|
|
10
|
+
return (typeof e === 'object' &&
|
|
11
|
+
e !== null &&
|
|
12
|
+
typeof e.getStatus === 'function' &&
|
|
13
|
+
typeof e.getResponse === 'function');
|
|
14
|
+
}
|
|
15
|
+
function toError(exception) {
|
|
16
|
+
if (exception instanceof Error)
|
|
17
|
+
return exception;
|
|
18
|
+
return new Error(typeof exception === 'string' ? exception : JSON.stringify(exception));
|
|
19
|
+
}
|
|
20
|
+
// Express: res.status(n).json(body). Fastify: reply.status(n).send(body).
|
|
21
|
+
// Both expose status(); the payload method is feature-detected. Guarded —
|
|
22
|
+
// an exception filter must never itself throw.
|
|
23
|
+
function sendJson(res, status, body) {
|
|
24
|
+
try {
|
|
25
|
+
const r = res;
|
|
26
|
+
if (typeof r.status !== 'function')
|
|
27
|
+
return;
|
|
28
|
+
const chained = r.status(status);
|
|
29
|
+
if (typeof chained.json === 'function')
|
|
30
|
+
chained.json(body);
|
|
31
|
+
else if (typeof chained.send === 'function')
|
|
32
|
+
chained.send(body);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* never break the response path from inside the filter */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function createNestExceptionFilter(opts = {}) {
|
|
39
|
+
// Same activation rule as replayErrorHandler: the CLI sets REPROVA_REPLAY=1
|
|
40
|
+
// when it boots the app for replay; checked once at construction.
|
|
41
|
+
const replay = process.env.REPROVA_REPLAY === '1';
|
|
42
|
+
return {
|
|
43
|
+
catch(exception, host) {
|
|
44
|
+
const httpEx = isHttpExceptionLike(exception);
|
|
45
|
+
let status = 500;
|
|
46
|
+
if (httpEx) {
|
|
47
|
+
try {
|
|
48
|
+
status = exception.getStatus();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
status = 500;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const res = host.switchToHttp().getResponse();
|
|
55
|
+
const err = toError(exception);
|
|
56
|
+
// Replay contract (only for server-side failures — intentional 4xx
|
|
57
|
+
// responses replay as themselves): emit the structured error the CLI's
|
|
58
|
+
// verdict comparison expects, via the SAME parseStack used at capture.
|
|
59
|
+
if (replay && status >= 500) {
|
|
60
|
+
sendJson(res, 500, {
|
|
61
|
+
error: {
|
|
62
|
+
type: err.constructor?.name ?? err.name ?? 'Error',
|
|
63
|
+
message: err.message,
|
|
64
|
+
stack: (0, stack_js_1.parseStack)(err),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Capture server-side failures with the ORIGINAL exception. 4xx
|
|
70
|
+
// HttpExceptions are intentional control flow, exactly as Nest treats
|
|
71
|
+
// them — not captured (a silent 500 from elsewhere still gets the 5xx
|
|
72
|
+
// hook as backstop).
|
|
73
|
+
if (status >= 500 && opts.capture) {
|
|
74
|
+
try {
|
|
75
|
+
opts.capture(err);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* capture must never break the response */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Respond the way Nest's own BaseExceptionFilter would have.
|
|
82
|
+
if (httpEx) {
|
|
83
|
+
let body;
|
|
84
|
+
try {
|
|
85
|
+
body = exception.getResponse();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
body = { statusCode: status, message: err.message };
|
|
89
|
+
}
|
|
90
|
+
sendJson(res, status, typeof body === 'string' ? { statusCode: status, message: body } : body);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
sendJson(res, 500, { statusCode: 500, message: 'Internal server error' });
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function wrapSubscriptionWithContext(source, runInContext, ctx) {
|
|
99
|
+
if (!source || typeof source.subscribe !== 'function')
|
|
100
|
+
return source;
|
|
101
|
+
const obs = source;
|
|
102
|
+
const originalSubscribe = obs.subscribe.bind(obs);
|
|
103
|
+
obs.subscribe = (...args) => runInContext(ctx, () => originalSubscribe(...args));
|
|
104
|
+
return obs;
|
|
105
|
+
}
|
|
106
|
+
function createNestContextInterceptor(opts) {
|
|
107
|
+
return {
|
|
108
|
+
intercept(context, next) {
|
|
109
|
+
let req;
|
|
110
|
+
try {
|
|
111
|
+
req = context.switchToHttp().getRequest();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
req = undefined;
|
|
115
|
+
}
|
|
116
|
+
let ctx;
|
|
117
|
+
try {
|
|
118
|
+
ctx = opts.resolveContext(req);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
ctx = undefined;
|
|
122
|
+
}
|
|
123
|
+
if (!ctx)
|
|
124
|
+
return next.handle();
|
|
125
|
+
// next.handle() itself must be called exactly once — on failure here
|
|
126
|
+
// there is nothing safe to retry (a second call could re-invoke the
|
|
127
|
+
// controller method, double-executing whatever it does), so this is
|
|
128
|
+
// deliberately not wrapped in a try/catch that falls back to a bare
|
|
129
|
+
// next.handle(). Constructing the Observable is cheap/lazy (no pipe or
|
|
130
|
+
// controller work happens yet); the actual context wrap lives inside
|
|
131
|
+
// wrapSubscriptionWithContext, applied to whatever subscribes to it
|
|
132
|
+
// later, whenever that turns out to be.
|
|
133
|
+
return wrapSubscriptionWithContext(next.handle(), opts.runInContext, ctx);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNextAdapter = createNextAdapter;
|
|
4
|
+
const context_js_1 = require("./context.js");
|
|
5
|
+
const sdk_js_1 = require("./sdk.js");
|
|
6
|
+
const registry_js_1 = require("./registry.js");
|
|
7
|
+
const stack_js_1 = require("./stack.js");
|
|
8
|
+
function toError(value) {
|
|
9
|
+
if (value instanceof Error)
|
|
10
|
+
return value;
|
|
11
|
+
return new Error(typeof value === 'string' ? value : JSON.stringify(value));
|
|
12
|
+
}
|
|
13
|
+
function headersToRecord(headers) {
|
|
14
|
+
const out = {};
|
|
15
|
+
headers.forEach((value, key) => {
|
|
16
|
+
out[key] = value;
|
|
17
|
+
});
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
// Builds a brand-new, self-contained context directly from a Fetch API
|
|
21
|
+
// Request — used when there is no ambient ALS context AND no outer context
|
|
22
|
+
// could be recovered by traceId (instrumentHttp disabled, an edge runtime
|
|
23
|
+
// with no Node http.Server underneath, or any other setup with no "outer"
|
|
24
|
+
// context to begin with). Registered/completed by the caller, same lifecycle
|
|
25
|
+
// runWithContext/runJob already give background work with no HTTP layer at
|
|
26
|
+
// all.
|
|
27
|
+
function contextFromFetchRequest(req) {
|
|
28
|
+
let path = '/';
|
|
29
|
+
try {
|
|
30
|
+
path = new URL(req.url).pathname;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// keep the default
|
|
34
|
+
}
|
|
35
|
+
const traceId = req.headers.get('traceparent') ?? (0, stack_js_1.generateTraceId)();
|
|
36
|
+
return {
|
|
37
|
+
traceId,
|
|
38
|
+
capturedAt: new Date(),
|
|
39
|
+
request: {
|
|
40
|
+
method: req.method,
|
|
41
|
+
path,
|
|
42
|
+
headers: headersToRecord(req.headers),
|
|
43
|
+
body: '',
|
|
44
|
+
},
|
|
45
|
+
footprint: [],
|
|
46
|
+
outboundCalls: [],
|
|
47
|
+
businessInvariantHits: [],
|
|
48
|
+
writeCountDriftHits: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// The core of wrapRouteHandler's universality: never assumes ambient ALS
|
|
52
|
+
// context survived whatever dispatch mechanism called into this route.
|
|
53
|
+
// Three cases, in order:
|
|
54
|
+
// 1. Ambient context is present — the common case for Pages Router/tRPC,
|
|
55
|
+
// and for any future Next.js version where App Router propagation
|
|
56
|
+
// works. Used as-is; nothing to register/complete, that's already
|
|
57
|
+
// owned by whoever established it.
|
|
58
|
+
// 2. No ambient context, but the request carries the SAME traceparent
|
|
59
|
+
// value contextFromIncoming wrote onto the raw connection — recovers
|
|
60
|
+
// the REAL outer context (the one instrumentHttpServers's res 'finish'
|
|
61
|
+
// handler already closes over for its own http_5xx fallback), so
|
|
62
|
+
// running the handler inside it and capturing marks that SAME ctx as
|
|
63
|
+
// captured, suppressing the fallback with zero coordination beyond the
|
|
64
|
+
// header. This is the case that fixes Next.js App Router: Next
|
|
65
|
+
// constructs its own Fetch Request rather than reusing the
|
|
66
|
+
// IncomingMessage, so object-identity recovery (ctxByRequest) can't
|
|
67
|
+
// work there, but the header survives regardless of what request
|
|
68
|
+
// object Next builds.
|
|
69
|
+
// 3. Neither — genuinely nothing to recover (instrumentHttp disabled, an
|
|
70
|
+
// edge runtime). Build a fresh context from the Request itself so a
|
|
71
|
+
// real capture still happens instead of silently deferring to a
|
|
72
|
+
// fallback that, in this case, doesn't exist either.
|
|
73
|
+
function resolveRequestContext(req) {
|
|
74
|
+
const existing = context_js_1.contextStorage.getStore();
|
|
75
|
+
if (existing)
|
|
76
|
+
return { ctx: existing, isFresh: false };
|
|
77
|
+
const traceId = req.headers.get('traceparent');
|
|
78
|
+
const recovered = traceId ? (0, registry_js_1.findContextByTraceId)(traceId) : undefined;
|
|
79
|
+
if (recovered)
|
|
80
|
+
return { ctx: recovered, isFresh: false };
|
|
81
|
+
const fresh = contextFromFetchRequest(req);
|
|
82
|
+
(0, registry_js_1.registerContext)(fresh);
|
|
83
|
+
return { ctx: fresh, isFresh: true };
|
|
84
|
+
}
|
|
85
|
+
// Shared by wrapRouteHandler and withRequestContext: resolves the context
|
|
86
|
+
// once, runs fn() inside it via ALS, and tears down a freshly-built one
|
|
87
|
+
// (never a recovered or already-ambient one — those are owned by whoever
|
|
88
|
+
// created them).
|
|
89
|
+
async function runInResolvedContext(req, fn) {
|
|
90
|
+
const { ctx, isFresh } = resolveRequestContext(req);
|
|
91
|
+
try {
|
|
92
|
+
return await context_js_1.contextStorage.run(ctx, () => fn(ctx));
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (isFresh)
|
|
96
|
+
(0, registry_js_1.completeContext)(ctx);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function createNextAdapter(opts) {
|
|
100
|
+
const { prisma, dmmf, ...reprovaOpts } = opts;
|
|
101
|
+
const sdk = sdk_js_1.Reprova.init(reprovaOpts);
|
|
102
|
+
// Not caught here deliberately: a Prisma instrumentation failure is a real
|
|
103
|
+
// setup bug (bad dmmf, a client shape instrumentPrisma doesn't recognize),
|
|
104
|
+
// not something to silently swallow — every ensureInit() caller sees the
|
|
105
|
+
// same rejection instrumentPrisma itself produced, same as the original
|
|
106
|
+
// cal.diy wiring this replaces (`await reprovaSdk.instrumentPrisma(...)`,
|
|
107
|
+
// no try/catch).
|
|
108
|
+
const initPromise = prisma !== undefined ? sdk.instrumentPrisma(prisma, { dmmf }) : Promise.resolve();
|
|
109
|
+
function ensureInit() {
|
|
110
|
+
return initPromise;
|
|
111
|
+
}
|
|
112
|
+
// Used by wrapApiHandler and trpcMiddleware — both reliably see the
|
|
113
|
+
// ambient context established further up the stack (Pages Router's
|
|
114
|
+
// apiResolver mutates the SAME req object instrumentHttpServers already
|
|
115
|
+
// saw; tRPC procedures run inside whatever context wrapped the HTTP
|
|
116
|
+
// handler that dispatches them). Confirmed live: only App Router's own
|
|
117
|
+
// internal dispatch loses the ALS store (see wrapRouteHandler below for
|
|
118
|
+
// the fix) — Pages Router and tRPC carry it through fine, so they don't
|
|
119
|
+
// need the recovery dance.
|
|
120
|
+
function captureUnhandled(err) {
|
|
121
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
122
|
+
if (!ctx || ctx.captured)
|
|
123
|
+
return;
|
|
124
|
+
sdk.capture('unhandled_exception', toError(err), ctx);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ensureInit,
|
|
128
|
+
captureError(err) {
|
|
129
|
+
const ctx = context_js_1.contextStorage.getStore();
|
|
130
|
+
if (!ctx || ctx.captured)
|
|
131
|
+
return;
|
|
132
|
+
sdk.captureError(toError(err));
|
|
133
|
+
},
|
|
134
|
+
wrapRouteHandler(handler) {
|
|
135
|
+
return (async (req, ...rest) => {
|
|
136
|
+
await ensureInit();
|
|
137
|
+
// Establishing ALS directly around this call — rather than
|
|
138
|
+
// depending on whatever context Next's own dispatch may or may not
|
|
139
|
+
// have carried through to get here — is what makes this work
|
|
140
|
+
// regardless of Next.js version, App Router internals, or
|
|
141
|
+
// Turbopack vs. webpack: there is no dispatch layer left between
|
|
142
|
+
// this contextStorage.run() and the handler for anything to lose.
|
|
143
|
+
return runInResolvedContext(req, async (ctx) => {
|
|
144
|
+
try {
|
|
145
|
+
return await handler(req, ...rest);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!ctx.captured)
|
|
149
|
+
sdk.capture('unhandled_exception', toError(err), ctx);
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
async withRequestContext(req, fn) {
|
|
156
|
+
await ensureInit();
|
|
157
|
+
return runInResolvedContext(req, () => Promise.resolve(fn()));
|
|
158
|
+
},
|
|
159
|
+
wrapApiHandler(handler) {
|
|
160
|
+
return async (req, res) => {
|
|
161
|
+
await ensureInit();
|
|
162
|
+
try {
|
|
163
|
+
await handler(req, res);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
captureUnhandled(err);
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
trpcMiddleware: async ({ next }) => {
|
|
172
|
+
await ensureInit();
|
|
173
|
+
const result = await next();
|
|
174
|
+
if (!result.ok && result.error?.code === 'INTERNAL_SERVER_ERROR') {
|
|
175
|
+
captureUnhandled(result.error.cause ?? result.error);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|