@ryuzaki13/react-foundation-ui 1.8.11 → 1.8.13

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 (281) hide show
  1. package/dist/chunks/defaultRenderers-u_Vep6Pj.js.map +1 -1
  2. package/dist/chunks/multi-select-DtmRtX6v.js.map +1 -1
  3. package/dist/chunks/select-MN6zDPKM.js.map +1 -1
  4. package/dist/chunks/useODataSelectBase-DR4PrY1j.js.map +1 -1
  5. package/dist/multi-select/model/useODataMultiSelect.d.ts +1 -1
  6. package/dist/multi-select/model/useODataMultiSelect.d.ts.map +1 -1
  7. package/dist/multi-select/ui/MultiSelect.d.ts +1 -1
  8. package/dist/multi-select/ui/MultiSelect.d.ts.map +1 -1
  9. package/dist/multi-select/ui/SerializableMultiSelect.d.ts.map +1 -1
  10. package/dist/multi-select/ui/defaultRenderers.d.ts +1 -1
  11. package/dist/multi-select/ui/defaultRenderers.d.ts.map +1 -1
  12. package/dist/select/model/useODataSelect.d.ts +1 -1
  13. package/dist/select/model/useODataSelect.d.ts.map +1 -1
  14. package/dist/select/model/useODataSelectBase.d.ts +1 -1
  15. package/dist/select/model/useODataSelectBase.d.ts.map +1 -1
  16. package/dist/tree-select/index.js.map +1 -1
  17. package/dist/tree-select/model/buildODataTreeNodes.d.ts +1 -1
  18. package/dist/tree-select/model/buildODataTreeNodes.d.ts.map +1 -1
  19. package/package.json +3 -3
  20. package/storybook-static/assets/{Badge-BojMuOws.js → Badge-BFhBl7yM.js} +1 -1
  21. package/storybook-static/assets/{Badge.stories-C9gJRuIO.js → Badge.stories-BEu55X4s.js} +1 -1
  22. package/storybook-static/assets/{BadgeList-dNEWI7-O.js → BadgeList-mqmnGtRR.js} +1 -1
  23. package/storybook-static/assets/{Button-Bbt3JzIY.js → Button-CESdnWrO.js} +1 -1
  24. package/storybook-static/assets/{Button-CrTHXWYk.js → Button-_YPAu9sd.js} +1 -1
  25. package/storybook-static/assets/{Button.stories-BaYXOBeF.js → Button.stories-B0Y_T17B.js} +1 -1
  26. package/storybook-static/assets/{CalendarView-Dp0zus1o.js → CalendarView-DLuO2Ywz.js} +1 -1
  27. package/storybook-static/assets/{CheckBox-BiK9yWNt.js → CheckBox-CGiNmAPT.js} +1 -1
  28. package/storybook-static/assets/{CheckBox-UzYHaD6S.js → CheckBox-l0pbBm9z.js} +1 -1
  29. package/storybook-static/assets/{CheckBox.stories-C2BOiWZj.js → CheckBox.stories-ROPAnqh8.js} +1 -1
  30. package/storybook-static/assets/{Color-6AOWCN3U-C5S4wUlD.js → Color-6AOWCN3U-kPaRN0HS.js} +1 -1
  31. package/storybook-static/assets/{ContextMenu-D9EZxb7d.js → ContextMenu-Bob4DtRn.js} +1 -1
  32. package/storybook-static/assets/{ContextMenu-BWQln_tb.js → ContextMenu-DxfUAeJS.js} +1 -1
  33. package/storybook-static/assets/{ContextMenu.stories-U38zPh9_.js → ContextMenu.stories-DCJmWy5n.js} +1 -1
  34. package/storybook-static/assets/{CustomOptionButton-COIOrxQn.js → CustomOptionButton-DuEz9CXt.js} +1 -1
  35. package/storybook-static/assets/DateInput-BQ8SoeLE.js +1 -0
  36. package/storybook-static/assets/{DateInput-C-entb-r.js → DateInput-DmNLD4Dz.js} +1 -1
  37. package/storybook-static/assets/{DateInput.stories-NmUXQLDU.js → DateInput.stories-sTpYf7Ob.js} +1 -1
  38. package/storybook-static/assets/{DateRangePresetSelect-BM1L1KOo.js → DateRangePresetSelect-DRq4eJ2n.js} +1 -1
  39. package/storybook-static/assets/{DateRangePresetSelect-CAJzxaq0.js → DateRangePresetSelect-fgJfLaLO.js} +1 -1
  40. package/storybook-static/assets/{DateRangePresetSelect.stories-CC47XLnD.js → DateRangePresetSelect.stories-ib7Qh_BB.js} +1 -1
  41. package/storybook-static/assets/{Detail-CDluXtts.js → Detail-CJniLn1R.js} +1 -1
  42. package/storybook-static/assets/{Detail.stories-DjniLC4n.js → Detail.stories-BXjdNaL-.js} +1 -1
  43. package/storybook-static/assets/{Dialog-bzRSgGU7.js → Dialog-D6a_qzr3.js} +1 -1
  44. package/storybook-static/assets/{Dialog-bw6cHUsz.js → Dialog-DOYnnXlU.js} +1 -1
  45. package/storybook-static/assets/{Dialog.stories-C6Rs6s_p.js → Dialog.stories-B1cZLRK3.js} +1 -1
  46. package/storybook-static/assets/{Disclosure-DelA6FsQ.js → Disclosure-Chh5o_0s.js} +1 -1
  47. package/storybook-static/assets/{Disclosure.stories-D7yUinyb.js → Disclosure.stories-Cvdvf7V8.js} +1 -1
  48. package/storybook-static/assets/{DisclosureGroup-DbK7mU_Q.js → DisclosureGroup-CRnQOgVj.js} +1 -1
  49. package/storybook-static/assets/{DocsRenderer-JROSPFPF-v3z0zVN1.js → DocsRenderer-JROSPFPF-BBiErkeC.js} +1 -1
  50. package/storybook-static/assets/{DropZone-CI3NW4AC.js → DropZone-Bm5gqdi9.js} +1 -1
  51. package/storybook-static/assets/{DropZone-Cyy-ttEO.js → DropZone-C4L7JAlq.js} +1 -1
  52. package/storybook-static/assets/{DropZone.stories-S0HSpGTB.js → DropZone.stories-BhA9q8Qv.js} +1 -1
  53. package/storybook-static/assets/{DropdownMenu-Ct1bSZsE.js → DropdownMenu-DtZNSUHF.js} +1 -1
  54. package/storybook-static/assets/{ExpandableActionPanel-Oj3xlasj.js → ExpandableActionPanel-0ugXlJy_.js} +1 -1
  55. package/storybook-static/assets/{ExpandableActionPanel.stories-Ch7CLkpR.js → ExpandableActionPanel.stories-CLgMlA9k.js} +1 -1
  56. package/storybook-static/assets/{Flex-UiFm8Tkc.js → Flex-CJlia-8f.js} +1 -1
  57. package/storybook-static/assets/{Flex.stories-CaDiVlBD.js → Flex.stories-DCvfi1EY.js} +1 -1
  58. package/storybook-static/assets/{FloatingPopover-CTYV9W7T.js → FloatingPopover-CsnfPiwV.js} +1 -1
  59. package/storybook-static/assets/{FormattedTableCell-D-ysSR3p.js → FormattedTableCell-Dz5xWVYO.js} +1 -1
  60. package/storybook-static/assets/{Grid-CigBXPe2.js → Grid-C-KwTRAM.js} +1 -1
  61. package/storybook-static/assets/{Grid.stories-ay3z9DQZ.js → Grid.stories-CzbqknNR.js} +1 -1
  62. package/storybook-static/assets/{ImageCropper.stories-Dkce_ugb.js → ImageCropper.stories-DaE-gm-b.js} +1 -1
  63. package/storybook-static/assets/{ImageRenderer-C9_EmGBW.js → ImageRenderer-IsNCXCrZ.js} +1 -1
  64. package/storybook-static/assets/{ImageRenderer.stories-C7bXaNBI.js → ImageRenderer.stories-BbsjUEfJ.js} +1 -1
  65. package/storybook-static/assets/{ImageView.stories-D-ZzGlk8.js → ImageView.stories-DY0UITYz.js} +1 -1
  66. package/storybook-static/assets/{ImageViewer.stories-ItcRfxyy.js → ImageViewer.stories--9q9b9nv.js} +2 -2
  67. package/storybook-static/assets/{ImageViewerRuntime-eaupID36.js → ImageViewerRuntime-CNEH3XGN.js} +1 -1
  68. package/storybook-static/assets/{Input-op4j7Beh.js → Input-DLubbE7P.js} +1 -1
  69. package/storybook-static/assets/{Input-BUDNR96c.js → Input-Dw2JblYt.js} +1 -1
  70. package/storybook-static/assets/{Input.stories-DwulvJIg.js → Input.stories-DQeyDULG.js} +1 -1
  71. package/storybook-static/assets/{InputDateTime-B_cxOp6k.js → InputDateTime-BRzl8Vrt.js} +1 -1
  72. package/storybook-static/assets/{InputDateTime.stories-BueLF4HC.js → InputDateTime.stories-BJTj5v8J.js} +1 -1
  73. package/storybook-static/assets/{InputFile-DSg7sugm.js → InputFile-B8UroZji.js} +1 -1
  74. package/storybook-static/assets/{InputFile-C8z6TPc1.js → InputFile-DPftxjGw.js} +1 -1
  75. package/storybook-static/assets/{InputFile.stories-C6XSbIua.js → InputFile.stories-C7yyqHqc.js} +1 -1
  76. package/storybook-static/assets/{InputFiles-DoatLq3q.js → InputFiles-BQVXHVLy.js} +1 -1
  77. package/storybook-static/assets/{InputFiles-DeZ84Uu_.js → InputFiles-UD_9S7-N.js} +1 -1
  78. package/storybook-static/assets/{InputFiles.stories-BMbE8SJU.js → InputFiles.stories-DjjJS5pZ.js} +1 -1
  79. package/storybook-static/assets/{InputFilesList-CsfDDqaJ.js → InputFilesList-BNjijD3Y.js} +1 -1
  80. package/storybook-static/assets/{InputImage-yJSq98EH.js → InputImage-C-pHr-Sx.js} +1 -1
  81. package/storybook-static/assets/{InputImage-D_YnB2Tw.js → InputImage-DbcbqIhY.js} +1 -1
  82. package/storybook-static/assets/{InputImage.stories-nivUG307.js → InputImage.stories-D-TC9r9t.js} +1 -1
  83. package/storybook-static/assets/{InteractiveComponents-IPkC_Rkg.js → InteractiveComponents-DbhWgM9y.js} +1 -1
  84. package/storybook-static/assets/{InteractiveComponents.stories-D4VzVKZJ.js → InteractiveComponents.stories-B4mkVSea.js} +1 -1
  85. package/storybook-static/assets/{LayoutPicker-ZHkMi-LM.js → LayoutPicker-BoV_E3qt.js} +1 -1
  86. package/storybook-static/assets/{LayoutPicker-aMzPN5Do.js → LayoutPicker-w1EsRpYP.js} +1 -1
  87. package/storybook-static/assets/{LayoutPicker.stories-DAzpzxn5.js → LayoutPicker.stories--ChGnGxf.js} +1 -1
  88. package/storybook-static/assets/{Listbox-CR5nXC9J.js → Listbox-CzZRsr6a.js} +1 -1
  89. package/storybook-static/assets/{Listbox-BmfQOnrb.js → Listbox-qZdTi11b.js} +1 -1
  90. package/storybook-static/assets/{Listbox.stories-CeOpsKxB.js → Listbox.stories-C9k_1850.js} +1 -1
  91. package/storybook-static/assets/{Modal-BStp52Zo.js → Modal-DCpul8QM.js} +1 -1
  92. package/storybook-static/assets/{Modal.stories-CwGm5GYE.js → Modal.stories-Cy0Xd4LF.js} +1 -1
  93. package/storybook-static/assets/{ModalManagerProvider-B5uUXWF9.js → ModalManagerProvider-BrPVMcHV.js} +1 -1
  94. package/storybook-static/assets/{MultiSelect-308npb27.js → MultiSelect-BEQdl-F4.js} +1 -1
  95. package/storybook-static/assets/{MultiSelect-BDs02MZh.js → MultiSelect-Dy4X94E2.js} +1 -1
  96. package/storybook-static/assets/{MultiSelect.stories-DRi_iTp4.js → MultiSelect.stories-FuzejJmz.js} +1 -1
  97. package/storybook-static/assets/{NoData-ovaQEE9W.js → NoData-BsQ9EfkA.js} +1 -1
  98. package/storybook-static/assets/{NotificationsHost-BpeMWhLY.js → NotificationsHost-BSrODvw7.js} +1 -1
  99. package/storybook-static/assets/NotificationsHost.stories-DeiWspcY.js +29 -0
  100. package/storybook-static/assets/{ODataMultiSelect-qlft9L9L.js → ODataMultiSelect-BEz9gcd2.js} +1 -1
  101. package/storybook-static/assets/{ODataMultiSelect.stories-CMPIoPiy.js → ODataMultiSelect.stories-BwMgAkbE.js} +2 -2
  102. package/storybook-static/assets/{ODataSelect-DKBQArAK.js → ODataSelect-BgC7GHOv.js} +1 -1
  103. package/storybook-static/assets/{ODataSelect.stories-DxYIhtwe.js → ODataSelect.stories-DEIRAB-s.js} +2 -2
  104. package/storybook-static/assets/{ODataTreeMultiSelect-DAt6THP3.js → ODataTreeMultiSelect-WyZZuX0_.js} +1 -1
  105. package/storybook-static/assets/{ODataTreeMultiSelect.stories-CBNJP0mP.js → ODataTreeMultiSelect.stories-B6cxuagy.js} +1 -1
  106. package/storybook-static/assets/{ODataTreeSelect-Bzzt4CQC.js → ODataTreeSelect-OJGi3-S7.js} +1 -1
  107. package/storybook-static/assets/{ODataTreeSelect.stories-CWVvuqOt.js → ODataTreeSelect.stories-wGRGErCH.js} +1 -1
  108. package/storybook-static/assets/{OneStepScroller-BY5lz8qi.js → OneStepScroller-DycQ3RuL.js} +1 -1
  109. package/storybook-static/assets/{Option-Rw8xwrPg.js → Option-taxbFfjo.js} +1 -1
  110. package/storybook-static/assets/{OptionButton-Br48w_Zg.js → OptionButton-Q0QaPCII.js} +1 -1
  111. package/storybook-static/assets/{PeriodSelect-8-wpXz6w.js → PeriodSelect-DUJ2nF3F.js} +1 -1
  112. package/storybook-static/assets/{PeriodSelect-DxSRwKw-.js → PeriodSelect-DnM7x-uC.js} +1 -1
  113. package/storybook-static/assets/{PeriodSelect.stories-B-qgUzlS.js → PeriodSelect.stories-Db0bqe3J.js} +1 -1
  114. package/storybook-static/assets/{PickerTriggerActions-C5xmysFh.js → PickerTriggerActions-CGJd7xmR.js} +1 -1
  115. package/storybook-static/assets/{Popover-NJwt_vZZ.js → Popover-B6oidz5p.js} +1 -1
  116. package/storybook-static/assets/{Popover-Ds8PUigp.js → Popover-Tbhf8T81.js} +1 -1
  117. package/storybook-static/assets/{Popover.stories-CUOFA_Xf.js → Popover.stories-DE1LxZvv.js} +1 -1
  118. package/storybook-static/assets/{PresetRangeDateInput-DyOpbkON.js → PresetRangeDateInput-CQ5FfmrL.js} +1 -1
  119. package/storybook-static/assets/{PresetRangeDateInput-Xk0bOy3S.js → PresetRangeDateInput-DfgoR0My.js} +1 -1
  120. package/storybook-static/assets/{PresetRangeDateInput.stories-CwjUlXj0.js → PresetRangeDateInput.stories-CgoYAYBG.js} +1 -1
  121. package/storybook-static/assets/{PresetSelect-Ddmr0niY.js → PresetSelect-BPtAknAr.js} +1 -1
  122. package/storybook-static/assets/README--TpGcM95.js +110 -0
  123. package/storybook-static/assets/{README-MzcO5Z_P.js → README-4iBHWAP5.js} +1 -1
  124. package/storybook-static/assets/README-98KIgy5G.js +75 -0
  125. package/storybook-static/assets/README-B6II8p0W.js +154 -0
  126. package/storybook-static/assets/{README-CkWglKbS.js → README-B6vU5_7I.js} +1 -1
  127. package/storybook-static/assets/README-BFJVjjZR.js +325 -0
  128. package/storybook-static/assets/README-BIT4HlFP.js +441 -0
  129. package/storybook-static/assets/{README-DqQDgJN-.js → README-BK6Epio1.js} +1 -1
  130. package/storybook-static/assets/{README-DRnlD7y8.js → README-BRlPB5_k.js} +1 -1
  131. package/storybook-static/assets/README-BTFpEaXG.js +64 -0
  132. package/storybook-static/assets/README-BWhXgC_o.js +226 -0
  133. package/storybook-static/assets/README-BbNHvOoR.js +101 -0
  134. package/storybook-static/assets/README-BbunUo8b.js +118 -0
  135. package/storybook-static/assets/README-BmYpfZKr.js +1844 -0
  136. package/storybook-static/assets/README-Bu5uvhAT.js +230 -0
  137. package/storybook-static/assets/README-Bx6fvSBE.js +1282 -0
  138. package/storybook-static/assets/README-Bx6gIGiA.js +69 -0
  139. package/storybook-static/assets/README-ByhxsX44.js +324 -0
  140. package/storybook-static/assets/README-Byi1FFpw.js +98 -0
  141. package/storybook-static/assets/README-C3Is3a3B.js +208 -0
  142. package/storybook-static/assets/{README-C0BwTe5N.js → README-C6prwAXw.js} +1 -1
  143. package/storybook-static/assets/{README-BzVni2ve.js → README-C9S83J_G.js} +1 -1
  144. package/storybook-static/assets/{README-BHw6MVjD.js → README-CGgxP5Zo.js} +1 -1
  145. package/storybook-static/assets/README-CJ2ZVG2U.js +2124 -0
  146. package/storybook-static/assets/README-CNGCAm5d.js +95 -0
  147. package/storybook-static/assets/README-Cb_q420B.js +90 -0
  148. package/storybook-static/assets/README-CtIDPNJ3.js +186 -0
  149. package/storybook-static/assets/README-CzO3Dchr.js +64 -0
  150. package/storybook-static/assets/README-D0rzQthv.js +264 -0
  151. package/storybook-static/assets/README-DEQu7djz.js +1704 -0
  152. package/storybook-static/assets/README-DFihT_sm.js +296 -0
  153. package/storybook-static/assets/{README-CTIBG4zH.js → README-DMiWsyv_.js} +1 -1
  154. package/storybook-static/assets/README-DOx8lXXk.js +82 -0
  155. package/storybook-static/assets/{README-DzTc6XmY.js → README-DPhDjr43.js} +1 -1
  156. package/storybook-static/assets/{README-Dki94Nf2.js → README-D_NqWPa9.js} +1 -1
  157. package/storybook-static/assets/README-DhJ5Rb1w.js +355 -0
  158. package/storybook-static/assets/README-DlwhWCvQ.js +1146 -0
  159. package/storybook-static/assets/README-Dxi73a2Q.js +740 -0
  160. package/storybook-static/assets/README-Dycov6NP.js +583 -0
  161. package/storybook-static/assets/README-GTBkSG4_.js +3591 -0
  162. package/storybook-static/assets/README-Gfd9tLTw.js +672 -0
  163. package/storybook-static/assets/{README-B4zYJg6u.js → README-Kfawetke.js} +1 -1
  164. package/storybook-static/assets/{README-BzVHwvgo.js → README-UDBM7ZMT.js} +1 -1
  165. package/storybook-static/assets/{README-D-XjRtAE.js → README-VTWQxLm_.js} +1 -1
  166. package/storybook-static/assets/README-YHNWrTZv.js +400 -0
  167. package/storybook-static/assets/README-_0Otw0eD.js +448 -0
  168. package/storybook-static/assets/README-aw2Xu9xu.js +158 -0
  169. package/storybook-static/assets/README-eg1emlll.js +192 -0
  170. package/storybook-static/assets/README-iqQHdhZ4.js +416 -0
  171. package/storybook-static/assets/{README-BdPlV5nA.js → README-llxZAtCj.js} +1 -1
  172. package/storybook-static/assets/README-wFnEIGl4.js +95 -0
  173. package/storybook-static/assets/{README-Dx3cpFMr.js → README-webB1yH0.js} +1 -1
  174. package/storybook-static/assets/README-wtreGRCe.js +208 -0
  175. package/storybook-static/assets/{RadioButton-DWbkCY8u.js → RadioButton-CNOLDk_o.js} +1 -1
  176. package/storybook-static/assets/{RadioButton-oSvQ-gQh.js → RadioButton-Ce05_q7s.js} +1 -1
  177. package/storybook-static/assets/{RadioButton.stories-GNQ9XJZT.js → RadioButton.stories-CPd4lTl0.js} +1 -1
  178. package/storybook-static/assets/{RadioGroup-CvXjGXcV.js → RadioGroup-BtfD5Szd.js} +1 -1
  179. package/storybook-static/assets/{RadioGroup-eBYggurL.js → RadioGroup-Ccm7G-dv.js} +1 -1
  180. package/storybook-static/assets/{RadioGroup.stories-C77j_cPv.js → RadioGroup.stories-D-RRPHwi.js} +1 -1
  181. package/storybook-static/assets/{Scrollable-De6SdhI3.js → Scrollable-DvdBIeig.js} +1 -1
  182. package/storybook-static/assets/{Select-DQMoVjn4.js → Select-8PV9PBKF.js} +1 -1
  183. package/storybook-static/assets/{Select-Bwc0bEpt.js → Select-G04B41FM.js} +1 -1
  184. package/storybook-static/assets/{Select.stories-BSIJKJnM.js → Select.stories-Cf6CBhV8.js} +1 -1
  185. package/storybook-static/assets/{SerializableSelect-B73irNFR.js → SerializableSelect-CdQdHIVp.js} +1 -1
  186. package/storybook-static/assets/{SerializableSelect.stories-Z9q6g5v2.js → SerializableSelect.stories-0AKUGpbS.js} +1 -1
  187. package/storybook-static/assets/{SingleDateTimeInput-Dq9LcjnS.js → SingleDateTimeInput-DWT1ORhL.js} +1 -1
  188. package/storybook-static/assets/{SingleDateTimeInput-BHl6srqA.js → SingleDateTimeInput-DattLt6w.js} +1 -1
  189. package/storybook-static/assets/SingleDateTimeInput.stories-BLJV3jzV.js +48 -0
  190. package/storybook-static/assets/{Slider-CvM0N_kc.js → Slider-DKEI4cDd.js} +1 -1
  191. package/storybook-static/assets/{Slider-BiyKexKa.js → Slider-Dci-mJt_.js} +1 -1
  192. package/storybook-static/assets/{Slider.stories-CdHAMDmY.js → Slider.stories-Dp7oQueF.js} +1 -1
  193. package/storybook-static/assets/{SliderInput-DqHotbhB.js → SliderInput-BGPQmlgY.js} +1 -1
  194. package/storybook-static/assets/{SliderInput-Bx_oHUCV.js → SliderInput-BhjqGOKt.js} +1 -1
  195. package/storybook-static/assets/{SliderInput.stories-R5yDhghM.js → SliderInput.stories-DXNhRU3X.js} +1 -1
  196. package/storybook-static/assets/{Sortable-D7pjTHO0.js → Sortable-CMd9l1Gd.js} +1 -1
  197. package/storybook-static/assets/{Sortable.stories-CVLG6pSH.js → Sortable.stories-BN1cFEFJ.js} +1 -1
  198. package/storybook-static/assets/{Splitter-B1Du6XI2.js → Splitter-GL65TlI9.js} +1 -1
  199. package/storybook-static/assets/{Splitter.stories-D-FRLMCc.js → Splitter.stories-q5PlI6rB.js} +1 -1
  200. package/storybook-static/assets/{StateSelect-zl-yZibY.js → StateSelect-Byik5-iZ.js} +1 -1
  201. package/storybook-static/assets/{StateSelect-BJdUEUBc.js → StateSelect-CT0JF2V6.js} +1 -1
  202. package/storybook-static/assets/{StateSelect.stories-DgidR5zR.js → StateSelect.stories-pPEPmgnN.js} +1 -1
  203. package/storybook-static/assets/{StringToggle.stories-IucGrtIi.js → StringToggle.stories-BQlFmCE7.js} +1 -1
  204. package/storybook-static/assets/{Switch-BJrn2lIN.js → Switch--xGPiYk-.js} +1 -1
  205. package/storybook-static/assets/{Switch-5dCshvVo.js → Switch-DhaLI9CG.js} +1 -1
  206. package/storybook-static/assets/{Switch.stories-Dp_l3_JL.js → Switch.stories-5DisgIyj.js} +1 -1
  207. package/storybook-static/assets/{Table-BFAEcUde.js → Table-Bf6MicdG.js} +1 -1
  208. package/storybook-static/assets/{Table.stories-CohejRQv.js → Table.stories-CS7rQgj9.js} +2 -2
  209. package/storybook-static/assets/{Tabs-CI32QiHf.js → Tabs-B4y4y8Ap.js} +1 -1
  210. package/storybook-static/assets/{Tabs.stories-Cr9d9ipA.js → Tabs.stories-C0SpdGkh.js} +1 -1
  211. package/storybook-static/assets/{TabsBox-BlDOUDMF.js → TabsBox-C5LLLhnj.js} +1 -1
  212. package/storybook-static/assets/{TagInput-YZ7eSv8W.js → TagInput-D4BpuCoH.js} +1 -1
  213. package/storybook-static/assets/{TagInput.stories-DrOvsiI5.js → TagInput.stories-rn2wtZP_.js} +1 -1
  214. package/storybook-static/assets/{Text-DK4jN26J.js → Text-BhBt9e7C.js} +1 -1
  215. package/storybook-static/assets/{Textarea-CFh5f_lC.js → Textarea-B-Hz7ujq.js} +1 -1
  216. package/storybook-static/assets/{Textarea-DxPuWBZj.js → Textarea-BHHxY5sd.js} +1 -1
  217. package/storybook-static/assets/{Textarea.stories-CUeOGKYc.js → Textarea.stories-CZ2kWz20.js} +1 -1
  218. package/storybook-static/assets/{TimePanel-BUhFtwlb.js → TimePanel-Cg8fgFsn.js} +1 -1
  219. package/storybook-static/assets/{TimePanel.stories-CpXpMBDN.js → TimePanel.stories-BxdwtK2e.js} +1 -1
  220. package/storybook-static/assets/{Toggle-l2AvDbRn.js → Toggle-BWwfk9fi.js} +1 -1
  221. package/storybook-static/assets/{Toggle-BtApzBNH.js → Toggle-Ce9rbjBc.js} +1 -1
  222. package/storybook-static/assets/{Toggle.stories-Ca8otmAa.js → Toggle.stories-0fzcmzKy.js} +1 -1
  223. package/storybook-static/assets/{TreeMultiSelect-C9QDV63L.js → TreeMultiSelect-B06Y0lIc.js} +1 -1
  224. package/storybook-static/assets/{TreeMultiSelect-nXC4ZB5b.js → TreeMultiSelect-CrAdR_jM.js} +1 -1
  225. package/storybook-static/assets/{TreeMultiSelect.stories-6STRGJbF.js → TreeMultiSelect.stories-CAVoj56E.js} +1 -1
  226. package/storybook-static/assets/{TreePickerBase-Cq7fSw3x.js → TreePickerBase-qU2xPlar.js} +1 -1
  227. package/storybook-static/assets/{TreeSelect-CITef9rp.js → TreeSelect-BIHydJFy.js} +1 -1
  228. package/storybook-static/assets/{TreeSelect-rMNyu_KT.js → TreeSelect-BKnWu0C7.js} +1 -1
  229. package/storybook-static/assets/{TreeSelect.stories-B-Emz9yW.js → TreeSelect.stories-Dp_8ybAf.js} +1 -1
  230. package/storybook-static/assets/{TreeTable-CLiP0td3.js → TreeTable-pVXjU9dG.js} +1 -1
  231. package/storybook-static/assets/{TreeTable.stories-DJb9qt8k.js → TreeTable.stories-CV_8Ui6R.js} +2 -2
  232. package/storybook-static/assets/{blocks-DndFRxxW.js → blocks-BY40djCc.js} +3 -3
  233. package/storybook-static/assets/{check-CLkTJVXe.js → check-CVkPTDzp.js} +1 -1
  234. package/storybook-static/assets/{chevron-down-miCejSEn.js → chevron-down-BtNO4Vhe.js} +1 -1
  235. package/storybook-static/assets/{chevron-left-DhQIPej2.js → chevron-left-kceSK3sZ.js} +1 -1
  236. package/storybook-static/assets/{chevron-right-DfBBx-fr.js → chevron-right-BA_LO2El.js} +1 -1
  237. package/storybook-static/assets/{chunk-W22LQPXL-IdwKKJUw.js → chunk-W22LQPXL-yJiFPv6e.js} +1 -1
  238. package/storybook-static/assets/{circle-alert-DOmVf8am.js → circle-alert-B2m4a1zz.js} +1 -1
  239. package/storybook-static/assets/{circle-check-e7eSQ2VD.js → circle-check-Cfjhotlm.js} +1 -1
  240. package/storybook-static/assets/{components-Bm4Cw8Ap.js → components-D6yV8PLX.js} +3 -3
  241. package/storybook-static/assets/{contextMenu-DpIKeJju.js → contextMenu-B0pQBIW0.js} +1 -1
  242. package/storybook-static/assets/{defaultRenderers-m21aWJXz.js → defaultRenderers-DfmnIuod.js} +1 -1
  243. package/storybook-static/assets/{download-eAKjms6q.js → download-Lba8elK9.js} +1 -1
  244. package/storybook-static/assets/{floating-ui.react-DmilONq6.js → floating-ui.react-BFNvfMpU.js} +1 -1
  245. package/storybook-static/assets/formatters-D33Uh3si-CxrG0jsB.js +1 -0
  246. package/storybook-static/assets/foundationApi-DZhnQ2cC.js +59 -0
  247. package/storybook-static/assets/foundationLib-CoVTo22U.js +30 -0
  248. package/storybook-static/assets/{hooks-RI5_2AXI.js → hooks-Cc6eXN6U.js} +1 -1
  249. package/storybook-static/assets/iframe-BXM-JBuH.js +1 -0
  250. package/storybook-static/assets/{iframe-Sr0umHM6.js → iframe-o7sH2IXC.js} +4 -4
  251. package/storybook-static/assets/{info-DhJTzwcL.js → info-BpsVP_dM.js} +1 -1
  252. package/storybook-static/assets/odata-CXsbxsRY.js +1 -0
  253. package/storybook-static/assets/odata-service-CWkHASdw-DlmSiioz.js +3 -0
  254. package/storybook-static/assets/{odataStoryFixtures-lgnWIUp7.js → odataStoryFixtures-Dj_bzRqh.js} +1 -1
  255. package/storybook-static/assets/{react-YFbUKK0m.js → react-DeJ0JTHb.js} +1 -1
  256. package/storybook-static/assets/{refresh-cw-C7WnxX9M.js → refresh-cw-BvGyjEXe.js} +1 -1
  257. package/storybook-static/assets/{settings-2-0NVvyKQl.js → settings-2-BdXxnHos.js} +1 -1
  258. package/storybook-static/assets/{sortable-DDGG-Zfs.js → sortable-XkyN-9r5.js} +1 -1
  259. package/storybook-static/assets/{syntaxhighlighter-QEQV7K2Y-CxWm8nc8.js → syntaxhighlighter-QEQV7K2Y-Cafn3LV1.js} +1 -1
  260. package/storybook-static/assets/{trash-2-BnceCuhg.js → trash-2-uPH9-fND.js} +1 -1
  261. package/storybook-static/assets/{triangle-alert-MPi3GVgt.js → triangle-alert-tT01vSMw.js} +1 -1
  262. package/storybook-static/assets/{useODataSelectBase-hmjK6chS.js → useODataSelectBase-B2LlOwPZ.js} +1 -1
  263. package/storybook-static/assets/useODataTreeData-CU7Rqcb9.js +1 -0
  264. package/storybook-static/assets/{usePickerQuery-CtsGhieD.js → usePickerQuery-D8GHQ6KR.js} +1 -1
  265. package/storybook-static/assets/{usePickerTriggerController-EaI8W2G0.js → usePickerTriggerController-CcK1jmDy.js} +1 -1
  266. package/storybook-static/assets/{x-CqPSvWfo.js → x-BlAURbAH.js} +1 -1
  267. package/storybook-static/iframe.html +2 -2
  268. package/storybook-static/index.json +1 -1
  269. package/storybook-static/project.json +1 -1
  270. package/storybook-static/assets/DateInput-05Iy69_a.js +0 -1
  271. package/storybook-static/assets/NotificationsHost.stories-CgbFboVD.js +0 -29
  272. package/storybook-static/assets/SingleDateTimeInput.stories-C2RuBRAF.js +0 -48
  273. package/storybook-static/assets/async-SjiCjIjO.js +0 -625
  274. package/storybook-static/assets/formatters-CBgrBeSA-CqB1pZxS.js +0 -1
  275. package/storybook-static/assets/foundationLib-DRrox3tN.js +0 -25
  276. package/storybook-static/assets/iframe-DQo-3eW2.js +0 -1
  277. package/storybook-static/assets/odata-DhISMZkE.js +0 -1
  278. package/storybook-static/assets/odata-service-B6TxBSLm-Bq8Ya67E.js +0 -3
  279. package/storybook-static/assets/odataFetchFn-Dv6Xi5CI.js +0 -763
  280. package/storybook-static/assets/persisted-CUFiw53N.js +0 -600
  281. package/storybook-static/assets/useODataTreeData-CKuavbWc.js +0 -1
