@honua/react 0.1.0-beta.0 → 0.1.1-beta.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 (315) hide show
  1. package/contract/discovery.d.ts +231 -0
  2. package/contract/discovery.d.ts.map +1 -0
  3. package/contract/discovery.js +554 -0
  4. package/contract/edit-sketch.d.ts +8 -0
  5. package/contract/edit-sketch.d.ts.map +1 -1
  6. package/contract/edit-sketch.js +15 -0
  7. package/contract/edit-snapping.d.ts +160 -0
  8. package/contract/edit-snapping.d.ts.map +1 -0
  9. package/contract/edit-snapping.js +675 -0
  10. package/contract/generated/projjson-v0.7-crs-validator.d.ts +35 -0
  11. package/contract/generated/projjson-v0.7-crs-validator.d.ts.map +1 -0
  12. package/contract/generated/projjson-v0.7-crs-validator.js +3941 -0
  13. package/contract/index.d.ts +11 -2
  14. package/contract/index.d.ts.map +1 -1
  15. package/contract/index.js +4 -1
  16. package/contract/pmtiles.d.ts +31 -0
  17. package/contract/pmtiles.d.ts.map +1 -1
  18. package/contract/pmtiles.js +60 -20
  19. package/contract/protocol-module.d.ts +70 -0
  20. package/contract/protocol-module.d.ts.map +1 -0
  21. package/contract/protocol-module.js +1 -0
  22. package/contract/schema-envelope.d.ts +26 -0
  23. package/contract/schema-envelope.d.ts.map +1 -0
  24. package/contract/schema-envelope.js +25 -0
  25. package/contract/schema.d.ts +582 -0
  26. package/contract/schema.d.ts.map +1 -0
  27. package/contract/schema.js +2483 -0
  28. package/contract/source-capability-support.d.ts +6 -0
  29. package/contract/source-capability-support.d.ts.map +1 -0
  30. package/contract/source-capability-support.js +154 -0
  31. package/contract/source.d.ts +35 -17
  32. package/contract/source.d.ts.map +1 -1
  33. package/contract/source.js +267 -71
  34. package/contract/types.d.ts +140 -4
  35. package/contract/types.d.ts.map +1 -1
  36. package/contract/types.js +6 -14
  37. package/core/bounded-json.d.ts +4 -0
  38. package/core/bounded-json.d.ts.map +1 -0
  39. package/core/bounded-json.js +38 -0
  40. package/core/cache-state.d.ts +9 -0
  41. package/core/cache-state.d.ts.map +1 -1
  42. package/core/cache-state.js +10 -0
  43. package/core/capabilities-xml.d.ts +33 -0
  44. package/core/capabilities-xml.d.ts.map +1 -0
  45. package/core/capabilities-xml.js +331 -0
  46. package/core/client.d.ts +4 -1
  47. package/core/client.d.ts.map +1 -1
  48. package/core/client.js +115 -29
  49. package/core/error-classifications.d.ts +153 -0
  50. package/core/error-classifications.d.ts.map +1 -0
  51. package/core/error-classifications.js +140 -0
  52. package/core/error-code-registry.d.ts +158 -0
  53. package/core/error-code-registry.d.ts.map +1 -0
  54. package/core/error-code-registry.js +143 -0
  55. package/core/error-envelope.d.ts +68 -0
  56. package/core/error-envelope.d.ts.map +1 -0
  57. package/core/error-envelope.js +357 -0
  58. package/core/errors.d.ts +45 -29
  59. package/core/errors.d.ts.map +1 -1
  60. package/core/errors.js +97 -50
  61. package/core/geoparquet-sql.d.ts +30 -2
  62. package/core/geoparquet-sql.d.ts.map +1 -1
  63. package/core/geoparquet-sql.js +38 -3
  64. package/core/odata-key-path.d.ts +12 -0
  65. package/core/odata-key-path.d.ts.map +1 -0
  66. package/core/odata-key-path.js +28 -0
  67. package/core/odata-write-codec.d.ts +66 -0
  68. package/core/odata-write-codec.d.ts.map +1 -0
  69. package/core/odata-write-codec.js +1083 -0
  70. package/core/odata.d.ts +59 -7
  71. package/core/odata.d.ts.map +1 -1
  72. package/core/odata.js +431 -99
  73. package/core/ogc-features.d.ts.map +1 -1
  74. package/core/ogc-features.js +3 -0
  75. package/core/ogc-maps.d.ts +7 -0
  76. package/core/ogc-maps.d.ts.map +1 -1
  77. package/core/ogc-maps.js +46 -7
  78. package/core/ogc-processes.d.ts +2 -1
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +33 -5
  81. package/core/ogc-records.d.ts +9 -0
  82. package/core/ogc-records.d.ts.map +1 -1
  83. package/core/ogc-records.js +78 -29
  84. package/core/ogc-tiles.d.ts +10 -1
  85. package/core/ogc-tiles.d.ts.map +1 -1
  86. package/core/ogc-tiles.js +56 -17
  87. package/core/path-utils.d.ts +9 -0
  88. package/core/path-utils.d.ts.map +1 -1
  89. package/core/path-utils.js +19 -0
  90. package/core/protocol-transport.d.ts +1 -0
  91. package/core/protocol-transport.d.ts.map +1 -1
  92. package/core/request-pipeline.d.ts.map +1 -1
  93. package/core/request-pipeline.js +1 -1
  94. package/core/spatial-filter.d.ts +8 -4
  95. package/core/spatial-filter.d.ts.map +1 -1
  96. package/core/spatial-filter.js +206 -16
  97. package/core/stac-static.d.ts +6 -2
  98. package/core/stac-static.d.ts.map +1 -1
  99. package/core/stac-static.js +260 -69
  100. package/core/surfaces.d.ts.map +1 -1
  101. package/core/surfaces.js +69 -5
  102. package/core/types.d.ts +125 -7
  103. package/core/types.d.ts.map +1 -1
  104. package/core/wfs-capabilities.d.ts +2 -0
  105. package/core/wfs-capabilities.d.ts.map +1 -1
  106. package/core/wfs-capabilities.js +8 -1
  107. package/core/wfs.d.ts.map +1 -1
  108. package/core/wfs.js +10 -0
  109. package/core/wms-capabilities.d.ts +56 -84
  110. package/core/wms-capabilities.d.ts.map +1 -1
  111. package/core/wms-capabilities.js +212 -509
  112. package/core/wmts-capabilities.d.ts +62 -62
  113. package/core/wmts-capabilities.d.ts.map +1 -1
  114. package/core/wmts-capabilities.js +224 -371
  115. package/esri-compat/editor.d.ts +8 -0
  116. package/esri-compat/editor.d.ts.map +1 -1
  117. package/esri-compat/editor.js +13 -0
  118. package/esri-compat/feature-layer.d.ts +2 -0
  119. package/esri-compat/feature-layer.d.ts.map +1 -1
  120. package/esri-compat/feature-layer.js +1 -0
  121. package/esri-compat/layer-list.d.ts +11 -0
  122. package/esri-compat/layer-list.d.ts.map +1 -1
  123. package/esri-compat/layer-list.js +49 -0
  124. package/esri-compat/legend.d.ts +10 -0
  125. package/esri-compat/legend.d.ts.map +1 -1
  126. package/esri-compat/legend.js +27 -0
  127. package/esri-compat/portal.d.ts +197 -0
  128. package/esri-compat/portal.d.ts.map +1 -0
  129. package/esri-compat/portal.js +251 -0
  130. package/esri-compat/renderer-objects.d.ts +40 -0
  131. package/esri-compat/renderer-objects.d.ts.map +1 -0
  132. package/esri-compat/renderer-objects.js +62 -0
  133. package/esri-compat/sketch.d.ts +43 -0
  134. package/esri-compat/sketch.d.ts.map +1 -1
  135. package/esri-compat/sketch.js +46 -0
  136. package/esri-compat/snapping.d.ts +43 -0
  137. package/esri-compat/snapping.d.ts.map +1 -0
  138. package/esri-compat/snapping.js +45 -0
  139. package/esri-compat/url.d.ts.map +1 -1
  140. package/esri-compat/url.js +1 -1
  141. package/esri-compat/widget-host.d.ts +84 -0
  142. package/esri-compat/widget-host.d.ts.map +1 -0
  143. package/esri-compat/widget-host.js +159 -0
  144. package/filter-registry/index.d.ts +145 -0
  145. package/filter-registry/index.d.ts.map +1 -0
  146. package/filter-registry/index.js +531 -0
  147. package/generated-app/errors.d.ts +18 -2
  148. package/generated-app/errors.d.ts.map +1 -1
  149. package/generated-app/errors.js +44 -11
  150. package/map/automatic-mount-integration.d.ts +134 -0
  151. package/map/automatic-mount-integration.d.ts.map +1 -0
  152. package/map/automatic-mount-integration.js +305 -0
  153. package/map/automatic-source-strategy.d.ts +125 -0
  154. package/map/automatic-source-strategy.d.ts.map +1 -0
  155. package/map/automatic-source-strategy.js +494 -0
  156. package/map/data-to-map-bridge.d.ts +293 -0
  157. package/map/data-to-map-bridge.d.ts.map +1 -0
  158. package/map/data-to-map-bridge.js +817 -0
  159. package/map/geojson-projection.d.ts +33 -0
  160. package/map/geojson-projection.d.ts.map +1 -0
  161. package/map/geojson-projection.js +118 -0
  162. package/map/index.d.ts +14 -0
  163. package/map/index.d.ts.map +1 -1
  164. package/map/index.js +8 -0
  165. package/map/raster-source-spec.d.ts +30 -0
  166. package/map/raster-source-spec.d.ts.map +1 -0
  167. package/map/raster-source-spec.js +190 -0
  168. package/map/raster-source-strategy.d.ts +65 -0
  169. package/map/raster-source-strategy.d.ts.map +1 -0
  170. package/map/raster-source-strategy.js +232 -0
  171. package/map/source-to-maplibre.d.ts +91 -0
  172. package/map/source-to-maplibre.d.ts.map +1 -0
  173. package/map/source-to-maplibre.js +454 -0
  174. package/map/temporal-playback.d.ts +139 -0
  175. package/map/temporal-playback.d.ts.map +1 -0
  176. package/map/temporal-playback.js +247 -0
  177. package/package.json +14 -1
  178. package/query-planner/canonical.d.ts +7 -0
  179. package/query-planner/canonical.d.ts.map +1 -0
  180. package/query-planner/canonical.js +145 -0
  181. package/query-planner/cql2-json.d.ts +17 -0
  182. package/query-planner/cql2-json.d.ts.map +1 -0
  183. package/query-planner/cql2-json.js +646 -0
  184. package/query-planner/diagnostics.d.ts +35 -0
  185. package/query-planner/diagnostics.d.ts.map +1 -0
  186. package/query-planner/diagnostics.js +373 -0
  187. package/query-planner/duckdb.d.ts +79 -0
  188. package/query-planner/duckdb.d.ts.map +1 -0
  189. package/query-planner/duckdb.js +758 -0
  190. package/query-planner/execution-context.d.ts +5 -0
  191. package/query-planner/execution-context.d.ts.map +1 -0
  192. package/query-planner/execution-context.js +50 -0
  193. package/query-planner/executor.d.ts +5 -0
  194. package/query-planner/executor.d.ts.map +1 -0
  195. package/query-planner/executor.js +223 -0
  196. package/query-planner/geoservices.d.ts +91 -0
  197. package/query-planner/geoservices.d.ts.map +1 -0
  198. package/query-planner/geoservices.js +667 -0
  199. package/query-planner/grpc.d.ts +100 -0
  200. package/query-planner/grpc.d.ts.map +1 -0
  201. package/query-planner/grpc.js +736 -0
  202. package/query-planner/index.d.ts +44 -0
  203. package/query-planner/index.d.ts.map +1 -0
  204. package/query-planner/index.js +30 -0
  205. package/query-planner/ir.d.ts +20 -0
  206. package/query-planner/ir.d.ts.map +1 -0
  207. package/query-planner/ir.js +417 -0
  208. package/query-planner/local-aggregate.d.ts +5 -0
  209. package/query-planner/local-aggregate.d.ts.map +1 -0
  210. package/query-planner/local-aggregate.js +126 -0
  211. package/query-planner/odata.d.ts +48 -0
  212. package/query-planner/odata.d.ts.map +1 -0
  213. package/query-planner/odata.js +662 -0
  214. package/query-planner/ogc-compiler.d.ts +55 -0
  215. package/query-planner/ogc-compiler.d.ts.map +1 -0
  216. package/query-planner/ogc-compiler.js +480 -0
  217. package/query-planner/ogc-features.d.ts +53 -0
  218. package/query-planner/ogc-features.d.ts.map +1 -0
  219. package/query-planner/ogc-features.js +617 -0
  220. package/query-planner/planner.d.ts +17 -0
  221. package/query-planner/planner.d.ts.map +1 -0
  222. package/query-planner/planner.js +818 -0
  223. package/query-planner/resource.d.ts +94 -0
  224. package/query-planner/resource.d.ts.map +1 -0
  225. package/query-planner/resource.js +526 -0
  226. package/query-planner/semantic-canonical.d.ts +15 -0
  227. package/query-planner/semantic-canonical.d.ts.map +1 -0
  228. package/query-planner/semantic-canonical.js +62 -0
  229. package/query-planner/semantic-compat.d.ts +10 -0
  230. package/query-planner/semantic-compat.d.ts.map +1 -0
  231. package/query-planner/semantic-compat.js +35 -0
  232. package/query-planner/semantic-compiler.d.ts +64 -0
  233. package/query-planner/semantic-compiler.d.ts.map +1 -0
  234. package/query-planner/semantic-compiler.js +81 -0
  235. package/query-planner/semantic-literals.d.ts +30 -0
  236. package/query-planner/semantic-literals.d.ts.map +1 -0
  237. package/query-planner/semantic-literals.js +133 -0
  238. package/query-planner/semantic-types.d.ts +265 -0
  239. package/query-planner/semantic-types.d.ts.map +1 -0
  240. package/query-planner/semantic-types.js +1 -0
  241. package/query-planner/semantic.d.ts +54 -0
  242. package/query-planner/semantic.d.ts.map +1 -0
  243. package/query-planner/semantic.js +1421 -0
  244. package/query-planner/types.d.ts +560 -0
  245. package/query-planner/types.d.ts.map +1 -0
  246. package/query-planner/types.js +49 -0
  247. package/query-planner/wfs-semantic.d.ts +58 -0
  248. package/query-planner/wfs-semantic.d.ts.map +1 -0
  249. package/query-planner/wfs-semantic.js +547 -0
  250. package/query-planner/wfs.d.ts +6 -0
  251. package/query-planner/wfs.d.ts.map +1 -0
  252. package/query-planner/wfs.js +71 -0
  253. package/react/external-map.d.ts +67 -0
  254. package/react/external-map.d.ts.map +1 -0
  255. package/react/external-map.js +63 -0
  256. package/react/honua-map.d.ts.map +1 -1
  257. package/react/honua-map.js +3 -2
  258. package/react/honua-source-layer.d.ts +110 -0
  259. package/react/honua-source-layer.d.ts.map +1 -0
  260. package/react/honua-source-layer.js +101 -0
  261. package/react/hooks.d.ts +28 -0
  262. package/react/hooks.d.ts.map +1 -1
  263. package/react/hooks.js +26 -4
  264. package/react/index.d.ts +8 -0
  265. package/react/index.d.ts.map +1 -1
  266. package/react/index.js +5 -0
  267. package/react/query-cache.d.ts +26 -4
  268. package/react/query-cache.d.ts.map +1 -1
  269. package/react/query-cache.js +51 -8
  270. package/react/selection.d.ts +177 -0
  271. package/react/selection.d.ts.map +1 -0
  272. package/react/selection.js +0 -0
  273. package/react/use-mounted-source.d.ts +69 -0
  274. package/react/use-mounted-source.d.ts.map +1 -0
  275. package/react/use-mounted-source.js +346 -0
  276. package/runtime/edit-snapping.d.ts +107 -0
  277. package/runtime/edit-snapping.d.ts.map +1 -0
  278. package/runtime/edit-snapping.js +190 -0
  279. package/runtime/errors.d.ts +3 -2
  280. package/runtime/errors.d.ts.map +1 -1
  281. package/runtime/errors.js +19 -2
  282. package/runtime/index.d.ts +6 -0
  283. package/runtime/index.d.ts.map +1 -1
  284. package/runtime/index.js +3 -0
  285. package/runtime/maplibre-renderer.d.ts +41 -0
  286. package/runtime/maplibre-renderer.d.ts.map +1 -0
  287. package/runtime/maplibre-renderer.js +322 -0
  288. package/runtime/query-tiles.d.ts +2 -1
  289. package/runtime/query-tiles.d.ts.map +1 -1
  290. package/runtime/query-tiles.js +6 -2
  291. package/runtime/source-bridge.d.ts +4 -4
  292. package/runtime/source-bridge.d.ts.map +1 -1
  293. package/runtime/source-bridge.js +9 -56
  294. package/runtime/style-interactions.d.ts +3 -2
  295. package/runtime/style-interactions.d.ts.map +1 -1
  296. package/runtime/style-interactions.js +11 -3
  297. package/runtime/terra-draw-sketch.d.ts +275 -0
  298. package/runtime/terra-draw-sketch.d.ts.map +1 -0
  299. package/runtime/terra-draw-sketch.js +404 -0
  300. package/source-capability-registry.d.ts +1 -0
  301. package/source-capability-registry.js +1 -0
  302. package/source-capability-types.d.ts +186 -0
  303. package/source-capability-types.js +7 -0
  304. package/style/index.d.ts +2 -0
  305. package/style/index.d.ts.map +1 -1
  306. package/style/index.js +2 -0
  307. package/style/renderers.d.ts +326 -0
  308. package/style/renderers.d.ts.map +1 -0
  309. package/style/renderers.js +517 -0
  310. package/webmap/convert-renderer.d.ts +24 -3
  311. package/webmap/convert-renderer.d.ts.map +1 -1
  312. package/webmap/convert-renderer.js +93 -112
  313. package/webmap/index.d.ts +1 -1
  314. package/webmap/index.d.ts.map +1 -1
  315. package/webmap/index.js +1 -1
