meadow-endpoints 4.0.15 → 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.
Files changed (33) hide show
  1. package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +1286 -1065
  2. package/dist/meadow-endpoints.js +285 -148
  3. package/dist/meadow-endpoints.js.map +1 -1
  4. package/dist/meadow-endpoints.min.js +7 -7
  5. package/dist/meadow-endpoints.min.js.map +1 -1
  6. package/docs/_version.json +7 -0
  7. package/docs/css/docuserve.css +277 -23
  8. package/docs/index.html +2 -2
  9. package/docs/retold-catalog.json +13 -1
  10. package/docs/retold-keyword-index.json +1 -1
  11. package/package.json +8 -7
  12. package/source/Meadow-Endpoints-Browser-Shim.js +4 -1
  13. package/source/Meadow-Endpoints.js +6 -6
  14. package/source/controller/Meadow-Endpoints-Controller-Base.js +6 -6
  15. package/source/controller/components/Meadow-Endpoints-Controller-BehaviorInjection.js +11 -11
  16. package/source/controller/components/Meadow-Endpoints-Controller-Error.js +23 -15
  17. package/source/controller/components/Meadow-Endpoints-Controller-Log.js +9 -9
  18. package/source/controller/utility/Meadow-Endpoints-Filter-Parser.js +16 -16
  19. package/source/controller/utility/Meadow-Endpoints-Session-Marshaler.js +42 -42
  20. package/source/controller/utility/Meadow-Endpoints-Stream-RecordArray.js +6 -6
  21. package/source/endpoints/create/Meadow-Endpoint-BulkCreate.js +6 -0
  22. package/source/endpoints/create/Meadow-Endpoint-Create.js +8 -0
  23. package/source/endpoints/create/Meadow-Operation-Create.js +1 -1
  24. package/source/endpoints/delete/Meadow-Endpoint-Delete.js +6 -0
  25. package/source/endpoints/delete/Meadow-Endpoint-Undelete.js +5 -0
  26. package/source/endpoints/read/Meadow-Endpoint-ReadDistinctList.js +7 -0
  27. package/source/endpoints/read/Meadow-Endpoint-ReadLiteList.js +6 -0
  28. package/source/endpoints/read/Meadow-Endpoint-ReadSelectList.js +6 -0
  29. package/source/endpoints/schema/Meadow-Endpoint-Validate.js +1 -1
  30. package/source/endpoints/update/Meadow-Endpoint-BulkUpdate.js +2 -0
  31. package/source/endpoints/update/Meadow-Operation-Update.js +10 -0
  32. package/source/endpoints/upsert/Meadow-Endpoint-BulkUpsert.js +3 -1
  33. package/test/MeadowEndpoints_basic_tests.js +1069 -0
@@ -1408,6 +1408,743 @@ suite
1408
1408
  );
1409
1409
  }
1410
1410
  );
