@reproapp/node-sdk 0.0.37 → 0.0.38

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/dist/index.js CHANGED
@@ -4092,6 +4092,45 @@ async function maybeCaptureDbValueAsync(params, sink) {
4092
4092
  }
4093
4093
  return result.captureRef;
4094
4094
  }
4095
+ function pushUniqueCaptureCandidate(candidates, value) {
4096
+ if (value === undefined)
4097
+ return;
4098
+ if (candidates.some((candidate) => candidate === value))
4099
+ return;
4100
+ candidates.push(value);
4101
+ }
4102
+ function buildResultMetaFullValueCandidates(resultMetaRaw, resultMetaSource) {
4103
+ const candidates = [];
4104
+ pushUniqueCaptureCandidate(candidates, resultMetaRaw);
4105
+ if (resultMetaSource &&
4106
+ typeof resultMetaSource === 'object' &&
4107
+ Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')) {
4108
+ pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
4109
+ }
4110
+ pushUniqueCaptureCandidate(candidates, resultMetaSource);
4111
+ return candidates;
4112
+ }
4113
+ async function maybeCaptureDbValueFromCandidatesAsync(params, sink) {
4114
+ let skippedRef;
4115
+ for (const rawValue of params.rawValues) {
4116
+ const result = await createCapturedValueEntryAsync({
4117
+ target: params.target,
4118
+ rawValue,
4119
+ previewValue: params.previewValue,
4120
+ capture: params.capture,
4121
+ });
4122
+ if (!result)
4123
+ continue;
4124
+ if (result.entry) {
4125
+ sink.push(result.entry);
4126
+ return result.captureRef;
4127
+ }
4128
+ if (!skippedRef) {
4129
+ skippedRef = result.captureRef;
4130
+ }
4131
+ }
4132
+ return skippedRef;
4133
+ }
4095
4134
  function reproMiddleware(cfg) {
4096
4135
  const masking = normalizeMaskingConfig(cfg.masking);
4097
4136
  patchMongoDb(cfg);
@@ -5285,9 +5324,9 @@ async function emitDbQueryAsync(cfg, sid, aid, payload) {
5285
5324
  previewValue: queryPreview,
5286
5325
  capture,
5287
5326
  }, dbValues);
5288
- const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
5327
+ const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
5289
5328
  target: 'db.resultMeta',
5290
- rawValue: payload.resultMetaRaw ?? resultMetaSource,
5329
+ rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
5291
5330
  previewValue: resultMetaPreview,
5292
5331
  capture,
5293
5332
  }, dbValues);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -4955,6 +4955,55 @@ async function maybeCaptureDbValueAsync(
4955
4955
  return result.captureRef;
4956
4956
  }
4957
4957
 
4958
+ function pushUniqueCaptureCandidate(candidates: any[], value: any): void {
4959
+ if (value === undefined) return;
4960
+ if (candidates.some((candidate) => candidate === value)) return;
4961
+ candidates.push(value);
4962
+ }
4963
+
4964
+ function buildResultMetaFullValueCandidates(resultMetaRaw: any, resultMetaSource: any): any[] {
4965
+ const candidates: any[] = [];
4966
+ pushUniqueCaptureCandidate(candidates, resultMetaRaw);
4967
+ if (
4968
+ resultMetaSource &&
4969
+ typeof resultMetaSource === 'object' &&
4970
+ Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')
4971
+ ) {
4972
+ pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
4973
+ }
4974
+ pushUniqueCaptureCandidate(candidates, resultMetaSource);
4975
+ return candidates;
4976
+ }
4977
+
4978
+ async function maybeCaptureDbValueFromCandidatesAsync(
4979
+ params: {
4980
+ target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error';
4981
+ rawValues: any[];
4982
+ previewValue: any;
4983
+ capture: FullValueCaptureContext;
4984
+ },
4985
+ sink: TraceValueBatchEntry[],
4986
+ ): Promise<TraceValueCaptureRef | undefined> {
4987
+ let skippedRef: TraceValueCaptureRef | undefined;
4988
+ for (const rawValue of params.rawValues) {
4989
+ const result = await createCapturedValueEntryAsync({
4990
+ target: params.target,
4991
+ rawValue,
4992
+ previewValue: params.previewValue,
4993
+ capture: params.capture,
4994
+ });
4995
+ if (!result) continue;
4996
+ if (result.entry) {
4997
+ sink.push(result.entry);
4998
+ return result.captureRef;
4999
+ }
5000
+ if (!skippedRef) {
5001
+ skippedRef = result.captureRef;
5002
+ }
5003
+ }
5004
+ return skippedRef;
5005
+ }
5006
+
4958
5007
  // ===================================================================