@@ -0,0 +1,3591 @@
1
+ import{n as e}from"./rolldown-runtime-DkW27tQK.js";import{n as t}from"./iframe-o7sH2IXC.js";import{i as n,r}from"./react-Bl2r1tuC.js";import{l as i,m as a}from"./blocks-BY40djCc.js";function o(e){let t={a:`a`,code:`code`,h1:`h1`,h2:`h2`,h3:`h3`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,table:`table`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,...n(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i,{title:`Foundation API/OData/OData V2 and SAP Gateway`}),`
2
+ `,(0,c.jsx)(t.h1,{id:`odata-v2-и-sap-gateway`,children:`OData V2 и SAP Gateway`}),`
3
+ `,(0,c.jsxs)(t.p,{children:[`Entrypoint `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-api/odata`}),` — основной API-модуль для browser-приложений, работающих с SAP Gateway и OData V2.`]}),`
4
+ `,(0,c.jsx)(t.p,{children:`Он объединяет:`}),`
5
+ `,(0,c.jsxs)(t.ul,{children:[`
6
+ `,(0,c.jsx)(t.li,{children:`HTTP transport с SAP headers, cookies, SSO recovery и X-CSRF token;`}),`
7
+ `,(0,c.jsx)(t.li,{children:`разбор стандартного OData V2 JSON envelope;`}),`
8
+ `,(0,c.jsx)(t.li,{children:`metadata-aware операции чтения и записи;`}),`
9
+ `,(0,c.jsx)(t.li,{children:`интеграцию с TanStack Query;`}),`
10
+ `,(0,c.jsxs)(t.li,{children:[`кеширование и обновление `,(0,c.jsx)(t.code,{children:`$metadata`}),`;`]}),`
11
+ `,(0,c.jsx)(t.li,{children:`загрузку code/text справочников;`}),`
12
+ `,(0,c.jsx)(t.li,{children:`модели зависимых справочников;`}),`
13
+ `,(0,c.jsx)(t.li,{children:`построение и обогащение колонок TanStack Table по metadata.`}),`
14
+ `]}),`
15
+ `,(0,c.jsx)(t.p,{children:`Документ рассчитан на разработчика, который не видит исходники пакета и только начинает работать с JavaScript, TypeScript, React, TanStack Query и OData.`}),`
16
+ `,(0,c.jsx)(t.h2,{id:`содержание`,children:`Содержание`}),`
17
+ `,(0,c.jsxs)(t.ul,{children:[`
18
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%87%D1%82%D0%BE-%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D1%82-%D0%B8-%D1%87%D0%B5%D0%B3%D0%BE-%D0%BD%D0%B5-%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D1%82-%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D1%8C`,children:`Что делает и чего не делает модуль`})}),`
19
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B8%D0%BC%D0%BF%D0%BE%D1%80%D1%82`,children:`Установка и импорт`})}),`
20
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8-%D0%B8-%D1%81%D1%80%D0%B5%D0%B4%D0%B0-%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F`,children:`Зависимости и среда выполнения`})}),`
21
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#odata-v2-%D0%BF%D1%80%D0%BE%D1%81%D1%82%D1%8B%D0%BC%D0%B8-%D1%81%D0%BB%D0%BE%D0%B2%D0%B0%D0%BC%D0%B8`,children:`OData V2 простыми словами`})}),`
22
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BA%D0%B0%D0%BA-%D0%B2%D1%8B%D0%B1%D1%80%D0%B0%D1%82%D1%8C-%D1%83%D1%80%D0%BE%D0%B2%D0%B5%D0%BD%D1%8C-api`,children:`Как выбрать уровень API`})}),`
23
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B1%D1%8B%D1%81%D1%82%D1%80%D1%8B%D0%B9-%D1%81%D1%82%D0%B0%D1%80%D1%82`,children:`Быстрый старт`})}),`
24
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BE%D0%B1%D1%89%D0%B8%D0%B9-%D0%BC%D0%B0%D1%80%D1%88%D1%80%D1%83%D1%82-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%B0`,children:`Общий маршрут запроса`})}),`
25
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-project-adapter`,children:`Инициализация project adapter`})}),`
26
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#base-url-%D0%B8-url-helpers`,children:`Base URL и URL helpers`})}),`
27
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BD%D0%B8%D0%B7%D0%BA%D0%BE%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5%D0%B2%D1%8B%D0%B9-sap-transport`,children:`Низкоуровневый SAP transport`})}),`
28
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#odata-v2-json-envelope`,children:`OData V2 JSON envelope`})}),`
29
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#%D0%BD%D0%B8%D0%B7%D0%BA%D0%BE%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5%D0%B2%D1%8B%D0%B9-odatafetch`,children:[`Низкоуровневый `,(0,c.jsx)(t.code,{children:`odataFetch`})]})}),`
30
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#metadata-aware-operation-helpers`,children:`Metadata-aware operation helpers`})}),`
31
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odataqueryfn-%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B8-%D0%B8-parameterized-query`,children:[(0,c.jsx)(t.code,{children:`odataQueryFn`}),`: списки и parameterized query`]})}),`
32
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odatareadfn-%D0%BE%D0%B4%D0%BD%D0%B0-entity`,children:[(0,c.jsx)(t.code,{children:`odataReadFn`}),`: одна entity`]})}),`
33
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odatacreatefn-%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5`,children:[(0,c.jsx)(t.code,{children:`odataCreateFn`}),`: создание`]})}),`
34
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odataupdatefn-%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5`,children:[(0,c.jsx)(t.code,{children:`odataUpdateFn`}),`: обновление`]})}),`
35
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odatadeletefn-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5`,children:[(0,c.jsx)(t.code,{children:`odataDeleteFn`}),`: удаление`]})}),`
36
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#odatafunctionimportfn-functionimport`,children:[(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`: FunctionImport`]})}),`
37
+ `,(0,c.jsx)(t.li,{children:(0,c.jsxs)(t.a,{href:`#autoparse-%D0%B8-transform`,children:[(0,c.jsx)(t.code,{children:`autoParse`}),` и `,(0,c.jsx)(t.code,{children:`transform`})]})}),`
38
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#tanstack-query-%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5`,children:`TanStack Query: чтение`})}),`
39
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#tanstack-query-mutations-%D0%B8-cache`,children:`TanStack Query: mutations и cache`})}),`
40
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#dev-dry-run-helpers`,children:`DEV dry-run helpers`})}),`
41
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#metadata-api`,children:`Metadata API`})}),`
42
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#entity-metadata-helpers`,children:`Entity metadata helpers`})}),`
43
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#odata-collections`,children:`OData collections`})}),`
44
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#collection-version-check-%D0%B8-service-worker-cache`,children:`Collection version-check и Service Worker cache`})}),`
45
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#collection-model-%D0%B4%D0%BB%D1%8F-ui`,children:`Collection model для UI`})}),`
46
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D1%8B%D0%B5-%D1%81%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA%D0%B8-%D0%B8-segments`,children:`Зависимые справочники и segments`})}),`
47
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#odata-metadata-%D0%B8-tanstack-table`,children:`OData metadata и TanStack Table`})}),`
48
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#sso-lifecycle`,children:`SSO lifecycle`})}),`
49
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#x-csrf-lifecycle`,children:`X-CSRF lifecycle`})}),`
50
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%87%D0%B5%D1%82%D1%8B%D1%80%D0%B5-%D1%80%D0%B0%D0%B7%D0%BD%D1%8B%D1%85-%D0%BA%D0%B5%D1%88%D0%B0`,children:`Четыре разных кеша`})}),`
51
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B8`,children:`Ошибки`})}),`
52
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D1%81%D1%82%D1%8C-%D0%B8-%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F`,children:`Безопасность и ограничения`})}),`
53
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#browser-ssr-%D0%B8-workers`,children:`Browser, SSR и workers`})}),`
54
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-consumer-%D0%BA%D0%BE%D0%B4%D0%B0`,children:`Тестирование consumer-кода`})}),`
55
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#deprecated-api`,children:`Deprecated API`})}),`
56
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BF%D0%BE%D0%BB%D0%BD%D0%B0%D1%8F-%D0%BA%D0%B0%D1%80%D1%82%D0%B0-%D0%BF%D1%83%D0%B1%D0%BB%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE-api`,children:`Полная карта публичного API`})}),`
57
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%87%D0%B0%D1%81%D1%82%D1%8B%D0%B5-%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81%D1%8B`,children:`Частые вопросы`})}),`
58
+ `,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%81%D0%B2%D1%8F%D0%B7%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D0%B8`,children:`Связанные модули`})}),`
59
+ `]}),`
60
+ `,(0,c.jsx)(t.h2,{id:`что-делает-и-чего-не-делает-модуль`,children:`Что делает и чего не делает модуль`}),`
61
+ `,(0,c.jsx)(t.h3,{id:`делает`,children:`Делает`}),`
62
+ `,(0,c.jsxs)(t.ul,{children:[`
63
+ `,(0,c.jsx)(t.li,{children:`Выполняет запросы к SAP Gateway и OData V2 endpoint.`}),`
64
+ `,(0,c.jsx)(t.li,{children:`Добавляет общие SAP headers.`}),`
65
+ `,(0,c.jsxs)(t.li,{children:[`Отправляет cookies через `,(0,c.jsx)(t.code,{children:`credentials: "include"`}),`.`]}),`
66
+ `,(0,c.jsx)(t.li,{children:`Определяет SAP client.`}),`
67
+ `,(0,c.jsx)(t.li,{children:`Получает, кеширует и обновляет X-CSRF token для write-запросов.`}),`
68
+ `,(0,c.jsx)(t.li,{children:`Распознаёт SSO redirect и SAML form.`}),`
69
+ `,(0,c.jsx)(t.li,{children:`Пытается восстановить SSO session в скрытом iframe.`}),`
70
+ `,(0,c.jsxs)(t.li,{children:[`Разбирает OData V2 envelope `,(0,c.jsx)(t.code,{children:`{ d: ... }`}),`.`]}),`
71
+ `,(0,c.jsxs)(t.li,{children:[`Извлекает `,(0,c.jsx)(t.code,{children:`__count`}),` как `,(0,c.jsx)(t.code,{children:`totalCount`}),`.`]}),`
72
+ `,(0,c.jsxs)(t.li,{children:[`Загружает и кеширует `,(0,c.jsx)(t.code,{children:`$metadata`}),`.`]}),`
73
+ `,(0,c.jsx)(t.li,{children:`Проверяет, является target Entity или FunctionImport.`}),`
74
+ `,(0,c.jsx)(t.li,{children:`Строит path операции по metadata.`}),`
75
+ `,(0,c.jsxs)(t.li,{children:[`Может преобразовать OData values по metadata через `,(0,c.jsx)(t.code,{children:`autoParse`}),`.`]}),`
76
+ `,(0,c.jsxs)(t.li,{children:[`Передаёт `,(0,c.jsx)(t.code,{children:`AbortSignal`}),` из TanStack Query в read-запрос.`]}),`
77
+ `,(0,c.jsx)(t.li,{children:`Даёт hooks для metadata, справочников и table columns.`}),`
78
+ `]}),`
79
+ `,(0,c.jsx)(t.h3,{id:`не-делает`,children:`Не делает`}),`
80
+ `,(0,c.jsxs)(t.ul,{children:[`
81
+ `,(0,c.jsx)(t.li,{children:`Не определяет бизнес-правила приложения.`}),`
82
+ `,(0,c.jsx)(t.li,{children:`Не знает, что такое «заказ», «вариант», «отчёт» или другой domain object.`}),`
83
+ `,(0,c.jsx)(t.li,{children:`Не проверяет authorization вместо SAP backend.`}),`
84
+ `,(0,c.jsx)(t.li,{children:`Не превращает TypeScript generic в runtime validation.`}),`
85
+ `,(0,c.jsx)(t.li,{children:`Не строит query key конкретной entity автоматически.`}),`
86
+ `,(0,c.jsx)(t.li,{children:`Не выбирает cache strategy после mutation автоматически.`}),`
87
+ `,(0,c.jsx)(t.li,{children:`Не хранит backend state в Zustand или React state.`}),`
88
+ `,(0,c.jsx)(t.li,{children:`Не заменяет TanStack Query.`}),`
89
+ `,(0,c.jsxs)(t.li,{children:[`Не делает Service Worker cache только из-за наличия `,(0,c.jsx)(t.code,{children:`swCache`}),`: header должен понимать Service Worker host-приложения.`]}),`
90
+ `,(0,c.jsx)(t.li,{children:`Не гарантирует SSR-совместимость SSO recovery.`}),`
91
+ `,(0,c.jsx)(t.li,{children:`Не экспортирует внутренние файлы как отдельные package subpath.`}),`
92
+ `]}),`
93
+ `,(0,c.jsx)(t.p,{children:`Граница ответственности выглядит так:`}),`
94
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`React component / route loader
95
+
96
+
97
+ entity или feature API приложения
98
+
99
+
100
+ TanStack Query + stable query key
101
+
102
+
103
+ @ryuzaki13/react-foundation-api/odata
104
+
105
+ ├── metadata
106
+ ├── URL и OData envelope
107
+ ├── SSO и X-CSRF
108
+ └── browser fetch
109
+
110
+
111
+ SAP Gateway
112
+ `})}),`
113
+ `,(0,c.jsxs)(t.p,{children:[`DTO-to-domain mapping конкретной entity остаётся в приложении. Pure builders для metadata, parameters, `,(0,c.jsx)(t.code,{children:`$filter`}),` и `,(0,c.jsx)(t.code,{children:`$orderby`}),` находятся в `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/odata-service`}),`.`]}),`
114
+ `,(0,c.jsx)(t.h2,{id:`установка-и-импорт`,children:`Установка и импорт`}),`
115
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-bash`,children:`npm install \\
116
+ @ryuzaki13/react-foundation-api \\
117
+ @ryuzaki13/react-foundation-lib \\
118
+ @tanstack/react-query \\
119
+ react
120
+ `})}),`
121
+ `,(0,c.jsxs)(t.p,{children:[`Для `,(0,c.jsx)(t.code,{children:`useODataTableColumns`}),` также нужен совместимый `,(0,c.jsx)(t.code,{children:`@tanstack/react-table`}),`.`]}),`
122
+ `,(0,c.jsx)(t.p,{children:`Пакет не имеет корневого entrypoint. Правильный импорт:`}),`
123
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import {
124
+ odataQueryFn,
125
+ odataReadFn,
126
+ useODataMetadataQuery
127
+ } from "@ryuzaki13/react-foundation-api/odata";
128
+ `})}),`
129
+ `,(0,c.jsx)(t.p,{children:`Неправильно:`}),`
130
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Корневой entrypoint не опубликован.
131
+ import { odataQueryFn } from "@ryuzaki13/react-foundation-api";
132
+
133
+ // Внутренний файл не является public contract.
134
+ import { odataFetchFn } from "@ryuzaki13/react-foundation-api/dist/odata/odataFetchFn";
135
+ `})}),`
136
+ `,(0,c.jsxs)(t.p,{children:[`Pure OData builders импортируются отдельно из `,(0,c.jsx)(t.code,{children:`foundation-lib`}),`:`]}),`
137
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import {
138
+ createFilterEqual,
139
+ createFilterBetween,
140
+ wrapODataParams
141
+ } from "@ryuzaki13/react-foundation-lib/odata-service";
142
+ `})}),`
143
+ `,(0,c.jsx)(t.h3,{id:`import-type`,children:(0,c.jsx)(t.code,{children:`import type`})}),`
144
+ `,(0,c.jsxs)(t.p,{children:[`TypeScript-типы лучше импортировать через `,(0,c.jsx)(t.code,{children:`import type`}),`:`]}),`
145
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import type {
146
+ ODataCollectionConfig,
147
+ ODataFetchFnRequest,
148
+ ODataFetchOptions,
149
+ ODataProjectAdapter
150
+ } from "@ryuzaki13/react-foundation-api/odata";
151
+ `})}),`
152
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`import type`}),` удаляется из JavaScript bundle и показывает читателю, что значение не вызывается в runtime.`]}),`
153
+ `,(0,c.jsx)(t.h2,{id:`зависимости-и-среда-выполнения`,children:`Зависимости и среда выполнения`}),`
154
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Возможность`}),(0,c.jsx)(t.th,{children:`Что требуется`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`odataFetch`}),`, `,(0,c.jsx)(t.code,{children:`fetchBase`}),`, `,(0,c.jsx)(t.code,{children:`fetchODataJson`})]}),(0,c.jsxs)(t.td,{children:[`Web Fetch API: `,(0,c.jsx)(t.code,{children:`fetch`}),`, `,(0,c.jsx)(t.code,{children:`Request`}),`, `,(0,c.jsx)(t.code,{children:`Response`}),`, `,(0,c.jsx)(t.code,{children:`Headers`}),`, `,(0,c.jsx)(t.code,{children:`URLSearchParams`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Metadata-aware operations`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`@tanstack/react-query`}),` и `,(0,c.jsx)(t.code,{children:`QueryClient`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`React hooks`}),(0,c.jsxs)(t.td,{children:[`React и `,(0,c.jsx)(t.code,{children:`QueryClientProvider`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`SSO recovery`}),(0,c.jsxs)(t.td,{children:[`Browser `,(0,c.jsx)(t.code,{children:`window`}),`, `,(0,c.jsx)(t.code,{children:`document`}),`, iframe и form submission`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Metadata XML parsing`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`DOMParser`}),` через `,(0,c.jsx)(t.code,{children:`foundation-lib/odata-service`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Table adapter`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`@tanstack/react-table`}),` и `,(0,c.jsx)(t.code,{children:`foundation-lib/table`})]})]})]})]}),`
155
+ `,(0,c.jsx)(t.p,{children:`Основная среда модуля — браузерное SPA-приложение.`}),`
156
+ `,(0,c.jsxs)(t.p,{children:[`В современном Node.js есть `,(0,c.jsx)(t.code,{children:`fetch`}),`, но это не делает весь entrypoint server-safe: automatic SSO recovery использует DOM, а relative SAP paths, cookies и compile-time project constants обычно рассчитаны на browser/proxy окружение.`]}),`
157
+ `,(0,c.jsx)(t.h2,{id:`odata-v2-простыми-словами`,children:`OData V2 простыми словами`}),`
158
+ `,(0,c.jsx)(t.h3,{id:`service`,children:`Service`}),`
159
+ `,(0,c.jsx)(t.p,{children:`OData service — набор endpoint, metadata и операций, опубликованных backend-ом.`}),`
160
+ `,(0,c.jsx)(t.p,{children:`Пример имени service:`}),`
161
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Z_SALES_ORDER_SRV
162
+ `})}),`
163
+ `,(0,c.jsx)(t.p,{children:`Обычный SAP Gateway base path:`}),`
164
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/sap/opu/odata/sap/Z_SALES_ORDER_SRV
165
+ `})}),`
166
+ `,(0,c.jsx)(t.p,{children:`В конфигурации operation имя service передаётся без base path:`}),`
167
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const odata = {
168
+ service: "Z_SALES_ORDER_SRV",
169
+ target: "SalesOrderSet"
170
+ };
171
+ `})}),`
172
+ `,(0,c.jsx)(t.h3,{id:`metadata`,children:`Metadata`}),`
173
+ `,(0,c.jsx)(t.p,{children:`Metadata — XML-документ, описывающий service:`}),`
174
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/$metadata
175
+ `})}),`
176
+ `,(0,c.jsx)(t.p,{children:`Из metadata библиотека узнаёт:`}),`
177
+ `,(0,c.jsxs)(t.ul,{children:[`
178
+ `,(0,c.jsx)(t.li,{children:`какие Entity и FunctionImport существуют;`}),`
179
+ `,(0,c.jsx)(t.li,{children:`какие параметры обязательны;`}),`
180
+ `,(0,c.jsx)(t.li,{children:`какие OData types имеют поля;`}),`
181
+ `,(0,c.jsx)(t.li,{children:`какой suffix результата использует parameterized entity;`}),`
182
+ `,(0,c.jsx)(t.li,{children:`какой HTTP method объявлен у FunctionImport;`}),`
183
+ `,(0,c.jsx)(t.li,{children:`какие code/text связи заданы SAP annotations.`}),`
184
+ `]}),`
185
+ `,(0,c.jsx)(t.p,{children:`Metadata не содержит фактические rows и не доказывает, что каждый response корректен.`}),`
186
+ `,(0,c.jsx)(t.h3,{id:`entity-и-entityset`,children:`Entity и EntitySet`}),`
187
+ `,(0,c.jsx)(t.p,{children:`Entity — описание одной записи. EntitySet — endpoint коллекции записей.`}),`
188
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`GET /Z_SALES_ORDER_SRV/SalesOrderSet
189
+ `})}),`
190
+ `,(0,c.jsx)(t.p,{children:`Ответ может содержать несколько заказов.`}),`
191
+ `,(0,c.jsx)(t.p,{children:`Чтение одной entity по key:`}),`
192
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`GET /Z_SALES_ORDER_SRV/SalesOrderSet(OrderId='42')
193
+ `})}),`
194
+ `,(0,c.jsxs)(t.p,{children:[`В API пакета collection/query читает `,(0,c.jsx)(t.code,{children:`odataQueryFn`}),`, одну entity — `,(0,c.jsx)(t.code,{children:`odataReadFn`}),`.`]}),`
195
+ `,(0,c.jsx)(t.h3,{id:`parameterized-query-entity`,children:`Parameterized query entity`}),`
196
+ `,(0,c.jsx)(t.p,{children:`Некоторые SAP services публикуют query entity с parameters и отдельным result suffix:`}),`
197
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`GET /Z_REPORT_SRV/SalesReport(
198
+ CompanyId='1000',
199
+ DateFrom=datetime'2026-01-01T00:00:00'
200
+ )/Results
201
+ `})}),`
202
+ `,(0,c.jsxs)(t.p,{children:[`Metadata entity содержит поле `,(0,c.jsx)(t.code,{children:`result`}),`, например `,(0,c.jsx)(t.code,{children:`Results`}),` или `,(0,c.jsx)(t.code,{children:`Set`}),`. Для такого target нужен `,(0,c.jsx)(t.code,{children:`odataQueryFn`}),`, а не `,(0,c.jsx)(t.code,{children:`odataReadFn`}),`.`]}),`
203
+ `,(0,c.jsx)(t.h3,{id:`functionimport`,children:`FunctionImport`}),`
204
+ `,(0,c.jsx)(t.p,{children:`FunctionImport — отдельная операция, описанная в metadata:`}),`
205
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`POST /Z_SALES_ORDER_SRV/CloseOrder?OrderId='42'
206
+ `})}),`
207
+ `,(0,c.jsx)(t.p,{children:`HTTP method берётся из metadata. FunctionImport может быть read-like или write-like по бизнес-смыслу, поэтому helper выбирают по типу metadata target, а не только по глаголу feature.`}),`
208
+ `,(0,c.jsxs)(t.p,{children:[`Для FunctionImport используется `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`.`]}),`
209
+ `,(0,c.jsx)(t.h3,{id:`target`,children:`Target`}),`
210
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`target`}),` — точное имя Entity/EntitySet-like target или FunctionImport внутри service metadata:`]}),`
211
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
212
+ service: "Z_SALES_ORDER_SRV",
213
+ target: "SalesOrderSet"
214
+ }
215
+ `})}),`
216
+ `,(0,c.jsx)(t.p,{children:`Target не является URL. Metadata-aware helper сам строит path.`}),`
217
+ `,(0,c.jsx)(t.h3,{id:`target-params-и-query-options--разные-вещи`,children:`Target params и query options — разные вещи`}),`
218
+ `,(0,c.jsx)(t.p,{children:`Это один из самых важных разделов.`}),`
219
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`params`}),` адресуют target или FunctionImport:`]}),`
220
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`SalesOrderSet(OrderId='42')
221
+ SalesReport(CompanyId='1000')/Results
222
+ CloseOrder?OrderId='42'
223
+ `})}),`
224
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`options`}),` управляют выборкой:`]}),`
225
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`$filter
226
+ $orderby
227
+ $select
228
+ $expand
229
+ $top
230
+ $skip
231
+ $inlinecount
232
+ `})}),`
233
+ `,(0,c.jsx)(t.p,{children:`Сравнение:`}),`
234
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Значение`}),(0,c.jsx)(t.th,{children:`Куда попадает`}),(0,c.jsx)(t.th,{children:`Чем строить`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Key entity`}),(0,c.jsxs)(t.td,{children:[`В `,(0,c.jsx)(t.code,{children:`(Key=...)`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`params`}),` + `,(0,c.jsx)(t.code,{children:`wrapODataParams`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Parameterized entity parameter`}),(0,c.jsxs)(t.td,{children:[`В `,(0,c.jsx)(t.code,{children:`(Param=...)`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`params`}),` + `,(0,c.jsx)(t.code,{children:`wrapODataParams`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`FunctionImport parameter`}),(0,c.jsx)(t.td,{children:`В query string самой функции`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`params`}),` + `,(0,c.jsx)(t.code,{children:`wrapODataParams`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Filter rows`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`$filter`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`options.expression`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Sorting`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`$orderby`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`options.sorts`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Paging`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`$top`}),`, `,(0,c.jsx)(t.code,{children:`$skip`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`options.top`}),`, `,(0,c.jsx)(t.code,{children:`options.skip`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Projection`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`$select`}),`, `,(0,c.jsx)(t.code,{children:`$expand`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`options.select`}),`, `,(0,c.jsx)(t.code,{children:`options.expand`})]})]})]})]}),`
235
+ `,(0,c.jsxs)(t.p,{children:[`Нельзя переносить key в `,(0,c.jsx)(t.code,{children:`$filter`}),`, если backend contract ожидает key path. Нельзя передавать `,(0,c.jsx)(t.code,{children:`$top`}),` как target parameter.`]}),`
236
+ `,(0,c.jsx)(t.h3,{id:`body`,children:`Body`}),`
237
+ `,(0,c.jsx)(t.p,{children:`Body используется только для entity create/update:`}),`
238
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
239
+ body: {
240
+ OrderId: "42",
241
+ Title: "Новый заказ"
242
+ }
243
+ }
244
+ `})}),`
245
+ `,(0,c.jsxs)(t.p,{children:[`Helper сериализует body через `,(0,c.jsx)(t.code,{children:`JSON.stringify`}),`.`]}),`
246
+ `,(0,c.jsx)(t.h3,{id:`odata-v2-envelope`,children:`OData V2 envelope`}),`
247
+ `,(0,c.jsx)(t.p,{children:`SAP Gateway часто возвращает JSON не как обычный array, а в envelope:`}),`
248
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{
249
+ "d": {
250
+ "results": [
251
+ { "OrderId": "42", "Title": "Заказ" }
252
+ ],
253
+ "__count": "1"
254
+ }
255
+ }
256
+ `})}),`
257
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`fetchODataJson`}),` превращает его в:`]}),`
258
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
259
+ data: [{ OrderId: "42", Title: "Заказ" }],
260
+ totalCount: 1
261
+ }
262
+ `})}),`
263
+ `,(0,c.jsx)(t.p,{children:`Metadata-aware runner возвращает тот же внешний result shape:`}),`
264
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ODataRunnerResult<T> = {
265
+ data: T;
266
+ totalCount?: number;
267
+ };
268
+ `})}),`
269
+ `,(0,c.jsxs)(t.p,{children:[`Не забывайте `,(0,c.jsx)(t.code,{children:`.data`}),` при использовании результата.`]}),`
270
+ `,(0,c.jsx)(t.h3,{id:`queryclient`,children:`QueryClient`}),`
271
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`QueryClient`}),` — объект TanStack Query, который хранит query cache и передаёт runtime context запросам.`]}),`
272
+ `,(0,c.jsx)(t.p,{children:`Metadata-aware helper возвращает не Promise результата, а runner:`}),`
273
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataQueryFn<unknown>({
274
+ odata: {
275
+ service: "Z_SALES_ORDER_SRV",
276
+ target: "SalesOrderSet"
277
+ }
278
+ });
279
+
280
+ const result = await runner({ client: queryClient });
281
+ `})}),`
282
+ `,(0,c.jsx)(t.p,{children:`Почему два вызова:`}),`
283
+ `,(0,c.jsxs)(t.ol,{children:[`
284
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`odataQueryFn(options)`}),` создаёт настроенную функцию.`]}),`
285
+ `,(0,c.jsxs)(t.li,{children:[`Эта функция получает текущий `,(0,c.jsx)(t.code,{children:`QueryClient`}),` и optional `,(0,c.jsx)(t.code,{children:`AbortSignal`}),` в момент выполнения.`]}),`
286
+ `]}),`
287
+ `,(0,c.jsxs)(t.p,{children:[`В `,(0,c.jsx)(t.code,{children:`useQuery`}),` второй вызов делает TanStack Query автоматически.`]}),`
288
+ `,(0,c.jsx)(t.h2,{id:`как-выбрать-уровень-api`,children:`Как выбрать уровень API`}),`
289
+ `,(0,c.jsx)(t.p,{children:`Entrypoint содержит несколько уровней. Начинайте с самого высокого подходящего уровня.`}),`
290
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Сценарий`}),(0,c.jsx)(t.th,{children:`API`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Обычный list/query Entity`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataQueryFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Одна Entity по key`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataReadFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Создание Entity`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataCreateFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Обновление Entity`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataUpdateFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Удаление Entity`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDeleteFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`FunctionImport`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFunctionImportFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Metadata service/entity`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`useODataMetadataQuery`}),`, `,(0,c.jsx)(t.code,{children:`useODataMetadata`}),`, `,(0,c.jsx)(t.code,{children:`getODataMetadataData`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Code/text справочник`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`useODataCollectionQuery`}),`, `,(0,c.jsx)(t.code,{children:`useODataCollection`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Table columns по metadata`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataTableColumns`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Уже известный raw OData path без metadata orchestration`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFetch`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Нужен raw `,(0,c.jsx)(t.code,{children:`Response`}),` и SAP policy`]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchBase`})})]})]})]}),`
291
+ `,(0,c.jsx)(t.p,{children:`Правило:`}),`
292
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`specialized helper подходит? ── да ──► используйте его
293
+
294
+ нет
295
+
296
+ нужен готовый OData path? ───── да ──► odataFetch
297
+
298
+ нет
299
+
300
+ нужен raw Response? ─────────── да ──► fetchBase / fetchQueryFn
301
+ `})}),`
302
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`fetchBase`}),` и `,(0,c.jsx)(t.code,{children:`odataFetch`}),` — infrastructure primitives. Они требуют от consumer больше знаний и дают меньше metadata guarantees.`]}),`
303
+ `,(0,c.jsx)(t.h2,{id:`быстрый-старт`,children:`Быстрый старт`}),`
304
+ `,(0,c.jsx)(t.h3,{id:`1-создать-singleton-queryclient`,children:`1. Создать singleton QueryClient`}),`
305
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { QueryClientProvider } from "@tanstack/react-query";
306
+ import { createQueryClient } from "@ryuzaki13/react-foundation-lib/query-client";
307
+
308
+ const queryClient = createQueryClient();
309
+
310
+ export function App() {
311
+ return (
312
+ <QueryClientProvider client={queryClient}>
313
+ <AppRoutes />
314
+ </QueryClientProvider>
315
+ );
316
+ }
317
+ `})}),`
318
+ `,(0,c.jsxs)(t.p,{children:[`Не создавайте `,(0,c.jsx)(t.code,{children:`QueryClient`}),` внутри component render:`]}),`
319
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`function App() {
320
+ // Неверно: новый client и новый cache на каждый render.
321
+ const queryClient = new QueryClient();
322
+ return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
323
+ }
324
+ `})}),`
325
+ `,(0,c.jsx)(t.h3,{id:`2-описать-dto-и-domain-model`,children:`2. Описать DTO и domain model`}),`
326
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type SalesOrderDto = {
327
+ CompanyId: string;
328
+ OrderId: string;
329
+ Title: string;
330
+ Amount: number;
331
+ CreatedAt: Date;
332
+ };
333
+
334
+ type SalesOrder = {
335
+ id: string;
336
+ title: string;
337
+ amount: number;
338
+ createdAt: Date;
339
+ };
340
+
341
+ function mapSalesOrder(dto: SalesOrderDto): SalesOrder {
342
+ return {
343
+ id: dto.OrderId,
344
+ title: dto.Title,
345
+ amount: dto.Amount,
346
+ createdAt: dto.CreatedAt
347
+ };
348
+ }
349
+ `})}),`
350
+ `,(0,c.jsx)(t.p,{children:`DTO повторяет внешний SAP contract. Domain model использует имена приложения.`}),`
351
+ `,(0,c.jsx)(t.h3,{id:`3-создать-stable-query-options`,children:`3. Создать stable query options`}),`
352
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { queryOptions } from "@tanstack/react-query";
353
+
354
+ import { odataQueryFn } from "@ryuzaki13/react-foundation-api/odata";
355
+ import { createFilterEqual } from "@ryuzaki13/react-foundation-lib/odata-service";
356
+
357
+ type SalesOrderListParams = {
358
+ companyId: string;
359
+ limit: number;
360
+ };
361
+
362
+ const salesOrderKeys = {
363
+ all: ["sales-orders"] as const,
364
+ list: (params: SalesOrderListParams) =>
365
+ [...salesOrderKeys.all, "list", params] as const
366
+ };
367
+
368
+ export function salesOrderListQueryOptions(params: SalesOrderListParams) {
369
+ const normalized = {
370
+ companyId: params.companyId.trim(),
371
+ limit: params.limit
372
+ };
373
+
374
+ return queryOptions({
375
+ queryKey: salesOrderKeys.list(normalized),
376
+ queryFn: odataQueryFn<SalesOrderDto, SalesOrder>({
377
+ odata: {
378
+ service: "Z_SALES_ORDER_SRV",
379
+ target: "SalesOrderSet"
380
+ },
381
+ options: {
382
+ expression: createFilterEqual<SalesOrderDto, string>(
383
+ "CompanyId",
384
+ normalized.companyId
385
+ ),
386
+ top: normalized.limit,
387
+ inlinecount: "allpages"
388
+ },
389
+ autoParse: true,
390
+ transform: (rows) => rows.map(mapSalesOrder)
391
+ })
392
+ });
393
+ }
394
+ `})}),`
395
+ `,(0,c.jsx)(t.p,{children:`Filter key полностью типизирован:`}),`
396
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`createFilterEqual<SalesOrderDto, string>("CompanyId", normalized.companyId);
397
+ `})}),`
398
+ `,(0,c.jsx)(t.h3,{id:`4-использовать-query-в-component`,children:`4. Использовать query в component`}),`
399
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useQuery } from "@tanstack/react-query";
400
+
401
+ export function SalesOrderList({ companyId }: { companyId: string }) {
402
+ const query = useQuery(
403
+ salesOrderListQueryOptions({
404
+ companyId,
405
+ limit: 50
406
+ })
407
+ );
408
+
409
+ if (query.isPending) {
410
+ return <p>Загрузка заказов…</p>;
411
+ }
412
+
413
+ if (query.isError) {
414
+ return <p>Не удалось загрузить заказы</p>;
415
+ }
416
+
417
+ return (
418
+ <ul>
419
+ {query.data.data.map((order) => (
420
+ <li key={order.id}>{order.title}</li>
421
+ ))}
422
+ </ul>
423
+ );
424
+ }
425
+ `})}),`
426
+ `,(0,c.jsx)(t.p,{children:`Обратите внимание:`}),`
427
+ `,(0,c.jsxs)(t.ul,{children:[`
428
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`query.data`}),` — result runner-а;`]}),`
429
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`query.data.data`}),` — массив domain objects;`]}),`
430
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`query.data.totalCount`}),` — optional полное количество rows;`]}),`
431
+ `,(0,c.jsxs)(t.li,{children:[`backend state не копируется в `,(0,c.jsx)(t.code,{children:`useState`}),` или Zustand.`]}),`
432
+ `]}),`
433
+ `,(0,c.jsx)(t.h2,{id:`общий-маршрут-запроса`,children:`Общий маршрут запроса`}),`
434
+ `,(0,c.jsx)(t.p,{children:`Metadata-aware read проходит этапы:`}),`
435
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`1. Consumer создаёт operation options
436
+
437
+
438
+ 2. TanStack Query вызывает runner({ client, signal })
439
+
440
+
441
+ 3. getODataMetadataData загружает/читает metadata
442
+
443
+
444
+ 4. Находится target Entity или FunctionImport
445
+
446
+
447
+ 5. Проверяется совместимость operation
448
+
449
+
450
+ 6. Строится target path и query options
451
+
452
+
453
+ 7. fetchBase добавляет SAP/SSO/X-CSRF policy
454
+
455
+
456
+ 8. fetchODataJson снимает OData envelope
457
+
458
+
459
+ 9. autoParse преобразует известные metadata fields
460
+
461
+
462
+ 10. transform строит result consumer-а
463
+ `})}),`
464
+ `,(0,c.jsx)(t.p,{children:`Write проходит те же этапы, но дополнительно получает X-CSRF token и использует HTTP method операции.`}),`
465
+ `,(0,c.jsx)(t.h2,{id:`инициализация-project-adapter`,children:`Инициализация project adapter`}),`
466
+ `,(0,c.jsx)(t.p,{children:`Project adapter задаёт настройки, которые нельзя надёжно определить внутри универсального пакета.`}),`
467
+ `,(0,c.jsx)(t.p,{children:`Настройте его один раз до первого OData запроса:`}),`
468
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { configureODataProjectAdapter } from "@ryuzaki13/react-foundation-api/odata";
469
+
470
+ configureODataProjectAdapter({
471
+ devDp0Service: "Z_SPECIAL_REPORT_SRV",
472
+ resolveSapClient: () => appConfig.sapClient,
473
+ metadataVersion: {
474
+ service: "Z_TECHNICAL_SRV",
475
+ target: "MetadataVersionSet"
476
+ },
477
+ collectionUpdates: {
478
+ service: "Z_TECHNICAL_SRV",
479
+ target: "CollectionUpdatesSet"
480
+ }
481
+ });
482
+ `})}),`
483
+ `,(0,c.jsx)(t.p,{children:`Типы:`}),`
484
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ODataProjectTechnicalEndpoint = {
485
+ service: string;
486
+ target: string;
487
+ baseUrl?: BaseURLType;
488
+ };
489
+
490
+ type ODataProjectAdapter = {
491
+ devDp0Service?: string;
492
+ resolveSapClient?: () => string | null | undefined;
493
+ metadataVersion?: ODataProjectTechnicalEndpoint;
494
+ collectionUpdates?: ODataProjectTechnicalEndpoint;
495
+ };
496
+ `})}),`
497
+ `,(0,c.jsx)(t.h3,{id:`devdp0service`,children:(0,c.jsx)(t.code,{children:`devDp0Service`})}),`
498
+ `,(0,c.jsxs)(t.p,{children:[`В dev/preview только указанный service автоматически получает base URL `,(0,c.jsx)(t.code,{children:`odataDp0`}),`:`]}),`
499
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveODataBaseUrl("Z_SPECIAL_REPORT_SRV");
500
+ // dev/preview: "odataDp0".
501
+ // production: "odata".
502
+ `})}),`
503
+ `,(0,c.jsxs)(t.p,{children:[`Сравнение выполняется после `,(0,c.jsx)(t.code,{children:`normalizeODataServiceName`}),`, поэтому suffix после `,(0,c.jsx)(t.code,{children:`;`}),` не влияет:`]}),`
504
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`normalizeODataServiceName("Z_SPECIAL_REPORT_SRV;v=2");
505
+ // "Z_SPECIAL_REPORT_SRV".
506
+ `})}),`
507
+ `,(0,c.jsx)(t.h3,{id:`resolvesapclient`,children:(0,c.jsx)(t.code,{children:`resolveSapClient`})}),`
508
+ `,(0,c.jsx)(t.p,{children:`Callback возвращает SAP client в runtime:`}),`
509
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveSapClient: () => "300"
510
+ `})}),`
511
+ `,(0,c.jsxs)(t.p,{children:[`Если callback отсутствует или вернул `,(0,c.jsx)(t.code,{children:`null`}),`/`,(0,c.jsx)(t.code,{children:`undefined`}),`, production transport не добавляет `,(0,c.jsx)(t.code,{children:`sap-client`}),` из adapter-а.`]}),`
512
+ `,(0,c.jsx)(t.p,{children:`В dev/preview часть client policy задаётся compile-time project constants и выбранным proxy.`}),`
513
+ `,(0,c.jsx)(t.h3,{id:`metadataversion`,children:(0,c.jsx)(t.code,{children:`metadataVersion`})}),`
514
+ `,(0,c.jsx)(t.p,{children:`Optional technical endpoint сообщает, когда service metadata были сгенерированы последний раз. Он позволяет хранить metadata долго и обновлять только после реального изменения.`}),`
515
+ `,(0,c.jsx)(t.p,{children:`Если endpoint не настроен, metadata всё равно загружаются и работают; automatic version-check просто отключён.`}),`
516
+ `,(0,c.jsx)(t.h3,{id:`collectionupdates`,children:(0,c.jsx)(t.code,{children:`collectionUpdates`})}),`
517
+ `,(0,c.jsxs)(t.p,{children:[`Optional technical endpoint сообщает, какие справочники изменились за последнее окно. Он нужен для безопасного `,(0,c.jsx)(t.code,{children:`ttl=forever`}),` справочников.`]}),`
518
+ `,(0,c.jsx)(t.p,{children:`Если endpoint отсутствует, query обновлений возвращает пустой успешный result без network request.`}),`
519
+ `,(0,c.jsx)(t.h3,{id:`глобальное-состояние-adapter-а`,children:`Глобальное состояние adapter-а`}),`
520
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const adapter = getODataProjectAdapter();
521
+ `})}),`
522
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`configureODataProjectAdapter(next)`}),` полностью заменяет текущий object, а не merge-ит его:`]}),`
523
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`configureODataProjectAdapter({ resolveSapClient });
524
+ configureODataProjectAdapter({ metadataVersion });
525
+
526
+ // resolveSapClient уже потерян.
527
+ `})}),`
528
+ `,(0,c.jsx)(t.p,{children:`Правильнее собрать единый object и вызвать конфигурацию один раз.`}),`
529
+ `,(0,c.jsx)(t.p,{children:`Для test cleanup можно вернуть пустой adapter:`}),`
530
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`afterEach(() => {
531
+ configureODataProjectAdapter({});
532
+ });
533
+ `})}),`
534
+ `,(0,c.jsx)(t.h2,{id:`base-url-и-url-helpers`,children:`Base URL и URL helpers`}),`
535
+ `,(0,c.jsx)(t.h3,{id:`baseurltype`,children:(0,c.jsx)(t.code,{children:`BaseURLType`})}),`
536
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type BaseURLType = "odata" | "odataDp0" | "odataUi2" | "config" | "";
537
+ `})}),`
538
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Type`}),(0,c.jsx)(t.th,{children:`Назначение`}),(0,c.jsx)(t.th,{children:`Base path`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odata`})}),(0,c.jsx)(t.td,{children:`Основные SAP OData services`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`/sap/opu/odata/sap`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDp0`})}),(0,c.jsx)(t.td,{children:`Выбранный dev/preview DP0 proxy`}),(0,c.jsxs)(t.td,{children:[`dev/preview `,(0,c.jsx)(t.code,{children:`/sap-dp0/opu/odata/sap`}),`, production обычный OData path`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataUi2`})}),(0,c.jsx)(t.td,{children:`SAP UI2 services`}),(0,c.jsxs)(t.td,{children:[`dev/preview proxy или `,(0,c.jsx)(t.code,{children:`/sap/opu/odata/UI2`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`config`})}),(0,c.jsx)(t.td,{children:`Project configuration endpoint`}),(0,c.jsxs)(t.td,{children:[`compile-time `,(0,c.jsx)(t.code,{children:`__BASE_APP_CONFIG_URL__`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`""`})}),(0,c.jsx)(t.td,{children:`Не добавлять base path`}),(0,c.jsx)(t.td,{children:`пустая строка`})]})]})]}),`
539
+ `,(0,c.jsx)(t.p,{children:`Экспортированные constants:`}),`
540
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`BaseODataURL;
541
+ BaseODataDp0URL;
542
+ BaseODataUI2URL;
543
+ BaseAppConfigURL;
544
+ BaseUrlMap;
545
+ `})}),`
546
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`BaseUrlMap`}),` readonly:`]}),`
547
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`BaseUrlMap.odata;
548
+ // "/sap/opu/odata/sap".
549
+ `})}),`
550
+ `,(0,c.jsx)(t.p,{children:`Не изменяйте его в consumer-коде.`}),`
551
+ `,(0,c.jsx)(t.h3,{id:`resolveodatabaseurl`,children:(0,c.jsx)(t.code,{children:`resolveODataBaseUrl`})}),`
552
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveODataBaseUrl(service, explicitBaseUrl?);
553
+ `})}),`
554
+ `,(0,c.jsx)(t.p,{children:`Порядок:`}),`
555
+ `,(0,c.jsxs)(t.ol,{children:[`
556
+ `,(0,c.jsxs)(t.li,{children:[`В dev/preview проверяется `,(0,c.jsx)(t.code,{children:`devDp0Service`}),`.`]}),`
557
+ `,(0,c.jsxs)(t.li,{children:[`Если service совпал, возвращается `,(0,c.jsx)(t.code,{children:`odataDp0`}),`.`]}),`
558
+ `,(0,c.jsxs)(t.li,{children:[`Иначе используется explicit `,(0,c.jsx)(t.code,{children:`baseUrl`}),`.`]}),`
559
+ `,(0,c.jsxs)(t.li,{children:[`Если explicit value нет, используется `,(0,c.jsx)(t.code,{children:`odata`}),`.`]}),`
560
+ `]}),`
561
+ `,(0,c.jsx)(t.p,{children:`Следствие: dev DP0 routing имеет приоритет над explicit base URL для совпавшего service.`}),`
562
+ `,(0,c.jsx)(t.h3,{id:`resolveodatasapclient`,children:(0,c.jsx)(t.code,{children:`resolveODataSapClient`})}),`
563
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const sapClient = resolveODataSapClient();
564
+ `})}),`
565
+ `,(0,c.jsxs)(t.p,{children:[`Возвращает значение project adapter callback или `,(0,c.jsx)(t.code,{children:`null`}),`.`]}),`
566
+ `,(0,c.jsx)(t.h3,{id:`getinputurl`,children:(0,c.jsx)(t.code,{children:`getInputUrl`})}),`
567
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`getInputUrl("/sap/example");
568
+ // "/sap/example".
569
+
570
+ getInputUrl(new Request("https://example.test/sap/example"));
571
+ // "https://example.test/sap/example".
572
+ `})}),`
573
+ `,(0,c.jsxs)(t.p,{children:[`Поддерживает string, `,(0,c.jsx)(t.code,{children:`Request`}),` и остальные `,(0,c.jsx)(t.code,{children:`RequestInfo`}),` через `,(0,c.jsx)(t.code,{children:`toString()`}),`.`]}),`
574
+ `,(0,c.jsx)(t.h3,{id:`normalizerelativepath`,children:(0,c.jsx)(t.code,{children:`normalizeRelativePath`})}),`
575
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`normalizeRelativePath("sap/example?x=1#hash");
576
+ // "/sap/example".
577
+
578
+ normalizeRelativePath("");
579
+ // "/".
580
+ `})}),`
581
+ `,(0,c.jsx)(t.p,{children:`Функция:`}),`
582
+ `,(0,c.jsxs)(t.ul,{children:[`
583
+ `,(0,c.jsx)(t.li,{children:`удаляет query string;`}),`
584
+ `,(0,c.jsx)(t.li,{children:`удаляет hash;`}),`
585
+ `,(0,c.jsxs)(t.li,{children:[`добавляет начальный `,(0,c.jsx)(t.code,{children:`/`}),`;`]}),`
586
+ `,(0,c.jsx)(t.li,{children:`не нормализует повторные slash;`}),`
587
+ `,(0,c.jsx)(t.li,{children:`не декодирует URL;`}),`
588
+ `,(0,c.jsxs)(t.li,{children:[`не удаляет SAP suffix `,(0,c.jsx)(t.code,{children:`;o=...`}),`.`]}),`
589
+ `]}),`
590
+ `,(0,c.jsx)(t.h2,{id:`низкоуровневый-sap-transport`,children:`Низкоуровневый SAP transport`}),`
591
+ `,(0,c.jsx)(t.h3,{id:`fetchbase`,children:(0,c.jsx)(t.code,{children:`fetchBase`})}),`
592
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`async function fetchBase(
593
+ input: RequestInfo,
594
+ init: RequestInitType = {},
595
+ baseUrlType: BaseURLType = "odata"
596
+ ): Promise<Response>;
597
+ `})}),`
598
+ `,(0,c.jsx)(t.p,{children:`Пример raw XML:`}),`
599
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const response = await fetchBase(
600
+ "/Z_SALES_ORDER_SRV/$metadata",
601
+ {
602
+ method: "GET",
603
+ headers: {
604
+ Accept: "application/xml"
605
+ }
606
+ },
607
+ "odata"
608
+ );
609
+
610
+ const xml = await response.text();
611
+ `})}),`
612
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`fetchBase`}),` возвращает raw `,(0,c.jsx)(t.code,{children:`Response`}),`. Body ещё не распарсен.`]}),`
613
+ `,(0,c.jsx)(t.h3,{id:`как-определяется-sap-request`,children:`Как определяется SAP request`}),`
614
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`baseUrlType`}),` не пустой, request считается SAP request.`]}),`
615
+ `,(0,c.jsxs)(t.p,{children:[`При `,(0,c.jsx)(t.code,{children:`baseUrlType: ""`}),` request считается SAP только когда normalized path начинается с:`]}),`
616
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/sap/
617
+ /sap-dp0/
618
+ `})}),`
619
+ `,(0,c.jsx)(t.p,{children:`Это влияет на:`}),`
620
+ `,(0,c.jsxs)(t.ul,{children:[`
621
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`sap-contextid-accept`}),`;`]}),`
622
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`sap-client`}),`;`]}),`
623
+ `,(0,c.jsx)(t.li,{children:`X-CSRF;`}),`
624
+ `,(0,c.jsx)(t.li,{children:`manual redirect;`}),`
625
+ `,(0,c.jsx)(t.li,{children:`SSO recovery.`}),`
626
+ `]}),`
627
+ `,(0,c.jsx)(t.h3,{id:`headers-по-умолчанию`,children:`Headers по умолчанию`}),`
628
+ `,(0,c.jsx)(t.p,{children:`Для всех requests:`}),`
629
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`sap-language: ru
630
+ Accept: application/json если consumer не передал Accept
631
+ `})}),`
632
+ `,(0,c.jsx)(t.p,{children:`Для SAP requests:`}),`
633
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`sap-contextid-accept: header
634
+ sap-client: <resolved client> если client доступен
635
+ `})}),`
636
+ `,(0,c.jsx)(t.p,{children:`Для write с body:`}),`
637
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Content-Type: application/json если consumer не задал Content-Type
638
+ `})}),`
639
+ `,(0,c.jsx)(t.p,{children:`Для SAP write:`}),`
640
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`x-csrf-token: <token>
641
+ `})}),`
642
+ `,(0,c.jsx)(t.p,{children:`Consumer headers сохраняются, кроме headers, которые transport намеренно устанавливает по своей policy.`}),`
643
+ `,(0,c.jsx)(t.h3,{id:`cookies-и-redirects`,children:`Cookies и redirects`}),`
644
+ `,(0,c.jsxs)(t.p,{children:[`Фактический `,(0,c.jsx)(t.code,{children:`fetch`}),` всегда получает:`]}),`
645
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
646
+ credentials: "include"
647
+ }
648
+ `})}),`
649
+ `,(0,c.jsx)(t.p,{children:`Для SAP request также используется:`}),`
650
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
651
+ redirect: "manual"
652
+ }
653
+ `})}),`
654
+ `,(0,c.jsx)(t.p,{children:`Manual redirect нужен, чтобы browser не ушёл автоматически на ADFS/SSO page до того, как transport распознает истёкшую session.`}),`
655
+ `,(0,c.jsx)(t.h3,{id:`get-без-body`,children:`GET без body`}),`
656
+ `,(0,c.jsxs)(t.p,{children:[`Transport не добавляет `,(0,c.jsx)(t.code,{children:`Content-Type`}),` для GET без body:`]}),`
657
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`await fetchJson("/Filters?cfo-count", {}, "config");
658
+ `})}),`
659
+ `,(0,c.jsxs)(t.p,{children:[`Ожидаемые headers включают `,(0,c.jsx)(t.code,{children:`Accept`}),`, но не `,(0,c.jsx)(t.code,{children:`Content-Type`}),`.`]}),`
660
+ `,(0,c.jsx)(t.h3,{id:`requestinittype`,children:(0,c.jsx)(t.code,{children:`RequestInitType`})}),`
661
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type RequestInitType = RequestInit & {
662
+ retried?: boolean;
663
+ ssoRetried?: boolean;
664
+ };
665
+ `})}),`
666
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`retried`}),` и `,(0,c.jsx)(t.code,{children:`ssoRetried`}),` — internal lifecycle flags. Не используйте их как прикладные options: consumer может случайно отключить защитный retry.`]}),`
667
+ `,(0,c.jsx)(t.h3,{id:`successful-response`,children:`Successful response`}),`
668
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`response.ok === true`}),`, `,(0,c.jsx)(t.code,{children:`fetchBase`}),` возвращает `,(0,c.jsx)(t.code,{children:`Response`}),`.`]}),`
669
+ `,(0,c.jsx)(t.h3,{id:`http-error`,children:`HTTP error`}),`
670
+ `,(0,c.jsx)(t.p,{children:`Если status неуспешный, transport пытается построить полезное сообщение.`}),`
671
+ `,(0,c.jsx)(t.p,{children:`Для SAP JSON:`}),`
672
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{
673
+ "error": {
674
+ "code": "SY/530",
675
+ "message": {
676
+ "value": "Необходимо указать запрос на перенос!"
677
+ }
678
+ }
679
+ }
680
+ `})}),`
681
+ `,(0,c.jsx)(t.p,{children:`Результат:`}),`
682
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Error: SY/530 | Необходимо указать запрос на перенос!
683
+ `})}),`
684
+ `,(0,c.jsx)(t.p,{children:`Рассматриваются unique значения:`}),`
685
+ `,(0,c.jsxs)(t.ul,{children:[`
686
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error`}),` как string;`]}),`
687
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error.code`}),`;`]}),`
688
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error.message.value`}),`;`]}),`
689
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error.message`}),` как string;`]}),`
690
+ `,(0,c.jsxs)(t.li,{children:[`top-level `,(0,c.jsx)(t.code,{children:`details`}),`;`]}),`
691
+ `,(0,c.jsxs)(t.li,{children:[`top-level `,(0,c.jsx)(t.code,{children:`message`}),`.`]}),`
692
+ `]}),`
693
+ `,(0,c.jsxs)(t.p,{children:[`Вложенный `,(0,c.jsx)(t.code,{children:`innererror.errordetails`}),` сейчас не добавляется в message.`]}),`
694
+ `,(0,c.jsx)(t.p,{children:`Для text error непустой text становится message. Иначе используется:`}),`
695
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`HTTP error: <status> <statusText>
696
+ `})}),`
697
+ `,(0,c.jsxs)(t.p,{children:[`Network/CORS/abort errors глобального `,(0,c.jsx)(t.code,{children:`fetch`}),` пробрасываются без замены.`]}),`
698
+ `,(0,c.jsx)(t.h3,{id:`fetcherrorreportcontext`,children:(0,c.jsx)(t.code,{children:`FetchErrorReportContext`})}),`
699
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type FetchErrorReportContext = {
700
+ input: RequestInfo;
701
+ init: RequestInitType;
702
+ baseUrlType: BaseURLType;
703
+ sapClient: string | null;
704
+ };
705
+ `})}),`
706
+ `,(0,c.jsx)(t.p,{children:`Это public type технического контекста error reporting. Обычный consumer редко создаёт его вручную.`}),`
707
+ `,(0,c.jsx)(t.h2,{id:`odata-v2-json-envelope`,children:`OData V2 JSON envelope`}),`
708
+ `,(0,c.jsx)(t.h3,{id:`fetchodatajson`,children:(0,c.jsx)(t.code,{children:`fetchODataJson`})}),`
709
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`async function fetchODataJson<T = unknown>(
710
+ input: RequestInfo,
711
+ init: RequestInitType = {},
712
+ baseUrlType: BaseURLType = "odata"
713
+ ): Promise<{
714
+ data: T;
715
+ totalCount?: number;
716
+ }>;
717
+ `})}),`
718
+ `,(0,c.jsx)(t.p,{children:`Пример:`}),`
719
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const response = await fetchODataJson<SalesOrderDto[]>(
720
+ "/Z_SALES_ORDER_SRV/SalesOrderSet?$inlinecount=allpages",
721
+ { signal },
722
+ "odata"
723
+ );
724
+
725
+ console.log(response.data);
726
+ console.log(response.totalCount);
727
+ `})}),`
728
+ `,(0,c.jsx)(t.p,{children:`Перед отправкой добавляются:`}),`
729
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`DataServiceVersion: 2.0
730
+ MaxDataServiceVersion: 2.0
731
+ `})}),`
732
+ `,(0,c.jsxs)(t.p,{children:[`Остальные SAP headers и SSO/X-CSRF lifecycle выполняет `,(0,c.jsx)(t.code,{children:`fetchBase`}),`.`]}),`
733
+ `,(0,c.jsx)(t.h3,{id:`collection-envelope`,children:`Collection envelope`}),`
734
+ `,(0,c.jsx)(t.p,{children:`Server:`}),`
735
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{
736
+ "d": {
737
+ "results": [
738
+ {
739
+ "__metadata": { "uri": "..." },
740
+ "OrderId": "42"
741
+ }
742
+ ],
743
+ "__count": "125"
744
+ }
745
+ }
746
+ `})}),`
747
+ `,(0,c.jsx)(t.p,{children:`Client result:`}),`
748
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
749
+ data: [{ OrderId: "42" }],
750
+ totalCount: 125
751
+ }
752
+ `})}),`
753
+ `,(0,c.jsx)(t.p,{children:`Правила:`}),`
754
+ `,(0,c.jsxs)(t.ul,{children:[`
755
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`d.results`}),` должен быть array;`]}),`
756
+ `,(0,c.jsxs)(t.li,{children:[`каждый top-level record получает shallow copy без `,(0,c.jsx)(t.code,{children:`__metadata`}),`;`]}),`
757
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`d.__count`}),` преобразуется через `,(0,c.jsx)(t.code,{children:`Number`}),`;`]}),`
758
+ `,(0,c.jsxs)(t.li,{children:[`отсутствие `,(0,c.jsx)(t.code,{children:`__count`}),` даёт `,(0,c.jsx)(t.code,{children:`totalCount: undefined`}),`;`]}),`
759
+ `,(0,c.jsx)(t.li,{children:`вложенные records не очищаются рекурсивно.`}),`
760
+ `]}),`
761
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`__count`}),` содержит некорректную строку, результатом может быть `,(0,c.jsx)(t.code,{children:`NaN`}),`. Backend contract и consumer validation должны исключать это.`]}),`
762
+ `,(0,c.jsx)(t.h3,{id:`single-envelope`,children:`Single envelope`}),`
763
+ `,(0,c.jsx)(t.p,{children:`Server:`}),`
764
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{
765
+ "d": {
766
+ "__metadata": { "uri": "..." },
767
+ "OrderId": "42"
768
+ }
769
+ }
770
+ `})}),`
771
+ `,(0,c.jsx)(t.p,{children:`Client:`}),`
772
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
773
+ data: { OrderId: "42" }
774
+ }
775
+ `})}),`
776
+ `,(0,c.jsx)(t.h3,{id:`raw-json-fallback`,children:`Raw JSON fallback`}),`
777
+ `,(0,c.jsxs)(t.p,{children:[`Если payload не содержит `,(0,c.jsx)(t.code,{children:`d`}),`, transport возвращает JSON как есть:`]}),`
778
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{ "ok": true }
779
+ `})}),`
780
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{ data: { ok: true } }
781
+ `})}),`
782
+ `,(0,c.jsx)(t.p,{children:`Это позволяет использовать transport с SAP endpoint, который не оборачивает ответ в стандартный OData envelope.`}),`
783
+ `,(0,c.jsx)(t.h3,{id:`content-type`,children:`Content-Type`}),`
784
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`Content-Type`}),` содержит:`]}),`
785
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`application/json
786
+ application/odata+json
787
+ `})}),`
788
+ `,(0,c.jsxs)(t.p,{children:[`используется `,(0,c.jsx)(t.code,{children:`response.json()`}),`.`]}),`
789
+ `,(0,c.jsx)(t.p,{children:`Если header другой, body сначала читается как text. Затем:`}),`
790
+ `,(0,c.jsxs)(t.ol,{children:[`
791
+ `,(0,c.jsx)(t.li,{children:`HTML проверяется на SSO form.`}),`
792
+ `,(0,c.jsx)(t.li,{children:`Неожиданный HTML превращается в transport error report и Error.`}),`
793
+ `,(0,c.jsxs)(t.li,{children:[`Остальной text пробуется через `,(0,c.jsx)(t.code,{children:`JSON.parse`}),`.`]}),`
794
+ `,(0,c.jsx)(t.li,{children:`Если JSON parse невозможен, бросается ошибка unsupported content/body.`}),`
795
+ `]}),`
796
+ `,(0,c.jsxs)(t.p,{children:[`Поэтому JSON с ошибочным `,(0,c.jsx)(t.code,{children:`Content-Type: text/plain`}),` всё ещё может быть разобран.`]}),`
797
+ `,(0,c.jsxs)(t.h3,{id:`204-и-205`,children:[(0,c.jsx)(t.code,{children:`204`}),` и `,(0,c.jsx)(t.code,{children:`205`})]}),`
798
+ `,(0,c.jsxs)(t.p,{children:[`Для response без body transport возвращает специальный marker в `,(0,c.jsx)(t.code,{children:`data`}),`.`]}),`
799
+ `,(0,c.jsx)(t.p,{children:`Проверка:`}),`
800
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const response = await fetchODataJson<unknown>(url, init);
801
+
802
+ if (isNoContentResponse(response.data)) {
803
+ return;
804
+ }
805
+
806
+ // Здесь response.data не является no-content marker.
807
+ `})}),`
808
+ `,(0,c.jsxs)(t.p,{children:[`Проверяйте `,(0,c.jsx)(t.code,{children:`response.data`}),`, не весь `,(0,c.jsx)(t.code,{children:`response`}),`:`]}),`
809
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Неверно.
810
+ isNoContentResponse(response);
811
+
812
+ // Правильно.
813
+ isNoContentResponse(response.data);
814
+ `})}),`
815
+ `,(0,c.jsx)(t.h3,{id:`fetchjson`,children:(0,c.jsx)(t.code,{children:`fetchJson`})}),`
816
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`async function fetchJson<T = unknown>(
817
+ input: RequestInfo,
818
+ init: RequestInitType = {},
819
+ baseUrlType: BaseURLType = "odata"
820
+ ): Promise<T>;
821
+ `})}),`
822
+ `,(0,c.jsx)(t.p,{children:`Это короткая форма:`}),`
823
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const response = await fetchODataJson<T>(input, init, baseUrlType);
824
+ return response.data;
825
+ `})}),`
826
+ `,(0,c.jsx)(t.p,{children:`Пример:`}),`
827
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const orders = await fetchJson<SalesOrderDto[]>(
828
+ "/Z_SALES_ORDER_SRV/SalesOrderSet"
829
+ );
830
+ `})}),`
831
+ `,(0,c.jsx)(t.p,{children:`Ограничения:`}),`
832
+ `,(0,c.jsxs)(t.ul,{children:[`
833
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`totalCount`}),` теряется;`]}),`
834
+ `,(0,c.jsx)(t.li,{children:`no-content marker типизируется как переданный generic;`}),`
835
+ `,(0,c.jsx)(t.li,{children:`generic не проверяет payload;`}),`
836
+ `,(0,c.jsx)(t.li,{children:`metadata target не проверяется.`}),`
837
+ `]}),`
838
+ `,(0,c.jsxs)(t.p,{children:[`Если нужен paging count, используйте `,(0,c.jsx)(t.code,{children:`fetchODataJson`}),`, `,(0,c.jsx)(t.code,{children:`odataFetch`}),` или metadata-aware runner.`]}),`
839
+ `,(0,c.jsx)(t.h3,{id:`fetchqueryfn`,children:(0,c.jsx)(t.code,{children:`fetchQueryFn`})}),`
840
+ `,(0,c.jsx)(t.p,{children:`Factory создаёт функцию для TanStack Query или ручного вызова:`}),`
841
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const loadMetadataXml = fetchQueryFn("/Z_SALES_ORDER_SRV/$metadata", {
842
+ baseUrl: "odata",
843
+ init: {
844
+ headers: {
845
+ Accept: "application/xml"
846
+ }
847
+ },
848
+ transform: async (response) => response.text()
849
+ });
850
+
851
+ const xml = await loadMetadataXml({ signal });
852
+ `})}),`
853
+ `,(0,c.jsx)(t.p,{children:`Options:`}),`
854
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
855
+ init?: RequestInit;
856
+ baseUrl?: BaseURLType;
857
+ swCache?: string;
858
+ transform: (data: Response) => Promise<R>;
859
+ }
860
+ `})}),`
861
+ `,(0,c.jsx)(t.p,{children:`Поведение signal:`}),`
862
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`options.init.signal есть? ── да ──► используется он
863
+
864
+ нет
865
+
866
+ params.signal есть? ─────── да ──► используется текущий signal
867
+ `})}),`
868
+ `,(0,c.jsxs)(t.p,{children:[`Для каждого запуска создаётся новый shallow `,(0,c.jsx)(t.code,{children:`RequestInit`}),`, поэтому отменённый signal предыдущего refetch не сохраняется в следующем запуске.`]}),`
869
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`transform`}),` отсутствует в runtime из-за unsafe cast/JavaScript, функция бросит:`]}),`
870
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`fetchQueryFn requires a transformation function!
871
+ `})}),`
872
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`swCache`}),` добавляет header `,(0,c.jsx)(t.code,{children:`x-sw-cache`}),`; сам transport не реализует Service Worker cache.`]}),`
873
+ `,(0,c.jsxs)(t.h2,{id:`низкоуровневый-odatafetch`,children:[`Низкоуровневый `,(0,c.jsx)(t.code,{children:`odataFetch`})]}),`
874
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`async function odataFetch<T, I = T>(
875
+ entitySetPath: string,
876
+ options: ODataFetchOptions<T> = {},
877
+ init: RequestInit = {}
878
+ ): Promise<{
879
+ data: I;
880
+ totalCount?: number;
881
+ }>;
882
+ `})}),`
883
+ `,(0,c.jsx)(t.p,{children:`Здесь:`}),`
884
+ `,(0,c.jsxs)(t.ul,{children:[`
885
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`T`}),` типизирует field names в filter/sort/select/expand;`]}),`
886
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`I`}),` описывает ожидаемый result `,(0,c.jsx)(t.code,{children:`data`}),`;`]}),`
887
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`entitySetPath`}),` уже должен быть готовым OData path;`]}),`
888
+ `,(0,c.jsx)(t.li,{children:`metadata автоматически не загружаются.`}),`
889
+ `]}),`
890
+ `,(0,c.jsx)(t.h3,{id:`полный-пример`,children:`Полный пример`}),`
891
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type SalesOrderRow = {
892
+ CompanyId: string;
893
+ OrderId: string;
894
+ Title: string;
895
+ Amount: string;
896
+ CreatedAt: string;
897
+ Customer: CustomerDto;
898
+ };
899
+
900
+ const response = await odataFetch<SalesOrderRow, SalesOrderRow[]>(
901
+ "/Z_SALES_ORDER_SRV/SalesOrderSet",
902
+ {
903
+ expression: createFilterEqual<SalesOrderRow, string>(
904
+ "CompanyId",
905
+ "1000"
906
+ ),
907
+ sorts: [
908
+ { key: "CreatedAt", desc: true },
909
+ { key: "OrderId" }
910
+ ],
911
+ select: ["OrderId", "Title", "Amount", "CreatedAt"],
912
+ expand: ["Customer"],
913
+ top: 50,
914
+ skip: 0,
915
+ inlinecount: "allpages",
916
+ baseUrl: "odata",
917
+ swCache: "off"
918
+ },
919
+ { signal }
920
+ );
921
+ `})}),`
922
+ `,(0,c.jsx)(t.p,{children:`Условно будет построен path:`}),`
923
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/SalesOrderSet
924
+ ?$filter=CompanyId eq '1000'
925
+ &$orderby=CreatedAt desc,OrderId asc
926
+ &$select=OrderId,Title,Amount,CreatedAt
927
+ &$expand=Customer
928
+ &$top=50
929
+ &$skip=0
930
+ &$inlinecount=allpages
931
+ `})}),`
932
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`URLSearchParams`}),` кодирует query string. В фактическом URL `,(0,c.jsx)(t.code,{children:`$`}),` может отображаться как `,(0,c.jsx)(t.code,{children:`%24`}),`, пробел — как `,(0,c.jsx)(t.code,{children:`+`}),` или percent encoding. Это нормально.`]}),`
933
+ `,(0,c.jsx)(t.h3,{id:`expression`,children:(0,c.jsx)(t.code,{children:`expression`})}),`
934
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`expression?: FilterExpression<T>;
935
+ `})}),`
936
+ `,(0,c.jsxs)(t.p,{children:[`Используйте filter factories `,(0,c.jsx)(t.code,{children:`foundation-lib/odata-service`}),`:`]}),`
937
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const expression = {
938
+ and: true,
939
+ filters: [
940
+ createFilterEqual<SalesOrderRow, string>("CompanyId", "1000"),
941
+ createFilterBetween<SalesOrderRow, Date>("CreatedAt", [dateFrom, dateTo])
942
+ ]
943
+ };
944
+ `})}),`
945
+ `,(0,c.jsxs)(t.p,{children:[`Не собирайте `,(0,c.jsx)(t.code,{children:`$filter`}),` ручной конкатенацией:`]}),`
946
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:"// Небезопасно и легко сломать quoting/escaping.\nconst filter = `CompanyId eq '${userInput}'`;\n"})}),`
947
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`buildODataFilter`}),` вызывается внутри `,(0,c.jsx)(t.code,{children:`odataFetch`}),`.`]}),`
948
+ `,(0,c.jsx)(t.h3,{id:`sorts`,children:(0,c.jsx)(t.code,{children:`sorts`})}),`
949
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`sorts?: Sort<keyof T>[];
950
+ `})}),`
951
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`sorts: [
952
+ { key: "CreatedAt", desc: true },
953
+ { key: "OrderId" }
954
+ ]
955
+ `})}),`
956
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`desc`}),` имеет literal type `,(0,c.jsx)(t.code,{children:`true`}),`. Ascending задаётся отсутствием поля:`]}),`
957
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{ key: "OrderId" } // asc.
958
+ `})}),`
959
+ `,(0,c.jsxs)(t.p,{children:[`Не передавайте `,(0,c.jsx)(t.code,{children:`{ desc: false }`}),`, если точный TypeScript type это запрещает.`]}),`
960
+ `,(0,c.jsx)(t.h3,{id:`select`,children:(0,c.jsx)(t.code,{children:`select`})}),`
961
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`select?: (keyof T)[];
962
+ `})}),`
963
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`select: ["OrderId", "Title"]
964
+ `})}),`
965
+ `,(0,c.jsx)(t.p,{children:`Values соединяются через запятую. Runtime metadata validation имён не выполняется.`}),`
966
+ `,(0,c.jsx)(t.p,{children:`Пустой array создаст пустое значение option. Лучше вообще не передавать поле:`}),`
967
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`select: selectedFields.length ? selectedFields : undefined
968
+ `})}),`
969
+ `,(0,c.jsx)(t.h3,{id:`expand`,children:(0,c.jsx)(t.code,{children:`expand`})}),`
970
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`expand?: (keyof T)[];
971
+ `})}),`
972
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`expand: ["Customer"]
973
+ `})}),`
974
+ `,(0,c.jsxs)(t.p,{children:[`Helper только соединяет имена через запятую. Он не проверяет, является поле navigation property, и не делает deep `,(0,c.jsx)(t.code,{children:`$select`}),` syntax.`]}),`
975
+ `,(0,c.jsxs)(t.h3,{id:`top-и-skip`,children:[(0,c.jsx)(t.code,{children:`top`}),` и `,(0,c.jsx)(t.code,{children:`skip`})]}),`
976
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`top?: number;
977
+ skip?: number;
978
+ `})}),`
979
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`0`}),` не отбрасывается:`]}),`
980
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{ top: 50, skip: 0 }
981
+ `})}),`
982
+ `,(0,c.jsxs)(t.p,{children:[`Helper не запрещает отрицательные, дробные, `,(0,c.jsx)(t.code,{children:`NaN`}),` и слишком большие values. Нормализуйте paging на application boundary.`]}),`
983
+ `,(0,c.jsx)(t.h3,{id:`inlinecount`,children:(0,c.jsx)(t.code,{children:`inlinecount`})}),`
984
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`inlinecount?: string;
985
+ `})}),`
986
+ `,(0,c.jsx)(t.p,{children:`Тип намеренно широкий. Для OData V2 paging обычно:`}),`
987
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`inlinecount: "allpages"
988
+ `})}),`
989
+ `,(0,c.jsxs)(t.p,{children:[`При корректном SAP response `,(0,c.jsx)(t.code,{children:`d.__count`}),` станет `,(0,c.jsx)(t.code,{children:`totalCount`}),`.`]}),`
990
+ `,(0,c.jsx)(t.h3,{id:`format`,children:(0,c.jsx)(t.code,{children:`format`})}),`
991
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`format?: "json";
992
+ `})}),`
993
+ `,(0,c.jsxs)(t.p,{children:[`Поле существует в public type, но текущий request builder не добавляет `,(0,c.jsx)(t.code,{children:`$format`}),` в URL. Не полагайтесь на него. JSON transport определяется headers и response parser-ом.`]}),`
994
+ `,(0,c.jsx)(t.h3,{id:`baseurl`,children:(0,c.jsx)(t.code,{children:`baseUrl`})}),`
995
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`baseUrl?: BaseURLType;
996
+ `})}),`
997
+ `,(0,c.jsxs)(t.p,{children:[`Передаётся в `,(0,c.jsx)(t.code,{children:`fetchODataJson`}),`. Если отсутствует, transport использует `,(0,c.jsx)(t.code,{children:`odata`}),`.`]}),`
998
+ `,(0,c.jsx)(t.h3,{id:`swcache`,children:(0,c.jsx)(t.code,{children:`swCache`})}),`
999
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`swCache?: string;
1000
+ `})}),`
1001
+ `,(0,c.jsx)(t.p,{children:`Если value truthy, request получает:`}),`
1002
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`x-sw-cache: <value>
1003
+ `})}),`
1004
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`init.headers`}),` уже содержит `,(0,c.jsx)(t.code,{children:`x-sw-cache`}),`, option его перезапишет.`]}),`
1005
+ `,(0,c.jsx)(t.h3,{id:`existing-query-string`,children:`Existing query string`}),`
1006
+ `,(0,c.jsx)(t.p,{children:`Можно передать path с query:`}),`
1007
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`await odataFetch("/Z_DEMO_SRV/Run?mode='full'", {
1008
+ top: 10
1009
+ });
1010
+ `})}),`
1011
+ `,(0,c.jsxs)(t.p,{children:[`Helper сохраняет существующие параметры и добавляет OData options через `,(0,c.jsx)(t.code,{children:`URLSearchParams`}),`.`]}),`
1012
+ `,(0,c.jsx)(t.p,{children:`Не передавайте fragment/hash: он не является частью HTTP query OData endpoint.`}),`
1013
+ `,(0,c.jsx)(t.h3,{id:`trailing-slash`,children:`Trailing slash`}),`
1014
+ `,(0,c.jsx)(t.p,{children:`Trailing slash raw path удаляется:`}),`
1015
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_DEMO_SRV/EntitySet/ -> /Z_DEMO_SRV/EntitySet
1016
+ `})}),`
1017
+ `,(0,c.jsxs)(t.h3,{id:`что-odatafetch-не-проверяет`,children:[`Что `,(0,c.jsx)(t.code,{children:`odataFetch`}),` не проверяет`]}),`
1018
+ `,(0,c.jsxs)(t.ul,{children:[`
1019
+ `,(0,c.jsx)(t.li,{children:`существование service;`}),`
1020
+ `,(0,c.jsx)(t.li,{children:`существование target;`}),`
1021
+ `,(0,c.jsx)(t.li,{children:`обязательные parameters;`}),`
1022
+ `,(0,c.jsx)(t.li,{children:`OData type parameters;`}),`
1023
+ `,(0,c.jsx)(t.li,{children:`совместимость operation и target;`}),`
1024
+ `,(0,c.jsx)(t.li,{children:`корректность select/expand field names;`}),`
1025
+ `,(0,c.jsx)(t.li,{children:`runtime shape response.`}),`
1026
+ `]}),`
1027
+ `,(0,c.jsx)(t.p,{children:`Для обычного application code предпочитайте metadata-aware helpers.`}),`
1028
+ `,(0,c.jsx)(t.h2,{id:`metadata-aware-operation-helpers`,children:`Metadata-aware operation helpers`}),`
1029
+ `,(0,c.jsx)(t.p,{children:`Публичные specialized helpers:`}),`
1030
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn
1031
+ odataReadFn
1032
+ odataCreateFn
1033
+ odataUpdateFn
1034
+ odataDeleteFn
1035
+ odataFunctionImportFn
1036
+ `})}),`
1037
+ `,(0,c.jsxs)(t.p,{children:[`Внутренний общий `,(0,c.jsx)(t.code,{children:`odataFetchFn`}),` не экспортируется public entrypoint. Не делайте deep import.`]}),`
1038
+ `,(0,c.jsx)(t.h3,{id:`runner-contract`,children:`Runner contract`}),`
1039
+ `,(0,c.jsx)(t.p,{children:`Каждый helper возвращает функцию такого смысла:`}),`
1040
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ODataRunner<TData> = (context: {
1041
+ client: QueryClient;
1042
+ signal?: AbortSignal;
1043
+ }) => Promise<{
1044
+ data: TData;
1045
+ totalCount?: number;
1046
+ }>;
1047
+ `})}),`
1048
+ `,(0,c.jsx)(t.p,{children:`Прямой вызов:`}),`
1049
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataReadFn<SalesOrderDto>({
1050
+ odata: {
1051
+ service: "Z_SALES_ORDER_SRV",
1052
+ target: "SalesOrderSet"
1053
+ },
1054
+ params: wrapODataParams({
1055
+ OrderId: "42"
1056
+ })
1057
+ });
1058
+
1059
+ const result = await runner({
1060
+ client: queryClient,
1061
+ signal: abortController.signal
1062
+ });
1063
+ `})}),`
1064
+ `,(0,c.jsx)(t.h3,{id:`общие-options`,children:`Общие options`}),`
1065
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
1066
+ odata: {
1067
+ service: string;
1068
+ target: string;
1069
+ };
1070
+ options?: ODataFetchOptions<I>;
1071
+ init?: Omit<RequestInit, "signal" | "method" | "body">;
1072
+ autoParse?: boolean;
1073
+ swCache?: string;
1074
+ transform?: (...args) => output;
1075
+ }
1076
+ `})}),`
1077
+ `,(0,c.jsxs)(t.p,{children:[`Почему `,(0,c.jsx)(t.code,{children:`init`}),` не разрешает `,(0,c.jsx)(t.code,{children:`signal`}),`, `,(0,c.jsx)(t.code,{children:`method`}),`, `,(0,c.jsx)(t.code,{children:`body`}),`:`]}),`
1078
+ `,(0,c.jsxs)(t.ul,{children:[`
1079
+ `,(0,c.jsx)(t.li,{children:`signal принадлежит конкретному запуску Query;`}),`
1080
+ `,(0,c.jsx)(t.li,{children:`method определяется semantic operation и metadata;`}),`
1081
+ `,(0,c.jsx)(t.li,{children:`body принадлежит create/update contract.`}),`
1082
+ `]}),`
1083
+ `,(0,c.jsx)(t.p,{children:`Это защищает от противоречивой конфигурации.`}),`
1084
+ `,(0,c.jsx)(t.h3,{id:`odatafetchfnrequest`,children:(0,c.jsx)(t.code,{children:`ODataFetchFnRequest`})}),`
1085
+ `,(0,c.jsx)(t.p,{children:`Public conditional type позволяет типизировать config по operation:`}),`
1086
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type UpdateRequest = ODataFetchFnRequest<
1087
+ "update",
1088
+ SalesOrderDto,
1089
+ SalesOrder,
1090
+ UpdateSalesOrderBody,
1091
+ { OrderId: string }
1092
+ >;
1093
+ `})}),`
1094
+ `,(0,c.jsx)(t.p,{children:`Обычно его не нужно писать вручную: specialized helper выводит options type.`}),`
1095
+ `,(0,c.jsx)(t.h3,{id:`generic-parameters`,children:`Generic parameters`}),`
1096
+ `,(0,c.jsx)(t.p,{children:`Упрощённо:`}),`
1097
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Helper`}),(0,c.jsx)(t.th,{children:`Generics`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataQueryFn`})}),(0,c.jsxs)(t.td,{children:[`input row `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, params `,(0,c.jsx)(t.code,{children:`P`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataReadFn`})}),(0,c.jsxs)(t.td,{children:[`input `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, params `,(0,c.jsx)(t.code,{children:`P`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataCreateFn`})}),(0,c.jsxs)(t.td,{children:[`input response `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, body `,(0,c.jsx)(t.code,{children:`T`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataUpdateFn`})}),(0,c.jsxs)(t.td,{children:[`input response `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, body `,(0,c.jsx)(t.code,{children:`T`}),`, params `,(0,c.jsx)(t.code,{children:`P`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDeleteFn`})}),(0,c.jsxs)(t.td,{children:[`input response `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, params `,(0,c.jsx)(t.code,{children:`P`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFunctionImportFn`})}),(0,c.jsxs)(t.td,{children:[`input response `,(0,c.jsx)(t.code,{children:`I`}),`, output `,(0,c.jsx)(t.code,{children:`O`}),`, params `,(0,c.jsx)(t.code,{children:`P`})]})]})]})]}),`
1098
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`I`}),` — не «тип backend, доказанный библиотекой». Это compile-time ожидание consumer-а. Runtime payload остаётся внешним значением.`]}),`
1099
+ `,(0,c.jsx)(t.h3,{id:`metadata-target-lookup`,children:`Metadata target lookup`}),`
1100
+ `,(0,c.jsxs)(t.p,{children:[`Перед request helper получает service metadata и ищет `,(0,c.jsx)(t.code,{children:`odata.target`}),`:`]}),`
1101
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`metadata.entities[target]
1102
+ metadata.functionImports[target]
1103
+ `})}),`
1104
+ `,(0,c.jsx)(t.p,{children:`Ошибки:`}),`
1105
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`OData target '<name>' не был загружен
1106
+ `})}),`
1107
+ `,(0,c.jsx)(t.p,{children:`Если одно имя одновременно найдено как Entity и FunctionImport:`}),`
1108
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Найден конфликт metadata: '<name>' существует и как Entity, и как FunctionImport
1109
+ `})}),`
1110
+ `,(0,c.jsx)(t.h3,{id:`operation-compatibility`,children:`Operation compatibility`}),`
1111
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Metadata target`}),(0,c.jsx)(t.th,{children:`Разрешённые helpers`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Plain Entity`}),(0,c.jsx)(t.td,{children:`query/read/create/update/delete`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Parameterized Entity с `,(0,c.jsx)(t.code,{children:`result`})]}),(0,c.jsx)(t.td,{children:`query/create/update/delete; read запрещён`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`FunctionImport`}),(0,c.jsx)(t.td,{children:`только function import helper`})]})]})]}),`
1112
+ `,(0,c.jsxs)(t.p,{children:[`Create/update/delete для parameterized Entity технически не запрещены общей validation только из-за `,(0,c.jsx)(t.code,{children:`result`}),`, но использовать их следует лишь если реальный SAP contract это поддерживает. Metadata и backend остаются источником истины.`]}),`
1113
+ `,(0,c.jsx)(t.h3,{id:`result-и-no-content`,children:`Result и no-content`}),`
1114
+ `,(0,c.jsx)(t.p,{children:`Query обычно возвращает array. Single helpers возвращают single value.`}),`
1115
+ `,(0,c.jsxs)(t.p,{children:[`Для backend operation с `,(0,c.jsx)(t.code,{children:`204/205`}),` безопаснее типизировать data как `,(0,c.jsx)(t.code,{children:`unknown`}),` и проверить marker:`]}),`
1116
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const result = await odataDeleteFn<unknown>({
1117
+ odata,
1118
+ params
1119
+ })({ client: queryClient });
1120
+
1121
+ if (isNoContentResponse(result.data)) {
1122
+ // Удаление успешно, response body отсутствует.
1123
+ }
1124
+ `})}),`
1125
+ `,(0,c.jsx)(t.p,{children:`Не ожидайте updated entity от endpoint, который документирован как no-content.`}),`
1126
+ `,(0,c.jsxs)(t.h2,{id:`odataqueryfn-списки-и-parameterized-query`,children:[(0,c.jsx)(t.code,{children:`odataQueryFn`}),`: списки и parameterized query`]}),`
1127
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataQueryFn`}),` — основной helper чтения коллекций.`]}),`
1128
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataQueryFn<I, O, P>({
1129
+ odata,
1130
+ params?,
1131
+ options?,
1132
+ init?,
1133
+ autoParse?,
1134
+ swCache?,
1135
+ transform?
1136
+ });
1137
+ `})}),`
1138
+ `,(0,c.jsxs)(t.p,{children:[`HTTP method всегда `,(0,c.jsx)(t.code,{children:`GET`}),`.`]}),`
1139
+ `,(0,c.jsx)(t.h3,{id:`plain-entity-query`,children:`Plain Entity query`}),`
1140
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const loadOrders = odataQueryFn<SalesOrderDto>({
1141
+ odata: {
1142
+ service: "Z_SALES_ORDER_SRV",
1143
+ target: "SalesOrderSet"
1144
+ },
1145
+ options: {
1146
+ top: 50,
1147
+ inlinecount: "allpages"
1148
+ }
1149
+ });
1150
+ `})}),`
1151
+ `,(0,c.jsx)(t.p,{children:`Path:`}),`
1152
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/SalesOrderSet?$top=50&$inlinecount=allpages
1153
+ `})}),`
1154
+ `,(0,c.jsxs)(t.p,{children:[`Для plain Entity `,(0,c.jsx)(t.code,{children:`params`}),` игнорируются:`]}),`
1155
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn({
1156
+ odata: {
1157
+ service: "Z_SALES_ORDER_SRV",
1158
+ target: "SalesOrderSet"
1159
+ },
1160
+ params: wrapODataParams({ OrderId: "42" })
1161
+ });
1162
+ `})}),`
1163
+ `,(0,c.jsxs)(t.p,{children:[`Это не прочитает entity `,(0,c.jsx)(t.code,{children:`42`}),`; request останется collection query. В dev runtime выводится `,(0,c.jsx)(t.code,{children:`console.warn`}),`.`]}),`
1164
+ `,(0,c.jsxs)(t.p,{children:[`Для одной entity используйте `,(0,c.jsx)(t.code,{children:`odataReadFn`}),`.`]}),`
1165
+ `,(0,c.jsx)(t.h3,{id:`parameterized-entity-query`,children:`Parameterized Entity query`}),`
1166
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ReportParams = {
1167
+ CompanyId: string;
1168
+ DateFrom: Date;
1169
+ DateTo: Date;
1170
+ };
1171
+
1172
+ const loadReport = odataQueryFn<ReportRowDto, ReportRow, ReportParams>({
1173
+ odata: {
1174
+ service: "Z_REPORT_SRV",
1175
+ target: "SalesReport"
1176
+ },
1177
+ params: wrapODataParams({
1178
+ CompanyId: "1000",
1179
+ DateFrom: new Date(2026, 0, 1),
1180
+ DateTo: new Date(2026, 0, 31)
1181
+ }),
1182
+ options: {
1183
+ top: 100,
1184
+ inlinecount: "allpages"
1185
+ },
1186
+ autoParse: true,
1187
+ transform: (rows) => rows.map(mapReportRow)
1188
+ });
1189
+ `})}),`
1190
+ `,(0,c.jsxs)(t.p,{children:[`Если metadata target содержит `,(0,c.jsx)(t.code,{children:`result: "Results"`}),`, path будет похож на:`]}),`
1191
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_REPORT_SRV/SalesReport(
1192
+ CompanyId='1000',
1193
+ DateFrom=datetime'2026-01-01T00:00:00',
1194
+ DateTo=datetime'2026-01-31T00:00:00'
1195
+ )/Results?$top=100&$inlinecount=allpages
1196
+ `})}),`
1197
+ `,(0,c.jsxs)(t.p,{children:[`Parameters форматируются по metadata, поэтому нужен `,(0,c.jsx)(t.code,{children:`wrapODataParams`}),`.`]}),`
1198
+ `,(0,c.jsx)(t.h3,{id:`query-transform-в-array`,children:`Query transform в array`}),`
1199
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataQueryFn<SalesOrderDto, SalesOrder>({
1200
+ odata,
1201
+ transform: (rows, targetMetadata) => {
1202
+ console.log(targetMetadata);
1203
+ return rows.map(mapSalesOrder);
1204
+ }
1205
+ });
1206
+ `})}),`
1207
+ `,(0,c.jsx)(t.p,{children:`Result:`}),`
1208
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`Promise<{
1209
+ data: SalesOrder[];
1210
+ totalCount?: number;
1211
+ }>
1212
+ `})}),`
1213
+ `,(0,c.jsx)(t.h3,{id:`query-transform-в-одно-значение`,children:`Query transform в одно значение`}),`
1214
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataQueryFn<SalesOrderDto, number>({
1215
+ odata,
1216
+ transform: (rows) => rows.reduce(
1217
+ (sum, row) => sum + row.Amount,
1218
+ 0
1219
+ )
1220
+ });
1221
+ `})}),`
1222
+ `,(0,c.jsx)(t.p,{children:`Result data — number:`}),`
1223
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const result = await runner({ client: queryClient });
1224
+ console.log(result.data);
1225
+ `})}),`
1226
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`totalCount`}),` при этом всё ещё описывает server collection, а не агрегированный output transform.`]}),`
1227
+ `,(0,c.jsxs)(t.h3,{id:`query-и-204`,children:[`Query и `,(0,c.jsx)(t.code,{children:`204`})]}),`
1228
+ `,(0,c.jsx)(t.p,{children:`Collection query по нормальному OData contract должен возвращать JSON body. Если backend вернул no-content, рассматривайте это как protocol anomaly:`}),`
1229
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const result = await runner({ client: queryClient });
1230
+
1231
+ if (isNoContentResponse(result.data)) {
1232
+ throw new Error("Collection query вернул ответ без body");
1233
+ }
1234
+ `})}),`
1235
+ `,(0,c.jsxs)(t.h2,{id:`odatareadfn-одна-entity`,children:[(0,c.jsx)(t.code,{children:`odataReadFn`}),`: одна Entity`]}),`
1236
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataReadFn`}),` читает одну Entity по key/parameters.`]}),`
1237
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataReadFn<I, O, P>({
1238
+ odata,
1239
+ params,
1240
+ options?,
1241
+ init?,
1242
+ autoParse?,
1243
+ swCache?,
1244
+ transform?
1245
+ });
1246
+ `})}),`
1247
+ `,(0,c.jsxs)(t.p,{children:[`HTTP method — `,(0,c.jsx)(t.code,{children:`GET`}),`.`]}),`
1248
+ `,(0,c.jsx)(t.h3,{id:`простой-key`,children:`Простой key`}),`
1249
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type SalesOrderKey = {
1250
+ OrderId: string;
1251
+ };
1252
+
1253
+ const readSalesOrder = odataReadFn<
1254
+ SalesOrderDto,
1255
+ SalesOrder,
1256
+ SalesOrderKey
1257
+ >({
1258
+ odata: {
1259
+ service: "Z_SALES_ORDER_SRV",
1260
+ target: "SalesOrderSet"
1261
+ },
1262
+ params: wrapODataParams({
1263
+ OrderId: "42"
1264
+ }),
1265
+ autoParse: true,
1266
+ transform: (dto) => mapSalesOrder(dto)
1267
+ });
1268
+ `})}),`
1269
+ `,(0,c.jsx)(t.p,{children:`Path:`}),`
1270
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/SalesOrderSet(OrderId='42')
1271
+ `})}),`
1272
+ `,(0,c.jsx)(t.h3,{id:`составной-key`,children:`Составной key`}),`
1273
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const readItem = odataReadFn<OrderItemDto>({
1274
+ odata: {
1275
+ service: "Z_SALES_ORDER_SRV",
1276
+ target: "SalesOrderItemSet"
1277
+ },
1278
+ params: wrapODataParams({
1279
+ OrderId: "42",
1280
+ ItemNo: "10"
1281
+ })
1282
+ });
1283
+ `})}),`
1284
+ `,(0,c.jsx)(t.p,{children:`Path:`}),`
1285
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/SalesOrderItemSet(OrderId='42',ItemNo='10')
1286
+ `})}),`
1287
+ `,(0,c.jsxs)(t.p,{children:[`Точный порядок, mandatory rules, quoting и OData literals определяет metadata builder из `,(0,c.jsx)(t.code,{children:`foundation-lib/odata-service`}),`.`]}),`
1288
+ `,(0,c.jsx)(t.h3,{id:`почему-нельзя-передать-обычный-object`,children:`Почему нельзя передать обычный object`}),`
1289
+ `,(0,c.jsx)(t.p,{children:`Неверно:`}),`
1290
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataReadFn({
1291
+ odata,
1292
+ params: {
1293
+ OrderId: "42"
1294
+ }
1295
+ });
1296
+ `})}),`
1297
+ `,(0,c.jsx)(t.p,{children:`Правильно:`}),`
1298
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`params: wrapODataParams({
1299
+ OrderId: "42"
1300
+ })
1301
+ `})}),`
1302
+ `,(0,c.jsxs)(t.p,{children:[`Обёртка `,(0,c.jsx)(t.code,{children:`{ value }`}),` отделяет OData parameter contract от произвольного JS object и позволяет metadata builder обработать optional/formatter semantics.`]}),`
1303
+ `,(0,c.jsx)(t.h3,{id:`parameterized-entity-запрещена`,children:`Parameterized Entity запрещена`}),`
1304
+ `,(0,c.jsxs)(t.p,{children:[`Если metadata Entity содержит `,(0,c.jsx)(t.code,{children:`result`}),`, `,(0,c.jsx)(t.code,{children:`odataReadFn`}),` бросит ошибку:`]}),`
1305
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`OData operation 'read' несовместима с parameterized query entity ...
1306
+ Для target с result='Results' используйте method: 'query'.
1307
+ `})}),`
1308
+ `,(0,c.jsxs)(t.p,{children:[`Используйте `,(0,c.jsx)(t.code,{children:`odataQueryFn`}),` с params.`]}),`
1309
+ `,(0,c.jsx)(t.h3,{id:`query-options-у-read`,children:`Query options у read`}),`
1310
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`options`}),` поддерживаются и для read:`]}),`
1311
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataReadFn<SalesOrderDto>({
1312
+ odata,
1313
+ params,
1314
+ options: {
1315
+ select: ["OrderId", "Title"],
1316
+ expand: ["Customer"]
1317
+ }
1318
+ });
1319
+ `})}),`
1320
+ `,(0,c.jsxs)(t.p,{children:[`Но `,(0,c.jsx)(t.code,{children:`$top`}),`, `,(0,c.jsx)(t.code,{children:`$skip`}),`, `,(0,c.jsx)(t.code,{children:`$inlinecount`}),` обычно не имеют смысла для одной entity. Используйте только options, поддерживаемые backend contract.`]}),`
1321
+ `,(0,c.jsxs)(t.h2,{id:`odatacreatefn-создание`,children:[(0,c.jsx)(t.code,{children:`odataCreateFn`}),`: создание`]}),`
1322
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataCreateFn`}),` создаёт Entity.`]}),`
1323
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataCreateFn<I, O, T>({
1324
+ odata,
1325
+ body,
1326
+ options?,
1327
+ init?,
1328
+ autoParse?,
1329
+ swCache?,
1330
+ transform?
1331
+ });
1332
+ `})}),`
1333
+ `,(0,c.jsxs)(t.p,{children:[`HTTP method — `,(0,c.jsx)(t.code,{children:`POST`}),`.`]}),`
1334
+ `,(0,c.jsx)(t.h3,{id:`типы-body-и-response-отдельно`,children:`Типы body и response отдельно`}),`
1335
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type CreateSalesOrderBody = {
1336
+ CompanyId: string;
1337
+ Title: string;
1338
+ };
1339
+
1340
+ type CreatedSalesOrderDto = {
1341
+ OrderId: string;
1342
+ CompanyId: string;
1343
+ Title: string;
1344
+ CreatedAt: Date;
1345
+ };
1346
+
1347
+ const createSalesOrder = odataCreateFn<
1348
+ CreatedSalesOrderDto,
1349
+ SalesOrder,
1350
+ CreateSalesOrderBody
1351
+ >({
1352
+ odata: {
1353
+ service: "Z_SALES_ORDER_SRV",
1354
+ target: "SalesOrderSet"
1355
+ },
1356
+ body: {
1357
+ CompanyId: "1000",
1358
+ Title: "Новый заказ"
1359
+ },
1360
+ autoParse: true,
1361
+ transform: mapSalesOrder
1362
+ });
1363
+ `})}),`
1364
+ `,(0,c.jsx)(t.p,{children:`Path/body:`}),`
1365
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`POST /Z_SALES_ORDER_SRV/SalesOrderSet
1366
+ Content-Type: application/json
1367
+
1368
+ {"CompanyId":"1000","Title":"Новый заказ"}
1369
+ `})}),`
1370
+ `,(0,c.jsxs)(t.h3,{id:`params-запрещены`,children:[(0,c.jsx)(t.code,{children:`params`}),` запрещены`]}),`
1371
+ `,(0,c.jsx)(t.p,{children:`Create path строится без entity params:`}),`
1372
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/SERVICE/ENTITY
1373
+ `})}),`
1374
+ `,(0,c.jsxs)(t.p,{children:[`Если backend публикует «создание» как FunctionImport, используйте `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`, а не пытайтесь добавить params к create.`]}),`
1375
+ `,(0,c.jsx)(t.h3,{id:`body-должен-быть-json-compatible`,children:`Body должен быть JSON-compatible`}),`
1376
+ `,(0,c.jsxs)(t.p,{children:[`Body сериализуется `,(0,c.jsx)(t.code,{children:`JSON.stringify`}),`.`]}),`
1377
+ `,(0,c.jsx)(t.p,{children:`Ошибки:`}),`
1378
+ `,(0,c.jsxs)(t.ul,{children:[`
1379
+ `,(0,c.jsx)(t.li,{children:`cyclic object → исключение;`}),`
1380
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`BigInt`}),` → исключение;`]}),`
1381
+ `,(0,c.jsx)(t.li,{children:`function/symbol/undefined fields → удаляются по правилам JSON;`}),`
1382
+ `,(0,c.jsxs)(t.li,{children:[`Date → ISO string стандартного `,(0,c.jsx)(t.code,{children:`Date.toJSON`}),`, если body mapper не преобразовал её иначе.`]}),`
1383
+ `]}),`
1384
+ `,(0,c.jsxs)(t.p,{children:[`Кроме того, текущий helper сериализует body только если value truthy. Используйте object body. Не используйте корневые `,(0,c.jsx)(t.code,{children:`0`}),`, `,(0,c.jsx)(t.code,{children:`false`}),`, `,(0,c.jsx)(t.code,{children:`""`}),` или `,(0,c.jsx)(t.code,{children:`null`}),` как create payload.`]}),`
1385
+ `,(0,c.jsx)(t.h3,{id:`response-может-отличаться-от-body`,children:`Response может отличаться от body`}),`
1386
+ `,(0,c.jsx)(t.p,{children:`Не пишите один тип автоматически для обоих, если backend добавляет поля:`}),`
1387
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataCreateFn<CreatedDto, DomainEntity, CreateBody>(...);
1388
+ `})}),`
1389
+ `,(0,c.jsx)(t.p,{children:`Это сохраняет связь:`}),`
1390
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`CreateBody -> SAP -> CreatedDto -> transform -> DomainEntity
1391
+ `})}),`
1392
+ `,(0,c.jsxs)(t.h2,{id:`odataupdatefn-обновление`,children:[(0,c.jsx)(t.code,{children:`odataUpdateFn`}),`: обновление`]}),`
1393
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataUpdateFn`}),` обновляет Entity по key/params и body.`]}),`
1394
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataUpdateFn<I, O, T, P>({
1395
+ odata,
1396
+ params,
1397
+ body,
1398
+ options?,
1399
+ init?,
1400
+ autoParse?,
1401
+ swCache?,
1402
+ transform?
1403
+ });
1404
+ `})}),`
1405
+ `,(0,c.jsxs)(t.p,{children:[`HTTP method — `,(0,c.jsx)(t.code,{children:`PUT`}),`.`]}),`
1406
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type UpdateSalesOrderBody = {
1407
+ Title: string;
1408
+ };
1409
+
1410
+ type SalesOrderKey = {
1411
+ OrderId: string;
1412
+ };
1413
+
1414
+ const updateSalesOrder = odataUpdateFn<
1415
+ SalesOrderDto,
1416
+ SalesOrder,
1417
+ UpdateSalesOrderBody,
1418
+ SalesOrderKey
1419
+ >({
1420
+ odata: {
1421
+ service: "Z_SALES_ORDER_SRV",
1422
+ target: "SalesOrderSet"
1423
+ },
1424
+ params: wrapODataParams({
1425
+ OrderId: "42"
1426
+ }),
1427
+ body: {
1428
+ Title: "Изменённый заказ"
1429
+ },
1430
+ autoParse: true,
1431
+ transform: mapSalesOrder
1432
+ });
1433
+ `})}),`
1434
+ `,(0,c.jsx)(t.p,{children:`Request:`}),`
1435
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`PUT /Z_SALES_ORDER_SRV/SalesOrderSet(OrderId='42')
1436
+
1437
+ {"Title":"Изменённый заказ"}
1438
+ `})}),`
1439
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`PUT`}),` зафиксирован текущей реализацией. Helper не поддерживает отдельную semantic PATCH/MERGE operation. Если backend требует другой contract, используйте опубликованную FunctionImport или согласованное расширение package owner; не подменяйте method через `,(0,c.jsx)(t.code,{children:`init`}),`.`]}),`
1440
+ `,(0,c.jsx)(t.p,{children:`Write автоматически проходит X-CSRF lifecycle.`}),`
1441
+ `,(0,c.jsxs)(t.h2,{id:`odatadeletefn-удаление`,children:[(0,c.jsx)(t.code,{children:`odataDeleteFn`}),`: удаление`]}),`
1442
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataDeleteFn`}),` удаляет Entity по key/params.`]}),`
1443
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataDeleteFn<I, O, P>({
1444
+ odata,
1445
+ params,
1446
+ options?,
1447
+ init?,
1448
+ autoParse?,
1449
+ swCache?,
1450
+ transform?
1451
+ });
1452
+ `})}),`
1453
+ `,(0,c.jsxs)(t.p,{children:[`HTTP method — `,(0,c.jsx)(t.code,{children:`DELETE`}),`. Body не поддерживается.`]}),`
1454
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const deleteSalesOrder = odataDeleteFn<unknown>({
1455
+ odata: {
1456
+ service: "Z_SALES_ORDER_SRV",
1457
+ target: "SalesOrderSet"
1458
+ },
1459
+ params: wrapODataParams({
1460
+ OrderId: "42"
1461
+ })
1462
+ });
1463
+
1464
+ const result = await deleteSalesOrder({ client: queryClient });
1465
+
1466
+ if (isNoContentResponse(result.data)) {
1467
+ // Нормальный DELETE без response body.
1468
+ }
1469
+ `})}),`
1470
+ `,(0,c.jsx)(t.p,{children:`Request:`}),`
1471
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`DELETE /Z_SALES_ORDER_SRV/SalesOrderSet(OrderId='42')
1472
+ `})}),`
1473
+ `,(0,c.jsxs)(t.p,{children:[`Delete success не удаляет query cache автоматически. Consumer обязан применить `,(0,c.jsx)(t.code,{children:`setQueryData`}),` или scoped invalidation.`]}),`
1474
+ `,(0,c.jsxs)(t.h2,{id:`odatafunctionimportfn-functionimport`,children:[(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`: FunctionImport`]}),`
1475
+ `,(0,c.jsxs)(t.p,{children:[`FunctionImport helper используется только когда `,(0,c.jsx)(t.code,{children:`target`}),` объявлен в metadata как FunctionImport.`]}),`
1476
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataFunctionImportFn<I, O, P>({
1477
+ odata,
1478
+ params,
1479
+ options?,
1480
+ init?,
1481
+ autoParse?,
1482
+ swCache?,
1483
+ transform?
1484
+ });
1485
+ `})}),`
1486
+ `,(0,c.jsx)(t.h3,{id:`http-method-из-metadata`,children:`HTTP method из metadata`}),`
1487
+ `,(0,c.jsx)(t.p,{children:`В отличие от entity helpers, method не выбирает consumer:`}),`
1488
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-xml`,children:`<FunctionImport Name="CloseOrder" m:HttpMethod="POST" ... />
1489
+ `})}),`
1490
+ `,(0,c.jsxs)(t.p,{children:[`Helper использует `,(0,c.jsx)(t.code,{children:`POST`}),`.`]}),`
1491
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`httpMethod`}),` отсутствует:`]}),`
1492
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`FunctionImport 'CloseOrder' не содержит httpMethod в metadata,
1493
+ поэтому operation 'fi' выполнить нельзя.
1494
+ `})}),`
1495
+ `,(0,c.jsx)(t.h3,{id:`post-functionimport`,children:`POST FunctionImport`}),`
1496
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type CloseOrderParams = {
1497
+ OrderId: string;
1498
+ Reason: string;
1499
+ };
1500
+
1501
+ type CloseOrderDto = {
1502
+ OrderId: string;
1503
+ Status: string;
1504
+ };
1505
+
1506
+ const closeOrder = odataFunctionImportFn<
1507
+ CloseOrderDto,
1508
+ SalesOrderStatus,
1509
+ CloseOrderParams
1510
+ >({
1511
+ odata: {
1512
+ service: "Z_SALES_ORDER_SRV",
1513
+ target: "CloseOrder"
1514
+ },
1515
+ params: wrapODataParams({
1516
+ OrderId: "42",
1517
+ Reason: "completed"
1518
+ }),
1519
+ transform: (dto) => ({
1520
+ orderId: dto.OrderId,
1521
+ status: dto.Status
1522
+ })
1523
+ });
1524
+ `})}),`
1525
+ `,(0,c.jsx)(t.p,{children:`Условный path:`}),`
1526
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/Z_SALES_ORDER_SRV/CloseOrder?OrderId='42'&Reason='completed'
1527
+ `})}),`
1528
+ `,(0,c.jsx)(t.p,{children:`Body не передаётся: FunctionImport parameters находятся в URL по metadata contract.`}),`
1529
+ `,(0,c.jsx)(t.h3,{id:`get-functionimport`,children:`GET FunctionImport`}),`
1530
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const loadStatistics = odataFunctionImportFn<StatisticsDto>({
1531
+ odata: {
1532
+ service: "Z_SALES_ORDER_SRV",
1533
+ target: "GetStatistics"
1534
+ },
1535
+ params: wrapODataParams({
1536
+ CompanyId: "1000"
1537
+ })
1538
+ });
1539
+ `})}),`
1540
+ `,(0,c.jsx)(t.p,{children:`Если metadata объявляет GET, X-CSRF token не нужен.`}),`
1541
+ `,(0,c.jsx)(t.h3,{id:`functionimport-result-metadata`,children:`FunctionImport result metadata`}),`
1542
+ `,(0,c.jsxs)(t.p,{children:[`Если FunctionImport содержит `,(0,c.jsx)(t.code,{children:`resultEntity`}),`, `,(0,c.jsx)(t.code,{children:`autoParse`}),` использует columns этой Entity.`]}),`
1543
+ `,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`resultEntity`}),` отсутствует или не найден, `,(0,c.jsx)(t.code,{children:`autoParse`}),` молча оставляет payload без metadata conversion. Это не ошибка: FunctionImport может возвращать primitive/raw structure.`]}),`
1544
+ `,(0,c.jsx)(t.h3,{id:`несовместимые-helpers`,children:`Несовместимые helpers`}),`
1545
+ `,(0,c.jsx)(t.p,{children:`Entity helper для FunctionImport:`}),`
1546
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`OData operation 'create' несовместима с FunctionImport 'CloseOrder'.
1547
+ Для FunctionImport используйте method: 'fi'.
1548
+ `})}),`
1549
+ `,(0,c.jsx)(t.p,{children:`FunctionImport helper для Entity:`}),`
1550
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`OData operation 'fi' несовместима с Entity 'SalesOrderSet'.
1551
+ `})}),`
1552
+ `,(0,c.jsxs)(t.h2,{id:`autoparse-и-transform`,children:[(0,c.jsx)(t.code,{children:`autoParse`}),` и `,(0,c.jsx)(t.code,{children:`transform`})]}),`
1553
+ `,(0,c.jsx)(t.p,{children:`Это два последовательных, но разных этапа.`}),`
1554
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`raw OData JSON
1555
+
1556
+
1557
+ autoParse по metadata
1558
+
1559
+
1560
+ metadata-parsed DTO-like value
1561
+
1562
+
1563
+ transform consumer-а
1564
+
1565
+
1566
+ domain/result value
1567
+ `})}),`
1568
+ `,(0,c.jsxs)(t.h3,{id:`без-autoparse`,children:[`Без `,(0,c.jsx)(t.code,{children:`autoParse`})]}),`
1569
+ `,(0,c.jsx)(t.p,{children:`SAP может вернуть:`}),`
1570
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
1571
+ FLAG: "X",
1572
+ AMOUNT: "10.5",
1573
+ CREATED_AT: "/Date(1773090000000)/"
1574
+ }
1575
+ `})}),`
1576
+ `,(0,c.jsxs)(t.p,{children:[`Без `,(0,c.jsx)(t.code,{children:`autoParse`}),` transform увидит strings:`]}),`
1577
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn<{
1578
+ FLAG: string;
1579
+ AMOUNT: string;
1580
+ CREATED_AT: string;
1581
+ }>({
1582
+ odata,
1583
+ transform: (rows) => {
1584
+ console.log(typeof rows[0]?.FLAG); // "string".
1585
+ console.log(typeof rows[0]?.AMOUNT); // "string".
1586
+ return rows;
1587
+ }
1588
+ });
1589
+ `})}),`
1590
+ `,(0,c.jsxs)(t.h3,{id:`с-autoparse`,children:[`С `,(0,c.jsx)(t.code,{children:`autoParse`})]}),`
1591
+ `,(0,c.jsx)(t.p,{children:`Если metadata columns описывают ABAP boolean-like, decimal и datetime:`}),`
1592
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn<{
1593
+ FLAG: boolean;
1594
+ AMOUNT: number;
1595
+ CREATED_AT: Date;
1596
+ }>({
1597
+ odata,
1598
+ autoParse: true
1599
+ });
1600
+ `})}),`
1601
+ `,(0,c.jsx)(t.p,{children:`Ожидаемое runtime представление:`}),`
1602
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
1603
+ FLAG: true,
1604
+ AMOUNT: 10.5,
1605
+ CREATED_AT: new Date(...)
1606
+ }
1607
+ `})}),`
1608
+ `,(0,c.jsx)(t.h3,{id:`что-именно-парсится`,children:`Что именно парсится`}),`
1609
+ `,(0,c.jsx)(t.p,{children:`Для каждого column metadata строится план из:`}),`
1610
+ `,(0,c.jsxs)(t.ul,{children:[`
1611
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`id`}),`;`]}),`
1612
+ `,(0,c.jsx)(t.li,{children:`base type;`}),`
1613
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`abapBooleanLike`}),`.`]}),`
1614
+ `]}),`
1615
+ `,(0,c.jsx)(t.p,{children:`Затем преобразуются только собственные top-level поля, которые реально есть в record.`}),`
1616
+ `,(0,c.jsxs)(t.p,{children:[`Поддерживаемые смысловые conversions определяет `,(0,c.jsx)(t.code,{children:`odataParseValueByMetadata`}),` из `,(0,c.jsx)(t.code,{children:`foundation-lib/odata-service`}),`, в том числе:`]}),`
1617
+ `,(0,c.jsxs)(t.ul,{children:[`
1618
+ `,(0,c.jsx)(t.li,{children:`boolean/ABAP boolean-like;`}),`
1619
+ `,(0,c.jsx)(t.li,{children:`integer/float/decimal/double;`}),`
1620
+ `,(0,c.jsx)(t.li,{children:`datetime/datetimeOffset/time;`}),`
1621
+ `,(0,c.jsx)(t.li,{children:`string-like values остаются string.`}),`
1622
+ `]}),`
1623
+ `,(0,c.jsx)(t.h3,{id:`shallow-parsing`,children:`Shallow parsing`}),`
1624
+ `,(0,c.jsx)(t.p,{children:`Не выполняется deep recursion:`}),`
1625
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
1626
+ AMOUNT: "10.5", // Может стать number.
1627
+ Customer: {
1628
+ LIMIT: "1000" // Вложенное поле автоматически не парсится.
1629
+ }
1630
+ }
1631
+ `})}),`
1632
+ `,(0,c.jsx)(t.h3,{id:`structural-sharing`,children:`Structural sharing`}),`
1633
+ `,(0,c.jsx)(t.p,{children:`Если conversion не изменил ни одного field, сохраняется исходный object reference.`}),`
1634
+ `,(0,c.jsx)(t.p,{children:`Для array создаётся копия только когда хотя бы один record изменился. Это помогает TanStack Query structural sharing, но consumer не должен строить бизнес-логику на identity implementation detail.`}),`
1635
+ `,(0,c.jsx)(t.h3,{id:`transform`,children:(0,c.jsx)(t.code,{children:`transform`})}),`
1636
+ `,(0,c.jsx)(t.p,{children:`Single operation transform:`}),`
1637
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`(data, targetMetadata) => output
1638
+ `})}),`
1639
+ `,(0,c.jsx)(t.p,{children:`Query transform:`}),`
1640
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`(rows, targetMetadata) => output[] | output
1641
+ `})}),`
1642
+ `,(0,c.jsxs)(t.p,{children:[`Transform вызывается после `,(0,c.jsx)(t.code,{children:`autoParse`}),`.`]}),`
1643
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const runner = odataQueryFn<SalesOrderDto, SalesOrder>({
1644
+ odata,
1645
+ autoParse: true,
1646
+ transform: (rows, target) => {
1647
+ console.log(target.title);
1648
+ return rows.map(mapSalesOrder);
1649
+ }
1650
+ });
1651
+ `})}),`
1652
+ `,(0,c.jsx)(t.h3,{id:`generics-не-являются-validation`,children:`Generics не являются validation`}),`
1653
+ `,(0,c.jsx)(t.p,{children:`Этот код компилируется:`}),`
1654
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn<{ Amount: number }>({
1655
+ odata
1656
+ });
1657
+ `})}),`
1658
+ `,(0,c.jsx)(t.p,{children:`Но backend может вернуть:`}),`
1659
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{ "Amount": "not-a-number" }
1660
+ `})}),`
1661
+ `,(0,c.jsxs)(t.p,{children:[`TypeScript не видит сеть. Для строгой boundary начните с `,(0,c.jsx)(t.code,{children:`unknown`}),`:`]}),`
1662
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { isRecord } from "@ryuzaki13/react-foundation-lib/validators";
1663
+
1664
+ function parseSalesOrder(value: unknown): SalesOrder {
1665
+ if (!isRecord(value)) {
1666
+ throw new Error("SalesOrder должен быть object");
1667
+ }
1668
+
1669
+ if (typeof value.OrderId !== "string") {
1670
+ throw new Error("SalesOrder.OrderId должен быть string");
1671
+ }
1672
+
1673
+ if (typeof value.Amount !== "number") {
1674
+ throw new Error("SalesOrder.Amount должен быть number");
1675
+ }
1676
+
1677
+ return {
1678
+ id: value.OrderId,
1679
+ title: typeof value.Title === "string" ? value.Title : "",
1680
+ amount: value.Amount,
1681
+ createdAt: value.CreatedAt instanceof Date
1682
+ ? value.CreatedAt
1683
+ : new Date(0)
1684
+ };
1685
+ }
1686
+
1687
+ const runner = odataQueryFn<unknown, SalesOrder>({
1688
+ odata,
1689
+ autoParse: true,
1690
+ transform: (rows) => rows.map(parseSalesOrder)
1691
+ });
1692
+ `})}),`
1693
+ `,(0,c.jsxs)(t.p,{children:[`Для критичного contract вместо fallback `,(0,c.jsx)(t.code,{children:`new Date(0)`}),` лучше бросить validation error. Пример показывает только технику narrowing.`]}),`
1694
+ `,(0,c.jsx)(t.h3,{id:`где-держать-mapping`,children:`Где держать mapping`}),`
1695
+ `,(0,c.jsxs)(t.ul,{children:[`
1696
+ `,(0,c.jsxs)(t.li,{children:[`Generic metadata conversion — `,(0,c.jsx)(t.code,{children:`foundation-api/odata`}),`.`]}),`
1697
+ `,(0,c.jsx)(t.li,{children:`DTO validation — API boundary владельца entity/feature.`}),`
1698
+ `,(0,c.jsx)(t.li,{children:`DTO-to-domain mapping — entity/feature приложения.`}),`
1699
+ `,(0,c.jsx)(t.li,{children:`Display formatting — formatter/UI owner.`}),`
1700
+ `]}),`
1701
+ `,(0,c.jsx)(t.p,{children:`Не переносите SAP-specific domain mapper в foundation package.`}),`
1702
+ `,(0,c.jsx)(t.h2,{id:`tanstack-query-чтение`,children:`TanStack Query: чтение`}),`
1703
+ `,(0,c.jsx)(t.h3,{id:`provider-обязателен`,children:`Provider обязателен`}),`
1704
+ `,(0,c.jsxs)(t.p,{children:[`Hooks и metadata-aware runners используют `,(0,c.jsx)(t.code,{children:`QueryClient`}),`.`]}),`
1705
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`<QueryClientProvider client={queryClient}>
1706
+ <App />
1707
+ </QueryClientProvider>
1708
+ `})}),`
1709
+ `,(0,c.jsx)(t.p,{children:`Вызов hook вне provider приведёт к ошибке TanStack Query.`}),`
1710
+ `,(0,c.jsx)(t.h3,{id:`query-key--identity-данных`,children:`Query key — identity данных`}),`
1711
+ `,(0,c.jsx)(t.p,{children:`Query key должен включать всё, что влияет на response:`}),`
1712
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const salesOrderKeys = {
1713
+ all: ["sales-orders"] as const,
1714
+ detail: (id: string) =>
1715
+ [...salesOrderKeys.all, "detail", id] as const,
1716
+ list: (params: {
1717
+ companyId: string;
1718
+ dateFrom: string;
1719
+ dateTo: string;
1720
+ skip: number;
1721
+ top: number;
1722
+ }) => [...salesOrderKeys.all, "list", params] as const
1723
+ };
1724
+ `})}),`
1725
+ `,(0,c.jsx)(t.p,{children:`Нормализуйте Date в string:`}),`
1726
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const keyParams = {
1727
+ companyId: companyId.trim(),
1728
+ dateFrom: dateFrom.toISOString(),
1729
+ dateTo: dateTo.toISOString(),
1730
+ skip,
1731
+ top
1732
+ };
1733
+ `})}),`
1734
+ `,(0,c.jsx)(t.p,{children:`Не кладите в key:`}),`
1735
+ `,(0,c.jsxs)(t.ul,{children:[`
1736
+ `,(0,c.jsx)(t.li,{children:`token;`}),`
1737
+ `,(0,c.jsx)(t.li,{children:`cookie;`}),`
1738
+ `,(0,c.jsx)(t.li,{children:`QueryClient;`}),`
1739
+ `,(0,c.jsx)(t.li,{children:`function;`}),`
1740
+ `,(0,c.jsx)(t.li,{children:`DOM node;`}),`
1741
+ `,(0,c.jsx)(t.li,{children:`огромный response body;`}),`
1742
+ `,(0,c.jsx)(t.li,{children:`mutable object, меняющийся после передачи.`}),`
1743
+ `]}),`
1744
+ `,(0,c.jsxs)(t.h3,{id:`переиспользуемые-queryoptions`,children:[`Переиспользуемые `,(0,c.jsx)(t.code,{children:`queryOptions`})]}),`
1745
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`export function salesOrderQueryOptions(params: SalesOrderListParams) {
1746
+ const normalized = normalizeSalesOrderListParams(params);
1747
+
1748
+ return queryOptions({
1749
+ queryKey: salesOrderKeys.list(normalized.key),
1750
+ queryFn: odataQueryFn<SalesOrderDto, SalesOrder>({
1751
+ odata: SALES_ORDER_ODATA,
1752
+ options: {
1753
+ expression: buildSalesOrderFilter(normalized),
1754
+ sorts: normalized.sorts,
1755
+ top: normalized.top,
1756
+ skip: normalized.skip,
1757
+ inlinecount: "allpages"
1758
+ },
1759
+ autoParse: true,
1760
+ transform: (rows) => rows.map(mapSalesOrder)
1761
+ }),
1762
+ staleTime: 60_000
1763
+ });
1764
+ }
1765
+ `})}),`
1766
+ `,(0,c.jsx)(t.p,{children:`Один factory можно использовать:`}),`
1767
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`useQuery(salesOrderQueryOptions(params));
1768
+
1769
+ queryClient.fetchQuery(salesOrderQueryOptions(params));
1770
+
1771
+ queryClient.ensureQueryData(salesOrderQueryOptions(params));
1772
+ `})}),`
1773
+ `,(0,c.jsx)(t.p,{children:`Это предотвращает расхождение key/query function между component и loader.`}),`
1774
+ `,(0,c.jsx)(t.h3,{id:`result-envelope-в-query-cache`,children:`Result envelope в Query cache`}),`
1775
+ `,(0,c.jsx)(t.p,{children:`По умолчанию cache data:`}),`
1776
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
1777
+ data: SalesOrder[];
1778
+ totalCount?: number;
1779
+ }
1780
+ `})}),`
1781
+ `,(0,c.jsx)(t.p,{children:`Component:`}),`
1782
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const query = useQuery(salesOrderQueryOptions(params));
1783
+
1784
+ const rows = query.data?.data ?? [];
1785
+ const totalCount = query.data?.totalCount ?? rows.length;
1786
+ `})}),`
1787
+ `,(0,c.jsxs)(t.p,{children:[`Не используйте `,(0,c.jsx)(t.code,{children:`rows.length`}),` как общее количество при server paging.`]}),`
1788
+ `,(0,c.jsxs)(t.h3,{id:`select-tanstack-query`,children:[(0,c.jsx)(t.code,{children:`select`}),` TanStack Query`]}),`
1789
+ `,(0,c.jsx)(t.p,{children:`Можно дать UI только rows:`}),`
1790
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const query = useQuery({
1791
+ ...salesOrderQueryOptions(params),
1792
+ select: (result) => result.data
1793
+ });
1794
+ `})}),`
1795
+ `,(0,c.jsxs)(t.p,{children:[`Но `,(0,c.jsx)(t.code,{children:`totalCount`}),` станет недоступен этому observer. Для table paging лучше сохранить envelope.`]}),`
1796
+ `,(0,c.jsx)(t.h3,{id:`conditional-query`,children:`Conditional query`}),`
1797
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const query = useQuery({
1798
+ ...salesOrderQueryOptions(params),
1799
+ enabled: Boolean(companyId)
1800
+ });
1801
+ `})}),`
1802
+ `,(0,c.jsxs)(t.p,{children:[`Не вызывайте `,(0,c.jsx)(t.code,{children:`useQuery`}),` условно:`]}),`
1803
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Неверно: нарушает Rules of Hooks.
1804
+ if (companyId) {
1805
+ useQuery(...);
1806
+ }
1807
+ `})}),`
1808
+ `,(0,c.jsx)(t.h3,{id:`abortsignal`,children:`AbortSignal`}),`
1809
+ `,(0,c.jsx)(t.p,{children:`TanStack Query передаёт signal в runner автоматически:`}),`
1810
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`component unmount / query cancellation / refetch replacement
1811
+
1812
+
1813
+ AbortSignal queryFn
1814
+
1815
+
1816
+ OData fetch
1817
+ `})}),`
1818
+ `,(0,c.jsx)(t.p,{children:`Не сохраняйте signal в module state или долгоживущее closure.`}),`
1819
+ `,(0,c.jsx)(t.p,{children:`При ручном вызове:`}),`
1820
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const controller = new AbortController();
1821
+
1822
+ const promise = runner({
1823
+ client: queryClient,
1824
+ signal: controller.signal
1825
+ });
1826
+
1827
+ controller.abort();
1828
+ await promise;
1829
+ `})}),`
1830
+ `,(0,c.jsxs)(t.p,{children:[`Abort обычно приводит к rejection от `,(0,c.jsx)(t.code,{children:`fetch`}),`. Не превращайте его автоматически в пустой array без явной UX policy.`]}),`
1831
+ `,(0,c.jsx)(t.h3,{id:`paging`,children:`Paging`}),`
1832
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const pageSize = 50;
1833
+ const pageIndex = 2;
1834
+
1835
+ const options = {
1836
+ top: pageSize,
1837
+ skip: pageIndex * pageSize,
1838
+ inlinecount: "allpages"
1839
+ };
1840
+ `})}),`
1841
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`top`}),`, `,(0,c.jsx)(t.code,{children:`skip`}),`, filters и sorts должны входить в query key.`]}),`
1842
+ `,(0,c.jsx)(t.h3,{id:`infinite-query`,children:`Infinite query`}),`
1843
+ `,(0,c.jsxs)(t.p,{children:[`Direct runner не получает `,(0,c.jsx)(t.code,{children:`pageParam`}),`, потому что его runtime contract использует только `,(0,c.jsx)(t.code,{children:`client`}),` и `,(0,c.jsx)(t.code,{children:`signal`}),`. Оберните его:`]}),`
1844
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const pageSize = 50;
1845
+
1846
+ const query = useInfiniteQuery({
1847
+ queryKey: [...salesOrderKeys.all, "infinite", normalizedFilters],
1848
+ initialPageParam: 0,
1849
+ queryFn: ({ client, signal, pageParam }) =>
1850
+ odataQueryFn<SalesOrderDto, SalesOrder>({
1851
+ odata: SALES_ORDER_ODATA,
1852
+ options: {
1853
+ expression: buildSalesOrderFilter(normalizedFilters),
1854
+ top: pageSize,
1855
+ skip: pageParam * pageSize,
1856
+ inlinecount: "allpages"
1857
+ },
1858
+ transform: (rows) => rows.map(mapSalesOrder)
1859
+ })({ client, signal }),
1860
+ getNextPageParam: (lastPage, pages) => {
1861
+ const loaded = pages.reduce(
1862
+ (sum, page) => sum + page.data.length,
1863
+ 0
1864
+ );
1865
+
1866
+ if (lastPage.totalCount !== undefined && loaded >= lastPage.totalCount) {
1867
+ return undefined;
1868
+ }
1869
+
1870
+ return pages.length;
1871
+ }
1872
+ });
1873
+ `})}),`
1874
+ `,(0,c.jsxs)(t.p,{children:[`Для backend, который не возвращает count, остановку нужно определять другим подтверждённым правилом, например `,(0,c.jsx)(t.code,{children:`lastPage.data.length < pageSize`}),`.`]}),`
1875
+ `,(0,c.jsx)(t.h3,{id:`ошибки-query`,children:`Ошибки query`}),`
1876
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`if (query.isError) {
1877
+ return <ErrorState error={query.error} />;
1878
+ }
1879
+ `})}),`
1880
+ `,(0,c.jsx)(t.p,{children:`Не показывайте пользователю raw SAP error payload целиком. UI message должен быть безопасным, а technical diagnostics — централизованным.`}),`
1881
+ `,(0,c.jsxs)(t.p,{children:[`Не копируйте `,(0,c.jsx)(t.code,{children:`query.data`}),` в local state через effect:`]}),`
1882
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Нежелательно: создаёт второй источник backend state.
1883
+ useEffect(() => {
1884
+ setRows(query.data?.data ?? []);
1885
+ }, [query.data]);
1886
+ `})}),`
1887
+ `,(0,c.jsx)(t.p,{children:`Используйте Query data напрямую. Local state оставьте для draft/selection/interaction.`}),`
1888
+ `,(0,c.jsx)(t.h2,{id:`tanstack-query-mutations-и-cache`,children:`TanStack Query: mutations и cache`}),`
1889
+ `,(0,c.jsxs)(t.p,{children:[`Metadata-aware write runner нельзя передать напрямую как обычную `,(0,c.jsx)(t.code,{children:`mutationFn(input)`}),`, потому что runner ожидает `,(0,c.jsx)(t.code,{children:`{ client, signal? }`}),`.`]}),`
1890
+ `,(0,c.jsx)(t.p,{children:`Получите текущий client:`}),`
1891
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const queryClient = useQueryClient();
1892
+ `})}),`
1893
+ `,(0,c.jsx)(t.p,{children:`И создавайте operation из mutation input.`}),`
1894
+ `,(0,c.jsx)(t.h3,{id:`create-mutation--invalidation`,children:`Create mutation + invalidation`}),`
1895
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`type CreateSalesOrderInput = {
1896
+ companyId: string;
1897
+ title: string;
1898
+ };
1899
+
1900
+ const createMutation = useMutation({
1901
+ mutationFn: (input: CreateSalesOrderInput) =>
1902
+ odataCreateFn<SalesOrderDto, SalesOrder, CreateSalesOrderBody>({
1903
+ odata: SALES_ORDER_ODATA,
1904
+ body: {
1905
+ CompanyId: input.companyId,
1906
+ Title: input.title
1907
+ },
1908
+ autoParse: true,
1909
+ transform: mapSalesOrder
1910
+ })({ client: queryClient }),
1911
+ onSuccess: async () => {
1912
+ await queryClient.invalidateQueries({
1913
+ queryKey: salesOrderKeys.all
1914
+ });
1915
+ }
1916
+ });
1917
+ `})}),`
1918
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`onSuccess`}),` получает runner result:`]}),`
1919
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`onSuccess: async (result) => {
1920
+ const created = result.data;
1921
+ console.log(created.id);
1922
+ }
1923
+ `})}),`
1924
+ `,(0,c.jsx)(t.h3,{id:`update-mutation--точечный-cache-update`,children:`Update mutation + точечный cache update`}),`
1925
+ `,(0,c.jsx)(t.p,{children:`Если backend возвращает актуальную Entity, можно обновить detail query без refetch:`}),`
1926
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const updateMutation = useMutation({
1927
+ mutationFn: (input: UpdateSalesOrderInput) =>
1928
+ odataUpdateFn<SalesOrderDto, SalesOrder, UpdateSalesOrderBody>({
1929
+ odata: SALES_ORDER_ODATA,
1930
+ params: wrapODataParams({
1931
+ OrderId: input.id
1932
+ }),
1933
+ body: {
1934
+ Title: input.title
1935
+ },
1936
+ autoParse: true,
1937
+ transform: mapSalesOrder
1938
+ })({ client: queryClient }),
1939
+ onSuccess: (result, input) => {
1940
+ queryClient.setQueryData(
1941
+ salesOrderKeys.detail(input.id),
1942
+ result
1943
+ );
1944
+ }
1945
+ });
1946
+ `})}),`
1947
+ `,(0,c.jsxs)(t.p,{children:[`Важно: если detail query хранит runner envelope, записывайте `,(0,c.jsx)(t.code,{children:`result`}),`, а не `,(0,c.jsx)(t.code,{children:`result.data`}),`. Cache shape должен совпадать.`]}),`
1948
+ `,(0,c.jsx)(t.h3,{id:`обновление-list-cache`,children:`Обновление list cache`}),`
1949
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`queryClient.setQueriesData<{
1950
+ data: SalesOrder[];
1951
+ totalCount?: number;
1952
+ }>({ queryKey: salesOrderKeys.all }, (current) => {
1953
+ if (!current) return current;
1954
+
1955
+ return {
1956
+ ...current,
1957
+ data: current.data.map((order) =>
1958
+ order.id === updated.id ? updated : order
1959
+ )
1960
+ };
1961
+ });
1962
+ `})}),`
1963
+ `,(0,c.jsxs)(t.p,{children:[`Не мутируйте `,(0,c.jsx)(t.code,{children:`current.data`}),` через `,(0,c.jsx)(t.code,{children:`push`}),`, `,(0,c.jsx)(t.code,{children:`splice`}),` или присваивание поля. Возвращайте новые arrays/objects.`]}),`
1964
+ `,(0,c.jsx)(t.h3,{id:`delete-mutation`,children:`Delete mutation`}),`
1965
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const deleteMutation = useMutation({
1966
+ mutationFn: (orderId: string) =>
1967
+ odataDeleteFn<unknown>({
1968
+ odata: SALES_ORDER_ODATA,
1969
+ params: wrapODataParams({ OrderId: orderId })
1970
+ })({ client: queryClient }),
1971
+ onSuccess: async (_result, orderId) => {
1972
+ queryClient.removeQueries({
1973
+ queryKey: salesOrderKeys.detail(orderId),
1974
+ exact: true
1975
+ });
1976
+
1977
+ await queryClient.invalidateQueries({
1978
+ queryKey: salesOrderKeys.all
1979
+ });
1980
+ }
1981
+ });
1982
+ `})}),`
1983
+ `,(0,c.jsx)(t.h3,{id:`functionimport-mutation`,children:`FunctionImport mutation`}),`
1984
+ `,(0,c.jsx)(t.p,{children:`Даже если helper называется FunctionImport, cache lifecycle остаётся обычной TanStack mutation:`}),`
1985
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const closeMutation = useMutation({
1986
+ mutationFn: (orderId: string) =>
1987
+ odataFunctionImportFn<CloseOrderDto, SalesOrderStatus>({
1988
+ odata: {
1989
+ service: "Z_SALES_ORDER_SRV",
1990
+ target: "CloseOrder"
1991
+ },
1992
+ params: wrapODataParams({ OrderId: orderId }),
1993
+ transform: mapSalesOrderStatus
1994
+ })({ client: queryClient }),
1995
+ onSuccess: async (_result, orderId) => {
1996
+ await queryClient.invalidateQueries({
1997
+ queryKey: salesOrderKeys.detail(orderId)
1998
+ });
1999
+ }
2000
+ });
2001
+ `})}),`
2002
+ `,(0,c.jsx)(t.h3,{id:`как-выбрать-cache-strategy`,children:`Как выбрать cache strategy`}),`
2003
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Ситуация`}),(0,c.jsx)(t.th,{children:`Strategy`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Response содержит полную актуальную Entity`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`setQueryData`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Mutation влияет на несколько неизвестных rows`}),(0,c.jsxs)(t.td,{children:[`scoped `,(0,c.jsx)(t.code,{children:`invalidateQueries`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Нужен мгновенный UI и rollback возможен`}),(0,c.jsx)(t.td,{children:`optimistic update`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Несколько вкладок должны синхронизироваться`}),(0,c.jsx)(t.td,{children:`принятый broadcast mechanism`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Используется persisted descriptor`}),(0,c.jsxs)(t.td,{children:[`cache strategy `,(0,c.jsx)(t.code,{children:`/persisted`})]})]})]})]}),`
2004
+ `,(0,c.jsx)(t.p,{children:`Не инвалидируйте весь QueryClient:`}),`
2005
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Слишком широко.
2006
+ queryClient.invalidateQueries();
2007
+ `})}),`
2008
+ `,(0,c.jsx)(t.p,{children:`Используйте key owner конкретного resource.`}),`
2009
+ `,(0,c.jsx)(t.h3,{id:`optimistic-update-с-rollback`,children:`Optimistic update с rollback`}),`
2010
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`type OrdersSnapshot = Array<[
2011
+ readonly unknown[],
2012
+ {
2013
+ data: SalesOrder[];
2014
+ totalCount?: number;
2015
+ } | undefined
2016
+ ]>;
2017
+
2018
+ const mutation = useMutation({
2019
+ mutationFn: (input: UpdateSalesOrderInput) =>
2020
+ odataUpdateFn<SalesOrderDto, SalesOrder, UpdateSalesOrderBody>({
2021
+ odata: SALES_ORDER_ODATA,
2022
+ params: wrapODataParams({ OrderId: input.id }),
2023
+ body: { Title: input.title },
2024
+ transform: mapSalesOrder
2025
+ })({ client: queryClient }),
2026
+ onMutate: async (input) => {
2027
+ await queryClient.cancelQueries({ queryKey: salesOrderKeys.all });
2028
+
2029
+ const snapshot = queryClient.getQueriesData<{
2030
+ data: SalesOrder[];
2031
+ totalCount?: number;
2032
+ }>({ queryKey: salesOrderKeys.all }) as OrdersSnapshot;
2033
+
2034
+ queryClient.setQueriesData<{
2035
+ data: SalesOrder[];
2036
+ totalCount?: number;
2037
+ }>({ queryKey: salesOrderKeys.all }, (current) => {
2038
+ if (!current) return current;
2039
+
2040
+ return {
2041
+ ...current,
2042
+ data: current.data.map((order) =>
2043
+ order.id === input.id
2044
+ ? { ...order, title: input.title }
2045
+ : order
2046
+ )
2047
+ };
2048
+ });
2049
+
2050
+ return { snapshot };
2051
+ },
2052
+ onError: (_error, _input, context) => {
2053
+ for (const [queryKey, data] of context?.snapshot ?? []) {
2054
+ queryClient.setQueryData(queryKey, data);
2055
+ }
2056
+ },
2057
+ onSettled: async () => {
2058
+ await queryClient.invalidateQueries({
2059
+ queryKey: salesOrderKeys.all
2060
+ });
2061
+ }
2062
+ });
2063
+ `})}),`
2064
+ `,(0,c.jsx)(t.p,{children:`Обязательные части optimistic flow:`}),`
2065
+ `,(0,c.jsxs)(t.ol,{children:[`
2066
+ `,(0,c.jsx)(t.li,{children:`cancel связанных queries;`}),`
2067
+ `,(0,c.jsx)(t.li,{children:`сохранить snapshot;`}),`
2068
+ `,(0,c.jsx)(t.li,{children:`записать immutable optimistic state;`}),`
2069
+ `,(0,c.jsx)(t.li,{children:`вернуть context;`}),`
2070
+ `,(0,c.jsxs)(t.li,{children:[`rollback в `,(0,c.jsx)(t.code,{children:`onError`}),`;`]}),`
2071
+ `,(0,c.jsxs)(t.li,{children:[`reconcile/invalidate в `,(0,c.jsx)(t.code,{children:`onSettled`}),`.`]}),`
2072
+ `]}),`
2073
+ `,(0,c.jsx)(t.p,{children:`Если rollback невозможно сделать корректно, используйте pending state mutation и server refetch вместо optimistic update.`}),`
2074
+ `,(0,c.jsx)(t.h3,{id:`abort-у-mutation`,children:`Abort у mutation`}),`
2075
+ `,(0,c.jsxs)(t.p,{children:[`Generic TanStack `,(0,c.jsx)(t.code,{children:`useMutation`}),` не передаёт AbortSignal в runner автоматически. Current OData runner contract допускает optional signal, но consumer должен создать отдельный lifecycle вручную, если write endpoint безопасно поддерживает cancellation.`]}),`
2076
+ `,(0,c.jsx)(t.p,{children:`Отмена browser request не означает, что SAP backend гарантированно отменил уже начатую write operation. Делайте write идемпотентной по backend contract, когда это возможно.`}),`
2077
+ `,(0,c.jsx)(t.h3,{id:`ошибка-mutation`,children:`Ошибка mutation`}),`
2078
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`if (mutation.isError) {
2079
+ showSafeErrorMessage();
2080
+ }
2081
+ `})}),`
2082
+ `,(0,c.jsx)(t.p,{children:`Не делайте пустой catch:`}),`
2083
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`try {
2084
+ await mutation.mutateAsync(input);
2085
+ } catch {
2086
+ // Неверно: technical failure потерян.
2087
+ }
2088
+ `})}),`
2089
+ `,(0,c.jsx)(t.p,{children:`Либо позвольте global mutation error policy обработать error, либо добавьте user recovery и сохраните technical error flow.`}),`
2090
+ `,(0,c.jsx)(t.h2,{id:`dev-dry-run-helpers`,children:`DEV dry-run helpers`}),`
2091
+ `,(0,c.jsx)(t.p,{children:`Публичные helpers:`}),`
2092
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFnDev
2093
+ odataReadFnDev
2094
+ odataCreateFnDev
2095
+ odataUpdateFnDev
2096
+ odataDeleteFnDev
2097
+ odataFunctionImportFnDev
2098
+ `})}),`
2099
+ `,(0,c.jsx)(t.p,{children:`Options и generics повторяют соответствующие real helpers.`}),`
2100
+ `,(0,c.jsxs)(t.h3,{id:`поведение-в-__dev__`,children:[`Поведение в `,(0,c.jsx)(t.code,{children:`__DEV__`})]}),`
2101
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`metadata request ─────────────── выполняется при необходимости
2102
+ target validation ───────────── выполняется
2103
+ path/query/body building ────── выполняется
2104
+ console diagnostics ─────────── выполняется
2105
+ warning notification ────────── показывается
2106
+ целевой data/write request ──── НЕ выполняется
2107
+ transform ───────────────────── НЕ выполняется
2108
+ `})}),`
2109
+ `,(0,c.jsx)(t.p,{children:`Query dry-run возвращает:`}),`
2110
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
2111
+ data: [],
2112
+ totalCount: 0
2113
+ }
2114
+ `})}),`
2115
+ `,(0,c.jsx)(t.p,{children:`Single dry-run:`}),`
2116
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
2117
+ data: undefined
2118
+ }
2119
+ `})}),`
2120
+ `,(0,c.jsx)(t.h3,{id:`что-выводится-в-console`,children:`Что выводится в console`}),`
2121
+ `,(0,c.jsxs)(t.ul,{children:[`
2122
+ `,(0,c.jsx)(t.li,{children:`helper name;`}),`
2123
+ `,(0,c.jsx)(t.li,{children:`semantic operation;`}),`
2124
+ `,(0,c.jsx)(t.li,{children:`service/target;`}),`
2125
+ `,(0,c.jsx)(t.li,{children:`target kind;`}),`
2126
+ `,(0,c.jsx)(t.li,{children:`полный URL;`}),`
2127
+ `,(0,c.jsx)(t.li,{children:`path без base URL;`}),`
2128
+ `,(0,c.jsx)(t.li,{children:`query string;`}),`
2129
+ `,(0,c.jsx)(t.li,{children:`body;`}),`
2130
+ `,(0,c.jsx)(t.li,{children:`request init;`}),`
2131
+ `,(0,c.jsx)(t.li,{children:`итоговые transport headers preview;`}),`
2132
+ `,(0,c.jsx)(t.li,{children:`metadata target;`}),`
2133
+ `,(0,c.jsx)(t.li,{children:`params/options;`}),`
2134
+ `,(0,c.jsxs)(t.li,{children:[`признак `,(0,c.jsx)(t.code,{children:`autoParse`}),` и `,(0,c.jsx)(t.code,{children:`transform`}),`.`]}),`
2135
+ `]}),`
2136
+ `,(0,c.jsx)(t.p,{children:`Не используйте real secrets/tokens в dev examples: params и body выводятся в console.`}),`
2137
+ `,(0,c.jsx)(t.h3,{id:`metadata-request-всё-ещё-реален`,children:`Metadata request всё ещё реален`}),`
2138
+ `,(0,c.jsxs)(t.p,{children:[`Dry-run не является полностью offline. Для построения корректного target path нужны metadata, поэтому `,(0,c.jsx)(t.code,{children:`$metadata`}),` может быть загружена из сети.`]}),`
2139
+ `,(0,c.jsx)(t.h3,{id:`production-поведение`,children:`Production поведение`}),`
2140
+ `,(0,c.jsxs)(t.p,{children:[`В non-dev build `,(0,c.jsx)(t.code,{children:`*Dev`}),` helper делегирует real helper и выполняет настоящий request.`]}),`
2141
+ `,(0,c.jsx)(t.p,{children:`Это важное правило безопасности:`}),`
2142
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`odataUpdateFnDev не является вечной заглушкой.
2143
+ В production он делает реальный update.
2144
+ `})}),`
2145
+ `,(0,c.jsx)(t.p,{children:`Не используйте Dev helper как mock API или способ гарантированно запретить write. Для теста подменяйте transport/network.`}),`
2146
+ `,(0,c.jsxs)(t.h3,{id:`почему-нет-public-odatafetchfndev`,children:[`Почему нет public `,(0,c.jsx)(t.code,{children:`odataFetchFnDev`})]}),`
2147
+ `,(0,c.jsx)(t.p,{children:`Внутренний generic builder не экспортирован package entrypoint. Используйте только шесть specialized Dev helpers.`}),`
2148
+ `,(0,c.jsx)(t.h2,{id:`metadata-описание-сервиса`,children:`Metadata: описание сервиса`}),`
2149
+ `,(0,c.jsxs)(t.p,{children:[`Metadata — это XML-документ по адресу `,(0,c.jsx)(t.code,{children:`/<service>/$metadata`}),`. В нём сервер описывает:`]}),`
2150
+ `,(0,c.jsxs)(t.ul,{children:[`
2151
+ `,(0,c.jsx)(t.li,{children:`какие Entity и FunctionImport существуют;`}),`
2152
+ `,(0,c.jsx)(t.li,{children:`какие свойства есть у Entity;`}),`
2153
+ `,(0,c.jsx)(t.li,{children:`какие свойства являются ключами;`}),`
2154
+ `,(0,c.jsxs)(t.li,{children:[`типы свойств: `,(0,c.jsx)(t.code,{children:`Edm.String`}),`, `,(0,c.jsx)(t.code,{children:`Edm.Boolean`}),`, `,(0,c.jsx)(t.code,{children:`Edm.DateTime`}),` и другие;`]}),`
2155
+ `,(0,c.jsx)(t.li,{children:`обязательность параметров;`}),`
2156
+ `,(0,c.jsx)(t.li,{children:`HTTP method FunctionImport;`}),`
2157
+ `,(0,c.jsx)(t.li,{children:`связи code/text и другие SAP annotations.`}),`
2158
+ `]}),`
2159
+ `,(0,c.jsxs)(t.p,{children:[`API-пакет загружает XML, передаёт его parser-у из `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/odata-service`}),` и сохраняет разобранный результат в TanStack Query.`]}),`
2160
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`GET /sap/opu/odata/sap/ZSALES_SRV/$metadata
2161
+
2162
+
2163
+ XML Response
2164
+
2165
+
2166
+ parseServiceMetadata(...)
2167
+
2168
+
2169
+ EntityMetadata / FunctionImportMetadata
2170
+
2171
+
2172
+ TanStack Query cache
2173
+ `})}),`
2174
+ `,(0,c.jsxs)(t.p,{children:[`Metadata нужна metadata-aware helpers. Именно благодаря ей `,(0,c.jsx)(t.code,{children:`odataReadFn`}),` понимает keys, `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),` — HTTP method, а `,(0,c.jsx)(t.code,{children:`autoParse`}),` — типы полей.`]}),`
2175
+ `,(0,c.jsx)(t.h3,{id:`fetchmetadata`,children:(0,c.jsx)(t.code,{children:`fetchMetadata`})}),`
2176
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`fetchMetadata(options)`}),` создаёт TanStack-compatible query function.`]}),`
2177
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { fetchMetadata } from "@ryuzaki13/react-foundation-api/odata";
2178
+
2179
+ const queryFn = fetchMetadata({
2180
+ service: "ZSALES_SRV"
2181
+ });
2182
+ `})}),`
2183
+ `,(0,c.jsxs)(t.p,{children:[`Обычно напрямую вызывать функцию не нужно. Используйте `,(0,c.jsx)(t.code,{children:`odataMetadataQueryOptions`}),`, `,(0,c.jsx)(t.code,{children:`getODataMetadataData`}),` или hooks ниже.`]}),`
2184
+ `,(0,c.jsx)(t.p,{children:`В запросе выставляются:`}),`
2185
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Accept: */*
2186
+ Content-Type: application/xml
2187
+ `})}),`
2188
+ `,(0,c.jsx)(t.p,{children:`Если XML невозможно разобрать, ошибка оборачивается сообщением с именем сервиса. Это помогает отличить transport error от некорректной metadata.`}),`
2189
+ `,(0,c.jsx)(t.h3,{id:`odatametadataqueryoptions`,children:(0,c.jsx)(t.code,{children:`odataMetadataQueryOptions`})}),`
2190
+ `,(0,c.jsxs)(t.p,{children:[`Factory возвращает готовые `,(0,c.jsx)(t.code,{children:`queryOptions`}),`:`]}),`
2191
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { odataMetadataQueryOptions } from "@ryuzaki13/react-foundation-api/odata";
2192
+
2193
+ const metadata = await queryClient.fetchQuery(
2194
+ odataMetadataQueryOptions({
2195
+ service: "ZSALES_SRV"
2196
+ })
2197
+ );
2198
+
2199
+ console.log(metadata.entities);
2200
+ console.log(metadata.functionImports);
2201
+ `})}),`
2202
+ `,(0,c.jsxs)(t.p,{children:[`Query key включает тип записи, service и уже resolved `,(0,c.jsx)(t.code,{children:`baseUrl`}),`:`]}),`
2203
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`[
2204
+ "odata",
2205
+ "metadata",
2206
+ {
2207
+ service: "ZSALES_SRV",
2208
+ baseUrl: "/sap/opu/odata/sap"
2209
+ }
2210
+ ]
2211
+ `})}),`
2212
+ `,(0,c.jsx)(t.p,{children:`Настройки query:`}),`
2213
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Настройка`}),(0,c.jsx)(t.th,{children:`Значение`}),(0,c.jsx)(t.th,{children:`Зачем`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`enabled`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Boolean(service)`})}),(0,c.jsx)(t.td,{children:`Пустое имя не запускает запрос`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`staleTime`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Infinity`})}),(0,c.jsx)(t.td,{children:`Metadata не протухает по обычному таймеру`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`gcTime`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Infinity`})}),(0,c.jsx)(t.td,{children:`Metadata не выгружается как обычный неактивный query`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`meta`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`persistedQueryMeta`})}),(0,c.jsx)(t.td,{children:`Snapshot разрешено сохранять persistence-слою`})]})]})]}),`
2214
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`Infinity`}),` не означает, что metadata никогда не обновится. За её актуальностью следит отдельный version-check.`]}),`
2215
+ `,(0,c.jsx)(t.h3,{id:`useodatametadataquery`,children:(0,c.jsx)(t.code,{children:`useODataMetadataQuery`})}),`
2216
+ `,(0,c.jsx)(t.p,{children:`Хук одновременно наблюдает metadata и её серверную версию:`}),`
2217
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataMetadataQuery } from "@ryuzaki13/react-foundation-api/odata";
2218
+
2219
+ export function MetadataState() {
2220
+ const query = useODataMetadataQuery({ service: "ZSALES_SRV" });
2221
+
2222
+ if (query.isPending) return <p>Загрузка metadata…</p>;
2223
+ if (query.isError) return <p>{query.error.message}</p>;
2224
+
2225
+ return <p>Entity: {Object.keys(query.data.entities).length}</p>;
2226
+ }
2227
+ `})}),`
2228
+ `,(0,c.jsxs)(t.p,{children:[`Хук требует `,(0,c.jsx)(t.code,{children:`QueryClientProvider`}),`, потому что внутри использует `,(0,c.jsx)(t.code,{children:`useQuery`}),` и `,(0,c.jsx)(t.code,{children:`useQueryClient`}),`.`]}),`
2229
+ `,(0,c.jsx)(t.h3,{id:`getodatametadatadata`,children:(0,c.jsx)(t.code,{children:`getODataMetadataData`})}),`
2230
+ `,(0,c.jsx)(t.p,{children:`Императивный вариант нужен вне React component: в query function, loader, mutation или service layer.`}),`
2231
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { getODataMetadataData } from "@ryuzaki13/react-foundation-api/odata";
2232
+
2233
+ const metadata = await getODataMetadataData(
2234
+ { service: "ZSALES_SRV" },
2235
+ queryClient
2236
+ );
2237
+
2238
+ const salesOrder = metadata?.entities.SalesOrderSet;
2239
+ `})}),`
2240
+ `,(0,c.jsx)(t.p,{children:`Алгоритм:`}),`
2241
+ `,(0,c.jsxs)(t.ol,{children:[`
2242
+ `,(0,c.jsxs)(t.li,{children:[`При пустом `,(0,c.jsx)(t.code,{children:`service`}),` вернуть `,(0,c.jsx)(t.code,{children:`undefined`}),`.`]}),`
2243
+ `,(0,c.jsx)(t.li,{children:`Получить metadata через общий QueryClient.`}),`
2244
+ `,(0,c.jsx)(t.li,{children:`Проверить server version, если endpoint настроен.`}),`
2245
+ `,(0,c.jsx)(t.li,{children:`При необходимости инвалидировать metadata только этого сервиса.`}),`
2246
+ `,(0,c.jsx)(t.li,{children:`Повторно получить metadata из QueryClient.`}),`
2247
+ `]}),`
2248
+ `,(0,c.jsx)(t.p,{children:`Если технический version endpoint временно недоступен, ошибка version-check подавляется, а уже сохранённая metadata продолжает использоваться. Ошибка самой metadata не подавляется.`}),`
2249
+ `,(0,c.jsxs)(t.h3,{id:`почему-нельзя-делать-обычный-fetchmetadata`,children:[`Почему нельзя делать обычный `,(0,c.jsx)(t.code,{children:`fetch(".../$metadata")`})]}),`
2250
+ `,(0,c.jsx)(t.p,{children:`Такой запрос обойдёт:`}),`
2251
+ `,(0,c.jsxs)(t.ul,{children:[`
2252
+ `,(0,c.jsx)(t.li,{children:`единый URL adapter;`}),`
2253
+ `,(0,c.jsx)(t.li,{children:`SAP headers и cookies;`}),`
2254
+ `,(0,c.jsx)(t.li,{children:`TanStack Query deduplication;`}),`
2255
+ `,(0,c.jsx)(t.li,{children:`persisted snapshot;`}),`
2256
+ `,(0,c.jsx)(t.li,{children:`version-check;`}),`
2257
+ `,(0,c.jsx)(t.li,{children:`общий parser;`}),`
2258
+ `,(0,c.jsx)(t.li,{children:`единый error contract.`}),`
2259
+ `]}),`
2260
+ `,(0,c.jsx)(t.p,{children:`Для application code это почти всегда неправильная граница ответственности.`}),`
2261
+ `,(0,c.jsx)(t.h2,{id:`metadata-version-check`,children:`Metadata version-check`}),`
2262
+ `,(0,c.jsx)(t.p,{children:`Metadata хранится долго, поэтому пакет умеет проверять время последней генерации сервиса через project-specific technical Entity.`}),`
2263
+ `,(0,c.jsx)(t.p,{children:`Endpoint настраивается в adapter:`}),`
2264
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`configureODataProjectAdapter({
2265
+ metadataVersion: {
2266
+ service: "ZTECH_SRV",
2267
+ target: "MetadataVersions",
2268
+ baseUrl: "default"
2269
+ }
2270
+ });
2271
+ `})}),`
2272
+ `,(0,c.jsx)(t.p,{children:`Ожидаемый server record:`}),`
2273
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataMetadataVersionRecord {
2274
+ serviceName: string;
2275
+ lastChanged: string;
2276
+ }
2277
+ `})}),`
2278
+ `,(0,c.jsx)(t.p,{children:`Запрос строится примерно так:`}),`
2279
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`/ZTECH_SRV/MetadataVersions('ZSALES_SRV')
2280
+ `})}),`
2281
+ `,(0,c.jsxs)(t.p,{children:[`Перед подстановкой имя основного сервиса нормализуется через `,(0,c.jsx)(t.code,{children:`normalizeODataServiceName`}),`.`]}),`
2282
+ `,(0,c.jsx)(t.h3,{id:`useodatametadataversionquery`,children:(0,c.jsx)(t.code,{children:`useODataMetadataVersionQuery`})}),`
2283
+ `,(0,c.jsx)(t.p,{children:`Хук даёт прямой доступ к диагностике версии без загрузки metadata:`}),`
2284
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataMetadataVersionQuery } from "@ryuzaki13/react-foundation-api/odata";
2285
+
2286
+ function MetadataVersion() {
2287
+ const version = useODataMetadataVersionQuery({
2288
+ service: "ZSALES_SRV",
2289
+ staleTime: 15 * 60 * 1000
2290
+ });
2291
+
2292
+ if (!version.data?.changedAt) return null;
2293
+ return <time>{version.data.changedAt.toISOString()}</time>;
2294
+ }
2295
+ `})}),`
2296
+ `,(0,c.jsx)(t.p,{children:`Result:`}),`
2297
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataMetadataVersionResult {
2298
+ service: string;
2299
+ changedAt: Date | null;
2300
+ version: string | null;
2301
+ }
2302
+ `})}),`
2303
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`version`}),` — OData datetime string, построенная из `,(0,c.jsx)(t.code,{children:`changedAt`}),`. Если adapter endpoint отсутствует, query function возвращает успешный пустой result с `,(0,c.jsx)(t.code,{children:`null`}),`, не делает network request.`]}),`
2304
+ `,(0,c.jsxs)(t.p,{children:[`Default `,(0,c.jsx)(t.code,{children:`staleTime`}),` version query — 2 часа, `,(0,c.jsx)(t.code,{children:`gcTime`}),` — `,(0,c.jsx)(t.code,{children:`Infinity`}),`. Query key также содержит application build id, чтобы разные сборки не смешивали persisted version snapshot.`]}),`
2305
+ `,(0,c.jsx)(t.h3,{id:`applyodatametadataversion`,children:(0,c.jsx)(t.code,{children:`applyODataMetadataVersion`})}),`
2306
+ `,(0,c.jsx)(t.p,{children:`Функция сравнивает:`}),`
2307
+ `,(0,c.jsxs)(t.ul,{children:[`
2308
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`version.changedAt`}),` с сервера;`]}),`
2309
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`dataUpdatedAt`}),` metadata query в TanStack Query.`]}),`
2310
+ `]}),`
2311
+ `,(0,c.jsx)(t.p,{children:`Metadata инвалидируется только когда:`}),`
2312
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`metadata уже есть
2313
+ AND changedAt существует
2314
+ AND changedAt > metadata.dataUpdatedAt
2315
+ `})}),`
2316
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import {
2317
+ applyODataMetadataVersion,
2318
+ useODataMetadataVersionQuery
2319
+ } from "@ryuzaki13/react-foundation-api/odata";
2320
+
2321
+ await applyODataMetadataVersion(
2322
+ queryClient,
2323
+ { service: "ZSALES_SRV" },
2324
+ {
2325
+ service: "ZSALES_SRV",
2326
+ changedAt: new Date("2026-08-21T08:00:00Z"),
2327
+ version: "datetime'2026-08-21T08:00:00'"
2328
+ }
2329
+ );
2330
+ `})}),`
2331
+ `,(0,c.jsx)(t.p,{children:`Инвалидация точечная: metadata других сервисов и сам version query не затрагиваются. Уже идущий refetch не отменяется.`}),`
2332
+ `,(0,c.jsx)(t.h2,{id:`entity-metadata-hooks`,children:`Entity metadata hooks`}),`
2333
+ `,(0,c.jsx)(t.h3,{id:`useodatametadata`,children:(0,c.jsx)(t.code,{children:`useODataMetadata`})}),`
2334
+ `,(0,c.jsx)(t.p,{children:`Возвращает metadata конкретной Entity:`}),`
2335
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataMetadata } from "@ryuzaki13/react-foundation-api/odata";
2336
+
2337
+ function EntityInfo() {
2338
+ const { metadata, metadataUpdatedAt, isLoading } = useODataMetadata({
2339
+ service: "ZSALES_SRV",
2340
+ target: "SalesOrderSet"
2341
+ });
2342
+
2343
+ if (isLoading) return <p>Загрузка…</p>;
2344
+
2345
+ return (
2346
+ <ul>
2347
+ {metadata?.columns?.map((column) => (
2348
+ <li key={column.id}>{column.id}</li>
2349
+ ))}
2350
+ </ul>
2351
+ );
2352
+ }
2353
+ `})}),`
2354
+ `,(0,c.jsx)(t.p,{children:`Result:`}),`
2355
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Поле`}),(0,c.jsx)(t.th,{children:`Значение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`metadata`})}),(0,c.jsxs)(t.td,{children:[`Entity metadata или `,(0,c.jsx)(t.code,{children:`undefined`}),` во время загрузки/если target не найден`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`metadataUpdatedAt`})}),(0,c.jsx)(t.td,{children:`timestamp обновления metadata query`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`isLoading`})}),(0,c.jsx)(t.td,{children:`идёт ли initial metadata load`})]})]})]}),`
2356
+ `,(0,c.jsx)(t.p,{children:`Если target оказался FunctionImport, хук выбросит понятную ошибку: FunctionImport нельзя использовать как Entity. Если metadata query завершилась ошибкой, хук также выбрасывает domain-oriented error с именем сервиса.`}),`
2357
+ `,(0,c.jsx)(t.h3,{id:`useodataentity`,children:(0,c.jsx)(t.code,{children:`useODataEntity`})}),`
2358
+ `,(0,c.jsx)(t.p,{children:`Добавляет удобные actions над Entity metadata:`}),`
2359
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataEntity } from "@ryuzaki13/react-foundation-api/odata";
2360
+
2361
+ function EntityDiagnostics() {
2362
+ const entity = useODataEntity({
2363
+ service: "ZSALES_SRV",
2364
+ target: "SalesOrderSet"
2365
+ });
2366
+
2367
+ if (entity.isLoading) return null;
2368
+
2369
+ const path = entity.actions.getEntityPath({ SalesOrderId: "50000123" });
2370
+ const columns = entity.actions.getKeys();
2371
+ const pairs = entity.actions.getKeyPairs();
2372
+ const pairsMap = entity.actions.getKeyPairsMap();
2373
+
2374
+ return <pre>{JSON.stringify({ path, columns, pairs, pairsMap }, null, 2)}</pre>;
2375
+ }
2376
+ `})}),`
2377
+ `,(0,c.jsx)(t.p,{children:`Actions:`}),`
2378
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Action`}),(0,c.jsx)(t.th,{children:`Что возвращает`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getEntityPath(values?)`})}),(0,c.jsx)(t.td,{children:`Metadata-correct путь Entity или parameterized Entity`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getKeys()`})}),(0,c.jsx)(t.td,{children:`Массив id всех columns Entity`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getKeyPairs()`})}),(0,c.jsxs)(t.td,{children:[`Пары `,(0,c.jsx)(t.code,{children:`{ codeKey, textKey }`}),` для связанных code/text columns`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getKeyPairsMap()`})}),(0,c.jsxs)(t.td,{children:[`Словарь `,(0,c.jsx)(t.code,{children:`{ [codeKey]: textKey }`})]})]})]})]}),`
2379
+ `,(0,c.jsxs)(t.p,{children:[`До окончания metadata load actions вызывать нельзя: они выбрасывают ошибку. Сначала проверяйте `,(0,c.jsx)(t.code,{children:`isLoading`}),`.`]}),`
2380
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`ODataEntity`}),` — type alias результата хука без поля `,(0,c.jsx)(t.code,{children:`isLoading`}),`. Он полезен для props дочернего компонента, который получает уже готовую Entity model.`]}),`
2381
+ `,(0,c.jsx)(t.h2,{id:`справочники-odata`,children:`Справочники OData`}),`
2382
+ `,(0,c.jsx)(t.p,{children:`Справочник — небольшая или средняя Entity, которая содержит пары code/text и используется в select, filter, autocomplete или зависимых фильтрах.`}),`
2383
+ `,(0,c.jsx)(t.p,{children:`Пример server rows:`}),`
2384
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`[
2385
+ {
2386
+ "CompanyCode": "1000",
2387
+ "CompanyName": "Главная компания",
2388
+ "Plant": "1100",
2389
+ "PlantName": "Основной завод"
2390
+ },
2391
+ {
2392
+ "CompanyCode": "1000",
2393
+ "CompanyName": "Главная компания",
2394
+ "Plant": "1200",
2395
+ "PlantName": "Резервный завод"
2396
+ }
2397
+ ]
2398
+ `})}),`
2399
+ `,(0,c.jsxs)(t.p,{children:[`Metadata связывает `,(0,c.jsx)(t.code,{children:`CompanyCode`}),` с `,(0,c.jsx)(t.code,{children:`CompanyName`}),`, а `,(0,c.jsx)(t.code,{children:`Plant`}),` с `,(0,c.jsx)(t.code,{children:`PlantName`}),`. Пакет строит из rows:`]}),`
2400
+ `,(0,c.jsxs)(t.ul,{children:[`
2401
+ `,(0,c.jsxs)(t.li,{children:[`полный `,(0,c.jsx)(t.code,{children:`items`}),`;`]}),`
2402
+ `,(0,c.jsx)(t.li,{children:`пары code/text;`}),`
2403
+ `,(0,c.jsx)(t.li,{children:`быстрый map code → text field;`}),`
2404
+ `,(0,c.jsx)(t.li,{children:`отдельные массивы для каждого уровня;`}),`
2405
+ `,(0,c.jsx)(t.li,{children:`chain уровней от наименьшей cardinality к наибольшей;`}),`
2406
+ `,(0,c.jsx)(t.li,{children:`timestamp cache snapshot.`}),`
2407
+ `]}),`
2408
+ `,(0,c.jsxs)(t.h3,{id:`минимальный-useodatacollectionquery`,children:[`Минимальный `,(0,c.jsx)(t.code,{children:`useODataCollectionQuery`})]}),`
2409
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataCollectionQuery } from "@ryuzaki13/react-foundation-api/odata";
2410
+
2411
+ interface PlantItem {
2412
+ CompanyCode: string;
2413
+ CompanyName: string;
2414
+ Plant: string;
2415
+ PlantName: string;
2416
+ }
2417
+
2418
+ function PlantSelect() {
2419
+ const plants = useODataCollectionQuery<PlantItem>({
2420
+ service: "ZREF_SRV",
2421
+ target: "PlantSet"
2422
+ });
2423
+
2424
+ if (plants.isPending) return <p>Загрузка…</p>;
2425
+ if (plants.isError) return <p>Ошибка: {plants.error.message}</p>;
2426
+
2427
+ return (
2428
+ <select>
2429
+ {plants.data.separated.Plant.map((item) => (
2430
+ <option key={item.Plant} value={item.Plant}>
2431
+ {item.Plant} — {item.PlantName}
2432
+ </option>
2433
+ ))}
2434
+ </select>
2435
+ );
2436
+ }
2437
+ `})}),`
2438
+ `,(0,c.jsx)(t.p,{children:`Перед request хук загружает Entity metadata, поэтому code/text pairs не надо дублировать вручную.`}),`
2439
+ `,(0,c.jsx)(t.h3,{id:`odatacollectionconfig`,children:(0,c.jsx)(t.code,{children:`ODataCollectionConfig`})}),`
2440
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataServiceCollectionConfig {
2441
+ service: string;
2442
+ target: string;
2443
+ limitedKeys?: readonly string[];
2444
+ serverFilter?: FilterExpression<unknown>;
2445
+ }
2446
+
2447
+ interface ODataCollectionConfig extends ODataServiceCollectionConfig {
2448
+ excludeEmpty?: true;
2449
+ sortByCode?: boolean;
2450
+ hideCode?: true;
2451
+ clientFilter?: (item: CollectionItem) => boolean;
2452
+ swCache?: string;
2453
+ }
2454
+ `})}),`
2455
+ `,(0,c.jsx)(t.p,{children:`Подробно:`}),`
2456
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Поле`}),(0,c.jsx)(t.th,{children:`Default`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`service`})}),(0,c.jsx)(t.td,{children:`—`}),(0,c.jsx)(t.td,{children:`Имя OData service`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`target`})}),(0,c.jsx)(t.td,{children:`—`}),(0,c.jsx)(t.td,{children:`Entity со справочником`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`limitedKeys`})}),(0,c.jsx)(t.td,{children:`все metadata columns`}),(0,c.jsxs)(t.td,{children:[`Ограничить `,(0,c.jsx)(t.code,{children:`$select`}),`; связанные text fields добавятся автоматически`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`serverFilter`})}),(0,c.jsx)(t.td,{children:`—`}),(0,c.jsxs)(t.td,{children:[`Выполнить `,(0,c.jsx)(t.code,{children:`$filter`}),` на сервере`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`excludeEmpty`})}),(0,c.jsx)(t.td,{children:`поведение lib helper`}),(0,c.jsxs)(t.td,{children:[`Исключение пустых codes при построении separated arrays; тип разрешает только `,(0,c.jsx)(t.code,{children:`true`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`sortByCode`})}),(0,c.jsx)(t.td,{children:`code order`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`false`}),` создаёт observer projection, отсортированную по text`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`hideCode`})}),(0,c.jsx)(t.td,{children:`—`}),(0,c.jsx)(t.td,{children:`UI-oriented flag; сам query его не использует`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`clientFilter`})}),(0,c.jsx)(t.td,{children:`—`}),(0,c.jsx)(t.td,{children:`Фильтрация уже загруженных rows до построения snapshot`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`swCache`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ttl=forever;name=ref`})}),(0,c.jsx)(t.td,{children:`Политика Service Worker cache`})]})]})]}),`
2457
+ `,(0,c.jsx)(t.p,{children:`Для config object желательно сохранять stable reference:`}),`
2458
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const plantOData = {
2459
+ service: "ZREF_SRV",
2460
+ target: "PlantSet",
2461
+ limitedKeys: ["CompanyCode", "Plant"]
2462
+ } as const;
2463
+
2464
+ function PlantField() {
2465
+ const query = useODataCollectionQuery(plantOData);
2466
+ // ...
2467
+ }
2468
+ `})}),`
2469
+ `,(0,c.jsxs)(t.h3,{id:`limitedkeys-и-linked-text`,children:[(0,c.jsx)(t.code,{children:`limitedKeys`}),` и linked text`]}),`
2470
+ `,(0,c.jsx)(t.p,{children:`Допустим, вы запросили только codes:`}),`
2471
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`limitedKeys: ["CompanyCode", "Plant"]
2472
+ `})}),`
2473
+ `,(0,c.jsx)(t.p,{children:`Metadata сообщает пары:`}),`
2474
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`[
2475
+ { codeKey: "CompanyCode", textKey: "CompanyName" },
2476
+ { codeKey: "Plant", textKey: "PlantName" }
2477
+ ]
2478
+ `})}),`
2479
+ `,(0,c.jsxs)(t.p,{children:[`Хук автоматически расширит `,(0,c.jsx)(t.code,{children:`$select`}),`:`]}),`
2480
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`$select=CompanyCode,Plant,CompanyName,PlantName
2481
+ `})}),`
2482
+ `,(0,c.jsx)(t.p,{children:`Это защищает UI от ситуации, когда code загружен, а label отсутствует.`}),`
2483
+ `,(0,c.jsxs)(t.h3,{id:`serverfilter-и-clientfilter`,children:[(0,c.jsx)(t.code,{children:`serverFilter`}),` и `,(0,c.jsx)(t.code,{children:`clientFilter`})]}),`
2484
+ `,(0,c.jsx)(t.p,{children:`Server filter сокращает network payload:`}),`
2485
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { createFilterEqual } from "@ryuzaki13/react-foundation-lib/odata-service";
2486
+
2487
+ const activePlants = useODataCollectionQuery({
2488
+ service: "ZREF_SRV",
2489
+ target: "PlantSet",
2490
+ serverFilter: createFilterEqual("IsActive", true)
2491
+ });
2492
+ `})}),`
2493
+ `,(0,c.jsx)(t.p,{children:`Client filter выполняется после загрузки:`}),`
2494
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const localPlants = useODataCollectionQuery({
2495
+ service: "ZREF_SRV",
2496
+ target: "PlantSet",
2497
+ clientFilter: (item) => item.Plant.startsWith("1")
2498
+ });
2499
+ `})}),`
2500
+ `,(0,c.jsxs)(t.p,{children:[`Критически важная разница: `,(0,c.jsx)(t.code,{children:`serverFilter`}),` входит в query key, а `,(0,c.jsx)(t.code,{children:`clientFilter`}),` — нет.`]}),`
2501
+ `,(0,c.jsxs)(t.p,{children:[`Это означает, что два consumers с одинаковыми `,(0,c.jsx)(t.code,{children:`service`}),`, `,(0,c.jsx)(t.code,{children:`target`}),`, `,(0,c.jsx)(t.code,{children:`limitedKeys`}),`, `,(0,c.jsx)(t.code,{children:`serverFilter`}),`, но разными `,(0,c.jsx)(t.code,{children:`clientFilter`}),` используют один Query cache slot и могут записать туда разные snapshots. Не создавайте разные `,(0,c.jsx)(t.code,{children:`clientFilter`}),` для одной identity. Если нужны разные projections, загружайте общий snapshot и фильтруйте через TanStack `,(0,c.jsx)(t.code,{children:`select`}),` или component `,(0,c.jsx)(t.code,{children:`useMemo`}),`.`]}),`
2502
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`excludeEmpty`}),` также не входит в key и влияет на cached `,(0,c.jsx)(t.code,{children:`separated`}),`. Consumers одной identity должны использовать одинаковое значение.`]}),`
2503
+ `,(0,c.jsx)(t.h3,{id:`точный-query-key-справочника`,children:`Точный query key справочника`}),`
2504
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { createODataCollectionQueryKey } from "@ryuzaki13/react-foundation-api/odata";
2505
+
2506
+ const key = createODataCollectionQueryKey({
2507
+ service: "ZREF_SRV",
2508
+ target: "PlantSet",
2509
+ limitedKeys: ["CompanyCode", "Plant"]
2510
+ });
2511
+ `})}),`
2512
+ `,(0,c.jsx)(t.p,{children:`Форма key:`}),`
2513
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`[
2514
+ "odata",
2515
+ "collection",
2516
+ {
2517
+ version: 2,
2518
+ service: "ZREF_SRV",
2519
+ target: "PlantSet",
2520
+ limitedKeys: ["CompanyCode", "Plant"],
2521
+ serverFilter: undefined
2522
+ }
2523
+ ]
2524
+ `})}),`
2525
+ `,(0,c.jsx)(t.p,{children:`Internal version отделяет snapshots разных форматов. Не собирайте key вручную.`}),`
2526
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`createODataCollectionBaseQueryKey()`}),` возвращает prefix всех collection queries:`]}),`
2527
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`await queryClient.invalidateQueries({
2528
+ queryKey: createODataCollectionBaseQueryKey()
2529
+ });
2530
+ `})}),`
2531
+ `,(0,c.jsx)(t.p,{children:`Это широкая инвалидация всех справочников. Для одного справочника используйте точный key.`}),`
2532
+ `,(0,c.jsx)(t.h3,{id:`odatacollectionresult`,children:(0,c.jsx)(t.code,{children:`ODataCollectionResult`})}),`
2533
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataCollectionResult<T> {
2534
+ items: T[];
2535
+ keyPairs: Array<{ codeKey: string; textKey: string }>;
2536
+ keyPairsMap: Record<string, string>;
2537
+ separated: Record<string, T[]>;
2538
+ chain: Array<{ codeKey: string; count: number }>;
2539
+ count: number;
2540
+ cacheUpdatedAt: number;
2541
+ }
2542
+ `})}),`
2543
+ `,(0,c.jsx)(t.p,{children:`Пример:`}),`
2544
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const data = query.data;
2545
+
2546
+ data.items;
2547
+ // Все rows после clientFilter.
2548
+
2549
+ data.keyPairs;
2550
+ // [{ codeKey: "CompanyCode", textKey: "CompanyName" }, ...]
2551
+
2552
+ data.keyPairsMap.Plant;
2553
+ // "PlantName"
2554
+
2555
+ data.separated.Plant;
2556
+ // Уникальные rows уровня Plant.
2557
+
2558
+ data.chain;
2559
+ // Уровни, отсортированные от меньшего количества уникальных codes к большему.
2560
+
2561
+ data.count;
2562
+ // Количество rows после clientFilter.
2563
+
2564
+ data.cacheUpdatedAt;
2565
+ // Timestamp для проверки актуальности snapshot.
2566
+ `})}),`
2567
+ `,(0,c.jsxs)(t.p,{children:[`Transport fields `,(0,c.jsx)(t.code,{children:`ID`}),`, `,(0,c.jsx)(t.code,{children:`CNT`}),`, `,(0,c.jsx)(t.code,{children:`__metadata`}),`, `,(0,c.jsx)(t.code,{children:`__Parameters`}),` удаляются из каждого item.`]}),`
2568
+ `,(0,c.jsx)(t.h3,{id:`ошибки-загрузки-справочника`,children:`Ошибки загрузки справочника`}),`
2569
+ `,(0,c.jsxs)(t.p,{children:[`Низкоуровневая загрузка справочника логирует transport/parsing error и возвращает `,(0,c.jsx)(t.code,{children:`{ items: [] }`}),`. Поэтому `,(0,c.jsx)(t.code,{children:`useODataCollectionQuery`}),` может завершиться успешным пустым result вместо `,(0,c.jsx)(t.code,{children:`isError`}),`.`]}),`
2570
+ `,(0,c.jsx)(t.p,{children:`Практический вывод:`}),`
2571
+ `,(0,c.jsxs)(t.ul,{children:[`
2572
+ `,(0,c.jsx)(t.li,{children:`пустой справочник не всегда означает, что server действительно вернул 0 rows;`}),`
2573
+ `,(0,c.jsx)(t.li,{children:`для критичных сценариев используйте metadata-aware query helper напрямую, если ошибка должна обязательно попасть в error boundary;`}),`
2574
+ `,(0,c.jsx)(t.li,{children:`проверяйте project logs/error reporting при неожиданно пустых данных.`}),`
2575
+ `]}),`
2576
+ `,(0,c.jsx)(t.p,{children:`Это фактическое поведение текущего API, а не рекомендация скрывать ошибки.`}),`
2577
+ `,(0,c.jsx)(t.h3,{id:`сортировка-codetext`,children:`Сортировка code/text`}),`
2578
+ `,(0,c.jsxs)(t.p,{children:[`Query cache всегда хранит канонический порядок по code. Если `,(0,c.jsx)(t.code,{children:`sortByCode: false`}),`, hook применяет TanStack Query `,(0,c.jsx)(t.code,{children:`select`}),` и возвращает observer projection по text.`]}),`
2579
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const byCode = useODataCollectionQuery({
2580
+ service: "ZREF_SRV",
2581
+ target: "PlantSet",
2582
+ sortByCode: true
2583
+ });
2584
+
2585
+ const byText = useODataCollectionQuery({
2586
+ service: "ZREF_SRV",
2587
+ target: "PlantSet",
2588
+ sortByCode: false
2589
+ });
2590
+ `})}),`
2591
+ `,(0,c.jsxs)(t.p,{children:[`Оба hooks используют один network snapshot. `,(0,c.jsx)(t.code,{children:`byText`}),` не переписывает порядок `,(0,c.jsx)(t.code,{children:`byCode`}),`.`]}),`
2592
+ `,(0,c.jsx)(t.h3,{id:`query-lifetime`,children:`Query lifetime`}),`
2593
+ `,(0,c.jsx)(t.p,{children:`Collection query использует:`}),`
2594
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`staleTime = 2 часа
2595
+ gcTime = 1 час после потери observers
2596
+ meta = persistedQueryMeta
2597
+ `})}),`
2598
+ `,(0,c.jsx)(t.p,{children:`Snapshot может одновременно находиться:`}),`
2599
+ `,(0,c.jsxs)(t.ul,{children:[`
2600
+ `,(0,c.jsx)(t.li,{children:`в памяти QueryClient;`}),`
2601
+ `,(0,c.jsx)(t.li,{children:`в persistence storage QueryClient;`}),`
2602
+ `,(0,c.jsx)(t.li,{children:`в HTTP cache Service Worker.`}),`
2603
+ `]}),`
2604
+ `,(0,c.jsx)(t.p,{children:`Это разные уровни, подробнее — в разделе о кэшах.`}),`
2605
+ `,(0,c.jsx)(t.h2,{id:`актуализация-справочников`,children:`Актуализация справочников`}),`
2606
+ `,(0,c.jsx)(t.p,{children:`Для больших справочников невыгодно постоянно скачивать всё заново. Поэтому пакет поддерживает технический endpoint с Entity, изменившимися за последние 7 дней.`}),`
2607
+ `,(0,c.jsx)(t.p,{children:`Adapter:`}),`
2608
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`configureODataProjectAdapter({
2609
+ collectionUpdates: {
2610
+ service: "ZTECH_SRV",
2611
+ target: "CollectionUpdates",
2612
+ baseUrl: "default"
2613
+ }
2614
+ });
2615
+ `})}),`
2616
+ `,(0,c.jsx)(t.p,{children:`Expected record:`}),`
2617
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataCollectionUpdateRecord {
2618
+ entityName: string;
2619
+ lastChanged: string;
2620
+ }
2621
+ `})}),`
2622
+ `,(0,c.jsx)(t.h3,{id:`useodatacollectionupdatesquery`,children:(0,c.jsx)(t.code,{children:`useODataCollectionUpdatesQuery`})}),`
2623
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataCollectionUpdatesQuery } from "@ryuzaki13/react-foundation-api/odata";
2624
+
2625
+ function UpdatesDiagnostics() {
2626
+ const query = useODataCollectionUpdatesQuery({
2627
+ staleTime: 60 * 60 * 1000
2628
+ });
2629
+
2630
+ return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
2631
+ }
2632
+ `})}),`
2633
+ `,(0,c.jsx)(t.p,{children:`Result:`}),`
2634
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataCollectionUpdatesResult {
2635
+ items: ODataCollectionUpdateItem[];
2636
+ byEntityName: Record<string, ODataCollectionUpdateItem>;
2637
+ fetchedAt: number;
2638
+ coverageStartedAt: number;
2639
+ }
2640
+
2641
+ interface ODataCollectionUpdateItem {
2642
+ entityName: string;
2643
+ lastChanged: string;
2644
+ lastChangedAt: Date | null;
2645
+ }
2646
+ `})}),`
2647
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`coverageStartedAt = fetchedAt - 7 дней`}),`.`]}),`
2648
+ `,(0,c.jsx)(t.p,{children:`Если endpoint не настроен, query остаётся активным, но возвращает успешный пустой result без network request. Collection hooks ждут завершения updates query перед своим request.`}),`
2649
+ `,(0,c.jsx)(t.p,{children:`Default:`}),`
2650
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Параметр`}),(0,c.jsx)(t.th,{children:`Значение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`updates `,(0,c.jsx)(t.code,{children:`staleTime`})]}),(0,c.jsx)(t.td,{children:`4 часа`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`updates `,(0,c.jsx)(t.code,{children:`gcTime`})]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Infinity`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`updates SW policy`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ttl=4h;name=ref-updates`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`updates persistence`}),(0,c.jsxs)(t.td,{children:[`включена через `,(0,c.jsx)(t.code,{children:`persistedQueryMeta`})]})]})]})]}),`
2651
+ `,(0,c.jsxs)(t.p,{children:[`Query key содержит build id. Используйте `,(0,c.jsx)(t.code,{children:`createODataCollectionUpdatesQueryKey()`}),`, не собирайте его вручную.`]}),`
2652
+ `,(0,c.jsx)(t.h3,{id:`shouldrefreshodatacollectioncache`,children:(0,c.jsx)(t.code,{children:`shouldRefreshODataCollectionCache`})}),`
2653
+ `,(0,c.jsx)(t.p,{children:`Helper отвечает только на вопрос «нужен ли свежий snapshot?».`}),`
2654
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { shouldRefreshODataCollectionCache } from "@ryuzaki13/react-foundation-api/odata";
2655
+
2656
+ const shouldRefresh = shouldRefreshODataCollectionCache(
2657
+ {
2658
+ update: {
2659
+ entityName: "PlantSet",
2660
+ lastChanged: "20260821090000",
2661
+ lastChangedAt: new Date("2026-08-21T09:00:00Z")
2662
+ },
2663
+ metadataUpdatedAt: Date.now(),
2664
+ updatesCoverageStartedAt: Date.now() - 7 * 24 * 60 * 60 * 1000
2665
+ },
2666
+ cachedCollection
2667
+ );
2668
+ `})}),`
2669
+ `,(0,c.jsxs)(t.p,{children:[`Возвращает `,(0,c.jsx)(t.code,{children:`true`}),`, если выполняется хотя бы одно:`]}),`
2670
+ `,(0,c.jsxs)(t.ul,{children:[`
2671
+ `,(0,c.jsxs)(t.li,{children:[`в updates есть usable `,(0,c.jsx)(t.code,{children:`lastChangedAt`}),`, а cache отсутствует или старее;`]}),`
2672
+ `,(0,c.jsx)(t.li,{children:`metadata обновлена позже cache;`}),`
2673
+ `,(0,c.jsx)(t.li,{children:`cache отсутствует;`}),`
2674
+ `,(0,c.jsx)(t.li,{children:`cache старше окна, которое покрывает updates endpoint.`}),`
2675
+ `]}),`
2676
+ `,(0,c.jsx)(t.p,{children:`Последнее правило принципиально: если endpoint сообщает только изменения за 7 дней, отсутствие Entity в ответе ничего не доказывает для cache возрастом больше недели.`}),`
2677
+ `,(0,c.jsxs)(t.p,{children:[`Некорректные timestamps (`,(0,c.jsx)(t.code,{children:`NaN`}),`, `,(0,c.jsx)(t.code,{children:`Infinity`}),`, `,(0,c.jsx)(t.code,{children:`0`}),`, отрицательные) игнорируются.`]}),`
2678
+ `,(0,c.jsx)(t.h3,{id:`resolveodatacollectionswcachepolicy`,children:(0,c.jsx)(t.code,{children:`resolveODataCollectionSwCachePolicy`})}),`
2679
+ `,(0,c.jsx)(t.p,{children:`Если snapshot нужно обновить, helper превращает обычную TTL policy в bust policy:`}),`
2680
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveODataCollectionSwCachePolicy("ttl=24h;name=ref", true);
2681
+ // "bust=24h;name=ref"
2682
+
2683
+ resolveODataCollectionSwCachePolicy("ttl=forever;name=ref", true);
2684
+ // "bust=forever;name=ref"
2685
+
2686
+ resolveODataCollectionSwCachePolicy("off", true);
2687
+ // "off"
2688
+
2689
+ resolveODataCollectionSwCachePolicy("bust=1h;name=ref", true);
2690
+ // уже bust, строка не меняется
2691
+
2692
+ resolveODataCollectionSwCachePolicy("custom-policy", true);
2693
+ // "bust=forever;name=ref"
2694
+ `})}),`
2695
+ `,(0,c.jsxs)(t.p,{children:[`При `,(0,c.jsx)(t.code,{children:`shouldBust === false`}),` исходная строка возвращается без изменений.`]}),`
2696
+ `,(0,c.jsx)(t.h3,{id:`public-constants`,children:`Public constants`}),`
2697
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`ODATA_COLLECTION_UPDATES_LOOKBACK_MS;
2698
+ // 7 дней в миллисекундах
2699
+
2700
+ ODATA_COLLECTION_DEFAULT_SW_CACHE;
2701
+ // "ttl=forever;name=ref"
2702
+
2703
+ ODATA_COLLECTION_BUST_SW_CACHE;
2704
+ // "bust=forever;name=ref"
2705
+
2706
+ ODATA_COLLECTION_UPDATES_SW_CACHE;
2707
+ // "ttl=4h;name=ref-updates"
2708
+ `})}),`
2709
+ `,(0,c.jsx)(t.p,{children:`Не копируйте строковые значения в application code, если нужен именно package default.`}),`
2710
+ `,(0,c.jsx)(t.h2,{id:`ui-model-справочника`,children:`UI model справочника`}),`
2711
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useODataCollectionQuery`}),` отвечает за server snapshot. `,(0,c.jsx)(t.code,{children:`useODataCollection`}),` добавляет локальные операции для select/listbox/virtualized UI.`]}),`
2712
+ `,(0,c.jsx)(t.h3,{id:`odatacollectionmodel`,children:(0,c.jsx)(t.code,{children:`ODataCollectionModel`})}),`
2713
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataCollectionModel {
2714
+ codeKey: string;
2715
+ minSearchTextLength?: number;
2716
+ minSearchCodeLength?: number;
2717
+ searchDebounceDelay?: number;
2718
+ maxVisibleItems?: number;
2719
+ }
2720
+ `})}),`
2721
+ `,(0,c.jsx)(t.h3,{id:`useodatacollectionmodel`,children:(0,c.jsx)(t.code,{children:`useODataCollectionModel`})}),`
2722
+ `,(0,c.jsx)(t.p,{children:`Подставляет defaults из общего collection store и фиксирует model на весь lifecycle component:`}),`
2723
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataCollectionModel } from "@ryuzaki13/react-foundation-api/odata";
2724
+
2725
+ const model = useODataCollectionModel({
2726
+ codeKey: "Plant",
2727
+ maxVisibleItems: 100,
2728
+ minSearchCodeLength: 1,
2729
+ minSearchTextLength: 2,
2730
+ searchDebounceDelay: 250
2731
+ });
2732
+ `})}),`
2733
+ `,(0,c.jsx)(t.p,{children:`Последующие изменения переданного object не пересоздают stable model. Рассматривайте вход как initial config, а не reactive props.`}),`
2734
+ `,(0,c.jsx)(t.h3,{id:`useodatacollection`,children:(0,c.jsx)(t.code,{children:`useODataCollection`})}),`
2735
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import {
2736
+ useODataCollection,
2737
+ useODataCollectionModel
2738
+ } from "@ryuzaki13/react-foundation-api/odata";
2739
+
2740
+ function PlantList() {
2741
+ const model = useODataCollectionModel({
2742
+ codeKey: "Plant",
2743
+ maxVisibleItems: 50
2744
+ });
2745
+
2746
+ const collection = useODataCollection({
2747
+ odata: {
2748
+ service: "ZREF_SRV",
2749
+ target: "PlantSet"
2750
+ },
2751
+ model,
2752
+ pageSize: 25
2753
+ });
2754
+
2755
+ const visible = collection.getItems(
2756
+ (code, text) => code.startsWith("1") || text.toLowerCase().includes("завод")
2757
+ );
2758
+
2759
+ return (
2760
+ <ul>
2761
+ {visible.map((item) => (
2762
+ <li key={item.Plant}>{item.PlantName}</li>
2763
+ ))}
2764
+ </ul>
2765
+ );
2766
+ }
2767
+ `})}),`
2768
+ `,(0,c.jsx)(t.p,{children:`Result API:`}),`
2769
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Поле/action`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`data`})}),(0,c.jsxs)(t.td,{children:[`Raw `,(0,c.jsx)(t.code,{children:`ODataCollectionResult`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`isLoading`}),`, `,(0,c.jsx)(t.code,{children:`isError`})]}),(0,c.jsx)(t.td,{children:`Query state`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`codeKey`})}),(0,c.jsx)(t.td,{children:`Активное code field из model`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`textKey`})}),(0,c.jsx)(t.td,{children:`Linked text field из metadata`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getItems(predicate?, selected?)`})}),(0,c.jsxs)(t.td,{children:[`Получить не более `,(0,c.jsx)(t.code,{children:`maxVisibleItems`}),`, учесть dependencies и выбранные rows`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`findSourceItemsByKeys(field, keys)`})}),(0,c.jsxs)(t.td,{children:[`Найти rows полного `,(0,c.jsx)(t.code,{children:`items`}),` по значениям произвольного field`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`setFilteredItems(items?)`})}),(0,c.jsx)(t.td,{children:`Ограничить активный code level набором dependency rows`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getPage(pageIndex)`})}),(0,c.jsxs)(t.td,{children:[`Slice по `,(0,c.jsx)(t.code,{children:`pageSize`}),` для virtualization`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`buildSearchIndex(field)`})}),(0,c.jsx)(t.td,{children:`Построить prefix index длиной от 1 до 5 symbols`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`debounce(key, fn, delay?)`})}),(0,c.jsx)(t.td,{children:`Независимый timer по string key`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`itemsMap`})}),(0,c.jsx)(t.td,{children:`Map active code → row`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`separatedItems`})}),(0,c.jsx)(t.td,{children:`Rows active code level`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`orderedKeys`})}),(0,c.jsx)(t.td,{children:`Codes в текущем порядке`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`totalCount`})}),(0,c.jsx)(t.td,{children:`Размер active separated level`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`pageSize`})}),(0,c.jsx)(t.td,{children:`Фактический размер page`})]})]})]}),`
2770
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`getItems`}),` исключает rows, уже переданные в `,(0,c.jsx)(t.code,{children:`currentSelectedKeys`}),`, применяет dependencies, затем predicate и останавливается на лимите.`]}),`
2771
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`setFilteredItems(undefined)`}),` или пустой массив снимает dependency filter.`]}),`
2772
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`buildSearchIndex`}),` индексирует lowercase prefixes максимум до 5 symbols. Для поиска по более длинной строке используйте первые 5 symbols для получения candidates, затем точный predicate.`]}),`
2773
+ `,(0,c.jsxs)(t.p,{children:[`Поле `,(0,c.jsx)(t.code,{children:`filter`}),` во входных props в текущей реализации не используется. Не полагайтесь на него; фильтруйте через `,(0,c.jsx)(t.code,{children:`odata.clientFilter`}),`, `,(0,c.jsx)(t.code,{children:`getItems(predicate)`}),` или собственную projection.`]}),`
2774
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`refetch`}),` и `,(0,c.jsx)(t.code,{children:`invalidate`}),` в result помечены deprecated. Для явного управления используйте `,(0,c.jsx)(t.code,{children:`QueryClient`}),` и public key factory.`]}),`
2775
+ `,(0,c.jsx)(t.h2,{id:`chains-зависимых-справочников`,children:`Chains зависимых справочников`}),`
2776
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`chain`}),` помогает расположить связанные filters от самого узкого уровня к самому широкому. Например:`]}),`
2777
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`[
2778
+ { codeKey: "CompanyCode", count: 3 },
2779
+ { codeKey: "Plant", count: 24 },
2780
+ { codeKey: "StorageLocation", count: 180 }
2781
+ ]
2782
+ `})}),`
2783
+ `,(0,c.jsx)(t.h3,{id:`useodatacollectionchains`,children:(0,c.jsx)(t.code,{children:`useODataCollectionChains`})}),`
2784
+ `,(0,c.jsx)(t.p,{children:`Хук читает chains нескольких collection snapshots и подписывается на изменения Query cache:`}),`
2785
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataCollectionChains } from "@ryuzaki13/react-foundation-api/odata";
2786
+
2787
+ const services = [
2788
+ { service: "ZREF_SRV", target: "PlantSet" },
2789
+ { service: "ZREF_SRV", target: "MaterialSet" }
2790
+ ] as const;
2791
+
2792
+ function ChainsView() {
2793
+ const chains = useODataCollectionChains([...services]);
2794
+
2795
+ return <pre>{JSON.stringify(chains, null, 2)}</pre>;
2796
+ }
2797
+ `})}),`
2798
+ `,(0,c.jsxs)(t.p,{children:[`Map key имеет форму `,(0,c.jsx)(t.code,{children:`service.target`}),`:`]}),`
2799
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
2800
+ "ZREF_SRV.PlantSet": [
2801
+ { codeKey: "CompanyCode", count: 3 },
2802
+ { codeKey: "Plant", count: 24 }
2803
+ ]
2804
+ }
2805
+ `})}),`
2806
+ `,(0,c.jsx)(t.p,{children:`Хук не загружает справочники сам. Он наблюдает только snapshots, которые уже появились в QueryClient.`}),`
2807
+ `,(0,c.jsx)(t.h2,{id:`зависимые-odata-segments`,children:`Зависимые OData segments`}),`
2808
+ `,(0,c.jsx)(t.p,{children:`Segment описывает один level сложного справочника.`}),`
2809
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataCollectionSegment {
2810
+ placeholder: string;
2811
+ hideCode?: true;
2812
+ selectText?: true;
2813
+ }
2814
+
2815
+ interface ODataDependentSegment extends ODataCollectionSegment {
2816
+ panelVisibility?: "user" | "always" | "none";
2817
+ }
2818
+ `})}),`
2819
+ `,(0,c.jsxs)(t.p,{children:[`Значения `,(0,c.jsx)(t.code,{children:`panelVisibility`}),`:`]}),`
2820
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Значение`}),(0,c.jsx)(t.th,{children:`Смысл`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`user`})}),(0,c.jsx)(t.td,{children:`Видимость определяется пользовательским вариантом; default`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`always`})}),(0,c.jsx)(t.td,{children:`Segment всегда показывается в общей filter panel`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`none`})}),(0,c.jsx)(t.td,{children:`Segment не показывается в panel`})]})]})]}),`
2821
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`selectText: true`}),` означает, что selected value должен быть text, а не code. Это контракт для UI consumer; сам transport ничего не меняет.`]}),`
2822
+ `,(0,c.jsx)(t.h3,{id:`flattenodatadependentservices`,children:(0,c.jsx)(t.code,{children:`flattenODataDependentServices`})}),`
2823
+ `,(0,c.jsx)(t.p,{children:`На входе services сгруппированы:`}),`
2824
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const dependentServices = [
2825
+ {
2826
+ odata: {
2827
+ service: "ZREF_SRV",
2828
+ target: "PlantSet"
2829
+ },
2830
+ segments: {
2831
+ CompanyCode: {
2832
+ placeholder: "Компания",
2833
+ panelVisibility: "always"
2834
+ },
2835
+ Plant: {
2836
+ placeholder: "Завод"
2837
+ }
2838
+ },
2839
+ model: {
2840
+ maxVisibleItems: 100
2841
+ }
2842
+ }
2843
+ ] as const;
2844
+ `})}),`
2845
+ `,(0,c.jsx)(t.p,{children:`Функция делает flat segment list:`}),`
2846
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { flattenODataDependentServices } from "@ryuzaki13/react-foundation-api/odata";
2847
+
2848
+ const segments = flattenODataDependentServices(dependentServices);
2849
+ `})}),`
2850
+ `,(0,c.jsxs)(t.p,{children:[`Каждый `,(0,c.jsx)(t.code,{children:`ODataDependentSegmentItem`}),` содержит:`]}),`
2851
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface ODataDependentSegmentItem {
2852
+ id: string;
2853
+ serviceKey: string;
2854
+ serviceIndex: number;
2855
+ segmentIndex: number;
2856
+ odata: ODataCollectionConfig;
2857
+ segment: ODataDependentSegment;
2858
+ model: ODataCollectionModel;
2859
+ panelVisibility: ControlPanelVisibility;
2860
+ }
2861
+ `})}),`
2862
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`model.codeKey`}),` автоматически равен id segment. `,(0,c.jsx)(t.code,{children:`serviceKey`}),` строится как `,(0,c.jsx)(t.code,{children:`service.target`}),`. Индексы сохраняют исходный стабильный порядок.`]}),`
2863
+ `,(0,c.jsx)(t.h3,{id:`sortodatadependentsegmentitemsbychains`,children:(0,c.jsx)(t.code,{children:`sortODataDependentSegmentItemsByChains`})}),`
2864
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { sortODataDependentSegmentItemsByChains } from "@ryuzaki13/react-foundation-api/odata";
2865
+
2866
+ const ordered = sortODataDependentSegmentItemsByChains(
2867
+ segments,
2868
+ {
2869
+ "ZREF_SRV.PlantSet": [
2870
+ { codeKey: "CompanyCode", count: 3 },
2871
+ { codeKey: "Plant", count: 24 }
2872
+ ]
2873
+ },
2874
+ ["Plant"]
2875
+ );
2876
+ `})}),`
2877
+ `,(0,c.jsx)(t.p,{children:`Порядок без override:`}),`
2878
+ `,(0,c.jsxs)(t.ol,{children:[`
2879
+ `,(0,c.jsx)(t.li,{children:`Исходный порядок services.`}),`
2880
+ `,(0,c.jsx)(t.li,{children:`Внутри service — chain order для известных ids.`}),`
2881
+ `,(0,c.jsx)(t.li,{children:`Segment, присутствующие в chain, идут раньше неизвестных.`}),`
2882
+ `,(0,c.jsxs)(t.li,{children:[`Неизвестные сохраняют исходный `,(0,c.jsx)(t.code,{children:`segmentIndex`}),`.`]}),`
2883
+ `,(0,c.jsx)(t.li,{children:`Полная стабильность обеспечивается исходным flat index.`}),`
2884
+ `]}),`
2885
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`orderOverrideIds`}),` поднимает указанные ids в заданном порядке. Дубликаты удаляются, неизвестные ids игнорируются, остальные segments сохраняют base order.`]}),`
2886
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`sortODataDependentServicesByChains`}),` оставлен как deprecated API старой service-level модели. Для нового кода сначала flatten, затем сортируйте segment items.`]}),`
2887
+ `,(0,c.jsx)(t.h2,{id:`odata-metadata--table-columns`,children:`OData metadata → Table columns`}),`
2888
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useODataTableColumns`}),` связывает metadata с helpers модуля `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/table`}),`. Есть два режима.`]}),`
2889
+ `,(0,c.jsxs)(t.h3,{id:`режим-build`,children:[`Режим `,(0,c.jsx)(t.code,{children:`build`})]}),`
2890
+ `,(0,c.jsx)(t.p,{children:`Генерирует columns только по metadata:`}),`
2891
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useODataTableColumns } from "@ryuzaki13/react-foundation-api/odata";
2892
+
2893
+ interface SalesOrderRow {
2894
+ SalesOrderId: string;
2895
+ NetAmount: number;
2896
+ CreatedAt: Date;
2897
+ }
2898
+
2899
+ function SalesOrderTable() {
2900
+ const { columns, defaultColumnVisibility, isLoading } =
2901
+ useODataTableColumns<SalesOrderRow>({
2902
+ mode: "build",
2903
+ service: "ZSALES_SRV",
2904
+ target: "SalesOrderSet",
2905
+ resolveHeader: (column) =>
2906
+ column.id === "SalesOrderId" ? "Заказ" : column.label ?? column.id,
2907
+ resolveVisible: (column) => column.id !== "InternalStatus",
2908
+ resolveFormatting: (column) => {
2909
+ if (column.id === "NetAmount") {
2910
+ return { role: "number" };
2911
+ }
2912
+
2913
+ return undefined;
2914
+ }
2915
+ });
2916
+
2917
+ if (isLoading) return <p>Подготовка columns…</p>;
2918
+
2919
+ return (
2920
+ <Table
2921
+ columns={columns}
2922
+ initialState={{ columnVisibility: defaultColumnVisibility }}
2923
+ />
2924
+ );
2925
+ }
2926
+ `})}),`
2927
+ `,(0,c.jsx)(t.p,{children:`Callback purposes:`}),`
2928
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Callback`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveHeader(column)`})}),(0,c.jsx)(t.td,{children:`Переопределить header generated column`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveVisible(column)`})}),(0,c.jsx)(t.td,{children:`Сформировать initial visibility`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveFormatting(column)`})}),(0,c.jsx)(t.td,{children:`Добавить typed formatting metadata`})]})]})]}),`
2929
+ `,(0,c.jsxs)(t.h3,{id:`режим-enrich`,children:[`Режим `,(0,c.jsx)(t.code,{children:`enrich`})]}),`
2930
+ `,(0,c.jsx)(t.p,{children:`Сохраняет ваши columns, но дополняет их OData formatting context:`}),`
2931
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const baseColumns = [
2932
+ {
2933
+ accessorKey: "SalesOrderId",
2934
+ header: "Заказ"
2935
+ },
2936
+ {
2937
+ accessorKey: "NetAmount",
2938
+ header: "Сумма"
2939
+ }
2940
+ ] as const;
2941
+
2942
+ const table = useODataTableColumns<SalesOrderRow>({
2943
+ mode: "enrich",
2944
+ service: "ZSALES_SRV",
2945
+ target: "SalesOrderSet",
2946
+ columns: baseColumns
2947
+ });
2948
+ `})}),`
2949
+ `,(0,c.jsxs)(t.p,{children:[`В режиме `,(0,c.jsx)(t.code,{children:`enrich`}),` initial visibility возвращается только для leaf columns, которые удалось сопоставить с metadata по stable column id. Group columns обходятся рекурсивно.`]}),`
2950
+ `,(0,c.jsx)(t.h3,{id:`result`,children:`Result`}),`
2951
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`interface UseODataTableColumnsResult<TData extends object> {
2952
+ columns: TableColumnDef<TData>[];
2953
+ defaultColumnVisibility: Record<string, boolean> | undefined;
2954
+ metadata: EntityMetadata | undefined;
2955
+ isLoading: boolean;
2956
+ }
2957
+ `})}),`
2958
+ `,(0,c.jsx)(t.p,{children:`До загрузки metadata:`}),`
2959
+ `,(0,c.jsxs)(t.ul,{children:[`
2960
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`build`}),` возвращает пустой `,(0,c.jsx)(t.code,{children:`columns`}),`;`]}),`
2961
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`enrich`}),` возвращает shallow copy входных columns;`]}),`
2962
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`defaultColumnVisibility`}),` равен `,(0,c.jsx)(t.code,{children:`undefined`}),`;`]}),`
2963
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`metadata`}),` равна `,(0,c.jsx)(t.code,{children:`undefined`}),`.`]}),`
2964
+ `]}),`
2965
+ `,(0,c.jsxs)(t.p,{children:[`Не сохраняйте `,(0,c.jsx)(t.code,{children:`defaultColumnVisibility`}),` обратно в metadata. Это стартовое состояние TanStack Table, которое пользователь затем может менять отдельно.`]}),`
2966
+ `,(0,c.jsx)(t.h2,{id:`автоматическое-восстановление-sso`,children:`Автоматическое восстановление SSO`}),`
2967
+ `,(0,c.jsxs)(t.p,{children:[`SAP Gateway иногда вместо OData JSON возвращает redirect или HTML-форму ADFS/SSO. Если обычный JSON parser попытается разобрать такой ответ, разработчик увидит непонятную ошибку вида «Unexpected token `,(0,c.jsx)(t.code,{children:`<`}),`». Transport распознаёт этот сценарий и пытается восстановить session.`]}),`
2968
+ `,(0,c.jsx)(t.h3,{id:`как-распознаётся-sso`,children:`Как распознаётся SSO`}),`
2969
+ `,(0,c.jsx)(t.p,{children:`Есть два основных сигнала:`}),`
2970
+ `,(0,c.jsxs)(t.ol,{children:[`
2971
+ `,(0,c.jsxs)(t.li,{children:[`При `,(0,c.jsx)(t.code,{children:`redirect: "manual"`}),` browser возвращает response с `,(0,c.jsx)(t.code,{children:`type === "opaqueredirect"`}),`.`]}),`
2972
+ `,(0,c.jsxs)(t.li,{children:[`Response body выглядит как HTML и содержит form с input `,(0,c.jsx)(t.code,{children:`SAMLRequest`}),` или `,(0,c.jsx)(t.code,{children:`SAMLResponse`}),`.`]}),`
2973
+ `]}),`
2974
+ `,(0,c.jsx)(t.p,{children:`Не любая HTML-форма считается SSO. Проверка намеренно узкая:`}),`
2975
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { isSsoForm } from "@ryuzaki13/react-foundation-api/odata";
2976
+
2977
+ isSsoForm({
2978
+ action: "/adfs/ls/",
2979
+ method: "POST",
2980
+ inputs: { SAMLRequest: "..." }
2981
+ });
2982
+ // true
2983
+
2984
+ isSsoForm({
2985
+ action: "/login",
2986
+ method: "POST",
2987
+ inputs: { username: "" }
2988
+ });
2989
+ // false
2990
+ `})}),`
2991
+ `,(0,c.jsx)(t.h3,{id:`ssoform`,children:(0,c.jsx)(t.code,{children:`SsoForm`})}),`
2992
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type SsoForm = {
2993
+ action: string;
2994
+ method: string;
2995
+ inputs: Record<string, string>;
2996
+ };
2997
+ `})}),`
2998
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`inputs`}),` могут содержать чувствительные одноразовые SAML values. Не выводите весь object в console и не добавляйте его в analytics/error report.`]}),`
2999
+ `,(0,c.jsx)(t.h3,{id:`ssorequirederror`,children:(0,c.jsx)(t.code,{children:`SsoRequiredError`})}),`
3000
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { SsoRequiredError } from "@ryuzaki13/react-foundation-api/odata";
3001
+
3002
+ try {
3003
+ await fetchODataJson("/ZSALES_SRV/SalesOrderSet");
3004
+ } catch (error) {
3005
+ if (error instanceof SsoRequiredError) {
3006
+ console.error(error.message, {
3007
+ recoverable: error.recoverable,
3008
+ hasForm: Boolean(error.form),
3009
+ hasRecoveryUrl: Boolean(error.recoveryUrl)
3010
+ });
3011
+ }
3012
+ }
3013
+ `})}),`
3014
+ `,(0,c.jsx)(t.p,{children:`Instance fields:`}),`
3015
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Поле`}),(0,c.jsx)(t.th,{children:`Смысл`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`message`})}),(0,c.jsx)(t.td,{children:`Человекочитаемая причина`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`form`})}),(0,c.jsx)(t.td,{children:`Извлечённая SAML form, если была в HTML`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`recoveryUrl`})}),(0,c.jsx)(t.td,{children:`URL для iframe navigation, если был redirect`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`recoverable`})}),(0,c.jsx)(t.td,{children:`Можно ли transport-у повторить automatic recovery`})]})]})]}),`
3016
+ `,(0,c.jsxs)(t.p,{children:[`Default `,(0,c.jsx)(t.code,{children:`recoverable`}),` равен `,(0,c.jsx)(t.code,{children:`true`}),`.`]}),`
3017
+ `,(0,c.jsx)(t.h3,{id:`automatic-recovery-lifecycle`,children:`Automatic recovery lifecycle`}),`
3018
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`OData request
3019
+
3020
+ ├─ normal JSON ───────────────────────────────► success
3021
+
3022
+ └─ redirect / SAML form
3023
+
3024
+
3025
+ hidden iframe recovery
3026
+
3027
+
3028
+ retry request once
3029
+
3030
+ ├─ success ─────────────────────────► success
3031
+
3032
+ └─ SSO снова требуется
3033
+
3034
+
3035
+ unrecoverable SsoRequiredError
3036
+ `})}),`
3037
+ `,(0,c.jsx)(t.p,{children:`Transport использует глобальный gate:`}),`
3038
+ `,(0,c.jsxs)(t.ul,{children:[`
3039
+ `,(0,c.jsx)(t.li,{children:`несколько одновременных SAP requests не создают несколько recovery iframes;`}),`
3040
+ `,(0,c.jsx)(t.li,{children:`первый request запускает recovery;`}),`
3041
+ `,(0,c.jsx)(t.li,{children:`остальные ждут тот же promise;`}),`
3042
+ `,(0,c.jsx)(t.li,{children:`после успешного gate каждый request повторяется;`}),`
3043
+ `,(0,c.jsx)(t.li,{children:`если recovery или retry снова потребовал SSO, transport блокирует последующие SAP requests unrecoverable error до reload страницы.`}),`
3044
+ `]}),`
3045
+ `,(0,c.jsx)(t.p,{children:`Такое блокирование защищает от бесконечного цикла redirect → iframe → retry.`}),`
3046
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`ssoRetried`}),` в `,(0,c.jsx)(t.code,{children:`RequestInitType`}),` — внутренняя метка одного retry. Не выставляйте её в application code.`]}),`
3047
+ `,(0,c.jsx)(t.h3,{id:`что-означает-завершение-iframe`,children:`Что означает завершение iframe`}),`
3048
+ `,(0,c.jsxs)(t.p,{children:[`Recovery promise завершается после `,(0,c.jsx)(t.code,{children:`load`}),` или `,(0,c.jsx)(t.code,{children:`error`}),` iframe и короткой settle delay. Само событие iframe ещё не доказывает, что authentication успешна. Истинная проверка — повторный OData request.`]}),`
3049
+ `,(0,c.jsx)(t.h3,{id:`recoverssosession`,children:(0,c.jsx)(t.code,{children:`recoverSsoSession`})}),`
3050
+ `,(0,c.jsx)(t.p,{children:`Обычно вызывается автоматически. Public function полезна для диагностического или controlled flow:`}),`
3051
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { recoverSsoSession } from "@ryuzaki13/react-foundation-api/odata";
3052
+
3053
+ const recovered = await recoverSsoSession({
3054
+ recoveryUrl: "/sap/opu/odata/sap/ZSALES_SRV/"
3055
+ });
3056
+
3057
+ if (!recovered) {
3058
+ // Среда без window/document либо recovery target отсутствует.
3059
+ }
3060
+ `})}),`
3061
+ `,(0,c.jsx)(t.p,{children:`Или с form:`}),`
3062
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const recovered = await recoverSsoSession({
3063
+ form: {
3064
+ action: "/adfs/ls/",
3065
+ method: "POST",
3066
+ inputs: {
3067
+ SAMLRequest: samlRequest
3068
+ }
3069
+ }
3070
+ });
3071
+ `})}),`
3072
+ `,(0,c.jsx)(t.p,{children:`Одновременные ручные вызовы также переиспользуют один promise.`}),`
3073
+ `,(0,c.jsxs)(t.p,{children:[`В SSR/Node без `,(0,c.jsx)(t.code,{children:`window`}),` функция возвращает `,(0,c.jsx)(t.code,{children:`false`}),`. Основной runtime модуля — browser SPA.`]}),`
3074
+ `,(0,c.jsx)(t.h3,{id:`submitssoform`,children:(0,c.jsx)(t.code,{children:`submitSsoForm`})}),`
3075
+ `,(0,c.jsxs)(t.p,{children:[`Низкоуровневая imperative function создаёт hidden iframe и form, монтирует их в document и вызывает `,(0,c.jsx)(t.code,{children:`form.submit()`}),`:`]}),`
3076
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { submitSsoForm } from "@ryuzaki13/react-foundation-api/odata";
3077
+
3078
+ const submitted = submitSsoForm(ssoForm);
3079
+
3080
+ if (!submitted) {
3081
+ // Нет document/body либо form не является SAML form.
3082
+ }
3083
+ `})}),`
3084
+ `,(0,c.jsxs)(t.p,{children:[`Это явный browser side effect. Для обычного OData request не вызывайте её — automatic recovery уже встроено в `,(0,c.jsx)(t.code,{children:`fetchBase`}),`/`,(0,c.jsx)(t.code,{children:`fetchODataJson`}),`.`]}),`
3085
+ `,(0,c.jsx)(t.h2,{id:`csrf-для-sap-write-operations`,children:`CSRF для SAP write operations`}),`
3086
+ `,(0,c.jsx)(t.p,{children:`SAP Gateway требует CSRF token для изменяющих requests. Transport управляет token автоматически.`}),`
3087
+ `,(0,c.jsxs)(t.p,{children:[`Write определяется как любой method, кроме `,(0,c.jsx)(t.code,{children:`GET`}),` и `,(0,c.jsx)(t.code,{children:`HEAD`}),`. Следовательно, token запрашивается для `,(0,c.jsx)(t.code,{children:`POST`}),`, `,(0,c.jsx)(t.code,{children:`PUT`}),`, `,(0,c.jsx)(t.code,{children:`PATCH`}),`, `,(0,c.jsx)(t.code,{children:`DELETE`}),` и других custom write methods, если request распознан как SAP.`]}),`
3088
+ `,(0,c.jsx)(t.h3,{id:`полный-lifecycle`,children:`Полный lifecycle`}),`
3089
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`POST /ZSALES_SRV/SalesOrderSet
3090
+
3091
+
3092
+ CSRF token уже есть в memory cache?
3093
+
3094
+ ┌────┴────┐
3095
+ да нет
3096
+ │ │
3097
+ │ ▼
3098
+ │ GET /ZSALES_SRV/
3099
+ │ x-csrf-token: Fetch
3100
+ │ │
3101
+ │ ▼
3102
+ │ сохранить token
3103
+ └────┬─────┘
3104
+
3105
+ write request + x-csrf-token
3106
+
3107
+ ├─ 2xx ───────────────────────────────► success
3108
+
3109
+ └─ 401/403
3110
+
3111
+
3112
+ удалить cached token
3113
+
3114
+
3115
+ запросить новый token
3116
+
3117
+
3118
+ retry once
3119
+ `})}),`
3120
+ `,(0,c.jsx)(t.h3,{id:`token-scope`,children:`Token scope`}),`
3121
+ `,(0,c.jsx)(t.p,{children:`Для обычного OData scope включает:`}),`
3122
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`baseUrlType + sapClient + serviceName
3123
+ `})}),`
3124
+ `,(0,c.jsx)(t.p,{children:`Например:`}),`
3125
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`odata:100:ZSALES_SRV
3126
+ `})}),`
3127
+ `,(0,c.jsx)(t.p,{children:`Это не позволяет случайно использовать token одного сервиса/mandant для другого.`}),`
3128
+ `,(0,c.jsxs)(t.p,{children:[`Для `,(0,c.jsx)(t.code,{children:`baseUrl: "config"`}),` scope строится из application id и SAP client, а token загружается с `,(0,c.jsx)(t.code,{children:`/Statistics`}),`.`]}),`
3129
+ `,(0,c.jsx)(t.h3,{id:`deduplication`,children:`Deduplication`}),`
3130
+ `,(0,c.jsx)(t.p,{children:`Пакет хранит отдельно:`}),`
3131
+ `,(0,c.jsxs)(t.ul,{children:[`
3132
+ `,(0,c.jsx)(t.li,{children:`готовые tokens;`}),`
3133
+ `,(0,c.jsx)(t.li,{children:`promises уже идущих token requests.`}),`
3134
+ `]}),`
3135
+ `,(0,c.jsx)(t.p,{children:`Если пять mutations стартовали одновременно без token, network получит один token request, а остальные дождутся его результата.`}),`
3136
+ `,(0,c.jsx)(t.h3,{id:`headers-token-request`,children:`Headers token request`}),`
3137
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`x-csrf-token: Fetch
3138
+ sap-language: ru
3139
+ sap-contextid-accept: header
3140
+ Accept-Language: ru
3141
+ sap-client: <client>, если определён
3142
+ `})}),`
3143
+ `,(0,c.jsxs)(t.p,{children:[`Credentials всегда `,(0,c.jsx)(t.code,{children:`include`}),`, redirect для SAP — `,(0,c.jsx)(t.code,{children:`manual`}),`.`]}),`
3144
+ `,(0,c.jsx)(t.h3,{id:`что-должен-делать-application-code`,children:`Что должен делать application code`}),`
3145
+ `,(0,c.jsx)(t.p,{children:`Ничего из следующего делать не надо:`}),`
3146
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Не нужно и не следует:
3147
+ let csrfToken: string;
3148
+ localStorage.setItem("csrf", csrfToken);
3149
+ headers.set("x-csrf-token", csrfToken);
3150
+ `})}),`
3151
+ `,(0,c.jsxs)(t.p,{children:[`Достаточно использовать metadata-aware mutation helper или `,(0,c.jsx)(t.code,{children:`fetchBase`}),` с write method. Token живёт только во внутреннем memory cache и не сохраняется в localStorage/IndexedDB.`]}),`
3152
+ `,(0,c.jsx)(t.h2,{id:`четыре-разных-уровня-кэширования`,children:`Четыре разных уровня кэширования`}),`
3153
+ `,(0,c.jsx)(t.p,{children:`В OData-сценарии слово «кэш» может означать разные механизмы. Их нельзя смешивать.`}),`
3154
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Уровень`}),(0,c.jsx)(t.th,{children:`Что хранит`}),(0,c.jsx)(t.th,{children:`Identity`}),(0,c.jsx)(t.th,{children:`Кто управляет`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`TanStack Query memory cache`}),(0,c.jsx)(t.td,{children:`Parsed backend snapshot и query state`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`queryKey`})}),(0,c.jsx)(t.td,{children:`QueryClient/application`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Query persistence`}),(0,c.jsx)(t.td,{children:`Разрешённые snapshots между reload`}),(0,c.jsxs)(t.td,{children:[`Тот же `,(0,c.jsx)(t.code,{children:`queryKey`}),` + persister policy`]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`query-client`}),` package`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Cross-tab broadcast`}),(0,c.jsx)(t.td,{children:`События/синхронизация между tabs`}),(0,c.jsx)(t.td,{children:`Query contract`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`query-client`}),` package`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Service Worker runtime cache`}),(0,c.jsx)(t.td,{children:`HTTP Response`}),(0,c.jsx)(t.td,{children:`URL + SW policy`}),(0,c.jsx)(t.td,{children:`Service Worker`})]})]})]}),`
3155
+ `,(0,c.jsx)(t.p,{children:`Browser HTTP cache может существовать дополнительно, но модуль не использует его как application state.`}),`
3156
+ `,(0,c.jsx)(t.h3,{id:`tanstack-query-cache`,children:`TanStack Query cache`}),`
3157
+ `,(0,c.jsx)(t.p,{children:`Это главный владелец server snapshots в React application:`}),`
3158
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const queryKey = ["sales-orders", { companyId }] as const;
3159
+
3160
+ const data = queryClient.getQueryData(queryKey);
3161
+ await queryClient.invalidateQueries({ queryKey });
3162
+ `})}),`
3163
+ `,(0,c.jsx)(t.p,{children:`Правила:`}),`
3164
+ `,(0,c.jsxs)(t.ul,{children:[`
3165
+ `,(0,c.jsx)(t.li,{children:`все inputs, меняющие server result, должны входить в key;`}),`
3166
+ `,(0,c.jsx)(t.li,{children:`query data — backend snapshot, не mutable form draft;`}),`
3167
+ `,(0,c.jsx)(t.li,{children:`не мутируйте object/array на месте;`}),`
3168
+ `,(0,c.jsx)(t.li,{children:`mutation должна явно invalidировать или обновить затронутые keys.`}),`
3169
+ `]}),`
3170
+ `,(0,c.jsx)(t.h3,{id:`query-persistence`,children:`Query persistence`}),`
3171
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`persistedQueryMeta`}),` разрешает инфраструктуре QueryClient сохранять query. Metadata, справочники и список их обновлений используют этот marker.`]}),`
3172
+ `,(0,c.jsx)(t.p,{children:`Persistence не означает вечную актуальность. После восстановления snapshot проверяется version/updates logic.`}),`
3173
+ `,(0,c.jsx)(t.p,{children:`Не сохраняйте auth tokens и SAML payload в query data.`}),`
3174
+ `,(0,c.jsx)(t.h3,{id:`cross-tab-broadcast`,children:`Cross-tab broadcast`}),`
3175
+ `,(0,c.jsxs)(t.p,{children:[`Если project QueryClient настроен на broadcast, изменения cache могут синхронизироваться между вкладками. Сам `,(0,c.jsx)(t.code,{children:`odata`}),` module не создаёт broadcast channel. Он только соблюдает stable query contracts.`]}),`
3176
+ `,(0,c.jsxs)(t.h3,{id:`service-worker-cache-и-x-sw-cache`,children:[`Service Worker cache и `,(0,c.jsx)(t.code,{children:`x-sw-cache`})]}),`
3177
+ `,(0,c.jsx)(t.p,{children:`OData transport только выставляет header:`}),`
3178
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`x-sw-cache: ttl=24h;max=100;name=ui
3179
+ `})}),`
3180
+ `,(0,c.jsx)(t.p,{children:`Интерпретация header принадлежит Service Worker policy проекта. Если Service Worker не установлен или не понимает policy, header сам по себе ничего не кэширует.`}),`
3181
+ `,(0,c.jsx)(t.p,{children:`Примеры:`}),`
3182
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Policy`}),(0,c.jsx)(t.th,{children:`Намерение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`off`})}),(0,c.jsx)(t.td,{children:`Network-only`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ttl=10m`})}),(0,c.jsx)(t.td,{children:`Cache с TTL 10 минут`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ttl=24h;max=200;name=ui`})}),(0,c.jsx)(t.td,{children:`Именованный cache с TTL и лимитом`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ttl=forever;name=ref`})}),(0,c.jsx)(t.td,{children:`Долгоживущий reference cache`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`bust=24h;name=ref`})}),(0,c.jsx)(t.td,{children:`Принудительно обновить запись из network`})]})]})]}),`
3183
+ `,(0,c.jsxs)(t.p,{children:[`Для auth-sensitive или user-specific responses безопасный default — `,(0,c.jsx)(t.code,{children:`off`}),`, если Service Worker policy не доказывает корректное разделение users/sessions. Не применяйте broad shared caching только ради скорости.`]}),`
3184
+ `,(0,c.jsxs)(t.h3,{id:`swcache-в-разных-apis`,children:[(0,c.jsx)(t.code,{children:`swCache`}),` в разных APIs`]}),`
3185
+ `,(0,c.jsx)(t.p,{children:`У metadata-aware helper есть два места, но итог один:`}),`
3186
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataQueryFn({
3187
+ odata,
3188
+ options: {
3189
+ top: 100
3190
+ },
3191
+ swCache: "ttl=5m;name=sales"
3192
+ });
3193
+ `})}),`
3194
+ `,(0,c.jsxs)(t.p,{children:[`У low-level `,(0,c.jsx)(t.code,{children:`odataFetch`}),` policy находится внутри options:`]}),`
3195
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`odataFetch("/ZSALES_SRV/SalesOrderSet", {
3196
+ top: 100,
3197
+ swCache: "ttl=5m;name=sales"
3198
+ });
3199
+ `})}),`
3200
+ `,(0,c.jsxs)(t.p,{children:[`У `,(0,c.jsx)(t.code,{children:`fetchQueryFn`}),` — рядом с `,(0,c.jsx)(t.code,{children:`init`}),` и `,(0,c.jsx)(t.code,{children:`transform`}),`.`]}),`
3201
+ `,(0,c.jsx)(t.h2,{id:`обработка-ошибок`,children:`Обработка ошибок`}),`
3202
+ `,(0,c.jsx)(t.h3,{id:`http-json-error`,children:`HTTP JSON error`}),`
3203
+ `,(0,c.jsx)(t.p,{children:`Transport пытается собрать полезное сообщение из OData/JSON payload в таком порядке:`}),`
3204
+ `,(0,c.jsxs)(t.ul,{children:[`
3205
+ `,(0,c.jsxs)(t.li,{children:[`строковый `,(0,c.jsx)(t.code,{children:`error`}),`;`]}),`
3206
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error.code`}),`;`]}),`
3207
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`error.message.value`}),` или `,(0,c.jsx)(t.code,{children:`error.message`}),`;`]}),`
3208
+ `,(0,c.jsxs)(t.li,{children:[`top-level `,(0,c.jsx)(t.code,{children:`details`}),`;`]}),`
3209
+ `,(0,c.jsxs)(t.li,{children:[`top-level `,(0,c.jsx)(t.code,{children:`message`}),`.`]}),`
3210
+ `]}),`
3211
+ `,(0,c.jsxs)(t.p,{children:[`Повторы удаляются, части соединяются через `,(0,c.jsx)(t.code,{children:`|`}),`.`]}),`
3212
+ `,(0,c.jsx)(t.p,{children:`Пример response:`}),`
3213
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-json`,children:`{
3214
+ "error": {
3215
+ "code": "ZSALES/001",
3216
+ "message": {
3217
+ "value": "Заказ заблокирован"
3218
+ }
3219
+ }
3220
+ }
3221
+ `})}),`
3222
+ `,(0,c.jsx)(t.p,{children:`Error message:`}),`
3223
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`ZSALES/001 | Заказ заблокирован
3224
+ `})}),`
3225
+ `,(0,c.jsx)(t.p,{children:`Если подходящего поля нет, fallback:`}),`
3226
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`HTTP error: 500 Internal Server Error
3227
+ `})}),`
3228
+ `,(0,c.jsx)(t.h3,{id:`plain-text-error`,children:`Plain text error`}),`
3229
+ `,(0,c.jsxs)(t.p,{children:[`Для не-JSON response непустой text становится `,(0,c.jsx)(t.code,{children:`Error.message`}),`. Поэтому UI не должен слепо рендерить HTML через `,(0,c.jsx)(t.code,{children:`dangerouslySetInnerHTML`}),`; transport возвращает обычную строку.`]}),`
3230
+ `,(0,c.jsx)(t.h3,{id:`networkcors`,children:`Network/CORS`}),`
3231
+ `,(0,c.jsxs)(t.p,{children:[`Для response status `,(0,c.jsx)(t.code,{children:`0`}),` формируется сообщение:`]}),`
3232
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Ошибка сети или CORS. Проверьте подключение и настройки сервера.
3233
+ `})}),`
3234
+ `,(0,c.jsxs)(t.p,{children:[`Browser может выбросить `,(0,c.jsx)(t.code,{children:`TypeError`}),` до создания Response. Такой error пробрасывается как есть.`]}),`
3235
+ `,(0,c.jsx)(t.h3,{id:`unexpected-html`,children:`Unexpected HTML`}),`
3236
+ `,(0,c.jsx)(t.p,{children:`Если HTML не содержит SAML form, transport:`}),`
3237
+ `,(0,c.jsxs)(t.ol,{children:[`
3238
+ `,(0,c.jsxs)(t.li,{children:[`Создаёт `,(0,c.jsx)(t.code,{children:`Error("Unexpected HTML response from OData endpoint")`}),`.`]}),`
3239
+ `,(0,c.jsx)(t.li,{children:`Передаёт безопасный summary в error-report infrastructure.`}),`
3240
+ `,(0,c.jsx)(t.li,{children:`Выбрасывает error consumer-у.`}),`
3241
+ `]}),`
3242
+ `,(0,c.jsx)(t.p,{children:`В report попадает:`}),`
3243
+ `,(0,c.jsxs)(t.ul,{children:[`
3244
+ `,(0,c.jsx)(t.li,{children:`request/response URL без query string;`}),`
3245
+ `,(0,c.jsx)(t.li,{children:`method, base URL type, SAP client;`}),`
3246
+ `,(0,c.jsx)(t.li,{children:`status и status text;`}),`
3247
+ `,(0,c.jsx)(t.li,{children:`content type, redirect flag, response type;`}),`
3248
+ `,(0,c.jsx)(t.li,{children:`длина HTML;`}),`
3249
+ `,(0,c.jsx)(t.li,{children:`title не длиннее 200 symbols;`}),`
3250
+ `,(0,c.jsx)(t.li,{children:`количество forms;`}),`
3251
+ `,(0,c.jsx)(t.li,{children:`до 40 уникальных input names;`}),`
3252
+ `,(0,c.jsx)(t.li,{children:`text preview не длиннее 1000 symbols.`}),`
3253
+ `]}),`
3254
+ `,(0,c.jsx)(t.p,{children:`В report не передаются:`}),`
3255
+ `,(0,c.jsxs)(t.ul,{children:[`
3256
+ `,(0,c.jsx)(t.li,{children:`полный raw HTML;`}),`
3257
+ `,(0,c.jsx)(t.li,{children:`query string URL;`}),`
3258
+ `,(0,c.jsx)(t.li,{children:`значения hidden inputs;`}),`
3259
+ `,(0,c.jsx)(t.li,{children:`SAMLRequest/SAMLResponse values.`}),`
3260
+ `]}),`
3261
+ `,(0,c.jsx)(t.p,{children:`Это снижает риск утечки credentials/tokens в diagnostic channel.`}),`
3262
+ `,(0,c.jsx)(t.h3,{id:`unsupported-body`,children:`Unsupported body`}),`
3263
+ `,(0,c.jsxs)(t.p,{children:[`Если Content-Type не JSON, body не HTML и `,(0,c.jsx)(t.code,{children:`JSON.parse(text)`}),` не удался:`]}),`
3264
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Unsupported response content type and body is not JSON
3265
+ `})}),`
3266
+ `,(0,c.jsx)(t.h3,{id:`ошибка-metadata-aware-validation`,children:`Ошибка metadata-aware validation`}),`
3267
+ `,(0,c.jsx)(t.p,{children:`До network request helper может сообщить:`}),`
3268
+ `,(0,c.jsxs)(t.ul,{children:[`
3269
+ `,(0,c.jsx)(t.li,{children:`target не найден;`}),`
3270
+ `,(0,c.jsx)(t.li,{children:`Entity использована как FunctionImport или наоборот;`}),`
3271
+ `,(0,c.jsx)(t.li,{children:`operation несовместима с target;`}),`
3272
+ `,(0,c.jsx)(t.li,{children:`FunctionImport не содержит HTTP method;`}),`
3273
+ `,(0,c.jsx)(t.li,{children:`отсутствуют обязательные params;`}),`
3274
+ `,(0,c.jsx)(t.li,{children:`plain read пытается использовать parameterized Entity;`}),`
3275
+ `,(0,c.jsx)(t.li,{children:`body имеет неправильную форму.`}),`
3276
+ `]}),`
3277
+ `,(0,c.jsx)(t.p,{children:`Такая ошибка обычно означает проблему application contract, а не временный network failure. Не запускайте бессмысленные automatic retries для неё.`}),`
3278
+ `,(0,c.jsx)(t.h3,{id:`error-handling-в-react`,children:`Error handling в React`}),`
3279
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`function SalesOrders() {
3280
+ const query = useQuery(salesOrdersQueryOptions({ companyId: "1000" }));
3281
+
3282
+ if (query.isPending) return <Spinner />;
3283
+
3284
+ if (query.isError) {
3285
+ return (
3286
+ <ErrorPanel
3287
+ title="Не удалось загрузить заказы"
3288
+ description={query.error.message}
3289
+ onRetry={() => query.refetch()}
3290
+ />
3291
+ );
3292
+ }
3293
+
3294
+ return <SalesOrderTable rows={query.data} />;
3295
+ }
3296
+ `})}),`
3297
+ `,(0,c.jsx)(t.p,{children:`Для mutation показывайте feedback на уровне пользовательского действия и не очищайте draft при ошибке.`}),`
3298
+ `,(0,c.jsx)(t.h2,{id:`abort-и-concurrency`,children:`Abort и concurrency`}),`
3299
+ `,(0,c.jsx)(t.h3,{id:`query-cancellation`,children:`Query cancellation`}),`
3300
+ `,(0,c.jsxs)(t.p,{children:[`TanStack Query передаёт `,(0,c.jsx)(t.code,{children:`signal`}),` в query function. Передайте его metadata-aware runner:`]}),`
3301
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`queryFn: ({ signal, client }) =>
3302
+ odataQueryFn<RawRow>({ odata })({ client, signal })
3303
+ `})}),`
3304
+ `,(0,c.jsx)(t.p,{children:`Если request отменён, browser fetch завершится AbortError. Это не обязательно application error; TanStack Query умеет корректно учитывать cancellation.`}),`
3305
+ `,(0,c.jsx)(t.h3,{id:`caller-provided-signal`,children:`Caller-provided signal`}),`
3306
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`fetchQueryFn`}),` добавляет query signal только когда `,(0,c.jsx)(t.code,{children:`init.signal`}),` не задан. Явный caller signal имеет приоритет.`]}),`
3307
+ `,(0,c.jsxs)(t.p,{children:[`Каждый запуск получает новый clone `,(0,c.jsx)(t.code,{children:`init`}),`, поэтому отменённый signal предыдущего refetch не «заражает» следующий.`]}),`
3308
+ `,(0,c.jsx)(t.h3,{id:`mutation-cancellation`,children:`Mutation cancellation`}),`
3309
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMutation`}),` не передаёт AbortSignal автоматически как query. Если сценарий требует cancel, создайте `,(0,c.jsx)(t.code,{children:`AbortController`}),` на уровне feature и передайте `,(0,c.jsx)(t.code,{children:`controller.signal`}),` runner-у.`]}),`
3310
+ `,(0,c.jsx)(t.p,{children:`Не переиспользуйте один уже aborted controller для следующей mutation.`}),`
3311
+ `,(0,c.jsx)(t.h2,{id:`security-checklist`,children:`Security checklist`}),`
3312
+ `,(0,c.jsxs)(t.ul,{children:[`
3313
+ `,(0,c.jsxs)(t.li,{children:[`Используйте только public import `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-api/odata`}),`.`]}),`
3314
+ `,(0,c.jsx)(t.li,{children:`Не сохраняйте CSRF/SAML values в localStorage, query cache или logs.`}),`
3315
+ `,(0,c.jsx)(t.li,{children:`Не вставляйте server HTML в DOM.`}),`
3316
+ `,(0,c.jsx)(t.li,{children:`Не передавайте user input в raw OData URL; стройте filters/params через typed helpers lib-пакета.`}),`
3317
+ `,(0,c.jsx)(t.li,{children:`Не считайте TypeScript generic runtime validation.`}),`
3318
+ `,(0,c.jsx)(t.li,{children:`Не включайте SW cache для user-specific данных без явной cache policy.`}),`
3319
+ `,(0,c.jsx)(t.li,{children:`Не собирайте metadata/function path строковой конкатенацией.`}),`
3320
+ `,(0,c.jsxs)(t.li,{children:[`Не отключайте `,(0,c.jsx)(t.code,{children:`credentials: "include"`}),` для SAP request: transport управляет cookies централизованно.`]}),`
3321
+ `,(0,c.jsx)(t.li,{children:`Не отправляйте mutation через GET только ради обхода CSRF; HTTP method определяется service metadata/operation contract.`}),`
3322
+ `,(0,c.jsxs)(t.li,{children:[`Не выводите `,(0,c.jsx)(t.code,{children:`body`}),` и params с secrets при использовании Dev helpers.`]}),`
3323
+ `]}),`
3324
+ `,(0,c.jsx)(t.h2,{id:`deprecated-api`,children:`Deprecated API`}),`
3325
+ `,(0,c.jsx)(t.p,{children:`Следующие helpers остаются public, но новый код на них строить не следует:`}),`
3326
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`API`}),(0,c.jsx)(t.th,{children:`Почему`}),(0,c.jsx)(t.th,{children:`Замена`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchJsonQueryFn`})}),(0,c.jsx)(t.td,{children:`Не использует metadata-aware target contract`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`odataQueryFn`}),` или `,(0,c.jsx)(t.code,{children:`fetchQueryFn`}),` для не-OData response`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchJsonMutationFn`})}),(0,c.jsx)(t.td,{children:`Raw URL и слабый operation contract`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`odataCreateFn`}),`, `,(0,c.jsx)(t.code,{children:`odataUpdateFn`}),`, `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchDeleteFn`})}),(0,c.jsx)(t.td,{children:`Raw URL, не валидирует Entity keys`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDeleteFn`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`sortODataDependentServicesByChains`})}),(0,c.jsx)(t.td,{children:`Старая service-level сортировка`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`flattenODataDependentServices`}),` + `,(0,c.jsx)(t.code,{children:`sortODataDependentSegmentItemsByChains`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollection(...).refetch`})}),(0,c.jsx)(t.td,{children:`Imperative method старой UI model`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`QueryClient`}),` + key factory`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollection(...).invalidate`})}),(0,c.jsx)(t.td,{children:`Imperative method старой UI model`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`QueryClient.invalidateQueries`})})]})]})]}),`
3327
+ `,(0,c.jsx)(t.p,{children:`Deprecated не означает «сломано сегодня». Это означает, что API может быть удалён и не должен распространяться на новые modules.`}),`
3328
+ `,(0,c.jsx)(t.h3,{id:`legacy-examples-только-для-миграции`,children:`Legacy examples только для миграции`}),`
3329
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Было
3330
+ const load = fetchJsonQueryFn<RawOrder[]>(
3331
+ "/ZSALES_SRV/SalesOrderSet"
3332
+ );
3333
+
3334
+ // Стало
3335
+ const load = odataQueryFn<RawOrder>({
3336
+ odata: {
3337
+ service: "ZSALES_SRV",
3338
+ target: "SalesOrderSet"
3339
+ }
3340
+ });
3341
+ `})}),`
3342
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Было
3343
+ const remove = fetchDeleteFn(
3344
+ "/ZSALES_SRV/SalesOrderSet('50000123')"
3345
+ );
3346
+
3347
+ // Стало
3348
+ const remove = odataDeleteFn({
3349
+ odata: {
3350
+ service: "ZSALES_SRV",
3351
+ target: "SalesOrderSet"
3352
+ },
3353
+ params: wrapODataParams({
3354
+ SalesOrderId: "50000123"
3355
+ })
3356
+ });
3357
+ `})}),`
3358
+ `,(0,c.jsx)(t.h2,{id:`тестирование`,children:`Тестирование`}),`
3359
+ `,(0,c.jsx)(t.h3,{id:`что-тестировать-в-application-module`,children:`Что тестировать в application module`}),`
3360
+ `,(0,c.jsx)(t.p,{children:`Не нужно повторно тестировать всю foundation implementation. Проверяйте свой contract:`}),`
3361
+ `,(0,c.jsxs)(t.ul,{children:[`
3362
+ `,(0,c.jsx)(t.li,{children:`правильные service/target;`}),`
3363
+ `,(0,c.jsx)(t.li,{children:`query key включает все identity inputs;`}),`
3364
+ `,(0,c.jsx)(t.li,{children:`params правильно собраны из feature inputs;`}),`
3365
+ `,(0,c.jsx)(t.li,{children:`transform создаёт ожидаемую domain model;`}),`
3366
+ `,(0,c.jsx)(t.li,{children:`mutation обновляет/инвалидирует нужные cache keys;`}),`
3367
+ `,(0,c.jsx)(t.li,{children:`UI различает loading, empty и error;`}),`
3368
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`enabled`}),` запрещает неполный request.`]}),`
3369
+ `]}),`
3370
+ `,(0,c.jsx)(t.h3,{id:`unit-test-mapper`,children:`Unit test mapper`}),`
3371
+ `,(0,c.jsx)(t.p,{children:`Mapper проще и надёжнее тестировать отдельно от network:`}),`
3372
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { describe, expect, it } from "vitest";
3373
+
3374
+ describe("mapSalesOrder", () => {
3375
+ it("создаёт domain model из raw OData row", () => {
3376
+ expect(
3377
+ mapSalesOrder({
3378
+ SalesOrderId: "50000123",
3379
+ NetAmount: "1250.50",
3380
+ CreatedAt: "/Date(1787280000000)/",
3381
+ IsBlocked: "X"
3382
+ })
3383
+ ).toEqual({
3384
+ id: "50000123",
3385
+ amount: 1250.5,
3386
+ createdAt: new Date(1787280000000),
3387
+ blocked: true
3388
+ });
3389
+ });
3390
+ });
3391
+ `})}),`
3392
+ `,(0,c.jsxs)(t.p,{children:[`Если mapping рассчитан на `,(0,c.jsx)(t.code,{children:`autoParse: true`}),`, test input должен соответствовать уже parsed runtime values.`]}),`
3393
+ `,(0,c.jsx)(t.h3,{id:`unit-test-query-options`,children:`Unit test query options`}),`
3394
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { QueryClient } from "@tanstack/react-query";
3395
+ import { describe, expect, it, vi } from "vitest";
3396
+
3397
+ describe("salesOrdersQueryOptions", () => {
3398
+ it("включает companyId в identity", () => {
3399
+ const first = salesOrdersQueryOptions({ companyId: "1000" });
3400
+ const second = salesOrdersQueryOptions({ companyId: "2000" });
3401
+
3402
+ expect(first.queryKey).not.toEqual(second.queryKey);
3403
+ });
3404
+
3405
+ it("не запускается без обязательного input", () => {
3406
+ const options = salesOrdersQueryOptions({ companyId: "" });
3407
+ expect(options.enabled).toBe(false);
3408
+ });
3409
+ });
3410
+ `})}),`
3411
+ `,(0,c.jsx)(t.h3,{id:`transport-test-без-metadata`,children:`Transport test без metadata`}),`
3412
+ `,(0,c.jsxs)(t.p,{children:[`Для low-level transport можно подменить `,(0,c.jsx)(t.code,{children:`fetch`}),`:`]}),`
3413
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { afterEach, describe, expect, it, vi } from "vitest";
3414
+ import { fetchODataJson } from "@ryuzaki13/react-foundation-api/odata";
3415
+
3416
+ afterEach(() => {
3417
+ vi.unstubAllGlobals();
3418
+ });
3419
+
3420
+ describe("orders transport", () => {
3421
+ it("разворачивает OData V2 collection envelope", async () => {
3422
+ vi.stubGlobal(
3423
+ "fetch",
3424
+ vi.fn().mockResolvedValue(
3425
+ new Response(
3426
+ JSON.stringify({
3427
+ d: {
3428
+ results: [
3429
+ {
3430
+ __metadata: { uri: "..." },
3431
+ SalesOrderId: "50000123"
3432
+ }
3433
+ ],
3434
+ __count: "1"
3435
+ }
3436
+ }),
3437
+ {
3438
+ status: 200,
3439
+ headers: { "Content-Type": "application/json" }
3440
+ }
3441
+ )
3442
+ )
3443
+ );
3444
+
3445
+ const result = await fetchODataJson<
3446
+ Array<{ SalesOrderId: string }>
3447
+ >("/ZSALES_SRV/SalesOrderSet");
3448
+
3449
+ expect(result).toEqual({
3450
+ data: [{ SalesOrderId: "50000123" }],
3451
+ totalCount: 1
3452
+ });
3453
+ });
3454
+ });
3455
+ `})}),`
3456
+ `,(0,c.jsxs)(t.p,{children:[`В project test environment учтите compile-time constants package (`,(0,c.jsx)(t.code,{children:`__DEV__`}),`, `,(0,c.jsx)(t.code,{children:`__SAP_CLIENT__`}),` и другие), которые обычно подставляет build config.`]}),`
3457
+ `,(0,c.jsx)(t.h3,{id:`mutation-cache-test`,children:`Mutation cache test`}),`
3458
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { QueryClient } from "@tanstack/react-query";
3459
+ import { describe, expect, it, vi } from "vitest";
3460
+
3461
+ it("после create инвалидирует список", async () => {
3462
+ const queryClient = new QueryClient();
3463
+ const invalidate = vi.spyOn(queryClient, "invalidateQueries");
3464
+ const listKey = ["sales-orders", "list", { companyId: "1000" }] as const;
3465
+
3466
+ // В integration test здесь вызывается mutation success handler.
3467
+ await queryClient.invalidateQueries({ queryKey: listKey });
3468
+
3469
+ expect(invalidate).toHaveBeenCalledWith({ queryKey: listKey });
3470
+ });
3471
+ `})}),`
3472
+ `,(0,c.jsx)(t.h3,{id:`ssocsrf-tests`,children:`SSO/CSRF tests`}),`
3473
+ `,(0,c.jsx)(t.p,{children:`В application tests обычно достаточно проверить пользовательский fallback. Детальный transport contract уже тестируется в package. Если всё же нужен integration test:`}),`
3474
+ `,(0,c.jsxs)(t.ul,{children:[`
3475
+ `,(0,c.jsx)(t.li,{children:`мокайте redirect/HTML form и subsequent retry;`}),`
3476
+ `,(0,c.jsx)(t.li,{children:`никогда не используйте real SAML payload;`}),`
3477
+ `,(0,c.jsx)(t.li,{children:`проверяйте, что concurrent writes не создают несколько token requests;`}),`
3478
+ `,(0,c.jsx)(t.li,{children:`проверяйте один retry после 401/403;`}),`
3479
+ `,(0,c.jsx)(t.li,{children:`очищайте DOM и global mocks после test.`}),`
3480
+ `]}),`
3481
+ `,(0,c.jsx)(t.h2,{id:`полный-public-api`,children:`Полный public API`}),`
3482
+ `,(0,c.jsx)(t.p,{children:`Ниже перечислены exports package subpath. Это карта поиска; подробное поведение основных функций описано выше.`}),`
3483
+ `,(0,c.jsx)(t.h3,{id:`metadata-aware-operations`,children:`Metadata-aware operations`}),`
3484
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataQueryFn`})}),(0,c.jsx)(t.td,{children:`GET collection/plain или parameterized Entity`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataReadFn`})}),(0,c.jsx)(t.td,{children:`GET одной Entity по keys`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataCreateFn`})}),(0,c.jsx)(t.td,{children:`POST новой Entity`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataUpdateFn`})}),(0,c.jsx)(t.td,{children:`PUT Entity по keys`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDeleteFn`})}),(0,c.jsx)(t.td,{children:`DELETE Entity по keys`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFunctionImportFn`})}),(0,c.jsx)(t.td,{children:`FunctionImport с method из metadata`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataFetchFnRequest`})}),(0,c.jsx)(t.td,{children:`Conditional type options по semantic operation`})]})]})]}),`
3485
+ `,(0,c.jsx)(t.h3,{id:`dev-operation-helpers`,children:`DEV operation helpers`}),`
3486
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataQueryFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run query в dev, real query в production`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataReadFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run read в dev`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataCreateFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run create в dev`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataUpdateFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run update в dev`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataDeleteFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run delete в dev`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFunctionImportFnDev`})}),(0,c.jsx)(t.td,{children:`Dry-run FunctionImport в dev`})]})]})]}),`
3487
+ `,(0,c.jsx)(t.h3,{id:`low-level-requests`,children:`Low-level requests`}),`
3488
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataFetch`})}),(0,c.jsx)(t.td,{children:`Собрать OData query options и выполнить OData V2 JSON request`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataFetchOptions`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`$filter`}),`, `,(0,c.jsx)(t.code,{children:`$orderby`}),`, `,(0,c.jsx)(t.code,{children:`$select`}),`, `,(0,c.jsx)(t.code,{children:`$expand`}),`, paging и SW policy`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchBase`})}),(0,c.jsxs)(t.td,{children:[`Базовый SAP-aware `,(0,c.jsx)(t.code,{children:`Response`}),` transport`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchODataJson`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`fetchBase`}),` + OData V2 envelope parsing`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchJson`})}),(0,c.jsxs)(t.td,{children:[`Вернуть только `,(0,c.jsx)(t.code,{children:`data`}),` из `,(0,c.jsx)(t.code,{children:`fetchODataJson`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchQueryFn`})}),(0,c.jsxs)(t.td,{children:[`Query function для произвольного `,(0,c.jsx)(t.code,{children:`Response`}),` transform`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`isNoContentResponse`})}),(0,c.jsx)(t.td,{children:`Runtime guard marker-а 204/205`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchJsonQueryFn`})}),(0,c.jsx)(t.td,{children:`Deprecated raw JSON query factory`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchJsonMutationFn`})}),(0,c.jsx)(t.td,{children:`Deprecated POST/PUT JSON factory`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchDeleteFn`})}),(0,c.jsx)(t.td,{children:`Deprecated DELETE factory`})]})]})]}),`
3489
+ `,(0,c.jsx)(t.h3,{id:`project-adapter-и-url`,children:`Project adapter и URL`}),`
3490
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`configureODataProjectAdapter`})}),(0,c.jsx)(t.td,{children:`Полностью заменить global adapter`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getODataProjectAdapter`})}),(0,c.jsx)(t.td,{children:`Прочитать текущий adapter`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataProjectAdapter`})}),(0,c.jsx)(t.td,{children:`Тип adapter`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataProjectTechnicalEndpoint`})}),(0,c.jsx)(t.td,{children:`Тип metadata/collection technical endpoint`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`normalizeODataServiceName`})}),(0,c.jsxs)(t.td,{children:[`Убрать suffix после `,(0,c.jsx)(t.code,{children:`;`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveODataBaseUrl`})}),(0,c.jsx)(t.td,{children:`Выбрать base URL с учётом dev DP0 service`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveODataSapClient`})}),(0,c.jsx)(t.td,{children:`Получить SAP client из adapter`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseURLType`})}),(0,c.jsx)(t.td,{children:`Union доступных base URL ids`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseUrlMap`})}),(0,c.jsx)(t.td,{children:`Readonly map id → URL prefix`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseODataURL`})}),(0,c.jsx)(t.td,{children:`Standard SAP OData prefix`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseODataDp0URL`})}),(0,c.jsx)(t.td,{children:`DP0 prefix в dev/preview, standard в production`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseODataUI2URL`})}),(0,c.jsx)(t.td,{children:`SAP UI2 prefix`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BaseAppConfigURL`})}),(0,c.jsx)(t.td,{children:`Build-time app config prefix`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getInputUrl`})}),(0,c.jsxs)(t.td,{children:[`Получить string URL из `,(0,c.jsx)(t.code,{children:`RequestInfo`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`normalizeRelativePath`})}),(0,c.jsx)(t.td,{children:`Удалить query/hash и гарантировать leading slash`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`RequestInitType`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`RequestInit`}),` + internal retry markers`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`FetchErrorReportContext`})}),(0,c.jsx)(t.td,{children:`Контекст безопасного transport report`})]})]})]}),`
3491
+ `,(0,c.jsx)(t.h3,{id:`sso`,children:`SSO`}),`
3492
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`SsoRequiredError`})}),(0,c.jsx)(t.td,{children:`Специализированная authentication error`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`SsoForm`})}),(0,c.jsx)(t.td,{children:`Action/method/hidden inputs contract`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`SsoRecoveryArgs`})}),(0,c.jsx)(t.td,{children:`Аргументы recovery`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`SsoRequiredErrorOptions`})}),(0,c.jsxs)(t.td,{children:[`Options error, включая `,(0,c.jsx)(t.code,{children:`recoverable`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`isSsoForm`})}),(0,c.jsx)(t.td,{children:`Guard для SAMLRequest/SAMLResponse form`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`recoverSsoSession`})}),(0,c.jsx)(t.td,{children:`Controlled hidden iframe recovery`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`submitSsoForm`})}),(0,c.jsx)(t.td,{children:`Imperative submit hidden SSO form`})]})]})]}),`
3493
+ `,(0,c.jsx)(t.h3,{id:`metadata-1`,children:`Metadata`}),`
3494
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchMetadata`})}),(0,c.jsxs)(t.td,{children:[`Создать query function загрузки `,(0,c.jsx)(t.code,{children:`$metadata`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataMetadataQueryOptions`})}),(0,c.jsx)(t.td,{children:`Stable metadata query options`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataMetadataQuery`})}),(0,c.jsx)(t.td,{children:`Metadata query + version synchronization`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataMetadataVersionQuery`})}),(0,c.jsx)(t.td,{children:`Отдельная диагностика server version`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`applyODataMetadataVersion`})}),(0,c.jsx)(t.td,{children:`Сравнить version и точечно invalidировать metadata`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getODataMetadataData`})}),(0,c.jsx)(t.td,{children:`Императивно получить актуальную metadata через QueryClient`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataMetadata`})}),(0,c.jsx)(t.td,{children:`Metadata одной Entity`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataEntity`})}),(0,c.jsx)(t.td,{children:`Entity metadata + path/columns/code-text actions`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataEntity`})}),(0,c.jsx)(t.td,{children:`Тип готовой Entity model без loading flag`})]})]})]}),`
3495
+ `,(0,c.jsx)(t.h3,{id:`collections`,children:`Collections`}),`
3496
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollectionQuery`})}),(0,c.jsx)(t.td,{children:`Загрузить и подготовить reference collection`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionResult`})}),(0,c.jsx)(t.td,{children:`Prepared collection snapshot`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataServiceCollectionConfig`})}),(0,c.jsx)(t.td,{children:`Identity/server options справочника`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionConfig`})}),(0,c.jsx)(t.td,{children:`Полный config query/UI справочника`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`createODataCollectionBaseQueryKey`})}),(0,c.jsx)(t.td,{children:`Prefix всех collection queries`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`createODataCollectionQueryKey`})}),(0,c.jsx)(t.td,{children:`Exact stable collection key`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollectionUpdatesQuery`})}),(0,c.jsx)(t.td,{children:`Наблюдать технический список обновлений`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`odataCollectionUpdatesQueryOptions`})}),(0,c.jsx)(t.td,{children:`Query options списка обновлений`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`fetchODataCollectionUpdates`})}),(0,c.jsx)(t.td,{children:`Query function списка обновлений`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`createODataCollectionUpdatesQueryKey`})}),(0,c.jsx)(t.td,{children:`Stable updates key`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionUpdatesOptions`})}),(0,c.jsxs)(t.td,{children:[`Настройка `,(0,c.jsx)(t.code,{children:`staleTime`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionUpdatesResult`})}),(0,c.jsx)(t.td,{children:`Prepared updates snapshot`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionUpdateItem`})}),(0,c.jsx)(t.td,{children:`Одна Entity и время изменения`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`shouldRefreshODataCollectionCache`})}),(0,c.jsx)(t.td,{children:`Проверить возраст snapshot`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveODataCollectionSwCachePolicy`})}),(0,c.jsx)(t.td,{children:`Превратить TTL в bust policy при обновлении`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODATA_COLLECTION_UPDATES_LOOKBACK_MS`})}),(0,c.jsx)(t.td,{children:`Окно updates endpoint — 7 дней`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODATA_COLLECTION_DEFAULT_SW_CACHE`})}),(0,c.jsx)(t.td,{children:`Default long-lived collection policy`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODATA_COLLECTION_BUST_SW_CACHE`})}),(0,c.jsx)(t.td,{children:`Default forced refresh policy`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODATA_COLLECTION_UPDATES_SW_CACHE`})}),(0,c.jsx)(t.td,{children:`Updates endpoint SW policy`})]})]})]}),`
3497
+ `,(0,c.jsx)(t.h3,{id:`collection-ui-model-и-props-types`,children:`Collection UI model и props types`}),`
3498
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollectionModel`})}),(0,c.jsx)(t.td,{children:`Зафиксировать model и подставить defaults`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollection`})}),(0,c.jsx)(t.td,{children:`Search/filter/index/paging actions для UI`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionModel`})}),(0,c.jsx)(t.td,{children:`Search, limits и active code key`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionSegment`})}),(0,c.jsx)(t.td,{children:`Placeholder/hide/select-text contract`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataCollectionProps`})}),(0,c.jsx)(t.td,{children:`Multi-value и dependencies props`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataSelectBaseProps`})}),(0,c.jsx)(t.td,{children:`Общие props collection select`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataSingleSelectProps`})}),(0,c.jsx)(t.td,{children:`Single-value вариант props`})]})]})]}),`
3499
+ `,(0,c.jsx)(t.h3,{id:`dependent-segments`,children:`Dependent segments`}),`
3500
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ControlPanelVisibility`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`user`}),`, `,(0,c.jsx)(t.code,{children:`always`}),` или `,(0,c.jsx)(t.code,{children:`none`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataDependentSegment`})}),(0,c.jsx)(t.td,{children:`Segment + panel visibility`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataDependentSegments`})}),(0,c.jsx)(t.td,{children:`Record segments по id`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataDependentBaseProps`})}),(0,c.jsx)(t.td,{children:`Service config с несколькими segments`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ODataDependentSegmentItem`})}),(0,c.jsx)(t.td,{children:`Нормализованный flat segment`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`flattenODataDependentServices`})}),(0,c.jsx)(t.td,{children:`Service groups → flat segments`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataCollectionChains`})}),(0,c.jsx)(t.td,{children:`Chains из Query cache`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`sortODataDependentSegmentItemsByChains`})}),(0,c.jsx)(t.td,{children:`Stable segment-level ordering`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`sortODataDependentServicesByChains`})}),(0,c.jsx)(t.td,{children:`Deprecated service-level ordering`})]})]})]}),`
3501
+ `,(0,c.jsx)(t.h3,{id:`table-columns`,children:`Table columns`}),`
3502
+ `,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useODataTableColumns`})}),(0,c.jsx)(t.td,{children:`Построить/обогатить TanStack columns по metadata`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`UseODataTableColumnsConfig`})}),(0,c.jsx)(t.td,{children:`Union build/enrich config`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`UseODataTableColumnsBuildConfig`})}),(0,c.jsx)(t.td,{children:`Config генерации columns`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`UseODataTableColumnsEnrichConfig`})}),(0,c.jsx)(t.td,{children:`Config обогащения existing columns`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`UseODataTableColumnsResult`})}),(0,c.jsx)(t.td,{children:`Columns, visibility, metadata, loading`})]})]})]}),`
3503
+ `,(0,c.jsx)(t.h2,{id:`частые-ошибки`,children:`Частые ошибки`}),`
3504
+ `,(0,c.jsxs)(t.h3,{id:`ошибка-перепутаны-params-и-options`,children:[`Ошибка: перепутаны `,(0,c.jsx)(t.code,{children:`params`}),` и `,(0,c.jsx)(t.code,{children:`options`})]}),`
3505
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Неверная идея: key Entity положен в $filter.
3506
+ odataReadFn({
3507
+ odata,
3508
+ params: wrapODataParams({}),
3509
+ options: {
3510
+ expression: createFilterEqual("SalesOrderId", "50000123")
3511
+ }
3512
+ });
3513
+ `})}),`
3514
+ `,(0,c.jsxs)(t.p,{children:[`Для read key должен быть в `,(0,c.jsx)(t.code,{children:`params`}),`. `,(0,c.jsx)(t.code,{children:`options.expression`}),` — `,(0,c.jsx)(t.code,{children:`$filter`}),` списка.`]}),`
3515
+ `,(0,c.jsx)(t.h3,{id:`ошибка-functionimport-вызван-через-create`,children:`Ошибка: FunctionImport вызван через create`}),`
3516
+ `,(0,c.jsxs)(t.p,{children:[`Business verb «создать» не делает target Entity. Если metadata объявляет FunctionImport, используйте `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`, даже если action бизнесово создаёт объект.`]}),`
3517
+ `,(0,c.jsx)(t.h3,{id:`ошибка-query-key-не-содержит-filter`,children:`Ошибка: query key не содержит filter`}),`
3518
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Плохо: все companyId используют один slot.
3519
+ queryKey: ["sales-orders"]
3520
+ `})}),`
3521
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Хорошо.
3522
+ queryKey: ["sales-orders", { companyId }]
3523
+ `})}),`
3524
+ `,(0,c.jsx)(t.h3,{id:`ошибка-cache-data-используется-как-form-draft`,children:`Ошибка: cache data используется как form draft`}),`
3525
+ `,(0,c.jsxs)(t.p,{children:[`Не редактируйте object из `,(0,c.jsx)(t.code,{children:`useQuery().data`}),` напрямую. Создайте отдельный draft state/form model. После save обновите или инвалидируйте cache.`]}),`
3526
+ `,(0,c.jsxs)(t.h3,{id:`ошибка-autoparse-принят-за-schema-validation`,children:[`Ошибка: `,(0,c.jsx)(t.code,{children:`autoParse`}),` принят за schema validation`]}),`
3527
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`autoParse`}),` преобразует известные top-level primitive fields по metadata. Он не доказывает, что обязательные поля существуют и business invariants соблюдены.`]}),`
3528
+ `,(0,c.jsxs)(t.h3,{id:`ошибка-sortbycode-добавили-в-collection-key-вручную`,children:[`Ошибка: `,(0,c.jsx)(t.code,{children:`sortByCode`}),` добавили в collection key вручную`]}),`
3529
+ `,(0,c.jsx)(t.p,{children:`Sort order — observer projection, не server identity. Используйте public key factory и hook option.`}),`
3530
+ `,(0,c.jsxs)(t.h3,{id:`ошибка-разные-clientfilter-используют-один-collection-key`,children:[`Ошибка: разные `,(0,c.jsx)(t.code,{children:`clientFilter`}),` используют один collection key`]}),`
3531
+ `,(0,c.jsx)(t.p,{children:`Это может привести к конкурирующим snapshots. Вынесите различие в observer/component projection.`}),`
3532
+ `,(0,c.jsx)(t.h3,{id:`ошибка-sw-cache-считается-query-cache`,children:`Ошибка: SW cache считается Query cache`}),`
3533
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`x-sw-cache`}),` не обновляет `,(0,c.jsx)(t.code,{children:`useQuery().data`}),` сам. После mutation всё равно нужна QueryClient cache strategy.`]}),`
3534
+ `,(0,c.jsx)(t.h3,{id:`ошибка-dev-helper-используется-как-production-mock`,children:`Ошибка: Dev helper используется как production mock`}),`
3535
+ `,(0,c.jsxs)(t.p,{children:[`В production `,(0,c.jsx)(t.code,{children:`*Dev`}),` выполняет реальный request. Для mock подменяйте network/runner в test layer.`]}),`
3536
+ `,(0,c.jsx)(t.h3,{id:`ошибка-raw-deep-import`,children:`Ошибка: raw deep import`}),`
3537
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Запрещено: private implementation path.
3538
+ import { odataFetch } from "@ryuzaki13/react-foundation-api/src/odata/odataFetch";
3539
+ `})}),`
3540
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Правильно.
3541
+ import { odataFetch } from "@ryuzaki13/react-foundation-api/odata";
3542
+ `})}),`
3543
+ `,(0,c.jsx)(t.h2,{id:`faq`,children:`FAQ`}),`
3544
+ `,(0,c.jsx)(t.h3,{id:`какой-helper-выбирать-для-обычного-списка`,children:`Какой helper выбирать для обычного списка?`}),`
3545
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataQueryFn`}),`. Если это code/text справочник для UI — `,(0,c.jsx)(t.code,{children:`useODataCollectionQuery`}),`.`]}),`
3546
+ `,(0,c.jsx)(t.h3,{id:`как-прочитать-одну-запись`,children:`Как прочитать одну запись?`}),`
3547
+ `,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`odataReadFn`}),` и keys в `,(0,c.jsx)(t.code,{children:`params`}),`.`]}),`
3548
+ `,(0,c.jsx)(t.h3,{id:`как-вызвать-sap-action`,children:`Как вызвать SAP action?`}),`
3549
+ `,(0,c.jsxs)(t.p,{children:[`Сначала посмотрите metadata. Если target — FunctionImport, используйте `,(0,c.jsx)(t.code,{children:`odataFunctionImportFn`}),`; method будет взят из metadata.`]}),`
3550
+ `,(0,c.jsxs)(t.h3,{id:`нужно-ли-сначала-вручную-загружать-metadata`,children:[`Нужно ли сначала вручную загружать `,(0,c.jsx)(t.code,{children:`$metadata`}),`?`]}),`
3551
+ `,(0,c.jsx)(t.p,{children:`Нет. Metadata-aware helpers делают это через переданный QueryClient.`}),`
3552
+ `,(0,c.jsxs)(t.h3,{id:`почему-runner-требует-client`,children:[`Почему runner требует `,(0,c.jsx)(t.code,{children:`client`}),`?`]}),`
3553
+ `,(0,c.jsx)(t.p,{children:`Metadata является общим server snapshot. QueryClient обеспечивает reuse, deduplication, persistence и version invalidation.`}),`
3554
+ `,(0,c.jsxs)(t.h3,{id:`почему-result-helper-а-содержит--data--а-не-только-data`,children:[`Почему result helper-а содержит `,(0,c.jsx)(t.code,{children:`{ data }`}),`, а не только data?`]}),`
3555
+ `,(0,c.jsxs)(t.p,{children:[`Envelope сохраняет `,(0,c.jsx)(t.code,{children:`totalCount`}),` и одинаково подходит query/mutation integrations.`]}),`
3556
+ `,(0,c.jsxs)(t.h3,{id:`где-находится-totalcount`,children:[`Где находится `,(0,c.jsx)(t.code,{children:`totalCount`}),`?`]}),`
3557
+ `,(0,c.jsxs)(t.p,{children:[`В `,(0,c.jsx)(t.code,{children:`result.totalCount`}),`, если server прислал OData V2 `,(0,c.jsx)(t.code,{children:`d.__count`}),`, обычно при `,(0,c.jsx)(t.code,{children:`$inlinecount`}),`.`]}),`
3558
+ `,(0,c.jsx)(t.h3,{id:`почему-поле-даты-осталось-строкой`,children:`Почему поле даты осталось строкой?`}),`
3559
+ `,(0,c.jsxs)(t.p,{children:[`Проверьте `,(0,c.jsx)(t.code,{children:`autoParse: true`}),`, metadata type и уровень вложенности. Parsing shallow: nested object не обходится рекурсивно.`]}),`
3560
+ `,(0,c.jsx)(t.h3,{id:`почему-mutation-получила-403`,children:`Почему mutation получила 403?`}),`
3561
+ `,(0,c.jsx)(t.p,{children:`Transport автоматически обновляет CSRF token и повторяет request один раз. Если ошибка осталась, вероятны authorization/business rules, неверный SAP client или SSO problem.`}),`
3562
+ `,(0,c.jsxs)(t.h3,{id:`почему-справочник-пустой-но-iserror--false`,children:[`Почему справочник пустой, но `,(0,c.jsx)(t.code,{children:`isError === false`}),`?`]}),`
3563
+ `,(0,c.jsx)(t.p,{children:`Текущий collection loader логирует request error и возвращает пустой items result. Смотрите logs и используйте более строгий query helper для критичного flow.`}),`
3564
+ `,(0,c.jsx)(t.h3,{id:`почему-справочник-скачался-хотя-есть-persisted-cache`,children:`Почему справочник скачался, хотя есть persisted cache?`}),`
3565
+ `,(0,c.jsx)(t.p,{children:`Snapshot мог быть старее server update, metadata update или семидневного coverage window. Тогда hook инвалидирует Query cache и отправляет SW bust policy.`}),`
3566
+ `,(0,c.jsx)(t.h3,{id:`можно-ли-использовать-пакет-в-ssr`,children:`Можно ли использовать пакет в SSR?`}),`
3567
+ `,(0,c.jsxs)(t.p,{children:[`Часть pure builders/types — да, но основной transport рассчитан на browser: cookies, `,(0,c.jsx)(t.code,{children:`window`}),`, `,(0,c.jsx)(t.code,{children:`document`}),`, iframe SSO, Service Worker headers. Разделяйте server/client boundaries явно.`]}),`
3568
+ `,(0,c.jsxs)(t.h3,{id:`нужно-ли-вручную-добавлять-sap-client`,children:[`Нужно ли вручную добавлять `,(0,c.jsx)(t.code,{children:`sap-client`}),`?`]}),`
3569
+ `,(0,c.jsxs)(t.p,{children:[`Нет. Настройте `,(0,c.jsx)(t.code,{children:`resolveSapClient`}),` в project adapter; transport добавит header.`]}),`
3570
+ `,(0,c.jsx)(t.h3,{id:`можно-ли-передать-custom-headers`,children:`Можно ли передать custom headers?`}),`
3571
+ `,(0,c.jsxs)(t.p,{children:[`Да, через `,(0,c.jsx)(t.code,{children:`init.headers`}),`. Обязательные transport headers могут быть дополнены/нормализованы. Не подменяйте централизованные CSRF/SSO механизмы.`]}),`
3572
+ `,(0,c.jsx)(t.h3,{id:`можно-ли-использовать-root-import-пакета`,children:`Можно ли использовать root import пакета?`}),`
3573
+ `,(0,c.jsx)(t.p,{children:`Нет. Используйте точный subpath:`}),`
3574
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { odataQueryFn } from "@ryuzaki13/react-foundation-api/odata";
3575
+ `})}),`
3576
+ `,(0,c.jsx)(t.h2,{id:`связанные-модули`,children:`Связанные модули`}),`
3577
+ `,(0,c.jsxs)(t.ul,{children:[`
3578
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/odata-service`}),` — metadata parser, filters, params, path builders и OData value conversion.`]}),`
3579
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/query-client`}),` — QueryClient, persistence и broadcast infrastructure.`]}),`
3580
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/table`}),` — metadata-driven columns и formatting runtime.`]}),`
3581
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-api/resource`}),` — generic resource descriptors поверх TanStack Query.`]}),`
3582
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-api/persisted`}),` — persisted resource operations и adapters.`]}),`
3583
+ `,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-api/error-report`}),` — доставка безопасных error reports.`]}),`
3584
+ `]}),`
3585
+ `,(0,c.jsx)(t.p,{children:`Главный принцип модуля:`}),`
3586
+ `,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`Metadata определяет transport contract.
3587
+ Query key определяет identity server snapshot.
3588
+ QueryClient владеет snapshot.
3589
+ Mutation явно синхронизирует cache.
3590
+ Service Worker cache остаётся отдельным HTTP-слоем.
3591
+ `})})]})}function s(e={}){let{wrapper:t}={...n(),...e.components};return t?(0,c.jsx)(t,{...e,children:(0,c.jsx)(o,{...e})}):o(e)}var c;function l(){return(l=e((()=>{c=t(),r(),a()})))()}l();export{s as default};