meadow-endpoints 4.0.16 → 4.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-endpoints",
3
- "version": "4.0.16",
3
+ "version": "4.0.18",
4
4
  "description": "Automatic API endpoints for Meadow data.",
5
5
  "main": "source/Meadow-Endpoints.js",
6
6
  "scripts": {
@@ -82,6 +82,7 @@ class MeadowEndpoints
82
82
 
83
83
  Upsert: require('./endpoints/upsert/Meadow-Endpoint-Upsert.js'),
84
84
  Upserts: require('./endpoints/upsert/Meadow-Endpoint-BulkUpsert.js'),
85
+ UpsertsDetailed: require('./endpoints/upsert/Meadow-Endpoint-BulkUpsertDetailed.js'),
85
86
 
86
87
  Delete: require('./endpoints/delete/Meadow-Endpoint-Delete.js'),
87
88
  Undelete: require('./endpoints/delete/Meadow-Endpoint-Undelete.js'),
@@ -211,6 +212,7 @@ class MeadowEndpoints
211
212
  this.connectRoute(pServiceServer, 'putWithBodyParser', `s`, this._Endpoints.Updates, `the internal behavior _Endpoints.Updates`);
212
213
  this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upsert`, this._Endpoints.Upsert, `the internal behavior _Endpoints.Upsert`);
213
214
  this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upserts`, this._Endpoints.Upserts, `the internal behavior _Endpoints.Upserts`);
215
+ this.connectRoute(pServiceServer, 'putWithBodyParser', `/Upserts/Detailed`, this._Endpoints.UpsertsDetailed, `the internal behavior _Endpoints.UpsertsDetailed`);
214
216
  }
215
217
  if (this._EnabledBehaviorSets.Delete)
216
218
  {
@@ -23,6 +23,12 @@ const doAPIEndpointBulkCreate = function(pRequest, pResponse, fNext)
23
23
 
24
24
  return fStageComplete();
25
25
  },
26
+ // Endpoint-level pre-request hook for bulk creates. Mirror of
27
+ // Create-PreRequest in the singular Create endpoint — fires
28
+ // after body-array validation and before any per-record
29
+ // operation. Use cases include bulk idempotency suppression /
30
+ // dedup across the incoming batch.
31
+ fBehaviorInjector(`CreateBulk-PreRequest`),
26
32
  fBehaviorInjector(`CreateBulk-PreOperation`),
27
33
  (fStageComplete) =>
28
34
  {
@@ -19,6 +19,14 @@ const doAPIEndpointCreate = function(pRequest, pResponse, fNext)
19
19
 
20
20
  return fStageComplete();
21
21
  },
22
+ // Endpoint-level pre-request hook. Runs after the body-type check
23
+ // but before any operation work, mirroring ME 2.x's
24
+ // Create-PreRequest stage. Use cases include idempotency
25
+ // suppression (e.g. look up by primary GUID and short-circuit
26
+ // if the row already exists). Handlers can abort the operation
27
+ // by calling fStageComplete with a truthy error or by fully
28
+ // writing pResponse and returning a sentinel.
29
+ fBehaviorInjector(`Create-PreRequest`),
22
30
  (fStageComplete) =>
23
31
  {
24
32
  doCreate.call(this, pRequest.body, pRequest, tmpRequestState, pResponse, fStageComplete);
@@ -55,6 +55,12 @@ const doAPIEndpointDelete = function(pRequest, pResponse, fNext)
55
55
  return fStageComplete(this.ErrorHandler.getError('Record not found.', 404));
56
56
  }
57
57
  tmpRequestState.Record = pRecord;
58
+ // Alias the loaded pre-delete row for symmetry
59
+ // with Update (see Meadow-Operation-Update.js).
60
+ // Post-op hooks that compare pre/post values can
61
+ // reliably read OriginalRecord without having to
62
+ // know which stage overwrote Record.
63
+ tmpRequestState.OriginalRecord = pRecord;
58
64
  return fStageComplete();
59
65
  });
60
66
  },
@@ -70,6 +70,11 @@ const doAPIEndpointUndelete = function(pRequest, pResponse, fNext)
70
70
  return fStageComplete(this.ErrorHandler.getError('Record not found.', 404));
71
71
  }