1411
+
1412
+ // ==================================================================
1413
+ // New hooks added 2026-04-18 to restore ME2 parity
1414
+ // (Update-Pre, Update-Query) and to extend ME4's bulk-level hooks
1415
+ // to Update/Upsert (UpdateBulk-*, UpsertBulk-*).
1416
+ // ==================================================================
1417
+
1418
+ test
1419
+ (
1420
+ 'setBehavior: Update-PreOperation hook can mutate RecordToModify before DAL update',
1421
+ function (fDone)
1422
+ {
1423
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Update-PreOperation',
1424
+ (pRequest, pRequestState, fCallback) =>
1425
+ {
1426
+ // Hook sees the incoming payload on RecordToModify,
1427
+ // existing loaded row on Record. Mutations to
1428
+ // RecordToModify flow through to the DAL update.
1429
+ Expect(pRequestState.RecordToModify).to.be.an('object');
1430
+ pRequestState.RecordToModify.Genre = 'MutatedByPreOp';
1431
+ return fCallback();
1432
+ });
1433
+
1434
+ _SuperTest
1435
+ .put('1.0/Book')
1436
+ .send({ IDBook: 4, Title: 'Snow Crash (v2)' })
1437
+ .end(
1438
+ (pError, pResponse) =>
1439
+ {
1440
+ let tmpResult = JSON.parse(pResponse.text);
1441
+ Expect(tmpResult.Genre).to.equal('MutatedByPreOp');
1442
+ Expect(tmpResult.Title).to.equal('Snow Crash (v2)');
1443
+
1444
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Update-PreOperation'];
1445
+ fDone();
1446
+ }
1447
+ );
1448
+ }
1449
+ );
1450
+ test
1451
+ (
1452
+ 'setBehavior: Update-PreOperation hook can reject an update',
1453
+ function (fDone)
1454
+ {
1455
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Update-PreOperation',
1456
+ (pRequest, pRequestState, fCallback) =>
1457
+ {
1458
+ if (!pRequestState.RecordToModify.Title)
1459
+ {
1460
+ const tmpError = new Error('Title is required');
1461
+ tmpError.StatusCode = 400;
1462
+ return fCallback(tmpError);
1463
+ }
1464
+ return fCallback();
1465
+ });
1466
+
1467
+ _SuperTest
1468
+ .put('1.0/Book')
1469
+ .send({ IDBook: 3 })
1470
+ .end(
1471
+ (pError, pResponse) =>
1472
+ {
1473
+ const tmpResult = JSON.parse(pResponse.text);
1474
+ Expect(tmpResult).to.have.property('Error');
1475
+
1476
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Update-PreOperation'];
1477
+ fDone();
1478
+ }
1479
+ );
1480
+ }
1481
+ );
1482
+ test
1483
+ (
1484
+ 'setBehavior: Update-QueryConfiguration hook fires after Query.addRecord',
1485
+ function (fDone)
1486
+ {
1487
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Update-QueryConfiguration',
1488
+ (pRequest, pRequestState, fCallback) =>
1489
+ {
1490
+ // Query should exist and have had the record added;
1491
+ // hook can scope the update further.
1492
+ Expect(pRequestState.Query).to.exist;
1493
+ pRequestState.Query.addFilter('Deleted', 0);
1494
+ return fCallback();
1495
+ });
1496
+
1497
+ _SuperTest
1498
+ .put('1.0/Book')
1499
+ .send({ IDBook: 2, Title: 'Dune (updated)' })
1500
+ .end(
1501
+ (pError, pResponse) =>
1502
+ {
1503
+ const tmpResult = JSON.parse(pResponse.text);
1504
+ Expect(tmpResult.Title).to.equal('Dune (updated)');
1505
+
1506
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Update-QueryConfiguration'];
1507
+ fDone();
1508
+ }
1509
+ );
1510
+ }
1511
+ );
1512
+ test
1513
+ (
1514
+ 'setBehavior: UpdateBulk-PreOperation fires once before the batch',
1515
+ function (fDone)
1516
+ {
1517
+ let _CallCount = 0;
1518
+ let _SawBulkRecords = false;
1519
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('UpdateBulk-PreOperation',
1520
+ (pRequest, pRequestState, fCallback) =>
1521
+ {
1522
+ _CallCount++;
1523
+ _SawBulkRecords = Array.isArray(pRequestState.BulkRecords);
1524
+ return fCallback();
1525
+ });
1526
+
1527
+ _SuperTest
1528
+ .put('1.0/Books')
1529
+ .send([
1530
+ { IDBook: 1, Title: 'Angels & Demons (batch)' },
1531
+ { IDBook: 2, Title: 'Dune (batch)' },
1532
+ ])
1533
+ .end(
1534
+ (pError, pResponse) =>
1535
+ {
1536
+ Expect(_CallCount).to.equal(1);
1537
+ Expect(_SawBulkRecords).to.equal(true);
1538
+
1539
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['UpdateBulk-PreOperation'];
1540
+ fDone();
1541
+ }
1542
+ );
1543
+ }
1544
+ );
1545
+ test
1546
+ (
1547
+ 'setBehavior: UpdateBulk-PostOperation fires once after the batch',
1548
+ function (fDone)
1549
+ {
1550
+ let _CallCount = 0;
1551
+ let _SawUpdatedRecords = false;
1552
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('UpdateBulk-PostOperation',
1553
+ (pRequest, pRequestState, fCallback) =>
1554
+ {
1555
+ _CallCount++;
1556
+ _SawUpdatedRecords = Array.isArray(pRequestState.UpdatedRecords) && pRequestState.UpdatedRecords.length > 0;
1557
+ return fCallback();
1558
+ });
1559
+
1560
+ _SuperTest
1561
+ .put('1.0/Books')
1562
+ .send([
1563
+ { IDBook: 3, Title: 'Neuromancer (batch post)' },
1564
+ ])
1565
+ .end(
1566
+ (pError, pResponse) =>
1567
+ {
1568
+ Expect(_CallCount).to.equal(1);
1569
+ Expect(_SawUpdatedRecords).to.equal(true);
1570
+
1571
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['UpdateBulk-PostOperation'];
1572
+ fDone();
1573
+ }
1574
+ );
1575
+ }
1576
+ );
1577
+ test
1578
+ (
1579
+ 'setBehavior: UpsertBulk-PreOperation fires once before the batch',
1580
+ function (fDone)
1581
+ {
1582
+ let _CallCount = 0;
1583
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('UpsertBulk-PreOperation',
1584
+ (pRequest, pRequestState, fCallback) =>
1585
+ {
1586
+ _CallCount++;
1587
+ Expect(Array.isArray(pRequestState.BulkRecords)).to.equal(true);
1588
+ return fCallback();
1589
+ });
1590
+
1591
+ _SuperTest
1592
+ .put('1.0/Book/Upserts')
1593
+ .send([
1594
+ { Title: 'Bulk Upsert New 1' },
1595
+ { Title: 'Bulk Upsert New 2' },
1596
+ ])
1597
+ .end(
1598
+ (pError, pResponse) =>
1599
+ {
1600
+ Expect(_CallCount).to.equal(1);
1601
+
1602
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['UpsertBulk-PreOperation'];
1603
+ fDone();
1604
+ }
1605
+ );
1606
+ }
1607
+ );
1608
+ test
1609
+ (
1610
+ 'setBehavior: UpsertBulk-PostOperation fires once after the batch',
1611
+ function (fDone)
1612
+ {
1613
+ let _CallCount = 0;
1614
+ let _SawUpserted = false;
1615
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('UpsertBulk-PostOperation',
1616
+ (pRequest, pRequestState, fCallback) =>
1617
+ {
1618
+ _CallCount++;
1619
+ _SawUpserted = Array.isArray(pRequestState.UpsertedRecords) && pRequestState.UpsertedRecords.length > 0;
1620
+ return fCallback();
1621
+ });
1622
+
1623
+ _SuperTest
1624
+ .put('1.0/Book/Upserts')
1625
+ .send([
1626
+ { Title: 'Bulk Upsert Post 1' },
1627
+ ])
1628
+ .end(
1629
+ (pError, pResponse) =>
1630
+ {
1631
+ Expect(_CallCount).to.equal(1);
1632
+ Expect(_SawUpserted).to.equal(true);
1633
+
1634
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['UpsertBulk-PostOperation'];
1635
+ fDone();
1636
+ }
1637
+ );
1638
+ }
1639
+ );
1640
+
1641
+ // ==================================================================
1642
+ // Coverage backfill for hooks that were present but previously
1643
+ // untested. Each test registers a handler, fires the endpoint,
1644
+ // and asserts the hook observed the expected state. These also
1645
+ // act as stage-semantics documentation for the hook surface.
1646
+ // ==================================================================
1647
+
1648
+ test
1649
+ (
1650
+ 'setBehavior: Create-QueryConfiguration fires after Query.addRecord',
1651
+ function (fDone)
1652
+ {
1653
+ let _Seen = null;
1654
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-QueryConfiguration',
1655
+ (pRequest, pRequestState, fCallback) =>
1656
+ {
1657
+ _Seen = {
1658
+ hasQuery: !!pRequestState.Query,
1659
+ hasRecordToCreate: !!pRequestState.RecordToCreate,
1660
+ };
1661
+ return fCallback();
1662
+ });
1663
+
1664
+ _SuperTest
1665
+ .post('1.0/Book')
1666
+ .send({ Title: 'Query-Config hook target' })
1667
+ .end(
1668
+ (pError, pResponse) =>
1669
+ {
1670
+ Expect(_Seen).to.deep.equal({ hasQuery: true, hasRecordToCreate: true });
1671
+
1672
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-QueryConfiguration'];
1673
+ fDone();
1674
+ }
1675
+ );
1676
+ }
1677
+ );
1678
+ test
1679
+ (
1680
+ 'setBehavior: Create-PostOperation sees the freshly-inserted Record',
1681
+ function (fDone)
1682
+ {
1683
+ let _SeenIDBook = null;
1684
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Create-PostOperation',
1685
+ (pRequest, pRequestState, fCallback) =>
1686
+ {
1687
+ _SeenIDBook = pRequestState.Record && pRequestState.Record.IDBook;
1688
+ pRequestState.Record.PostOpTag = 'was-here';
1689
+ return fCallback();
1690
+ });
1691
+
1692
+ _SuperTest
1693
+ .post('1.0/Book')
1694
+ .send({ Title: 'Post-op hook target' })
1695
+ .end(
1696
+ (pError, pResponse) =>
1697
+ {
1698
+ const tmpResult = JSON.parse(pResponse.text);
1699
+ Expect(_SeenIDBook).to.be.above(0);
1700
+ Expect(tmpResult.PostOpTag).to.equal('was-here');
1701
+
1702
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Create-PostOperation'];
1703
+ fDone();
1704
+ }
1705
+ );
1706
+ }
1707
+ );
1708
+ test
1709
+ (
1710
+ 'setBehavior: CreateBulk-PreOperation fires once with RecordsToBulkCreate',
1711
+ function (fDone)
1712
+ {
1713
+ let _CallCount = 0;
1714
+ let _SawRecords = false;
1715
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('CreateBulk-PreOperation',
1716
+ (pRequest, pRequestState, fCallback) =>
1717
+ {
1718
+ _CallCount++;
1719
+ _SawRecords = Array.isArray(pRequest.RecordsToBulkCreate);
1720
+ return fCallback();
1721
+ });
1722
+
1723
+ _SuperTest
1724
+ .post('1.0/Books')
1725
+ .send([
1726
+ { Title: 'Bulk Create 1' },
1727
+ { Title: 'Bulk Create 2' },
1728
+ ])
1729
+ .end(
1730
+ (pError, pResponse) =>
1731
+ {
1732
+ Expect(_CallCount).to.equal(1);
1733
+ Expect(_SawRecords).to.equal(true);
1734
+
1735
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['CreateBulk-PreOperation'];
1736
+ fDone();
1737
+ }
1738
+ );
1739
+ }
1740
+ );
1741
+ test
1742
+ (
1743
+ 'setBehavior: CreateBulk-PostOperation fires once with CreatedRecords',
1744
+ function (fDone)
1745
+ {
1746
+ let _SawCreated = null;
1747
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('CreateBulk-PostOperation',
1748
+ (pRequest, pRequestState, fCallback) =>
1749
+ {
1750
+ _SawCreated = Array.isArray(pRequestState.CreatedRecords) && pRequestState.CreatedRecords.length;
1751
+ return fCallback();
1752
+ });
1753
+
1754
+ _SuperTest
1755
+ .post('1.0/Books')
1756
+ .send([
1757
+ { Title: 'Bulk Create Post 1' },
1758
+ ])
1759
+ .end(
1760
+ (pError, pResponse) =>
1761
+ {
1762
+ Expect(_SawCreated).to.equal(1);
1763
+
1764
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['CreateBulk-PostOperation'];
1765
+ fDone();
1766
+ }
1767
+ );
1768
+ }
1769
+ );
1770
+ test
1771
+ (
1772
+ 'setBehavior: Delete-QueryConfiguration can scope the delete query',
1773
+ function (fDone)
1774
+ {
1775
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Delete-QueryConfiguration',
1776
+ (pRequest, pRequestState, fCallback) =>
1777
+ {
1778
+ Expect(pRequestState.Query).to.exist;
1779
+ return fCallback();
1780
+ });
1781
+
1782
+ // Use a record we don't care about the state of; create one inline.
1783
+ _SuperTest
1784
+ .post('1.0/Book')
1785
+ .send({ Title: 'Delete-Query target' })
1786
+ .end(
1787
+ (pPostErr, pPostRes) =>
1788
+ {
1789
+ const tmpID = JSON.parse(pPostRes.text).IDBook;
1790
+ _SuperTest
1791
+ .delete(`1.0/Book/${tmpID}`)
1792
+ .end(
1793
+ (pDelErr, pDelRes) =>
1794
+ {
1795
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Delete-QueryConfiguration'];
1796
+ fDone();
1797
+ }
1798
+ );
1799
+ }
1800
+ );
1801
+ }
1802
+ );
1803
+ test
1804
+ (
1805
+ 'setBehavior: Delete-PostOperation fires after soft-delete',
1806
+ function (fDone)
1807
+ {
1808
+ let _HookFired = false;
1809
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Delete-PostOperation',
1810
+ (pRequest, pRequestState, fCallback) =>
1811
+ {
1812
+ _HookFired = true;
1813
+ return fCallback();
1814
+ });
1815
+
1816
+ _SuperTest
1817
+ .post('1.0/Book')
1818
+ .send({ Title: 'Delete-Post target' })
1819
+ .end(
1820
+ (pPostErr, pPostRes) =>
1821
+ {
1822
+ const tmpID = JSON.parse(pPostRes.text).IDBook;
1823
+ _SuperTest
1824
+ .delete(`1.0/Book/${tmpID}`)
1825
+ .end(
1826
+ () =>
1827
+ {
1828
+ Expect(_HookFired).to.equal(true);
1829
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Delete-PostOperation'];
1830
+ fDone();
1831
+ }
1832
+ );
1833
+ }
1834
+ );
1835
+ }
1836
+ );
1837
+ test
1838
+ (
1839
+ 'setBehavior: Read-QueryConfiguration runs after filter is applied',
1840
+ function (fDone)
1841
+ {
1842
+ let _SeenCriteria = null;
1843
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Read-QueryConfiguration',
1844
+ (pRequest, pRequestState, fCallback) =>
1845
+ {
1846
+ _SeenCriteria = pRequestState.RecordSearchCriteria;
1847
+ return fCallback();
1848
+ });
1849
+
1850
+ _SuperTest
1851
+ .get('1.0/Book/1')
1852
+ .end(
1853
+ () =>
1854
+ {
1855
+ Expect(_SeenCriteria).to.be.a('string').and.satisfy((pS) => pS.includes('IDBook'));
1856
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Read-QueryConfiguration'];
1857
+ fDone();
1858
+ }
1859
+ );
1860
+ }
1861
+ );
1862
+ test
1863
+ (
1864
+ 'setBehavior: Reads-PostOperation fires after list load',
1865
+ function (fDone)
1866
+ {
1867
+ let _SawRecords = false;
1868
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Reads-PostOperation',
1869
+ (pRequest, pRequestState, fCallback) =>
1870
+ {
1871
+ _SawRecords = Array.isArray(pRequestState.Records) && pRequestState.Records.length > 0;
1872
+ return fCallback();
1873
+ });
1874
+
1875
+ _SuperTest
1876
+ .get('1.0/Books/0/10')
1877
+ .end(
1878
+ () =>
1879
+ {
1880
+ Expect(_SawRecords).to.equal(true);
1881
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Reads-PostOperation'];
1882
+ fDone();
1883
+ }
1884
+ );
1885
+ }
1886
+ );
1887
+ test
1888
+ (
1889
+ 'setBehavior: ReadMax-QueryConfiguration fires with column name',
1890
+ function (fDone)
1891
+ {
1892
+ let _SeenColumn = null;
1893
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('ReadMax-QueryConfiguration',
1894
+ (pRequest, pRequestState, fCallback) =>
1895
+ {
1896
+ _SeenColumn = pRequestState.ColumnName;
1897
+ return fCallback();
1898
+ });
1899
+
1900
+ _SuperTest
1901
+ .get('1.0/Book/Max/PublicationYear')
1902
+ .end(
1903
+ () =>
1904
+ {
1905
+ Expect(_SeenColumn).to.equal('PublicationYear');
1906
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['ReadMax-QueryConfiguration'];
1907
+ fDone();
1908
+ }
1909
+ );
1910
+ }
1911
+ );
1912
+ test
1913
+ (
1914
+ 'setBehavior: ReadMax-PostOperation fires after max lookup',
1915
+ function (fDone)
1916
+ {
1917
+ let _HookFired = false;
1918
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('ReadMax-PostOperation',
1919
+ (pRequest, pRequestState, fCallback) =>
1920
+ {
1921
+ _HookFired = true;
1922
+ return fCallback();
1923
+ });
1924
+
1925
+ _SuperTest
1926
+ .get('1.0/Book/Max/PublicationYear')
1927
+ .end(
1928
+ () =>
1929
+ {
1930
+ Expect(_HookFired).to.equal(true);
1931
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['ReadMax-PostOperation'];
1932
+ fDone();
1933
+ }
1934
+ );
1935
+ }
1936
+ );
1937
+ test
1938
+ (
1939
+ 'setBehavior: CountBy-QueryConfiguration can modify count-by query',
1940
+ function (fDone)
1941
+ {
1942
+ let _HookFired = false;
1943
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('CountBy-QueryConfiguration',
1944
+ (pRequest, pRequestState, fCallback) =>
1945
+ {
1946
+ _HookFired = true;
1947
+ Expect(pRequestState.Query).to.exist;
1948
+ return fCallback();
1949
+ });
1950
+
1951
+ _SuperTest
1952
+ .get('1.0/Books/Count/By/Genre/Thriller')
1953
+ .end(
1954
+ () =>
1955
+ {
1956
+ Expect(_HookFired).to.equal(true);
1957
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['CountBy-QueryConfiguration'];
1958
+ fDone();
1959
+ }
1960
+ );
1961
+ }
1962
+ );
1963
+ test
1964
+ (
1965
+ 'setBehavior: Undelete-PreOperation fires before undelete',
1966
+ function (fDone)
1967
+ {
1968
+ let _HookFired = false;
1969
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Undelete-PreOperation',
1970
+ (pRequest, pRequestState, fCallback) =>
1971
+ {
1972
+ _HookFired = true;
1973
+ return fCallback();
1974
+ });
1975
+
1976
+ // Create, delete, then undelete to hit the hook.
1977
+ _SuperTest
1978
+ .post('1.0/Book')
1979
+ .send({ Title: 'Undelete-Pre target' })
1980
+ .end(
1981
+ (pPostErr, pPostRes) =>
1982
+ {
1983
+ const tmpID = JSON.parse(pPostRes.text).IDBook;
1984
+ _SuperTest
1985
+ .delete(`1.0/Book/${tmpID}`)
1986
+ .end(
1987
+ () =>
1988
+ {
1989
+ _SuperTest
1990
+ .get(`1.0/Book/Undelete/${tmpID}`)
1991
+ .end(
1992
+ () =>
1993
+ {
1994
+ Expect(_HookFired).to.equal(true);
1995
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Undelete-PreOperation'];
1996
+ fDone();
1997
+ }
1998
+ );
1999
+ }
2000
+ );
2001
+ }
2002
+ );
2003
+ }
2004
+ );
2005
+ test
2006
+ (
2007
+ 'setBehavior: Undelete-PostOperation fires after undelete',
2008
+ function (fDone)
2009
+ {
2010
+ let _HookFired = false;
2011
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Undelete-PostOperation',
2012
+ (pRequest, pRequestState, fCallback) =>
2013
+ {
2014
+ _HookFired = true;
2015
+ return fCallback();
2016
+ });
2017
+
2018
+ _SuperTest
2019
+ .post('1.0/Book')
2020
+ .send({ Title: 'Undelete-Post target' })
2021
+ .end(
2022
+ (pPostErr, pPostRes) =>
2023
+ {
2024
+ const tmpID = JSON.parse(pPostRes.text).IDBook;
2025
+ _SuperTest
2026
+ .delete(`1.0/Book/${tmpID}`)
2027
+ .end(
2028
+ () =>
2029
+ {
2030
+ _SuperTest
2031
+ .get(`1.0/Book/Undelete/${tmpID}`)
2032
+ .end(
2033
+ () =>
2034
+ {
2035
+ Expect(_HookFired).to.equal(true);
2036
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Undelete-PostOperation'];
2037
+ fDone();
2038
+ }
2039
+ );
2040
+ }
2041
+ );
2042
+ }
2043
+ );
2044
+ }
2045
+ );
2046
+ test
2047
+ (
2048
+ 'setBehavior: Schema-PreOperation fires before schema render',
2049
+ function (fDone)
2050
+ {
2051
+ let _HookFired = false;
2052
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Schema-PreOperation',
2053
+ (pRequest, pRequestState, fCallback) =>
2054
+ {
2055
+ _HookFired = true;
2056
+ return fCallback();
2057
+ });
2058
+
2059
+ _SuperTest
2060
+ .get('1.0/Book/Schema')
2061
+ .end(
2062
+ () =>
2063
+ {
2064
+ Expect(_HookFired).to.equal(true);
2065
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Schema-PreOperation'];
2066
+ fDone();
2067
+ }
2068
+ );
2069
+ }
2070
+ );
2071
+ test
2072
+ (
2073
+ 'setBehavior: Validate-PreOperation fires before validate',
2074
+ function (fDone)
2075
+ {
2076
+ let _HookFired = false;
2077
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Validate-PreOperation',
2078
+ (pRequest, pRequestState, fCallback) =>
2079
+ {
2080
+ _HookFired = true;
2081
+ return fCallback();
2082
+ });
2083
+
2084
+ _SuperTest
2085
+ .post('1.0/Book/Schema/Validate')
2086
+ .send({ Title: 'Validate target' })
2087
+ .end(
2088
+ () =>
2089
+ {
2090
+ Expect(_HookFired).to.equal(true);
2091
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Validate-PreOperation'];
2092
+ fDone();
2093
+ }
2094
+ );
2095
+ }
2096
+ );
2097
+ test
2098
+ (
2099
+ 'setBehavior: Validate-PostOperation fires after validate',
2100
+ function (fDone)
2101
+ {
2102
+ let _HookFired = false;
2103
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('Validate-PostOperation',
2104
+ (pRequest, pRequestState, fCallback) =>
2105
+ {
2106
+ _HookFired = true;
2107
+ return fCallback();
2108
+ });
2109
+
2110
+ _SuperTest
2111
+ .post('1.0/Book/Schema/Validate')
2112
+ .send({ Title: 'Validate target 2' })
2113
+ .end(
2114
+ () =>
2115
+ {
2116
+ Expect(_HookFired).to.equal(true);
2117
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['Validate-PostOperation'];
2118
+ fDone();
2119
+ }
2120
+ );
2121
+ }
2122
+ );
2123
+ test
2124
+ (
2125
+ 'setBehavior: New-PreOperation fires before empty record render',
2126
+ function (fDone)
2127
+ {
2128
+ let _HookFired = false;
2129
+ _MeadowEndpoints.controller.BehaviorInjection.setBehavior('New-PreOperation',
2130
+ (pRequest, pRequestState, fCallback) =>
2131
+ {
2132
+ _HookFired = true;
2133
+ return fCallback();
2134
+ });
2135
+
2136
+ _SuperTest
2137
+ .get('1.0/Book/Schema/New')
2138
+ .end(
2139
+ () =>
2140
+ {
2141
+ Expect(_HookFired).to.equal(true);
2142
+ delete _MeadowEndpoints.controller.BehaviorInjection._BehaviorFunctions['New-PreOperation'];
2143
+ fDone();
2144
+ }
2145
+ );
2146
+ }
2147
+ );
1411
2148
  }
1412
2149
  );
1413
2150
 
@@ -1667,5 +2404,337 @@ suite
1667
2404
  );
1668
2405
  }
1669
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
+ );
1670
2739
  }
1671
2740
  );