@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.
- package/LICENSE +201 -0
- package/README.md +9 -0
- package/contract/analytics-sources.d.ts +149 -0
- package/contract/analytics-sources.d.ts.map +1 -0
- package/contract/analytics-sources.js +235 -0
- package/contract/edit-session.d.ts +203 -0
- package/contract/edit-session.d.ts.map +1 -0
- package/contract/edit-session.js +760 -0
- package/contract/edit-sketch.d.ts +91 -0
- package/contract/edit-sketch.d.ts.map +1 -0
- package/contract/edit-sketch.js +281 -0
- package/contract/index.d.ts +61 -0
- package/contract/index.d.ts.map +1 -0
- package/contract/index.js +51 -0
- package/contract/jobs.d.ts +149 -0
- package/contract/jobs.d.ts.map +1 -0
- package/contract/jobs.js +40 -0
- package/contract/source.d.ts +423 -0
- package/contract/source.d.ts.map +1 -0
- package/contract/source.js +3802 -0
- package/contract/spatial-aggregation.d.ts +336 -0
- package/contract/spatial-aggregation.d.ts.map +1 -0
- package/contract/spatial-aggregation.js +298 -0
- package/contract/temporal.d.ts +455 -0
- package/contract/temporal.d.ts.map +1 -0
- package/contract/temporal.js +146 -0
- package/contract/tiles.d.ts +370 -0
- package/contract/tiles.d.ts.map +1 -0
- package/contract/tiles.js +519 -0
- package/contract/types.d.ts +665 -0
- package/contract/types.d.ts.map +1 -0
- package/contract/types.js +175 -0
- package/contract/widget-source.d.ts +205 -0
- package/contract/widget-source.d.ts.map +1 -0
- package/contract/widget-source.js +1473 -0
- package/control-plane/client.d.ts +95 -0
- package/control-plane/client.d.ts.map +1 -0
- package/control-plane/client.js +374 -0
- package/control-plane/index.d.ts +18 -0
- package/control-plane/index.d.ts.map +1 -0
- package/control-plane/index.js +14 -0
- package/control-plane/types.d.ts +233 -0
- package/control-plane/types.d.ts.map +1 -0
- package/control-plane/types.js +7 -0
- package/core/batch.d.ts +37 -0
- package/core/batch.d.ts.map +1 -0
- package/core/batch.js +79 -0
- package/core/cache-state.d.ts +79 -0
- package/core/cache-state.d.ts.map +1 -0
- package/core/cache-state.js +87 -0
- package/core/client.d.ts +577 -0
- package/core/client.d.ts.map +1 -0
- package/core/client.js +1664 -0
- package/core/compatibility.d.ts +18 -0
- package/core/compatibility.d.ts.map +1 -0
- package/core/compatibility.js +245 -0
- package/core/errors.d.ts +107 -0
- package/core/errors.d.ts.map +1 -0
- package/core/errors.js +156 -0
- package/core/esri-geojson.d.ts +66 -0
- package/core/esri-geojson.d.ts.map +1 -0
- package/core/esri-geojson.js +363 -0
- package/core/geoservices.d.ts +34 -0
- package/core/geoservices.d.ts.map +1 -0
- package/core/geoservices.js +434 -0
- package/core/grpc-adapter.d.ts +23 -0
- package/core/grpc-adapter.d.ts.map +1 -0
- package/core/grpc-adapter.js +688 -0
- package/core/odata.d.ts +347 -0
- package/core/odata.d.ts.map +1 -0
- package/core/odata.js +913 -0
- package/core/ogc-conformance.d.ts +35 -0
- package/core/ogc-conformance.d.ts.map +1 -0
- package/core/ogc-conformance.js +134 -0
- package/core/ogc-features.d.ts +23 -0
- package/core/ogc-features.d.ts.map +1 -0
- package/core/ogc-features.js +114 -0
- package/core/ogc-maps.d.ts +49 -0
- package/core/ogc-maps.d.ts.map +1 -0
- package/core/ogc-maps.js +126 -0
- package/core/ogc-processes.d.ts +118 -0
- package/core/ogc-processes.d.ts.map +1 -0
- package/core/ogc-processes.js +423 -0
- package/core/ogc-records.d.ts +75 -0
- package/core/ogc-records.d.ts.map +1 -0
- package/core/ogc-records.js +251 -0
- package/core/ogc-tiles.d.ts +125 -0
- package/core/ogc-tiles.d.ts.map +1 -0
- package/core/ogc-tiles.js +183 -0
- package/core/path-utils.d.ts +69 -0
- package/core/path-utils.d.ts.map +1 -0
- package/core/path-utils.js +123 -0
- package/core/pbf-decoder.d.ts +39 -0
- package/core/pbf-decoder.d.ts.map +1 -0
- package/core/pbf-decoder.js +702 -0
- package/core/process-runner.d.ts +104 -0
- package/core/process-runner.d.ts.map +1 -0
- package/core/process-runner.js +315 -0
- package/core/protocol-transport.d.ts +76 -0
- package/core/protocol-transport.d.ts.map +1 -0
- package/core/protocol-transport.js +15 -0
- package/core/query-builder.d.ts +261 -0
- package/core/query-builder.d.ts.map +1 -0
- package/core/query-builder.js +538 -0
- package/core/request-pipeline.d.ts +88 -0
- package/core/request-pipeline.d.ts.map +1 -0
- package/core/request-pipeline.js +273 -0
- package/core/spatial-filter.d.ts +93 -0
- package/core/spatial-filter.d.ts.map +1 -0
- package/core/spatial-filter.js +153 -0
- package/core/stac.d.ts +55 -0
- package/core/stac.d.ts.map +1 -0
- package/core/stac.js +229 -0
- package/core/surfaces.d.ts +527 -0
- package/core/surfaces.d.ts.map +1 -0
- package/core/surfaces.js +1917 -0
- package/core/terrain-elevation.d.ts +114 -0
- package/core/terrain-elevation.d.ts.map +1 -0
- package/core/terrain-elevation.js +150 -0
- package/core/types.d.ts +1355 -0
- package/core/types.d.ts.map +1 -0
- package/core/types.js +13 -0
- package/core/wfs-capabilities.d.ts +143 -0
- package/core/wfs-capabilities.d.ts.map +1 -0
- package/core/wfs-capabilities.js +478 -0
- package/core/wfs-filter.d.ts +118 -0
- package/core/wfs-filter.d.ts.map +1 -0
- package/core/wfs-filter.js +634 -0
- package/core/wfs.d.ts +219 -0
- package/core/wfs.d.ts.map +1 -0
- package/core/wfs.js +406 -0
- package/core/wire-shared.d.ts +32 -0
- package/core/wire-shared.d.ts.map +1 -0
- package/core/wire-shared.js +76 -0
- package/core/wms-axis.d.ts +36 -0
- package/core/wms-axis.d.ts.map +1 -0
- package/core/wms-axis.js +73 -0
- package/core/wms-capabilities.d.ts +110 -0
- package/core/wms-capabilities.d.ts.map +1 -0
- package/core/wms-capabilities.js +559 -0
- package/core/wms-types.d.ts +128 -0
- package/core/wms-types.d.ts.map +1 -0
- package/core/wms-types.js +32 -0
- package/core/wms.d.ts +144 -0
- package/core/wms.d.ts.map +1 -0
- package/core/wms.js +338 -0
- package/core/wmts-capabilities.d.ts +82 -0
- package/core/wmts-capabilities.d.ts.map +1 -0
- package/core/wmts-capabilities.js +402 -0
- package/core/wmts.d.ts +103 -0
- package/core/wmts.d.ts.map +1 -0
- package/core/wmts.js +332 -0
- package/core/xml-text.d.ts +20 -0
- package/core/xml-text.d.ts.map +1 -0
- package/core/xml-text.js +85 -0
- package/esri-compat/basemap-gallery.d.ts +38 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -0
- package/esri-compat/basemap-gallery.js +159 -0
- package/esri-compat/basemap-layer-list.d.ts +35 -0
- package/esri-compat/basemap-layer-list.d.ts.map +1 -0
- package/esri-compat/basemap-layer-list.js +139 -0
- package/esri-compat/basemap.d.ts +32 -0
- package/esri-compat/basemap.d.ts.map +1 -0
- package/esri-compat/basemap.js +84 -0
- package/esri-compat/bookmarks.d.ts +36 -0
- package/esri-compat/bookmarks.d.ts.map +1 -0
- package/esri-compat/bookmarks.js +108 -0
- package/esri-compat/class-breaks-renderer.d.ts +50 -0
- package/esri-compat/class-breaks-renderer.d.ts.map +1 -0
- package/esri-compat/class-breaks-renderer.js +164 -0
- package/esri-compat/color.d.ts +22 -0
- package/esri-compat/color.d.ts.map +1 -0
- package/esri-compat/color.js +106 -0
- package/esri-compat/controls.d.ts +174 -0
- package/esri-compat/controls.d.ts.map +1 -0
- package/esri-compat/controls.js +479 -0
- package/esri-compat/coordinate-conversion.d.ts +41 -0
- package/esri-compat/coordinate-conversion.d.ts.map +1 -0
- package/esri-compat/coordinate-conversion.js +125 -0
- package/esri-compat/directions.d.ts +45 -0
- package/esri-compat/directions.d.ts.map +1 -0
- package/esri-compat/directions.js +123 -0
- package/esri-compat/editor.d.ts +47 -0
- package/esri-compat/editor.d.ts.map +1 -0
- package/esri-compat/editor.js +170 -0
- package/esri-compat/esri-config.d.ts +16 -0
- package/esri-compat/esri-config.d.ts.map +1 -0
- package/esri-compat/esri-config.js +23 -0
- package/esri-compat/esri-request.d.ts +17 -0
- package/esri-compat/esri-request.d.ts.map +1 -0
- package/esri-compat/esri-request.js +127 -0
- package/esri-compat/event-bus.d.ts +106 -0
- package/esri-compat/event-bus.d.ts.map +1 -0
- package/esri-compat/event-bus.js +90 -0
- package/esri-compat/expand.d.ts +36 -0
- package/esri-compat/expand.d.ts.map +1 -0
- package/esri-compat/expand.js +97 -0
- package/esri-compat/extent.d.ts +43 -0
- package/esri-compat/extent.d.ts.map +1 -0
- package/esri-compat/extent.js +140 -0
- package/esri-compat/feature-filter.d.ts +55 -0
- package/esri-compat/feature-filter.d.ts.map +1 -0
- package/esri-compat/feature-filter.js +109 -0
- package/esri-compat/feature-form.d.ts +64 -0
- package/esri-compat/feature-form.d.ts.map +1 -0
- package/esri-compat/feature-form.js +137 -0
- package/esri-compat/feature-layer.d.ts +173 -0
- package/esri-compat/feature-layer.d.ts.map +1 -0
- package/esri-compat/feature-layer.js +661 -0
- package/esri-compat/feature-set.d.ts +32 -0
- package/esri-compat/feature-set.d.ts.map +1 -0
- package/esri-compat/feature-set.js +80 -0
- package/esri-compat/feature-table.d.ts +141 -0
- package/esri-compat/feature-table.d.ts.map +1 -0
- package/esri-compat/feature-table.js +500 -0
- package/esri-compat/feature-templates.d.ts +41 -0
- package/esri-compat/feature-templates.d.ts.map +1 -0
- package/esri-compat/feature-templates.js +97 -0
- package/esri-compat/feature.d.ts +33 -0
- package/esri-compat/feature.d.ts.map +1 -0
- package/esri-compat/feature.js +83 -0
- package/esri-compat/geojson-layer.d.ts +86 -0
- package/esri-compat/geojson-layer.d.ts.map +1 -0
- package/esri-compat/geojson-layer.js +191 -0
- package/esri-compat/geometry-engine.d.ts +107 -0
- package/esri-compat/geometry-engine.d.ts.map +1 -0
- package/esri-compat/geometry-engine.js +266 -0
- package/esri-compat/graphic.d.ts +46 -0
- package/esri-compat/graphic.d.ts.map +1 -0
- package/esri-compat/graphic.js +103 -0
- package/esri-compat/graphics-layer.d.ts +49 -0
- package/esri-compat/graphics-layer.d.ts.map +1 -0
- package/esri-compat/graphics-layer.js +185 -0
- package/esri-compat/group-layer.d.ts +48 -0
- package/esri-compat/group-layer.d.ts.map +1 -0
- package/esri-compat/group-layer.js +212 -0
- package/esri-compat/identify.d.ts +71 -0
- package/esri-compat/identify.d.ts.map +1 -0
- package/esri-compat/identify.js +299 -0
- package/esri-compat/identity-manager.d.ts +31 -0
- package/esri-compat/identity-manager.d.ts.map +1 -0
- package/esri-compat/identity-manager.js +115 -0
- package/esri-compat/imagery-layer.d.ts +78 -0
- package/esri-compat/imagery-layer.d.ts.map +1 -0
- package/esri-compat/imagery-layer.js +167 -0
- package/esri-compat/label-class.d.ts +31 -0
- package/esri-compat/label-class.d.ts.map +1 -0
- package/esri-compat/label-class.js +102 -0
- package/esri-compat/layer-list.d.ts +77 -0
- package/esri-compat/layer-list.d.ts.map +1 -0
- package/esri-compat/layer-list.js +342 -0
- package/esri-compat/legend.d.ts +53 -0
- package/esri-compat/legend.d.ts.map +1 -0
- package/esri-compat/legend.js +254 -0
- package/esri-compat/map-image-layer.d.ts +162 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -0
- package/esri-compat/map-image-layer.js +709 -0
- package/esri-compat/map-view.d.ts +323 -0
- package/esri-compat/map-view.d.ts.map +1 -0
- package/esri-compat/map-view.js +1175 -0
- package/esri-compat/map.d.ts +47 -0
- package/esri-compat/map.d.ts.map +1 -0
- package/esri-compat/map.js +209 -0
- package/esri-compat/measurement-2d.d.ts +61 -0
- package/esri-compat/measurement-2d.d.ts.map +1 -0
- package/esri-compat/measurement-2d.js +184 -0
- package/esri-compat/measurement.d.ts +45 -0
- package/esri-compat/measurement.d.ts.map +1 -0
- package/esri-compat/measurement.js +193 -0
- package/esri-compat/oauth-info.d.ts +35 -0
- package/esri-compat/oauth-info.d.ts.map +1 -0
- package/esri-compat/oauth-info.js +115 -0
- package/esri-compat/picture-marker-symbol.d.ts +35 -0
- package/esri-compat/picture-marker-symbol.d.ts.map +1 -0
- package/esri-compat/picture-marker-symbol.js +126 -0
- package/esri-compat/point.d.ts +31 -0
- package/esri-compat/point.d.ts.map +1 -0
- package/esri-compat/point.js +102 -0
- package/esri-compat/polygon.d.ts +31 -0
- package/esri-compat/polygon.d.ts.map +1 -0
- package/esri-compat/polygon.js +107 -0
- package/esri-compat/polyline.d.ts +31 -0
- package/esri-compat/polyline.d.ts.map +1 -0
- package/esri-compat/polyline.js +107 -0
- package/esri-compat/popup-template.d.ts +55 -0
- package/esri-compat/popup-template.d.ts.map +1 -0
- package/esri-compat/popup-template.js +128 -0
- package/esri-compat/popup.d.ts +54 -0
- package/esri-compat/popup.d.ts.map +1 -0
- package/esri-compat/popup.js +312 -0
- package/esri-compat/print.d.ts +47 -0
- package/esri-compat/print.d.ts.map +1 -0
- package/esri-compat/print.js +105 -0
- package/esri-compat/query.d.ts +58 -0
- package/esri-compat/query.d.ts.map +1 -0
- package/esri-compat/query.js +128 -0
- package/esri-compat/reactive-utils.d.ts +24 -0
- package/esri-compat/reactive-utils.d.ts.map +1 -0
- package/esri-compat/reactive-utils.js +193 -0
- package/esri-compat/request.d.ts +41 -0
- package/esri-compat/request.d.ts.map +1 -0
- package/esri-compat/request.js +253 -0
- package/esri-compat/route-layer.d.ts +59 -0
- package/esri-compat/route-layer.d.ts.map +1 -0
- package/esri-compat/route-layer.js +223 -0
- package/esri-compat/route-task.d.ts +88 -0
- package/esri-compat/route-task.d.ts.map +1 -0
- package/esri-compat/route-task.js +225 -0
- package/esri-compat/scene-view.d.ts +41 -0
- package/esri-compat/scene-view.d.ts.map +1 -0
- package/esri-compat/scene-view.js +68 -0
- package/esri-compat/search.d.ts +112 -0
- package/esri-compat/search.d.ts.map +1 -0
- package/esri-compat/search.js +820 -0
- package/esri-compat/simple-fill-symbol.d.ts +27 -0
- package/esri-compat/simple-fill-symbol.d.ts.map +1 -0
- package/esri-compat/simple-fill-symbol.js +85 -0
- package/esri-compat/simple-line-symbol.d.ts +27 -0
- package/esri-compat/simple-line-symbol.d.ts.map +1 -0
- package/esri-compat/simple-line-symbol.js +85 -0
- package/esri-compat/simple-marker-symbol.d.ts +29 -0
- package/esri-compat/simple-marker-symbol.d.ts.map +1 -0
- package/esri-compat/simple-marker-symbol.js +92 -0
- package/esri-compat/simple-renderer.d.ts +29 -0
- package/esri-compat/simple-renderer.d.ts.map +1 -0
- package/esri-compat/simple-renderer.js +92 -0
- package/esri-compat/sketch.d.ts +64 -0
- package/esri-compat/sketch.d.ts.map +1 -0
- package/esri-compat/sketch.js +227 -0
- package/esri-compat/spatial-reference.d.ts +31 -0
- package/esri-compat/spatial-reference.d.ts.map +1 -0
- package/esri-compat/spatial-reference.js +102 -0
- package/esri-compat/swipe.d.ts +34 -0
- package/esri-compat/swipe.d.ts.map +1 -0
- package/esri-compat/swipe.js +93 -0
- package/esri-compat/table-list.d.ts +36 -0
- package/esri-compat/table-list.d.ts.map +1 -0
- package/esri-compat/table-list.js +126 -0
- package/esri-compat/text-symbol.d.ts +37 -0
- package/esri-compat/text-symbol.d.ts.map +1 -0
- package/esri-compat/text-symbol.js +123 -0
- package/esri-compat/tile-layer.d.ts +52 -0
- package/esri-compat/tile-layer.d.ts.map +1 -0
- package/esri-compat/tile-layer.js +168 -0
- package/esri-compat/time-slider.d.ts +70 -0
- package/esri-compat/time-slider.d.ts.map +1 -0
- package/esri-compat/time-slider.js +208 -0
- package/esri-compat/track.d.ts +51 -0
- package/esri-compat/track.d.ts.map +1 -0
- package/esri-compat/track.js +174 -0
- package/esri-compat/unique-value-renderer.d.ts +41 -0
- package/esri-compat/unique-value-renderer.d.ts.map +1 -0
- package/esri-compat/unique-value-renderer.js +127 -0
- package/esri-compat/url.d.ts +13 -0
- package/esri-compat/url.d.ts.map +1 -0
- package/esri-compat/url.js +68 -0
- package/esri-compat/vector-tile-layer.d.ts +47 -0
- package/esri-compat/vector-tile-layer.d.ts.map +1 -0
- package/esri-compat/vector-tile-layer.js +130 -0
- package/esri-compat/web-map.d.ts +14 -0
- package/esri-compat/web-map.d.ts.map +1 -0
- package/esri-compat/web-map.js +24 -0
- package/esri-compat/wfs-layer.d.ts +64 -0
- package/esri-compat/wfs-layer.d.ts.map +1 -0
- package/esri-compat/wfs-layer.js +145 -0
- package/esri-compat/wms-layer.d.ts +76 -0
- package/esri-compat/wms-layer.d.ts.map +1 -0
- package/esri-compat/wms-layer.js +157 -0
- package/exploration/context.d.ts +25 -0
- package/exploration/context.d.ts.map +1 -0
- package/exploration/context.js +396 -0
- package/exploration/index.d.ts +27 -0
- package/exploration/index.d.ts.map +1 -0
- package/exploration/index.js +24 -0
- package/exploration/presets.d.ts +27 -0
- package/exploration/presets.d.ts.map +1 -0
- package/exploration/presets.js +114 -0
- package/exploration/reducer.d.ts +23 -0
- package/exploration/reducer.d.ts.map +1 -0
- package/exploration/reducer.js +332 -0
- package/exploration/selection.d.ts +25 -0
- package/exploration/selection.d.ts.map +1 -0
- package/exploration/selection.js +34 -0
- package/exploration/selectors.d.ts +56 -0
- package/exploration/selectors.d.ts.map +1 -0
- package/exploration/selectors.js +110 -0
- package/exploration/types.d.ts +294 -0
- package/exploration/types.d.ts.map +1 -0
- package/exploration/types.js +33 -0
- package/expr/expression.d.ts +457 -0
- package/expr/expression.d.ts.map +1 -0
- package/expr/expression.js +663 -0
- package/expr/index.d.ts +32 -0
- package/expr/index.d.ts.map +1 -0
- package/expr/index.js +30 -0
- package/gen/honua/v1/feature_service_pb.d.ts +918 -0
- package/gen/honua/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/honua/v1/feature_service_pb.js +375 -0
- package/generated-app/errors.d.ts +41 -0
- package/generated-app/errors.d.ts.map +1 -0
- package/generated-app/errors.js +41 -0
- package/generated-app/index.d.ts +17 -0
- package/generated-app/index.d.ts.map +1 -0
- package/generated-app/index.js +12 -0
- package/generated-app/manifest.d.ts +183 -0
- package/generated-app/manifest.d.ts.map +1 -0
- package/generated-app/manifest.js +14 -0
- package/generated-app/projection.d.ts +44 -0
- package/generated-app/projection.d.ts.map +1 -0
- package/generated-app/projection.js +234 -0
- package/generated-app/runtime.d.ts +215 -0
- package/generated-app/runtime.d.ts.map +1 -0
- package/generated-app/runtime.js +1196 -0
- package/interactions/detail-model.d.ts +54 -0
- package/interactions/detail-model.d.ts.map +1 -0
- package/interactions/detail-model.js +105 -0
- package/interactions/exploration-bindings.d.ts +120 -0
- package/interactions/exploration-bindings.d.ts.map +1 -0
- package/interactions/exploration-bindings.js +218 -0
- package/interactions/feature-state.d.ts +144 -0
- package/interactions/feature-state.d.ts.map +1 -0
- package/interactions/feature-state.js +169 -0
- package/interactions/hit-test.d.ts +112 -0
- package/interactions/hit-test.d.ts.map +1 -0
- package/interactions/hit-test.js +291 -0
- package/interactions/index.d.ts +31 -0
- package/interactions/index.d.ts.map +1 -0
- package/interactions/index.js +24 -0
- package/interactions/share-state.d.ts +36 -0
- package/interactions/share-state.d.ts.map +1 -0
- package/interactions/share-state.js +123 -0
- package/map/feature-service-adapter.d.ts +138 -0
- package/map/feature-service-adapter.d.ts.map +1 -0
- package/map/feature-service-adapter.js +279 -0
- package/map/honua-map.d.ts +203 -0
- package/map/honua-map.d.ts.map +1 -0
- package/map/honua-map.js +347 -0
- package/map/index.d.ts +27 -0
- package/map/index.d.ts.map +1 -0
- package/map/index.js +22 -0
- package/map/maplibre-target.d.ts +103 -0
- package/map/maplibre-target.d.ts.map +1 -0
- package/map/maplibre-target.js +164 -0
- package/map/webmap-maplibre.d.ts +60 -0
- package/map/webmap-maplibre.d.ts.map +1 -0
- package/map/webmap-maplibre.js +212 -0
- package/package.json +37 -0
- package/react/context.d.ts +32 -0
- package/react/context.d.ts.map +1 -0
- package/react/context.js +19 -0
- package/react/honua-layer.d.ts +35 -0
- package/react/honua-layer.d.ts.map +1 -0
- package/react/honua-layer.js +47 -0
- package/react/honua-map.d.ts +83 -0
- package/react/honua-map.d.ts.map +1 -0
- package/react/honua-map.js +109 -0
- package/react/honua-popup.d.ts +26 -0
- package/react/honua-popup.d.ts.map +1 -0
- package/react/honua-popup.js +34 -0
- package/react/hooks.d.ts +127 -0
- package/react/hooks.d.ts.map +1 -0
- package/react/hooks.js +206 -0
- package/react/index.d.ts +42 -0
- package/react/index.d.ts.map +1 -0
- package/react/index.js +34 -0
- package/react/provider.d.ts +36 -0
- package/react/provider.d.ts.map +1 -0
- package/react/provider.js +35 -0
- package/react/query-cache.d.ts +76 -0
- package/react/query-cache.d.ts.map +1 -0
- package/react/query-cache.js +188 -0
- package/runtime/diff.d.ts +31 -0
- package/runtime/diff.d.ts.map +1 -0
- package/runtime/diff.js +143 -0
- package/runtime/errors.d.ts +40 -0
- package/runtime/errors.d.ts.map +1 -0
- package/runtime/errors.js +35 -0
- package/runtime/index.d.ts +59 -0
- package/runtime/index.d.ts.map +1 -0
- package/runtime/index.js +43 -0
- package/runtime/legend.d.ts +23 -0
- package/runtime/legend.d.ts.map +1 -0
- package/runtime/legend.js +38 -0
- package/runtime/load-package.d.ts +125 -0
- package/runtime/load-package.d.ts.map +1 -0
- package/runtime/load-package.js +264 -0
- package/runtime/map-package-fetch.d.ts +107 -0
- package/runtime/map-package-fetch.d.ts.map +1 -0
- package/runtime/map-package-fetch.js +336 -0
- package/runtime/map-package-realtime.d.ts +169 -0
- package/runtime/map-package-realtime.d.ts.map +1 -0
- package/runtime/map-package-realtime.js +350 -0
- package/runtime/map-package-validation.d.ts +43 -0
- package/runtime/map-package-validation.d.ts.map +1 -0
- package/runtime/map-package-validation.js +269 -0
- package/runtime/map-package-watch.d.ts +78 -0
- package/runtime/map-package-watch.d.ts.map +1 -0
- package/runtime/map-package-watch.js +695 -0
- package/runtime/map-package.d.ts +167 -0
- package/runtime/map-package.d.ts.map +1 -0
- package/runtime/map-package.js +15 -0
- package/runtime/ogc-styles.d.ts +110 -0
- package/runtime/ogc-styles.d.ts.map +1 -0
- package/runtime/ogc-styles.js +195 -0
- package/runtime/popups.d.ts +62 -0
- package/runtime/popups.d.ts.map +1 -0
- package/runtime/popups.js +95 -0
- package/runtime/query-tiles.d.ts +217 -0
- package/runtime/query-tiles.d.ts.map +1 -0
- package/runtime/query-tiles.js +801 -0
- package/runtime/runtime.d.ts +243 -0
- package/runtime/runtime.d.ts.map +1 -0
- package/runtime/runtime.js +1147 -0
- package/runtime/source-bridge.d.ts +99 -0
- package/runtime/source-bridge.d.ts.map +1 -0
- package/runtime/source-bridge.js +401 -0
- package/runtime/style-compose.d.ts +46 -0
- package/runtime/style-compose.d.ts.map +1 -0
- package/runtime/style-compose.js +144 -0
- package/runtime/style-interactions.d.ts +141 -0
- package/runtime/style-interactions.d.ts.map +1 -0
- package/runtime/style-interactions.js +574 -0
- package/runtime/style-spec-validation.d.ts +23 -0
- package/runtime/style-spec-validation.d.ts.map +1 -0
- package/runtime/style-spec-validation.js +357 -0
- package/studio/capability-manifest.d.ts +285 -0
- package/studio/capability-manifest.d.ts.map +1 -0
- package/studio/capability-manifest.js +59 -0
- package/studio/chart-spec.d.ts +85 -0
- package/studio/chart-spec.d.ts.map +1 -0
- package/studio/chart-spec.js +106 -0
- package/studio/index.d.ts +34 -0
- package/studio/index.d.ts.map +1 -0
- package/studio/index.js +27 -0
- package/studio/provenance.d.ts +131 -0
- package/studio/provenance.d.ts.map +1 -0
- package/studio/provenance.js +39 -0
- package/studio/publish.d.ts +64 -0
- package/studio/publish.d.ts.map +1 -0
- package/studio/publish.js +13 -0
- package/studio/types.d.ts +278 -0
- package/studio/types.d.ts.map +1 -0
- package/studio/types.js +71 -0
- package/studio/validate.d.ts +45 -0
- package/studio/validate.d.ts.map +1 -0
- package/studio/validate.js +255 -0
- package/studio/validation.d.ts +112 -0
- package/studio/validation.d.ts.map +1 -0
- package/studio/validation.js +42 -0
- package/style/factory.d.ts +30 -0
- package/style/factory.d.ts.map +1 -0
- package/style/factory.js +61 -0
- package/style/index.d.ts +25 -0
- package/style/index.d.ts.map +1 -0
- package/style/index.js +24 -0
- package/style/specification.d.ts +203 -0
- package/style/specification.d.ts.map +1 -0
- package/style/specification.js +178 -0
- package/style/wms-wmts-resolvers.d.ts +38 -0
- package/style/wms-wmts-resolvers.d.ts.map +1 -0
- package/style/wms-wmts-resolvers.js +92 -0
- package/webmap/contract.d.ts +28 -0
- package/webmap/contract.d.ts.map +1 -0
- package/webmap/contract.js +130 -0
- package/webmap/convert-basemap.d.ts +17 -0
- package/webmap/convert-basemap.d.ts.map +1 -0
- package/webmap/convert-basemap.js +78 -0
- package/webmap/convert-extent.d.ts +17 -0
- package/webmap/convert-extent.d.ts.map +1 -0
- package/webmap/convert-extent.js +114 -0
- package/webmap/convert-label.d.ts +13 -0
- package/webmap/convert-label.d.ts.map +1 -0
- package/webmap/convert-label.js +125 -0
- package/webmap/convert-layer.d.ts +22 -0
- package/webmap/convert-layer.d.ts.map +1 -0
- package/webmap/convert-layer.js +181 -0
- package/webmap/convert-popup.d.ts +34 -0
- package/webmap/convert-popup.d.ts.map +1 -0
- package/webmap/convert-popup.js +68 -0
- package/webmap/convert-renderer.d.ts +17 -0
- package/webmap/convert-renderer.d.ts.map +1 -0
- package/webmap/convert-renderer.js +161 -0
- package/webmap/convert-symbol.d.ts +17 -0
- package/webmap/convert-symbol.d.ts.map +1 -0
- package/webmap/convert-symbol.js +122 -0
- package/webmap/index.d.ts +44 -0
- package/webmap/index.d.ts.map +1 -0
- package/webmap/index.js +33 -0
- package/webmap/parse.d.ts +25 -0
- package/webmap/parse.d.ts.map +1 -0
- package/webmap/parse.js +83 -0
- package/webmap/types.d.ts +264 -0
- package/webmap/types.d.ts.map +1 -0
- package/webmap/types.js +13 -0
- package/webmap/warnings.d.ts +30 -0
- package/webmap/warnings.d.ts.map +1 -0
- package/webmap/warnings.js +37 -0
package/core/odata.js
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OData v4 entity-set adapter. Wraps an OData entity set behind the shared
|
|
3
|
+
* `Source<T>` contract surface in `src/contract/source.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Library posture is recorded in
|
|
6
|
+
* `docs/decisions/odata-library-selection.md`: the canonical surface is a
|
|
7
|
+
* thin URL/JSON serializer over `HonuaClient.pipelineFetch` /
|
|
8
|
+
* `pipelineRequestJson` (rather than the GeoServices-shaped
|
|
9
|
+
* `request()` helper, which would inject `f=json` and trip Honua
|
|
10
|
+
* Server's OData validators) so that the auth / retry / telemetry
|
|
11
|
+
* pipeline stays centralized. The dialect-specific `$batch` / `$apply`
|
|
12
|
+
* / `$search` / `$deltatoken` operations live behind the typed escape
|
|
13
|
+
* hatch returned by `Source.protocol("odata")`.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { createHonuaCacheState, honuaCacheValidatorFromHeaders, honuaMetadataRequestHeaders, isHonuaCacheEntryFresh, normalizeHonuaMetadataRequestOptions, withHonuaCacheState, withoutHonuaCacheState, } from "./cache-state.js";
|
|
18
|
+
import { trimTrailingSlashes } from "./path-utils.js";
|
|
19
|
+
// ── Runtime class ─────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Runtime handle for one OData entity set. Returned by
|
|
22
|
+
* `Source.protocol("odata")` so callers can reach the dialect-specific
|
|
23
|
+
* surface (`$batch`, `$apply`, `$search`, `$deltatoken`) without leaking
|
|
24
|
+
* OData-shaped types into the canonical `Source` API.
|
|
25
|
+
*/
|
|
26
|
+
export class HonuaOdataEntitySet {
|
|
27
|
+
client;
|
|
28
|
+
entitySet;
|
|
29
|
+
/**
|
|
30
|
+
* Unqualified entity-set name used as the CSDL metadata-lookup key.
|
|
31
|
+
* Equals `entitySet` for direct paths like `"Parcels"`. For
|
|
32
|
+
* layer-scoped navigation paths like `"Layers(1)/Features"` it is the
|
|
33
|
+
* navigation suffix `"Features"` so capability and schema lookups
|
|
34
|
+
* still find the entry the server emits in `<EntitySet Name="…">`.
|
|
35
|
+
*/
|
|
36
|
+
entitySetName;
|
|
37
|
+
basePath;
|
|
38
|
+
cachedMetadata;
|
|
39
|
+
inflightMetadata;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.client = options.client;
|
|
42
|
+
this.entitySet = options.entitySet;
|
|
43
|
+
this.entitySetName = extractEntitySetName(options.entitySet);
|
|
44
|
+
this.basePath = normalizeBasePath(options.basePath);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fetch (and cache) the `$metadata` document. The cache is per
|
|
48
|
+
* `HonuaOdataEntitySet` instance so callers can pin a metadata snapshot
|
|
49
|
+
* to a long-lived `Source` without re-parsing on every request.
|
|
50
|
+
*/
|
|
51
|
+
async metadata(options = {}) {
|
|
52
|
+
const metadataOptions = normalizeHonuaMetadataRequestOptions(options);
|
|
53
|
+
const bypass = metadataOptions.cache === "bypass";
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const freshCachedMetadata = this.cachedMetadata
|
|
56
|
+
? isHonuaCacheEntryFresh(this.cachedMetadata.cachedAtMs, now, metadataOptions.ttlMs)
|
|
57
|
+
: false;
|
|
58
|
+
if (!bypass && !metadataOptions.refresh && this.cachedMetadata && freshCachedMetadata) {
|
|
59
|
+
return withOdataMetadataCacheState(this.cachedMetadata, "hit", {
|
|
60
|
+
now,
|
|
61
|
+
ttlMs: metadataOptions.ttlMs,
|
|
62
|
+
staleIfErrorMs: metadataOptions.staleIfErrorMs,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!bypass && !metadataOptions.refresh && this.inflightMetadata)
|
|
66
|
+
return this.inflightMetadata;
|
|
67
|
+
const previous = this.cachedMetadata;
|
|
68
|
+
const promise = this.fetchMetadata(metadataOptions, previous).then((entry) => {
|
|
69
|
+
if (!bypass) {
|
|
70
|
+
this.cachedMetadata = entry;
|
|
71
|
+
}
|
|
72
|
+
const status = bypass ? "bypass" : entry.status;
|
|
73
|
+
return withOdataMetadataCacheState(entry, status, {
|
|
74
|
+
now: Date.now(),
|
|
75
|
+
ttlMs: metadataOptions.ttlMs,
|
|
76
|
+
staleIfErrorMs: metadataOptions.staleIfErrorMs,
|
|
77
|
+
...(status === "refreshed" ? { revalidatedAt: new Date().toISOString() } : {}),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const staleFallback = (error) => {
|
|
81
|
+
if (!bypass && metadataOptions.staleIfError && previous) {
|
|
82
|
+
return withOdataMetadataCacheState(previous, "stale", {
|
|
83
|
+
now: Date.now(),
|
|
84
|
+
ttlMs: metadataOptions.ttlMs,
|
|
85
|
+
staleIfErrorMs: metadataOptions.staleIfErrorMs,
|
|
86
|
+
refreshErrorId: odataMetadataRefreshErrorId(error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
};
|
|
91
|
+
if (!bypass && !metadataOptions.refresh) {
|
|
92
|
+
const trackedPromise = promise.catch(staleFallback).finally(() => {
|
|
93
|
+
if (this.inflightMetadata === trackedPromise)
|
|
94
|
+
this.inflightMetadata = undefined;
|
|
95
|
+
});
|
|
96
|
+
this.inflightMetadata = trackedPromise;
|
|
97
|
+
return trackedPromise;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return await promise;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return staleFallback(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Fetch one page of the entity set. */
|
|
107
|
+
async query(params = {}) {
|
|
108
|
+
const path = this.entitySetPath();
|
|
109
|
+
const query = serializeQueryParams(params);
|
|
110
|
+
const body = await this.requestJson("GET", path, query, params.signal);
|
|
111
|
+
return {
|
|
112
|
+
rows: body.value ?? [],
|
|
113
|
+
...(typeof body["@odata.count"] === "number" ? { totalCount: body["@odata.count"] } : {}),
|
|
114
|
+
...(typeof body["@odata.nextLink"] === "string" ? { nextLink: body["@odata.nextLink"] } : {}),
|
|
115
|
+
...(typeof body["@odata.deltaLink"] === "string" ? { deltaLink: body["@odata.deltaLink"] } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Drain server-driven `@odata.nextLink` pages. When `params.top` is
|
|
120
|
+
* supplied it is treated as a hard cap on collected rows. Callers that
|
|
121
|
+
* want a lookahead row to detect truncation must add it themselves; the
|
|
122
|
+
* canonical `Source.queryAll` path does this with `top = limit + 1`,
|
|
123
|
+
* while ids-only projections enforce their own result cap.
|
|
124
|
+
* Returning `top + 1` here would double-count for callers that already
|
|
125
|
+
* added the lookahead, so this helper respects `top` exactly.
|
|
126
|
+
*/
|
|
127
|
+
async queryAll(params = {}) {
|
|
128
|
+
const cap = typeof params.top === "number" && params.top >= 0 ? params.top : undefined;
|
|
129
|
+
const collected = [];
|
|
130
|
+
let totalCount;
|
|
131
|
+
let next;
|
|
132
|
+
let firstPageParams = params;
|
|
133
|
+
while (true) {
|
|
134
|
+
const page = next
|
|
135
|
+
? await this.followNextLink(next, params.signal)
|
|
136
|
+
: await this.query(firstPageParams ?? {});
|
|
137
|
+
firstPageParams = undefined;
|
|
138
|
+
if (totalCount === undefined && typeof page.totalCount === "number") {
|
|
139
|
+
totalCount = page.totalCount;
|
|
140
|
+
}
|
|
141
|
+
for (const row of page.rows) {
|
|
142
|
+
if (cap !== undefined && collected.length >= cap)
|
|
143
|
+
break;
|
|
144
|
+
collected.push(row);
|
|
145
|
+
if (cap !== undefined && collected.length >= cap)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (cap !== undefined && collected.length >= cap)
|
|
149
|
+
break;
|
|
150
|
+
if (!page.nextLink)
|
|
151
|
+
break;
|
|
152
|
+
next = page.nextLink;
|
|
153
|
+
}
|
|
154
|
+
return totalCount === undefined ? { rows: collected } : { rows: collected, totalCount };
|
|
155
|
+
}
|
|
156
|
+
/** Yield one page per server response. Used by `Source.stream()`. */
|
|
157
|
+
async *queryStream(params = {}) {
|
|
158
|
+
let page = await this.query(params);
|
|
159
|
+
yield page;
|
|
160
|
+
while (page.nextLink) {
|
|
161
|
+
page = await this.followNextLink(page.nextLink, params.signal);
|
|
162
|
+
yield page;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Insert a row via `POST /<entitySet>`. */
|
|
166
|
+
async add(body, options = {}) {
|
|
167
|
+
const path = this.entitySetPath();
|
|
168
|
+
return this.requestJson("POST", path, undefined, options.signal, JSON.stringify(body), {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Update a row via `PATCH /<entitySet>(<key>)`. The OData server in
|
|
174
|
+
* Honua Server intentionally does not implement `PUT`; the SDK issues
|
|
175
|
+
* `PATCH` with the full canonical body to match a `PUT` replacement on
|
|
176
|
+
* the canonical surface (documented in
|
|
177
|
+
* `docs/protocol-capability-matrix.md` under the OData section).
|
|
178
|
+
*/
|
|
179
|
+
async update(key, body, options = {}) {
|
|
180
|
+
const path = `${this.entitySetPath()}(${encodeOdataKey(key)})`;
|
|
181
|
+
return this.requestJson("PATCH", path, undefined, options.signal, JSON.stringify(body), {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** Delete a row via `DELETE /<entitySet>(<key>)`. */
|
|
186
|
+
async delete(key, options = {}) {
|
|
187
|
+
const path = `${this.entitySetPath()}(${encodeOdataKey(key)})`;
|
|
188
|
+
await this.requestJson("DELETE", path, undefined, options.signal);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Submit a `$batch` request using the OData JSON batch format.
|
|
192
|
+
* Multipart/mixed fallback is intentionally not implemented; Honua
|
|
193
|
+
* Server emits both formats but the JSON envelope is sufficient for
|
|
194
|
+
* the canonical surface and avoids the multipart parser.
|
|
195
|
+
*
|
|
196
|
+
* `atomicity: "all"` stamps the same `atomicityGroup` token on every
|
|
197
|
+
* request so the server runs them in one change-set with rollback on
|
|
198
|
+
* any failure (per OData v4 §11.7.7.3 and Honua Server's
|
|
199
|
+
* `ODataBatchHandler`, which groups by `request.AtomicityGroup`).
|
|
200
|
+
* `atomicity: "none"` (default) leaves the field unset so each request
|
|
201
|
+
* runs independently.
|
|
202
|
+
*/
|
|
203
|
+
async batch(operations, options = {}) {
|
|
204
|
+
if (operations.length === 0)
|
|
205
|
+
return { responses: [] };
|
|
206
|
+
const groupId = options.atomicity === "all" ? "g1" : undefined;
|
|
207
|
+
const requests = operations.map((op, index) => ({
|
|
208
|
+
id: op.id ?? String(index + 1),
|
|
209
|
+
method: op.method,
|
|
210
|
+
url: stripLeadingSlash(op.url),
|
|
211
|
+
...(op.headers
|
|
212
|
+
? { headers: { "Content-Type": "application/json", ...op.headers } }
|
|
213
|
+
: {
|
|
214
|
+
headers: { "Content-Type": "application/json" },
|
|
215
|
+
}),
|
|
216
|
+
...(op.body !== undefined ? { body: op.body } : {}),
|
|
217
|
+
...(groupId !== undefined ? { atomicityGroup: groupId } : {}),
|
|
218
|
+
}));
|
|
219
|
+
const response = await this.requestJson("POST", `${this.basePath}/$batch`, undefined, options.signal, JSON.stringify({ requests }), { "Content-Type": "application/json" });
|
|
220
|
+
return { responses: response.responses ?? [] };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Run an `$apply` aggregation. The OData server applies the
|
|
224
|
+
* transformation pipeline (`aggregate`, `groupby`, `filter`,
|
|
225
|
+
* `compute`); the SDK returns the rows unchanged so callers can shape
|
|
226
|
+
* them onto the canonical `Result.aggregateRows`.
|
|
227
|
+
*/
|
|
228
|
+
async apply(transformations, params = {}) {
|
|
229
|
+
const path = this.entitySetPath();
|
|
230
|
+
const query = serializeQueryParams({ apply: transformations, filter: params.filter });
|
|
231
|
+
const body = await this.requestJson("GET", path, query, params.signal);
|
|
232
|
+
return {
|
|
233
|
+
rows: body.value ?? [],
|
|
234
|
+
...(typeof body["@odata.count"] === "number" ? { totalCount: body["@odata.count"] } : {}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Run a `$search` full-text query. Returns an `HonuaOdataPage` so the
|
|
239
|
+
* canonical `Result<T>` projection is symmetric with `query()`.
|
|
240
|
+
*/
|
|
241
|
+
async search(text, params = {}) {
|
|
242
|
+
return this.query({ ...params, search: text });
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* `$deltatoken`-driven change feed. Yields `HonuaOdataDeltaPage` per
|
|
246
|
+
* server response. The final page carries `deltaLink`; callers persist
|
|
247
|
+
* the encoded token for the next call to `delta({ since })`.
|
|
248
|
+
*/
|
|
249
|
+
async *delta(options = {}) {
|
|
250
|
+
const params = options.since ? { deltatoken: options.since } : {};
|
|
251
|
+
if (options.signal)
|
|
252
|
+
params.signal = options.signal;
|
|
253
|
+
let page = await this.query(params);
|
|
254
|
+
yield {
|
|
255
|
+
rows: page.rows,
|
|
256
|
+
...(page.nextLink ? { nextLink: page.nextLink } : {}),
|
|
257
|
+
...(page.deltaLink ? { deltaLink: page.deltaLink } : {}),
|
|
258
|
+
};
|
|
259
|
+
while (page.nextLink) {
|
|
260
|
+
page = await this.followNextLink(page.nextLink, options.signal);
|
|
261
|
+
yield {
|
|
262
|
+
rows: page.rows,
|
|
263
|
+
...(page.nextLink ? { nextLink: page.nextLink } : {}),
|
|
264
|
+
...(page.deltaLink ? { deltaLink: page.deltaLink } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Last-resort raw passthrough — flows through `HonuaClient.pipelineFetch`
|
|
270
|
+
* so auth headers, retry, timeout, interceptors, telemetry, and
|
|
271
|
+
* normalized error mapping all apply. The returned `Response` is
|
|
272
|
+
* unconsumed; callers pick `.json()`, `.text()`, or `.arrayBuffer()`.
|
|
273
|
+
*/
|
|
274
|
+
async raw(method, path, init = {}) {
|
|
275
|
+
const normalized = method.toUpperCase();
|
|
276
|
+
return this.client.pipelineFetch(normalized, ensureLeadingSlash(path), init);
|
|
277
|
+
}
|
|
278
|
+
// ── Internal ────────────────────────────────────────────────
|
|
279
|
+
entitySetPath() {
|
|
280
|
+
return `${this.basePath}/${this.entitySet}`;
|
|
281
|
+
}
|
|
282
|
+
async fetchMetadata(options, cached) {
|
|
283
|
+
// Route through pipelineFetch so auth headers, interceptors, retry,
|
|
284
|
+
// timeout, and normalized error handling all apply. The default
|
|
285
|
+
// pipeline does not impose an `Accept` header — we ask for XML
|
|
286
|
+
// explicitly because the OData `$metadata` route emits CSDL.
|
|
287
|
+
const response = await this.client.pipelineFetch("GET", `${this.basePath}/$metadata`, {
|
|
288
|
+
headers: honuaMetadataRequestHeaders({
|
|
289
|
+
accept: "application/xml",
|
|
290
|
+
refresh: options.refresh || Boolean(cached),
|
|
291
|
+
bypass: options.cache === "bypass",
|
|
292
|
+
validator: cached?.validator,
|
|
293
|
+
}),
|
|
294
|
+
}, options.signal, { okStatuses: [304] });
|
|
295
|
+
if (response.status === 304 && cached) {
|
|
296
|
+
const validator = honuaCacheValidatorFromHeaders(response.headers) ?? cached.validator;
|
|
297
|
+
return {
|
|
298
|
+
...cached,
|
|
299
|
+
cachedAtMs: Date.now(),
|
|
300
|
+
...(validator ? { validator } : {}),
|
|
301
|
+
status: "refreshed",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const xml = await response.text();
|
|
305
|
+
const validator = honuaCacheValidatorFromHeaders(response.headers);
|
|
306
|
+
const sourceUpdatedAt = response.headers.get("last-modified") ?? undefined;
|
|
307
|
+
return {
|
|
308
|
+
metadata: withoutHonuaCacheState(parseOdataMetadata(xml)),
|
|
309
|
+
cachedAtMs: Date.now(),
|
|
310
|
+
keyFingerprint: `metadata:odata:${this.basePath}:$metadata`,
|
|
311
|
+
status: options.cache === "bypass" ? "bypass" : cached ? "refreshed" : "miss",
|
|
312
|
+
...(validator ? { validator } : {}),
|
|
313
|
+
...(sourceUpdatedAt ? { sourceUpdatedAt } : {}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async followNextLink(nextLink, signal) {
|
|
317
|
+
// OData servers may emit either an absolute URL or a relative one.
|
|
318
|
+
// Normalize through the same fetch path used by `query()` so retry,
|
|
319
|
+
// auth interceptors, and telemetry still apply.
|
|
320
|
+
const { path, query } = parseNextLink(nextLink, this.client.serverBaseUrl, this.basePath);
|
|
321
|
+
const body = await this.requestJson("GET", path, query, signal);
|
|
322
|
+
return {
|
|
323
|
+
rows: body.value ?? [],
|
|
324
|
+
...(typeof body["@odata.count"] === "number" ? { totalCount: body["@odata.count"] } : {}),
|
|
325
|
+
...(typeof body["@odata.nextLink"] === "string" ? { nextLink: body["@odata.nextLink"] } : {}),
|
|
326
|
+
...(typeof body["@odata.deltaLink"] === "string" ? { deltaLink: body["@odata.deltaLink"] } : {}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Wrap `HonuaClient.pipelineRequestJson` with OData-shaped query
|
|
331
|
+
* serialization so callers can pass parsed query bags. Critically this
|
|
332
|
+
* does **not** go through `HonuaClient.request` — that helper injects
|
|
333
|
+
* `f=json`, which Honua Server's OData validators reject as
|
|
334
|
+
* `InvalidQueryOption`. The OData server defaults to JSON when the
|
|
335
|
+
* request advertises `Accept: application/json` (the pipeline default).
|
|
336
|
+
*/
|
|
337
|
+
async requestJson(method, path, query, signal, body, headers) {
|
|
338
|
+
const params = new URLSearchParams();
|
|
339
|
+
if (query) {
|
|
340
|
+
for (const [key, value] of Object.entries(query)) {
|
|
341
|
+
params.set(key, String(value));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const pathWithQuery = params.size > 0 ? `${path}?${params.toString()}` : path;
|
|
345
|
+
return this.client.pipelineRequestJson(method, pathWithQuery, {
|
|
346
|
+
...(headers ? { headers } : {}),
|
|
347
|
+
...(body !== undefined ? { body } : {}),
|
|
348
|
+
}, signal);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
352
|
+
function withOdataMetadataCacheState(entry, status, options) {
|
|
353
|
+
return withHonuaCacheState(entry.metadata, createHonuaCacheState({
|
|
354
|
+
scope: "metadata",
|
|
355
|
+
status,
|
|
356
|
+
keyFingerprint: entry.keyFingerprint,
|
|
357
|
+
ageMs: Math.max(0, options.now - entry.cachedAtMs),
|
|
358
|
+
...(options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
|
|
359
|
+
...(options.staleIfErrorMs !== undefined ? { staleIfErrorMs: options.staleIfErrorMs } : {}),
|
|
360
|
+
...(options.revalidatedAt ? { revalidatedAt: options.revalidatedAt } : {}),
|
|
361
|
+
...(entry.sourceUpdatedAt ? { sourceUpdatedAt: entry.sourceUpdatedAt } : {}),
|
|
362
|
+
...(entry.validator ? { validator: entry.validator } : {}),
|
|
363
|
+
...(options.refreshErrorId ? { refreshErrorId: options.refreshErrorId } : {}),
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
function odataMetadataRefreshErrorId(error) {
|
|
367
|
+
if (error instanceof Error && error.name) {
|
|
368
|
+
return error.name;
|
|
369
|
+
}
|
|
370
|
+
return "unknown";
|
|
371
|
+
}
|
|
372
|
+
function normalizeBasePath(path) {
|
|
373
|
+
if (path === undefined || path === "")
|
|
374
|
+
return "/odata";
|
|
375
|
+
return path.startsWith("/") ? trimTrailingSlashes(path) || "/" : `/${trimTrailingSlashes(path)}`;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extract the unqualified entity-set name from a path-shaped token.
|
|
379
|
+
* Direct identifiers (`"Parcels"`) round-trip unchanged; navigation paths
|
|
380
|
+
* (`"Layers(1)/Features"`) collapse to the trailing segment so CSDL
|
|
381
|
+
* metadata lookups (which key by `<EntitySet Name="…">`) still hit.
|
|
382
|
+
*/
|
|
383
|
+
function extractEntitySetName(path) {
|
|
384
|
+
const slash = path.lastIndexOf("/");
|
|
385
|
+
return slash === -1 ? path : path.slice(slash + 1);
|
|
386
|
+
}
|
|
387
|
+
function ensureLeadingSlash(path) {
|
|
388
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
389
|
+
}
|
|
390
|
+
function stripLeadingSlash(path) {
|
|
391
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Make an OData entity key safe to interpolate into the URL path segment
|
|
395
|
+
* `<entitySet>(<key>)`. A raw key containing characters such as `&`, `?`,
|
|
396
|
+
* `#`, `/`, space, or `'` would otherwise split or mis-target the path
|
|
397
|
+
* (e.g. `Customers(A&B)`).
|
|
398
|
+
*
|
|
399
|
+
* The key is treated as an OData literal: single-quoted spans are kept as
|
|
400
|
+
* quoted string literals (with embedded `'` doubled per OData v4 §5.1.1.6.1),
|
|
401
|
+
* and structural characters that delimit composite keys (`,`, `=`) and
|
|
402
|
+
* literal quotes are preserved. Everything else is percent-encoded so the
|
|
403
|
+
* resulting segment is a single, correctly-targeted URL path component.
|
|
404
|
+
*/
|
|
405
|
+
function encodeOdataKey(key) {
|
|
406
|
+
const raw = String(key);
|
|
407
|
+
let out = "";
|
|
408
|
+
let inQuote = false;
|
|
409
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
410
|
+
const ch = raw[i];
|
|
411
|
+
if (ch === "'") {
|
|
412
|
+
// Toggle quoted-literal state and keep the (URL-safe) quote verbatim.
|
|
413
|
+
inQuote = !inQuote;
|
|
414
|
+
out += "'";
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (!inQuote && (ch === "," || ch === "=")) {
|
|
418
|
+
// Composite-key delimiters stay structural outside quoted spans.
|
|
419
|
+
out += ch;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Percent-encode anything that is unsafe in a path segment. Unreserved
|
|
423
|
+
// characters (RFC 3986) pass through untouched.
|
|
424
|
+
if (/[A-Za-z0-9\-._~]/.test(ch)) {
|
|
425
|
+
out += ch;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
out += encodeURIComponent(ch);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Translate canonical query params onto the OData `$`-prefixed query
|
|
435
|
+
* string. Empty strings, undefined values, and zero-length lists are
|
|
436
|
+
* dropped so the URL stays minimal.
|
|
437
|
+
*/
|
|
438
|
+
function serializeQueryParams(params) {
|
|
439
|
+
const out = {};
|
|
440
|
+
if (params.filter !== undefined && params.filter !== "")
|
|
441
|
+
out.$filter = params.filter;
|
|
442
|
+
if (params.select && params.select.length > 0)
|
|
443
|
+
out.$select = params.select.join(",");
|
|
444
|
+
if (params.orderBy && params.orderBy.length > 0)
|
|
445
|
+
out.$orderby = params.orderBy.join(",");
|
|
446
|
+
if (typeof params.top === "number" && Number.isFinite(params.top))
|
|
447
|
+
out.$top = params.top;
|
|
448
|
+
if (typeof params.skip === "number" && Number.isFinite(params.skip) && params.skip > 0)
|
|
449
|
+
out.$skip = params.skip;
|
|
450
|
+
if (params.expand && params.expand.length > 0)
|
|
451
|
+
out.$expand = params.expand.join(",");
|
|
452
|
+
if (params.count)
|
|
453
|
+
out.$count = "true";
|
|
454
|
+
if (params.skiptoken !== undefined && params.skiptoken !== "")
|
|
455
|
+
out.$skiptoken = params.skiptoken;
|
|
456
|
+
if (params.search !== undefined && params.search !== "")
|
|
457
|
+
out.$search = params.search;
|
|
458
|
+
if (params.apply !== undefined && params.apply !== "")
|
|
459
|
+
out.$apply = params.apply;
|
|
460
|
+
if (params.deltatoken !== undefined && params.deltatoken !== "")
|
|
461
|
+
out.$deltatoken = params.deltatoken;
|
|
462
|
+
if (params.extra)
|
|
463
|
+
Object.assign(out, params.extra);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* `@odata.nextLink` is either an absolute URL (preferred by spec) or a
|
|
468
|
+
* service-relative URL. Reduce it to the path + query bag the same fetch
|
|
469
|
+
* pipeline expects.
|
|
470
|
+
*/
|
|
471
|
+
function parseNextLink(link, serverBaseUrl, basePath) {
|
|
472
|
+
const absolute = link.startsWith("http://") || link.startsWith("https://")
|
|
473
|
+
? new URL(link)
|
|
474
|
+
: new URL(link, `${ensureTrailingSlash(serverBaseUrl)}${stripLeadingSlash(basePath)}/`);
|
|
475
|
+
const path = absolute.pathname;
|
|
476
|
+
const query = {};
|
|
477
|
+
for (const [key, value] of absolute.searchParams)
|
|
478
|
+
query[key] = value;
|
|
479
|
+
return { path, query };
|
|
480
|
+
}
|
|
481
|
+
function ensureTrailingSlash(url) {
|
|
482
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
483
|
+
}
|
|
484
|
+
// ── EDMX (CSDL) parsing ───────────────────────────────────────
|
|
485
|
+
/**
|
|
486
|
+
* Parse the subset of CSDL we need to power capability negotiation and
|
|
487
|
+
* key resolution: entity sets, entity-type keys, property names + types,
|
|
488
|
+
* `Edm.Geography` / `Edm.Geometry` typing, and `Capabilities.*`
|
|
489
|
+
* annotations. The XML is read via small regular expressions because we
|
|
490
|
+
* intentionally do not pull a DOM dep — the surface we read is
|
|
491
|
+
* well-defined by OData CSDL §6 and the Honua Server spec page.
|
|
492
|
+
*/
|
|
493
|
+
export function parseOdataMetadata(xml) {
|
|
494
|
+
const entitySets = {};
|
|
495
|
+
const keys = {};
|
|
496
|
+
const fields = {};
|
|
497
|
+
const capabilities = {};
|
|
498
|
+
// Sibling annotations: <Annotations Target="<schema>.<container>/<entitySet>">
|
|
499
|
+
// …</Annotations>. Honua Server emits Capabilities.* this way (next to
|
|
500
|
+
// the EntityContainer) rather than nesting them inside <EntitySet>, so
|
|
501
|
+
// capability negotiation must merge both shapes.
|
|
502
|
+
const siblingByEntitySet = new Map();
|
|
503
|
+
const siblingAnnotationsRe = /<Annotations\s+([^>]*?)>([\s\S]*?)<\/Annotations>/g;
|
|
504
|
+
for (const match of xml.matchAll(siblingAnnotationsRe)) {
|
|
505
|
+
const attrs = match[1] ?? "";
|
|
506
|
+
const target = readAttr(attrs, "Target");
|
|
507
|
+
if (!target)
|
|
508
|
+
continue;
|
|
509
|
+
const slash = target.lastIndexOf("/");
|
|
510
|
+
if (slash === -1)
|
|
511
|
+
continue;
|
|
512
|
+
const setName = target.slice(slash + 1);
|
|
513
|
+
if (!setName)
|
|
514
|
+
continue;
|
|
515
|
+
const body = match[2] ?? "";
|
|
516
|
+
const existing = siblingByEntitySet.get(setName);
|
|
517
|
+
siblingByEntitySet.set(setName, existing === undefined ? body : `${existing}\n${body}`);
|
|
518
|
+
}
|
|
519
|
+
// Entity sets: <EntitySet Name="Foo" EntityType="ns.FooEntity"/>
|
|
520
|
+
const entitySetRe = /<EntitySet\s+([^>]*?)\/>|<EntitySet\s+([^>]*?)>([\s\S]*?)<\/EntitySet>/g;
|
|
521
|
+
for (const match of xml.matchAll(entitySetRe)) {
|
|
522
|
+
const attrs = match[1] ?? match[2] ?? "";
|
|
523
|
+
const name = readAttr(attrs, "Name");
|
|
524
|
+
const type = readAttr(attrs, "EntityType");
|
|
525
|
+
if (!name || !type)
|
|
526
|
+
continue;
|
|
527
|
+
entitySets[name] = stripNamespace(type);
|
|
528
|
+
const inlineBody = match[3] ?? "";
|
|
529
|
+
const siblingBody = siblingByEntitySet.get(name) ?? "";
|
|
530
|
+
capabilities[name] = parseEntitySetAnnotations(`${inlineBody}\n${siblingBody}`);
|
|
531
|
+
siblingByEntitySet.delete(name);
|
|
532
|
+
}
|
|
533
|
+
// Capture annotations for entity sets that were not declared inline (e.g.
|
|
534
|
+
// when a server emits only the sibling block). The simple-name suffix
|
|
535
|
+
// from the Target still keys a capability entry so callers can find it.
|
|
536
|
+
for (const [setName, body] of siblingByEntitySet) {
|
|
537
|
+
capabilities[setName] = parseEntitySetAnnotations(body);
|
|
538
|
+
}
|
|
539
|
+
// Entity types: <EntityType Name="FooEntity">…<Key><PropertyRef Name="Id"/></Key><Property…/>…</EntityType>
|
|
540
|
+
const entityTypeRe = /<EntityType\s+([^>]*?)>([\s\S]*?)<\/EntityType>/g;
|
|
541
|
+
for (const match of xml.matchAll(entityTypeRe)) {
|
|
542
|
+
const attrs = match[1] ?? "";
|
|
543
|
+
const body = match[2] ?? "";
|
|
544
|
+
const name = readAttr(attrs, "Name");
|
|
545
|
+
if (!name)
|
|
546
|
+
continue;
|
|
547
|
+
const keyNames = [];
|
|
548
|
+
const keyBlock = /<Key>([\s\S]*?)<\/Key>/.exec(body);
|
|
549
|
+
if (keyBlock) {
|
|
550
|
+
for (const ref of keyBlock[1].matchAll(/<PropertyRef\s+([^>]*?)\/>/g)) {
|
|
551
|
+
const refName = readAttr(ref[1], "Name");
|
|
552
|
+
if (refName)
|
|
553
|
+
keyNames.push(refName);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
keys[name] = keyNames;
|
|
557
|
+
const props = [];
|
|
558
|
+
for (const propMatch of body.matchAll(/<Property\s+([^>]*?)\/>|<Property\s+([^>]*?)>/g)) {
|
|
559
|
+
const propAttrs = propMatch[1] ?? propMatch[2] ?? "";
|
|
560
|
+
const propName = readAttr(propAttrs, "Name");
|
|
561
|
+
const propType = readAttr(propAttrs, "Type");
|
|
562
|
+
if (!propName || !propType)
|
|
563
|
+
continue;
|
|
564
|
+
const nullable = readAttr(propAttrs, "Nullable");
|
|
565
|
+
const sridAttr = readAttr(propAttrs, "SRID");
|
|
566
|
+
const isSpatial = propType.startsWith("Edm.Geography") || propType.startsWith("Edm.Geometry");
|
|
567
|
+
const info = { name: propName, type: propType };
|
|
568
|
+
if (nullable !== undefined)
|
|
569
|
+
info.nullable = nullable !== "false";
|
|
570
|
+
if (isSpatial)
|
|
571
|
+
info.isSpatial = true;
|
|
572
|
+
if (sridAttr !== undefined) {
|
|
573
|
+
const srid = Number(sridAttr);
|
|
574
|
+
if (Number.isFinite(srid))
|
|
575
|
+
info.srid = srid;
|
|
576
|
+
}
|
|
577
|
+
props.push(info);
|
|
578
|
+
}
|
|
579
|
+
fields[name] = props;
|
|
580
|
+
}
|
|
581
|
+
return { entitySets, keys, fields, capabilities };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Convenience: project the field schema for one entity set into the
|
|
585
|
+
* canonical `HonuaFieldInfo[]` shape reused by `Result.fields`.
|
|
586
|
+
*/
|
|
587
|
+
export function odataFieldSchema(metadata, entitySet) {
|
|
588
|
+
const typeName = metadata.entitySets[entitySet];
|
|
589
|
+
if (!typeName)
|
|
590
|
+
return [];
|
|
591
|
+
const props = metadata.fields[typeName];
|
|
592
|
+
if (!props)
|
|
593
|
+
return [];
|
|
594
|
+
return props.map((p) => ({ name: p.name, type: edmToHonuaFieldType(p.type) }));
|
|
595
|
+
}
|
|
596
|
+
function edmToHonuaFieldType(edm) {
|
|
597
|
+
if (edm.startsWith("Edm.Geography") || edm.startsWith("Edm.Geometry"))
|
|
598
|
+
return "esriFieldTypeGeometry";
|
|
599
|
+
if (edm === "Edm.String")
|
|
600
|
+
return "esriFieldTypeString";
|
|
601
|
+
if (edm === "Edm.Int32")
|
|
602
|
+
return "esriFieldTypeInteger";
|
|
603
|
+
if (edm === "Edm.Int64")
|
|
604
|
+
return "esriFieldTypeOID";
|
|
605
|
+
if (edm === "Edm.Double")
|
|
606
|
+
return "esriFieldTypeDouble";
|
|
607
|
+
if (edm === "Edm.Single")
|
|
608
|
+
return "esriFieldTypeSingle";
|
|
609
|
+
if (edm === "Edm.Boolean")
|
|
610
|
+
return "esriFieldTypeSmallInteger";
|
|
611
|
+
if (edm === "Edm.DateTimeOffset" || edm === "Edm.Date")
|
|
612
|
+
return "esriFieldTypeDate";
|
|
613
|
+
return "esriFieldTypeString";
|
|
614
|
+
}
|
|
615
|
+
function parseEntitySetAnnotations(body) {
|
|
616
|
+
const out = {};
|
|
617
|
+
for (const match of body.matchAll(/<Annotation\s+([^>]*?)(?:\/>|>([\s\S]*?)<\/Annotation>)/g)) {
|
|
618
|
+
const attrs = match[1] ?? "";
|
|
619
|
+
const term = readAttr(attrs, "Term");
|
|
620
|
+
if (!term || !term.includes("Capabilities."))
|
|
621
|
+
continue;
|
|
622
|
+
const inner = (match[2] ?? "").trim();
|
|
623
|
+
const boolValue = readAttr(attrs, "Bool") ??
|
|
624
|
+
matchBool(inner) ??
|
|
625
|
+
matchPropertyValueBool(inner, "Insertable") ??
|
|
626
|
+
matchPropertyValueBool(inner, "Updatable") ??
|
|
627
|
+
matchPropertyValueBool(inner, "Deletable") ??
|
|
628
|
+
matchPropertyValueBool(inner, "Searchable") ??
|
|
629
|
+
matchPropertyValueBool(inner, "Filterable") ??
|
|
630
|
+
matchPropertyValueBool(inner, "Expandable") ??
|
|
631
|
+
matchPropertyValueBool(inner, "Selectable") ??
|
|
632
|
+
matchPropertyValueBool(inner, "Countable") ??
|
|
633
|
+
// Honua Server uses `Supported` for ChangeTracking and other
|
|
634
|
+
// capability records; without this fallback, `<PropertyValue
|
|
635
|
+
// Property="Supported" Bool="false"/>` would be silently treated
|
|
636
|
+
// as supported.
|
|
637
|
+
matchPropertyValueBool(inner, "Supported");
|
|
638
|
+
if (term.endsWith("InsertRestrictions")) {
|
|
639
|
+
out.insert = matchPropertyValueBool(inner, "Insertable") !== "false";
|
|
640
|
+
}
|
|
641
|
+
else if (term.endsWith("UpdateRestrictions")) {
|
|
642
|
+
out.update = matchPropertyValueBool(inner, "Updatable") !== "false";
|
|
643
|
+
}
|
|
644
|
+
else if (term.endsWith("DeleteRestrictions")) {
|
|
645
|
+
out.delete = matchPropertyValueBool(inner, "Deletable") !== "false";
|
|
646
|
+
}
|
|
647
|
+
else if (term.endsWith("BatchSupported") || term.endsWith("BatchSupport")) {
|
|
648
|
+
out.batch = boolValue !== "false";
|
|
649
|
+
}
|
|
650
|
+
else if (term.endsWith("FilterRestrictions") || term.endsWith("FilterFunctions")) {
|
|
651
|
+
out.filter = boolValue !== "false";
|
|
652
|
+
}
|
|
653
|
+
else if (term.endsWith("ExpandRestrictions")) {
|
|
654
|
+
out.expand = boolValue !== "false";
|
|
655
|
+
}
|
|
656
|
+
else if (term.endsWith("SelectSupport")) {
|
|
657
|
+
out.select = boolValue !== "false";
|
|
658
|
+
}
|
|
659
|
+
else if (term.endsWith("SearchRestrictions")) {
|
|
660
|
+
out.search = boolValue !== "false";
|
|
661
|
+
}
|
|
662
|
+
else if (term.endsWith("CountRestrictions")) {
|
|
663
|
+
out.count = boolValue !== "false";
|
|
664
|
+
}
|
|
665
|
+
else if (term.endsWith("ChangeTracking")) {
|
|
666
|
+
out.delta = boolValue !== "false";
|
|
667
|
+
}
|
|
668
|
+
else if (term.endsWith("ApplySupported") || term.endsWith("ApplyRestrictions")) {
|
|
669
|
+
out.apply = boolValue !== "false";
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
function readAttr(attrs, name) {
|
|
675
|
+
const re = new RegExp(`${name}\\s*=\\s*"([^"]*)"`);
|
|
676
|
+
const match = re.exec(attrs);
|
|
677
|
+
return match ? match[1] : undefined;
|
|
678
|
+
}
|
|
679
|
+
function stripNamespace(name) {
|
|
680
|
+
const idx = name.lastIndexOf(".");
|
|
681
|
+
return idx === -1 ? name : name.slice(idx + 1);
|
|
682
|
+
}
|
|
683
|
+
function matchBool(inner) {
|
|
684
|
+
const m = /<Bool>(true|false)<\/Bool>/.exec(inner);
|
|
685
|
+
return m ? m[1] : undefined;
|
|
686
|
+
}
|
|
687
|
+
function matchPropertyValueBool(inner, property) {
|
|
688
|
+
const re = new RegExp(`<PropertyValue\\s+Property="${property}"\\s+Bool="(true|false)"`);
|
|
689
|
+
const match = re.exec(inner);
|
|
690
|
+
return match ? match[1] : undefined;
|
|
691
|
+
}
|
|
692
|
+
const UNSUPPORTED_OPERATOR_RES = [
|
|
693
|
+
{ token: "has", re: /\bhas\b/i },
|
|
694
|
+
{ token: "in", re: /\bin\s*\(/i },
|
|
695
|
+
{ token: "any", re: /\bany\s*\(/i },
|
|
696
|
+
{ token: "all", re: /\ball\s*\(/i },
|
|
697
|
+
{ token: "cast", re: /\bcast\s*\(/i },
|
|
698
|
+
{ token: "isof", re: /\bisof\s*\(/i },
|
|
699
|
+
];
|
|
700
|
+
// ── SQL → OData filter rewrite ────────────────────────────────
|
|
701
|
+
/**
|
|
702
|
+
* `Query.where` is documented as SQL-92 / CQL2 on the canonical surface.
|
|
703
|
+
* The OData `$filter` grammar is close to but not identical to SQL-92;
|
|
704
|
+
* this minimal rewriter handles the cases that would otherwise produce a
|
|
705
|
+
* server-side parse error rather than a translated query. Full SQL→OData
|
|
706
|
+
* translation is tracked as follow-up work in
|
|
707
|
+
* `docs/decisions/odata-library-selection.md`.
|
|
708
|
+
*
|
|
709
|
+
* Rewrites applied:
|
|
710
|
+
* - `IS NULL` / `IS NOT NULL` → `eq null` / `ne null`
|
|
711
|
+
* - `<col> = '...'`, `<col> = N` → `<col> eq '...'` / `<col> eq N`
|
|
712
|
+
* - SQL-style `<>` → `ne`
|
|
713
|
+
* - SQL comparison operators `>=` / `<=` / `>` / `<` → `ge` / `le` /
|
|
714
|
+
* `gt` / `lt` (Honua Server's OData lexer rejects bare punctuation
|
|
715
|
+
* comparisons as `InvalidQueryOption`).
|
|
716
|
+
*
|
|
717
|
+
* Predicates whose syntax already matches OData (e.g. `STATE eq 'CA'`)
|
|
718
|
+
* pass through unchanged. Unsupported operators (`HAS`, `IN`, `ANY`,
|
|
719
|
+
* `ALL`, `CAST`, `ISOF` from the parity matrix) are detected and
|
|
720
|
+
* rejected with a typed error rather than left to the server.
|
|
721
|
+
*/
|
|
722
|
+
export function rewriteWhereToOdataFilter(where) {
|
|
723
|
+
if (where === "" || where === "1=1")
|
|
724
|
+
return "";
|
|
725
|
+
// Tokenize into alternating non-literal / literal spans so the rewriter
|
|
726
|
+
// and unsupported-operator detector skip the contents of quoted string
|
|
727
|
+
// literals. Without this split, a value like `NAME = 'A=B'` would have
|
|
728
|
+
// its embedded `=` rewritten to ` eq `, corrupting the literal before it
|
|
729
|
+
// reaches `$filter`. OData uses single-quoted strings with `''` for
|
|
730
|
+
// embedded quotes (per OData v4 §5.1.1.6.1).
|
|
731
|
+
const spans = splitOdataLiteralSpans(where);
|
|
732
|
+
for (let idx = 0; idx < spans.length; idx += 2) {
|
|
733
|
+
const segment = spans[idx];
|
|
734
|
+
if (segment === "")
|
|
735
|
+
continue;
|
|
736
|
+
for (const banned of UNSUPPORTED_OPERATOR_RES) {
|
|
737
|
+
if (banned.re.test(segment)) {
|
|
738
|
+
throw new Error(`odata: $filter does not support "${banned.token}"; the parity matrix lists this operator as unsupported on Honua Server.`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
let out = segment;
|
|
742
|
+
out = out.replace(/\bIS\s+NOT\s+NULL\b/gi, "ne null");
|
|
743
|
+
out = out.replace(/\bIS\s+NULL\b/gi, "eq null");
|
|
744
|
+
// `<>` must be rewritten before bare `<` / `>` so the not-equal
|
|
745
|
+
// operator does not get split into `lt` / `gt`. Two-char operators
|
|
746
|
+
// (`>=` / `<=`) likewise come before bare `>` / `<`.
|
|
747
|
+
out = out.replace(/<>/g, "ne");
|
|
748
|
+
out = out.replace(/(\b\w+\b)\s*>=\s*/g, (_match, lhs) => `${lhs} ge `);
|
|
749
|
+
out = out.replace(/(\b\w+\b)\s*<=\s*/g, (_match, lhs) => `${lhs} le `);
|
|
750
|
+
out = out.replace(/(\b\w+\b)\s*>\s*/g, (_match, lhs) => `${lhs} gt `);
|
|
751
|
+
out = out.replace(/(\b\w+\b)\s*<\s*/g, (_match, lhs) => `${lhs} lt `);
|
|
752
|
+
out = out.replace(/(\b\w+\b)\s*=\s*/g, (_match, lhs) => `${lhs} eq `);
|
|
753
|
+
out = out.replace(/\bAND\b/gi, "and");
|
|
754
|
+
out = out.replace(/\bOR\b/gi, "or");
|
|
755
|
+
out = out.replace(/\bNOT\b/gi, "not");
|
|
756
|
+
spans[idx] = out;
|
|
757
|
+
}
|
|
758
|
+
return spans.join("").trim();
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Split an OData `$filter`-style expression into alternating non-literal
|
|
762
|
+
* and literal spans. Even indices contain code; odd indices contain
|
|
763
|
+
* single-quoted string literals (with quotes preserved). Embedded `''`
|
|
764
|
+
* escape sequences inside a literal are kept verbatim. An unterminated
|
|
765
|
+
* literal is treated as if it ran to the end of the input.
|
|
766
|
+
*/
|
|
767
|
+
function splitOdataLiteralSpans(input) {
|
|
768
|
+
const spans = [];
|
|
769
|
+
let buf = "";
|
|
770
|
+
let i = 0;
|
|
771
|
+
while (i < input.length) {
|
|
772
|
+
const ch = input[i];
|
|
773
|
+
if (ch !== "'") {
|
|
774
|
+
buf += ch;
|
|
775
|
+
i += 1;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
spans.push(buf);
|
|
779
|
+
let literal = "'";
|
|
780
|
+
i += 1;
|
|
781
|
+
while (i < input.length) {
|
|
782
|
+
const c = input[i];
|
|
783
|
+
if (c === "'") {
|
|
784
|
+
// OData string literals double the quote to embed it.
|
|
785
|
+
if (input[i + 1] === "'") {
|
|
786
|
+
literal += "''";
|
|
787
|
+
i += 2;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
literal += "'";
|
|
791
|
+
i += 1;
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
literal += c;
|
|
795
|
+
i += 1;
|
|
796
|
+
}
|
|
797
|
+
spans.push(literal);
|
|
798
|
+
buf = "";
|
|
799
|
+
}
|
|
800
|
+
spans.push(buf);
|
|
801
|
+
return spans;
|
|
802
|
+
}
|
|
803
|
+
export function buildOdataSpatialFilter(spatialFilter, context = {}) {
|
|
804
|
+
const rel = spatialFilter.spatialRel ?? "esriSpatialRelIntersects";
|
|
805
|
+
if (rel !== "esriSpatialRelIntersects" &&
|
|
806
|
+
rel !== "esriSpatialRelEnvelopeIntersects" &&
|
|
807
|
+
rel !== "esriSpatialRelDistance") {
|
|
808
|
+
throw new Error(`odata: spatialRel "${rel}" is not supported; OData parity matrix only implements geo.intersects (intersects/envelope-intersects) and geo.distance.`);
|
|
809
|
+
}
|
|
810
|
+
const column = resolveGeometryColumn(context);
|
|
811
|
+
const wkt = geometryToWkt(spatialFilter.geometry, spatialFilter.geometryType);
|
|
812
|
+
const inputSrid = resolveInputSrid(spatialFilter.geometry, context);
|
|
813
|
+
const literal = wktLiteral(wkt, inputSrid);
|
|
814
|
+
if (rel === "esriSpatialRelDistance") {
|
|
815
|
+
if (typeof spatialFilter.distance !== "number" || !Number.isFinite(spatialFilter.distance)) {
|
|
816
|
+
throw new Error('odata: spatialRel "esriSpatialRelDistance" requires a numeric distance (meters for geography).');
|
|
817
|
+
}
|
|
818
|
+
return `geo.distance(${column},${literal}) le ${spatialFilter.distance}`;
|
|
819
|
+
}
|
|
820
|
+
return `geo.intersects(${column},${literal})`;
|
|
821
|
+
}
|
|
822
|
+
function resolveGeometryColumn(context) {
|
|
823
|
+
if (context.geometryColumn)
|
|
824
|
+
return context.geometryColumn;
|
|
825
|
+
const spatial = context.geometryFields?.find((f) => f.isSpatial);
|
|
826
|
+
if (!spatial) {
|
|
827
|
+
throw new Error("odata: cannot translate spatialFilter — no geometry column was supplied and metadata exposes no Edm.Geography field.");
|
|
828
|
+
}
|
|
829
|
+
return spatial.name;
|
|
830
|
+
}
|
|
831
|
+
function resolveInputSrid(geometry, context) {
|
|
832
|
+
if (context.inputSrid !== undefined && context.inputSrid !== null && context.inputSrid !== "") {
|
|
833
|
+
return context.inputSrid;
|
|
834
|
+
}
|
|
835
|
+
const sr = geometry.spatialReference;
|
|
836
|
+
if (sr?.wkid !== undefined && Number.isFinite(sr.wkid))
|
|
837
|
+
return sr.wkid;
|
|
838
|
+
if (sr?.latestWkid !== undefined && Number.isFinite(sr.latestWkid))
|
|
839
|
+
return sr.latestWkid;
|
|
840
|
+
// Prefer the SRID of the *selected* geometry column when a name was
|
|
841
|
+
// pinned by the caller — entity types with multiple spatial fields
|
|
842
|
+
// can declare different SRIDs per column, and the WKT must be stamped
|
|
843
|
+
// with the SRID of the column the predicate is being evaluated
|
|
844
|
+
// against. Falls back to the first spatial field only when no column
|
|
845
|
+
// was pinned (or the pinned column is absent from the supplied
|
|
846
|
+
// metadata fields).
|
|
847
|
+
const selected = context.geometryColumn !== undefined
|
|
848
|
+
? context.geometryFields?.find((f) => f.name === context.geometryColumn)
|
|
849
|
+
: undefined;
|
|
850
|
+
if (selected?.srid !== undefined && Number.isFinite(selected.srid))
|
|
851
|
+
return selected.srid;
|
|
852
|
+
if (context.geometryColumn !== undefined && selected)
|
|
853
|
+
return undefined;
|
|
854
|
+
const spatial = context.geometryFields?.find((f) => f.isSpatial);
|
|
855
|
+
if (spatial?.srid !== undefined && Number.isFinite(spatial.srid))
|
|
856
|
+
return spatial.srid;
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
function wktLiteral(wkt, srid) {
|
|
860
|
+
if (srid === undefined || srid === null || srid === "") {
|
|
861
|
+
return `geography'${wkt}'`;
|
|
862
|
+
}
|
|
863
|
+
return `geography'SRID=${srid};${wkt}'`;
|
|
864
|
+
}
|
|
865
|
+
// Coordinates are interpolated verbatim into a WKT string that is wrapped in an
|
|
866
|
+
// OData `geography'...'` literal and passed straight into `$filter`. Any value
|
|
867
|
+
// that is not a finite number must be rejected here: a non-numeric string (e.g.
|
|
868
|
+
// one containing a single quote) would otherwise terminate the OData string
|
|
869
|
+
// literal and inject attacker-controlled tokens into the server-side filter, and
|
|
870
|
+
// NaN/Infinity/undefined would produce malformed WKT.
|
|
871
|
+
function wktCoordinate(value, context) {
|
|
872
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
873
|
+
if (!Number.isFinite(num)) {
|
|
874
|
+
throw new Error(`odata: spatialFilter ${context} must be a finite number.`);
|
|
875
|
+
}
|
|
876
|
+
return num;
|
|
877
|
+
}
|
|
878
|
+
function wktPoint(pt, context) {
|
|
879
|
+
return `${wktCoordinate(pt[0], `${context}[0]`)} ${wktCoordinate(pt[1], `${context}[1]`)}`;
|
|
880
|
+
}
|
|
881
|
+
function geometryToWkt(geometry, geometryType) {
|
|
882
|
+
if (geometryType === "esriGeometryEnvelope") {
|
|
883
|
+
const xmin = wktCoordinate(geometry.xmin, "envelope.xmin");
|
|
884
|
+
const ymin = wktCoordinate(geometry.ymin, "envelope.ymin");
|
|
885
|
+
const xmax = wktCoordinate(geometry.xmax, "envelope.xmax");
|
|
886
|
+
const ymax = wktCoordinate(geometry.ymax, "envelope.ymax");
|
|
887
|
+
return `POLYGON((${xmin} ${ymin}, ${xmax} ${ymin}, ${xmax} ${ymax}, ${xmin} ${ymax}, ${xmin} ${ymin}))`;
|
|
888
|
+
}
|
|
889
|
+
if (geometryType === "esriGeometryPoint") {
|
|
890
|
+
const x = wktCoordinate(geometry.x, "point.x");
|
|
891
|
+
const y = wktCoordinate(geometry.y, "point.y");
|
|
892
|
+
return `POINT(${x} ${y})`;
|
|
893
|
+
}
|
|
894
|
+
if (geometryType === "esriGeometryPolygon") {
|
|
895
|
+
const rings = geometry.rings;
|
|
896
|
+
if (!Array.isArray(rings) || rings.length === 0) {
|
|
897
|
+
throw new Error("odata: polygon spatialFilter has no rings");
|
|
898
|
+
}
|
|
899
|
+
const inner = rings
|
|
900
|
+
.map((ring) => `(${ring.map((pt) => wktPoint(pt, "polygon.ring")).join(", ")})`)
|
|
901
|
+
.join(", ");
|
|
902
|
+
return `POLYGON(${inner})`;
|
|
903
|
+
}
|
|
904
|
+
if (geometryType === "esriGeometryPolyline") {
|
|
905
|
+
const paths = geometry.paths;
|
|
906
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
907
|
+
throw new Error("odata: polyline spatialFilter has no paths");
|
|
908
|
+
}
|
|
909
|
+
const path = paths[0];
|
|
910
|
+
return `LINESTRING(${path.map((pt) => wktPoint(pt, "polyline.path")).join(", ")})`;
|
|
911
|
+
}
|
|
912
|
+
throw new Error(`odata: spatialFilter.geometryType "${geometryType}" is not supported.`);
|
|
913
|
+
}
|