72
72
  tmpRequestState.Record = pRecord;
73
+ // Alias the loaded pre-undelete row for symmetry
74
+ // with Update / Delete (see their endpoints). Post-op
75
+ // hooks that compare pre/post values can reliably
76
+ // read OriginalRecord.
77
+ tmpRequestState.OriginalRecord = pRecord;
73
78
  return fStageComplete();
74
79
  });
75
80
  },
@@ -70,6 +70,13 @@ const doAPIEndpointReadDistinct = function(pRequest, pResponse, fNext)
70
70
  tmpRequestState.Records = pRecords;
71
71
  return fStageComplete();
72
72
  },
73
+ // Stage-specific post-op hook. Fires after DAL read but BEFORE
74
+ // the records are projected to distinct-column shape, so
75
+ // handlers can run against full rows. Separate from
76
+ // Reads-PostOperation so registering one doesn't unintentionally
77
+ // fire on the other. Hash matches the endpoint's action label
78
+ // (initializeRequestState(..., 'ReadDistinct')).
79
+ fBehaviorInjector(`ReadDistinct-PostOperation`),
73
80
  (fStageComplete) =>
74
81
  {
75
82
  tmpRequestState.ResultRecords = marshalDistinctList.call(this, tmpRequestState.Records, pRequest, tmpRequestState.DistinctColumns);
@@ -60,8 +60,14 @@ const doAPIEndpointReadLite = function(pRequest, pResponse, fNext)
60
60
  pRecords = [];
61
61
  }
62
62
  tmpRequestState.RawRecords = pRecords;
63
+ // Expose the loaded records under pRequestState.Records
64
+ // so post-op hooks operate on the same shape regular
65
+ // Reads uses. Marshalling to lite shape runs AFTER the
66
+ // hook so hooks see full rows.
67
+ tmpRequestState.Records = pRecords;
63
68
  return fStageComplete();
64
69
  },
70
+ fBehaviorInjector(`ReadsLite-PostOperation`),
65
71
  (fStageComplete) =>
66
72
  {
67
73
  tmpRequestState.Records = marshalLiteList.call(this, tmpRequestState.RawRecords, pRequest, (typeof(pRequest.params.ExtraColumns) === 'string') ? pRequest.params.ExtraColumns.split(',') : []);
@@ -54,6 +54,12 @@ const doAPIEndpointReadSelectList = function(pRequest, pResponse, fNext)
54
54
 
55
55
  return fStageComplete();
56
56
  },
57
+ // Stage-specific post-op hook. Fires after DAL read but
58
+ // BEFORE the records are projected to select-list
59
+ // (Hash/Value) shape, so handlers can run against full
60
+ // rows. Separate from Reads-PostOperation so registering
61
+ // one doesn't unintentionally fire on the other.
62
+ fBehaviorInjector(`ReadSelectList-PostOperation`),
57
63
  (fStageComplete) =>
58
64
  {
59
65
  tmpRequestState.SelectList = [];
@@ -49,6 +49,14 @@ const doUpdate = function(pRecordToModify, pRequest, pRequestState, pResponse, f
49
49
  return fStageComplete(this.ErrorHandler.getError('Record not Found', 404));
50
50
  }
51
51
  tmpRequestState.Record = pRecord;
52
+ // Alias the loaded pre-update row under an
53
+ // unambiguous name. pRequestState.Record gets
54
+ // overwritten with the POST-update row later in
55
+ // this waterfall; OriginalRecord preserves the
56
+ // PRE-update reference for post-op hooks that
57
+ // need to compare before/after values (change
58
+ // logs, customer-boundary checks, etc.).
59
+ tmpRequestState.OriginalRecord = pRecord;
52
60
  return fStageComplete();
53
61
  });
54
62
  }
@@ -40,11 +40,35 @@ const doAPIEndpointUpserts = function(pRequest, pResponse, fNext)
40
40
  fBehaviorInjector(`UpsertBulk-PostOperation`),
41
41
  (fStageComplete) =>