@@ -22,10 +22,11 @@ import { HonuaStacStaticCatalog } from "../core/stac-static.js";
22
22
  import { HonuaStacSearch } from "../core/stac.js";
23
23
  import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatureCollection, HonuaOgcFeatures, } from "../core/surfaces.js";
24
24
  import { UNSUPPORTED_FES, compileSpatialFilter, compileWhere, geoJsonGeometryToGml, serializeFes, } from "../core/wfs-filter.js";
25
- import { HonuaWfs, HonuaWfsFeatureType } from "../core/wfs.js";
25
+ import { HonuaWfsFeatureType } from "../core/wfs.js";
26
26
  import { HonuaWms, HonuaWmsLayer, parseWmsLayerNames } from "../core/wms.js";
27
27
  import { HonuaWmts, HonuaWmtsLayer, HonuaWmtsTileset } from "../core/wmts.js";
28
- import { HonuaPmtilesArchive, stripPmtilesScheme } from "./pmtiles.js";
28
+ import { pmtilesProtocolModule } from "./pmtiles.js";
29
+ import { addCapabilitySupport, normalizeCapabilityDescriptor } from "./source-capability-support.js";
29
30
  import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
30
31
  /**
31
32
  * Construct a protocol-neutral `Dataset` over a `HonuaClient`.
@@ -85,7 +86,8 @@ export function createDataset(options) {
85
86
  const descriptors = new Map();
86
87
  const handles = new Map();
87
88
  let compatibilityPromise;
88
- for (const descriptor of sources) {
89
+ for (const input of sources) {
90
+ const descriptor = normalizeCapabilityDescriptor(input);
89
91
  if (descriptors.has(descriptor.id)) {
90
92
  throw new Error(`createDataset: duplicate source id "${descriptor.id}"`);
91
93
  }
@@ -95,13 +97,14 @@ export function createDataset(options) {
95
97
  const cached = handles.get(descriptor.id);
96
98
  if (cached)
97
99
  return cached;
98
- const built = buildBuiltInSource(descriptor, client, policy) ??
99
- resolveSource?.(descriptor, { client, capabilityPolicy: policy });
100
+ const builtIn = buildBuiltInSource(descriptor, client, policy);
101
+ const built = builtIn ?? resolveSource?.(descriptor, { client, capabilityPolicy: policy });
100
102
  if (!built) {
101
103
  throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
102
104
  }
103
- handles.set(descriptor.id, built);
104
- return built;
105
+ const source = builtIn ?? addCapabilitySupport(built, descriptor);
106
+ handles.set(descriptor.id, source);
107
+ return source;
105
108
  }
106
109
  return {
107
110
  id,
@@ -135,6 +138,15 @@ export function createDataset(options) {
135
138
  // ── Built-in resolver ─────────────────────────────────────────
136
139
  function buildBuiltInSource(descriptor, client, policy) {
137
140
  switch (descriptor.protocol) {
141
+ case "grpc":
142
+ // Honua gRPC is a transport-selectable fast path over the same
143
+ // canonical FeatureServer semantics, not a distinct adapter: the
144
+ // returned `HonuaFeatureLayer` transparently routes `queryFeatures`
145
+ // over gRPC-Web when `client.transport === "grpc-web"` (see
146
+ // `docs/protocol-capability-matrix.md`, "gRPC FeatureService"). The
147
+ // discovered descriptor's narrower `capabilities` (verified through a
148
+ // live gRPC parity probe in `connect-grpc.ts`) still gate execution.
149
+ return geoServicesFeatureSource(descriptor, client, policy);
138
150
  case "geoservices-feature-service":
139
151
  return geoServicesFeatureSource(descriptor, client, policy);
140
152
  case "geoservices-map-service":
@@ -196,9 +208,16 @@ function buildBuiltInSource(descriptor, client, policy) {
196
208
  * ```
197
209
  */
