@honua/react 0.0.20-alpha.0

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 (597) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +9 -0
  3. package/contract/analytics-sources.d.ts +149 -0
  4. package/contract/analytics-sources.d.ts.map +1 -0
  5. package/contract/analytics-sources.js +235 -0
  6. package/contract/edit-session.d.ts +203 -0
  7. package/contract/edit-session.d.ts.map +1 -0
  8. package/contract/edit-session.js +760 -0
  9. package/contract/edit-sketch.d.ts +91 -0
  10. package/contract/edit-sketch.d.ts.map +1 -0
  11. package/contract/edit-sketch.js +281 -0
  12. package/contract/index.d.ts +61 -0
  13. package/contract/index.d.ts.map +1 -0
  14. package/contract/index.js +51 -0
  15. package/contract/jobs.d.ts +149 -0
  16. package/contract/jobs.d.ts.map +1 -0
  17. package/contract/jobs.js +40 -0
  18. package/contract/source.d.ts +423 -0
  19. package/contract/source.d.ts.map +1 -0
  20. package/contract/source.js +3802 -0
  21. package/contract/spatial-aggregation.d.ts +336 -0
  22. package/contract/spatial-aggregation.d.ts.map +1 -0
  23. package/contract/spatial-aggregation.js +298 -0
  24. package/contract/temporal.d.ts +455 -0
  25. package/contract/temporal.d.ts.map +1 -0
  26. package/contract/temporal.js +146 -0
  27. package/contract/tiles.d.ts +370 -0
  28. package/contract/tiles.d.ts.map +1 -0
  29. package/contract/tiles.js +519 -0
  30. package/contract/types.d.ts +665 -0
  31. package/contract/types.d.ts.map +1 -0
  32. package/contract/types.js +175 -0
  33. package/contract/widget-source.d.ts +205 -0
  34. package/contract/widget-source.d.ts.map +1 -0
  35. package/contract/widget-source.js +1473 -0
  36. package/control-plane/client.d.ts +95 -0
  37. package/control-plane/client.d.ts.map +1 -0
  38. package/control-plane/client.js +374 -0
  39. package/control-plane/index.d.ts +18 -0
  40. package/control-plane/index.d.ts.map +1 -0
  41. package/control-plane/index.js +14 -0
  42. package/control-plane/types.d.ts +233 -0
  43. package/control-plane/types.d.ts.map +1 -0
  44. package/control-plane/types.js +7 -0
  45. package/core/batch.d.ts +37 -0
  46. package/core/batch.d.ts.map +1 -0
  47. package/core/batch.js +79 -0
  48. package/core/cache-state.d.ts +79 -0
  49. package/core/cache-state.d.ts.map +1 -0
  50. package/core/cache-state.js +87 -0
  51. package/core/client.d.ts +577 -0
  52. package/core/client.d.ts.map +1 -0
  53. package/core/client.js +1664 -0
  54. package/core/compatibility.d.ts +18 -0
  55. package/core/compatibility.d.ts.map +1 -0
  56. package/core/compatibility.js +245 -0
  57. package/core/errors.d.ts +107 -0
  58. package/core/errors.d.ts.map +1 -0
  59. package/core/errors.js +156 -0
  60. package/core/esri-geojson.d.ts +66 -0
  61. package/core/esri-geojson.d.ts.map +1 -0
  62. package/core/esri-geojson.js +363 -0
  63. package/core/geoservices.d.ts +34 -0
  64. package/core/geoservices.d.ts.map +1 -0
  65. package/core/geoservices.js +434 -0
  66. package/core/grpc-adapter.d.ts +23 -0
  67. package/core/grpc-adapter.d.ts.map +1 -0
  68. package/core/grpc-adapter.js +688 -0
  69. package/core/odata.d.ts +347 -0
  70. package/core/odata.d.ts.map +1 -0
  71. package/core/odata.js +913 -0
  72. package/core/ogc-conformance.d.ts +35 -0
  73. package/core/ogc-conformance.d.ts.map +1 -0
  74. package/core/ogc-conformance.js +134 -0
  75. package/core/ogc-features.d.ts +23 -0
  76. package/core/ogc-features.d.ts.map +1 -0
  77. package/core/ogc-features.js +114 -0
  78. package/core/ogc-maps.d.ts +49 -0
  79. package/core/ogc-maps.d.ts.map +1 -0
  80. package/core/ogc-maps.js +126 -0
  81. package/core/ogc-processes.d.ts +118 -0
  82. package/core/ogc-processes.d.ts.map +1 -0
  83. package/core/ogc-processes.js +423 -0
  84. package/core/ogc-records.d.ts +75 -0
  85. package/core/ogc-records.d.ts.map +1 -0
  86. package/core/ogc-records.js +251 -0
  87. package/core/ogc-tiles.d.ts +125 -0
  88. package/core/ogc-tiles.d.ts.map +1 -0
  89. package/core/ogc-tiles.js +183 -0
  90. package/core/path-utils.d.ts +69 -0
  91. package/core/path-utils.d.ts.map +1 -0
  92. package/core/path-utils.js +123 -0
  93. package/core/pbf-decoder.d.ts +39 -0
  94. package/core/pbf-decoder.d.ts.map +1 -0
  95. package/core/pbf-decoder.js +702 -0
  96. package/core/process-runner.d.ts +104 -0
  97. package/core/process-runner.d.ts.map +1 -0
  98. package/core/process-runner.js +315 -0
  99. package/core/protocol-transport.d.ts +76 -0
  100. package/core/protocol-transport.d.ts.map +1 -0
  101. package/core/protocol-transport.js +15 -0
  102. package/core/query-builder.d.ts +261 -0
  103. package/core/query-builder.d.ts.map +1 -0
  104. package/core/query-builder.js +538 -0
  105. package/core/request-pipeline.d.ts +88 -0
  106. package/core/request-pipeline.d.ts.map +1 -0
  107. package/core/request-pipeline.js +273 -0
  108. package/core/spatial-filter.d.ts +93 -0
  109. package/core/spatial-filter.d.ts.map +1 -0
  110. package/core/spatial-filter.js +153 -0
  111. package/core/stac.d.ts +55 -0
  112. package/core/stac.d.ts.map +1 -0
  113. package/core/stac.js +229 -0
  114. package/core/surfaces.d.ts +527 -0
  115. package/core/surfaces.d.ts.map +1 -0
  116. package/core/surfaces.js +1917 -0
  117. package/core/terrain-elevation.d.ts +114 -0
  118. package/core/terrain-elevation.d.ts.map +1 -0
  119. package/core/terrain-elevation.js +150 -0
  120. package/core/types.d.ts +1355 -0
  121. package/core/types.d.ts.map +1 -0
  122. package/core/types.js +13 -0
  123. package/core/wfs-capabilities.d.ts +143 -0
  124. package/core/wfs-capabilities.d.ts.map +1 -0
  125. package/core/wfs-capabilities.js +478 -0
  126. package/core/wfs-filter.d.ts +118 -0
  127. package/core/wfs-filter.d.ts.map +1 -0
  128. package/core/wfs-filter.js +634 -0
  129. package/core/wfs.d.ts +219 -0
  130. package/core/wfs.d.ts.map +1 -0
  131. package/core/wfs.js +406 -0
  132. package/core/wire-shared.d.ts +32 -0
  133. package/core/wire-shared.d.ts.map +1 -0
  134. package/core/wire-shared.js +76 -0
  135. package/core/wms-axis.d.ts +36 -0
  136. package/core/wms-axis.d.ts.map +1 -0
  137. package/core/wms-axis.js +73 -0
  138. package/core/wms-capabilities.d.ts +110 -0
  139. package/core/wms-capabilities.d.ts.map +1 -0
  140. package/core/wms-capabilities.js +559 -0
  141. package/core/wms-types.d.ts +128 -0
  142. package/core/wms-types.d.ts.map +1 -0
  143. package/core/wms-types.js +32 -0
  144. package/core/wms.d.ts +144 -0
  145. package/core/wms.d.ts.map +1 -0
  146. package/core/wms.js +338 -0
  147. package/core/wmts-capabilities.d.ts +82 -0
  148. package/core/wmts-capabilities.d.ts.map +1 -0
  149. package/core/wmts-capabilities.js +402 -0
  150. package/core/wmts.d.ts +103 -0
  151. package/core/wmts.d.ts.map +1 -0
  152. package/core/wmts.js +332 -0
  153. package/core/xml-text.d.ts +20 -0
  154. package/core/xml-text.d.ts.map +1 -0
  155. package/core/xml-text.js +85 -0
  156. package/esri-compat/basemap-gallery.d.ts +38 -0
  157. package/esri-compat/basemap-gallery.d.ts.map +1 -0
  158. package/esri-compat/basemap-gallery.js +159 -0
  159. package/esri-compat/basemap-layer-list.d.ts +35 -0
  160. package/esri-compat/basemap-layer-list.d.ts.map +1 -0
  161. package/esri-compat/basemap-layer-list.js +139 -0
  162. package/esri-compat/basemap.d.ts +32 -0
  163. package/esri-compat/basemap.d.ts.map +1 -0
  164. package/esri-compat/basemap.js +84 -0
  165. package/esri-compat/bookmarks.d.ts +36 -0
  166. package/esri-compat/bookmarks.d.ts.map +1 -0
  167. package/esri-compat/bookmarks.js +108 -0
  168. package/esri-compat/class-breaks-renderer.d.ts +50 -0
  169. package/esri-compat/class-breaks-renderer.d.ts.map +1 -0
  170. package/esri-compat/class-breaks-renderer.js +164 -0
  171. package/esri-compat/color.d.ts +22 -0
  172. package/esri-compat/color.d.ts.map +1 -0
  173. package/esri-compat/color.js +106 -0
  174. package/esri-compat/controls.d.ts +174 -0
  175. package/esri-compat/controls.d.ts.map +1 -0
  176. package/esri-compat/controls.js +479 -0
  177. package/esri-compat/coordinate-conversion.d.ts +41 -0
  178. package/esri-compat/coordinate-conversion.d.ts.map +1 -0
  179. package/esri-compat/coordinate-conversion.js +125 -0
  180. package/esri-compat/directions.d.ts +45 -0
  181. package/esri-compat/directions.d.ts.map +1 -0
  182. package/esri-compat/directions.js +123 -0
  183. package/esri-compat/editor.d.ts +47 -0
  184. package/esri-compat/editor.d.ts.map +1 -0
  185. package/esri-compat/editor.js +170 -0
  186. package/esri-compat/esri-config.d.ts +16 -0
  187. package/esri-compat/esri-config.d.ts.map +1 -0
  188. package/esri-compat/esri-config.js +23 -0
  189. package/esri-compat/esri-request.d.ts +17 -0
  190. package/esri-compat/esri-request.d.ts.map +1 -0
  191. package/esri-compat/esri-request.js +127 -0
  192. package/esri-compat/event-bus.d.ts +106 -0
  193. package/esri-compat/event-bus.d.ts.map +1 -0
  194. package/esri-compat/event-bus.js +90 -0
  195. package/esri-compat/expand.d.ts +36 -0
  196. package/esri-compat/expand.d.ts.map +1 -0
  197. package/esri-compat/expand.js +97 -0
  198. package/esri-compat/extent.d.ts +43 -0
  199. package/esri-compat/extent.d.ts.map +1 -0
  200. package/esri-compat/extent.js +140 -0
  201. package/esri-compat/feature-filter.d.ts +55 -0
  202. package/esri-compat/feature-filter.d.ts.map +1 -0
  203. package/esri-compat/feature-filter.js +109 -0
  204. package/esri-compat/feature-form.d.ts +64 -0
  205. package/esri-compat/feature-form.d.ts.map +1 -0
  206. package/esri-compat/feature-form.js +137 -0
  207. package/esri-compat/feature-layer.d.ts +173 -0
  208. package/esri-compat/feature-layer.d.ts.map +1 -0
  209. package/esri-compat/feature-layer.js +661 -0
  210. package/esri-compat/feature-set.d.ts +32 -0
  211. package/esri-compat/feature-set.d.ts.map +1 -0
  212. package/esri-compat/feature-set.js +80 -0
  213. package/esri-compat/feature-table.d.ts +141 -0
  214. package/esri-compat/feature-table.d.ts.map +1 -0
  215. package/esri-compat/feature-table.js +500 -0
  216. package/esri-compat/feature-templates.d.ts +41 -0
  217. package/esri-compat/feature-templates.d.ts.map +1 -0
  218. package/esri-compat/feature-templates.js +97 -0
  219. package/esri-compat/feature.d.ts +33 -0
  220. package/esri-compat/feature.d.ts.map +1 -0
  221. package/esri-compat/feature.js +83 -0
  222. package/esri-compat/geojson-layer.d.ts +86 -0
  223. package/esri-compat/geojson-layer.d.ts.map +1 -0
  224. package/esri-compat/geojson-layer.js +191 -0
  225. package/esri-compat/geometry-engine.d.ts +107 -0
  226. package/esri-compat/geometry-engine.d.ts.map +1 -0
  227. package/esri-compat/geometry-engine.js +266 -0
  228. package/esri-compat/graphic.d.ts +46 -0
  229. package/esri-compat/graphic.d.ts.map +1 -0
  230. package/esri-compat/graphic.js +103 -0
  231. package/esri-compat/graphics-layer.d.ts +49 -0
  232. package/esri-compat/graphics-layer.d.ts.map +1 -0
  233. package/esri-compat/graphics-layer.js +185 -0
  234. package/esri-compat/group-layer.d.ts +48 -0
  235. package/esri-compat/group-layer.d.ts.map +1 -0
  236. package/esri-compat/group-layer.js +212 -0
  237. package/esri-compat/identify.d.ts +71 -0
  238. package/esri-compat/identify.d.ts.map +1 -0
  239. package/esri-compat/identify.js +299 -0
  240. package/esri-compat/identity-manager.d.ts +31 -0
  241. package/esri-compat/identity-manager.d.ts.map +1 -0
  242. package/esri-compat/identity-manager.js +115 -0
  243. package/esri-compat/imagery-layer.d.ts +78 -0
  244. package/esri-compat/imagery-layer.d.ts.map +1 -0
  245. package/esri-compat/imagery-layer.js +167 -0
  246. package/esri-compat/label-class.d.ts +31 -0
  247. package/esri-compat/label-class.d.ts.map +1 -0
  248. package/esri-compat/label-class.js +102 -0
  249. package/esri-compat/layer-list.d.ts +77 -0
  250. package/esri-compat/layer-list.d.ts.map +1 -0
  251. package/esri-compat/layer-list.js +342 -0
  252. package/esri-compat/legend.d.ts +53 -0
  253. package/esri-compat/legend.d.ts.map +1 -0
  254. package/esri-compat/legend.js +254 -0
  255. package/esri-compat/map-image-layer.d.ts +162 -0
  256. package/esri-compat/map-image-layer.d.ts.map +1 -0
  257. package/esri-compat/map-image-layer.js +709 -0
  258. package/esri-compat/map-view.d.ts +323 -0
  259. package/esri-compat/map-view.d.ts.map +1 -0
  260. package/esri-compat/map-view.js +1175 -0
  261. package/esri-compat/map.d.ts +47 -0
  262. package/esri-compat/map.d.ts.map +1 -0
  263. package/esri-compat/map.js +209 -0
  264. package/esri-compat/measurement-2d.d.ts +61 -0
  265. package/esri-compat/measurement-2d.d.ts.map +1 -0
  266. package/esri-compat/measurement-2d.js +184 -0
  267. package/esri-compat/measurement.d.ts +45 -0
  268. package/esri-compat/measurement.d.ts.map +1 -0
  269. package/esri-compat/measurement.js +193 -0
  270. package/esri-compat/oauth-info.d.ts +35 -0
  271. package/esri-compat/oauth-info.d.ts.map +1 -0
  272. package/esri-compat/oauth-info.js +115 -0
  273. package/esri-compat/picture-marker-symbol.d.ts +35 -0
  274. package/esri-compat/picture-marker-symbol.d.ts.map +1 -0
  275. package/esri-compat/picture-marker-symbol.js +126 -0
  276. package/esri-compat/point.d.ts +31 -0
  277. package/esri-compat/point.d.ts.map +1 -0
  278. package/esri-compat/point.js +102 -0
  279. package/esri-compat/polygon.d.ts +31 -0
  280. package/esri-compat/polygon.d.ts.map +1 -0
  281. package/esri-compat/polygon.js +107 -0
  282. package/esri-compat/polyline.d.ts +31 -0
  283. package/esri-compat/polyline.d.ts.map +1 -0
  284. package/esri-compat/polyline.js +107 -0
  285. package/esri-compat/popup-template.d.ts +55 -0
  286. package/esri-compat/popup-template.d.ts.map +1 -0
  287. package/esri-compat/popup-template.js +128 -0
  288. package/esri-compat/popup.d.ts +54 -0
  289. package/esri-compat/popup.d.ts.map +1 -0
  290. package/esri-compat/popup.js +312 -0
  291. package/esri-compat/print.d.ts +47 -0
  292. package/esri-compat/print.d.ts.map +1 -0
  293. package/esri-compat/print.js +105 -0
  294. package/esri-compat/query.d.ts +58 -0
  295. package/esri-compat/query.d.ts.map +1 -0
  296. package/esri-compat/query.js +128 -0
  297. package/esri-compat/reactive-utils.d.ts +24 -0
  298. package/esri-compat/reactive-utils.d.ts.map +1 -0
  299. package/esri-compat/reactive-utils.js +193 -0
  300. package/esri-compat/request.d.ts +41 -0
  301. package/esri-compat/request.d.ts.map +1 -0
  302. package/esri-compat/request.js +253 -0
  303. package/esri-compat/route-layer.d.ts +59 -0
  304. package/esri-compat/route-layer.d.ts.map +1 -0
  305. package/esri-compat/route-layer.js +223 -0
  306. package/esri-compat/route-task.d.ts +88 -0
  307. package/esri-compat/route-task.d.ts.map +1 -0
  308. package/esri-compat/route-task.js +225 -0
  309. package/esri-compat/scene-view.d.ts +41 -0
  310. package/esri-compat/scene-view.d.ts.map +1 -0
  311. package/esri-compat/scene-view.js +68 -0
  312. package/esri-compat/search.d.ts +112 -0
  313. package/esri-compat/search.d.ts.map +1 -0
  314. package/esri-compat/search.js +820 -0
  315. package/esri-compat/simple-fill-symbol.d.ts +27 -0
  316. package/esri-compat/simple-fill-symbol.d.ts.map +1 -0
  317. package/esri-compat/simple-fill-symbol.js +85 -0
  318. package/esri-compat/simple-line-symbol.d.ts +27 -0
  319. package/esri-compat/simple-line-symbol.d.ts.map +1 -0
  320. package/esri-compat/simple-line-symbol.js +85 -0
  321. package/esri-compat/simple-marker-symbol.d.ts +29 -0
  322. package/esri-compat/simple-marker-symbol.d.ts.map +1 -0
  323. package/esri-compat/simple-marker-symbol.js +92 -0
  324. package/esri-compat/simple-renderer.d.ts +29 -0
  325. package/esri-compat/simple-renderer.d.ts.map +1 -0
  326. package/esri-compat/simple-renderer.js +92 -0
  327. package/esri-compat/sketch.d.ts +64 -0
  328. package/esri-compat/sketch.d.ts.map +1 -0
  329. package/esri-compat/sketch.js +227 -0
  330. package/esri-compat/spatial-reference.d.ts +31 -0
  331. package/esri-compat/spatial-reference.d.ts.map +1 -0
  332. package/esri-compat/spatial-reference.js +102 -0
  333. package/esri-compat/swipe.d.ts +34 -0
  334. package/esri-compat/swipe.d.ts.map +1 -0
  335. package/esri-compat/swipe.js +93 -0
  336. package/esri-compat/table-list.d.ts +36 -0
  337. package/esri-compat/table-list.d.ts.map +1 -0
  338. package/esri-compat/table-list.js +126 -0
  339. package/esri-compat/text-symbol.d.ts +37 -0
  340. package/esri-compat/text-symbol.d.ts.map +1 -0
  341. package/esri-compat/text-symbol.js +123 -0
  342. package/esri-compat/tile-layer.d.ts +52 -0
  343. package/esri-compat/tile-layer.d.ts.map +1 -0
  344. package/esri-compat/tile-layer.js +168 -0
  345. package/esri-compat/time-slider.d.ts +70 -0
  346. package/esri-compat/time-slider.d.ts.map +1 -0
  347. package/esri-compat/time-slider.js +208 -0
  348. package/esri-compat/track.d.ts +51 -0
  349. package/esri-compat/track.d.ts.map +1 -0
  350. package/esri-compat/track.js +174 -0
  351. package/esri-compat/unique-value-renderer.d.ts +41 -0
  352. package/esri-compat/unique-value-renderer.d.ts.map +1 -0
  353. package/esri-compat/unique-value-renderer.js +127 -0
  354. package/esri-compat/url.d.ts +13 -0
  355. package/esri-compat/url.d.ts.map +1 -0
  356. package/esri-compat/url.js +68 -0
  357. package/esri-compat/vector-tile-layer.d.ts +47 -0
  358. package/esri-compat/vector-tile-layer.d.ts.map +1 -0
  359. package/esri-compat/vector-tile-layer.js +130 -0
  360. package/esri-compat/web-map.d.ts +14 -0
  361. package/esri-compat/web-map.d.ts.map +1 -0
  362. package/esri-compat/web-map.js +24 -0
  363. package/esri-compat/wfs-layer.d.ts +64 -0
  364. package/esri-compat/wfs-layer.d.ts.map +1 -0
  365. package/esri-compat/wfs-layer.js +145 -0
  366. package/esri-compat/wms-layer.d.ts +76 -0
  367. package/esri-compat/wms-layer.d.ts.map +1 -0
  368. package/esri-compat/wms-layer.js +157 -0
  369. package/exploration/context.d.ts +25 -0
  370. package/exploration/context.d.ts.map +1 -0
  371. package/exploration/context.js +396 -0
  372. package/exploration/index.d.ts +27 -0
  373. package/exploration/index.d.ts.map +1 -0
  374. package/exploration/index.js +24 -0
  375. package/exploration/presets.d.ts +27 -0
  376. package/exploration/presets.d.ts.map +1 -0
  377. package/exploration/presets.js +114 -0
  378. package/exploration/reducer.d.ts +23 -0
  379. package/exploration/reducer.d.ts.map +1 -0
  380. package/exploration/reducer.js +332 -0
  381. package/exploration/selection.d.ts +25 -0
  382. package/exploration/selection.d.ts.map +1 -0
  383. package/exploration/selection.js +34 -0
  384. package/exploration/selectors.d.ts +56 -0
  385. package/exploration/selectors.d.ts.map +1 -0
  386. package/exploration/selectors.js +110 -0
  387. package/exploration/types.d.ts +294 -0
  388. package/exploration/types.d.ts.map +1 -0
  389. package/exploration/types.js +33 -0
  390. package/expr/expression.d.ts +457 -0
  391. package/expr/expression.d.ts.map +1 -0
  392. package/expr/expression.js +663 -0
  393. package/expr/index.d.ts +32 -0
  394. package/expr/index.d.ts.map +1 -0
  395. package/expr/index.js +30 -0
  396. package/gen/honua/v1/feature_service_pb.d.ts +918 -0
  397. package/gen/honua/v1/feature_service_pb.d.ts.map +1 -0
  398. package/gen/honua/v1/feature_service_pb.js +375 -0
  399. package/generated-app/errors.d.ts +41 -0
  400. package/generated-app/errors.d.ts.map +1 -0
  401. package/generated-app/errors.js +41 -0
  402. package/generated-app/index.d.ts +17 -0
  403. package/generated-app/index.d.ts.map +1 -0
  404. package/generated-app/index.js +12 -0
  405. package/generated-app/manifest.d.ts +183 -0
  406. package/generated-app/manifest.d.ts.map +1 -0
  407. package/generated-app/manifest.js +14 -0
  408. package/generated-app/projection.d.ts +44 -0
  409. package/generated-app/projection.d.ts.map +1 -0
  410. package/generated-app/projection.js +234 -0
  411. package/generated-app/runtime.d.ts +215 -0
  412. package/generated-app/runtime.d.ts.map +1 -0
  413. package/generated-app/runtime.js +1196 -0
  414. package/interactions/detail-model.d.ts +54 -0
  415. package/interactions/detail-model.d.ts.map +1 -0
  416. package/interactions/detail-model.js +105 -0
  417. package/interactions/exploration-bindings.d.ts +120 -0
  418. package/interactions/exploration-bindings.d.ts.map +1 -0
  419. package/interactions/exploration-bindings.js +218 -0
  420. package/interactions/feature-state.d.ts +144 -0
  421. package/interactions/feature-state.d.ts.map +1 -0
  422. package/interactions/feature-state.js +169 -0
  423. package/interactions/hit-test.d.ts +112 -0
  424. package/interactions/hit-test.d.ts.map +1 -0
  425. package/interactions/hit-test.js +291 -0
  426. package/interactions/index.d.ts +31 -0
  427. package/interactions/index.d.ts.map +1 -0
  428. package/interactions/index.js +24 -0
  429. package/interactions/share-state.d.ts +36 -0
  430. package/interactions/share-state.d.ts.map +1 -0
  431. package/interactions/share-state.js +123 -0
  432. package/map/feature-service-adapter.d.ts +138 -0
  433. package/map/feature-service-adapter.d.ts.map +1 -0
  434. package/map/feature-service-adapter.js +279 -0
  435. package/map/honua-map.d.ts +203 -0
  436. package/map/honua-map.d.ts.map +1 -0
  437. package/map/honua-map.js +347 -0
  438. package/map/index.d.ts +27 -0
  439. package/map/index.d.ts.map +1 -0
  440. package/map/index.js +22 -0
  441. package/map/maplibre-target.d.ts +103 -0
  442. package/map/maplibre-target.d.ts.map +1 -0
  443. package/map/maplibre-target.js +164 -0
  444. package/map/webmap-maplibre.d.ts +60 -0
  445. package/map/webmap-maplibre.d.ts.map +1 -0
  446. package/map/webmap-maplibre.js +212 -0
  447. package/package.json +37 -0
  448. package/react/context.d.ts +32 -0
  449. package/react/context.d.ts.map +1 -0
  450. package/react/context.js +19 -0
  451. package/react/honua-layer.d.ts +35 -0
  452. package/react/honua-layer.d.ts.map +1 -0
  453. package/react/honua-layer.js +47 -0
  454. package/react/honua-map.d.ts +83 -0
  455. package/react/honua-map.d.ts.map +1 -0
  456. package/react/honua-map.js +109 -0
  457. package/react/honua-popup.d.ts +26 -0
  458. package/react/honua-popup.d.ts.map +1 -0
  459. package/react/honua-popup.js +34 -0
  460. package/react/hooks.d.ts +127 -0
  461. package/react/hooks.d.ts.map +1 -0
  462. package/react/hooks.js +206 -0
  463. package/react/index.d.ts +42 -0
  464. package/react/index.d.ts.map +1 -0
  465. package/react/index.js +34 -0
  466. package/react/provider.d.ts +36 -0
  467. package/react/provider.d.ts.map +1 -0
  468. package/react/provider.js +35 -0
  469. package/react/query-cache.d.ts +76 -0
  470. package/react/query-cache.d.ts.map +1 -0
  471. package/react/query-cache.js +188 -0
  472. package/runtime/diff.d.ts +31 -0
  473. package/runtime/diff.d.ts.map +1 -0
  474. package/runtime/diff.js +143 -0
  475. package/runtime/errors.d.ts +40 -0
  476. package/runtime/errors.d.ts.map +1 -0
  477. package/runtime/errors.js +35 -0
  478. package/runtime/index.d.ts +59 -0
  479. package/runtime/index.d.ts.map +1 -0
  480. package/runtime/index.js +43 -0
  481. package/runtime/legend.d.ts +23 -0
  482. package/runtime/legend.d.ts.map +1 -0
  483. package/runtime/legend.js +38 -0
  484. package/runtime/load-package.d.ts +125 -0
  485. package/runtime/load-package.d.ts.map +1 -0
  486. package/runtime/load-package.js +264 -0
  487. package/runtime/map-package-fetch.d.ts +107 -0
  488. package/runtime/map-package-fetch.d.ts.map +1 -0
  489. package/runtime/map-package-fetch.js +336 -0
  490. package/runtime/map-package-realtime.d.ts +169 -0
  491. package/runtime/map-package-realtime.d.ts.map +1 -0
  492. package/runtime/map-package-realtime.js +350 -0
  493. package/runtime/map-package-validation.d.ts +43 -0
  494. package/runtime/map-package-validation.d.ts.map +1 -0
  495. package/runtime/map-package-validation.js +269 -0
  496. package/runtime/map-package-watch.d.ts +78 -0
  497. package/runtime/map-package-watch.d.ts.map +1 -0
  498. package/runtime/map-package-watch.js +695 -0
  499. package/runtime/map-package.d.ts +167 -0
  500. package/runtime/map-package.d.ts.map +1 -0
  501. package/runtime/map-package.js +15 -0
  502. package/runtime/ogc-styles.d.ts +110 -0
  503. package/runtime/ogc-styles.d.ts.map +1 -0
  504. package/runtime/ogc-styles.js +195 -0
  505. package/runtime/popups.d.ts +62 -0
  506. package/runtime/popups.d.ts.map +1 -0
  507. package/runtime/popups.js +95 -0
  508. package/runtime/query-tiles.d.ts +217 -0
  509. package/runtime/query-tiles.d.ts.map +1 -0
  510. package/runtime/query-tiles.js +801 -0
  511. package/runtime/runtime.d.ts +243 -0
  512. package/runtime/runtime.d.ts.map +1 -0
  513. package/runtime/runtime.js +1147 -0
  514. package/runtime/source-bridge.d.ts +99 -0
  515. package/runtime/source-bridge.d.ts.map +1 -0
  516. package/runtime/source-bridge.js +401 -0
  517. package/runtime/style-compose.d.ts +46 -0
  518. package/runtime/style-compose.d.ts.map +1 -0
  519. package/runtime/style-compose.js +144 -0
  520. package/runtime/style-interactions.d.ts +141 -0
  521. package/runtime/style-interactions.d.ts.map +1 -0
  522. package/runtime/style-interactions.js +574 -0
  523. package/runtime/style-spec-validation.d.ts +23 -0
  524. package/runtime/style-spec-validation.d.ts.map +1 -0
  525. package/runtime/style-spec-validation.js +357 -0
  526. package/studio/capability-manifest.d.ts +285 -0
  527. package/studio/capability-manifest.d.ts.map +1 -0
  528. package/studio/capability-manifest.js +59 -0
  529. package/studio/chart-spec.d.ts +85 -0
  530. package/studio/chart-spec.d.ts.map +1 -0
  531. package/studio/chart-spec.js +106 -0
  532. package/studio/index.d.ts +34 -0
  533. package/studio/index.d.ts.map +1 -0
  534. package/studio/index.js +27 -0
  535. package/studio/provenance.d.ts +131 -0
  536. package/studio/provenance.d.ts.map +1 -0
  537. package/studio/provenance.js +39 -0
  538. package/studio/publish.d.ts +64 -0
  539. package/studio/publish.d.ts.map +1 -0
  540. package/studio/publish.js +13 -0
  541. package/studio/types.d.ts +278 -0
  542. package/studio/types.d.ts.map +1 -0
  543. package/studio/types.js +71 -0
  544. package/studio/validate.d.ts +45 -0
  545. package/studio/validate.d.ts.map +1 -0
  546. package/studio/validate.js +255 -0
  547. package/studio/validation.d.ts +112 -0
  548. package/studio/validation.d.ts.map +1 -0
  549. package/studio/validation.js +42 -0
  550. package/style/factory.d.ts +30 -0
  551. package/style/factory.d.ts.map +1 -0
  552. package/style/factory.js +61 -0
  553. package/style/index.d.ts +25 -0
  554. package/style/index.d.ts.map +1 -0
  555. package/style/index.js +24 -0
  556. package/style/specification.d.ts +203 -0
  557. package/style/specification.d.ts.map +1 -0
  558. package/style/specification.js +178 -0
  559. package/style/wms-wmts-resolvers.d.ts +38 -0
  560. package/style/wms-wmts-resolvers.d.ts.map +1 -0
  561. package/style/wms-wmts-resolvers.js +92 -0
  562. package/webmap/contract.d.ts +28 -0
  563. package/webmap/contract.d.ts.map +1 -0
  564. package/webmap/contract.js +130 -0
  565. package/webmap/convert-basemap.d.ts +17 -0
  566. package/webmap/convert-basemap.d.ts.map +1 -0
  567. package/webmap/convert-basemap.js +78 -0
  568. package/webmap/convert-extent.d.ts +17 -0
  569. package/webmap/convert-extent.d.ts.map +1 -0
  570. package/webmap/convert-extent.js +114 -0
  571. package/webmap/convert-label.d.ts +13 -0
  572. package/webmap/convert-label.d.ts.map +1 -0
  573. package/webmap/convert-label.js +125 -0
  574. package/webmap/convert-layer.d.ts +22 -0
  575. package/webmap/convert-layer.d.ts.map +1 -0
  576. package/webmap/convert-layer.js +181 -0
  577. package/webmap/convert-popup.d.ts +34 -0
  578. package/webmap/convert-popup.d.ts.map +1 -0
  579. package/webmap/convert-popup.js +68 -0
  580. package/webmap/convert-renderer.d.ts +17 -0
  581. package/webmap/convert-renderer.d.ts.map +1 -0
  582. package/webmap/convert-renderer.js +161 -0
  583. package/webmap/convert-symbol.d.ts +17 -0
  584. package/webmap/convert-symbol.d.ts.map +1 -0
  585. package/webmap/convert-symbol.js +122 -0
  586. package/webmap/index.d.ts +44 -0
  587. package/webmap/index.d.ts.map +1 -0
  588. package/webmap/index.js +33 -0
  589. package/webmap/parse.d.ts +25 -0
  590. package/webmap/parse.d.ts.map +1 -0
  591. package/webmap/parse.js +83 -0
  592. package/webmap/types.d.ts +264 -0
  593. package/webmap/types.d.ts.map +1 -0
  594. package/webmap/types.js +13 -0
  595. package/webmap/warnings.d.ts +30 -0
  596. package/webmap/warnings.d.ts.map +1 -0
  597. package/webmap/warnings.js +37 -0