42
42
  {
43
+ // Surface per-row error counts to the caller via response
44
+ // headers BEFORE streaming the success array. Without these,
45
+ // callers can only see "N records came back" and have no
46
+ // signal that other records were silently dropped (e.g.
47
+ // from a NOT NULL constraint or a column-too-long error).
48
+ // We keep the response body shape stable (bare array of
49
+ // upserted records) for back-compat — the headers are the
50
+ // non-breaking surface for the failure count + total.
51
+ let tmpInputCount = (tmpRequestState.BulkRecords && tmpRequestState.BulkRecords.length) || 0;
52
+ let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
53
+ let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
54
+ try
55
+ {
56
+ pResponse.header('X-Meadow-Upsert-Total', String(tmpInputCount));
57
+ pResponse.header('X-Meadow-Upsert-Succeeded', String(tmpUpsertedCount));
58
+ pResponse.header('X-Meadow-Upsert-Errored', String(tmpErrorCount));
59
+ }
60
+ catch (pHdrErr) { /* response.header may not exist on all servers; degrade silently */ }
61
+
43
62
  return this.doStreamRecordArray(pResponse, marshalLiteList.call(this, tmpRequestState.UpsertedRecords, pRequest), fStageComplete);
44
63
  },
45
64
  (fStageComplete) =>
46
65
  {
47
- this.log.requestCompletedSuccessfully(pRequest, tmpRequestState, `Bulk upsert complete -- ${tmpRequestState.UpsertedRecords.length} records processed`);
66
+ let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
67
+ let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
68
+ let tmpMessage = (tmpErrorCount > 0)
69
+ ? `Bulk upsert complete -- ${tmpUpsertedCount} records succeeded, ${tmpErrorCount} errored`
70
+ : `Bulk upsert complete -- ${tmpUpsertedCount} records processed`;
71
+ this.log.requestCompletedSuccessfully(pRequest, tmpRequestState, tmpMessage);
48
72
  return fStageComplete();
49
73
  }
