meadow-endpoints 4.0.16 → 4.0.17

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.17",
4
4
  "description": "Automatic API endpoints for Meadow data.",
5
5
  "main": "source/Meadow-Endpoints.js",
6
6
  "scripts": {
@@ -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
  }
@@ -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
  );