@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.
Files changed (145) hide show
  1. package/README.md +75 -0
  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 +18 -0
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/context.js.map +1 -1
  37. package/dist/distributionBaselinePoller.d.ts +36 -0
  38. package/dist/distributionBaselinePoller.d.ts.map +1 -0
  39. package/dist/distributionBaselinePoller.js +84 -0
  40. package/dist/distributionBaselinePoller.js.map +1 -0
  41. package/dist/distributionDrift.d.ts +41 -0
  42. package/dist/distributionDrift.d.ts.map +1 -0
  43. package/dist/distributionDrift.js +161 -0
  44. package/dist/distributionDrift.js.map +1 -0
  45. package/dist/drizzle.d.ts.map +1 -1
  46. package/dist/drizzle.js +10 -4
  47. package/dist/drizzle.js.map +1 -1
  48. package/dist/frameworkAdapter.d.ts +5 -0
  49. package/dist/frameworkAdapter.d.ts.map +1 -0
  50. package/dist/frameworkAdapter.js +2 -0
  51. package/dist/frameworkAdapter.js.map +1 -0
  52. package/dist/httpFetch.d.ts +13 -0
  53. package/dist/httpFetch.d.ts.map +1 -0
  54. package/dist/httpFetch.js +54 -0
  55. package/dist/httpFetch.js.map +1 -0
  56. package/dist/idempotency.d.ts +26 -0
  57. package/dist/idempotency.d.ts.map +1 -0
  58. package/dist/idempotency.js +124 -0
  59. package/dist/idempotency.js.map +1 -0
  60. package/dist/idempotencySettingsPoller.d.ts +23 -0
  61. package/dist/idempotencySettingsPoller.d.ts.map +1 -0
  62. package/dist/idempotencySettingsPoller.js +68 -0
  63. package/dist/idempotencySettingsPoller.js.map +1 -0
  64. package/dist/index.d.ts +30 -5
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +17 -3
  67. package/dist/index.js.map +1 -1
  68. package/dist/invariantInference.d.ts +28 -0
  69. package/dist/invariantInference.d.ts.map +1 -0
  70. package/dist/invariantInference.js +87 -0
  71. package/dist/invariantInference.js.map +1 -0
  72. package/dist/invariantRules.d.ts +16 -0
  73. package/dist/invariantRules.d.ts.map +1 -0
  74. package/dist/invariantRules.js +104 -0
  75. package/dist/invariantRules.js.map +1 -0
  76. package/dist/knex.d.ts.map +1 -1
  77. package/dist/knex.js +1 -0
  78. package/dist/knex.js.map +1 -1
  79. package/dist/kysely.d.ts.map +1 -1
  80. package/dist/kysely.js +2 -1
  81. package/dist/kysely.js.map +1 -1
  82. package/dist/legacyPrisma.d.ts +6 -0
  83. package/dist/legacyPrisma.d.ts.map +1 -0
  84. package/dist/legacyPrisma.js +128 -0
  85. package/dist/legacyPrisma.js.map +1 -0
  86. package/dist/mikroorm.d.ts +2 -0
  87. package/dist/mikroorm.d.ts.map +1 -0
  88. package/dist/mikroorm.js +72 -0
  89. package/dist/mikroorm.js.map +1 -0
  90. package/dist/mongoSchemaReporter.d.ts +23 -0
  91. package/dist/mongoSchemaReporter.d.ts.map +1 -0
  92. package/dist/mongoSchemaReporter.js +82 -0
  93. package/dist/mongoSchemaReporter.js.map +1 -0
  94. package/dist/mongoose.d.ts +48 -0
  95. package/dist/mongoose.d.ts.map +1 -0
  96. package/dist/mongoose.js +331 -0
  97. package/dist/mongoose.js.map +1 -0
  98. package/dist/nPlusOne.d.ts +52 -0
  99. package/dist/nPlusOne.d.ts.map +1 -0
  100. package/dist/nPlusOne.js +0 -0
  101. package/dist/nPlusOne.js.map +1 -0
  102. package/dist/nest.d.ts +13 -0
  103. package/dist/nest.d.ts.map +1 -1
  104. package/dist/nest.js +39 -0
  105. package/dist/nest.js.map +1 -1
  106. package/dist/nextjs.d.ts +27 -0
  107. package/dist/nextjs.d.ts.map +1 -0
  108. package/dist/nextjs.js +178 -0
  109. package/dist/nextjs.js.map +1 -0
  110. package/dist/prisma.d.ts +21 -5
  111. package/dist/prisma.d.ts.map +1 -1
  112. package/dist/prisma.js +247 -22
  113. package/dist/prisma.js.map +1 -1
  114. package/dist/proto.gen.d.ts +10 -2
  115. package/dist/proto.gen.d.ts.map +1 -1
  116. package/dist/registry.d.ts +1 -0
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +24 -0
  119. package/dist/registry.js.map +1 -1
  120. package/dist/retryStorm.d.ts +42 -0
  121. package/dist/retryStorm.d.ts.map +1 -0
  122. package/dist/retryStorm.js +65 -0
  123. package/dist/retryStorm.js.map +1 -0
  124. package/dist/rulesPoller.d.ts +26 -0
  125. package/dist/rulesPoller.d.ts.map +1 -0
  126. package/dist/rulesPoller.js +74 -0
  127. package/dist/rulesPoller.js.map +1 -0
  128. package/dist/sdk.d.ts +48 -7
  129. package/dist/sdk.d.ts.map +1 -1
  130. package/dist/sdk.js +619 -47
  131. package/dist/sdk.js.map +1 -1
  132. package/dist/sequelize.d.ts.map +1 -1
  133. package/dist/sequelize.js +4 -3
  134. package/dist/sequelize.js.map +1 -1
  135. package/dist/stack.d.ts.map +1 -1
  136. package/dist/stack.js +6 -3
  137. package/dist/stack.js.map +1 -1
  138. package/dist/transport.d.ts.map +1 -1
  139. package/dist/transport.js +2 -1
  140. package/dist/transport.js.map +1 -1
  141. package/dist/typeorm.d.ts +1 -0
  142. package/dist/typeorm.d.ts.map +1 -1
  143. package/dist/typeorm.js +133 -1
  144. package/dist/typeorm.js.map +1 -1
  145. 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
@@ -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"}