@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,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;
@@ -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
@@ -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
+ }