4959
5008
  // reproMiddleware — unchanged behavior + passive per-request trace
4960
5009
  // ===================================================================
@@ -6241,9 +6290,9 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
6241
6290
  previewValue: queryPreview,
6242
6291
  capture,
6243
6292
  }, dbValues);
6244
- const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
6293
+ const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
6245
6294
  target: 'db.resultMeta',
6246
- rawValue: payload.resultMetaRaw ?? resultMetaSource,
6295
+ rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
6247
6296
  previewValue: resultMetaPreview,
6248
6297
  capture,
6249
6298
  }, dbValues);
@@ -133,12 +133,24 @@ function execPre(hooks, name, context) {
133
133
  });
134
134
  }
135
135
 
136
+ function execPost(hooks, name, context, args = []) {
137
+ return new Promise((resolve, reject) => {
138
+ hooks.execPost(name, context, args, {}, (error) => (error ? reject(error) : resolve()));
139
+ });
140
+ }
141
+
136
142
  function dbChanges(events) {
137
143
  return events
138
144
  .filter((event) => event.event_type === 'db_change')
139
145
  .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
140
146
  }
141
147
 
148
+ function dbValueEntries(events) {
149
+ return events
150
+ .filter((event) => event.event_type === 'db_change')
151
+ .flatMap((event) => Array.isArray(event.payload?.dbValues) ? event.payload.dbValues : []);
152
+ }
153
+
142
154
  function requestCaptureManifests(events) {
143
155
  return events
144
156
  .filter((event) => event.event_type === 'request_capture_manifest')
@@ -146,6 +158,81 @@ function requestCaptureManifests(events) {
146
158
  .filter(Boolean);
147
159
  }
148
160
 
161
+ function makeNotificationModuleDocument() {
162
+ const notificationPreferences = Array.from({ length: 24 }, (_, index) => ({
163
+ trigger: 'SubjectScreened',
164
+ enabled: true,
165
+ channels: [
166
+ {
167
+ type: 'email',
168
+ template: {
169
+ subject: `Participant Screening ${index}`,
170
+ html: `<p>Screening notification ${index}</p>`,
171
+ },
172
+ recipients: {
173
+ to: ['noreply@korioclinical.com'],
174
+ cc: [
175
+ `pobrien+dev-conf-csm${index}@korioclinical.com`,
176
+ `mnebot+site-monitor${index}@korioclinical.com`,
177
+ ],
178
+ bcc: [`audit-${index}@korioclinical.com`],
179
+ },
180
+ },
181
+ ],
182
+ }));
183
+ const notificationTrigger = Array.from({ length: 24 }, (_, index) => ({
184
+ active: true,
185
+ event: 'SubjectScreened',
186
+ visitType: 'Screening',
187
+ subjectType: 'Participant',
188
+ rules: [
189
+ {
190
+ condition: {
191
+ path: 'subject.subjectStatusOption',
192
+ operator: 'equals',
193
+ value: 'Screened',
194
+ },
195
+ action: {
196
+ module: 'NotificationGeneratorService',
197
+ method: 'SubjectScreened',
198
+ order: index,
199
+ },
200
+ },
201
+ ],
202
+ }));
203
+
204
+ return {
205
+ __t: 'NotificationModule',
206
+ _id: '69e77c77ab541cfac917b37a',
207
+ active: true,
208
+ environment: 'dev-configure-pulmovant',
209
+ notificationPreferences,
210
+ notification_trigger: notificationTrigger,
211
+ protocol: {
212
+ id: '69e77c77ab541cfac917b36b',
213
+ name: 'RVT-2301-301',
214
+ },
215
+ schema_version: 1,
216
+ sponsor_name: 'Pulmovant',
217
+ study_name: 'RVT-2301-301',
218
+ };
219
+ }
220
+
221
+ class NotificationModuleHostDocument {
222
+ constructor(value) {
223
+ this.$__ = {};
224
+ this.value = value;
225
+ }
226
+
227
+ toObject() {
228
+ return JSON.parse(JSON.stringify(this.value));
229
+ }
230
+
231
+ toJSON() {
232
+ return this.toObject();
233
+ }
234
+ }
235
+
149
236
  async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
150
237
  await withCapturedFetch(async (capturedBodies) => {
151
238
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -477,6 +564,82 @@ async function testClientScreenNativeMongoInsertOneShapeIsCaptured() {
477
564
  });
478
565
  }
479
566
 
567
+ async function testLargeMongooseFindOneResultStoresFullResultMetaSidecar() {
568
+ await withCapturedFetch(async (capturedBodies) => {
569
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
570
+ const sessionId = 'S_mongoose_large_result_meta';
571
+ const actionId = 'A_mongoose_large_result_meta';
572
+ const cfg = {
573
+ ...makeConfig(sessionId, actionId),
574
+ privacy: { environment: 'dev', captureRaw: true },
575
+ };
576
+
577
+ const schema = new mongoose.Schema({}, { strict: false });
578
+ reproMongoosePlugin(cfg)(schema);
579
+
580
+ const notificationModule = makeNotificationModuleDocument();
581
+ const rawResult = new NotificationModuleHostDocument(notificationModule);
582
+ const queryContext = {
583
+ model: {
584
+ collection: {
585
+ name: 'configStudy',
586
+ },
587
+ },
588
+ getFilter: () => ({
589
+ __t: 'NotificationModule',
590
+ active: true,
591
+ protocol: {
592
+ id: '69e77c77ab541cfac917b36b',
593
+ },
594
+ }),
595
+ getUpdate: () => undefined,
596
+ projection: () => undefined,
597
+ getOptions: () => ({ lean: false }),
598
+ };
599
+
600
+ await runCapturedRequest(cfg, sessionId, actionId, async () => {
601
+ await execPre(schema.s.hooks, 'findOne', queryContext);
602
+ await execPost(schema.s.hooks, 'findOne', queryContext, [rawResult]);
603
+ });
604
+
605
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
606
+ await waitFor(() => dbValueEntries(flattenEvents(capturedBodies)).some((entry) => entry.target === 'db.resultMeta'));
607
+ await drainSdk();
608
+
609
+ const [change] = dbChanges(flattenEvents(capturedBodies));
610
+ const values = dbValueEntries(flattenEvents(capturedBodies));
611
+ const resultMetaValue = values.find((entry) => entry.id === change.resultMetaValueCapture?.valueId);
612
+
613
+ assert.strictEqual(change.type, 'mongo');
614
+ assert.strictEqual(change.collection, 'configStudy');
615
+ assert.strictEqual(change.op, 'findOne');
616
+ assert.strictEqual(change.query.filter.__t, 'NotificationModule');
617
+ assert.strictEqual(change.resultMeta.docsCount, 1);
618
+ assert.deepStrictEqual(change.resultMeta.result.notificationPreferences[0], {
619
+ __kind: 'omitted',
620
+ reason: 'depth>3',
621
+ });
622
+ assert.deepStrictEqual(change.resultMeta.result.notification_trigger[0], {
623
+ __kind: 'omitted',
624
+ reason: 'depth>3',
625
+ });
626
+ assert.strictEqual(change.resultMetaValueCapture.status, 'stored');
627
+ assert.ok(change.resultMetaValueCapture.valueId, 'expected resultMeta value id');
628
+ assert.ok(resultMetaValue, 'expected matching db.resultMeta sidecar');
629
+ assert.strictEqual(resultMetaValue.target, 'db.resultMeta');
630
+ assert.strictEqual(resultMetaValue.value.__t, 'NotificationModule');
631
+ assert.strictEqual(resultMetaValue.value.notificationPreferences.length, 24);
632
+ assert.strictEqual(
633
+ resultMetaValue.value.notificationPreferences[0].channels[0].recipients.cc[0],
634
+ 'pobrien+dev-conf-csm0@korioclinical.com',
635
+ );
636
+ assert.strictEqual(
637
+ resultMetaValue.value.notification_trigger[0].rules[0].action.method,
638
+ 'SubjectScreened',
639
+ );
640
+ });
641
+ }
642
+
480
643
  async function testNativeInsertOneIsCaptured() {
481
644
  await withCapturedFetch(async (capturedBodies) => {
482
645
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -742,6 +905,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
742
905
  await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
743
906
  await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
744
907
  await testClientScreenNativeMongoInsertOneShapeIsCaptured();
908
+ await testLargeMongooseFindOneResultStoresFullResultMetaSidecar();
745
909
  await testNativeInsertOneIsCaptured();
746
910
  await testCaptureRawNativeInsertOneUsesJsonSafeTransportValues();
747
911
  await testNativeUpdateOneUpsertIsCaptured();