50
74
  ], (pError) =>
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Meadow Endpoint - Upsert a set of Records, with detailed envelope response.
3
+ *
4
+ * Same upsert pipeline as Meadow-Endpoint-BulkUpsert.js, but the
5
+ * response body is an envelope:
6
+ *
7
+ * {
8
+ * "Counts": { "Total": N, "Succeeded": K, "Errored": M },
9
+ * "UpsertedRecords": [ ... lite-marshaled successes ... ],
10
+ * "ErrorRecords": [ { "Record": {...}, "Operation": "...", "Error": "..." }, ... ]
11
+ * }
12
+ *
13
+ * The vanilla `/Upserts` endpoint streams a bare array of successes for
14
+ * back-compat; this `/Upserts/Detailed` variant trades that compatibility
15
+ * for full per-row error visibility. Same X-Meadow-Upsert-* response
16
+ * headers are set on both.
17
+ */
18
+ const doUpsert = require('./Meadow-Operation-Upsert.js');
19
+
20
+ const marshalLiteList = require('../read/Meadow-Marshal-LiteList.js');
21
+
22
+ const doAPIEndpointUpsertsDetailed = function(pRequest, pResponse, fNext)
23
+ {
24
+ let tmpRequestState = this.initializeRequestState(pRequest, 'UpsertBulkDetailed');
25
+ let fBehaviorInjector = (pBehaviorHash) => { return (fStageComplete) => { this.BehaviorInjection.runBehavior(pBehaviorHash, this, pRequest, tmpRequestState, fStageComplete); }; };
26
+
27
+ tmpRequestState.CreatedRecords = [];
28
+ tmpRequestState.UpdatedRecords = [];
29
+ tmpRequestState.UpsertedRecords = [];
30
+ tmpRequestState.ErrorRecords = [];
31
+
32
+ this.waterfall(
33
+ [
34
+ (fStageComplete) =>
35
+ {
36
+ if (!Array.isArray(pRequest.body))
37
+ {
38
+ return fStageComplete(this.ErrorHandler.getError(`Record bulk upsert (detailed) failure - a valid array of records is required.`, 500));
39
+ }
40
+
41
+ tmpRequestState.BulkRecords = pRequest.body;
42
+
43
+ return fStageComplete();
44
+ },
45
+ fBehaviorInjector(`UpsertBulk-PreOperation`),
46
+ (fStageComplete) =>
47
+ {
48
+ this.eachLimit(tmpRequestState.BulkRecords, 1,
49
+ (pRecord, fCallback) =>
50
+ {
51
+ doUpsert.call(this, pRecord, pRequest, tmpRequestState, pResponse, fCallback);
52
+ }, fStageComplete);
53
+ },
54
+ fBehaviorInjector(`UpsertBulk-PostOperation`),
55
+ (fStageComplete) =>
56
+ {
57
+ let tmpInputCount = (tmpRequestState.BulkRecords && tmpRequestState.BulkRecords.length) || 0;
58
+ let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
59
+ let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
60
+
61
+ // Mirror the X-Meadow-Upsert-* headers from /Upserts so a
62
+ // caller can dispatch to either endpoint and read the same
63
+ // summary surface. The body envelope adds the per-row
64
+ // detail; the headers stay the structured summary.
65
+ try
66
+ {
67
+ pResponse.header('X-Meadow-Upsert-Total', String(tmpInputCount));
68
+ pResponse.header('X-Meadow-Upsert-Succeeded', String(tmpUpsertedCount));
69
+ pResponse.header('X-Meadow-Upsert-Errored', String(tmpErrorCount));
70
+ }
71
+ catch (pHdrErr) { /* response.header may not exist on all servers; degrade silently */ }
72
+
73
+ let tmpEnvelope = {
74
+ Counts: { Total: tmpInputCount, Succeeded: tmpUpsertedCount, Errored: tmpErrorCount },
75
+ UpsertedRecords: marshalLiteList.call(this, tmpRequestState.UpsertedRecords, pRequest),
76
+ ErrorRecords: tmpRequestState.ErrorRecords || []
77
+ };
78
+ pResponse.send(tmpEnvelope);
79
+ return fStageComplete();
80
+ },
81
+ (fStageComplete) =>
82
+ {
83
+ let tmpUpsertedCount = (tmpRequestState.UpsertedRecords && tmpRequestState.UpsertedRecords.length) || 0;
84
+ let tmpErrorCount = (tmpRequestState.ErrorRecords && tmpRequestState.ErrorRecords.length) || 0;
85
+ let tmpMessage = (tmpErrorCount > 0)
86
+ ? `Bulk upsert (detailed) complete -- ${tmpUpsertedCount} records succeeded, ${tmpErrorCount} errored`
87
+ : `Bulk upsert (detailed) complete -- ${tmpUpsertedCount} records processed`;
88
+ this.log.requestCompletedSuccessfully(pRequest, tmpRequestState, tmpMessage);
89
+ return fStageComplete();
90
+ }
91
+ ], (pError) =>
92
+ {
93
+ return this.ErrorHandler.handleErrorIfSet(pRequest, tmpRequestState, pResponse, pError, fNext);
94
+ });
95
+ };
96
+ module.exports = doAPIEndpointUpsertsDetailed;
@@ -126,6 +126,21 @@ const doUpsert = function(pRecordToUpsert, pRequest, pRequestState, pResponse, f
126
126
  if (pError)
127
127
  {
128
128
  tmpRequestState.Record.Error = pError;
129
+ // Surface per-row failures back to the bulk caller. The
130
+ // parent BulkUpsert endpoint reads this array to count
131
+ // errors and signal partial-success to clients via a
132
+ // response header. Without this push, individual upsert
133
+ // failures vanish silently and the bulk response only
134
+ // reflects what survived.
135
+ if (tmpRequestState.ParentRequestState && Array.isArray(tmpRequestState.ParentRequestState.ErrorRecords))
136
+ {
137
+ let tmpErrorMessage = (pError && (pError.message || pError.Error)) ? (pError.message || pError.Error) : String(pError);
138
+ tmpRequestState.ParentRequestState.ErrorRecords.push({
139
+ Record: tmpRequestState.Record,
140
+ Operation: tmpRequestState.Operation || 'Unknown',
141
+ Error: tmpErrorMessage
142
+ });
143
+ }
129
144
  }
130
145
  return fCallback();
131
146
  });
@@ -2404,5 +2404,337 @@ suite
2404
2404
  );
2405
2405
  }
2406
2406
  );