198
210
  export function geoServicesFeatureSource(descriptor, client, policy) {
211
+ descriptor = normalizeCapabilityDescriptor(descriptor);
199
212
  const { serviceId, layerId } = requireFeatureServiceLocator(descriptor);
200
213
  const layer = new HonuaFeatureLayer({ client, serviceId, layerId });
201
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"];
214
+ // Indexed by the descriptor's own protocol (not hardcoded to
215
+ // "geoservices-feature-service") so a `protocol: "grpc"` descriptor built
216
+ // without explicit capabilities falls back to the narrower gRPC default
217
+ // set rather than silently inheriting REST-only capabilities (`pbf`,
218
+ // `sql`, `attachments`, `queryRelated`) the gRPC FeatureService RPC
219
+ // surface does not support.
220
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES[descriptor.protocol];
202
221
  const adapterRegistry = {
203
222
  "geoservices-feature-service": layer,
204
223
  };
@@ -287,6 +306,7 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
287
306
  * ```
288
307
  */
289
308
  export function geoServicesMapServiceSource(descriptor, client, policy) {
309
+ descriptor = normalizeCapabilityDescriptor(descriptor);
290
310
  const { serviceId, layerId } = requireMapServiceLocator(descriptor);
291
311
  const service = new HonuaMapService({ client, serviceId });
292
312
  const layer = new HonuaMapLayer({ client, serviceId, layerId });
@@ -381,6 +401,7 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
381
401
  * ```
382
402
  */
383
403
  export function ogcFeaturesSource(descriptor, client, policy) {
404
+ descriptor = normalizeCapabilityDescriptor(descriptor);
384
405
  const { collectionId } = requireOgcLocator(descriptor);
385
406
  const layoutMode = ogcFeaturesLayoutMode(descriptor.locator.layout);
386
407
  const collection = new HonuaOgcFeatureCollection({
@@ -600,6 +621,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
600
621
  * ```
601
622
  */
602
623
  export function geoServicesImageSource(descriptor, client, policy) {
624
+ descriptor = normalizeCapabilityDescriptor(descriptor);
603
625
  const { serviceId } = requireImageServiceLocator(descriptor);
604
626
  const service = new HonuaImageService({ client, serviceId });
605
627
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"];
@@ -709,6 +731,7 @@ export function geoServicesImageSource(descriptor, client, policy) {
709
731
  * ```
710
732
  */
711
733
  export function geoServicesGeometryServiceSource(descriptor, client, policy) {
734
+ descriptor = normalizeCapabilityDescriptor(descriptor);
712
735
  const service = new HonuaGeometryService({ client });
713
736
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-geometry-service"];
714
737
  const adapterRegistry = {
@@ -731,6 +754,7 @@ export function geoServicesGeometryServiceSource(descriptor, client, policy) {
731
754
  * ```
732
755
  */
733
756
  export function geoServicesGPServiceSource(descriptor, client, policy) {
757
+ descriptor = normalizeCapabilityDescriptor(descriptor);
734
758
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-gp-service"];
735
759
  const { serviceId, taskName } = requireGPServiceLocator(descriptor, caps);
736
760
  const service = new HonuaGeoprocessingService({
@@ -758,15 +782,19 @@ export function geoServicesGPServiceSource(descriptor, client, policy) {
758
782
  * ```
759
783
  */
760
784
  export function ogcTilesSource(descriptor, client, policy) {
785
+ descriptor = normalizeCapabilityDescriptor(descriptor);
761
786
  const { collectionId, tileMatrixSetId } = requireOgcTilesLocator(descriptor);
762
787
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-tiles"];
788
+ const basePath = descriptor.locator.basePath;
763
789
  // Descriptors without a tileMatrixSetId cannot construct a usable
764
790
  // HonuaOgcTileset (every tile route requires `tileMatrixSetId`). Expose
765
791
  // the root HonuaOgcTiles adapter instead so callers can discover the
766
792
  // tilesets the server advertises for the collection before binding one.
793
+ // A `connect()`-discovered third-party root threads its basePath so tile
794
+ // routes resolve against the advertised layout, not the `/ogc/tiles` facade.
767
795
  const adapter = tileMatrixSetId !== undefined && tileMatrixSetId !== ""
768
- ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId })
769
- : new HonuaOgcTiles({ client });
796
+ ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId, ...(basePath !== undefined ? { basePath } : {}) })
797
+ : new HonuaOgcTiles({ client, ...(basePath !== undefined ? { basePath } : {}) });
770
798
  const adapterRegistry = {
771
799
  "ogc-tiles": adapter,
772
800
  };
@@ -787,6 +815,12 @@ export function ogcTilesSource(descriptor, client, policy) {
787
815
  * `locator.url` may carry a leading `pmtiles://` scheme (the MapLibre form) or
788
816
  * be a bare archive URL; both resolve to the same archive.
789
817
  *
818
+ * This builds the escape-hatch adapter through {@link pmtilesProtocolModule}
819
+ * (issue #538) rather than constructing `HonuaPmtilesArchive` directly, so
820
+ * the built-in wiring runs the same public `ProtocolModule` seam a module
821
+ * registered through `HonuaPluginRegistry` uses
822
+ * (`pmtilesProtocolPlugin`, `@honua/sdk-js/plugin`).
823
+ *
790
824
  * @example
791
825
  * ```ts
792
826
  * const archive = dataset.source("basemap")!.protocol("pmtiles");
@@ -795,13 +829,18 @@ export function ogcTilesSource(descriptor, client, policy) {
795
829
  * ```
796
830
  */
797
831
  export function pmtilesSource(descriptor, _client, policy) {
798
- const url = stripPmtilesScheme(requirePmtilesLocator(descriptor));
799
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.pmtiles;
800
- const archive = new HonuaPmtilesArchive(url);
832
+ descriptor = normalizeCapabilityDescriptor(descriptor);
833
+ const discovered = pmtilesProtocolModule().discover(descriptor);
834
+ if (discovered instanceof Promise) {
835
+ // PMTiles discovery is always synchronous (the reader opens lazily on the
836
+ // handle's own first `describe()` call); a module that started returning
837
+ // a promise would be a breaking change to this built-in's own contract.
838
+ throw new Error("pmtiles: built-in source construction requires synchronous protocol-module discovery");
839
+ }
801
840
  const adapterRegistry = {
802
- pmtiles: archive,
841
+ pmtiles: discovered.adapter,
803
842
  };
804
- return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
843
+ return makeSource(descriptor, discovered.capabilities, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
805
844
  }
806
845
  // ── OGC API Maps ──────────────────────────────────────────────
807
846
  /**
@@ -816,13 +855,18 @@ export function pmtilesSource(descriptor, _client, policy) {
816
855
  * ```
817
856
  */
818
857
  export function ogcMapsSource(descriptor, client, policy) {
819
- const root = new HonuaOgcMaps({ client });
858
+ descriptor = normalizeCapabilityDescriptor(descriptor);
859
+ const basePath = descriptor.locator.basePath;
860
+ // A `connect()`-discovered third-party root threads its basePath so render
861
+ // routes resolve against the advertised layout, not the `/ogc/maps` facade.
862
+ const root = new HonuaOgcMaps({ client, ...(basePath !== undefined ? { basePath } : {}) });
820
863
  const adapterRegistry = {};
821
864
  if (descriptor.locator.collectionId !== undefined) {
822
865
  adapterRegistry["ogc-maps"] = new HonuaOgcCollectionMap({
823
866
  client,
824
867
  collectionId: descriptor.locator.collectionId,
825
868
  styleId: descriptor.locator.styleId,
869
+ ...(basePath !== undefined ? { basePath } : {}),
826
870
  });
827
871
  }
828
872
  else {
@@ -846,8 +890,14 @@ export function ogcMapsSource(descriptor, client, policy) {
846
890
  * ```
847
891
  */
848
892
  export function ogcRecordsSource(descriptor, client, policy) {
893
+ descriptor = normalizeCapabilityDescriptor(descriptor);
849
894
  const { collectionId } = requireOgcRecordsLocator(descriptor);
850
- const collection = new HonuaOgcRecordCollection({ client, collectionId });
895
+ const basePath = descriptor.locator.basePath;
896
+ const collection = new HonuaOgcRecordCollection({
897
+ client,
898
+ collectionId,
899
+ ...(basePath !== undefined ? { basePath } : {}),
900
+ });
851
901
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-records"];
852
902
  const adapterRegistry = {
853
903
  "ogc-records": collection,
@@ -959,6 +1009,18 @@ export function ogcRecordsSource(descriptor, client, policy) {
959
1009
  * ```
960
1010
  */
961
1011
  export function wmsSource(descriptor, client, policy) {
1012
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1013
+ const advertisedCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
1014
+ if (typeof descriptor.locator.serviceId !== "string" || descriptor.locator.serviceId.length === 0) {
1015
+ if (descriptor.locator.raster?.kind !== "wms-kvp")
1016
+ requireWmsLocator(descriptor);
1017
+ // Raw third-party discovery is renderable through the existing descriptor
1018
+ // → MapLibre raster projection. The existing canonical FeatureInfo adapter
1019
+ // is Honua-service-id based, so keep that surface fail-closed here.
1020
+ const caps = new Set([...advertisedCaps].filter((capability) => capability !== "query"));
1021
+ descriptor = { ...descriptor, capabilities: caps };
1022
+ return makeSource(descriptor, caps, policy, {}, unsupportedFeatureSurface(descriptor));
1023
+ }
962
1024
  const { serviceId } = requireWmsLocator(descriptor);
963
1025
  const layerName = descriptor.locator.typeName;
964
1026
  const styleId = descriptor.locator.styleId;
@@ -981,7 +1043,7 @@ export function wmsSource(descriptor, client, policy) {
981
1043
  layerOpts.defaultStyleId = styleId;
982
1044
  adapterRegistry["wms-layer"] = new HonuaWmsLayer(layerOpts);
983
1045
  }
984
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
1046
+ const caps = advertisedCaps;
985
1047
  return makeSource(descriptor, caps, policy, adapterRegistry, {
986
1048
  async query(request) {
987
1049
  ensureCapability(descriptor, caps, "query");
@@ -1059,6 +1121,16 @@ export function wmsSource(descriptor, client, policy) {
1059
1121
  * ```
1060
1122
  */
1061
1123
  export function wmtsSource(descriptor, client, policy) {
1124
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1125
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1126
+ if (typeof descriptor.locator.serviceId !== "string" || descriptor.locator.serviceId.length === 0) {
1127
+ if (descriptor.locator.raster?.kind !== "wmts-kvp" && descriptor.locator.raster?.kind !== "wmts-template") {
1128
+ requireWmtsLocator(descriptor);
1129
+ }
1130
+ // Raw WMTS sources use the validated descriptor binding in the existing
1131
+ // MapLibre projection; no second protocol wire adapter is introduced.
1132
+ return makeSource(descriptor, caps, policy, {}, unsupportedFeatureSurface(descriptor));
1133
+ }
1062
1134
  const { serviceId } = requireWmtsLocator(descriptor);
1063
1135
  const layerName = descriptor.locator.typeName;
1064
1136
  const tileMatrixSetId = descriptor.locator.tileMatrixSetId ?? "WebMercatorQuad";
@@ -1081,7 +1153,6 @@ export function wmtsSource(descriptor, client, policy) {
1081
1153
  tileMatrixSetId,
1082
1154
  });
1083
1155
  }
1084
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1085
1156
  return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
1086
1157
  }
1087
1158
  // ── STAC API ──────────────────────────────────────────────────
@@ -1114,6 +1185,7 @@ export function wmtsSource(descriptor, client, policy) {
1114
1185
  * ```
1115
1186
  */
1116
1187
  export function stacSearchSource(descriptor, client, policy) {
1188
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1117
1189
  // Endpoint-layout selection. `stac-api` treats `locator.url` as a raw STAC
1118
1190
  // API root (search / collections mounted directly under it) rather than the
1119
1191
  // Honua `/stac` facade. `stac-static` reads a static catalog.json tree with
@@ -1121,7 +1193,9 @@ export function stacSearchSource(descriptor, client, policy) {
1121
1193
  const layout = descriptor.locator.layout;
1122
1194
  const stacBasePath = layout === "stac-api" ? "" : undefined;
1123
1195
  const stac = new HonuaStacSearch({ client, ...(stacBasePath !== undefined ? { basePath: stacBasePath } : {}) });
1124
- const staticCatalog = layout === "stac-static" ? new HonuaStacStaticCatalog(client, client.serverBaseUrl) : undefined;
1196
+ const staticCatalog = layout === "stac-static"
1197
+ ? new HonuaStacStaticCatalog(client, descriptor.locator.url, descriptor.locator.stacStatic)
1198
+ : undefined;
1125
1199
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.stac;
1126
1200
  const collectionScope = descriptor.locator.collectionId;
1127
1201
  const adapterRegistry = {
@@ -1268,8 +1342,9 @@ const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
1268
1342
  * ```
1269
1343
  */
1270
1344
  export function wfsSource(descriptor, client, policy) {
1345
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1271
1346
  const { url, typeName, featureNamespace, srsName: wfsSrsName } = requireWfsLocator(descriptor);
1272
- const root = new HonuaWfs({ client, endpointUrl: url });
1347
+ const root = client.wfs(url);
1273
1348
  const featureType = new HonuaWfsFeatureType({ root, typeName });
1274
1349
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs;
1275
1350
  void policy;
@@ -1502,6 +1577,10 @@ export function wfsSource(descriptor, client, policy) {
1502
1577
  summary = { totalInserted: 0, totalUpdated: 0, totalDeleted: 0, insertResults: [] };
1503
1578
  }
1504
1579
  else {
1580
+ // Ensure a caller-cache hit still resolves the advertised Transaction
1581
+ // POST URL before sending edits. Fresh connect discovery reuses this
1582
+ // same root, so the capabilities promise is already settled there.
1583
+ await root.capabilities(envelope.signal ? { signal: envelope.signal } : undefined);
1505
1584
  const filtered = { ...envelope, updates: validUpdates };
1506
1585
  const body = buildTransactionBody(typeName, filtered, featureNamespace, wfsSrsName);
1507
1586
  const transactionOptions = { body };
@@ -1958,21 +2037,12 @@ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIn
1958
2037
  }));
1959
2038
  return { added, updated, deleted };
1960
2039
  }
1961
- // ── OData entity set ──────────────────────────────────────────
1962
- /**
1963
- * Build a `Source` over an OData v4 entity set. The `Query` translation
1964
- * lowers `where` / `outFields` / `orderBy` / pagination / spatial filter
1965
- * onto OData's `$`-prefixed query options. Dialect-specific operations
1966
- * (`$batch`, `$apply`, `$search`, `$deltatoken`) live behind
1967
- * `Source.protocol("odata")` on the returned `HonuaOdataEntitySet`.
1968
- *
1969
- * `$metadata` is fetched lazily on the first call that needs it and
1970
- * cached on the entity-set instance. The fetched capability annotations
1971
- * are intersected with the descriptor's declared `Capabilities` set the
1972
- * first time a capability-gated method is invoked — this is the
1973
- * precedent for the metadata-driven downgrade pattern referenced in
1974
- * `docs/shared-client-contract.md`.
1975
- */
2040
+ let odataWriteCodecPromise;
2041
+ /** Load the opt-in codec once; legacy sources never evaluate this import. */
2042
+ function loadOdataWriteCodec() {
2043
+ odataWriteCodecPromise ??= import("../core/odata-write-codec.js");
2044
+ return odataWriteCodecPromise;
2045
+ }
1976
2046
  /**
1977
2047
  * Adapter factory for an OData v4 entity set.
1978
2048
  *
@@ -2000,11 +2070,16 @@ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIn
2000
2070
  * });
2001
2071
  * ```
2002
2072
  */
2003
- export function odataSource(descriptor, client, policy) {
2073
+ export function odataSource(descriptor, client, policy, options = {}) {
2074
+ descriptor = normalizeCapabilityDescriptor(descriptor);
2004
2075
  const { entitySet, basePath } = requireOdataLocator(descriptor);
2005
2076
  const entity = new HonuaOdataEntitySet({ client, entitySet, basePath });
2006
2077
  const declaredCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.odata;
2007
2078
  const negotiation = new OdataCapabilityNegotiator(entity, declaredCaps);
2079
+ const writeEncoding = options.writeEncoding ?? "legacy";
2080
+ if (writeEncoding !== "legacy" && writeEncoding !== "lossless-json") {
2081
+ throw new TypeError('odata: writeEncoding must be "legacy" or "lossless-json"');
2082
+ }
2008
2083
  const adapterRegistry = {
2009
2084
  odata: entity,
2010
2085
  };
@@ -2107,6 +2182,9 @@ export function odataSource(descriptor, client, policy) {
2107
2182
  ensureCapability(descriptor, declaredCaps, "applyEdits");
2108
2183
  await negotiation.ensureAdvertised(descriptor, "applyEdits");
2109
2184
  const keyFields = await negotiation.keyFields(descriptor.id);
2185
+ const prepared = writeEncoding === "lossless-json"
2186
+ ? prepareLosslessOdataEdits(entity, envelope, keyFields, await negotiation.metadata(), await loadOdataWriteCodec())
2187
+ : undefined;
2110
2188
  // Atomic path: when the caller asks for rollback-on-failure AND the
2111
2189
  // service advertises `$batch`, collapse the envelope into a single
2112
2190
  // OData batch with a shared `atomicityGroup` so a later failure
@@ -2117,9 +2195,9 @@ export function odataSource(descriptor, client, policy) {
2117
2195
  if (envelope.rollbackOnFailure === true) {
2118
2196
  const batchAdvertised = await negotiation.batchAdvertised();
2119
2197
  if (batchAdvertised) {
2120
- return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor);
2198
+ return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor, prepared);
2121
2199
  }
2122
- const result = await perCallOdataApplyEdits(entity, envelope, keyFields);
2200
+ const result = await perCallOdataApplyEdits(entity, envelope, keyFields, prepared);
2123
2201
  return {
2124
2202
  ...result,
2125
2203
  degraded: [
@@ -2133,7 +2211,7 @@ export function odataSource(descriptor, client, policy) {
2133
2211
  ],
2134
2212
  };
2135
2213
  }
2136
- return perCallOdataApplyEdits(entity, envelope, keyFields);
2214
+ return perCallOdataApplyEdits(entity, envelope, keyFields, prepared);
2137
2215
  },
2138
2216
  async queryRelated() {
2139
2217
  // OData has no canonical related-records surface; navigation
@@ -2144,21 +2222,114 @@ export function odataSource(descriptor, client, policy) {
2144
2222
  attachments: unsupportedAttachmentApi(descriptor),
2145
2223
  });
2146
2224
  }
2225
+ /**
2226
+ * Preflight the complete opted-in envelope against one cached metadata
2227
+ * snapshot. Both direct and atomic execution consume this projection, so a
2228
+ * local encoding failure cannot leak a partial request or take a different
2229
+ * branch-specific coercion path.
2230
+ */
2231
+ function prepareLosslessOdataEdits(entity, envelope, keyFields, metadata, codec) {
2232
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2233
+ const adds = (envelope.adds ?? []).map((add) => preparedOdataBody(metadata, entity.entitySet, add, codec));
2234
+ const updates = (envelope.updates ?? []).map((update) => {
2235
+ const prepared = preparedOdataBody(metadata, entity.entitySet, update, codec);
2236
+ const key = preparedOdataUpdateKey(metadata, entity.entitySet, update.id, prepared.body, urlKeys, codec);
2237
+ return { ...prepared, key };
2238
+ });
2239
+ const deletes = (envelope.deletes ?? []).map((id) => {
2240
+ if (urlKeys.length > 1) {
2241
+ // The generic contract carries only a scalar FeatureId. Lossless
2242
+ // composite addressing requires named components, so do not interpret
2243
+ // an ad-hoc string expression or risk targeting a different entity.
2244
+ throw new codec.HonuaOdataEdmEncodingError("missing-key", "$.key");
2245
+ }
2246
+ return {
2247
+ key: codec.encodeOdataEntityKey(metadata, entity.entitySet, id, {
2248
+ keyFields: urlKeys,
2249
+ }),
2250
+ };
2251
+ });
2252
+ return { adds, updates, deletes };
2253
+ }
2254
+ function preparedOdataBody(metadata, entitySet, feature, codec) {
2255
+ const attributes = codec.encodeOdataWriteBody(metadata, entitySet, feature.attributes);
2256
+ let encoded = attributes;
2257
+ if (feature.geometry !== undefined &&
2258
+ feature.geometry !== null &&
2259
+ !Object.hasOwn(attributes.body, "Geometry") &&
2260
+ !Object.hasOwn(attributes.body, "geometry")) {
2261
+ const geometry = codec.encodeOdataWriteBody(metadata, entitySet, { Geometry: feature.geometry });
2262
+ encoded = {
2263
+ body: { ...attributes.body, ...geometry.body },
2264
+ requiresIeee754Compatible: attributes.requiresIeee754Compatible || geometry.requiresIeee754Compatible,
2265
+ };
2266
+ }
2267
+ return {
2268
+ body: encoded.body,
2269
+ contentType: encoded.requiresIeee754Compatible ? "application/json;IEEE754Compatible=true" : "application/json",
2270
+ };
2271
+ }
2272
+ function preparedOdataUpdateKey(metadata, entitySet, id, attributes, keyFields, codec) {
2273
+ if (keyFields.length === 1) {
2274
+ const field = keyFields[0];
2275
+ const attributeValue = ownPreparedValue(attributes, field);
2276
+ const attributeKey = attributeValue === undefined || attributeValue === null
2277
+ ? undefined
2278
+ : codec.encodeOdataEntityKey(metadata, entitySet, attributeValue, { keyFields });
2279
+ const idKey = id === undefined || id === null ? undefined : codec.encodeOdataEntityKey(metadata, entitySet, id, { keyFields });
2280
+ if (attributeKey &&
2281
+ idKey &&
2282
+ (attributeKey.literal !== idKey.literal || attributeKey.pathSegment !== idKey.pathSegment)) {
2283
+ throw new codec.HonuaOdataEdmEncodingError("invalid-value", "$.key");
2284
+ }
2285
+ return attributeKey ?? idKey ?? codec.encodeOdataEntityKey(metadata, entitySet, undefined, { keyFields });
2286
+ }
2287
+ if (keyFields.length === 0) {
2288
+ return codec.encodeOdataEntityKey(metadata, entitySet, id, { keyFields });
2289
+ }
2290
+ if (id !== undefined && id !== null) {
2291
+ // A scalar generic id cannot prove equivalence to a named composite key.
2292
+ // Reject it even when the body carries all components so the adapter never
2293
+ // targets one identity while reporting another.
2294
+ throw new codec.HonuaOdataEdmEncodingError("invalid-value", "$.key");
2295
+ }
2296
+ const key = Object.create(null);
2297
+ for (const field of keyFields) {
2298
+ const value = ownPreparedValue(attributes, field);
2299
+ if (value === undefined || value === null)
2300
+ continue;
2301
+ Object.defineProperty(key, field, {
2302
+ configurable: true,
2303
+ enumerable: true,
2304
+ writable: true,
2305
+ value,
2306
+ });
2307
+ }
2308
+ return codec.encodeOdataEntityKey(metadata, entitySet, key, { keyFields });
2309
+ }
2310
+ function ownPreparedValue(attributes, field) {
2311
+ const descriptor = Object.getOwnPropertyDescriptor(attributes, field);
2312
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
2313
+ }
2147
2314
  /**
2148
2315
  * Per-call OData edits — issues independent POST/PATCH/DELETE requests
2149
2316
  * and collects per-row outcomes. This is the non-atomic fallback used
2150
2317
  * when `rollbackOnFailure` is unset or the server does not advertise
2151
2318
  * `$batch`.
2152
2319
  */
2153
- async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2320
+ async function perCallOdataApplyEdits(entity, envelope, keyFields, prepared) {
2154
2321
  const added = [];
2155
2322
  const updated = [];
2156
2323
  const deleted = [];
2157
2324
  const { signal } = envelope;
2158
- for (const add of envelope.adds ?? []) {
2325
+ const adds = envelope.adds ?? [];
2326
+ for (let index = 0; index < adds.length; index += 1) {
2327
+ const add = adds[index];
2328
+ const encoded = prepared?.adds[index];
2159
2329
  try {
2160
- const created = await entity.add(featureToOdataBody(add), {
2330
+ const created = await entity.add(encoded?.body ?? featureToOdataBody(add), {
2161
2331
  ...(signal ? { signal } : {}),
2332
+ ...(encoded ? { contentType: encoded.contentType } : {}),
2162
2333
  });
2163
2334
  const id = readKey(created, keyFields, add.id);
2164
2335
  added.push(id !== undefined ? { id, success: true } : { success: true });
@@ -2173,22 +2344,30 @@ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2173
2344
  // using the full `keyFields` because the response body still carries
2174
2345
  // every key.
2175
2346
  const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2176
- for (const update of envelope.updates ?? []) {
2177
- const key = canonicalKeyToOdata(update, urlKeys);
2347
+ const updates = envelope.updates ?? [];
2348
+ for (let index = 0; index < updates.length; index += 1) {
2349
+ const update = updates[index];
2350
+ const encoded = prepared?.updates[index];
2351
+ const key = encoded?.key.literal ?? canonicalKeyToOdata(update, urlKeys);
2178
2352
  if (key === undefined) {
2179
2353
  updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
2180
2354
  continue;
2181
2355
  }
2182
2356
  try {
2183
- await entity.update(key, featureToOdataBody(update), { ...(signal ? { signal } : {}) });
2357
+ await entity.update(key, encoded?.body ?? featureToOdataBody(update), {
2358
+ ...(signal ? { signal } : {}),
2359
+ ...(encoded ? { contentType: encoded.contentType } : {}),
2360
+ });
2184
2361
  updated.push({ id: update.id ?? readKeyFromBody(update.attributes, keyFields), success: true });
2185
2362
  }
2186
2363
  catch (err) {
2187
2364
  updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
2188
2365
  }
2189
2366
  }
2190
- for (const id of envelope.deletes ?? []) {
2191
- const key = canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2367
+ const deletes = envelope.deletes ?? [];
2368
+ for (let index = 0; index < deletes.length; index += 1) {
2369
+ const id = deletes[index];
2370
+ const key = prepared?.deletes[index]?.key.literal ?? canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2192
2371
  if (key === undefined) {
2193
2372
  deleted.push({ success: false, error: { code: 400, description: "delete id is required" } });
2194
2373
  continue;
@@ -2211,7 +2390,7 @@ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2211
2390
  * threaded back into per-row `EditOutcome` entries in the original
2212
2391
  * order so callers see the same shape regardless of transport.
2213
2392
  */
2214
- async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2393
+ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor, prepared) {
2215
2394
  const operations = [];
2216
2395
  const plan = [];
2217
2396
  const bucketIndices = { add: [], update: [], delete: [] };
@@ -2223,21 +2402,26 @@ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2223
2402
  // and the test surface stays small.
2224
2403
  let seq = 1;
2225
2404
  const adds = envelope.adds ?? [];
2226
- for (const add of adds) {
2405
+ for (let index = 0; index < adds.length; index += 1) {
2406
+ const add = adds[index];
2407
+ const encoded = prepared?.adds[index];
2227
2408
  const id = String(seq++);
2228
2409
  operations.push({
2229
2410
  id,
2230
2411
  method: "POST",
2231
2412
  url: stripLeadingSlashLocal(entity.entitySet),
2232
- body: featureToOdataBody(add),
2413
+ body: encoded?.body ?? featureToOdataBody(add),
2414
+ ...(encoded ? { headers: { "Content-Type": encoded.contentType } } : {}),
2233
2415
  });
2234
2416
  bucketIndices.add.push(plan.length);
2235
2417
  plan.push({ kind: "add", id: add.id });
2236
2418
  }
2237
2419
  const updates = envelope.updates ?? [];
2238
- for (const update of updates) {
2420
+ for (let index = 0; index < updates.length; index += 1) {
2421
+ const update = updates[index];
2422
+ const encoded = prepared?.updates[index];
2239
2423
  const id = String(seq++);
2240
- const key = canonicalKeyToOdata(update, urlKeys);
2424
+ const key = encoded?.key.literal ?? canonicalKeyToOdata(update, urlKeys);
2241
2425
  if (key === undefined) {
2242
2426
  bucketIndices.update.push(plan.length);
2243
2427
  plan.push({ kind: "update", id: update.id });
@@ -2246,22 +2430,29 @@ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2246
2430
  operations.push({
2247
2431
  id,
2248
2432
  method: "PATCH",
2249
- url: `${entity.entitySet}(${key})`,
2250
- body: featureToOdataBody(update),
2433
+ url: `${entity.entitySet}(${encoded?.key.pathSegment ?? key})`,
2434
+ body: encoded?.body ?? featureToOdataBody(update),
2435
+ ...(encoded ? { headers: { "Content-Type": encoded.contentType } } : {}),
2251
2436
  });
2252
2437
  bucketIndices.update.push(plan.length);
2253
2438
  plan.push({ kind: "update", id: update.id, key });
2254
2439
  }
2255
2440
  const deletes = envelope.deletes ?? [];
2256
- for (const rawId of deletes) {
2441
+ for (let index = 0; index < deletes.length; index += 1) {
2442
+ const rawId = deletes[index];
2443
+ const encoded = prepared?.deletes[index];
2257
2444
  const id = String(seq++);
2258
- const key = canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2445
+ const key = encoded?.key.literal ?? canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2259
2446
  if (key === undefined) {
2260
2447
  bucketIndices.delete.push(plan.length);
2261
2448
  plan.push({ kind: "delete", id: rawId });
2262
2449
  continue;
2263
2450
  }
2264
- operations.push({ id, method: "DELETE", url: `${entity.entitySet}(${key})` });
2451
+ operations.push({
2452
+ id,
2453
+ method: "DELETE",
2454
+ url: `${entity.entitySet}(${encoded?.key.pathSegment ?? key})`,
2455
+ });
2265
2456
  bucketIndices.delete.push(plan.length);
2266
2457
  plan.push({ kind: "delete", id: rawId, key });
2267
2458
  }
@@ -2552,6 +2743,13 @@ async function buildOdataParams(entity, descriptor, request, options) {
2552
2743
  if (request.returnGeometry === false) {
2553
2744
  rootSelect = rootSelect.filter((f) => !isOdataGeometryFieldName(f, options.geomColumn));
2554
2745
  }
2746
+ else if (options.geomColumn && !rootSelect.includes(options.geomColumn)) {
2747
+ // OData `$select` is exclusive: once callers project attributes the
2748
+ // service omits every unlisted property. Preserve canonical
2749
+ // `returnGeometry` (default true) by explicitly retaining the resolved
2750
+ // geometry column.
2751
+ rootSelect = [...rootSelect, options.geomColumn];
2752
+ }
2555
2753
  if (rootSelect.length > 0)
2556
2754
  out.select = rootSelect;
2557
2755
  if (split.expand.length > 0)
@@ -2790,6 +2988,10 @@ class OdataCapabilityNegotiator {
2790
2988
  return [];
2791
2989
  return meta.keys[typeName] ?? [];
2792
2990
  }
2991
+ /** Return the same single-flight snapshot used by every capability probe. */
2992
+ metadata() {
2993
+ return this.materialize();
2994
+ }
2793
2995
  async keyField(sourceId) {
2794
2996
  const keys = await this.keyFields(sourceId);
2795
2997
  if (keys.length === 1)
@@ -2846,7 +3048,7 @@ function makeSource(descriptor, caps, _policy, adapters, impl) {
2846
3048
  function lookupAdapter(kind) {
2847
3049
  return adapters[kind];
2848
3050
  }
2849
- return {
3051
+ const source = {
2850
3052
  descriptor: { ...descriptor, capabilities: caps },
2851
3053
  capabilities: caps,
2852
3054
  query: impl.query.bind(impl),
@@ -2861,6 +3063,7 @@ function makeSource(descriptor, caps, _policy, adapters, impl) {
2861
3063
  protocol: lookupAdapter,
2862
3064
  adapter: lookupAdapter,
2863
3065
  };
3066
+ return addCapabilitySupport(source, source.descriptor);
2864
3067
  }
2865
3068
  /**
2866
3069
  * Helper: build a no-attachment-support `AttachmentApi` whose every method
@@ -2989,13 +3192,6 @@ function requireOgcRecordsLocator(descriptor) {
2989
3192
  }
2990
3193
  return { collectionId };
2991
3194
  }
2992
- function requirePmtilesLocator(descriptor) {
2993
- const { url } = descriptor.locator;
2994
- if (typeof url !== "string" || url === "") {
2995
- throw new Error(`createDataset: source "${descriptor.id}" (pmtiles) requires locator.url`);
2996
- }
2997
- return url;
2998
- }
2999
3195
  function requireImageServiceLocator(descriptor) {
3000
3196
  const { serviceId } = descriptor.locator;
3001
3197
  if (typeof serviceId !== "string") {
@@ -3035,8 +3231,6 @@ function requireGPServiceLocator(descriptor, caps) {
3035
3231
  // /rest/services/<serviceId>/GPServer/<taskName>/..., so descriptors that
3036
3232
  // advertise the `geoprocess` capability must carry a task name. Without
3037
3233
  // one the lifecycle routes resolve to non-existent paths on the server.
3038
- // Descriptors with only `connect` (service-root metadata probe) may omit
3039
- // taskName.
3040
3234
  if (caps.has("geoprocess") && (typeof taskName !== "string" || taskName.length === 0)) {
3041
3235
  throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) advertises "geoprocess" but locator.taskName is missing; GP lifecycle routes require /GPServer/<taskName>/...`);
3042
3236
  }
@@ -3451,8 +3645,10 @@ function toOgcRequest(request) {
3451
3645
  if (!request)
3452
3646
  return {};
3453
3647
  const out = {};
3454
- if (request.where !== undefined)
3648
+ if (request.where !== undefined) {
3455
3649
  out.filter = request.where;
3650
+ out.filterLang = "cql2-text";
3651
+ }
3456
3652
  if (request.outFields && request.outFields.length > 0)
3457
3653
  out.properties = [...request.outFields];
3458
3654
  if (request.outSr !== undefined)
@@ -3657,10 +3853,10 @@ function readField(attributes, field) {
3657
3853
  function toApplyEditsRequest(envelope) {
3658
3854
  const out = {};
3659
3855
  if (envelope.adds && envelope.adds.length > 0) {
3660
- out.adds = envelope.adds.map((f) => canonicalToHonuaFeature(f));
3856
+ out.adds = envelope.adds.map(canonicalToHonuaFeature);
3661
3857
  }
3662
3858
  if (envelope.updates && envelope.updates.length > 0) {
3663
- out.updates = envelope.updates.map((f) => canonicalToHonuaFeature(f));
3859
+ out.updates = envelope.updates.map(canonicalToHonuaFeature);
3664
3860
  }
3665
3861
  if (envelope.deletes && envelope.deletes.length > 0) {
3666
3862
  const numericIds = [];