@@ -0,0 +1,3802 @@
1
+ /**
2
+ * Built-in `Source` adapters that wrap the runtime classes in
3
+ * `src/core/surfaces.ts` (`HonuaFeatureLayer`, `HonuaMapLayer`,
4
+ * `HonuaOgcFeatureCollection`) and the OGC / STAC / WFS / WMS / OData
5
+ * adapters in `src/core/`. Consumers register adapters for any other
6
+ * protocol (MapLibre-native sources, etc.) via
7
+ * `CreateDatasetOptions.resolveSource`.
8
+ *
9
+ * These adapters do not reimplement query logic — they translate the
10
+ * canonical `Query` / `Result` envelope to and from the existing per-class
11
+ * request and response shapes.
12
+ *
13
+ * @module
14
+ */
15
+ import { HonuaCapabilityNotSupportedError, HonuaHttpError } from "../core/errors.js";
16
+ import { HonuaOdataEntitySet, buildOdataSpatialFilter, odataFieldSchema, rewriteWhereToOdataFilter, } from "../core/odata.js";
17
+ import { HonuaOgcCollectionMap, HonuaOgcMaps } from "../core/ogc-maps.js";
18
+ import { HonuaOgcRecordCollection } from "../core/ogc-records.js";
19
+ import { HonuaOgcTiles, HonuaOgcTileset } from "../core/ogc-tiles.js";
20
+ import { trimTrailingSlashes } from "../core/path-utils.js";
21
+ import { HonuaStacSearch } from "../core/stac.js";
22
+ import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatureCollection, HonuaOgcFeatures, } from "../core/surfaces.js";
23
+ import { UNSUPPORTED_FES, compileSpatialFilter, compileWhere, geoJsonGeometryToGml, serializeFes, } from "../core/wfs-filter.js";
24
+ import { HonuaWfs, HonuaWfsFeatureType } from "../core/wfs.js";
25
+ import { HonuaWms, HonuaWmsLayer, parseWmsLayerNames } from "../core/wms.js";
26
+ import { HonuaWmts, HonuaWmtsLayer, HonuaWmtsTileset } from "../core/wmts.js";
27
+ import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
28
+ /**
29
+ * Construct a protocol-neutral `Dataset` over a `HonuaClient`.
30
+ *
31
+ * A `Dataset` groups one or more `Source`s. Each `Source` accepts a protocol-neutral
32
+ * `Query` and returns a protocol-neutral `Result`. The dataset is the cross-protocol
33
+ * unit of work: a single `Dataset` can mix GeoServices FeatureServer, OGC API Features,
34
+ * WFS, STAC, and OData sources under one capability policy.
35
+ *
36
+ * `Source` handles are constructed lazily, server compatibility is checked once per
37
+ * dataset (and cached on the underlying client), and capability gaps surface as
38
+ * {@link HonuaCapabilityNotSupportedError} under the default `"strict"` policy rather
39
+ * than silently returning empty results.
40
+ *
41
+ * Operations the canonical surface does not cover stay reachable through the typed
42
+ * `source.protocol(...)` escape hatch (e.g. ImageServer `exportImage`, GeometryServer
43
+ * `project`, GPServer `submitJob`, WFS `LockFeature`).
44
+ *
45
+ * @param options - The dataset id, the `HonuaClient` to talk to, the list of
46
+ * `SourceDescriptor`s, and (optionally) `capabilityPolicy` and a custom
47
+ * `resolveSource` factory.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { createDataset, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
52
+ * import { HonuaClient } from "@honua/sdk-js/honua";
53
+ *
54
+ * const client = new HonuaClient({ baseUrl: "https://your-honua-server.example" });
55
+ *
56
+ * const dataset = createDataset({
57
+ * id: "parcels",
58
+ * client,
59
+ * sources: [
60
+ * {
61
+ * id: "parcels-fs",
62
+ * protocol: "geoservices-feature-service",
63
+ * locator: { url: "https://your-honua-server.example", serviceId: "parcels", layerId: 0 },
64
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
65
+ * },
66
+ * ],
67
+ * });
68
+ *
69
+ * const parcels = dataset.source("parcels-fs")!;
70
+ * const result = await parcels.queryAll({
71
+ * where: "STATUS = 'ACTIVE'",
72
+ * outFields: ["OBJECTID", "NAME"],
73
+ * returnGeometry: true,
74
+ * pagination: { limit: 500 },
75
+ * });
76
+ *
77
+ * console.log(`Loaded ${result.features.length} features`);
78
+ * ```
79
+ */
80
+ export function createDataset(options) {
81
+ const { id, client, sources, resolveSource } = options;
82
+ const policy = options.capabilityPolicy ?? "strict";
83
+ const descriptors = new Map();
84
+ const handles = new Map();
85
+ let compatibilityPromise;
86
+ for (const descriptor of sources) {
87
+ if (descriptors.has(descriptor.id)) {
88
+ throw new Error(`createDataset: duplicate source id "${descriptor.id}"`);
89
+ }
90
+ descriptors.set(descriptor.id, descriptor);
91
+ }
92
+ function resolve(descriptor) {
93
+ const cached = handles.get(descriptor.id);
94
+ if (cached)
95
+ return cached;
96
+ const built = buildBuiltInSource(descriptor, client, policy) ??
97
+ resolveSource?.(descriptor, { client, capabilityPolicy: policy });
98
+ if (!built) {
99
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
100
+ }
101
+ handles.set(descriptor.id, built);
102
+ return built;
103
+ }
104
+ return {
105
+ id,
106
+ client,
107
+ sourceDescriptors: [...descriptors.values()],
108
+ source(sourceId) {
109
+ const descriptor = descriptors.get(sourceId);
110
+ if (!descriptor)
111
+ return undefined;
112
+ return resolve(descriptor);
113
+ },
114
+ sourceIds() {
115
+ return [...descriptors.keys()];
116
+ },
117
+ isCompatible() {
118
+ if (options.skipCompatibilityCheck)
119
+ return Promise.resolve(true);
120
+ if (!compatibilityPromise) {
121
+ compatibilityPromise = client
122
+ .checkCompatibility()
123
+ .then((status) => status.supported)
124
+ .catch(() => false);
125
+ }
126
+ return compatibilityPromise;
127
+ },
128
+ supportsFeature(feature) {
129
+ return client.supportsFeature(feature).catch(() => false);
130
+ },
131
+ };
132
+ }
133
+ // ── Built-in resolver ─────────────────────────────────────────
134
+ function buildBuiltInSource(descriptor, client, policy) {
135
+ switch (descriptor.protocol) {
136
+ case "geoservices-feature-service":
137
+ return geoServicesFeatureSource(descriptor, client, policy);
138
+ case "geoservices-map-service":
139
+ return geoServicesMapServiceSource(descriptor, client, policy);
140
+ case "geoservices-image-service":
141
+ return geoServicesImageSource(descriptor, client, policy);
142
+ case "geoservices-geometry-service":
143
+ return geoServicesGeometryServiceSource(descriptor, client, policy);
144
+ case "geoservices-gp-service":
145
+ return geoServicesGPServiceSource(descriptor, client, policy);
146
+ case "ogc-features":
147
+ return ogcFeaturesSource(descriptor, client, policy);
148
+ case "ogc-tiles":
149
+ return ogcTilesSource(descriptor, client, policy);
150
+ case "ogc-maps":
151
+ return ogcMapsSource(descriptor, client, policy);
152
+ case "ogc-records":
153
+ return ogcRecordsSource(descriptor, client, policy);
154
+ case "wms":
155
+ return wmsSource(descriptor, client, policy);
156
+ case "wmts":
157
+ return wmtsSource(descriptor, client, policy);
158
+ case "stac":
159
+ return stacSearchSource(descriptor, client, policy);
160
+ case "wfs":
161
+ return wfsSource(descriptor, client, policy);
162
+ case "odata":
163
+ return odataSource(descriptor, client, policy);
164
+ default:
165
+ return undefined;
166
+ }
167
+ }
168
+ // ── GeoServices Feature Service ───────────────────────────────
169
+ /**
170
+ * Adapter factory for a GeoServices FeatureServer layer.
171
+ *
172
+ * Used internally by {@link createDataset} when it sees a descriptor with
173
+ * `protocol: "geoservices-feature-service"`. Calling it directly is rarely
174
+ * necessary; pass the descriptor to `createDataset` instead.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const dataset = createDataset({
179
+ * id: "parcels",
180
+ * client,
181
+ * sources: [{
182
+ * id: "parcels-fs",
183
+ * protocol: "geoservices-feature-service",
184
+ * locator: { url: "https://your-honua-server.example", serviceId: "parcels", layerId: 0 },
185
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
186
+ * }],
187
+ * });
188
+ * const result = await dataset.source("parcels-fs")!.queryAll({
189
+ * where: "STATUS = 'ACTIVE'",
190
+ * outFields: ["OBJECTID", "NAME"],
191
+ * });
192
+ * ```
193
+ */
194
+ export function geoServicesFeatureSource(descriptor, client, policy) {
195
+ const { serviceId, layerId } = requireFeatureServiceLocator(descriptor);
196
+ const layer = new HonuaFeatureLayer({ client, serviceId, layerId });
197
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"];
198
+ const adapterRegistry = {
199
+ "geoservices-feature-service": layer,
200
+ };
201
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
202
+ async query(request) {
203
+ ensureCapability(descriptor, caps, "query");
204
+ const requestParams = toFeatureLayerRequest(request);
205
+ if (request?.aggregation) {
206
+ ensureCapability(descriptor, caps, "queryAggregate");
207
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
208
+ return aggregateResultFromFeatureLayer(await layer.queryFeatures({ ...requestParams, ...aggFields }), aggregateAlias);
209
+ }
210
+ const response = await layer.queryFeatures(requestParams);
211
+ return featureLayerResultFromTyped(response);
212
+ },
213
+ async queryAll(request) {
214
+ ensureCapability(descriptor, caps, "query");
215
+ const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
216
+ const features = await layer.queryFeaturesAll(params);
217
+ const { features: limited, exceededTransferLimit } = applyQueryAllLimit(features, request?.pagination?.limit);
218
+ return {
219
+ features: limited,
220
+ exceededTransferLimit,
221
+ totalCount: limited.length,
222
+ };
223
+ },
224
+ async queryAggregate(request) {
225
+ ensureCapability(descriptor, caps, "queryAggregate");
226
+ const requestParams = toFeatureLayerRequest(request);
227
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
228
+ const response = await layer.queryFeatures({ ...requestParams, ...aggFields });
229
+ return aggregateResultFromFeatureLayer(response, aggregateAlias);
230
+ },
231
+ async queryExtent(request) {
232
+ ensureCapability(descriptor, caps, "queryExtent");
233
+ const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
234
+ return extractExtentEnvelope(response);
235
+ },
236
+ async *stream(request) {
237
+ ensureCapability(descriptor, caps, "stream");
238
+ const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
239
+ for await (const page of stream) {
240
+ yield {
241
+ features: page,
242
+ exceededTransferLimit: false,
243
+ };
244
+ }
245
+ },
246
+ async queryObjectIds(request) {
247
+ ensureCapability(descriptor, caps, "queryObjectIds");
248
+ return layer.queryObjectIds(toFeatureLayerRequest(request));
249
+ },
250
+ async applyEdits(envelope) {
251
+ ensureCapability(descriptor, caps, "applyEdits");
252
+ const response = await layer.applyEdits(toApplyEditsRequest(envelope));
253
+ return canonicalEditResult(response);
254
+ },
255
+ async queryRelated(request) {
256
+ ensureCapability(descriptor, caps, "queryRelated");
257
+ const response = await layer.queryRelatedRecords(toRelatedRecordsRequest(request));
258
+ return canonicalRelatedResult(response);
259
+ },
260
+ attachments: featureLayerAttachmentApi(descriptor, caps, layer),
261
+ });
262
+ }
263
+ // ── GeoServices Map Service / Map Layer ───────────────────────
264
+ /**
265
+ * Adapter factory for a GeoServices MapServer layer.
266
+ *
267
+ * Reach `Source.protocol("geoservices-map-service")` for raw `exportImage` /
268
+ * `identify` / `find` access; the canonical `query()` runs against a single
269
+ * sublayer via the MapServer query endpoint.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * const dataset = createDataset({
274
+ * id: "basemap",
275
+ * client,
276
+ * sources: [{
277
+ * id: "states-mapserver",
278
+ * protocol: "geoservices-map-service",
279
+ * locator: { url: "https://your-honua-server.example", serviceId: "states", layerId: 0 },
280
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-map-service"],
281
+ * }],
282
+ * });
283
+ * ```
284
+ */
285
+ export function geoServicesMapServiceSource(descriptor, client, policy) {
286
+ const { serviceId, layerId } = requireMapServiceLocator(descriptor);
287
+ const service = new HonuaMapService({ client, serviceId });
288
+ const layer = new HonuaMapLayer({ client, serviceId, layerId });
289
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-map-service"];
290
+ const adapterRegistry = {
291
+ "geoservices-map-service": service,
292
+ "geoservices-map-layer": layer,
293
+ };
294
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
295
+ async query(request) {
296
+ ensureCapability(descriptor, caps, "query");
297
+ const params = toFeatureLayerRequest(request);
298
+ if (request?.aggregation) {
299
+ ensureCapability(descriptor, caps, "queryAggregate");
300
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
301
+ return aggregateResultFromUntyped(await layer.queryFeatures({ ...params, ...aggFields }), aggregateAlias);
302
+ }
303
+ const response = await layer.queryFeatures(params);
304
+ return featureLayerResultFromUntyped(response);
305
+ },
306
+ async queryAll(request) {
307
+ ensureCapability(descriptor, caps, "query");
308
+ const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
309
+ const features = await layer.queryFeaturesAll(params);
310
+ const typed = features.map((toTypedFeature));
311
+ const { features: limited, exceededTransferLimit } = applyQueryAllLimit(typed, request?.pagination?.limit);
312
+ return {
313
+ features: limited,
314
+ exceededTransferLimit,
315
+ totalCount: limited.length,
316
+ };
317
+ },
318
+ async queryAggregate(request) {
319
+ ensureCapability(descriptor, caps, "queryAggregate");
320
+ const params = toFeatureLayerRequest(request);
321
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
322
+ const response = await layer.queryFeatures({ ...params, ...aggFields });
323
+ return aggregateResultFromUntyped(response, aggregateAlias);
324
+ },
325
+ async queryExtent(request) {
326
+ ensureCapability(descriptor, caps, "queryExtent");
327
+ const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
328
+ return extractExtentEnvelope(response);
329
+ },
330
+ async *stream(request) {
331
+ ensureCapability(descriptor, caps, "stream");
332
+ const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
333
+ for await (const page of stream) {
334
+ yield {
335
+ features: page.map((toTypedFeature)),
336
+ exceededTransferLimit: false,
337
+ };
338
+ }
339
+ },
340
+ async queryObjectIds(request) {
341
+ ensureCapability(descriptor, caps, "queryObjectIds");
342
+ return layer.queryObjectIds(toFeatureLayerRequest(request));
343
+ },
344
+ async applyEdits() {
345
+ // MapServer is read-only; applyEdits exists only on the FeatureServer
346
+ // surface. Refuse rather than silently no-op so callers can branch.
347
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
348
+ },
349
+ async queryRelated(request) {
350
+ ensureCapability(descriptor, caps, "queryRelated");
351
+ const response = await layer.queryRelatedRecords(toRelatedRecordsRequest(request));
352
+ return canonicalRelatedResult(response);
353
+ },
354
+ attachments: unsupportedAttachmentApi(descriptor),
355
+ });
356
+ }
357
+ // ── OGC API Features ──────────────────────────────────────────
358
+ /**
359
+ * Adapter factory for an OGC API Features collection.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const dataset = createDataset({
364
+ * id: "addresses",
365
+ * client,
366
+ * sources: [{
367
+ * id: "addresses-ogc",
368
+ * protocol: "ogc-features",
369
+ * locator: { url: "https://your-honua-server.example", collectionId: "addresses" },
370
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"],
371
+ * }],
372
+ * });
373
+ * const result = await dataset.source("addresses-ogc")!.queryAll({
374
+ * where: "city = 'Honolulu'",
375
+ * pagination: { limit: 200 },
376
+ * });
377
+ * ```
378
+ */
379
+ export function ogcFeaturesSource(descriptor, client, policy) {
380
+ const { collectionId } = requireOgcLocator(descriptor);
381
+ const collection = new HonuaOgcFeatureCollection({ client, collectionId });
382
+ const root = new HonuaOgcFeatures({ client });
383
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"];
384
+ const adapterRegistry = {
385
+ "ogc-features": collection,
386
+ };
387
+ void root;
388
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
389
+ async query(request) {
390
+ ensureCapability(descriptor, caps, "query");
391
+ if (request?.aggregation) {
392
+ // OGC has no server-side aggregation; the adapter always aggregates
393
+ // client-side over the returned page. `queryAggregate` is therefore a
394
+ // degradable capability here.
395
+ ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
396
+ }
397
+ const response = await collection.items(toOgcRequest(request));
398
+ const features = response.features.map((toTypedFeatureFromOgc));
399
+ const totalCount = response.numberMatched;
400
+ const degraded = [];
401
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
402
+ if (request?.aggregation) {
403
+ const aggregateRows = clientSideAggregate(features, request.aggregation);
404
+ degraded.push({
405
+ capability: "queryAggregate",
406
+ reason: "OGC API Features does not expose server-side aggregation; aggregating client-side over the returned page.",
407
+ protocol: "ogc-features",
408
+ sourceId: descriptor.id,
409
+ });
410
+ return {
411
+ features,
412
+ exceededTransferLimit,
413
+ totalCount,
414
+ aggregateRows,
415
+ degraded,
416
+ };
417
+ }
418
+ return {
419
+ features,
420
+ exceededTransferLimit,
421
+ totalCount,
422
+ };
423
+ },
424
+ async queryAll(request) {
425
+ ensureCapability(descriptor, caps, "query");
426
+ const limit = request?.pagination?.limit;
427
+ // When the caller caps the result with `pagination.limit`, request
428
+ // `limit + 1` rows from the OGC helper (the lookahead row) so the
429
+ // adapter can stamp `exceededTransferLimit: true` when more records
430
+ // exist. Mirrors the GeoServices `withPagingBounds` + `applyQueryAllLimit`
431
+ // pattern so `queryAll({ pagination: { limit } })` has the same
432
+ // contract across protocols.
433
+ const ogcRequest = toOgcRequest(request);
434
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
435
+ ogcRequest.limit = limit + 1;
436
+ }
437
+ const all = await collection.itemsAll(withUnboundedMaxPages(ogcRequest));
438
+ const typed = all.map((toTypedFeatureFromOgc));
439
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
440
+ return {
441
+ features,
442
+ exceededTransferLimit,
443
+ totalCount: features.length,
444
+ };
445
+ },
446
+ async queryAggregate(request) {
447
+ // Always client-side (OGC has no server-side aggregation). Under `strict`
448
+ // the descriptor must advertise `queryAggregate`; under `degraded` the
449
+ // fallback runs unconditionally.
450
+ ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
451
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
452
+ const features = all.map((toTypedFeatureFromOgc));
453
+ return {
454
+ features,
455
+ exceededTransferLimit: false,
456
+ totalCount: features.length,
457
+ aggregateRows: clientSideAggregate(features, request.aggregation),
458
+ degraded: [
459
+ {
460
+ capability: "queryAggregate",
461
+ reason: "OGC API Features aggregation evaluated client-side over the materialized result set.",
462
+ protocol: "ogc-features",
463
+ sourceId: descriptor.id,
464
+ },
465
+ ],
466
+ };
467
+ },
468
+ async queryExtent(request) {
469
+ ensureCapabilityOrFallback(descriptor, caps, "queryExtent", policy);
470
+ // Collection-wide extent (no filters): the metadata bbox is accurate
471
+ // and avoids draining items. A filtered request must be computed
472
+ // client-side — the collection bbox would over-report the matching
473
+ // subset's extent. `outSr` also forces the items path so the extent
474
+ // is expressed in the caller's requested CRS; the metadata bbox is
475
+ // frozen in the collection's native CRS (typically CRS84) and would
476
+ // silently mislead `queryExtent({ outSr: 3857 })` callers.
477
+ if (!hasExtentFilter(request) && request?.outSr === undefined) {
478
+ const meta = await collection.metadata();
479
+ const bbox = meta.extent?.spatial?.bbox?.[0];
480
+ if (!bbox || bbox.length < 4)
481
+ return { extent: null };
482
+ const [xmin, ymin, xmax, ymax] = bbox;
483
+ return { extent: { xmin, ymin, xmax, ymax } };
484
+ }
485
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
486
+ return computeExtentFromOgcFeatures(all);
487
+ },
488
+ async *stream(request) {
489
+ ensureCapability(descriptor, caps, "stream");
490
+ const stream = collection.itemsStream(withUnboundedMaxPages(toOgcRequest(request)));
491
+ for await (const page of stream) {
492
+ yield {
493
+ features: page.map((toTypedFeatureFromOgc)),
494
+ exceededTransferLimit: false,
495
+ };
496
+ }
497
+ },
498
+ async queryObjectIds(request) {
499
+ ensureCapability(descriptor, caps, "queryObjectIds");
500
+ // OGC `/items` does not expose a server-side ids-only mode; drain the
501
+ // matching set and project the GeoJSON `id`. Callers that need a
502
+ // bounded scan should pass `pagination.limit`.
503
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcRequest(request)));
504
+ const ids = [];
505
+ for (const feature of all) {
506
+ if (feature.id !== undefined && feature.id !== null) {
507
+ ids.push(feature.id);
508
+ }
509
+ }
510
+ return ids;
511
+ },
512
+ async applyEdits(envelope) {
513
+ ensureCapability(descriptor, caps, "applyEdits");
514
+ const added = [];
515
+ const updated = [];
516
+ const deleted = [];
517
+ // OGC has no batch edit endpoint; edits fan out to per-item
518
+ // createItem / replaceItem / deleteItem. The canonical envelope's
519
+ // signal aborts every remaining request as soon as the caller
520
+ // cancels — each item call passes the same signal through.
521
+ const { signal } = envelope;
522
+ for (const add of envelope.adds ?? []) {
523
+ try {
524
+ const created = await collection.createItem({
525
+ feature: featureToGeoJsonFeature(add),
526
+ ...(signal ? { signal } : {}),
527
+ });
528
+ added.push({ id: created.id, success: true });
529
+ }
530
+ catch (err) {
531
+ added.push({ success: false, error: editErrorFromCatch(err) });
532
+ }
533
+ }
534
+ for (const update of envelope.updates ?? []) {
535
+ if (update.id === undefined || update.id === null) {
536
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
537
+ continue;
538
+ }
539
+ try {
540
+ await collection.replaceItem({
541
+ featureId: update.id,
542
+ feature: featureToGeoJsonFeature(update),
543
+ ...(signal ? { signal } : {}),
544
+ });
545
+ updated.push({ id: update.id, success: true });
546
+ }
547
+ catch (err) {
548
+ updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
549
+ }
550
+ }
551
+ for (const id of envelope.deletes ?? []) {
552
+ try {
553
+ await collection.deleteItem({ featureId: id, ...(signal ? { signal } : {}) });
554
+ deleted.push({ id, success: true });
555
+ }
556
+ catch (err) {
557
+ deleted.push({ id, success: false, error: editErrorFromCatch(err) });
558
+ }
559
+ }
560
+ return { added, updated, deleted };
561
+ },
562
+ async queryRelated() {
563
+ // OGC API Features has no related-records concept; refuse rather than
564
+ // silently return empty groups.
565
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
566
+ },
567
+ attachments: unsupportedAttachmentApi(descriptor),
568
+ });
569
+ }
570
+ // ── GeoServices Image Service ────────────────────────────────
571
+ /**
572
+ * Build a `Source` over an Esri-style ImageServer endpoint. The query
573
+ * surface drives the raster catalog (each row is a raster with footprint
574
+ * geometry); image export, identify, and tile operations live behind
575
+ * `Source.protocol("geoservices-image-service")`.
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * const dataset = createDataset({
580
+ * id: "imagery",
581
+ * client,
582
+ * sources: [{
583
+ * id: "naip",
584
+ * protocol: "geoservices-image-service",
585
+ * locator: { url: "https://your-honua-server.example", serviceId: "naip" },
586
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"],
587
+ * }],
588
+ * });
589
+ * const img = dataset.source("naip")!.protocol("geoservices-image-service");
590
+ * const png = await img?.exportImage({ bbox: [-158.5, 21.2, -157.6, 21.7], size: [512, 512] });
591
+ * ```
592
+ */
593
+ export function geoServicesImageSource(descriptor, client, policy) {
594
+ const { serviceId } = requireImageServiceLocator(descriptor);
595
+ const service = new HonuaImageService({ client, serviceId });
596
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"];
597
+ const adapterRegistry = {
598
+ "geoservices-image-service": service,
599
+ };
600
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
601
+ async query(request) {
602
+ ensureCapability(descriptor, caps, "query");
603
+ requireImageServerCompatibleQuery(request);
604
+ // ImageServer query returns the raster catalog. The GeoServices
605
+ // request shape is identical to FeatureServer query so we reuse
606
+ // `toFeatureLayerRequest` and dispatch through the service.
607
+ const response = await service.queryRasterCatalog(toFeatureLayerRequest(request));
608
+ return featureLayerResultFromUntyped(response);
609
+ },
610
+ async queryAll(request) {
611
+ ensureCapability(descriptor, caps, "query");
612
+ requireImageServerCompatibleQuery(request);
613
+ const baseParams = toFeatureLayerRequest(request);
614
+ const limit = request?.pagination?.limit;
615
+ // ImageServer's catalog endpoint supports `resultOffset`/`resultRecordCount`
616
+ // but the wrapper does not expose a `queryAllRasterCatalog` helper, so
617
+ // drain pages here. Mirror `withPagingBounds` + `applyQueryAllLimit` so
618
+ // an empty/short page or a `limit + 1` lookahead row stops the loop and
619
+ // stamps `exceededTransferLimit` correctly.
620
+ const startingOffset = typeof baseParams.resultOffset === "number" ? baseParams.resultOffset : 0;
621
+ const pageSize = typeof limit === "number" && limit > 0
622
+ ? Math.max(1, Math.min(limit, 2000))
623
+ : typeof baseParams.resultRecordCount === "number" && baseParams.resultRecordCount > 0
624
+ ? baseParams.resultRecordCount
625
+ : 2000;
626
+ const collected = [];
627
+ let lastFields;
628
+ const target = typeof limit === "number" && limit >= 0 ? limit + 1 : Number.POSITIVE_INFINITY;
629
+ for (let page = 0;; page += 1) {
630
+ const response = await service.queryRasterCatalog({
631
+ ...baseParams,
632
+ resultOffset: startingOffset + page * pageSize,
633
+ resultRecordCount: pageSize,
634
+ });
635
+ if (response.fields)
636
+ lastFields = response.fields;
637
+ const pageFeatures = response.features ?? [];
638
+ for (const feature of pageFeatures) {
639
+ collected.push(feature);
640
+ if (collected.length >= target)
641
+ break;
642
+ }
643
+ if (collected.length >= target)
644
+ break;
645
+ if (pageFeatures.length < pageSize)
646
+ break;
647
+ }
648
+ const typed = collected.map((toTypedFeature));
649
+ const { features: bounded, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
650
+ return {
651
+ features: bounded,
652
+ exceededTransferLimit,
653
+ totalCount: bounded.length,
654
+ ...(lastFields ? { fields: lastFields } : {}),
655
+ };
656
+ },
657
+ async queryAggregate() {
658
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
659
+ },
660
+ async queryExtent(request) {
661
+ ensureCapability(descriptor, caps, "queryExtent");
662
+ requireImageServerCompatibleQuery(request);
663
+ const response = await service.queryRasterCatalog(toExtentOnlyRequest(toFeatureLayerRequest(request)));
664
+ return extractExtentEnvelope(response);
665
+ },
666
+ // biome-ignore lint/correctness/useYield: ImageServer has no streaming raster-catalog mode; this generator refuses iteration rather than silently emit a single page
667
+ async *stream() {
668
+ throw new HonuaCapabilityNotSupportedError("stream", descriptor.protocol, descriptor.id);
669
+ },
670
+ async queryObjectIds(request) {
671
+ ensureCapability(descriptor, caps, "queryObjectIds");
672
+ requireImageServerCompatibleQuery(request);
673
+ return service.queryRasterCatalogObjectIds(toFeatureLayerRequest(request));
674
+ },
675
+ async applyEdits() {
676
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
677
+ },
678
+ async queryRelated() {
679
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
680
+ },
681
+ attachments: unsupportedAttachmentApi(descriptor),
682
+ });
683
+ }
684
+ // ── GeoServices Geometry Service ─────────────────────────────
685
+ /**
686
+ * Build a `Source` over an Esri-style Geometry Service endpoint. Geometry
687
+ * Service is a stateless utility — it does not host features — so the
688
+ * query family throws `HonuaCapabilityNotSupportedError` and operations
689
+ * (buffer, project, simplify, etc.) live behind
690
+ * `Source.protocol("geoservices-geometry-service")`.
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * const geom = dataset.source("geom")!.protocol("geoservices-geometry-service");
695
+ * const projected = await geom?.project({
696
+ * inSr: 4326,
697
+ * outSr: 3857,
698
+ * geometries: [{ x: -158, y: 21 }],
699
+ * });
700
+ * ```
701
+ */
702
+ export function geoServicesGeometryServiceSource(descriptor, client, policy) {
703
+ const service = new HonuaGeometryService({ client });
704
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-geometry-service"];
705
+ const adapterRegistry = {
706
+ "geoservices-geometry-service": service,
707
+ };
708
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
709
+ }
710
+ // ── GeoServices GP Service ───────────────────────────────────
711
+ /**
712
+ * Build a `Source` over an Esri-style GP (geoprocessing) service. GP
713
+ * services run async tasks rather than hosting features; the canonical
714
+ * feature surface throws and task submission / status / result lookup
715
+ * live behind `Source.protocol("geoservices-gp-service")`.
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * const gp = dataset.source("buffer-task")!.protocol("geoservices-gp-service");
720
+ * const job = await gp!.submitJob({ inputs: { distance: 500 } });
721
+ * const result = await gp!.awaitJobResult(job.jobId);
722
+ * ```
723
+ */
724
+ export function geoServicesGPServiceSource(descriptor, client, policy) {
725
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-gp-service"];
726
+ const { serviceId, taskName } = requireGPServiceLocator(descriptor, caps);
727
+ const service = new HonuaGeoprocessingService({
728
+ client,
729
+ serviceId,
730
+ taskName,
731
+ });
732
+ const adapterRegistry = {
733
+ "geoservices-gp-service": service,
734
+ };
735
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
736
+ }
737
+ // ── OGC API Tiles ─────────────────────────────────────────────
738
+ /**
739
+ * Render-only Source adapter for OGC API Tiles. The query family throws
740
+ * `HonuaCapabilityNotSupportedError` (the conformance class is
741
+ * tile-fetch, not feature-query); rendering integrations consume the
742
+ * tileset through `Source.protocol("ogc-tiles")` to reach the underlying
743
+ * runtime class.
744
+ *
745
+ * @example
746
+ * ```ts
747
+ * const tiles = dataset.source("ogc-tileset")!.protocol("ogc-tiles");
748
+ * const bytes = await tiles!.tile({ tileMatrix: "WebMercatorQuad", tileRow: 5, tileCol: 8 });
749
+ * ```
750
+ */
751
+ export function ogcTilesSource(descriptor, client, policy) {
752
+ const { collectionId, tileMatrixSetId } = requireOgcTilesLocator(descriptor);
753
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-tiles"];
754
+ // Descriptors without a tileMatrixSetId cannot construct a usable
755
+ // HonuaOgcTileset (every tile route requires `tileMatrixSetId`). Expose
756
+ // the root HonuaOgcTiles adapter instead so callers can discover the
757
+ // tilesets the server advertises for the collection before binding one.
758
+ const adapter = tileMatrixSetId !== undefined && tileMatrixSetId !== ""
759
+ ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId })
760
+ : new HonuaOgcTiles({ client });
761
+ const adapterRegistry = {
762
+ "ogc-tiles": adapter,
763
+ };
764
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
765
+ }
766
+ // ── OGC API Maps ──────────────────────────────────────────────
767
+ /**
768
+ * Render-only Source adapter for OGC API Maps. Same shape as the Tiles
769
+ * adapter — `Source.protocol("ogc-maps")` exposes the runtime class for
770
+ * server-rendered map images; the canonical query family throws.
771
+ *
772
+ * @example
773
+ * ```ts
774
+ * const maps = dataset.source("ogc-map")!.protocol("ogc-maps");
775
+ * const png = await maps!.map({ bbox: [-158, 21, -157, 22], width: 512, height: 512 });
776
+ * ```
777
+ */
778
+ export function ogcMapsSource(descriptor, client, policy) {
779
+ const root = new HonuaOgcMaps({ client });
780
+ const adapterRegistry = {};
781
+ if (descriptor.locator.collectionId !== undefined) {
782
+ adapterRegistry["ogc-maps"] = new HonuaOgcCollectionMap({
783
+ client,
784
+ collectionId: descriptor.locator.collectionId,
785
+ styleId: descriptor.locator.styleId,
786
+ });
787
+ }
788
+ else {
789
+ adapterRegistry["ogc-maps"] = root;
790
+ }
791
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-maps"];
792
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
793
+ }
794
+ // ── OGC API Records ───────────────────────────────────────────
795
+ /**
796
+ * Metadata-catalog Source adapter for OGC API Records. The canonical query
797
+ * family searches one catalog (`locator.collectionId`, the Records
798
+ * collection/catalog id) and returns record documents as typed features.
799
+ * Records-specific search affordances (`q`, `type`, `externalIds`,
800
+ * `profile`, raw HTML/JSON access) live on `Source.protocol("ogc-records")`.
801
+ *
802
+ * @example
803
+ * ```ts
804
+ * const records = dataset.source("catalog")!;
805
+ * const result = await records.query({ where: "type = 'dataset' AND q = 'parcels'" });
806
+ * ```
807
+ */
808
+ export function ogcRecordsSource(descriptor, client, policy) {
809
+ const { collectionId } = requireOgcRecordsLocator(descriptor);
810
+ const collection = new HonuaOgcRecordCollection({ client, collectionId });
811
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-records"];
812
+ const adapterRegistry = {
813
+ "ogc-records": collection,
814
+ };
815
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
816
+ async query(request) {
817
+ ensureCapability(descriptor, caps, "query");
818
+ if (request?.aggregation) {
819
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
820
+ }
821
+ const response = await collection.search(toOgcRecordsRequest(request));
822
+ const features = (response.features ?? []).map((toTypedFeatureFromOgcRecord));
823
+ const totalCount = response.numberMatched;
824
+ return {
825
+ features,
826
+ exceededTransferLimit: totalCount !== undefined && features.length < totalCount,
827
+ ...(totalCount !== undefined ? { totalCount } : {}),
828
+ };
829
+ },
830
+ async queryAll(request) {
831
+ ensureCapability(descriptor, caps, "query");
832
+ const limit = request?.pagination?.limit;
833
+ const records = await collection.searchAll({
834
+ ...toOgcRecordsRequest(request),
835
+ ...withPagingBounds({}, limit),
836
+ });
837
+ const typed = records.map((toTypedFeatureFromOgcRecord));
838
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
839
+ return {
840
+ features,
841
+ exceededTransferLimit,
842
+ totalCount: features.length,
843
+ };
844
+ },
845
+ async queryAggregate() {
846
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
847
+ },
848
+ async queryExtent() {
849
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
850
+ },
851
+ async *stream(request) {
852
+ ensureCapability(descriptor, caps, "stream");
853
+ const limit = request?.pagination?.limit;
854
+ const stream = collection.searchStream({
855
+ ...toOgcRecordsRequest(request),
856
+ pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
857
+ maxPages: Number.MAX_SAFE_INTEGER,
858
+ });
859
+ for await (const page of stream) {
860
+ yield {
861
+ features: page.map((toTypedFeatureFromOgcRecord)),
862
+ exceededTransferLimit: false,
863
+ };
864
+ }
865
+ },
866
+ async queryObjectIds(request) {
867
+ ensureCapability(descriptor, caps, "queryObjectIds");
868
+ const limit = request?.pagination?.limit;
869
+ const records = await collection.searchAll({
870
+ ...toOgcRecordsRequest(request),
871
+ ...withPagingBounds({}, limit),
872
+ });
873
+ const ids = [];
874
+ for (const record of records) {
875
+ if (record.id !== undefined && record.id !== null) {
876
+ ids.push(record.id);
877
+ }
878
+ }
879
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
880
+ return ids.slice(0, limit);
881
+ }
882
+ return ids;
883
+ },
884
+ async applyEdits() {
885
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
886
+ },
887
+ async queryRelated() {
888
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
889
+ },
890
+ attachments: unsupportedAttachmentApi(descriptor),
891
+ });
892
+ }
893
+ // ── WMS 1.3 ───────────────────────────────────────────────────
894
+ /**
895
+ * `Source` adapter for first-party WMS 1.3.0 services. The render path
896
+ * routes through `HonuaWms` / `HonuaWmsLayer` (both of which sit
897
+ * behind the canonical `Source.protocol("wms" | "wms-layer")` escape
898
+ * hatch); `Source.query()` translates a point spatial filter into a
899
+ * `GetFeatureInfo` call. Non-point queries throw
900
+ * `HonuaCapabilityNotSupportedError` so the canonical query envelope
901
+ * stays honest — multi-pixel feature info lives on
902
+ * `Source.protocol("wms").featureInfo()`.
903
+ */
904
+ /**
905
+ * Adapter factory for a WMS 1.3.0 endpoint.
906
+ *
907
+ * The canonical `Source.query()` is implemented through point-only
908
+ * `GetFeatureInfo`. Reach `Source.protocol("wms")` for raw `GetMap` /
909
+ * `GetLegendGraphic` access (with TIME / ELEVATION dimension handling and
910
+ * CRS axis-order swap per WMS 1.3 §6.7.3.2).
911
+ *
912
+ * @example
913
+ * ```ts
914
+ * const wms = dataset.source("usgs-imagery")!.protocol("wms");
915
+ * const png = await wms!.getMap({
916
+ * layers: ["topo"], bbox: [-158, 21, -157, 22], crs: "EPSG:4326",
917
+ * width: 512, height: 512,
918
+ * });
919
+ * ```
920
+ */
921
+ export function wmsSource(descriptor, client, policy) {
922
+ const { serviceId } = requireWmsLocator(descriptor);
923
+ const layerName = descriptor.locator.typeName;
924
+ const styleId = descriptor.locator.styleId;
925
+ const root = new HonuaWms({ client, serviceId });
926
+ const adapterRegistry = { wms: root };
927
+ // `HonuaWmsLayer` is a single-layer handle (its `describe()` resolves
928
+ // exactly one `<Layer>` from the parsed Capabilities). Multi-layer
929
+ // composites (`LAYERS=a,b`) must stay on the service-level `wms`
930
+ // handle and use `featureInfo()` directly; registering them as a
931
+ // `wms-layer` would silently mis-route `describe()` / `stylesIn()` /
932
+ // `legend()` to a single layer name that does not exist on the wire.
933
+ const parsedLayers = parseWmsLayerNames(layerName);
934
+ if (parsedLayers.length === 1) {
935
+ const layerOpts = {
936
+ client,
937
+ serviceId,
938
+ layerName: parsedLayers[0],
939
+ };
940
+ if (typeof styleId === "string")
941
+ layerOpts.defaultStyleId = styleId;
942
+ adapterRegistry["wms-layer"] = new HonuaWmsLayer(layerOpts);
943
+ }
944
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
945
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
946
+ async query(request) {
947
+ ensureCapability(descriptor, caps, "query");
948
+ requireWmsCompatibleQuery(descriptor, request);
949
+ const layers = wmsRequireLayers(descriptor, layerName);
950
+ const { x: px, y: py, crs } = wmsExtractPointFromQuery(request, descriptor);
951
+ const bboxRadius = 0.0001; // tiny envelope around the point keeps the request a 1×1 image.
952
+ const widthHeight = 1;
953
+ const featureCount = request?.pagination?.limit;
954
+ const response = await client.getWmsFeatureInfo({
955
+ serviceId,
956
+ layers,
957
+ ...(styleId !== undefined ? { styles: [styleId] } : {}),
958
+ queryLayers: layers,
959
+ crs,
960
+ bbox: [px - bboxRadius, py - bboxRadius, px + bboxRadius, py + bboxRadius],
961
+ width: widthHeight,
962
+ height: widthHeight,
963
+ i: 0,
964
+ j: 0,
965
+ infoFormat: "application/json",
966
+ ...(featureCount !== undefined ? { featureCount } : {}),
967
+ ...(request?.signal ? { signal: request.signal } : {}),
968
+ });
969
+ return {
970
+ features: response.features ?? [],
971
+ exceededTransferLimit: false,
972
+ };
973
+ },
974
+ async queryAll(request) {
975
+ ensureCapability(descriptor, caps, "query");
976
+ // GetFeatureInfo returns a single response set; queryAll degenerates
977
+ // to query() because there is no paging on the wire.
978
+ const result = await this.query(request);
979
+ return result;
980
+ },
981
+ async queryAggregate() {
982
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
983
+ },
984
+ async queryExtent() {
985
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
986
+ },
987
+ // biome-ignore lint/correctness/useYield: WMS exposes only single-shot GetFeatureInfo, which is delivered through query()
988
+ async *stream() {
989
+ throw new HonuaCapabilityNotSupportedError("stream", descriptor.protocol, descriptor.id);
990
+ },
991
+ async queryObjectIds() {
992
+ throw new HonuaCapabilityNotSupportedError("queryObjectIds", descriptor.protocol, descriptor.id);
993
+ },
994
+ async applyEdits() {
995
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
996
+ },
997
+ async queryRelated() {
998
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
999
+ },
1000
+ attachments: unsupportedAttachmentApi(descriptor),
1001
+ });
1002
+ }
1003
+ // ── WMTS 1.0 ──────────────────────────────────────────────────
1004
+ /**
1005
+ * Render-only `Source` adapter for first-party WMTS 1.0.0 services.
1006
+ * `Source.query()` throws because WMTS GetFeatureInfo is keyed on tile
1007
+ * pixels (not a canonical spatial filter); raw access lives on
1008
+ * `Source.protocol("wmts" | "wmts-layer" | "wmts-tileset")`.
1009
+ */
1010
+ /**
1011
+ * Adapter factory for a WMTS 1.0.0 endpoint. Render-only; `Source.query()`
1012
+ * throws `HonuaCapabilityNotSupportedError`. Reach `Source.protocol("wmts")`
1013
+ * for RESTful + KVP tile fetch and capabilities.
1014
+ *
1015
+ * @example
1016
+ * ```ts
1017
+ * const wmts = dataset.source("basemap-tiles")!.protocol("wmts");
1018
+ * const tile = await wmts!.tile({ tileMatrixSet: "WebMercatorQuad", tileMatrix: "8", tileRow: 5, tileCol: 8 });
1019
+ * ```
1020
+ */
1021
+ export function wmtsSource(descriptor, client, policy) {
1022
+ const { serviceId } = requireWmtsLocator(descriptor);
1023
+ const layerName = descriptor.locator.typeName;
1024
+ const tileMatrixSetId = descriptor.locator.tileMatrixSetId ?? "WebMercatorQuad";
1025
+ const styleId = descriptor.locator.styleId ?? "default";
1026
+ const root = new HonuaWmts({ client, serviceId });
1027
+ const adapterRegistry = { wmts: root };
1028
+ if (typeof layerName === "string" && layerName.length > 0) {
1029
+ adapterRegistry["wmts-layer"] = new HonuaWmtsLayer({
1030
+ client,
1031
+ serviceId,
1032
+ layerName,
1033
+ defaultStyleId: styleId,
1034
+ defaultTileMatrixSetId: tileMatrixSetId,
1035
+ });
1036
+ adapterRegistry["wmts-tileset"] = new HonuaWmtsTileset({
1037
+ client,
1038
+ serviceId,
1039
+ layerName,
1040
+ styleId,
1041
+ tileMatrixSetId,
1042
+ });
1043
+ }
1044
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1045
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
1046
+ }
1047
+ // ── STAC API ──────────────────────────────────────────────────
1048
+ /**
1049
+ * Adapter factory for a STAC API search endpoint.
1050
+ *
1051
+ * The canonical `Source.query()` runs a `POST /search` against the STAC root,
1052
+ * with `Query.spatialFilter` (e.g. an `envelope(...)` bounding box) and
1053
+ * `Query.where` translated into STAC's `bbox` / `datetime` / `filter`
1054
+ * parameters.
1055
+ *
1056
+ * @example
1057
+ * ```ts
1058
+ * import { envelope } from "@honua/sdk-js";
1059
+ *
1060
+ * const dataset = createDataset({
1061
+ * id: "imagery",
1062
+ * client,
1063
+ * sources: [{
1064
+ * id: "stac-search",
1065
+ * protocol: "stac",
1066
+ * locator: { url: "https://your-honua-server.example/stac" },
1067
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.stac,
1068
+ * }],
1069
+ * });
1070
+ * const result = await dataset.source("stac-search")!.query({
1071
+ * where: "collections IN ('landsat-c2-l2')",
1072
+ * spatialFilter: envelope(-158.5, 21.2, -157.6, 21.7),
1073
+ * });
1074
+ * ```
1075
+ */
1076
+ export function stacSearchSource(descriptor, client, policy) {
1077
+ const stac = new HonuaStacSearch({ client });
1078
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.stac;
1079
+ const collectionScope = descriptor.locator.collectionId;
1080
+ const adapterRegistry = {
1081
+ stac,
1082
+ };
1083
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
1084
+ async query(request) {
1085
+ ensureCapability(descriptor, caps, "query");
1086
+ if (request?.aggregation) {
1087
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1088
+ }
1089
+ const stacRequest = toStacRequest(request, collectionScope);
1090
+ const response = await stac.search(stacRequest);
1091
+ const features = (response.features ?? []).map((toTypedFeatureFromStac));
1092
+ const totalCount = response.numberMatched ?? response.context?.matched;
1093
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
1094
+ return {
1095
+ features,
1096
+ exceededTransferLimit,
1097
+ totalCount,
1098
+ };
1099
+ },
1100
+ async queryAll(request) {
1101
+ ensureCapability(descriptor, caps, "query");
1102
+ const limit = request?.pagination?.limit;
1103
+ const items = await stac.searchAll({
1104
+ ...toStacRequest(request, collectionScope),
1105
+ ...withPagingBounds({}, limit),
1106
+ });
1107
+ const typed = items.map((toTypedFeatureFromStac));
1108
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
1109
+ return {
1110
+ features,
1111
+ exceededTransferLimit,
1112
+ totalCount: features.length,
1113
+ };
1114
+ },
1115
+ async queryAggregate() {
1116
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1117
+ },
1118
+ async queryExtent() {
1119
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
1120
+ },
1121
+ async *stream(request) {
1122
+ ensureCapability(descriptor, caps, "stream");
1123
+ const limit = request?.pagination?.limit;
1124
+ const stream = stac.searchStream({
1125
+ ...toStacRequest(request, collectionScope),
1126
+ pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
1127
+ maxPages: Number.MAX_SAFE_INTEGER,
1128
+ });
1129
+ for await (const page of stream) {
1130
+ yield {
1131
+ features: page.map((toTypedFeatureFromStac)),
1132
+ exceededTransferLimit: false,
1133
+ };
1134
+ }
1135
+ },
1136
+ async queryObjectIds(request) {
1137
+ ensureCapability(descriptor, caps, "queryObjectIds");
1138
+ // STAC `/search` does not expose a server-side ids-only mode; drain
1139
+ // the matching items and project the GeoJSON `id`. `pagination.limit`
1140
+ // routes through `withPagingBounds` so the underlying searchAll
1141
+ // fetches at most `limit + 1` rows, never a full-catalog scan.
1142
+ const limit = request?.pagination?.limit;
1143
+ const items = await stac.searchAll({
1144
+ ...toStacRequest(request, collectionScope),
1145
+ ...withPagingBounds({}, limit),
1146
+ });
1147
+ const ids = [];
1148
+ for (const item of items) {
1149
+ if (item.id !== undefined && item.id !== null) {
1150
+ ids.push(item.id);
1151
+ }
1152
+ }
1153
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
1154
+ return ids.slice(0, limit);
1155
+ }
1156
+ return ids;
1157
+ },
1158
+ async applyEdits() {
1159
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
1160
+ },
1161
+ async queryRelated() {
1162
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
1163
+ },
1164
+ attachments: unsupportedAttachmentApi(descriptor),
1165
+ });
1166
+ }
1167
+ // ── WFS 2.0 ───────────────────────────────────────────────────
1168
+ /**
1169
+ * Threshold above which the WFS adapter switches to POST GetFeature with a
1170
+ * `<fes:Filter>` body. URL budget under 7000 chars stays GET-friendly; longer
1171
+ * filters are routed through POST so we do not stress middleboxes that
1172
+ * trim long query strings. The threshold is intentionally a single
1173
+ * constant we can revise after telemetry lands.
1174
+ */
1175
+ const WFS_GET_FILTER_BUDGET = 7000;
1176
+ const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
1177
+ /**
1178
+ * Adapter factory for a WFS 2.0 endpoint.
1179
+ *
1180
+ * `Query.where` and `Query.spatialFilter` compile to FES 2.0; GeoJSON is
1181
+ * preferred over GML via `OperationsMetadata` negotiation. `applyEdits()`
1182
+ * builds `<wfs:Transaction>` bodies. Reach `Source.protocol("wfs")` for raw
1183
+ * GML / `LockFeature` / stored-query access.
1184
+ *
1185
+ * @example
1186
+ * ```ts
1187
+ * const dataset = createDataset({
1188
+ * id: "wfs-parcels",
1189
+ * client,
1190
+ * sources: [{
1191
+ * id: "parcels",
1192
+ * protocol: "wfs",
1193
+ * locator: { url: "https://your-honua-server.example/wfs", typeName: "ns:Parcels" },
1194
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.wfs,
1195
+ * }],
1196
+ * });
1197
+ * const result = await dataset.source("parcels")!.queryAll({
1198
+ * where: "STATUS = 'ACTIVE'",
1199
+ * pagination: { limit: 500 },
1200
+ * });
1201
+ * ```
1202
+ */
1203
+ export function wfsSource(descriptor, client, policy) {
1204
+ const { url, typeName, featureNamespace, srsName: wfsSrsName } = requireWfsLocator(descriptor);
1205
+ const root = new HonuaWfs({ client, endpointUrl: url });
1206
+ const featureType = new HonuaWfsFeatureType({ root, typeName });
1207
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs;
1208
+ void policy;
1209
+ const adapterRegistry = {
1210
+ wfs: featureType,
1211
+ };
1212
+ /**
1213
+ * Negotiated output format cache. Populated lazily on the first
1214
+ * capability-gated call so a no-op `createDataset({ sources: [...wfs] })`
1215
+ * does not issue network traffic.
1216
+ */
1217
+ let cachedFormat;
1218
+ async function negotiateJsonOrThrow() {
1219
+ if (cachedFormat)
1220
+ return cachedFormat;
1221
+ const snapshot = await root.capabilities();
1222
+ const choice = root.negotiateOutputFormat(snapshot);
1223
+ if (!choice) {
1224
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
1225
+ }
1226
+ if (choice.kind !== "json") {
1227
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, `${descriptor.id} (server advertises only ${choice.format}; reach raw GML through Source.protocol("wfs"))`);
1228
+ }
1229
+ cachedFormat = choice;
1230
+ return choice;
1231
+ }
1232
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
1233
+ async query(request) {
1234
+ ensureCapability(descriptor, caps, "query");
1235
+ if (request?.aggregation) {
1236
+ // WFS 2.0 has no server-side aggregation; refuse rather than ship a
1237
+ // silent partial result.
1238
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1239
+ }
1240
+ // `pagination.limit === 0` is an explicit zero cap, not "unbounded";
1241
+ // short-circuit before the wire call so we do not silently widen to the
1242
+ // server's default page size.
1243
+ if (request?.pagination?.limit === 0) {
1244
+ return { features: [], exceededTransferLimit: false };
1245
+ }
1246
+ const choice = await negotiateJsonOrThrow();
1247
+ const json = await runGetFeatureJson(featureType, typeName, choice, request, request?.pagination?.limit);
1248
+ return resultFromGeoJson(json);
1249
+ },
1250
+ async queryAll(request) {
1251
+ ensureCapability(descriptor, caps, "query");
1252
+ const choice = await negotiateJsonOrThrow();
1253
+ const limit = request?.pagination?.limit;
1254
+ const target = typeof limit === "number" && limit >= 0 ? limit + 1 : Number.POSITIVE_INFINITY;
1255
+ const collected = [];
1256
+ let totalCount;
1257
+ let offset = request?.pagination?.offset ?? 0;
1258
+ // Mirror `withPagingBounds`: a finite limit (including 0) sets the
1259
+ // lookahead page size to `limit + 1` so we can stamp
1260
+ // `exceededTransferLimit` without overfetching a full default page.
1261
+ const pageSize = typeof limit === "number" && limit >= 0 ? Math.max(1, Math.min(limit + 1, 2000)) : 2000;
1262
+ while (collected.length < target) {
1263
+ const pageRequest = {
1264
+ ...(request ?? {}),
1265
+ pagination: { offset, limit: pageSize },
1266
+ };
1267
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1268
+ const result = resultFromGeoJson(json);
1269
+ if (result.totalCount !== undefined)
1270
+ totalCount = result.totalCount;
1271
+ collected.push(...result.features);
1272
+ if (result.features.length < pageSize)
1273
+ break;
1274
+ offset += result.features.length;
1275
+ }
1276
+ const { features, exceededTransferLimit } = applyQueryAllLimit(collected, limit);
1277
+ return {
1278
+ features,
1279
+ exceededTransferLimit,
1280
+ totalCount: totalCount ?? features.length,
1281
+ };
1282
+ },
1283
+ async queryAggregate() {
1284
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1285
+ },
1286
+ async queryExtent(request) {
1287
+ ensureCapability(descriptor, caps, "queryExtent");
1288
+ // Unfiltered queryExtent: prefer the per-feature-type WGS84BoundingBox
1289
+ // from GetCapabilities so we avoid an extra HTTP request entirely.
1290
+ if (!hasExtentFilter(request) && request?.outSr === undefined) {
1291
+ const snapshot = await root.capabilities();
1292
+ const ft = snapshot.featureTypes.find((entry) => entry.name === typeName);
1293
+ if (ft?.wgs84BoundingBox) {
1294
+ return { extent: { ...ft.wgs84BoundingBox } };
1295
+ }
1296
+ }
1297
+ // Filtered request: drain every matching page so the returned extent
1298
+ // covers all features the filter resolves to, not just the first
1299
+ // server-default page. Caller pagination is intentionally ignored —
1300
+ // queryExtent is a "what bbox holds the matching set" question.
1301
+ const choice = await negotiateJsonOrThrow();
1302
+ const drainPageSize = 2000;
1303
+ const extentRequest = toWfsExtentDrainRequest(request);
1304
+ let xmin = Number.POSITIVE_INFINITY;
1305
+ let ymin = Number.POSITIVE_INFINITY;
1306
+ let xmax = Number.NEGATIVE_INFINITY;
1307
+ let ymax = Number.NEGATIVE_INFINITY;
1308
+ let saw = false;
1309
+ let count = 0;
1310
+ let offset = 0;
1311
+ while (true) {
1312
+ const pageRequest = {
1313
+ ...extentRequest,
1314
+ pagination: { offset, limit: drainPageSize },
1315
+ };
1316
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, drainPageSize);
1317
+ const page = computeExtentFromFeatureCollection(json);
1318
+ count += page.count;
1319
+ if (page.extent) {
1320
+ if (page.extent.xmin < xmin)
1321
+ xmin = page.extent.xmin;
1322
+ if (page.extent.ymin < ymin)
1323
+ ymin = page.extent.ymin;
1324
+ if (page.extent.xmax > xmax)
1325
+ xmax = page.extent.xmax;
1326
+ if (page.extent.ymax > ymax)
1327
+ ymax = page.extent.ymax;
1328
+ saw = true;
1329
+ }
1330
+ if (page.count < drainPageSize)
1331
+ break;
1332
+ offset += page.count;
1333
+ }
1334
+ return saw ? { extent: { xmin, ymin, xmax, ymax }, count } : { extent: null, count };
1335
+ },
1336
+ async *stream(request) {
1337
+ ensureCapability(descriptor, caps, "stream");
1338
+ // WFS 2.0 has no native streaming verb; iterate pages over GetFeature
1339
+ // and yield each as a Result. Reuses the same negotiation as `query`.
1340
+ const limit = request?.pagination?.limit;
1341
+ // `pagination.limit === 0` is an explicit zero cap; yield nothing
1342
+ // rather than silently fall back to the default 2000-row page size.
1343
+ if (limit === 0)
1344
+ return;
1345
+ const choice = await negotiateJsonOrThrow();
1346
+ const pageSize = typeof limit === "number" && limit > 0 ? Math.max(1, limit) : 2000;
1347
+ let offset = request?.pagination?.offset ?? 0;
1348
+ while (true) {
1349
+ const pageRequest = {
1350
+ ...(request ?? {}),
1351
+ pagination: { offset, limit: pageSize },
1352
+ };
1353
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1354
+ const result = resultFromGeoJson(json);
1355
+ if (result.features.length === 0)
1356
+ break;
1357
+ yield {
1358
+ features: result.features,
1359
+ exceededTransferLimit: false,
1360
+ };
1361
+ if (result.features.length < pageSize)
1362
+ break;
1363
+ offset += result.features.length;
1364
+ }
1365
+ },
1366
+ async queryObjectIds(request) {
1367
+ ensureCapability(descriptor, caps, "queryObjectIds");
1368
+ // WFS does not expose a server-side ids-only mode that interoperates
1369
+ // across implementations; drain the matching set across pages and
1370
+ // project the GeoJSON `id`. `Query.pagination.limit` bounds the scan
1371
+ // as a global cap (not a per-page count) so callers can stop the drain
1372
+ // without learning the server's default page size. A finite limit
1373
+ // (including 0) is honored as a real cap; only `undefined` / negative
1374
+ // means "drain everything".
1375
+ const requestedLimit = request?.pagination?.limit;
1376
+ if (requestedLimit === 0)
1377
+ return [];
1378
+ const choice = await negotiateJsonOrThrow();
1379
+ const limitCap = typeof requestedLimit === "number" && requestedLimit > 0 ? requestedLimit : undefined;
1380
+ const drainPageSize = 2000;
1381
+ const ids = [];
1382
+ let offset = request?.pagination?.offset ?? 0;
1383
+ const idsRequest = toWfsObjectIdsDrainRequest(request);
1384
+ while (true) {
1385
+ const remainingCap = limitCap !== undefined ? Math.max(0, limitCap - ids.length) : undefined;
1386
+ if (remainingCap === 0)
1387
+ break;
1388
+ const pageSize = remainingCap !== undefined ? Math.min(drainPageSize, remainingCap) : drainPageSize;
1389
+ const pageRequest = {
1390
+ ...idsRequest,
1391
+ pagination: { offset, limit: pageSize },
1392
+ };
1393
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1394
+ const collection = json;
1395
+ const features = collection.features ?? [];
1396
+ for (const feature of features) {
1397
+ if (feature.id !== undefined && feature.id !== null) {
1398
+ ids.push(feature.id);
1399
+ if (limitCap !== undefined && ids.length >= limitCap)
1400
+ break;
1401
+ }
1402
+ }
1403
+ if (limitCap !== undefined && ids.length >= limitCap)
1404
+ break;
1405
+ if (features.length < pageSize)
1406
+ break;
1407
+ offset += features.length;
1408
+ }
1409
+ return ids;
1410
+ },
1411
+ async applyEdits(envelope) {
1412
+ ensureCapability(descriptor, caps, "applyEdits");
1413
+ // CanonicalFeature.id is required for updates because each <wfs:Update>
1414
+ // is filtered by `<fes:ResourceId>`; without an id the block would
1415
+ // mass-update every feature in the type. Mirror the OGC adapter's
1416
+ // per-item guard: push a failure outcome for each malformed update and
1417
+ // build the transaction with only the valid ones.
1418
+ const malformedUpdateIndices = new Set();
1419
+ const validUpdates = [];
1420
+ for (const [idx, update] of (envelope.updates ?? []).entries()) {
1421
+ if (update.id === undefined || update.id === null) {
1422
+ malformedUpdateIndices.add(idx);
1423
+ }
1424
+ else {
1425
+ validUpdates.push(update);
1426
+ }
1427
+ }
1428
+ const adds = envelope.adds ?? [];
1429
+ const deletes = envelope.deletes ?? [];
1430
+ let summary;
1431
+ if (adds.length === 0 && validUpdates.length === 0 && deletes.length === 0) {
1432
+ // Nothing to send — every operation was either absent or malformed.
1433
+ // Skip the wire round-trip entirely so the server never sees an
1434
+ // unaddressed transaction.
1435
+ summary = { totalInserted: 0, totalUpdated: 0, totalDeleted: 0, insertResults: [] };
1436
+ }
1437
+ else {
1438
+ const filtered = { ...envelope, updates: validUpdates };
1439
+ const body = buildTransactionBody(typeName, filtered, featureNamespace, wfsSrsName);
1440
+ const transactionOptions = { body };
1441
+ if (envelope.signal)
1442
+ transactionOptions.signal = envelope.signal;
1443
+ summary = await featureType.transaction(transactionOptions);
1444
+ }
1445
+ return canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIndices);
1446
+ },
1447
+ async queryRelated() {
1448
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
1449
+ },
1450
+ attachments: unsupportedAttachmentApi(descriptor),
1451
+ });
1452
+ }
1453
+ function requireWfsLocator(descriptor) {
1454
+ const { url, typeName, featureNamespace, srsName } = descriptor.locator;
1455
+ if (typeof url !== "string" || url.length === 0) {
1456
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.url`);
1457
+ }
1458
+ if (typeof typeName !== "string" || typeName.length === 0) {
1459
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.typeName`);
1460
+ }
1461
+ if (featureNamespace !== undefined && (typeof featureNamespace !== "string" || featureNamespace.length === 0)) {
1462
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) locator.featureNamespace must be a non-empty string`);
1463
+ }
1464
+ return {
1465
+ url,
1466
+ typeName,
1467
+ featureNamespace: typeof featureNamespace === "string" ? featureNamespace : undefined,
1468
+ srsName: wfsSrsNameFromOutSr(srsName),
1469
+ };
1470
+ }
1471
+ function toWfsExtentDrainRequest(request) {
1472
+ // The drain computes a bbox over geometries, so a caller-supplied
1473
+ // `returnGeometry: false` would suppress geometry on every page and
1474
+ // leave the extent empty. Strip it alongside `outFields` and
1475
+ // `pagination` so the drain always sees the full geometry-bearing
1476
+ // response regardless of the caller's projection / paging / geometry
1477
+ // intent.
1478
+ const { outFields: _outFields, pagination: _pagination, returnGeometry: _returnGeometry, ...extentRequest } = request ?? {};
1479
+ void _outFields;
1480
+ void _pagination;
1481
+ void _returnGeometry;
1482
+ return extentRequest;
1483
+ }
1484
+ /**
1485
+ * `queryObjectIds` reads the GeoJSON `id` from each drained feature, so
1486
+ * neither `outFields` nor `returnGeometry` affects the result. Strip
1487
+ * both before issuing each `GetFeature` page so a caller-supplied
1488
+ * `returnGeometry: false` cannot throw the runGetFeatureJson guard
1489
+ * (which refuses to suppress geometry without an explicit `outFields`)
1490
+ * and so the drain's `propertyName=` does not balloon with the
1491
+ * geometry property the caller never asked for.
1492
+ */
1493
+ function toWfsObjectIdsDrainRequest(request) {
1494
+ const { outFields: _outFields, returnGeometry: _returnGeometry, pagination: _pagination, ...idsRequest } = request ?? {};
1495
+ void _outFields;
1496
+ void _returnGeometry;
1497
+ void _pagination;
1498
+ return idsRequest;
1499
+ }
1500
+ /**
1501
+ * Compile `Query.where` and `Query.spatialFilter` into a (possibly empty)
1502
+ * FES filter and route the request through GET or POST GetFeature based on
1503
+ * the URL budget. Returns the decoded GeoJSON FeatureCollection.
1504
+ */
1505
+ async function runGetFeatureJson(featureType, typeName, choice, request, pageSize) {
1506
+ const filterNodes = [];
1507
+ let needsPostBody = false;
1508
+ let bbox;
1509
+ if (request?.where !== undefined && request.where !== "") {
1510
+ const compiled = compileWhere(request.where);
1511
+ if (compiled === UNSUPPORTED_FES) {
1512
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `where clause is not expressible in FES; route the request through Source.protocol("wfs") to emit a custom filter`);
1513
+ }
1514
+ if (compiled.kind !== "and" || compiled.operands.length > 0) {
1515
+ filterNodes.push(compiled);
1516
+ }
1517
+ }
1518
+ if (request?.spatialFilter) {
1519
+ const spatialRel = request.spatialFilter.spatialRel;
1520
+ // The `bbox=` KVP and `<fes:BBOX>` both encode envelope-intersects
1521
+ // semantics (OGC 09-026r2). Only take the bbox shortcut when the caller
1522
+ // wants intersects (default), envelope-intersects, or did not specify.
1523
+ // Anything else (Contains, Within, Crosses, Overlaps, Touches) needs an
1524
+ // FES predicate that preserves the requested relation, which means
1525
+ // routing the envelope through compileSpatialFilter so the geometry is
1526
+ // serialized as a polygon under the correct spatial op.
1527
+ const isEnvelopeIntersects = spatialRel === undefined ||
1528
+ spatialRel === "esriSpatialRelIntersects" ||
1529
+ spatialRel === "esriSpatialRelEnvelopeIntersects";
1530
+ if (request.spatialFilter.geometryType === "esriGeometryEnvelope" &&
1531
+ isEnvelopeIntersects &&
1532
+ filterNodes.length === 0 &&
1533
+ request.where === undefined) {
1534
+ // BBox-only requests are short and travel safely as a `bbox=` KVP, no
1535
+ // FES emission required. Saves a round-trip through XML serialization
1536
+ // and keeps the URL human-debuggable.
1537
+ const env = request.spatialFilter.geometry;
1538
+ if (typeof env.xmin === "number" &&
1539
+ typeof env.ymin === "number" &&
1540
+ typeof env.xmax === "number" &&
1541
+ typeof env.ymax === "number") {
1542
+ bbox = `${env.xmin},${env.ymin},${env.xmax},${env.ymax}`;
1543
+ }
1544
+ }
1545
+ else {
1546
+ const filterSrsName = wfsSrsNameFromOutSr(request.outSr);
1547
+ const compiled = compileSpatialFilter(request.spatialFilter, {
1548
+ geometryProperty: DEFAULT_WFS_GEOMETRY_PROPERTY,
1549
+ ...(filterSrsName !== undefined ? { srsName: filterSrsName } : {}),
1550
+ });
1551
+ if (compiled === UNSUPPORTED_FES) {
1552
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `spatial filter (geometryType=${request.spatialFilter.geometryType}, spatialRel=${request.spatialFilter.spatialRel ?? "default"}) is not expressible in FES; reach the wire through Source.protocol("wfs")`);
1553
+ }
1554
+ filterNodes.push(compiled);
1555
+ }
1556
+ }
1557
+ const filterXml = filterNodes.length > 0 ? serializeFes(filterNodes, { typeName }) : undefined;
1558
+ if (filterXml !== undefined) {
1559
+ const encoded = encodeURIComponent(filterXml);
1560
+ if (encoded.length > WFS_GET_FILTER_BUDGET)
1561
+ needsPostBody = true;
1562
+ }
1563
+ const params = {};
1564
+ if (bbox !== undefined)
1565
+ params.bbox = bbox;
1566
+ if (filterXml !== undefined && !needsPostBody)
1567
+ params.filter = filterXml;
1568
+ // WFS `propertyName=` drops every property the caller does not list,
1569
+ // including the geometry column. Honor the canonical contract: when
1570
+ // `outFields` is set and `returnGeometry !== false`, append the
1571
+ // geometry property so geometry is preserved; when
1572
+ // `returnGeometry === false`, omit the geometry property; when
1573
+ // `returnGeometry === false` is asked without an `outFields` list,
1574
+ // refuse the request because WFS cannot suppress geometry without
1575
+ // enumerating every non-geometry property — silently widening to
1576
+ // "geometry included" would break the canonical contract.
1577
+ const callerOutFields = request?.outFields && request.outFields.length > 0 ? request.outFields : undefined;
1578
+ const wantsGeometry = request?.returnGeometry !== false;
1579
+ let propertyNames;
1580
+ if (callerOutFields !== undefined) {
1581
+ if (wantsGeometry) {
1582
+ const merged = [...callerOutFields];
1583
+ if (!merged.includes(DEFAULT_WFS_GEOMETRY_PROPERTY))
1584
+ merged.push(DEFAULT_WFS_GEOMETRY_PROPERTY);
1585
+ propertyNames = merged;
1586
+ }
1587
+ else {
1588
+ propertyNames = [...callerOutFields];
1589
+ }
1590
+ }
1591
+ else if (request?.returnGeometry === false) {
1592
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `returnGeometry=false requires an explicit outFields list (WFS propertyName cannot suppress geometry without enumerating non-geometry properties); set Query.outFields or reach the wire through Source.protocol("wfs")`);
1593
+ }
1594
+ if (propertyNames)
1595
+ params.propertyName = [...propertyNames];
1596
+ const sortBy = request?.orderBy && request.orderBy.length > 0
1597
+ ? request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " D" : " A"}`).join(",")
1598
+ : undefined;
1599
+ if (sortBy !== undefined)
1600
+ params.sortBy = sortBy;
1601
+ if (typeof pageSize === "number" && pageSize > 0) {
1602
+ params.count = pageSize;
1603
+ }
1604
+ else if (typeof request?.pagination?.limit === "number" && request.pagination.limit > 0) {
1605
+ params.count = request.pagination.limit;
1606
+ }
1607
+ if (typeof request?.pagination?.offset === "number" && request.pagination.offset > 0) {
1608
+ params.startIndex = request.pagination.offset;
1609
+ }
1610
+ const srsName = wfsSrsNameFromOutSr(request?.outSr);
1611
+ if (srsName !== undefined)
1612
+ params.srsName = srsName;
1613
+ params.outputFormat = choice.format;
1614
+ if (request?.signal)
1615
+ params.signal = request.signal;
1616
+ if (needsPostBody && filterXml !== undefined) {
1617
+ params.method = "POST";
1618
+ const postOptions = {
1619
+ typeName,
1620
+ filter: filterXml,
1621
+ count: params.count,
1622
+ startIndex: params.startIndex,
1623
+ outputFormat: choice.format,
1624
+ };
1625
+ if (propertyNames)
1626
+ postOptions.propertyNames = propertyNames;
1627
+ if (sortBy !== undefined)
1628
+ postOptions.sortBy = sortBy;
1629
+ if (srsName !== undefined)
1630
+ postOptions.srsName = srsName;
1631
+ params.body = buildPostGetFeatureBody(postOptions);
1632
+ }
1633
+ const response = await featureType.getFeature(params);
1634
+ if (response.kind !== "json") {
1635
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `WFS GetFeature returned ${response.contentType}; canonical surface only carries JSON. Reach raw output through Source.protocol("wfs")`);
1636
+ }
1637
+ return response.data;
1638
+ }
1639
+ function buildPostGetFeatureBody(options) {
1640
+ const countAttr = typeof options.count === "number" ? ` count="${options.count}"` : "";
1641
+ const startAttr = typeof options.startIndex === "number" ? ` startIndex="${options.startIndex}"` : "";
1642
+ const outputAttr = ` outputFormat="${escapeXmlAttr(options.outputFormat)}"`;
1643
+ const queryAttrs = options.srsName !== undefined ? ` srsName="${escapeXmlAttr(options.srsName)}"` : "";
1644
+ const propertyXml = options.propertyNames && options.propertyNames.length > 0
1645
+ ? options.propertyNames.map((name) => `<wfs:PropertyName>${escapeXmlText(name)}</wfs:PropertyName>`).join("")
1646
+ : "";
1647
+ const sortByXml = options.sortBy !== undefined ? buildSortByXml(options.sortBy) : "";
1648
+ return `<wfs:GetFeature xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" service="WFS" version="2.0.0"${outputAttr}${countAttr}${startAttr}><wfs:Query typeNames="${escapeXmlAttr(options.typeName)}"${queryAttrs}>${propertyXml}${options.filter}${sortByXml}</wfs:Query></wfs:GetFeature>`;
1649
+ }
1650
+ /**
1651
+ * Build a `<fes:SortBy>` block from the same comma-separated `sortBy` string
1652
+ * the GET path emits (`FIELD A,OTHER D`). Empty entries are skipped.
1653
+ */
1654
+ function buildSortByXml(sortBy) {
1655
+ const entries = sortBy
1656
+ .split(",")
1657
+ .map((entry) => entry.trim())
1658
+ .filter((entry) => entry.length > 0);
1659
+ if (entries.length === 0)
1660
+ return "";
1661
+ const properties = entries.map((entry) => {
1662
+ const parts = entry.split(/\s+/);
1663
+ const field = parts[0];
1664
+ const direction = parts[1]?.toUpperCase() === "D" ? "DESC" : "ASC";
1665
+ return `<fes:SortProperty><fes:ValueReference>${escapeXmlText(field)}</fes:ValueReference><fes:SortOrder>${direction}</fes:SortOrder></fes:SortProperty>`;
1666
+ });
1667
+ return `<fes:SortBy>${properties.join("")}</fes:SortBy>`;
1668
+ }
1669
+ /**
1670
+ * Translate a canonical `Query.outSr` (string CRS URI / EPSG token, or numeric
1671
+ * WKID) into the WFS `srsName` form. Numeric WKIDs become the OGC URN form
1672
+ * `urn:ogc:def:crs:EPSG::<wkid>` so cross-server interop matches the format
1673
+ * advertised in `OperationsMetadata` / `Filter_Capabilities`.
1674
+ */
1675
+ function wfsSrsNameFromOutSr(outSr) {
1676
+ if (typeof outSr === "string")
1677
+ return outSr.length > 0 ? outSr : undefined;
1678
+ if (typeof outSr === "number" && Number.isFinite(outSr))
1679
+ return `urn:ogc:def:crs:EPSG::${outSr}`;
1680
+ return undefined;
1681
+ }
1682
+ function escapeXmlAttr(value) {
1683
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1684
+ }
1685
+ function resultFromGeoJson(data) {
1686
+ if (typeof data !== "object" || data === null) {
1687
+ return { features: [], exceededTransferLimit: false };
1688
+ }
1689
+ const collection = data;
1690
+ const features = (collection.features ?? []).map((f) => ({
1691
+ attributes: (f.properties ?? {}),
1692
+ geometry: f.geometry,
1693
+ }));
1694
+ const totalCount = typeof collection.numberMatched === "number" && Number.isFinite(collection.numberMatched)
1695
+ ? collection.numberMatched
1696
+ : undefined;
1697
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
1698
+ const out = {
1699
+ features,
1700
+ exceededTransferLimit,
1701
+ };
1702
+ if (totalCount !== undefined)
1703
+ out.totalCount = totalCount;
1704
+ return out;
1705
+ }
1706
+ function computeExtentFromFeatureCollection(data) {
1707
+ if (typeof data !== "object" || data === null)
1708
+ return { extent: null, count: 0 };
1709
+ const collection = data;
1710
+ let xmin = Number.POSITIVE_INFINITY;
1711
+ let ymin = Number.POSITIVE_INFINITY;
1712
+ let xmax = Number.NEGATIVE_INFINITY;
1713
+ let ymax = Number.NEGATIVE_INFINITY;
1714
+ let saw = false;
1715
+ let count = 0;
1716
+ for (const feature of collection.features ?? []) {
1717
+ count += 1;
1718
+ visitGeometryCoords(feature.geometry, (x, y) => {
1719
+ if (x < xmin)
1720
+ xmin = x;
1721
+ if (y < ymin)
1722
+ ymin = y;
1723
+ if (x > xmax)
1724
+ xmax = x;
1725
+ if (y > ymax)
1726
+ ymax = y;
1727
+ saw = true;
1728
+ });
1729
+ }
1730
+ if (!saw)
1731
+ return { extent: null, count };
1732
+ return { extent: { xmin, ymin, xmax, ymax }, count };
1733
+ }
1734
+ function buildTransactionBody(typeName, envelope, featureNamespace, srsName) {
1735
+ const releaseAction = envelope.rollbackOnFailure ? "ALL" : "SOME";
1736
+ const { prefix } = splitTypeName(typeName);
1737
+ // Bind the feature-namespace prefix on the Transaction root so prefixed
1738
+ // feature elements inside <wfs:Insert> and prefixed `typeName=` attribute
1739
+ // references on <wfs:Update>/<wfs:Delete> resolve. When the descriptor's
1740
+ // locator does not advertise `featureNamespace`, fall back to a synthetic
1741
+ // URN so the document is at least well-formed XML; strict servers will
1742
+ // reject the unknown URI with a diagnostic ExceptionReport that the caller
1743
+ // can resolve by setting `locator.featureNamespace`.
1744
+ const featureNs = prefix ? (featureNamespace ?? syntheticFeatureNamespace(prefix)) : undefined;
1745
+ const featureNsAttr = prefix && featureNs ? ` xmlns:${prefix}="${escapeXmlAttr(featureNs)}"` : "";
1746
+ const blocks = [];
1747
+ // Insert handles are read back in `canonicalEditResultFromTransaction`
1748
+ // to map server-echoed `<wfs:Feature handle="…">` entries inside
1749
+ // `<wfs:InsertResults>` onto the originating `envelope.adds[i]`. The
1750
+ // shared scheme lives in `wfsInsertHandle` so the build side and the
1751
+ // result-mapping side cannot drift.
1752
+ const adds = envelope.adds ?? [];
1753
+ for (let i = 0; i < adds.length; i += 1) {
1754
+ blocks.push(buildInsertBlock(typeName, adds[i], wfsInsertHandle(i), srsName));
1755
+ }
1756
+ let handleCounter = adds.length;
1757
+ for (const update of envelope.updates ?? []) {
1758
+ handleCounter += 1;
1759
+ blocks.push(buildUpdateBlock(typeName, update, `upd-${handleCounter}`, srsName));
1760
+ }
1761
+ for (const id of envelope.deletes ?? []) {
1762
+ handleCounter += 1;
1763
+ blocks.push(buildDeleteBlock(typeName, id, `del-${handleCounter}`));
1764
+ }
1765
+ return `<wfs:Transaction xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:gml="http://www.opengis.net/gml/3.2"${featureNsAttr} service="WFS" version="2.0.0" releaseAction="${releaseAction}">${blocks.join("")}</wfs:Transaction>`;
1766
+ }
1767
+ /**
1768
+ * Split a namespace-qualified WFS type name like `parcels:lot` into its
1769
+ * prefix and local components. Returns `{ prefix: undefined, local: typeName }`
1770
+ * for unprefixed names.
1771
+ */
1772
+ function splitTypeName(typeName) {
1773
+ const colon = typeName.indexOf(":");
1774
+ if (colon < 0)
1775
+ return { prefix: undefined, local: typeName };
1776
+ return { prefix: typeName.slice(0, colon), local: typeName.slice(colon + 1) };
1777
+ }
1778
+ /**
1779
+ * Deterministic stub URI used when the locator does not declare a
1780
+ * `featureNamespace`. Servers that perform strict schema validation will
1781
+ * reject this URI with an `<ows:ExceptionReport>` rather than silently
1782
+ * accept the body — the message names the prefix so callers know which
1783
+ * `featureNamespace` to set on the descriptor.
1784
+ */
1785
+ function syntheticFeatureNamespace(prefix) {
1786
+ return `urn:honua:wfs:feature-namespace:${prefix}`;
1787
+ }
1788
+ function buildInsertBlock(typeName, feature, handle, srsName) {
1789
+ const attributes = feature.attributes;
1790
+ const propertyXml = Object.entries(attributes)
1791
+ .filter(([, value]) => value !== undefined)
1792
+ .map(([key, value]) => `<${escapeXmlElement(key)}>${escapeXmlText(formatLiteral(value))}</${escapeXmlElement(key)}>`)
1793
+ .join("");
1794
+ const geometryXml = feature.geometry ? (geoJsonGeometryToGml(feature.geometry, srsName) ?? "") : "";
1795
+ const geometryProperty = escapeXmlElement(DEFAULT_WFS_GEOMETRY_PROPERTY);
1796
+ const featureXml = `<${escapeXmlElement(typeName)}>${propertyXml}${geometryXml ? `<${geometryProperty}>${geometryXml}</${geometryProperty}>` : ""}</${escapeXmlElement(typeName)}>`;
1797
+ return `<wfs:Insert handle="${handle}">${featureXml}</wfs:Insert>`;
1798
+ }
1799
+ function buildUpdateBlock(typeName, feature, handle, srsName) {
1800
+ const attributes = feature.attributes;
1801
+ const properties = Object.entries(attributes)
1802
+ .filter(([, value]) => value !== undefined)
1803
+ .map(([key, value]) => `<wfs:Property><wfs:ValueReference>${escapeXmlText(key)}</wfs:ValueReference><wfs:Value>${escapeXmlText(formatLiteral(value))}</wfs:Value></wfs:Property>`)
1804
+ .join("");
1805
+ const geometryProperty = feature.geometry
1806
+ ? `<wfs:Property><wfs:ValueReference>${escapeXmlText(DEFAULT_WFS_GEOMETRY_PROPERTY)}</wfs:ValueReference><wfs:Value>${geoJsonGeometryToGml(feature.geometry, srsName) ?? ""}</wfs:Value></wfs:Property>`
1807
+ : "";
1808
+ const filter = feature.id !== undefined
1809
+ ? `<fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(feature.id))}"/></fes:Filter>`
1810
+ : "";
1811
+ return `<wfs:Update handle="${handle}" typeName="${escapeXmlAttr(typeName)}">${properties}${geometryProperty}${filter}</wfs:Update>`;
1812
+ }
1813
+ function buildDeleteBlock(typeName, id, handle) {
1814
+ return `<wfs:Delete handle="${handle}" typeName="${escapeXmlAttr(typeName)}"><fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(id))}"/></fes:Filter></wfs:Delete>`;
1815
+ }
1816
+ function formatLiteral(value) {
1817
+ if (value === null)
1818
+ return "";
1819
+ if (typeof value === "number" || typeof value === "boolean")
1820
+ return String(value);
1821
+ return String(value);
1822
+ }
1823
+ function escapeXmlElement(name) {
1824
+ // Element names in WFS are namespace-qualified, but we treat the typeName
1825
+ // as a single token so callers may include `:` or `_`. Disallow anything
1826
+ // that could break the document.
1827
+ return name.replace(/[^A-Za-z0-9_:.-]/g, "_");
1828
+ }
1829
+ function escapeXmlText(value) {
1830
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1831
+ }
1832
+ /**
1833
+ * Stable handle scheme for `<wfs:Insert>` blocks. The server is expected
1834
+ * (but not required by WFS 2.0) to echo this on the matching
1835
+ * `<wfs:Feature handle="…">` element inside `<wfs:InsertResults>`, which
1836
+ * lets `canonicalEditResultFromTransaction` map returned ResourceIds back
1837
+ * onto `envelope.adds[i]` even when the server reorders the buckets or
1838
+ * omits failed inserts under `releaseAction="SOME"`.
1839
+ */
1840
+ function wfsInsertHandle(idx) {
1841
+ return `add-${idx + 1}`;
1842
+ }
1843
+ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIndices = new Set()) {
1844
+ // Index `<wfs:InsertResults>` by the handle the server echoed so that
1845
+ // server-side reordering or `releaseAction="SOME"` partial failures do
1846
+ // not misassign ResourceIds to the wrong `envelope.adds[i]`. The
1847
+ // handle attribute is informational in the WFS 2.0 spec, so when the
1848
+ // server omits it on every `<wfs:Feature>` we fall back to the legacy
1849
+ // positional pairing rather than dropping every insert id silently.
1850
+ let anyHandleEchoed = false;
1851
+ const insertByHandle = new Map();
1852
+ for (const bucket of summary.insertResults) {
1853
+ if (bucket.handle !== undefined) {
1854
+ anyHandleEchoed = true;
1855
+ insertByHandle.set(bucket.handle, bucket.ids);
1856
+ }
1857
+ }
1858
+ const added = (envelope.adds ?? []).map((_, idx) => {
1859
+ const matchedIds = anyHandleEchoed ? insertByHandle.get(wfsInsertHandle(idx)) : summary.insertResults[idx]?.ids;
1860
+ const insertedId = matchedIds?.[0];
1861
+ // When handles are echoed, presence in `InsertResults` is the
1862
+ // authoritative per-insert success signal (the server only emits a
1863
+ // bucket for the inserts that actually committed). Without handles
1864
+ // we fall back to the legacy "first N succeeded" heuristic anchored
1865
+ // on `totalInserted`.
1866
+ const ok = anyHandleEchoed ? matchedIds !== undefined : idx < summary.totalInserted;
1867
+ const out = { success: ok };
1868
+ if (insertedId !== undefined)
1869
+ out.id = insertedId;
1870
+ return out;
1871
+ });
1872
+ // Updates that were skipped because they had no id never went on the wire;
1873
+ // their outcome is a deterministic 400. The remaining indices align with
1874
+ // the transaction summary in the order the body sent them.
1875
+ const updated = [];
1876
+ let validUpdateIdx = 0;
1877
+ for (const [idx, update] of (envelope.updates ?? []).entries()) {
1878
+ if (malformedUpdateIndices.has(idx)) {
1879
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
1880
+ continue;
1881
+ }
1882
+ const out = { success: validUpdateIdx < summary.totalUpdated };
1883
+ if (update.id !== undefined)
1884
+ out.id = update.id;
1885
+ updated.push(out);
1886
+ validUpdateIdx += 1;
1887
+ }
1888
+ const deleted = (envelope.deletes ?? []).map((id, idx) => ({
1889
+ id,
1890
+ success: idx < summary.totalDeleted,
1891
+ }));
1892
+ return { added, updated, deleted };
1893
+ }
1894
+ // ── OData entity set ──────────────────────────────────────────
1895
+ /**
1896
+ * Build a `Source` over an OData v4 entity set. The `Query` translation
1897
+ * lowers `where` / `outFields` / `orderBy` / pagination / spatial filter
1898
+ * onto OData's `$`-prefixed query options. Dialect-specific operations
1899
+ * (`$batch`, `$apply`, `$search`, `$deltatoken`) live behind
1900
+ * `Source.protocol("odata")` on the returned `HonuaOdataEntitySet`.
1901
+ *
1902
+ * `$metadata` is fetched lazily on the first call that needs it and
1903
+ * cached on the entity-set instance. The fetched capability annotations
1904
+ * are intersected with the descriptor's declared `Capabilities` set the
1905
+ * first time a capability-gated method is invoked — this is the
1906
+ * precedent for the metadata-driven downgrade pattern referenced in
1907
+ * `docs/shared-client-contract.md`.
1908
+ */
1909
+ /**
1910
+ * Adapter factory for an OData v4 entity set.
1911
+ *
1912
+ * `Query.where` compiles to OData `$filter`, `Query.outFields` becomes
1913
+ * `$select`, `Query.orderBy` becomes `$orderby`, and `Query.pagination`
1914
+ * becomes `$top`/`$skip`. Geospatial filters translate to OData's
1915
+ * `geo.intersects` / `geo.distance` family.
1916
+ *
1917
+ * @example
1918
+ * ```ts
1919
+ * const dataset = createDataset({
1920
+ * id: "incidents",
1921
+ * client,
1922
+ * sources: [{
1923
+ * id: "incidents",
1924
+ * protocol: "odata",
1925
+ * locator: { url: "https://your-honua-server.example/odata", entitySet: "Incidents" },
1926
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.odata,
1927
+ * }],
1928
+ * });
1929
+ * const result = await dataset.source("incidents")!.queryAll({
1930
+ * where: "Severity ge 3",
1931
+ * orderBy: [{ field: "ReportedAt", direction: "desc" }],
1932
+ * pagination: { limit: 100 },
1933
+ * });
1934
+ * ```
1935
+ */
1936
+ export function odataSource(descriptor, client, policy) {
1937
+ const { entitySet, basePath } = requireOdataLocator(descriptor);
1938
+ const entity = new HonuaOdataEntitySet({ client, entitySet, basePath });
1939
+ const declaredCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.odata;
1940
+ const negotiation = new OdataCapabilityNegotiator(entity, declaredCaps);
1941
+ const adapterRegistry = {
1942
+ odata: entity,
1943
+ };
1944
+ return makeSource(descriptor, declaredCaps, policy, adapterRegistry, {
1945
+ async query(request) {
1946
+ ensureCapability(descriptor, declaredCaps, "query");
1947
+ await negotiation.ensureAdvertised(descriptor, "query");
1948
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
1949
+ const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
1950
+ const page = await entity.query(params);
1951
+ return odataResultFromPage(descriptor, page, await negotiation.fieldsFor(descriptor.id), request?.returnGeometry, geomColumn);
1952
+ },
1953
+ async queryAll(request) {
1954
+ ensureCapability(descriptor, declaredCaps, "query");
1955
+ await negotiation.ensureAdvertised(descriptor, "query");
1956
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
1957
+ const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
1958
+ const limit = request?.pagination?.limit;
1959
+ // Lookahead row: ask the server for `limit + 1` so we can prove
1960
+ // truncation by collecting more than `limit` rows even when the
1961
+ // response carries no `@odata.nextLink`. Mirrors the GeoServices
1962
+ // and OGC `queryAll` truncation pattern.
1963
+ if (typeof limit === "number" && limit >= 0) {
1964
+ params.top = limit + 1;
1965
+ }
1966
+ const drained = await entity.queryAll(params);
1967
+ const allFeatures = drained.rows.map((row) => odataRowToFeature(row, descriptor, request?.returnGeometry, geomColumn));
1968
+ const { features: limited, exceededTransferLimit: collectedExceeded } = applyQueryAllLimit(allFeatures, limit);
1969
+ // Belt-and-braces: if the server respected `$top` exactly but
1970
+ // reports `@odata.count > limited.length`, that also proves
1971
+ // truncation. Either signal sets the flag.
1972
+ const countExceeded = typeof drained.totalCount === "number" && typeof limit === "number" && drained.totalCount > limited.length;
1973
+ const exceededTransferLimit = collectedExceeded || countExceeded;
1974
+ const fields = await negotiation.fieldsFor(descriptor.id);
1975
+ return {
1976
+ features: limited,
1977
+ exceededTransferLimit,
1978
+ ...(typeof drained.totalCount === "number"
1979
+ ? { totalCount: drained.totalCount }
1980
+ : { totalCount: limited.length }),
1981
+ ...(fields.length > 0 ? { fields } : {}),
1982
+ };
1983
+ },
1984
+ async queryAggregate() {
1985
+ // OData aggregation is dialect-specific (`$apply` pipeline). The
1986
+ // canonical surface refuses; callers reach the typed escape hatch
1987
+ // (`Source.protocol("odata").apply(...)`) for the dialect surface.
1988
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1989
+ },
1990
+ async queryExtent() {
1991
+ // OData does not expose an extent endpoint; computing one client-side
1992
+ // would require draining every matching row, which the canonical
1993
+ // surface refuses to do silently. Mirrors the OGC `attachments` /
1994
+ // `queryRelated` posture: refuse rather than ship a degraded path.
1995
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
1996
+ },
1997
+ async *stream(request) {
1998
+ ensureCapability(descriptor, declaredCaps, "stream");
1999
+ await negotiation.ensureAdvertised(descriptor, "query");
2000
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2001
+ const params = await buildOdataParams(entity, descriptor, request, { count: false, geomColumn });
2002
+ const fields = await negotiation.fieldsFor(descriptor.id);
2003
+ const stream = entity.queryStream(params);
2004
+ for await (const page of stream) {
2005
+ yield odataResultFromPage(descriptor, page, fields, request?.returnGeometry, geomColumn);
2006
+ }
2007
+ },
2008
+ async queryObjectIds(request) {
2009
+ ensureCapability(descriptor, declaredCaps, "queryObjectIds");
2010
+ await negotiation.ensureAdvertised(descriptor, "query");
2011
+ const keyField = await negotiation.keyField(descriptor.id);
2012
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2013
+ const params = await buildOdataParams(entity, descriptor, request, { count: false, geomColumn });
2014
+ params.select = [keyField];
2015
+ // `queryObjectIds` projects only the key field, so `out.expand`
2016
+ // built from dotted `outFields` is irrelevant; clear it so a
2017
+ // caller-supplied projection cannot inflate the wire request.
2018
+ delete params.expand;
2019
+ // `entity.queryAll` respects `top` exactly. The canonical
2020
+ // `queryObjectIds` honors `Query.pagination.limit` as a hard cap
2021
+ // and slices the projection to it (mirrors the STAC adapter at
2022
+ // line 825). No lookahead row is needed because the result is
2023
+ // ids, not features — there is no `exceededTransferLimit` flag
2024
+ // to stamp.
2025
+ const limit = request?.pagination?.limit;
2026
+ const drained = await entity.queryAll(params);
2027
+ const ids = [];
2028
+ for (const row of drained.rows) {
2029
+ const value = row[keyField];
2030
+ if (value === undefined || value === null)
2031
+ continue;
2032
+ ids.push(typeof value === "number" ? value : String(value));
2033
+ }
2034
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
2035
+ return ids.slice(0, limit);
2036
+ }
2037
+ return ids;
2038
+ },
2039
+ async applyEdits(envelope) {
2040
+ ensureCapability(descriptor, declaredCaps, "applyEdits");
2041
+ await negotiation.ensureAdvertised(descriptor, "applyEdits");
2042
+ const keyFields = await negotiation.keyFields(descriptor.id);
2043
+ // Atomic path: when the caller asks for rollback-on-failure AND the
2044
+ // service advertises `$batch`, collapse the envelope into a single
2045
+ // OData batch with a shared `atomicityGroup` so a later failure
2046
+ // tears down earlier edits server-side. When `$batch` is not
2047
+ // advertised the rollback contract cannot be honored — degrade to
2048
+ // the per-call path and stamp `degraded[]` so downstream views can
2049
+ // flag the result.
2050
+ if (envelope.rollbackOnFailure === true) {
2051
+ const batchAdvertised = await negotiation.batchAdvertised();
2052
+ if (batchAdvertised) {
2053
+ return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor);
2054
+ }
2055
+ const result = await perCallOdataApplyEdits(entity, envelope, keyFields);
2056
+ return {
2057
+ ...result,
2058
+ degraded: [
2059
+ ...(result.degraded ?? []),
2060
+ {
2061
+ capability: "applyEdits",
2062
+ protocol: descriptor.protocol,
2063
+ reason: "rollbackOnFailure was requested but the OData service does not advertise $batch; edits ran per-call without atomicity.",
2064
+ sourceId: descriptor.id,
2065
+ },
2066
+ ],
2067
+ };
2068
+ }
2069
+ return perCallOdataApplyEdits(entity, envelope, keyFields);
2070
+ },
2071
+ async queryRelated() {
2072
+ // OData has no canonical related-records surface; navigation
2073
+ // properties live behind the typed escape hatch (`$expand` via
2074
+ // `protocol("odata").raw(...)` or `apply()` for cross-set joins).
2075
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
2076
+ },
2077
+ attachments: unsupportedAttachmentApi(descriptor),
2078
+ });
2079
+ }
2080
+ /**
2081
+ * Per-call OData edits — issues independent POST/PATCH/DELETE requests
2082
+ * and collects per-row outcomes. This is the non-atomic fallback used
2083
+ * when `rollbackOnFailure` is unset or the server does not advertise
2084
+ * `$batch`.
2085
+ */
2086
+ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2087
+ const added = [];
2088
+ const updated = [];
2089
+ const deleted = [];
2090
+ const { signal } = envelope;
2091
+ for (const add of envelope.adds ?? []) {
2092
+ try {
2093
+ const created = await entity.add(featureToOdataBody(add), {
2094
+ ...(signal ? { signal } : {}),
2095
+ });
2096
+ const id = readKey(created, keyFields, add.id);
2097
+ added.push(id !== undefined ? { id, success: true } : { success: true });
2098
+ }
2099
+ catch (err) {
2100
+ added.push({ success: false, error: editErrorFromCatch(err) });
2101
+ }
2102
+ }
2103
+ // Layer-scoped paths like `Layers(<n>)/Features` carry the parent key
2104
+ // (`LayerId`) in the URL itself, so the entity-set key parens only
2105
+ // need the non-parent components. `readKey` / `readKeyFromBody` keep
2106
+ // using the full `keyFields` because the response body still carries
2107
+ // every key.
2108
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2109
+ for (const update of envelope.updates ?? []) {
2110
+ const key = canonicalKeyToOdata(update, urlKeys);
2111
+ if (key === undefined) {
2112
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
2113
+ continue;
2114
+ }
2115
+ try {
2116
+ await entity.update(key, featureToOdataBody(update), { ...(signal ? { signal } : {}) });
2117
+ updated.push({ id: update.id ?? readKeyFromBody(update.attributes, keyFields), success: true });
2118
+ }
2119
+ catch (err) {
2120
+ updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
2121
+ }
2122
+ }
2123
+ for (const id of envelope.deletes ?? []) {
2124
+ const key = canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2125
+ if (key === undefined) {
2126
+ deleted.push({ success: false, error: { code: 400, description: "delete id is required" } });
2127
+ continue;
2128
+ }
2129
+ try {
2130
+ await entity.delete(key, { ...(signal ? { signal } : {}) });
2131
+ deleted.push({ id, success: true });
2132
+ }
2133
+ catch (err) {
2134
+ deleted.push({ id, success: false, error: editErrorFromCatch(err) });
2135
+ }
2136
+ }
2137
+ return { added, updated, deleted };
2138
+ }
2139
+ /**
2140
+ * Atomic OData edits — collapses the envelope into a single
2141
+ * `$batch` request whose change-set wraps every operation in the same
2142
+ * `atomicityGroup`. Honua Server's `ODataBatchHandler` rolls back the
2143
+ * entire group when any operation fails. The batch responses are
2144
+ * threaded back into per-row `EditOutcome` entries in the original
2145
+ * order so callers see the same shape regardless of transport.
2146
+ */
2147
+ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2148
+ const operations = [];
2149
+ const plan = [];
2150
+ const bucketIndices = { add: [], update: [], delete: [] };
2151
+ // Layer-scoped paths like `Layers(<n>)/Features` carry the parent key
2152
+ // (`LayerId`) in the URL itself, so the entity-set key parens only
2153
+ // need the non-parent components.
2154
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2155
+ // Sequence ids deterministically so the response shape is predictable
2156
+ // and the test surface stays small.
2157
+ let seq = 1;
2158
+ const adds = envelope.adds ?? [];
2159
+ for (const add of adds) {
2160
+ const id = String(seq++);
2161
+ operations.push({
2162
+ id,
2163
+ method: "POST",
2164
+ url: stripLeadingSlashLocal(entity.entitySet),
2165
+ body: featureToOdataBody(add),
2166
+ });
2167
+ bucketIndices.add.push(plan.length);
2168
+ plan.push({ kind: "add", id: add.id });
2169
+ }
2170
+ const updates = envelope.updates ?? [];
2171
+ for (const update of updates) {
2172
+ const id = String(seq++);
2173
+ const key = canonicalKeyToOdata(update, urlKeys);
2174
+ if (key === undefined) {
2175
+ bucketIndices.update.push(plan.length);
2176
+ plan.push({ kind: "update", id: update.id });
2177
+ continue;
2178
+ }
2179
+ operations.push({
2180
+ id,
2181
+ method: "PATCH",
2182
+ url: `${entity.entitySet}(${key})`,
2183
+ body: featureToOdataBody(update),
2184
+ });
2185
+ bucketIndices.update.push(plan.length);
2186
+ plan.push({ kind: "update", id: update.id, key });
2187
+ }
2188
+ const deletes = envelope.deletes ?? [];
2189
+ for (const rawId of deletes) {
2190
+ const id = String(seq++);
2191
+ const key = canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2192
+ if (key === undefined) {
2193
+ bucketIndices.delete.push(plan.length);
2194
+ plan.push({ kind: "delete", id: rawId });
2195
+ continue;
2196
+ }
2197
+ operations.push({ id, method: "DELETE", url: `${entity.entitySet}(${key})` });
2198
+ bucketIndices.delete.push(plan.length);
2199
+ plan.push({ kind: "delete", id: rawId, key });
2200
+ }
2201
+ // Items with `key === undefined` were never added to operations; mark
2202
+ // their outcome here without consulting the response.
2203
+ const added = new Array(adds.length);
2204
+ const updated = new Array(updates.length);
2205
+ const deleted = new Array(deletes.length);
2206
+ for (let i = 0; i < updates.length; i += 1) {
2207
+ const planIdx = bucketIndices.update[i];
2208
+ if (plan[planIdx].key === undefined) {
2209
+ updated[i] = { success: false, error: { code: 400, description: "update.id is required" } };
2210
+ }
2211
+ }
2212
+ for (let i = 0; i < deletes.length; i += 1) {
2213
+ const planIdx = bucketIndices.delete[i];
2214
+ if (plan[planIdx].key === undefined) {
2215
+ deleted[i] = { success: false, error: { code: 400, description: "delete id is required" } };
2216
+ }
2217
+ }
2218
+ if (operations.length === 0) {
2219
+ // Every requested edit was malformed — return the validation outcomes
2220
+ // rather than send an empty batch.
2221
+ return { added, updated, deleted };
2222
+ }
2223
+ let batchOutcomes;
2224
+ try {
2225
+ const batchResult = await entity.batch(operations, {
2226
+ atomicity: "all",
2227
+ ...(envelope.signal ? { signal: envelope.signal } : {}),
2228
+ });
2229
+ batchOutcomes = batchResult.responses;
2230
+ }
2231
+ catch (err) {
2232
+ // Whole-batch failure (network/auth/etc.). Apply the same error to
2233
+ // every requested row so the caller sees a uniform rollback signal.
2234
+ const error = editErrorFromCatch(err);
2235
+ for (let i = 0; i < adds.length; i += 1)
2236
+ added[i] = { success: false, error };
2237
+ for (let i = 0; i < updates.length; i += 1) {
2238
+ if (!updated[i])
2239
+ updated[i] = { id: updates[i].id, success: false, error };
2240
+ }
2241
+ for (let i = 0; i < deletes.length; i += 1) {
2242
+ if (!deleted[i])
2243
+ deleted[i] = { id: deletes[i], success: false, error };
2244
+ }
2245
+ return { added, updated, deleted };
2246
+ }
2247
+ // Index outcomes by id for O(1) lookup.
2248
+ const byId = new Map();
2249
+ for (const r of batchOutcomes)
2250
+ byId.set(String(r.id), r);
2251
+ // Walk the operations in submission order so outcome indices line up
2252
+ // with adds/updates/deletes positions.
2253
+ let opCursor = 0;
2254
+ for (let i = 0; i < adds.length; i += 1) {
2255
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2256
+ opCursor += 1;
2257
+ const outcome = byId.get(String(id));
2258
+ added[i] = batchOutcomeToEdit(outcome, adds[i].id, keyFields);
2259
+ }
2260
+ for (let i = 0; i < updates.length; i += 1) {
2261
+ if (updated[i])
2262
+ continue; // skipped (no key)
2263
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2264
+ opCursor += 1;
2265
+ const outcome = byId.get(String(id));
2266
+ updated[i] = batchOutcomeToEdit(outcome, updates[i].id, keyFields);
2267
+ }
2268
+ for (let i = 0; i < deletes.length; i += 1) {
2269
+ if (deleted[i])
2270
+ continue;
2271
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2272
+ opCursor += 1;
2273
+ const outcome = byId.get(String(id));
2274
+ deleted[i] = batchOutcomeToEdit(outcome, deletes[i], keyFields);
2275
+ }
2276
+ void descriptor;
2277
+ return { added, updated, deleted };
2278
+ }
2279
+ function batchOutcomeToEdit(outcome, fallbackId, keyFields) {
2280
+ if (!outcome) {
2281
+ return {
2282
+ ...(fallbackId !== undefined ? { id: fallbackId } : {}),
2283
+ success: false,
2284
+ error: { code: 0, description: "missing batch response" },
2285
+ };
2286
+ }
2287
+ if (outcome.status >= 200 && outcome.status < 300) {
2288
+ const body = outcome.body && typeof outcome.body === "object" ? outcome.body : undefined;
2289
+ const id = body ? readKey(body, keyFields, fallbackId) : fallbackId;
2290
+ return id !== undefined ? { id, success: true } : { success: true };
2291
+ }
2292
+ const description = describeBatchError(outcome.body, outcome.status);
2293
+ return {
2294
+ ...(fallbackId !== undefined ? { id: fallbackId } : {}),
2295
+ success: false,
2296
+ error: { code: outcome.status, description },
2297
+ };
2298
+ }
2299
+ function describeBatchError(body, status) {
2300
+ if (body && typeof body === "object") {
2301
+ const err = body.error;
2302
+ if (err && typeof err.message === "string")
2303
+ return err.message;
2304
+ }
2305
+ return `HTTP ${status}`;
2306
+ }
2307
+ function stripLeadingSlashLocal(url) {
2308
+ return url.startsWith("/") ? url.slice(1) : url;
2309
+ }
2310
+ function requireOdataLocator(descriptor) {
2311
+ const { entitySet, url, layerId } = descriptor.locator;
2312
+ // Resolve the entity-set token. Honua Server's `SourceLocator` only
2313
+ // carries `url`, `serviceId`, and `layerId`, so server-produced OData
2314
+ // bindings arrive without `entitySet`. The canonical server route is
2315
+ // layer-scoped — `/odata/Layers(<layerId>)/Features` — so derive that
2316
+ // path when only `layerId` is provided. SDK callers that already pass
2317
+ // `entitySet` (the historical path) continue to win.
2318
+ let resolvedEntitySet;
2319
+ if (typeof entitySet === "string" && entitySet !== "") {
2320
+ resolvedEntitySet = entitySet;
2321
+ }
2322
+ else if (typeof layerId === "number" && Number.isFinite(layerId)) {
2323
+ resolvedEntitySet = `Layers(${layerId})/Features`;
2324
+ }
2325
+ if (resolvedEntitySet === undefined) {
2326
+ throw new Error(`createDataset: source "${descriptor.id}" (odata) requires locator.entitySet or locator.layerId`);
2327
+ }
2328
+ // `locator.url` is informational on the canonical surface; the request
2329
+ // path is built from `basePath/<entitySet>`. When `locator.url` carries
2330
+ // a path, treat its trailing path component as the basePath so a
2331
+ // descriptor pointing at `https://srv/odata/v4` still resolves correctly.
2332
+ const basePath = url ? extractOdataBasePath(url) : "/odata";
2333
+ return { entitySet: resolvedEntitySet, basePath };
2334
+ }
2335
+ function extractOdataBasePath(url) {
2336
+ try {
2337
+ const parsed = new URL(url);
2338
+ const pathname = trimTrailingSlashes(parsed.pathname);
2339
+ return pathname === "" ? "/odata" : pathname;
2340
+ }
2341
+ catch {
2342
+ // Relative URL: take it as the basePath verbatim.
2343
+ return url.startsWith("/") ? trimTrailingSlashes(url) || "/odata" : `/${trimTrailingSlashes(url)}`;
2344
+ }
2345
+ }
2346
+ /**
2347
+ * Resolve the OData geometry column for a descriptor. Prefers
2348
+ * `descriptor.schema.fields` (where a field of type
2349
+ * `esriFieldTypeGeometry` names the column), then falls back to the
2350
+ * lazy `$metadata` probe (the first property typed `Edm.Geography` /
2351
+ * `Edm.Geometry` per CSDL parsing). Returns `undefined` when neither
2352
+ * declares one — callers fall back to the canonical name guesses
2353
+ * (`Geometry` / `Geography` / `Shape`) where the column name is
2354
+ * needed for client-side filtering only (never for emitting a
2355
+ * spatial filter, which throws when the column cannot be resolved).
2356
+ */
2357
+ async function resolveOdataGeometryColumn(entity, descriptor) {
2358
+ const schemaCol = descriptor.schema?.fields?.find((f) => f.type === "esriFieldTypeGeometry")?.name;
2359
+ if (schemaCol)
2360
+ return schemaCol;
2361
+ try {
2362
+ const meta = await entity.metadata();
2363
+ const typeName = meta.entitySets[entity.entitySetName];
2364
+ const fields = (typeName ? meta.fields[typeName] : undefined) ?? [];
2365
+ return fields.find((f) => f.isSpatial)?.name;
2366
+ }
2367
+ catch {
2368
+ return undefined;
2369
+ }
2370
+ }
2371
+ /**
2372
+ * Split a list of canonical `outFields` into a root `$select` list and
2373
+ * an OData `$expand` argument. Plain field names go into `$select`.
2374
+ * Dotted field paths (e.g. `Owner.name`) are translated to OData
2375
+ * navigation expands such as `Owner($select=name)`. Multi-level paths
2376
+ * (e.g. `Owner.address.street`) nest as
2377
+ * `Owner($expand=address($select=street))`. Multiple fields under the
2378
+ * same navigation share one expand entry
2379
+ * (`Owner($select=name,email)`) so the URL stays compact.
2380
+ */
2381
+ function splitOdataOutFields(outFields) {
2382
+ const root = { select: [], children: new Map() };
2383
+ for (const field of outFields) {
2384
+ const segments = field.split(".");
2385
+ if (segments.length === 1) {
2386
+ root.select.push(segments[0]);
2387
+ continue;
2388
+ }
2389
+ let cursor = root;
2390
+ for (let i = 0; i < segments.length - 1; i += 1) {
2391
+ const seg = segments[i];
2392
+ let child = cursor.children.get(seg);
2393
+ if (!child) {
2394
+ child = { select: [], children: new Map() };
2395
+ cursor.children.set(seg, child);
2396
+ }
2397
+ cursor = child;
2398
+ }
2399
+ cursor.select.push(segments[segments.length - 1]);
2400
+ }
2401
+ function serialize(node) {
2402
+ const parts = [];
2403
+ if (node.select.length > 0)
2404
+ parts.push(`$select=${node.select.join(",")}`);
2405
+ if (node.children.size > 0) {
2406
+ const segs = [];
2407
+ for (const [name, child] of node.children) {
2408
+ const inner = serialize(child);
2409
+ segs.push(inner === "" ? name : `${name}(${inner})`);
2410
+ }
2411
+ parts.push(`$expand=${segs.join(",")}`);
2412
+ }
2413
+ return parts.join(";");
2414
+ }
2415
+ const expand = [];
2416
+ for (const [name, child] of root.children) {
2417
+ const inner = serialize(child);
2418
+ expand.push(inner === "" ? name : `${name}(${inner})`);
2419
+ }
2420
+ return { select: root.select, expand };
2421
+ }
2422
+ /**
2423
+ * Decide whether a candidate root `outFields` entry refers to the
2424
+ * geometry column. When the resolver supplied a column name (from
2425
+ * descriptor schema or metadata), match against it directly. The
2426
+ * canonical name guesses (`geometry`, `geography`, `shape`) are
2427
+ * preserved as a fallback so a server that emits a hard-coded geometry
2428
+ * field still drops out of `$select` even when neither schema nor
2429
+ * metadata declares one.
2430
+ */
2431
+ function isOdataGeometryFieldName(field, resolved) {
2432
+ if (resolved && field === resolved)
2433
+ return true;
2434
+ if (resolved)
2435
+ return false;
2436
+ const lower = field.toLowerCase();
2437
+ return lower === "geometry" || lower === "geography" || lower === "shape";
2438
+ }
2439
+ /**
2440
+ * Build the OData query params for a canonical `Query`. Materializes
2441
+ * `$metadata` through the negotiator only when a translation rule needs
2442
+ * a geometry column or the key field — otherwise the request stays
2443
+ * metadata-free.
2444
+ */
2445
+ async function buildOdataParams(entity, descriptor, request, options) {
2446
+ const out = {};
2447
+ if (options.count)
2448
+ out.count = true;
2449
+ if (!request)
2450
+ return out;
2451
+ const filterParts = [];
2452
+ if (request.where !== undefined && request.where !== "") {
2453
+ const rewritten = rewriteWhereToOdataFilter(request.where);
2454
+ if (rewritten !== "")
2455
+ filterParts.push(rewritten);
2456
+ }
2457
+ if (request.spatialFilter) {
2458
+ // `$metadata` is advisory here — it only refines the geometry column
2459
+ // and SRID context. When it is missing or failing, fall back to the
2460
+ // descriptor-derived `geomColumn` and column defaults instead of
2461
+ // rejecting the query (same degradation as `resolveOdataGeometryColumn`).
2462
+ const meta = await entity.metadata().catch(() => undefined);
2463
+ const typeName = meta ? meta.entitySets[entity.entitySetName] : undefined;
2464
+ const spatialFields = (typeName && meta ? meta.fields[typeName] : undefined) ?? [];
2465
+ // The WKT SRID stamps the **input** literal's coordinate system, not
2466
+ // the desired output SR (`Query.outSr` controls the response geometry
2467
+ // SR via column-side projection on the server). Derive the input SRID
2468
+ // from `spatialFilter.geometry.spatialReference` and fall back to the
2469
+ // metadata-declared column SRID; if neither is available, omit the
2470
+ // SRID prefix and let the column default apply. The geometry column
2471
+ // itself is preferred from `options.geomColumn` (descriptor schema
2472
+ // first, then metadata isSpatial) so a schema-declared column named
2473
+ // anything other than `Geometry`/`Geography`/`Shape` is honored.
2474
+ const ctx = {
2475
+ ...(options.geomColumn ? { geometryColumn: options.geomColumn } : {}),
2476
+ ...(spatialFields.length > 0 ? { geometryFields: spatialFields } : {}),
2477
+ };
2478
+ filterParts.push(buildOdataSpatialFilter(request.spatialFilter, ctx));
2479
+ }
2480
+ if (filterParts.length > 0)
2481
+ out.filter = filterParts.join(" and ");
2482
+ if (request.outFields && request.outFields.length > 0) {
2483
+ const split = splitOdataOutFields(request.outFields);
2484
+ let rootSelect = split.select;
2485
+ if (request.returnGeometry === false) {
2486
+ rootSelect = rootSelect.filter((f) => !isOdataGeometryFieldName(f, options.geomColumn));
2487
+ }
2488
+ if (rootSelect.length > 0)
2489
+ out.select = rootSelect;
2490
+ if (split.expand.length > 0)
2491
+ out.expand = split.expand;
2492
+ }
2493
+ else if (request.returnGeometry === false) {
2494
+ // No outFields supplied — derive `$select` from metadata so the
2495
+ // geometry column never reaches the wire. Falls through silently
2496
+ // when metadata is unavailable; the result-side dropper still
2497
+ // strips the geometry from the canonical Result.
2498
+ const meta = await entity.metadata().catch(() => undefined);
2499
+ if (meta) {
2500
+ const typeName = meta.entitySets[entity.entitySetName];
2501
+ const allFields = (typeName ? meta.fields[typeName] : undefined) ?? [];
2502
+ const nonSpatial = allFields
2503
+ .filter((f) => !f.isSpatial && (options.geomColumn === undefined || f.name !== options.geomColumn))
2504
+ .map((f) => f.name);
2505
+ if (nonSpatial.length > 0)
2506
+ out.select = nonSpatial;
2507
+ }
2508
+ }
2509
+ if (request.orderBy && request.orderBy.length > 0) {
2510
+ out.orderBy = request.orderBy.map((s) => (s.direction === "desc" ? `${s.field} desc` : s.field));
2511
+ }
2512
+ if (request.pagination?.limit !== undefined)
2513
+ out.top = request.pagination.limit;
2514
+ if (request.pagination?.offset !== undefined && request.pagination.offset > 0) {
2515
+ out.skip = request.pagination.offset;
2516
+ }
2517
+ if (request.signal)
2518
+ out.signal = request.signal;
2519
+ void descriptor;
2520
+ return out;
2521
+ }
2522
+ function odataResultFromPage(descriptor, page, fields, returnGeometry, geomColumn) {
2523
+ const features = page.rows.map((row) => odataRowToFeature(row, descriptor, returnGeometry, geomColumn));
2524
+ const exceededTransferLimit = typeof page.totalCount === "number" ? features.length < page.totalCount : Boolean(page.nextLink);
2525
+ return {
2526
+ features,
2527
+ exceededTransferLimit,
2528
+ ...(typeof page.totalCount === "number" ? { totalCount: page.totalCount } : {}),
2529
+ ...(fields.length > 0 ? { fields } : {}),
2530
+ };
2531
+ }
2532
+ /**
2533
+ * Convert one OData JSON row into a canonical `HonuaTypedFeature`. The
2534
+ * geometry column (when the row carries one as GeoJSON via Honua Server's
2535
+ * spatial encoding) is split out from the attributes envelope. When
2536
+ * `returnGeometry === false`, the geometry column is dropped from both
2537
+ * the attributes envelope and the `geometry` field — defensive against
2538
+ * servers that ignore the `$select` exclusion (or callers that bypassed
2539
+ * metadata-derived selects). The geometry column is sourced from the
2540
+ * caller-resolved `geomColumn` (descriptor schema → metadata
2541
+ * `isSpatial`) and falls back to the canonical name guesses on the
2542
+ * row keys when neither declares one.
2543
+ */
2544
+ function odataRowToFeature(row, descriptor, returnGeometry, geomColumn) {
2545
+ const geometryKey = geomColumn ??
2546
+ descriptor.schema?.fields?.find((f) => f.type === "esriFieldTypeGeometry")?.name ??
2547
+ findGeometryKey(row);
2548
+ if (geometryKey && row[geometryKey] !== undefined) {
2549
+ const { [geometryKey]: geometry, ...rest } = row;
2550
+ return {
2551
+ attributes: rest,
2552
+ geometry: returnGeometry === false ? null : (geometry ?? null),
2553
+ };
2554
+ }
2555
+ return { attributes: row, geometry: null };
2556
+ }
2557
+ function findGeometryKey(row) {
2558
+ for (const key of Object.keys(row)) {
2559
+ const lower = key.toLowerCase();
2560
+ if (lower === "geometry" || lower === "geography" || lower === "shape")
2561
+ return key;
2562
+ }
2563
+ return undefined;
2564
+ }
2565
+ function featureToOdataBody(feature) {
2566
+ const body = { ...feature.attributes };
2567
+ if (feature.geometry !== undefined && feature.geometry !== null) {
2568
+ if (body.Geometry === undefined && body.geometry === undefined) {
2569
+ body.Geometry = feature.geometry;
2570
+ }
2571
+ }
2572
+ return body;
2573
+ }
2574
+ function readKey(body, keyFields, fallback) {
2575
+ if (keyFields.length === 1) {
2576
+ const value = body[keyFields[0]];
2577
+ if (typeof value === "number" || typeof value === "string")
2578
+ return value;
2579
+ }
2580
+ if (keyFields.length > 1) {
2581
+ // Composite key: surface as `Field=value,Field=value`. Numeric fallback
2582
+ // is preferred when only one of the key parts is OBJECTID-like to keep
2583
+ // the EditOutcome.id readable.
2584
+ const objectId = body.ObjectId ?? body.objectId ?? body.OBJECTID;
2585
+ if (typeof objectId === "number" || typeof objectId === "string")
2586
+ return objectId;
2587
+ }
2588
+ return fallback;
2589
+ }
2590
+ function readKeyFromBody(attributes, keyFields) {
2591
+ if (typeof attributes !== "object" || attributes === null)
2592
+ return undefined;
2593
+ return readKey(attributes, keyFields, undefined);
2594
+ }
2595
+ /**
2596
+ * For OData navigation paths like `Layers(<n>)/Features`, the parent key
2597
+ * (`LayerId`) is already encoded in the URL path — the entity-set key
2598
+ * parens should only carry the non-parent components of the composite
2599
+ * key. Returns the metadata key fields with the parent-key field removed
2600
+ * when the path is layer-scoped, so a caller addressing a row by its
2601
+ * bare ObjectId still produces a valid `Features(<objectId>)` URL.
2602
+ *
2603
+ * Direct paths (e.g. `Parcels`) return the input key fields unchanged.
2604
+ */
2605
+ function urlKeyFields(entitySetPath, keyFields) {
2606
+ if (!/^Layers\(\d+\)\/Features$/i.test(entitySetPath))
2607
+ return keyFields;
2608
+ const filtered = keyFields.filter((f) => f.toLowerCase() !== "layerid");
2609
+ // Defense in depth: if filtering eliminates every key field (the
2610
+ // metadata declared LayerId-only) fall back to the original list so
2611
+ // the formatter at least has something to work with.
2612
+ return filtered.length === 0 ? keyFields : filtered;
2613
+ }
2614
+ function canonicalKeyToOdata(feature, keyFields) {
2615
+ if (keyFields.length === 0) {
2616
+ if (feature.id === undefined || feature.id === null)
2617
+ return undefined;
2618
+ return formatOdataKeyValue(feature.id);
2619
+ }
2620
+ if (keyFields.length === 1) {
2621
+ const fieldName = keyFields[0];
2622
+ const value = feature.attributes?.[fieldName] ?? feature.id;
2623
+ if (value === undefined || value === null)
2624
+ return undefined;
2625
+ return formatOdataKeyValue(value);
2626
+ }
2627
+ // Composite key — prefer attribute-derived components. When `feature.id`
2628
+ // is a pre-formatted key expression (`LayerId=1,ObjectId=3` or the bare
2629
+ // `1,3` tuple), use it verbatim so callers that address composite-key
2630
+ // rows on the canonical `deletes: FeatureId[]` envelope still work.
2631
+ if (typeof feature.id === "string" && (feature.id.includes("=") || feature.id.includes(","))) {
2632
+ return feature.id;
2633
+ }
2634
+ const parts = [];
2635
+ for (const fieldName of keyFields) {
2636
+ const value = feature.attributes?.[fieldName];
2637
+ if (value === undefined || value === null) {
2638
+ // Allow a single-value `feature.id` to fill the ObjectId component
2639
+ // when the key is the conventional Honua (LayerId, ObjectId) pair
2640
+ // and the caller addressed the row by its bare ObjectId.
2641
+ if (fieldName === "ObjectId" && (typeof feature.id === "number" || typeof feature.id === "string")) {
2642
+ parts.push(`${fieldName}=${formatOdataKeyValue(feature.id)}`);
2643
+ continue;
2644
+ }
2645
+ return undefined;
2646
+ }
2647
+ parts.push(`${fieldName}=${formatOdataKeyValue(value)}`);
2648
+ }
2649
+ return parts.join(",");
2650
+ }
2651
+ function formatOdataKeyValue(value) {
2652
+ if (typeof value === "number")
2653
+ return String(value);
2654
+ // Strings are quoted; numeric strings stay as numbers because OData's
2655
+ // key syntax accepts both for Edm.Int* keys.
2656
+ const numeric = Number(value);
2657
+ if (Number.isFinite(numeric) && String(numeric) === value)
2658
+ return String(numeric);
2659
+ return `'${String(value).replace(/'/g, "''")}'`;
2660
+ }
2661
+ /**
2662
+ * Lazy, per-source negotiator that fetches `$metadata` exactly once and
2663
+ * intersects the declared capability set with what the service actually
2664
+ * advertises through `Capabilities.*` annotations. The intersection is
2665
+ * surfaced as `HonuaCapabilityNotSupportedError` on the first call that
2666
+ * needs the missing capability.
2667
+ */
2668
+ class OdataCapabilityNegotiator {
2669
+ entity;
2670
+ declared;
2671
+ metaPromise;
2672
+ fieldsCache;
2673
+ constructor(entity, declared) {
2674
+ this.entity = entity;
2675
+ this.declared = [...declared];
2676
+ }
2677
+ async ensureAdvertised(descriptor, capability) {
2678
+ // Only check capabilities the descriptor declares — caller-passed
2679
+ // narrower sets short-circuit through `ensureCapability` before this
2680
+ // is reached.
2681
+ if (!this.declared.includes(capability))
2682
+ return;
2683
+ // A missing or failing `$metadata` endpoint is allowed at runtime —
2684
+ // degrade to the descriptor's declared capability set instead of
2685
+ // rejecting the canonical call, mirroring `fieldsFor` / `keyFields` /
2686
+ // `batchAdvertised`. Errors from the data request itself still
2687
+ // surface from `query`.
2688
+ const meta = await this.materialize().catch(() => undefined);
2689
+ if (!meta)
2690
+ return;
2691
+ const advertised = meta.capabilities[this.entity.entitySetName] ?? {};
2692
+ const flag = advertisedFlag(advertised, capability);
2693
+ if (flag === false) {
2694
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2695
+ }
2696
+ }
2697
+ async fieldsFor(_sourceId) {
2698
+ void _sourceId;
2699
+ if (this.fieldsCache)
2700
+ return this.fieldsCache;
2701
+ try {
2702
+ const meta = await this.materialize();
2703
+ const fields = odataFieldSchema(meta, this.entity.entitySetName);
2704
+ this.fieldsCache = fields;
2705
+ return fields;
2706
+ }
2707
+ catch {
2708
+ // A missing `$metadata` endpoint is allowed at runtime — the
2709
+ // canonical surface still works, just without the schema in the
2710
+ // result envelope. Errors are surfaced from `query` itself so the
2711
+ // adapter does not double-throw.
2712
+ this.fieldsCache = [];
2713
+ return this.fieldsCache;
2714
+ }
2715
+ }
2716
+ async keyFields(_sourceId) {
2717
+ void _sourceId;
2718
+ const meta = await this.materialize().catch(() => undefined);
2719
+ if (!meta)
2720
+ return [];
2721
+ const typeName = meta.entitySets[this.entity.entitySetName];
2722
+ if (!typeName)
2723
+ return [];
2724
+ return meta.keys[typeName] ?? [];
2725
+ }
2726
+ async keyField(sourceId) {
2727
+ const keys = await this.keyFields(sourceId);
2728
+ if (keys.length === 1)
2729
+ return keys[0];
2730
+ // Composite key: prefer the conventional Honua `ObjectId` field for
2731
+ // `queryObjectIds()` so the canonical surface returns a flat
2732
+ // `FeatureId[]` instead of composite-key strings.
2733
+ if (keys.includes("ObjectId"))
2734
+ return "ObjectId";
2735
+ if (keys.length > 0)
2736
+ return keys[0];
2737
+ return "ObjectId";
2738
+ }
2739
+ /**
2740
+ * Returns true when the entity-set's `Capabilities.BatchSupported`
2741
+ * annotation is `true`, false when explicitly `false`, and true by
2742
+ * default when the metadata is unavailable (the OData spec defaults
2743
+ * `$batch` to supported when the annotation is absent).
2744
+ */
2745
+ async batchAdvertised() {
2746
+ const meta = await this.materialize().catch(() => undefined);
2747
+ if (!meta)
2748
+ return true;
2749
+ const flag = meta.capabilities[this.entity.entitySetName]?.batch;
2750
+ return flag !== false;
2751
+ }
2752
+ materialize() {
2753
+ if (!this.metaPromise) {
2754
+ this.metaPromise = this.entity.metadata();
2755
+ }
2756
+ return this.metaPromise;
2757
+ }
2758
+ }
2759
+ function advertisedFlag(advertised, capability) {
2760
+ switch (capability) {
2761
+ case "query":
2762
+ return advertised.query;
2763
+ case "stream":
2764
+ return advertised.query;
2765
+ case "queryObjectIds":
2766
+ return advertised.query;
2767
+ case "applyEdits":
2768
+ // Treat the union as the canonical "applyEdits" gate — adapters
2769
+ // refuse only when every relevant flag is explicitly false.
2770
+ if (advertised.insert === false && advertised.update === false && advertised.delete === false) {
2771
+ return false;
2772
+ }
2773
+ return undefined;
2774
+ default:
2775
+ return undefined;
2776
+ }
2777
+ }
2778
+ function makeSource(descriptor, caps, _policy, adapters, impl) {
2779
+ function lookupAdapter(kind) {
2780
+ return adapters[kind];
2781
+ }
2782
+ return {
2783
+ descriptor: { ...descriptor, capabilities: caps },
2784
+ capabilities: caps,
2785
+ query: impl.query.bind(impl),
2786
+ queryAll: impl.queryAll.bind(impl),
2787
+ queryAggregate: impl.queryAggregate.bind(impl),
2788
+ queryExtent: impl.queryExtent.bind(impl),
2789
+ stream: impl.stream.bind(impl),
2790
+ queryObjectIds: impl.queryObjectIds.bind(impl),
2791
+ applyEdits: impl.applyEdits.bind(impl),
2792
+ queryRelated: impl.queryRelated.bind(impl),
2793
+ attachments: impl.attachments,
2794
+ protocol: lookupAdapter,
2795
+ adapter: lookupAdapter,
2796
+ };
2797
+ }
2798
+ /**
2799
+ * Helper: build a no-attachment-support `AttachmentApi` whose every method
2800
+ * throws `HonuaCapabilityNotSupportedError` for the descriptor. Used by
2801
+ * adapters that do not advertise `attachments` so the namespace property
2802
+ * is present (callers can always read `source.attachments`) but the calls
2803
+ * themselves participate in capability negotiation.
2804
+ */
2805
+ function unsupportedAttachmentApi(descriptor) {
2806
+ const fail = () => {
2807
+ throw new HonuaCapabilityNotSupportedError("attachments", descriptor.protocol, descriptor.id);
2808
+ };
2809
+ return {
2810
+ async query() {
2811
+ return fail();
2812
+ },
2813
+ async list() {
2814
+ return fail();
2815
+ },
2816
+ async add() {
2817
+ return fail();
2818
+ },
2819
+ async update() {
2820
+ return fail();
2821
+ },
2822
+ async delete() {
2823
+ return fail();
2824
+ },
2825
+ };
2826
+ }
2827
+ /**
2828
+ * Build a `SourceImplementation` skeleton whose every method throws
2829
+ * `HonuaCapabilityNotSupportedError`. Used by the geometry- and gp-service
2830
+ * sources, which do not host features and only surface protocol-specific
2831
+ * operations through `Source.protocol()`.
2832
+ */
2833
+ function unsupportedFeatureSurface(descriptor) {
2834
+ const fail = (capability) => {
2835
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2836
+ };
2837
+ return {
2838
+ async query() {
2839
+ return fail("query");
2840
+ },
2841
+ async queryAll() {
2842
+ return fail("query");
2843
+ },
2844
+ async queryAggregate() {
2845
+ return fail("queryAggregate");
2846
+ },
2847
+ async queryExtent() {
2848
+ return fail("queryExtent");
2849
+ },
2850
+ // biome-ignore lint/correctness/useYield: utility-only services (Geometry, GP) do not host features; this generator refuses iteration rather than silently complete with no pages
2851
+ async *stream() {
2852
+ fail("stream");
2853
+ },
2854
+ async queryObjectIds() {
2855
+ return fail("queryObjectIds");
2856
+ },
2857
+ async applyEdits() {
2858
+ return fail("applyEdits");
2859
+ },
2860
+ async queryRelated() {
2861
+ return fail("queryRelated");
2862
+ },
2863
+ attachments: unsupportedAttachmentApi(descriptor),
2864
+ };
2865
+ }
2866
+ function ensureCapability(descriptor, caps, capability) {
2867
+ if (caps.has(capability))
2868
+ return;
2869
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2870
+ }
2871
+ // `degraded` may only bypass a missing capability when the call site immediately
2872
+ // takes a defined fallback path. Returns true when the descriptor advertises the
2873
+ // capability (use the native path), false when policy is `degraded` and a
2874
+ // fallback will execute, and throws under `strict` when missing.
2875
+ function ensureCapabilityOrFallback(descriptor, caps, capability, policy) {
2876
+ if (caps.has(capability))
2877
+ return true;
2878
+ if (policy === "strict") {
2879
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2880
+ }
2881
+ return false;
2882
+ }
2883
+ function requireFeatureServiceLocator(descriptor) {
2884
+ const { serviceId, layerId } = descriptor.locator;
2885
+ if (typeof serviceId !== "string" || typeof layerId !== "number") {
2886
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-feature-service) requires locator.serviceId and locator.layerId`);
2887
+ }
2888
+ return { serviceId, layerId };
2889
+ }
2890
+ function requireMapServiceLocator(descriptor) {
2891
+ const { serviceId, layerId } = descriptor.locator;
2892
+ if (typeof serviceId !== "string" || typeof layerId !== "number") {
2893
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-map-service) requires locator.serviceId and locator.layerId`);
2894
+ }
2895
+ return { serviceId, layerId };
2896
+ }
2897
+ function requireOgcLocator(descriptor) {
2898
+ const { collectionId } = descriptor.locator;
2899
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
2900
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-features) requires locator.collectionId`);
2901
+ }
2902
+ return { collectionId };
2903
+ }
2904
+ function requireOgcRecordsLocator(descriptor) {
2905
+ const { collectionId } = descriptor.locator;
2906
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
2907
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-records) requires locator.collectionId`);
2908
+ }
2909
+ return { collectionId };
2910
+ }
2911
+ function requireImageServiceLocator(descriptor) {
2912
+ const { serviceId } = descriptor.locator;
2913
+ if (typeof serviceId !== "string") {
2914
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-image-service) requires locator.serviceId`);
2915
+ }
2916
+ return { serviceId };
2917
+ }
2918
+ /**
2919
+ * Reject canonical `Query` fields the ImageServer catalog endpoint cannot
2920
+ * honor. The Honua Server catalog handler reads `where` / `objectIds` /
2921
+ * `outSR` / `resultOffset` / `resultRecordCount` / `returnGeometry` and
2922
+ * silently ignores `outFields`, `geometry` / `geometryType` / `spatialRel`,
2923
+ * and `orderByFields`. Refusing the request explicitly prevents callers
2924
+ * from receiving an unfiltered, unsorted, or wider result that looks like
2925
+ * a silent data quality bug. Mirrors the OGC adapter's spatialFilter /
2926
+ * spatialRel guards.
2927
+ */
2928
+ function requireImageServerCompatibleQuery(request) {
2929
+ if (!request)
2930
+ return;
2931
+ if (request.spatialFilter) {
2932
+ throw new Error("geoservices-image-service: Query.spatialFilter is not supported on the raster catalog; the ImageServer catalog endpoint does not accept geometry / geometryType / spatialRel filters. Use Query.where to constrain the catalog or call protocol().identify() / exportImage() on the typed escape hatch.");
2933
+ }
2934
+ if (request.orderBy && request.orderBy.length > 0) {
2935
+ throw new Error("geoservices-image-service: Query.orderBy is not supported on the raster catalog; the ImageServer catalog endpoint does not honor orderByFields. Drop orderBy or sort client-side.");
2936
+ }
2937
+ if (request.outFields && request.outFields.length > 0) {
2938
+ throw new Error("geoservices-image-service: Query.outFields is not supported on the raster catalog; the ImageServer catalog endpoint always returns the full catalog row schema. Drop outFields or project client-side.");
2939
+ }
2940
+ }
2941
+ function requireGPServiceLocator(descriptor, caps) {
2942
+ const { serviceId, taskName } = descriptor.locator;
2943
+ if (typeof serviceId !== "string") {
2944
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) requires locator.serviceId`);
2945
+ }
2946
+ // Honua Server publishes submitJob / jobs / cancel / results only under
2947
+ // /rest/services/<serviceId>/GPServer/<taskName>/..., so descriptors that
2948
+ // advertise the `geoprocess` capability must carry a task name. Without
2949
+ // one the lifecycle routes resolve to non-existent paths on the server.
2950
+ // Descriptors with only `connect` (service-root metadata probe) may omit
2951
+ // taskName.
2952
+ if (caps.has("geoprocess") && (typeof taskName !== "string" || taskName.length === 0)) {
2953
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) advertises "geoprocess" but locator.taskName is missing; GP lifecycle routes require /GPServer/<taskName>/...`);
2954
+ }
2955
+ return { serviceId, taskName };
2956
+ }
2957
+ function requireOgcTilesLocator(descriptor) {
2958
+ const { collectionId, tileMatrixSetId } = descriptor.locator;
2959
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
2960
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-tiles) requires locator.collectionId`);
2961
+ }
2962
+ return { collectionId, tileMatrixSetId };
2963
+ }
2964
+ function requireWmsLocator(descriptor) {
2965
+ const { serviceId } = descriptor.locator;
2966
+ if (typeof serviceId !== "string" || serviceId.length === 0) {
2967
+ throw new Error(`createDataset: source "${descriptor.id}" (wms) requires locator.serviceId`);
2968
+ }
2969
+ return { serviceId };
2970
+ }
2971
+ function requireWmtsLocator(descriptor) {
2972
+ const { serviceId } = descriptor.locator;
2973
+ if (typeof serviceId !== "string" || serviceId.length === 0) {
2974
+ throw new Error(`createDataset: source "${descriptor.id}" (wmts) requires locator.serviceId`);
2975
+ }
2976
+ return { serviceId };
2977
+ }
2978
+ function wmsRequireLayers(descriptor, locatorTypeName) {
2979
+ const parsed = parseWmsLayerNames(locatorTypeName);
2980
+ if (parsed.length === 0) {
2981
+ throw new Error(`createDataset: source "${descriptor.id}" (wms) requires locator.typeName (the WMS LAYER name) for canonical query()`);
2982
+ }
2983
+ return parsed;
2984
+ }
2985
+ /**
2986
+ * Reject canonical `Query` fields that WMS GetFeatureInfo cannot honor so a
2987
+ * mixed-source caller does not silently receive an unfiltered, reprojected,
2988
+ * or differently-shaped result. WMS GetFeatureInfo only consumes the
2989
+ * (i, j) pixel pair plus the rendered envelope; there is no SQL/CQL filter,
2990
+ * no field projection, no order, no offset paging, no geometry toggle, and
2991
+ * no separate output-SR knob on the wire — honua-server projects the
2992
+ * response in the request CRS itself. `aggregation` is mapped to
2993
+ * `queryAggregate` so the error carries the same capability vocabulary as
2994
+ * the rest of the contract. `pagination.limit` is honored (it maps to
2995
+ * `FEATURE_COUNT`).
2996
+ */
2997
+ function requireWmsCompatibleQuery(descriptor, request) {
2998
+ if (!request)
2999
+ return;
3000
+ if (request.aggregation) {
3001
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
3002
+ }
3003
+ if (typeof request.where === "string" && request.where.length > 0) {
3004
+ throw new Error(`wms: Query.where is not supported on GetFeatureInfo for source "${descriptor.id}"; WMS has no SQL/CQL filter on the wire. Pre-filter via Query.spatialFilter (point) or use a tabular protocol.`);
3005
+ }
3006
+ if (request.outFields && request.outFields.length > 0) {
3007
+ throw new Error(`wms: Query.outFields is not supported on GetFeatureInfo for source "${descriptor.id}"; the server returns the layer's full attribute schema. Project client-side after the result lands.`);
3008
+ }
3009
+ if (request.orderBy && request.orderBy.length > 0) {
3010
+ throw new Error(`wms: Query.orderBy is not supported on GetFeatureInfo for source "${descriptor.id}"; the server has no sort surface. Sort client-side after the result lands.`);
3011
+ }
3012
+ if (typeof request.pagination?.offset === "number" && request.pagination.offset > 0) {
3013
+ throw new Error(`wms: Query.pagination.offset is not supported on GetFeatureInfo for source "${descriptor.id}"; the request returns at most FEATURE_COUNT records and does not paginate. Drop pagination.offset.`);
3014
+ }
3015
+ if (request.returnGeometry === false) {
3016
+ throw new Error(`wms: Query.returnGeometry=false is not supported on GetFeatureInfo for source "${descriptor.id}"; the server controls geometry inclusion via INFO_FORMAT and the layer template. Drop returnGeometry or strip geometry client-side.`);
3017
+ }
3018
+ if (request.outSr !== undefined) {
3019
+ throw new Error(`wms: Query.outSr is not supported on GetFeatureInfo for source "${descriptor.id}"; honua-server projects the response in the request CRS and has no separate output-SR knob. Stamp the spatial filter geometry's spatialReference with the desired CRS instead, or reproject the result client-side.`);
3020
+ }
3021
+ }
3022
+ /**
3023
+ * Extract `(x, y)` and the WMS CRS from `Query.spatialFilter`. WMS only
3024
+ * exposes feature-info on a pixel coordinate, so the canonical
3025
+ * `Source.query()` is restricted to point spatial filters. The CRS is
3026
+ * derived from the geometry's `spatialReference` (`wkid` / `latestWkid` /
3027
+ * `wkt`) and falls back to `CRS:84` — the WMS 1.3.0 longitude/latitude
3028
+ * code that preserves the canonical `(x, y)` axis order — when the
3029
+ * caller did not stamp the geometry with a spatial reference. Non-point
3030
+ * geometries throw `HonuaCapabilityNotSupportedError`. `Query.outSr` is
3031
+ * intentionally not consulted here: it is the **output** spatial
3032
+ * reference for projected results, not the input CRS for the
3033
+ * GetFeatureInfo wire request.
3034
+ */
3035
+ function wmsExtractPointFromQuery(request, descriptor) {
3036
+ const filter = request?.spatialFilter;
3037
+ if (!filter) {
3038
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3039
+ }
3040
+ const geom = filter.geometry;
3041
+ if (filter.geometryType !== "esriGeometryPoint") {
3042
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3043
+ }
3044
+ const crs = wmsCrsFromGeometrySpatialReference(geom?.spatialReference);
3045
+ if (geom && typeof geom.x === "number" && typeof geom.y === "number") {
3046
+ return { x: geom.x, y: geom.y, crs };
3047
+ }
3048
+ if (geom && Array.isArray(geom.coordinates) && geom.coordinates.length >= 2) {
3049
+ const [x, y] = geom.coordinates;
3050
+ if (typeof x === "number" && typeof y === "number")
3051
+ return { x, y, crs };
3052
+ }
3053
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3054
+ }
3055
+ /**
3056
+ * Translate a geometry's `spatialReference` (Esri-style `{ wkid?,
3057
+ * latestWkid?, wkt? }`) into a WMS-compatible CRS code. `wkid` /
3058
+ * `latestWkid` map to `EPSG:N`. A non-empty `wkt` is passed through
3059
+ * verbatim — the server validates it. When no recognizable hint is
3060
+ * present the function returns `CRS:84` so the canonical (x, y) axis
3061
+ * order is preserved on the wire.
3062
+ */
3063
+ function wmsCrsFromGeometrySpatialReference(spatialReference) {
3064
+ if (spatialReference && typeof spatialReference === "object") {
3065
+ const sr = spatialReference;
3066
+ if (typeof sr.latestWkid === "number" && Number.isFinite(sr.latestWkid)) {
3067
+ return `EPSG:${sr.latestWkid}`;
3068
+ }
3069
+ if (typeof sr.wkid === "number" && Number.isFinite(sr.wkid)) {
3070
+ return `EPSG:${sr.wkid}`;
3071
+ }
3072
+ if (typeof sr.wkt === "string" && sr.wkt.trim().length > 0) {
3073
+ return sr.wkt.trim();
3074
+ }
3075
+ }
3076
+ return "CRS:84";
3077
+ }
3078
+ function toStacRequest(request, collectionScope) {
3079
+ const out = {};
3080
+ if (request?.where !== undefined) {
3081
+ out.filter = request.where;
3082
+ out.filterLang = "cql2-text";
3083
+ }
3084
+ if (request?.outFields && request.outFields.length > 0) {
3085
+ out.fields = { include: [...request.outFields] };
3086
+ }
3087
+ if (request?.pagination?.limit !== undefined)
3088
+ out.limit = request.pagination.limit;
3089
+ if (request?.pagination?.offset !== undefined)
3090
+ out.offset = request.pagination.offset;
3091
+ if (request?.orderBy && request.orderBy.length > 0) {
3092
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3093
+ }
3094
+ if (request?.spatialFilter) {
3095
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3096
+ throw new Error(`stac: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to STAC bbox.`);
3097
+ }
3098
+ const env = request.spatialFilter.geometry;
3099
+ if (typeof env.xmin === "number" &&
3100
+ typeof env.ymin === "number" &&
3101
+ typeof env.xmax === "number" &&
3102
+ typeof env.ymax === "number") {
3103
+ out.bbox = [env.xmin, env.ymin, env.xmax, env.ymax];
3104
+ }
3105
+ }
3106
+ if (collectionScope !== undefined && collectionScope !== "") {
3107
+ out.collections = [String(collectionScope)];
3108
+ }
3109
+ if (request?.signal)
3110
+ out.signal = request.signal;
3111
+ return out;
3112
+ }
3113
+ function toTypedFeatureFromStac(feature) {
3114
+ return {
3115
+ attributes: (feature.properties ?? {}),
3116
+ geometry: feature.geometry,
3117
+ };
3118
+ }
3119
+ function toOgcRecordsRequest(request) {
3120
+ if (!request)
3121
+ return {};
3122
+ const out = {};
3123
+ if (request.where !== undefined) {
3124
+ out.filter = request.where;
3125
+ out.filterLang = "cql2-text";
3126
+ }
3127
+ if (request.outFields && request.outFields.length > 0)
3128
+ out.properties = [...request.outFields];
3129
+ if (request.pagination) {
3130
+ if (request.pagination.limit !== undefined)
3131
+ out.limit = request.pagination.limit;
3132
+ if (request.pagination.offset !== undefined)
3133
+ out.offset = request.pagination.offset;
3134
+ }
3135
+ if (request.orderBy && request.orderBy.length > 0) {
3136
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3137
+ }
3138
+ if (request.spatialFilter) {
3139
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3140
+ throw new Error(`ogc-records: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to Records bbox.`);
3141
+ }
3142
+ const rel = request.spatialFilter.spatialRel;
3143
+ if (rel !== undefined && rel !== "esriSpatialRelIntersects" && rel !== "esriSpatialRelEnvelopeIntersects") {
3144
+ throw new Error(`ogc-records: spatialFilter.spatialRel "${rel}" is not supported; the Records bbox parameter only expresses envelope-intersects.`);
3145
+ }
3146
+ const env = request.spatialFilter.geometry;
3147
+ if (typeof env.xmin === "number" &&
3148
+ typeof env.ymin === "number" &&
3149
+ typeof env.xmax === "number" &&
3150
+ typeof env.ymax === "number") {
3151
+ out.bbox = [env.xmin, env.ymin, env.xmax, env.ymax];
3152
+ }
3153
+ }
3154
+ if (request.signal)
3155
+ out.signal = request.signal;
3156
+ return out;
3157
+ }
3158
+ function toTypedFeatureFromOgcRecord(feature) {
3159
+ return {
3160
+ attributes: (feature.properties ?? {}),
3161
+ geometry: feature.geometry,
3162
+ };
3163
+ }
3164
+ /**
3165
+ * The core paging helpers default `maxPages` to 100, which would silently
3166
+ * truncate the canonical `queryAll()` / `stream()` whose contract is to
3167
+ * drain until the source is exhausted. Override with
3168
+ * `Number.MAX_SAFE_INTEGER` so the helper loop only terminates on an empty
3169
+ * page or short page.
3170
+ *
3171
+ * `normalizeMaxPages` / the inline GeoServices checks require a finite
3172
+ * integer, so `Infinity` cannot be used as the sentinel.
3173
+ */
3174
+ function withUnboundedMaxPages(params) {
3175
+ return { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3176
+ }
3177
+ /**
3178
+ * `HonuaFeatureLayer.queryFeaturesAll` / `HonuaMapLayer.queryFeaturesAll` only
3179
+ * stop on an empty or short page — they have no total-row cap. When the
3180
+ * canonical `Query.pagination.limit` supplies a cap, derive the per-page size
3181
+ * *and* `maxPages` from it so the paging loop fetches at most `limit + 1`
3182
+ * rows (the extra row lets `applyQueryAllLimit` stamp `exceededTransferLimit`
3183
+ * when more records exist). Without this, `queryAll({ pagination: { limit: 1 } })`
3184
+ * would keep issuing `resultOffset=0,1,2,…` until the source is drained.
3185
+ */
3186
+ function withPagingBounds(params, limit) {
3187
+ if (typeof limit !== "number" || limit < 0) {
3188
+ return { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3189
+ }
3190
+ const pageSize = Math.max(1, Math.min(Math.max(limit, 1), 2000));
3191
+ const target = limit + 1;
3192
+ const maxPages = Math.max(1, Math.ceil(target / pageSize));
3193
+ return { ...params, pageSize, maxPages };
3194
+ }
3195
+ /**
3196
+ * `HonuaFeatureLayer.queryFeaturesStream` / `HonuaMapLayer.queryFeaturesStream`
3197
+ * derive per-page size from `pageSize` and default to 2000 when it is
3198
+ * omitted. `toFeatureLayerRequest` maps `pagination.limit` to
3199
+ * `resultRecordCount`, which the stream helper then overwrites with its own
3200
+ * `pageSize`. Without this bridge `source.stream({ pagination: { limit: 10 } })`
3201
+ * would still fetch 2000-row pages. Treat `pagination.limit` as the caller's
3202
+ * per-batch budget in streaming mode (it carries the same meaning as
3203
+ * `resultRecordCount` for a single-page `query()`).
3204
+ */
3205
+ function withStreamPageSize(params, limit) {
3206
+ const base = { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3207
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1) {
3208
+ return base;
3209
+ }
3210
+ return { ...base, pageSize: Math.max(1, Math.trunc(limit)) };
3211
+ }
3212
+ function applyQueryAllLimit(features, limit) {
3213
+ if (typeof limit !== "number" || limit < 0) {
3214
+ return { features, exceededTransferLimit: false };
3215
+ }
3216
+ if (features.length <= limit) {
3217
+ return { features, exceededTransferLimit: false };
3218
+ }
3219
+ return { features: features.slice(0, limit), exceededTransferLimit: true };
3220
+ }
3221
+ function toFeatureLayerRequest(request) {
3222
+ if (!request)
3223
+ return {};
3224
+ const out = {};
3225
+ if (request.where !== undefined)
3226
+ out.where = request.where;
3227
+ if (request.outFields && request.outFields.length > 0)
3228
+ out.outFields = [...request.outFields];
3229
+ if (request.returnGeometry !== undefined)
3230
+ out.returnGeometry = request.returnGeometry;
3231
+ if (request.outSr !== undefined)
3232
+ out.outSr = request.outSr;
3233
+ if (request.orderBy && request.orderBy.length > 0) {
3234
+ out.orderByFields = request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " DESC" : ""}`).join(",");
3235
+ }
3236
+ if (request.spatialFilter) {
3237
+ out.geometry = request.spatialFilter.geometry;
3238
+ out.geometryType = request.spatialFilter.geometryType;
3239
+ if (request.spatialFilter.spatialRel)
3240
+ out.spatialRel = request.spatialFilter.spatialRel;
3241
+ }
3242
+ if (request.pagination) {
3243
+ if (request.pagination.offset !== undefined)
3244
+ out.resultOffset = request.pagination.offset;
3245
+ if (request.pagination.limit !== undefined)
3246
+ out.resultRecordCount = request.pagination.limit;
3247
+ }
3248
+ if (request.signal)
3249
+ out.signal = request.signal;
3250
+ return out;
3251
+ }
3252
+ /**
3253
+ * Convert a translated GeoServices request into an extent-only variant. The
3254
+ * caller's spatial filter / outSr / signal / orderByFields must survive; only
3255
+ * paging and geometry-return are dropped, and `returnExtentOnly=true` is
3256
+ * stamped via `extraParams`. The returnCountOnly/ExtentOnly/IdsOnly side
3257
+ * channel lives in the untyped `extraParams` bag because it sits outside
3258
+ * `QueryFeaturesRequest`.
3259
+ */
3260
+ function toExtentOnlyRequest(params) {
3261
+ const { resultOffset: _offset, resultRecordCount: _count, outFields: _outFields, ...rest } = params;
3262
+ void _offset;
3263
+ void _count;
3264
+ void _outFields;
3265
+ return {
3266
+ ...rest,
3267
+ returnGeometry: false,
3268
+ extraParams: {
3269
+ ...(rest.extraParams ?? {}),
3270
+ returnExtentOnly: true,
3271
+ },
3272
+ };
3273
+ }
3274
+ /**
3275
+ * True when the canonical `Query` carries constraints that restrict which
3276
+ * records count towards the extent. Used by the degraded OGC fallback to
3277
+ * decide whether the collection metadata bbox (unfiltered) is a safe
3278
+ * shortcut or whether the extent must be computed client-side over the
3279
+ * matching records.
3280
+ */
3281
+ function hasExtentFilter(request) {
3282
+ if (!request)
3283
+ return false;
3284
+ if (request.where !== undefined && request.where !== "")
3285
+ return true;
3286
+ if (request.spatialFilter)
3287
+ return true;
3288
+ return false;
3289
+ }
3290
+ function computeExtentFromOgcFeatures(features) {
3291
+ let xmin = Number.POSITIVE_INFINITY;
3292
+ let ymin = Number.POSITIVE_INFINITY;
3293
+ let xmax = Number.NEGATIVE_INFINITY;
3294
+ let ymax = Number.NEGATIVE_INFINITY;
3295
+ let saw = false;
3296
+ for (const feature of features) {
3297
+ visitGeometryCoords(feature.geometry, (x, y) => {
3298
+ if (x < xmin)
3299
+ xmin = x;
3300
+ if (y < ymin)
3301
+ ymin = y;
3302
+ if (x > xmax)
3303
+ xmax = x;
3304
+ if (y > ymax)
3305
+ ymax = y;
3306
+ saw = true;
3307
+ });
3308
+ }
3309
+ if (!saw)
3310
+ return { extent: null, count: features.length };
3311
+ return { extent: { xmin, ymin, xmax, ymax }, count: features.length };
3312
+ }
3313
+ function visitGeometryCoords(geometry, visit) {
3314
+ if (typeof geometry !== "object" || geometry === null)
3315
+ return;
3316
+ const geom = geometry;
3317
+ if (Array.isArray(geom.geometries)) {
3318
+ for (const inner of geom.geometries)
3319
+ visitGeometryCoords(inner, visit);
3320
+ return;
3321
+ }
3322
+ visitCoordinates(geom.coordinates, visit);
3323
+ }
3324
+ function visitCoordinates(coords, visit) {
3325
+ if (!Array.isArray(coords))
3326
+ return;
3327
+ if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
3328
+ visit(coords[0], coords[1]);
3329
+ return;
3330
+ }
3331
+ for (const inner of coords)
3332
+ visitCoordinates(inner, visit);
3333
+ }
3334
+ function extractExtentEnvelope(response) {
3335
+ if (typeof response !== "object" || response === null)
3336
+ return { extent: null };
3337
+ const obj = response;
3338
+ const extent = typeof obj.extent === "object" && obj.extent !== null
3339
+ ? obj.extent
3340
+ : null;
3341
+ const count = typeof obj.count === "number" && Number.isFinite(obj.count) ? obj.count : undefined;
3342
+ return { extent, count };
3343
+ }
3344
+ function toOgcRequest(request) {
3345
+ if (!request)
3346
+ return {};
3347
+ const out = {};
3348
+ if (request.where !== undefined)
3349
+ out.filter = request.where;
3350
+ if (request.outFields && request.outFields.length > 0)
3351
+ out.properties = [...request.outFields];
3352
+ if (request.outSr !== undefined)
3353
+ out.crs = String(request.outSr);
3354
+ if (request.pagination) {
3355
+ if (request.pagination.limit !== undefined)
3356
+ out.limit = request.pagination.limit;
3357
+ if (request.pagination.offset !== undefined)
3358
+ out.offset = request.pagination.offset;
3359
+ }
3360
+ if (request.orderBy && request.orderBy.length > 0) {
3361
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3362
+ }
3363
+ if (request.spatialFilter) {
3364
+ // OGC API Features only exposes bbox at /items; arbitrary geometry types
3365
+ // would need CQL2 (which this adapter does not yet emit). Refuse the
3366
+ // request explicitly instead of silently dropping the constraint and
3367
+ // returning unfiltered features, which would look like a silent data
3368
+ // quality bug to the caller.
3369
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3370
+ throw new Error(`ogc-features: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to OGC bbox. Convert the geometry to an envelope or use a GeoServices source.`);
3371
+ }
3372
+ // The OGC bbox parameter is defined as an envelope-intersects predicate
3373
+ // (OGC 17-069r4 §7.15.3). `contains`, `within`, `crosses`, etc. would
3374
+ // require CQL2 which this adapter does not yet emit. Refuse the request
3375
+ // rather than fall back to bbox semantics — a "within" request that
3376
+ // silently returns every intersecting feature is a correctness bug.
3377
+ const rel = request.spatialFilter.spatialRel;
3378
+ if (rel !== undefined && rel !== "esriSpatialRelIntersects" && rel !== "esriSpatialRelEnvelopeIntersects") {
3379
+ throw new Error(`ogc-features: spatialFilter.spatialRel "${rel}" is not supported; the OGC bbox parameter only expresses envelope-intersects. Use a GeoServices source or convert to an intersects predicate.`);
3380
+ }
3381
+ const env = request.spatialFilter.geometry;
3382
+ if (typeof env.xmin === "number" &&
3383
+ typeof env.ymin === "number" &&
3384
+ typeof env.xmax === "number" &&
3385
+ typeof env.ymax === "number") {
3386
+ out.bbox = `${env.xmin},${env.ymin},${env.xmax},${env.ymax}`;
3387
+ }
3388
+ }
3389
+ if (request.signal)
3390
+ out.signal = request.signal;
3391
+ return out;
3392
+ }
3393
+ /**
3394
+ * OGC operations that must inspect the full matching record set (`queryAggregate`
3395
+ * and degraded `queryExtent`) cannot inherit caller pagination. `limit` /
3396
+ * `offset` would silently materialize only a window of the match set and break
3397
+ * the whole-result contract those methods promise.
3398
+ */
3399
+ function toOgcMaterializedRequest(request) {
3400
+ const { limit: _limit, offset: _offset, ...out } = toOgcRequest(request);
3401
+ void _limit;
3402
+ void _offset;
3403
+ return out;
3404
+ }
3405
+ function featureLayerResultFromTyped(response) {
3406
+ return {
3407
+ features: response.features ?? [],
3408
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3409
+ fields: response.fields,
3410
+ };
3411
+ }
3412
+ function featureLayerResultFromUntyped(response) {
3413
+ const features = (response.features ?? []).map((toTypedFeature));
3414
+ return {
3415
+ features,
3416
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3417
+ fields: response.fields,
3418
+ };
3419
+ }
3420
+ function toTypedFeature(feature) {
3421
+ return {
3422
+ attributes: feature.attributes,
3423
+ geometry: feature.geometry,
3424
+ };
3425
+ }
3426
+ function toTypedFeatureFromOgc(feature) {
3427
+ return {
3428
+ attributes: (feature.properties ?? {}),
3429
+ geometry: feature.geometry,
3430
+ };
3431
+ }
3432
+ /**
3433
+ * Build the GeoServices aggregation fields that must live on the top-level
3434
+ * `QueryFeaturesRequest` / `MapLayerQueryRequest`. Both the REST serializer
3435
+ * (`client.ts`) and the gRPC adapter (`grpc-adapter.ts`) read `outStatistics`
3436
+ * and `groupByFieldsForStatistics` from the request root; stashing them in
3437
+ * `extraParams` would be silently dropped by the gRPC path.
3438
+ */
3439
+ function buildAggregationFields(aggregation) {
3440
+ const stats = aggregation.metrics.map((m) => ({
3441
+ statisticType: toGeoServicesStatisticType(m.fn),
3442
+ onStatisticField: m.field,
3443
+ outStatisticFieldName: m.alias ?? `${m.fn}_${m.field}`,
3444
+ }));
3445
+ const out = {
3446
+ outStatistics: stats,
3447
+ returnGeometry: false,
3448
+ aggregateAlias: stats[0]?.outStatisticFieldName,
3449
+ };
3450
+ if (aggregation.groupBy && aggregation.groupBy.length > 0) {
3451
+ out.groupByFieldsForStatistics = aggregation.groupBy.join(",");
3452
+ }
3453
+ return out;
3454
+ }
3455
+ function toGeoServicesStatisticType(fn) {
3456
+ switch (fn) {
3457
+ case "stddev":
3458
+ return "stddev";
3459
+ case "var":
3460
+ return "var";
3461
+ default:
3462
+ return fn;
3463
+ }
3464
+ }
3465
+ function aggregateResultFromFeatureLayer(response, _aggregateAlias) {
3466
+ const aggregateRows = (response.features ?? []).map((f) => f.attributes);
3467
+ return {
3468
+ features: [],
3469
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3470
+ aggregateRows,
3471
+ fields: response.fields,
3472
+ };
3473
+ }
3474
+ function aggregateResultFromUntyped(response, _alias) {
3475
+ const aggregateRows = (response.features ?? []).map((f) => f.attributes);
3476
+ return {
3477
+ features: [],
3478
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3479
+ aggregateRows,
3480
+ fields: response.fields,
3481
+ };
3482
+ }
3483
+ function clientSideAggregate(features, aggregation) {
3484
+ const groupKeys = aggregation.groupBy ?? [];
3485
+ if (groupKeys.length === 0) {
3486
+ return [computeMetrics(features, aggregation.metrics)];
3487
+ }
3488
+ const groups = new Map();
3489
+ for (const feature of features) {
3490
+ const key = groupKeys.map((field) => String(readField(feature.attributes, field) ?? "")).join("\u0000");
3491
+ let bucket = groups.get(key);
3492
+ if (!bucket) {
3493
+ bucket = [];
3494
+ groups.set(key, bucket);
3495
+ }
3496
+ bucket.push(feature);
3497
+ }
3498
+ const rows = [];
3499
+ for (const bucket of groups.values()) {
3500
+ const head = bucket[0];
3501
+ const row = {};
3502
+ for (const field of groupKeys) {
3503
+ row[field] = readField(head.attributes, field);
3504
+ }
3505
+ Object.assign(row, computeMetrics(bucket, aggregation.metrics));
3506
+ rows.push(row);
3507
+ }
3508
+ return rows;
3509
+ }
3510
+ function computeMetrics(features, metrics) {
3511
+ const out = {};
3512
+ for (const metric of metrics) {
3513
+ const alias = metric.alias ?? `${metric.fn}_${metric.field}`;
3514
+ const values = features
3515
+ .map((f) => readField(f.attributes, metric.field))
3516
+ .filter((v) => typeof v === "number" && Number.isFinite(v));
3517
+ out[alias] = applyAggregation(metric.fn, values, features.length);
3518
+ }
3519
+ return out;
3520
+ }
3521
+ function applyAggregation(fn, values, totalLength) {
3522
+ if (fn === "count")
3523
+ return totalLength;
3524
+ if (values.length === 0)
3525
+ return 0;
3526
+ switch (fn) {
3527
+ case "sum":
3528
+ return values.reduce((a, b) => a + b, 0);
3529
+ case "avg":
3530
+ return values.reduce((a, b) => a + b, 0) / values.length;
3531
+ case "min":
3532
+ return Math.min(...values);
3533
+ case "max":
3534
+ return Math.max(...values);
3535
+ case "stddev":
3536
+ case "var": {
3537
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
3538
+ const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
3539
+ return fn === "var" ? variance : Math.sqrt(variance);
3540
+ }
3541
+ default:
3542
+ return 0;
3543
+ }
3544
+ }
3545
+ function readField(attributes, field) {
3546
+ if (typeof attributes !== "object" || attributes === null)
3547
+ return undefined;
3548
+ return attributes[field];
3549
+ }
3550
+ // ── Edit / related / attachment translators ───────────────────
3551
+ function toApplyEditsRequest(envelope) {
3552
+ const out = {};
3553
+ if (envelope.adds && envelope.adds.length > 0) {
3554
+ out.adds = envelope.adds.map((f) => canonicalToHonuaFeature(f));
3555
+ }
3556
+ if (envelope.updates && envelope.updates.length > 0) {
3557
+ out.updates = envelope.updates.map((f) => canonicalToHonuaFeature(f));
3558
+ }
3559
+ if (envelope.deletes && envelope.deletes.length > 0) {
3560
+ const numericIds = [];
3561
+ let allNumeric = true;
3562
+ for (const id of envelope.deletes) {
3563
+ const parsed = Number(id);
3564
+ if (Number.isFinite(parsed)) {
3565
+ numericIds.push(parsed);
3566
+ }
3567
+ else {
3568
+ allNumeric = false;
3569
+ break;
3570
+ }
3571
+ }
3572
+ out.deletes = allNumeric ? numericIds : envelope.deletes.map(String).join(",");
3573
+ }
3574
+ if (envelope.rollbackOnFailure !== undefined) {
3575
+ out.rollbackOnFailure = envelope.rollbackOnFailure;
3576
+ }
3577
+ if (envelope.signal) {
3578
+ out.signal = envelope.signal;
3579
+ }
3580
+ return out;
3581
+ }
3582
+ function canonicalToHonuaFeature(feature) {
3583
+ const attributes = { ...feature.attributes };
3584
+ // GeoServices addresses updates by `OBJECTID` inside attributes; populate it
3585
+ // from the canonical `id` when the caller supplied one but did not embed it
3586
+ // in attributes themselves. Skip when an OBJECTID is already present so a
3587
+ // mismatch surfaces as a server-side validation error rather than being
3588
+ // silently overwritten.
3589
+ if (feature.id !== undefined && feature.id !== null && attributes.OBJECTID === undefined) {
3590
+ attributes.OBJECTID = feature.id;
3591
+ }
3592
+ const out = { attributes };
3593
+ if (feature.geometry !== undefined) {
3594
+ out.geometry = feature.geometry;
3595
+ }
3596
+ return out;
3597
+ }
3598
+ function canonicalEditResult(response) {
3599
+ return {
3600
+ added: (response.addResults ?? []).map(toEditOutcome),
3601
+ updated: (response.updateResults ?? []).map(toEditOutcome),
3602
+ deleted: (response.deleteResults ?? []).map(toEditOutcome),
3603
+ };
3604
+ }
3605
+ function toEditOutcome(result) {
3606
+ const out = { success: result.success };
3607
+ if (typeof result.objectId === "number" && Number.isFinite(result.objectId)) {
3608
+ out.id = result.objectId;
3609
+ }
3610
+ if (result.error) {
3611
+ out.error = { code: result.error.code, description: result.error.description };
3612
+ }
3613
+ return out;
3614
+ }
3615
+ function toRelatedRecordsRequest(request) {
3616
+ const out = { relationshipId: request.relationshipId };
3617
+ if (request.sourceIds.length > 0) {
3618
+ const numeric = [];
3619
+ let allNumeric = true;
3620
+ for (const id of request.sourceIds) {
3621
+ const parsed = Number(id);
3622
+ if (Number.isFinite(parsed))
3623
+ numeric.push(parsed);
3624
+ else {
3625
+ allNumeric = false;
3626
+ break;
3627
+ }
3628
+ }
3629
+ out.objectIds = allNumeric ? numeric : request.sourceIds.map(String).join(",");
3630
+ }
3631
+ if (request.where !== undefined)
3632
+ out.where = request.where;
3633
+ if (request.outFields && request.outFields.length > 0)
3634
+ out.outFields = [...request.outFields];
3635
+ if (request.returnGeometry !== undefined)
3636
+ out.returnGeometry = request.returnGeometry;
3637
+ if (request.signal)
3638
+ out.signal = request.signal;
3639
+ return out;
3640
+ }
3641
+ function canonicalRelatedResult(response) {
3642
+ const groups = (response.relatedRecordGroups ?? []).map((g) => ({
3643
+ sourceId: g.objectId,
3644
+ features: (g.relatedRecords ?? []).map((f) => ({
3645
+ attributes: f.attributes,
3646
+ geometry: f.geometry ?? null,
3647
+ })),
3648
+ }));
3649
+ const out = { groups };
3650
+ if (response.fields)
3651
+ out.fields = response.fields;
3652
+ return out;
3653
+ }
3654
+ function featureToGeoJsonFeature(feature) {
3655
+ const out = {
3656
+ type: "Feature",
3657
+ geometry: feature.geometry ?? null,
3658
+ properties: { ...feature.attributes },
3659
+ };
3660
+ if (feature.id !== undefined && feature.id !== null) {
3661
+ out.id = feature.id;
3662
+ }
3663
+ return out;
3664
+ }
3665
+ function editErrorFromCatch(err) {
3666
+ if (err instanceof HonuaHttpError) {
3667
+ return { code: err.statusCode, description: err.message };
3668
+ }
3669
+ if (err instanceof Error) {
3670
+ // Fall back to ad-hoc `status` / `code` / `statusCode` shapes that
3671
+ // third-party errors might carry; default to 500 when nothing fits so
3672
+ // the per-item EditOutcome still surfaces a numeric code.
3673
+ const candidate = err;
3674
+ const code = typeof candidate.statusCode === "number"
3675
+ ? candidate.statusCode
3676
+ : typeof candidate.status === "number"
3677
+ ? candidate.status
3678
+ : typeof candidate.code === "number"
3679
+ ? candidate.code
3680
+ : 500;
3681
+ return { code, description: err.message };
3682
+ }
3683
+ return { code: 500, description: String(err) };
3684
+ }
3685
+ function featureLayerAttachmentApi(descriptor, caps, layer) {
3686
+ function ensureAttachments() {
3687
+ if (!caps.has("attachments")) {
3688
+ throw new HonuaCapabilityNotSupportedError("attachments", descriptor.protocol, descriptor.id);
3689
+ }
3690
+ }
3691
+ return {
3692
+ async query(request) {
3693
+ ensureAttachments();
3694
+ const response = await layer.queryAttachments({
3695
+ ...(request?.parentIds
3696
+ ? { objectIds: request.parentIds.map(toAttachmentNumericId).filter(isFiniteNumberStrict) }
3697
+ : {}),
3698
+ ...(request?.where !== undefined ? { where: request.where } : {}),
3699
+ ...(request?.signal ? { signal: request.signal } : {}),
3700
+ });
3701
+ return (response.attachmentGroups ?? []).map((group) => ({
3702
+ parentId: group.parentObjectId,
3703
+ attachments: (group.attachmentInfos ?? []).map(toAttachmentInfo(group.parentObjectId)),
3704
+ }));
3705
+ },
3706
+ async list(parentId, options) {
3707
+ ensureAttachments();
3708
+ const response = await layer.listAttachments({
3709
+ objectId: parentId,
3710
+ ...(options?.signal ? { signal: options.signal } : {}),
3711
+ });
3712
+ const numericParent = Number(parentId);
3713
+ return (response.attachmentInfos ?? []).map(toAttachmentInfo(Number.isFinite(numericParent) ? numericParent : parentId));
3714
+ },
3715
+ async add(request) {
3716
+ ensureAttachments();
3717
+ const response = await layer.addAttachment({
3718
+ objectId: request.parentId,
3719
+ attachment: request.attachment,
3720
+ ...(request.name ? { name: request.name } : {}),
3721
+ ...(request.contentType ? { contentType: request.contentType } : {}),
3722
+ ...(request.signal ? { signal: request.signal } : {}),
3723
+ });
3724
+ return toAttachmentEditOutcome(response.addAttachmentResult, request.parentId);
3725
+ },
3726
+ async update(request) {
3727
+ ensureAttachments();
3728
+ const response = await layer.updateAttachment({
3729
+ objectId: request.parentId,
3730
+ attachmentId: request.attachmentId,
3731
+ attachment: request.attachment,
3732
+ ...(request.name ? { name: request.name } : {}),
3733
+ ...(request.contentType ? { contentType: request.contentType } : {}),
3734
+ ...(request.signal ? { signal: request.signal } : {}),
3735
+ });
3736
+ return toAttachmentEditOutcome(response.updateAttachmentResult, request.parentId);
3737
+ },
3738
+ async delete(request) {
3739
+ ensureAttachments();
3740
+ const numericIds = request.attachmentIds.map(toAttachmentNumericId).filter(isFiniteNumberStrict);
3741
+ const response = await layer.deleteAttachments({
3742
+ objectId: request.parentId,
3743
+ attachmentIds: numericIds.length === request.attachmentIds.length ? numericIds : request.attachmentIds.map(String).join(","),
3744
+ ...(request.signal ? { signal: request.signal } : {}),
3745
+ });
3746
+ return (response.deleteAttachmentResults ?? []).map((r) => toAttachmentEditOutcome(r, request.parentId));
3747
+ },
3748
+ };
3749
+ }
3750
+ function toAttachmentInfo(parentId) {
3751
+ return (info) => {
3752
+ const out = {
3753
+ id: info.id,
3754
+ parentId: info.parentObjectId ?? parentId,
3755
+ };
3756
+ if (info.name !== undefined)
3757
+ out.name = info.name;
3758
+ if (info.contentType !== undefined)
3759
+ out.contentType = info.contentType;
3760
+ if (info.size !== undefined)
3761
+ out.size = info.size;
3762
+ return out;
3763
+ };
3764
+ }
3765
+ function toAttachmentEditOutcome(result, parentId) {
3766
+ const out = { success: result.success };
3767
+ if (parentId !== undefined)
3768
+ out.parentId = parentId;
3769
+ if (typeof result.objectId === "number" && Number.isFinite(result.objectId)) {
3770
+ out.attachmentId = result.objectId;
3771
+ }
3772
+ if (result.error) {
3773
+ out.error = { code: result.error.code, description: result.error.description };
3774
+ }
3775
+ return out;
3776
+ }
3777
+ function toAttachmentNumericId(id) {
3778
+ const parsed = Number(id);
3779
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
3780
+ }
3781
+ function isFiniteNumberStrict(n) {
3782
+ return Number.isFinite(n);
3783
+ }
3784
+ // ── Re-exports for downstream tickets ─────────────────────────
3785
+ /** Sentinel: `Capabilities` set that contains every defined capability. */
3786
+ export const ALL_CAPABILITIES = new Set(CAPABILITIES);
3787
+ /** Tag a `Protocol` as "first-party-supported by this build". */
3788
+ export const FIRST_PARTY_PROTOCOLS = new Set([
3789
+ "geoservices-feature-service",
3790
+ "geoservices-map-service",
3791
+ "geoservices-image-service",
3792
+ "geoservices-geometry-service",
3793
+ "geoservices-gp-service",
3794
+ "ogc-features",
3795
+ "ogc-tiles",
3796
+ "ogc-maps",
3797
+ "ogc-records",
3798
+ "stac",
3799
+ "wfs",
3800
+ "wms",
3801
+ "wmts",
3802
+ ]);