2407
+
2408
+ // ======================================================================
2409
+ // v4.0.17 additions: Create-PreRequest, OriginalRecord retention,
2410
+ // Reads-PostOperation on Lite / Select / Distinct list endpoints.
2411
+ // ======================================================================
2412
+ suite
2413
+ (
2414
+ 'Create-PreRequest fires before Create-Operation',
2415
+ () =>
2416
+ {
2417
+ test
2418
+ (
2419
+ 'setBehavior: Create-PreRequest hook fires before the operation pipeline',
2420
+ function (fDone)
2421
+ {
2422
+ let tmpFired = false;
2423
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-PreRequest',
2424
+ (pRequest, pRequestState, fCallback) =>
2425
+ {
2426
+ tmpFired = true;
2427
+ // At this stage the operation hasn't started — Record
2428
+ // should be undefined, RecordToCreate shouldn't exist yet
2429
+ // either. The HTTP body is the only record source.
2430
+ Expect(pRequestState.Record).to.be.undefined;
2431
+ Expect(pRequest.body.Title).to.equal('PreRequest Test');
2432
+ return fCallback();
2433
+ });
2434
+
2435
+ _SuperTest
2436
+ .post('1.0/Book')
2437
+ .send({ Title: 'PreRequest Test' })
2438
+ .end(
2439
+ (pError, pResponse) =>
2440
+ {
2441
+ Expect(tmpFired, 'Create-PreRequest did not fire').to.be.true;
2442
+ let tmpResult = JSON.parse(pResponse.text);
2443
+ Expect(tmpResult.Title).to.equal('PreRequest Test');
2444
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-PreRequest'];
2445
+ fDone();
2446
+ }
2447
+ );
2448
+ }
2449
+ );
2450
+ test
2451
+ (
2452
+ 'setBehavior: Create-PreRequest hook can abort the operation',
2453
+ function (fDone)
2454
+ {
2455
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-PreRequest',
2456
+ (pRequest, pRequestState, fCallback) =>
2457
+ {
2458
+ let tmpError = new Error('Rejected by pre-request');
2459
+ tmpError.StatusCode = 400;
2460
+ return fCallback(tmpError);
2461
+ });
2462
+
2463
+ _SuperTest
2464
+ .post('1.0/Book')
2465
+ .send({ Title: 'Should be rejected' })
2466
+ .end(
2467
+ (pError, pResponse) =>
2468
+ {
2469
+ let tmpResult = JSON.parse(pResponse.text);
2470
+ Expect(tmpResult).to.have.property('Error');
2471
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-PreRequest'];
2472
+ fDone();
2473
+ }
2474
+ );
2475
+ }
2476
+ );
2477
+ test
2478
+ (
2479
+ 'setBehavior: CreateBulk-PreRequest fires before per-record operations',
2480
+ function (fDone)
2481
+ {
2482
+ let tmpFired = false;
2483
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('CreateBulk-PreRequest',
2484
+ (pRequest, pRequestState, fCallback) =>
2485
+ {
2486
+ tmpFired = true;
2487
+ Expect(Array.isArray(pRequest.RecordsToBulkCreate)).to.be.true;
2488
+ Expect(pRequest.RecordsToBulkCreate).to.have.lengthOf(2);
2489
+ return fCallback();
2490
+ });
2491
+
2492
+ _SuperTest
2493
+ .post('1.0/Books')
2494
+ .send([ { Title: 'Bulk A' }, { Title: 'Bulk B' } ])
2495
+ .end(
2496
+ (pError, pResponse) =>
2497
+ {
2498
+ Expect(tmpFired, 'CreateBulk-PreRequest did not fire').to.be.true;
2499
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['CreateBulk-PreRequest'];
2500
+ fDone();
2501
+ }
2502
+ );
2503
+ }
2504
+ );
2505
+ }
2506
+ );
2507
+
2508
+ suite
2509
+ (
2510
+ 'OriginalRecord retained on pRequestState after Update/Delete/Undelete',
2511
+ () =>
2512
+ {
2513
+ let _OriginalTestID = 0;
2514
+ test
2515
+ (
2516
+ 'pre-op: create a seed record for OriginalRecord testing',
2517
+ function (fDone)
2518
+ {
2519
+ _SuperTest
2520
+ .post('1.0/Book')
2521
+ .send({ Title: 'Original Title', Genre: 'Original Genre' })
2522
+ .end(
2523
+ (pError, pResponse) =>
2524
+ {
2525
+ let tmpResult = JSON.parse(pResponse.text);
2526
+ _OriginalTestID = tmpResult.IDBook;
2527
+ Expect(_OriginalTestID).to.be.above(0);
2528
+ fDone();
2529
+ }
2530
+ );
2531
+ }
2532
+ );
2533
+ test
2534
+ (
2535
+ 'Update-PostOperation: pRequestState.OriginalRecord holds the pre-update row',
2536
+ function (fDone)
2537
+ {
2538
+ let tmpSeenOriginal = null;
2539
+ let tmpSeenRecord = null;
2540
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Update-PostOperation',
2541
+ (pRequest, pRequestState, fCallback) =>
2542
+ {
2543
+ tmpSeenOriginal = pRequestState.OriginalRecord;
2544
+ tmpSeenRecord = pRequestState.Record;
2545
+ return fCallback();
2546
+ });
2547
+
2548
+ _SuperTest
2549
+ .put('1.0/Book')
2550
+ .send({ IDBook: _OriginalTestID, Title: 'Updated Title', Genre: 'Updated Genre' })
2551
+ .end(
2552
+ (pError, pResponse) =>
2553
+ {
2554
+ Expect(tmpSeenOriginal, 'OriginalRecord should be set at post-op').to.not.be.null;
2555
+ Expect(tmpSeenOriginal.Title).to.equal('Original Title');
2556
+ Expect(tmpSeenRecord.Title).to.equal('Updated Title');
2557
+ // The two references must be DIFFERENT objects — the
2558
+ // operation overwrites Record with the post-update row
2559
+ // while OriginalRecord keeps the pre-update reference.
2560
+ Expect(tmpSeenOriginal).to.not.equal(tmpSeenRecord);
2561
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Update-PostOperation'];
2562
+ fDone();
2563
+ }
2564
+ );
2565
+ }
2566
+ );
2567
+ test
2568
+ (
2569
+ 'Delete-PostOperation: pRequestState.OriginalRecord holds the pre-delete row',
2570
+ function (fDone)
2571
+ {
2572
+ let tmpSeenOriginal = null;
2573
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Delete-PostOperation',
2574
+ (pRequest, pRequestState, fCallback) =>
2575
+ {
2576
+ tmpSeenOriginal = pRequestState.OriginalRecord;
2577
+ return fCallback();
2578
+ });
2579
+
2580
+ _SuperTest
2581
+ .delete(`1.0/Book/${_OriginalTestID}`)
2582
+ .end(
2583
+ (pError, pResponse) =>
2584
+ {
2585
+ Expect(tmpSeenOriginal, 'OriginalRecord should be set at delete post-op').to.not.be.null;
2586
+ Expect(tmpSeenOriginal.IDBook).to.equal(_OriginalTestID);
2587
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Delete-PostOperation'];
2588
+ fDone();
2589
+ }
2590
+ );
2591
+ }
2592
+ );
2593
+ test
2594
+ (
2595
+ 'Undelete-PostOperation: pRequestState.OriginalRecord holds the pre-undelete row',
2596
+ function (fDone)
2597
+ {
2598
+ let tmpSeenOriginal = null;
2599
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Undelete-PostOperation',
2600
+ (pRequest, pRequestState, fCallback) =>
2601
+ {
2602
+ tmpSeenOriginal = pRequestState.OriginalRecord;
2603
+ return fCallback();
2604
+ });
2605
+
2606
+ _SuperTest
2607
+ .get(`1.0/Book/Undelete/${_OriginalTestID}`)
2608
+ .end(
2609
+ (pError, pResponse) =>
2610
+ {
2611
+ Expect(tmpSeenOriginal, 'OriginalRecord should be set at undelete post-op').to.not.be.null;
2612
+ Expect(tmpSeenOriginal.IDBook).to.equal(_OriginalTestID);
2613
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Undelete-PostOperation'];
2614
+ fDone();
2615
+ }
2616
+ );
2617
+ }
2618
+ );
2619
+ }
2620
+ );
2621
+
2622
+ suite
2623
+ (
2624
+ 'Stage-specific PostOperation hooks on Lite / SelectList / Distinct list endpoints',
2625
+ () =>
2626
+ {
2627
+ test
2628
+ (
2629
+ 'ReadsLite-PostOperation fires on /s/Lite and receives loaded records before marshal (ME 2.x hash)',
2630
+ function (fDone)
2631
+ {
2632
+ let tmpFired = false;
2633
+ let tmpSeenRecords = null;
2634
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('ReadsLite-PostOperation',
2635
+ (pRequest, pRequestState, fCallback) =>
2636
+ {
2637
+ tmpFired = true;
2638
+ tmpSeenRecords = pRequestState.Records;
2639
+ return fCallback();
2640
+ });
2641
+
2642
+ _SuperTest
2643
+ .get('1.0/Books/Lite')
2644
+ .end(
2645
+ (pError, pResponse) =>
2646
+ {
2647
+ Expect(tmpFired, 'ReadsLite-PostOperation did not fire on lite list').to.be.true;
2648
+ Expect(Array.isArray(tmpSeenRecords)).to.be.true;
2649
+ // Hook runs BEFORE marshalling — records should still
2650
+ // have their full-row shape (Title + Genre + IDBook etc.),
2651
+ // not the lite (Hash/Value) shape the client receives.
2652
+ if (tmpSeenRecords.length > 0)
2653
+ {
2654
+ Expect(tmpSeenRecords[0]).to.have.property('IDBook');
2655
+ }
2656
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['ReadsLite-PostOperation'];
2657
+ fDone();
2658
+ }
2659
+ );
2660
+ }
2661
+ );
2662
+ test
2663
+ (
2664
+ 'Reads-PostOperation does NOT fire on /s/Lite (stage isolation)',
2665
+ function (fDone)
2666
+ {
2667
+ let tmpFiredReads = false;
2668
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Reads-PostOperation',
2669
+ (pRequest, pRequestState, fCallback) =>
2670
+ {
2671
+ tmpFiredReads = true;
2672
+ return fCallback();
2673
+ });
2674
+
2675
+ _SuperTest
2676
+ .get('1.0/Books/Lite')
2677
+ .end(
2678
+ () =>
2679
+ {
2680
+ Expect(tmpFiredReads, 'Reads-PostOperation should NOT fire on lite list — consumers register at ReadsLite-PostOperation').to.be.false;
2681
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Reads-PostOperation'];
2682
+ fDone();
2683
+ }
2684
+ );
2685
+ }
2686
+ );
2687
+ test
2688
+ (
2689
+ 'ReadSelectList-PostOperation fires on /Select before marshal',
2690
+ function (fDone)
2691
+ {
2692
+ let tmpFired = false;
2693
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('ReadSelectList-PostOperation',
2694
+ (pRequest, pRequestState, fCallback) =>
2695
+ {
2696
+ tmpFired = true;
2697
+ return fCallback();
2698
+ });
2699
+
2700
+ _SuperTest
2701
+ .get('1.0/BookSelect')
2702
+ .end(
2703
+ (pError, pResponse) =>
2704
+ {
2705
+ Expect(tmpFired, 'ReadSelectList-PostOperation did not fire on select list').to.be.true;
2706
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['ReadSelectList-PostOperation'];
2707
+ fDone();
2708
+ }
2709
+ );
2710
+ }
2711
+ );
2712
+ test
2713
+ (
2714
+ 'ReadDistinct-PostOperation fires on /s/Distinct/:Columns before marshal',
2715
+ function (fDone)
2716
+ {
2717
+ let tmpFired = false;
2718
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('ReadDistinct-PostOperation',
2719
+ (pRequest, pRequestState, fCallback) =>
2720
+ {
2721
+ tmpFired = true;
2722
+ return fCallback();
2723
+ });
2724
+
2725
+ _SuperTest
2726
+ .get('1.0/Books/Distinct/Genre')
2727
+ .end(
2728
+ (pError, pResponse) =>
2729
+ {
2730
+ Expect(tmpFired, 'ReadDistinct-PostOperation did not fire on distinct list').to.be.true;
2731
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['ReadDistinct-PostOperation'];
2732
+ fDone();
2733
+ }
2734
+ );
2735
+ }
2736
+ );
2737
+ }
2738
+ );
2407
2739
  }
2408
2740
  );