@honua/sdk-esri-compat 0.1.12 → 0.1.14
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/README.md +2 -0
- package/columnar/apache-arrow.d.ts +17 -0
- package/columnar/apache-arrow.d.ts.map +1 -0
- package/columnar/apache-arrow.js +1036 -0
- package/columnar/batch-cache-conformance.d.ts +66 -0
- package/columnar/batch-cache-conformance.d.ts.map +1 -0
- package/columnar/batch-cache-conformance.js +317 -0
- package/columnar/batch-cache-indexeddb.d.ts +31 -0
- package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
- package/columnar/batch-cache-indexeddb.js +148 -0
- package/columnar/batch-cache.d.ts +298 -0
- package/columnar/batch-cache.d.ts.map +1 -0
- package/columnar/batch-cache.js +757 -0
- package/columnar/geoarrow-aggregate.d.ts +178 -0
- package/columnar/geoarrow-aggregate.d.ts.map +1 -0
- package/columnar/geoarrow-aggregate.js +976 -0
- package/columnar/geoarrow-patch.d.ts +382 -0
- package/columnar/geoarrow-patch.d.ts.map +1 -0
- package/columnar/geoarrow-patch.js +1656 -0
- package/columnar/geoarrow-reproject.d.ts +22 -0
- package/columnar/geoarrow-reproject.d.ts.map +1 -0
- package/columnar/geoarrow-reproject.js +119 -0
- package/columnar/geoarrow-serialization.d.ts +68 -0
- package/columnar/geoarrow-serialization.d.ts.map +1 -0
- package/columnar/geoarrow-serialization.js +331 -0
- package/columnar/geoarrow-types.d.ts +271 -0
- package/columnar/geoarrow-types.d.ts.map +1 -0
- package/columnar/geoarrow-types.js +15 -0
- package/columnar/geoarrow.d.ts +52 -0
- package/columnar/geoarrow.d.ts.map +1 -0
- package/columnar/geoarrow.js +1469 -0
- package/columnar/index.d.ts +43 -0
- package/columnar/index.d.ts.map +1 -0
- package/columnar/index.js +29 -0
- package/columnar/operators.d.ts +70 -0
- package/columnar/operators.d.ts.map +1 -0
- package/columnar/operators.js +307 -0
- package/columnar/result.d.ts +369 -0
- package/columnar/result.d.ts.map +1 -0
- package/columnar/result.js +815 -0
- package/columnar/telemetry.d.ts +152 -0
- package/columnar/telemetry.d.ts.map +1 -0
- package/columnar/telemetry.js +265 -0
- package/columnar/transfer.d.ts +28 -0
- package/columnar/transfer.d.ts.map +1 -0
- package/columnar/transfer.js +669 -0
- package/columnar/types.d.ts +175 -0
- package/columnar/types.d.ts.map +1 -0
- package/columnar/types.js +22 -0
- package/columnar/worker.d.ts +157 -0
- package/columnar/worker.d.ts.map +1 -0
- package/columnar/worker.js +1059 -0
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +11 -8
- package/core/client.d.ts.map +1 -1
- package/core/client.js +87 -19
- package/core/deprecation-notice.d.ts +28 -0
- package/core/deprecation-notice.d.ts.map +1 -0
- package/core/deprecation-notice.js +22 -0
- package/core/esri-geojson.d.ts.map +1 -1
- package/core/esri-geojson.js +14 -8
- package/core/optional-grpc-peer.d.ts +15 -0
- package/core/optional-grpc-peer.d.ts.map +1 -0
- package/core/optional-grpc-peer.js +60 -0
- package/esri-compat/basemap-gallery.d.ts +2 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -1
- package/esri-compat/basemap-gallery.js +12 -0
- package/esri-compat/basemap.d.ts +4 -0
- package/esri-compat/basemap.d.ts.map +1 -1
- package/esri-compat/basemap.js +70 -0
- package/esri-compat/bookmarks.d.ts +2 -0
- package/esri-compat/bookmarks.d.ts.map +1 -1
- package/esri-compat/bookmarks.js +12 -0
- package/esri-compat/controls.d.ts +18 -0
- package/esri-compat/controls.d.ts.map +1 -1
- package/esri-compat/controls.js +120 -57
- package/esri-compat/directions.d.ts +4 -0
- package/esri-compat/directions.d.ts.map +1 -1
- package/esri-compat/directions.js +30 -20
- package/esri-compat/editor.d.ts +4 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +16 -0
- package/esri-compat/esri-config.d.ts +2 -1
- package/esri-compat/esri-config.d.ts.map +1 -1
- package/esri-compat/feature-effect.d.ts +23 -0
- package/esri-compat/feature-effect.d.ts.map +1 -0
- package/esri-compat/feature-effect.js +12 -0
- package/esri-compat/feature-form.d.ts +6 -0
- package/esri-compat/feature-form.d.ts.map +1 -1
- package/esri-compat/feature-form.js +63 -0
- package/esri-compat/feature-layer.d.ts +21 -2
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +172 -1
- package/esri-compat/feature-table.d.ts +5 -0
- package/esri-compat/feature-table.d.ts.map +1 -1
- package/esri-compat/feature-table.js +23 -0
- package/esri-compat/identity-manager.d.ts +1 -1
- package/esri-compat/identity-manager.js +1 -1
- package/esri-compat/legend.d.ts +2 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +98 -0
- package/esri-compat/locator.d.ts +1 -1
- package/esri-compat/locator.js +1 -1
- package/esri-compat/map-view-mount.d.ts +1 -0
- package/esri-compat/map-view-mount.d.ts.map +1 -1
- package/esri-compat/map-view-mount.js +46 -25
- package/esri-compat/map-view.d.ts +13 -0
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +94 -8
- package/esri-compat/map.d.ts +5 -1
- package/esri-compat/map.d.ts.map +1 -1
- package/esri-compat/map.js +10 -0
- package/esri-compat/measurement.d.ts +2 -0
- package/esri-compat/measurement.d.ts.map +1 -1
- package/esri-compat/measurement.js +14 -0
- package/esri-compat/point.d.ts +7 -0
- package/esri-compat/point.d.ts.map +1 -1
- package/esri-compat/point.js +33 -3
- package/esri-compat/popup.d.ts +2 -0
- package/esri-compat/popup.d.ts.map +1 -1
- package/esri-compat/popup.js +12 -0
- package/esri-compat/portal.d.ts +1 -1
- package/esri-compat/portal.js +1 -1
- package/esri-compat/print.d.ts +3 -0
- package/esri-compat/print.d.ts.map +1 -1
- package/esri-compat/print.js +15 -0
- package/esri-compat/search.d.ts +4 -0
- package/esri-compat/search.d.ts.map +1 -1
- package/esri-compat/search.js +25 -28
- package/esri-compat/sketch.d.ts +2 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +12 -0
- package/esri-compat/widget-host.d.ts +46 -17
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +84 -20
- package/esri-compat/widget-shell-hosts.d.ts +61 -0
- package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
- package/esri-compat/widget-shell-hosts.js +76 -0
- package/index.d.ts +3 -1
- package/index.js +1 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.js +58 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.js +370 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.js +75 -0
- package/package.json +21 -2
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/canonical.d.ts +7 -0
- package/query-planner/canonical.d.ts.map +1 -0
- package/query-planner/canonical.js +145 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/cql2-json.d.ts +17 -0
- package/query-planner/cql2-json.d.ts.map +1 -0
- package/query-planner/cql2-json.js +646 -0
- package/query-planner/diagnostics.d.ts +35 -0
- package/query-planner/diagnostics.d.ts.map +1 -0
- package/query-planner/diagnostics.js +386 -0
- package/query-planner/duckdb.d.ts +79 -0
- package/query-planner/duckdb.d.ts.map +1 -0
- package/query-planner/duckdb.js +772 -0
- package/query-planner/execution-context.d.ts +5 -0
- package/query-planner/execution-context.d.ts.map +1 -0
- package/query-planner/execution-context.js +52 -0
- package/query-planner/executor.d.ts +5 -0
- package/query-planner/executor.d.ts.map +1 -0
- package/query-planner/executor.js +229 -0
- package/query-planner/geoservices.d.ts +91 -0
- package/query-planner/geoservices.d.ts.map +1 -0
- package/query-planner/geoservices.js +688 -0
- package/query-planner/grpc.d.ts +100 -0
- package/query-planner/grpc.d.ts.map +1 -0
- package/query-planner/grpc.js +760 -0
- package/query-planner/index.d.ts +59 -0
- package/query-planner/index.d.ts.map +1 -0
- package/query-planner/index.js +43 -0
- package/query-planner/ir.d.ts +25 -0
- package/query-planner/ir.d.ts.map +1 -0
- package/query-planner/ir.js +479 -0
- package/query-planner/local-aggregate.d.ts +5 -0
- package/query-planner/local-aggregate.d.ts.map +1 -0
- package/query-planner/local-aggregate.js +126 -0
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +45 -0
- package/query-planner/odata.d.ts.map +1 -0
- package/query-planner/odata.js +519 -0
- package/query-planner/ogc-compiler.d.ts +55 -0
- package/query-planner/ogc-compiler.d.ts.map +1 -0
- package/query-planner/ogc-compiler.js +480 -0
- package/query-planner/ogc-features.d.ts +53 -0
- package/query-planner/ogc-features.d.ts.map +1 -0
- package/query-planner/ogc-features.js +639 -0
- package/query-planner/planner.d.ts +17 -0
- package/query-planner/planner.d.ts.map +1 -0
- package/query-planner/planner.js +937 -0
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/resource.d.ts +94 -0
- package/query-planner/resource.d.ts.map +1 -0
- package/query-planner/resource.js +526 -0
- package/query-planner/semantic-canonical.d.ts +15 -0
- package/query-planner/semantic-canonical.d.ts.map +1 -0
- package/query-planner/semantic-canonical.js +62 -0
- package/query-planner/semantic-compat.d.ts +10 -0
- package/query-planner/semantic-compat.d.ts.map +1 -0
- package/query-planner/semantic-compat.js +35 -0
- package/query-planner/semantic-compiler.d.ts +64 -0
- package/query-planner/semantic-compiler.d.ts.map +1 -0
- package/query-planner/semantic-compiler.js +81 -0
- package/query-planner/semantic-literals.d.ts +30 -0
- package/query-planner/semantic-literals.d.ts.map +1 -0
- package/query-planner/semantic-literals.js +133 -0
- package/query-planner/semantic-types.d.ts +265 -0
- package/query-planner/semantic-types.d.ts.map +1 -0
- package/query-planner/semantic-types.js +1 -0
- package/query-planner/semantic.d.ts +54 -0
- package/query-planner/semantic.d.ts.map +1 -0
- package/query-planner/semantic.js +1421 -0
- package/query-planner/types.d.ts +712 -0
- package/query-planner/types.d.ts.map +1 -0
- package/query-planner/types.js +49 -0
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-semantic.d.ts +58 -0
- package/query-planner/wfs-semantic.d.ts.map +1 -0
- package/query-planner/wfs-semantic.js +547 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +4 -0
- package/query-planner/wfs.d.ts.map +1 -0
- package/query-planner/wfs.js +2 -0
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
import { compareLogicalDomainValues, isLogicalDomainValueCompatible, parseSourceSchemaV2, validateExecutableCrsBinding, validateSourceCrsDefinition, } from "../contract/schema.js";
|
|
2
|
+
import { PROTOCOLS } from "../contract/types.js";
|
|
3
|
+
import { HonuaQueryPlanningError } from "./types.js";
|
|
4
|
+
export const MAX_SEMANTIC_QUERY_BYTES = 256 * 1024;
|
|
5
|
+
export const MAX_SEMANTIC_QUERY_DEPTH = 64;
|
|
6
|
+
export const MAX_SEMANTIC_QUERY_NODES = 20_000;
|
|
7
|
+
export const MAX_SEMANTIC_QUERY_COLLECTION_ITEMS = 10_000;
|
|
8
|
+
export const MAX_SEMANTIC_QUERY_TEXT_BYTES = 64 * 1024;
|
|
9
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
10
|
+
const EXTENSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*\.)+[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11
|
+
const BUILT_IN_DIALECTS = new Set([
|
|
12
|
+
"honua-grpc",
|
|
13
|
+
"geoservices-sql92",
|
|
14
|
+
"cql2-json",
|
|
15
|
+
"cql2-text",
|
|
16
|
+
"fes-2.0",
|
|
17
|
+
"odata-4.0",
|
|
18
|
+
"duckdb-sql",
|
|
19
|
+
]);
|
|
20
|
+
const ORDERED_TYPE_KINDS = new Set(["integer", "float", "decimal", "string", "uuid", "date", "time", "timestamp"]);
|
|
21
|
+
const NUMERIC_TYPE_KINDS = new Set(["integer", "float", "decimal"]);
|
|
22
|
+
const SCALAR_TYPE_KINDS = new Set([
|
|
23
|
+
"boolean",
|
|
24
|
+
"integer",
|
|
25
|
+
"float",
|
|
26
|
+
"decimal",
|
|
27
|
+
"string",
|
|
28
|
+
"binary",
|
|
29
|
+
"uuid",
|
|
30
|
+
"date",
|
|
31
|
+
"time",
|
|
32
|
+
"timestamp",
|
|
33
|
+
"duration",
|
|
34
|
+
]);
|
|
35
|
+
const TOPOLOGICAL_OPERATORS = new Set([
|
|
36
|
+
"equals",
|
|
37
|
+
"intersects",
|
|
38
|
+
"within",
|
|
39
|
+
"contains",
|
|
40
|
+
"disjoint",
|
|
41
|
+
"touches",
|
|
42
|
+
"overlaps",
|
|
43
|
+
"crosses",
|
|
44
|
+
]);
|
|
45
|
+
const DISTANCE_UNITS = new Set([
|
|
46
|
+
"metre",
|
|
47
|
+
"kilometre",
|
|
48
|
+
"foot",
|
|
49
|
+
"us-survey-foot",
|
|
50
|
+
"mile",
|
|
51
|
+
"nautical-mile",
|
|
52
|
+
"degree",
|
|
53
|
+
"radian",
|
|
54
|
+
]);
|
|
55
|
+
const NATIVE_TEXT_DIALECTS = new Set(["geoservices-sql92", "cql2-text", "odata-4.0", "duckdb-sql"]);
|
|
56
|
+
const NATIVE_JSON_DIALECTS = new Set(["honua-grpc", "cql2-json"]);
|
|
57
|
+
const RFC3339_DATE_TYPE = { kind: "date" };
|
|
58
|
+
const RFC3339_INSTANT_TYPE = {
|
|
59
|
+
kind: "timestamp",
|
|
60
|
+
unit: "nanosecond",
|
|
61
|
+
timezone: "offset",
|
|
62
|
+
};
|
|
63
|
+
/** @internal Shared bounded JSON boundary for semantic interchange codecs. */
|
|
64
|
+
export function parseBoundedSemanticJson(value) {
|
|
65
|
+
return deepFreeze(parseBoundedJson(value));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse untrusted JSON or an untyped JavaScript value into a deeply immutable
|
|
69
|
+
* semantic query. Syntax, resource bounds, dialect payload shape, and (when
|
|
70
|
+
* supplied) schema field/type compatibility are all checked before return.
|
|
71
|
+
*/
|
|
72
|
+
export function parseSemanticQuery(value, options = {}) {
|
|
73
|
+
try {
|
|
74
|
+
const json = parseBoundedJson(value);
|
|
75
|
+
const query = parseQueryObject(json, "$");
|
|
76
|
+
const schema = options.schema === undefined ? undefined : parseSourceSchemaV2(options.schema);
|
|
77
|
+
const protocol = parseProtocolOption(options.protocol);
|
|
78
|
+
validateParsedQuery(query, schema, protocol);
|
|
79
|
+
return deepFreeze(query);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof HonuaQueryPlanningError)
|
|
83
|
+
throw error;
|
|
84
|
+
throw invalid("$", errorMessage(error));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Validate and freeze a typed query while preserving its generic relationship. */
|
|
88
|
+
export function defineSemanticQuery(query, options = {}) {
|
|
89
|
+
return parseSemanticQuery(query, options);
|
|
90
|
+
}
|
|
91
|
+
/** Create immutable, schema-typed AST nodes without importing a protocol compiler. */
|
|
92
|
+
export function createSemanticQueryBuilder() {
|
|
93
|
+
const property = (name) => node({ kind: "property", name });
|
|
94
|
+
const literal = (value) => node({ kind: "literal", value });
|
|
95
|
+
const asLiteral = (value) => isLiteralNode(value) ? node(value) : literal(value);
|
|
96
|
+
return {
|
|
97
|
+
property,
|
|
98
|
+
literal,
|
|
99
|
+
comparison(operator, left, right) {
|
|
100
|
+
return node({ kind: "comparison", operator, left, right: asLiteral(right) });
|
|
101
|
+
},
|
|
102
|
+
inList(operand, values) {
|
|
103
|
+
return node({ kind: "list", operator: "in", operand, values: values.map(asLiteral) });
|
|
104
|
+
},
|
|
105
|
+
between(operand, lower, upper) {
|
|
106
|
+
return node({
|
|
107
|
+
kind: "range",
|
|
108
|
+
operator: "between",
|
|
109
|
+
operand,
|
|
110
|
+
lower: asLiteral(lower),
|
|
111
|
+
upper: asLiteral(upper),
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
isNull(operand, operator = "is-null") {
|
|
115
|
+
return node({ kind: "null", operator, operand });
|
|
116
|
+
},
|
|
117
|
+
like(operand, pattern, options = {}) {
|
|
118
|
+
return node({
|
|
119
|
+
kind: "pattern",
|
|
120
|
+
operator: "like",
|
|
121
|
+
operand,
|
|
122
|
+
pattern,
|
|
123
|
+
...(options.caseSensitive === undefined ? {} : { caseSensitive: options.caseSensitive }),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
and(...args) {
|
|
127
|
+
return node({ kind: "boolean", operator: "and", args });
|
|
128
|
+
},
|
|
129
|
+
or(...args) {
|
|
130
|
+
return node({ kind: "boolean", operator: "or", args });
|
|
131
|
+
},
|
|
132
|
+
not(arg) {
|
|
133
|
+
return node({ kind: "not", arg });
|
|
134
|
+
},
|
|
135
|
+
temporal(operator, operand, value) {
|
|
136
|
+
return node({ kind: "temporal", operator, operand, value });
|
|
137
|
+
},
|
|
138
|
+
native(dialect, payload) {
|
|
139
|
+
return node({ kind: "native", dialect, payload });
|
|
140
|
+
},
|
|
141
|
+
features(query = {}) {
|
|
142
|
+
return node({ kind: "features", ...query });
|
|
143
|
+
},
|
|
144
|
+
aggregate(query) {
|
|
145
|
+
return node({ kind: "aggregate", ...query });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Build a validated spatial node without weakening its executable operand. */
|
|
150
|
+
export function defineSpatialNode(value) {
|
|
151
|
+
const query = parseSemanticQuery({ kind: "features", filter: value });
|
|
152
|
+
return query.filter;
|
|
153
|
+
}
|
|
154
|
+
export function temporalLiteral(valueType, value) {
|
|
155
|
+
return node({ kind: "temporal-literal", valueType, value });
|
|
156
|
+
}
|
|
157
|
+
function parseQueryObject(value, path) {
|
|
158
|
+
const object = expectObject(value, path);
|
|
159
|
+
const kind = expectEnum(object.kind, `${path}.kind`, ["features", "aggregate"]);
|
|
160
|
+
const baseKeys = ["kind", "filter", "sort", "page", "outputCrs"];
|
|
161
|
+
const filter = object.filter === undefined ? undefined : parseFilter(object.filter, `${path}.filter`, 0);
|
|
162
|
+
const sort = object.sort === undefined ? undefined : parseSort(object.sort, `${path}.sort`);
|
|
163
|
+
const page = object.page === undefined ? undefined : parsePage(object.page, `${path}.page`);
|
|
164
|
+
const outputCrs = object.outputCrs === undefined ? undefined : parseOutputCrs(object.outputCrs, `${path}.outputCrs`);
|
|
165
|
+
if (kind === "features") {
|
|
166
|
+
assertOnlyKeys(object, [...baseKeys, "select", "geometry"], path);
|
|
167
|
+
const select = object.select === undefined ? undefined : parseStringArray(object.select, `${path}.select`, true);
|
|
168
|
+
const geometry = object.geometry === undefined ? undefined : parseGeometryProjection(object.geometry, `${path}.geometry`);
|
|
169
|
+
return {
|
|
170
|
+
kind,
|
|
171
|
+
...(filter ? { filter } : {}),
|
|
172
|
+
...(sort ? { sort } : {}),
|
|
173
|
+
...(page ? { page } : {}),
|
|
174
|
+
...(outputCrs ? { outputCrs } : {}),
|
|
175
|
+
...(select ? { select } : {}),
|
|
176
|
+
...(geometry ? { geometry } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
assertOnlyKeys(object, [...baseKeys, "groupBy", "metrics"], path);
|
|
180
|
+
const groupBy = parseStringArray(object.groupBy, `${path}.groupBy`, false);
|
|
181
|
+
const metrics = parseMetrics(object.metrics, `${path}.metrics`);
|
|
182
|
+
return {
|
|
183
|
+
kind,
|
|
184
|
+
...(filter ? { filter } : {}),
|
|
185
|
+
...(sort ? { sort } : {}),
|
|
186
|
+
...(page ? { page } : {}),
|
|
187
|
+
...(outputCrs ? { outputCrs } : {}),
|
|
188
|
+
groupBy,
|
|
189
|
+
metrics,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function parseFilter(value, path, depth) {
|
|
193
|
+
if (depth > MAX_SEMANTIC_QUERY_DEPTH)
|
|
194
|
+
throw invalid(path, "exceeds the semantic filter depth bound");
|
|
195
|
+
const object = expectObject(value, path);
|
|
196
|
+
const kind = expectString(object.kind, `${path}.kind`);
|
|
197
|
+
switch (kind) {
|
|
198
|
+
case "comparison": {
|
|
199
|
+
assertOnlyKeys(object, ["kind", "operator", "left", "right"], path);
|
|
200
|
+
const operator = expectEnum(object.operator, `${path}.operator`, ["eq", "ne", "lt", "lte", "gt", "gte"]);
|
|
201
|
+
return {
|
|
202
|
+
kind,
|
|
203
|
+
operator,
|
|
204
|
+
left: parseProperty(object.left, `${path}.left`),
|
|
205
|
+
right: parseLiteral(object.right, `${path}.right`),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
case "list": {
|
|
209
|
+
assertOnlyKeys(object, ["kind", "operator", "operand", "values"], path);
|
|
210
|
+
expectExact(object.operator, "in", `${path}.operator`);
|
|
211
|
+
const values = expectArray(object.values, `${path}.values`);
|
|
212
|
+
if (values.length === 0)
|
|
213
|
+
throw invalid(`${path}.values`, "must contain at least one literal");
|
|
214
|
+
return {
|
|
215
|
+
kind,
|
|
216
|
+
operator: "in",
|
|
217
|
+
operand: parseProperty(object.operand, `${path}.operand`),
|
|
218
|
+
values: values.map((entry, index) => parseLiteral(entry, `${path}.values[${index}]`)),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "range": {
|
|
222
|
+
assertOnlyKeys(object, ["kind", "operator", "operand", "lower", "upper"], path);
|
|
223
|
+
expectExact(object.operator, "between", `${path}.operator`);
|
|
224
|
+
return {
|
|
225
|
+
kind,
|
|
226
|
+
operator: "between",
|
|
227
|
+
operand: parseProperty(object.operand, `${path}.operand`),
|
|
228
|
+
lower: parseLiteral(object.lower, `${path}.lower`),
|
|
229
|
+
upper: parseLiteral(object.upper, `${path}.upper`),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "null": {
|
|
233
|
+
assertOnlyKeys(object, ["kind", "operator", "operand"], path);
|
|
234
|
+
return {
|
|
235
|
+
kind,
|
|
236
|
+
operator: expectEnum(object.operator, `${path}.operator`, ["is-null", "is-not-null"]),
|
|
237
|
+
operand: parseProperty(object.operand, `${path}.operand`),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
case "pattern": {
|
|
241
|
+
assertOnlyKeys(object, ["kind", "operator", "operand", "pattern", "caseSensitive"], path);
|
|
242
|
+
expectExact(object.operator, "like", `${path}.operator`);
|
|
243
|
+
return {
|
|
244
|
+
kind,
|
|
245
|
+
operator: "like",
|
|
246
|
+
operand: parseProperty(object.operand, `${path}.operand`),
|
|
247
|
+
pattern: expectBoundedText(object.pattern, `${path}.pattern`),
|
|
248
|
+
caseSensitive: object.caseSensitive === undefined ? true : expectBoolean(object.caseSensitive, `${path}.caseSensitive`),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case "boolean": {
|
|
252
|
+
assertOnlyKeys(object, ["kind", "operator", "args"], path);
|
|
253
|
+
const args = expectArray(object.args, `${path}.args`);
|
|
254
|
+
if (args.length === 0)
|
|
255
|
+
throw invalid(`${path}.args`, "must contain at least one filter");
|
|
256
|
+
return {
|
|
257
|
+
kind,
|
|
258
|
+
operator: expectEnum(object.operator, `${path}.operator`, ["and", "or"]),
|
|
259
|
+
args: args.map((entry, index) => parseFilter(entry, `${path}.args[${index}]`, depth + 1)),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
case "not":
|
|
263
|
+
assertOnlyKeys(object, ["kind", "arg"], path);
|
|
264
|
+
return { kind, arg: parseFilter(object.arg, `${path}.arg`, depth + 1) };
|
|
265
|
+
case "spatial":
|
|
266
|
+
return parseSpatialFilter(object, path);
|
|
267
|
+
case "temporal": {
|
|
268
|
+
assertOnlyKeys(object, ["kind", "operator", "operand", "value"], path);
|
|
269
|
+
return {
|
|
270
|
+
kind,
|
|
271
|
+
operator: expectEnum(object.operator, `${path}.operator`, [
|
|
272
|
+
"before",
|
|
273
|
+
"after",
|
|
274
|
+
"during",
|
|
275
|
+
"time-intersects",
|
|
276
|
+
]),
|
|
277
|
+
operand: parseProperty(object.operand, `${path}.operand`),
|
|
278
|
+
value: parseTemporalLiteral(object.value, `${path}.value`),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
case "native":
|
|
282
|
+
if (depth !== 0)
|
|
283
|
+
throw invalid(path, "native filters must be the complete top-level filter");
|
|
284
|
+
return parseNativeFilter(object, path);
|
|
285
|
+
default:
|
|
286
|
+
throw invalid(`${path}.kind`, `has unsupported value ${JSON.stringify(kind)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function parseProperty(value, path) {
|
|
290
|
+
const object = expectObject(value, path);
|
|
291
|
+
assertOnlyKeys(object, ["kind", "name"], path);
|
|
292
|
+
expectExact(object.kind, "property", `${path}.kind`);
|
|
293
|
+
const name = expectNonblankBoundedText(object.name, `${path}.name`);
|
|
294
|
+
return { kind: "property", name };
|
|
295
|
+
}
|
|
296
|
+
function parseLiteral(value, path) {
|
|
297
|
+
const object = expectObject(value, path);
|
|
298
|
+
assertOnlyKeys(object, ["kind", "value"], path);
|
|
299
|
+
expectExact(object.kind, "literal", `${path}.kind`);
|
|
300
|
+
if (!Object.hasOwn(object, "value"))
|
|
301
|
+
throw invalid(`${path}.value`, "is required");
|
|
302
|
+
return { kind: "literal", value: object.value };
|
|
303
|
+
}
|
|
304
|
+
function parseSpatialFilter(object, path) {
|
|
305
|
+
const operator = expectBoundedText(object.operator, `${path}.operator`);
|
|
306
|
+
const property = object.property === undefined ? undefined : parseProperty(object.property, `${path}.property`);
|
|
307
|
+
if (operator === "bbox-intersects") {
|
|
308
|
+
assertOnlyKeys(object, ["kind", "operator", "property", "bbox"], path);
|
|
309
|
+
return {
|
|
310
|
+
kind: "spatial",
|
|
311
|
+
operator,
|
|
312
|
+
...(property ? { property } : {}),
|
|
313
|
+
bbox: parseExecutableBbox(object.bbox, `${path}.bbox`),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (operator === "within-distance" || operator === "beyond-distance") {
|
|
317
|
+
assertOnlyKeys(object, ["kind", "operator", "property", "geometry", "distance"], path);
|
|
318
|
+
return {
|
|
319
|
+
kind: "spatial",
|
|
320
|
+
operator,
|
|
321
|
+
...(property ? { property } : {}),
|
|
322
|
+
geometry: parseExecutableGeometry(object.geometry, `${path}.geometry`),
|
|
323
|
+
distance: parseDistance(object.distance, `${path}.distance`),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (!TOPOLOGICAL_OPERATORS.has(operator))
|
|
327
|
+
throw invalid(`${path}.operator`, `has unsupported value ${operator}`);
|
|
328
|
+
assertOnlyKeys(object, ["kind", "operator", "property", "geometry"], path);
|
|
329
|
+
return {
|
|
330
|
+
kind: "spatial",
|
|
331
|
+
operator,
|
|
332
|
+
...(property ? { property } : {}),
|
|
333
|
+
geometry: parseExecutableGeometry(object.geometry, `${path}.geometry`),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function parseExecutableGeometry(value, path) {
|
|
337
|
+
const object = expectObject(value, path);
|
|
338
|
+
assertOnlyKeys(object, ["state", "geometry", "crs", "layout"], path);
|
|
339
|
+
expectExact(object.state, "present", `${path}.state`);
|
|
340
|
+
const layout = expectEnum(object.layout, `${path}.layout`, ["xy", "xyz", "xym", "xyzm"]);
|
|
341
|
+
const crs = parseExecutableCrsBinding(object.crs, `${path}.crs`);
|
|
342
|
+
const geometry = parseCanonicalGeometry(object.geometry, `${path}.geometry`, layout, 0);
|
|
343
|
+
return { state: "present", geometry, crs: crs, layout };
|
|
344
|
+
}
|
|
345
|
+
function parseExecutableBbox(value, path) {
|
|
346
|
+
const object = expectObject(value, path);
|
|
347
|
+
assertOnlyKeys(object, ["box", "crs"], path);
|
|
348
|
+
const box = expectObject(object.box, `${path}.box`);
|
|
349
|
+
assertOnlyKeys(box, ["layout", "bounds"], `${path}.box`);
|
|
350
|
+
const layout = expectEnum(box.layout, `${path}.box.layout`, ["xy", "xyz"]);
|
|
351
|
+
const bounds = expectArray(box.bounds, `${path}.box.bounds`);
|
|
352
|
+
const expectedLength = layout === "xy" ? 4 : 6;
|
|
353
|
+
if (bounds.length !== expectedLength)
|
|
354
|
+
throw invalid(`${path}.box.bounds`, `must contain ${expectedLength} ordinates`);
|
|
355
|
+
const numbers = bounds.map((entry, index) => expectFiniteNumber(entry, `${path}.box.bounds[${index}]`));
|
|
356
|
+
const dimensions = expectedLength / 2;
|
|
357
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
358
|
+
if (numbers[index] > numbers[index + dimensions]) {
|
|
359
|
+
throw invalid(`${path}.box.bounds`, "minimum ordinates must not exceed maximum ordinates");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
box: { layout, bounds: numbers },
|
|
364
|
+
crs: parseExecutableCrsBinding(object.crs, `${path}.crs`),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function parseExecutableCrsBinding(value, path) {
|
|
368
|
+
try {
|
|
369
|
+
return validateExecutableCrsBinding(value, path);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (error instanceof HonuaQueryPlanningError)
|
|
373
|
+
throw error;
|
|
374
|
+
throw invalid(path, error instanceof TypeError ? error.message : "must be a valid executable CRS binding");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function parseCanonicalGeometry(value, path, layout, depth) {
|
|
378
|
+
if (depth > 8)
|
|
379
|
+
throw invalid(path, "exceeds the supported geometry collection depth");
|
|
380
|
+
const object = expectObject(value, path);
|
|
381
|
+
const type = expectEnum(object.type, `${path}.type`, [
|
|
382
|
+
"Point",
|
|
383
|
+
"MultiPoint",
|
|
384
|
+
"LineString",
|
|
385
|
+
"MultiLineString",
|
|
386
|
+
"Polygon",
|
|
387
|
+
"MultiPolygon",
|
|
388
|
+
"GeometryCollection",
|
|
389
|
+
]);
|
|
390
|
+
if (type === "GeometryCollection") {
|
|
391
|
+
assertOnlyKeys(object, ["type", "geometries"], path);
|
|
392
|
+
const geometries = expectArray(object.geometries, `${path}.geometries`);
|
|
393
|
+
if (geometries.length === 0)
|
|
394
|
+
throw invalid(`${path}.geometries`, "must not be empty");
|
|
395
|
+
return {
|
|
396
|
+
type,
|
|
397
|
+
geometries: geometries.map((entry, index) => parseCanonicalGeometry(entry, `${path}.geometries[${index}]`, layout, depth + 1)),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
assertOnlyKeys(object, ["type", "coordinates"], path);
|
|
401
|
+
const coordinateDepth = type === "Point"
|
|
402
|
+
? 1
|
|
403
|
+
: type === "MultiPoint" || type === "LineString"
|
|
404
|
+
? 2
|
|
405
|
+
: type === "MultiLineString" || type === "Polygon"
|
|
406
|
+
? 3
|
|
407
|
+
: 4;
|
|
408
|
+
const coordinates = parseCoordinates(object.coordinates, `${path}.coordinates`, coordinateDepth, layout);
|
|
409
|
+
validateGeometryCardinality(type, coordinates, `${path}.coordinates`);
|
|
410
|
+
return { type, coordinates };
|
|
411
|
+
}
|
|
412
|
+
function validateGeometryCardinality(type, coordinates, path) {
|
|
413
|
+
if (type === "LineString") {
|
|
414
|
+
assertLine(coordinates, path);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (type === "MultiLineString") {
|
|
418
|
+
coordinates.forEach((line, index) => assertLine(line, `${path}[${index}]`));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (type === "Polygon") {
|
|
422
|
+
coordinates.forEach((ring, index) => assertRing(ring, `${path}[${index}]`));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (type === "MultiPolygon") {
|
|
426
|
+
coordinates.forEach((polygon, polygonIndex) => polygon.forEach((ring, ringIndex) => assertRing(ring, `${path}[${polygonIndex}][${ringIndex}]`)));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function assertLine(line, path) {
|
|
430
|
+
if (line.length < 2)
|
|
431
|
+
throw invalid(path, "must contain at least two positions");
|
|
432
|
+
}
|
|
433
|
+
function assertRing(ring, path) {
|
|
434
|
+
if (ring.length < 4)
|
|
435
|
+
throw invalid(path, "must contain at least four positions");
|
|
436
|
+
if (JSON.stringify(ring[0]) !== JSON.stringify(ring.at(-1)))
|
|
437
|
+
throw invalid(path, "must be closed");
|
|
438
|
+
}
|
|
439
|
+
function parseCoordinates(value, path, depth, layout) {
|
|
440
|
+
const array = expectArray(value, path);
|
|
441
|
+
if (array.length === 0)
|
|
442
|
+
throw invalid(path, "must not be empty");
|
|
443
|
+
if (depth === 1) {
|
|
444
|
+
const expected = layout === "xy" ? 2 : layout === "xyzm" ? 4 : 3;
|
|
445
|
+
if (array.length !== expected)
|
|
446
|
+
throw invalid(path, `must contain ${expected} ordinates for ${layout}`);
|
|
447
|
+
return array.map((entry, index) => expectFiniteNumber(entry, `${path}[${index}]`));
|
|
448
|
+
}
|
|
449
|
+
return array.map((entry, index) => parseCoordinates(entry, `${path}[${index}]`, depth - 1, layout));
|
|
450
|
+
}
|
|
451
|
+
function parseDistance(value, path) {
|
|
452
|
+
const object = expectObject(value, path);
|
|
453
|
+
assertOnlyKeys(object, ["value", "unit", "mode"], path);
|
|
454
|
+
const distance = expectFiniteNumber(object.value, `${path}.value`);
|
|
455
|
+
if (distance <= 0)
|
|
456
|
+
throw invalid(`${path}.value`, "must be greater than zero");
|
|
457
|
+
const unit = expectBoundedText(object.unit, `${path}.unit`);
|
|
458
|
+
if (!DISTANCE_UNITS.has(unit))
|
|
459
|
+
throw invalid(`${path}.unit`, `has unsupported value ${unit}`);
|
|
460
|
+
return {
|
|
461
|
+
value: distance,
|
|
462
|
+
unit: unit,
|
|
463
|
+
mode: expectEnum(object.mode, `${path}.mode`, ["planar", "geodesic"]),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function parseTemporalLiteral(value, path) {
|
|
467
|
+
const object = expectObject(value, path);
|
|
468
|
+
assertOnlyKeys(object, ["kind", "valueType", "value"], path);
|
|
469
|
+
expectExact(object.kind, "temporal-literal", `${path}.kind`);
|
|
470
|
+
const valueType = expectEnum(object.valueType, `${path}.valueType`, ["date", "instant", "interval"]);
|
|
471
|
+
if (valueType === "interval") {
|
|
472
|
+
const interval = expectArray(object.value, `${path}.value`);
|
|
473
|
+
if (interval.length !== 2)
|
|
474
|
+
throw invalid(`${path}.value`, "must contain exactly two temporal endpoints");
|
|
475
|
+
const start = expectBoundedText(interval[0], `${path}.value[0]`);
|
|
476
|
+
const end = expectBoundedText(interval[1], `${path}.value[1]`);
|
|
477
|
+
const startKind = temporalEndpointKind(start);
|
|
478
|
+
const endKind = temporalEndpointKind(end);
|
|
479
|
+
if (!startKind)
|
|
480
|
+
throw invalid(`${path}.value[0]`, "must be an RFC 3339 full-date or instant");
|
|
481
|
+
if (!endKind)
|
|
482
|
+
throw invalid(`${path}.value[1]`, "must be an RFC 3339 full-date or instant");
|
|
483
|
+
if (startKind !== endKind)
|
|
484
|
+
throw invalid(`${path}.value`, "endpoints must use the same temporal representation");
|
|
485
|
+
const comparison = compareLogicalDomainValues(start, end, startKind === "date" ? RFC3339_DATE_TYPE : RFC3339_INSTANT_TYPE);
|
|
486
|
+
if (comparison === undefined)
|
|
487
|
+
throw invalid(`${path}.value`, "endpoints cannot be ordered deterministically");
|
|
488
|
+
if (comparison > 0)
|
|
489
|
+
throw invalid(`${path}.value`, "start must not be after end");
|
|
490
|
+
return { kind: "temporal-literal", valueType, value: [start, end] };
|
|
491
|
+
}
|
|
492
|
+
const text = expectBoundedText(object.value, `${path}.value`);
|
|
493
|
+
if (valueType === "date")
|
|
494
|
+
assertIsoDate(text, `${path}.value`);
|
|
495
|
+
else
|
|
496
|
+
assertIsoInstant(text, `${path}.value`);
|
|
497
|
+
return { kind: "temporal-literal", valueType, value: text };
|
|
498
|
+
}
|
|
499
|
+
function parseNativeFilter(object, path) {
|
|
500
|
+
assertOnlyKeys(object, ["kind", "dialect", "payload"], path);
|
|
501
|
+
const dialect = expectNonblankBoundedText(object.dialect, `${path}.dialect`);
|
|
502
|
+
if (!isBuiltInDialect(dialect) && !EXTENSION_ID_PATTERN.test(dialect)) {
|
|
503
|
+
throw invalid(`${path}.dialect`, "must be a built-in or namespaced extension dialect");
|
|
504
|
+
}
|
|
505
|
+
const payload = expectObject(object.payload, `${path}.payload`);
|
|
506
|
+
const format = expectEnum(payload.format, `${path}.payload.format`, ["text", "xml", "json"]);
|
|
507
|
+
if (format === "json") {
|
|
508
|
+
assertOnlyKeys(payload, ["format", "value"], `${path}.payload`);
|
|
509
|
+
if (!Object.hasOwn(payload, "value"))
|
|
510
|
+
throw invalid(`${path}.payload.value`, "is required");
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
assertOnlyKeys(payload, ["format", "text"], `${path}.payload`);
|
|
514
|
+
expectNonblankBoundedText(payload.text, `${path}.payload.text`);
|
|
515
|
+
}
|
|
516
|
+
if (NATIVE_JSON_DIALECTS.has(dialect) && format !== "json") {
|
|
517
|
+
throw invalid(`${path}.payload.format`, `${dialect} requires json`);
|
|
518
|
+
}
|
|
519
|
+
if (dialect === "fes-2.0" && format !== "xml")
|
|
520
|
+
throw invalid(`${path}.payload.format`, "fes-2.0 requires xml");
|
|
521
|
+
if (NATIVE_TEXT_DIALECTS.has(dialect) && format !== "text") {
|
|
522
|
+
throw invalid(`${path}.payload.format`, `${dialect} requires text`);
|
|
523
|
+
}
|
|
524
|
+
return { kind: "native", dialect, payload };
|
|
525
|
+
}
|
|
526
|
+
function parseSort(value, path) {
|
|
527
|
+
const array = expectArray(value, path);
|
|
528
|
+
return array.map((entry, index) => {
|
|
529
|
+
const itemPath = `${path}[${index}]`;
|
|
530
|
+
const object = expectObject(entry, itemPath);
|
|
531
|
+
assertOnlyKeys(object, ["field", "direction", "nulls"], itemPath);
|
|
532
|
+
return {
|
|
533
|
+
field: expectNonblankBoundedText(object.field, `${itemPath}.field`),
|
|
534
|
+
direction: expectEnum(object.direction, `${itemPath}.direction`, ["asc", "desc"]),
|
|
535
|
+
nulls: object.nulls === undefined
|
|
536
|
+
? "native"
|
|
537
|
+
: expectEnum(object.nulls, `${itemPath}.nulls`, ["first", "last", "native"]),
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function parsePage(value, path) {
|
|
542
|
+
const object = expectObject(value, path);
|
|
543
|
+
const kind = expectEnum(object.kind, `${path}.kind`, ["first", "offset"]);
|
|
544
|
+
if (kind === "first") {
|
|
545
|
+
assertOnlyKeys(object, ["kind", "limit"], path);
|
|
546
|
+
return {
|
|
547
|
+
kind,
|
|
548
|
+
...(object.limit === undefined ? {} : { limit: expectPositiveSafeInteger(object.limit, `${path}.limit`) }),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
assertOnlyKeys(object, ["kind", "offset", "limit"], path);
|
|
552
|
+
return {
|
|
553
|
+
kind,
|
|
554
|
+
offset: expectNonnegativeSafeInteger(object.offset, `${path}.offset`),
|
|
555
|
+
...(object.limit === undefined ? {} : { limit: expectPositiveSafeInteger(object.limit, `${path}.limit`) }),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function parseOutputCrs(value, path) {
|
|
559
|
+
let definition;
|
|
560
|
+
try {
|
|
561
|
+
definition = validateSourceCrsDefinition(value);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
throw invalid(path, errorMessage(error));
|
|
565
|
+
}
|
|
566
|
+
if (definition.kind === "unknown")
|
|
567
|
+
throw invalid(path, "must be resolved for execution");
|
|
568
|
+
if (definition.kind === "wkt" && definition.validation !== "engine") {
|
|
569
|
+
throw invalid(path, "WKT must be engine-validated for execution");
|
|
570
|
+
}
|
|
571
|
+
return definition;
|
|
572
|
+
}
|
|
573
|
+
function parseGeometryProjection(value, path) {
|
|
574
|
+
if (value === "include" || value === "omit")
|
|
575
|
+
return value;
|
|
576
|
+
const object = expectObject(value, path);
|
|
577
|
+
assertOnlyKeys(object, ["field"], path);
|
|
578
|
+
return { field: expectNonblankBoundedText(object.field, `${path}.field`) };
|
|
579
|
+
}
|
|
580
|
+
function parseMetrics(value, path) {
|
|
581
|
+
const array = expectArray(value, path);
|
|
582
|
+
if (array.length === 0)
|
|
583
|
+
throw invalid(path, "must contain at least one metric");
|
|
584
|
+
return array.map((entry, index) => {
|
|
585
|
+
const itemPath = `${path}[${index}]`;
|
|
586
|
+
const object = expectObject(entry, itemPath);
|
|
587
|
+
const fn = expectEnum(object.fn, `${itemPath}.fn`, [
|
|
588
|
+
"count",
|
|
589
|
+
"sum",
|
|
590
|
+
"avg",
|
|
591
|
+
"min",
|
|
592
|
+
"max",
|
|
593
|
+
"stddev",
|
|
594
|
+
"variance",
|
|
595
|
+
]);
|
|
596
|
+
assertOnlyKeys(object, ["fn", "field", "as"], itemPath);
|
|
597
|
+
const alias = expectNonblankBoundedText(object.as, `${itemPath}.as`);
|
|
598
|
+
if (fn === "count") {
|
|
599
|
+
return {
|
|
600
|
+
fn,
|
|
601
|
+
...(object.field === undefined ? {} : { field: expectNonblankBoundedText(object.field, `${itemPath}.field`) }),
|
|
602
|
+
as: alias,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return { fn, field: expectNonblankBoundedText(object.field, `${itemPath}.field`), as: alias };
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function validateParsedQuery(query, schema, protocol) {
|
|
609
|
+
if (query.filter?.kind === "native")
|
|
610
|
+
validateNativeDialect(query.filter.dialect, protocol, "$.filter.dialect");
|
|
611
|
+
else if (query.filter && schema)
|
|
612
|
+
validateFilter(query.filter, schema, "$.filter");
|
|
613
|
+
if (!schema)
|
|
614
|
+
return;
|
|
615
|
+
const spatiality = schemaSpatiality(schema);
|
|
616
|
+
if (query.sort) {
|
|
617
|
+
assertUnique(query.sort.map((sort) => sort.field), "$.sort", "sort field");
|
|
618
|
+
query.sort.forEach((sort, index) => {
|
|
619
|
+
const field = schemaField(schema, sort.field, `$.sort[${index}].field`);
|
|
620
|
+
assertTypeKind(field, ORDERED_TYPE_KINDS, `$.sort[${index}].field`, "orderable");
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (query.outputCrs && spatiality === "non-spatial") {
|
|
624
|
+
throw invalid("$.outputCrs", "is not valid for a non-spatial schema");
|
|
625
|
+
}
|
|
626
|
+
if (query.kind === "features") {
|
|
627
|
+
if (query.select) {
|
|
628
|
+
assertUnique(query.select, "$.select", "projection field");
|
|
629
|
+
query.select.forEach((field, index) => schemaField(schema, field, `$.select[${index}]`));
|
|
630
|
+
}
|
|
631
|
+
validateGeometryProjection(query.geometry, query.select, schema, spatiality);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
assertUnique(query.groupBy, "$.groupBy", "group field");
|
|
635
|
+
query.groupBy.forEach((name, index) => {
|
|
636
|
+
const field = schemaField(schema, name, `$.groupBy[${index}]`);
|
|
637
|
+
assertTypeKind(field, SCALAR_TYPE_KINDS, `$.groupBy[${index}]`, "scalar/groupable");
|
|
638
|
+
});
|
|
639
|
+
const aliases = query.metrics.map((metric) => metric.as);
|
|
640
|
+
assertUnique(aliases, "$.metrics", "metric alias");
|
|
641
|
+
const groupNames = new Set(query.groupBy);
|
|
642
|
+
query.metrics.forEach((metric, index) => {
|
|
643
|
+
if (groupNames.has(metric.as))
|
|
644
|
+
throw invalid(`$.metrics[${index}].as`, "must not collide with a group field");
|
|
645
|
+
if (metric.fn === "count" && metric.field === undefined)
|
|
646
|
+
return;
|
|
647
|
+
const field = schemaField(schema, metric.field, `$.metrics[${index}].field`);
|
|
648
|
+
const expected = metric.fn === "sum" || metric.fn === "avg" || metric.fn === "stddev" || metric.fn === "variance"
|
|
649
|
+
? NUMERIC_TYPE_KINDS
|
|
650
|
+
: metric.fn === "count"
|
|
651
|
+
? undefined
|
|
652
|
+
: ORDERED_TYPE_KINDS;
|
|
653
|
+
if (expected)
|
|
654
|
+
assertTypeKind(field, expected, `$.metrics[${index}].field`, metric.fn === "count" ? "field" : `${metric.fn}-compatible`);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function parseProtocolOption(value) {
|
|
658
|
+
if (value === undefined)
|
|
659
|
+
return undefined;
|
|
660
|
+
if (typeof value !== "string")
|
|
661
|
+
throw invalid("options.protocol", "must be a string");
|
|
662
|
+
if (TEXT_ENCODER.encode(value).byteLength > MAX_SEMANTIC_QUERY_TEXT_BYTES) {
|
|
663
|
+
throw invalid("options.protocol", "text is too large");
|
|
664
|
+
}
|
|
665
|
+
if (value.trim() === "")
|
|
666
|
+
throw invalid("options.protocol", "must be a non-empty string");
|
|
667
|
+
if (!PROTOCOLS.includes(value) && !EXTENSION_ID_PATTERN.test(value)) {
|
|
668
|
+
throw invalid("options.protocol", "must be a built-in or namespaced extension protocol");
|
|
669
|
+
}
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
function validateFilter(filter, schema, path) {
|
|
673
|
+
switch (filter.kind) {
|
|
674
|
+
case "comparison": {
|
|
675
|
+
const field = schemaField(schema, filter.left.name, `${path}.left.name`);
|
|
676
|
+
const allowed = filter.operator === "eq" || filter.operator === "ne" ? SCALAR_TYPE_KINDS : ORDERED_TYPE_KINDS;
|
|
677
|
+
assertTypeKind(field, allowed, `${path}.left.name`, filter.operator === "eq" || filter.operator === "ne" ? "scalar" : "orderable");
|
|
678
|
+
validateLiteralForField(filter.right.value, field, `${path}.right.value`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
case "list": {
|
|
682
|
+
const field = schemaField(schema, filter.operand.name, `${path}.operand.name`);
|
|
683
|
+
assertTypeKind(field, SCALAR_TYPE_KINDS, `${path}.operand.name`, "scalar");
|
|
684
|
+
filter.values.forEach((literal, index) => validateLiteralForField(literal.value, field, `${path}.values[${index}].value`));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
case "range": {
|
|
688
|
+
const field = schemaField(schema, filter.operand.name, `${path}.operand.name`);
|
|
689
|
+
assertTypeKind(field, ORDERED_TYPE_KINDS, `${path}.operand.name`, "orderable");
|
|
690
|
+
validateLiteralForField(filter.lower.value, field, `${path}.lower.value`);
|
|
691
|
+
validateLiteralForField(filter.upper.value, field, `${path}.upper.value`);
|
|
692
|
+
const comparison = compareLiteral(filter.lower.value, filter.upper.value, field.type);
|
|
693
|
+
if (comparison === undefined)
|
|
694
|
+
throw invalid(path, "range bounds cannot be ordered deterministically");
|
|
695
|
+
if (comparison > 0) {
|
|
696
|
+
throw invalid(path, "range lower bound must not exceed its upper bound");
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
case "null":
|
|
701
|
+
schemaField(schema, filter.operand.name, `${path}.operand.name`);
|
|
702
|
+
return;
|
|
703
|
+
case "pattern": {
|
|
704
|
+
const field = schemaField(schema, filter.operand.name, `${path}.operand.name`);
|
|
705
|
+
if (field.type.kind !== "string")
|
|
706
|
+
throw invalid(`${path}.operand.name`, "must identify a string field");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
case "boolean":
|
|
710
|
+
filter.args.forEach((arg, index) => validateFilter(arg, schema, `${path}.args[${index}]`));
|
|
711
|
+
return;
|
|
712
|
+
case "not":
|
|
713
|
+
validateFilter(filter.arg, schema, `${path}.arg`);
|
|
714
|
+
return;
|
|
715
|
+
case "spatial":
|
|
716
|
+
validateSpatialFilter(filter, schema, path);
|
|
717
|
+
return;
|
|
718
|
+
case "temporal":
|
|
719
|
+
validateTemporalFilter(filter, schema, path);
|
|
720
|
+
return;
|
|
721
|
+
case "native":
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function validateSpatialFilter(filter, schema, path) {
|
|
726
|
+
if (schema.geometry.state === "none")
|
|
727
|
+
throw invalid(path, "cannot target a non-spatial schema");
|
|
728
|
+
const property = filter.property;
|
|
729
|
+
let fieldName = property?.name;
|
|
730
|
+
if (!fieldName && schema.geometry.state === "known" && schema.geometry.primaryField.state === "known") {
|
|
731
|
+
fieldName = schema.geometry.primaryField.field;
|
|
732
|
+
}
|
|
733
|
+
if (!fieldName)
|
|
734
|
+
throw invalid(`${path}.property`, "is required when the schema has no known primary geometry");
|
|
735
|
+
const field = schemaField(schema, fieldName, `${path}.property.name`);
|
|
736
|
+
if (field.type.kind !== "geometry")
|
|
737
|
+
throw invalid(`${path}.property.name`, "must identify a geometry field");
|
|
738
|
+
if (schema.geometry.state === "known" && !schema.geometry.fields.some((candidate) => candidate.field === fieldName)) {
|
|
739
|
+
throw invalid(`${path}.property.name`, "is not declared by the source geometry schema");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function validateTemporalFilter(filter, schema, path) {
|
|
743
|
+
const operand = filter.operand;
|
|
744
|
+
const field = schemaField(schema, operand.name, `${path}.operand.name`);
|
|
745
|
+
const temporal = schema.temporal;
|
|
746
|
+
const declared = temporal.state === "instant"
|
|
747
|
+
? [temporal.field]
|
|
748
|
+
: temporal.state === "interval"
|
|
749
|
+
? [temporal.startField, temporal.endField]
|
|
750
|
+
: temporal.state === "mixed"
|
|
751
|
+
? temporal.fields
|
|
752
|
+
: [];
|
|
753
|
+
const roleEligible = field.roles.some((role) => role === "time-instant" || role === "time-start" || role === "time-end");
|
|
754
|
+
if (!declared.includes(field.name) && !roleEligible) {
|
|
755
|
+
throw invalid(`${path}.operand.name`, "is not a schema-declared temporal field");
|
|
756
|
+
}
|
|
757
|
+
if (field.type.kind !== "date" && field.type.kind !== "timestamp") {
|
|
758
|
+
throw invalid(`${path}.operand.name`, "must identify a date or timestamp field");
|
|
759
|
+
}
|
|
760
|
+
if (field.type.kind === "timestamp" && (field.type.timezone === "local" || field.type.timezone === "unknown")) {
|
|
761
|
+
throw invalid(`${path}.operand.name`, "must have an unambiguous UTC or offset timestamp timezone");
|
|
762
|
+
}
|
|
763
|
+
const value = filter.value;
|
|
764
|
+
if (field.type.kind === "date" && value.valueType === "instant") {
|
|
765
|
+
throw invalid(`${path}.value.valueType`, "instant is incompatible with a date field");
|
|
766
|
+
}
|
|
767
|
+
if (field.type.kind === "timestamp" && value.valueType === "date") {
|
|
768
|
+
throw invalid(`${path}.value.valueType`, "date is incompatible with a timestamp field");
|
|
769
|
+
}
|
|
770
|
+
if (value.valueType === "interval") {
|
|
771
|
+
const endpointKind = temporalEndpointKind(value.value[0]);
|
|
772
|
+
if (field.type.kind === "date" && endpointKind !== "date") {
|
|
773
|
+
throw invalid(`${path}.value.value`, "date fields require full-date interval endpoints");
|
|
774
|
+
}
|
|
775
|
+
if (field.type.kind === "timestamp" && endpointKind !== "instant") {
|
|
776
|
+
throw invalid(`${path}.value.value`, "timestamp fields require instant interval endpoints");
|
|
777
|
+
}
|
|
778
|
+
value.value.forEach((endpoint, index) => validateLiteralForField(endpoint, field, `${path}.value.value[${index}]`));
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
validateLiteralForField(value.value, field, `${path}.value.value`);
|
|
782
|
+
}
|
|
783
|
+
if ((filter.operator === "during" || filter.operator === "time-intersects") && value.valueType !== "interval") {
|
|
784
|
+
throw invalid(`${path}.value.valueType`, `${filter.operator} requires an interval`);
|
|
785
|
+
}
|
|
786
|
+
if ((filter.operator === "before" || filter.operator === "after") && value.valueType === "interval") {
|
|
787
|
+
throw invalid(`${path}.value.valueType`, `${filter.operator} requires one date or instant`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function validateGeometryProjection(geometry, select, schema, spatiality) {
|
|
791
|
+
if (geometry === undefined)
|
|
792
|
+
return;
|
|
793
|
+
if (geometry === "omit")
|
|
794
|
+
return;
|
|
795
|
+
if (geometry === "include") {
|
|
796
|
+
if (spatiality === "non-spatial")
|
|
797
|
+
throw invalid("$.geometry", "cannot include geometry for a non-spatial schema");
|
|
798
|
+
if (spatiality === "ambiguous-geometry")
|
|
799
|
+
throw invalid("$.geometry", "must name a geometry field for this schema");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const projection = geometry;
|
|
803
|
+
const field = schemaField(schema, projection.field, "$.geometry.field");
|
|
804
|
+
if (field.type.kind !== "geometry")
|
|
805
|
+
throw invalid("$.geometry.field", "must identify a geometry field");
|
|
806
|
+
if (select && !select.includes(projection.field))
|
|
807
|
+
throw invalid("$.geometry.field", "must also appear in select");
|
|
808
|
+
}
|
|
809
|
+
function validateNativeDialect(dialect, protocol, path) {
|
|
810
|
+
if (!protocol)
|
|
811
|
+
return;
|
|
812
|
+
const allowed = dialectsForProtocol(protocol);
|
|
813
|
+
if (allowed === "extension") {
|
|
814
|
+
if (!dialect.startsWith(`${protocol}.`) || !EXTENSION_ID_PATTERN.test(dialect)) {
|
|
815
|
+
throw invalid(path, `does not belong to extension protocol ${protocol}`);
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (!allowed.includes(dialect)) {
|
|
820
|
+
throw invalid(path, `dialect ${dialect} is not valid for protocol ${protocol}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function dialectsForProtocol(protocol) {
|
|
824
|
+
switch (protocol) {
|
|
825
|
+
case "grpc":
|
|
826
|
+
return ["honua-grpc"];
|
|
827
|
+
case "geoservices-feature-service":
|
|
828
|
+
case "geoservices-map-service":
|
|
829
|
+
case "geoservices-image-service":
|
|
830
|
+
return ["geoservices-sql92"];
|
|
831
|
+
case "ogc-features":
|
|
832
|
+
case "ogc-records":
|
|
833
|
+
case "stac":
|
|
834
|
+
return ["cql2-json", "cql2-text"];
|
|
835
|
+
case "wfs":
|
|
836
|
+
return ["fes-2.0"];
|
|
837
|
+
case "odata":
|
|
838
|
+
return ["odata-4.0"];
|
|
839
|
+
case "geoparquet":
|
|
840
|
+
return ["duckdb-sql"];
|
|
841
|
+
default:
|
|
842
|
+
return EXTENSION_ID_PATTERN.test(protocol) ? "extension" : [];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function schemaSpatiality(schema) {
|
|
846
|
+
if (schema.geometry.state === "none")
|
|
847
|
+
return "non-spatial";
|
|
848
|
+
if (schema.geometry.state === "known" && schema.geometry.primaryField.state === "known")
|
|
849
|
+
return "primary-geometry";
|
|
850
|
+
return "ambiguous-geometry";
|
|
851
|
+
}
|
|
852
|
+
function schemaField(schema, name, path) {
|
|
853
|
+
const field = schema.fields.find((candidate) => candidate.name === name);
|
|
854
|
+
if (!field)
|
|
855
|
+
throw invalid(path, `references unknown schema field ${JSON.stringify(name)}`);
|
|
856
|
+
return field;
|
|
857
|
+
}
|
|
858
|
+
function assertTypeKind(field, allowed, path, expected) {
|
|
859
|
+
if (!allowed.has(field.type.kind))
|
|
860
|
+
throw invalid(path, `field ${field.name} is ${field.type.kind}, not ${expected}`);
|
|
861
|
+
}
|
|
862
|
+
function validateLiteralForField(value, field, path) {
|
|
863
|
+
const type = field.type;
|
|
864
|
+
if (!literalMatchesType(value, type))
|
|
865
|
+
throw invalid(path, `is incompatible with ${field.name}:${type.kind}`);
|
|
866
|
+
if (typeof value === "string" &&
|
|
867
|
+
type.kind === "string" &&
|
|
868
|
+
type.maxLength !== undefined &&
|
|
869
|
+
value.length > type.maxLength) {
|
|
870
|
+
throw invalid(path, `exceeds ${field.name}'s maximum length ${type.maxLength}`);
|
|
871
|
+
}
|
|
872
|
+
if (field.domain.state === "coded" && field.domain.openness === "closed") {
|
|
873
|
+
if (!field.domain.values.some((entry) => Object.is(entry.value, value))) {
|
|
874
|
+
throw invalid(path, `is outside ${field.name}'s closed coded domain`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (field.domain.state === "range") {
|
|
878
|
+
if (field.domain.minimum) {
|
|
879
|
+
const comparison = compareLiteral(value, field.domain.minimum.value, type);
|
|
880
|
+
if (comparison === undefined)
|
|
881
|
+
throw invalid(path, `cannot be ordered against ${field.name}'s declared minimum`);
|
|
882
|
+
if (comparison < (field.domain.minimum.inclusive ? 0 : 1)) {
|
|
883
|
+
throw invalid(path, `is below ${field.name}'s declared minimum`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (field.domain.maximum) {
|
|
887
|
+
const comparison = compareLiteral(value, field.domain.maximum.value, type);
|
|
888
|
+
if (comparison === undefined)
|
|
889
|
+
throw invalid(path, `cannot be ordered against ${field.name}'s declared maximum`);
|
|
890
|
+
if (comparison > (field.domain.maximum.inclusive ? 0 : -1)) {
|
|
891
|
+
throw invalid(path, `is above ${field.name}'s declared maximum`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (field.constraints.state === "known" || field.constraints.state === "partial") {
|
|
896
|
+
// ECMA-262 patterns are declarative schema metadata, not trusted client
|
|
897
|
+
// code. Semantic admission deliberately leaves them to the adapter/server
|
|
898
|
+
// instead of executing a potentially backtracking expression on caller
|
|
899
|
+
// input. The remaining constraints are deterministic and bounded here.
|
|
900
|
+
for (const constraint of field.constraints.values) {
|
|
901
|
+
if (constraint.kind === "length" && typeof value === "string") {
|
|
902
|
+
if (constraint.minimum !== undefined && value.length < constraint.minimum) {
|
|
903
|
+
throw invalid(path, `is shorter than ${field.name}'s minimum length ${constraint.minimum}`);
|
|
904
|
+
}
|
|
905
|
+
if (constraint.maximum !== undefined && value.length > constraint.maximum) {
|
|
906
|
+
throw invalid(path, `exceeds ${field.name}'s maximum length ${constraint.maximum}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else if (constraint.kind === "multiple-of") {
|
|
910
|
+
if (!literalMatchesMultipleOf(value, type, constraint.value)) {
|
|
911
|
+
throw invalid(path, `is not a multiple of ${field.name}'s declared step ${constraint.value}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function literalMatchesType(value, type) {
|
|
918
|
+
return isLogicalDomainValueCompatible(value, type);
|
|
919
|
+
}
|
|
920
|
+
function literalMatchesMultipleOf(value, type, step) {
|
|
921
|
+
if (typeof value === "string" &&
|
|
922
|
+
(type.kind === "integer" || type.kind === "decimal") &&
|
|
923
|
+
type.jsonEncoding === "string") {
|
|
924
|
+
return exactDecimalMultiple(value, step);
|
|
925
|
+
}
|
|
926
|
+
if (typeof value !== "number")
|
|
927
|
+
return false;
|
|
928
|
+
const quotient = value / step;
|
|
929
|
+
return Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.max(1, Math.abs(quotient)) * 8;
|
|
930
|
+
}
|
|
931
|
+
function exactDecimalMultiple(value, step) {
|
|
932
|
+
const candidate = decimalIntegerRatio(value);
|
|
933
|
+
const divisor = decimalIntegerRatio(String(step));
|
|
934
|
+
if (!candidate || !divisor || divisor.coefficient === 0n)
|
|
935
|
+
return false;
|
|
936
|
+
if (candidate.coefficient === 0n)
|
|
937
|
+
return true;
|
|
938
|
+
const scaleDelta = divisor.scale - candidate.scale;
|
|
939
|
+
if (scaleDelta >= 0) {
|
|
940
|
+
return (candidate.coefficient * 10n ** BigInt(scaleDelta)) % divisor.coefficient === 0n;
|
|
941
|
+
}
|
|
942
|
+
return candidate.coefficient % (divisor.coefficient * 10n ** BigInt(-scaleDelta)) === 0n;
|
|
943
|
+
}
|
|
944
|
+
function decimalIntegerRatio(value) {
|
|
945
|
+
const match = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(value);
|
|
946
|
+
if (!match)
|
|
947
|
+
return undefined;
|
|
948
|
+
const exponent = Number(match[4] ?? "0");
|
|
949
|
+
if (!Number.isSafeInteger(exponent))
|
|
950
|
+
return undefined;
|
|
951
|
+
const digits = `${match[2]}${match[3] ?? ""}`.replace(/^0+/, "") || "0";
|
|
952
|
+
let coefficient = BigInt(digits);
|
|
953
|
+
let scale = (match[3]?.length ?? 0) - exponent;
|
|
954
|
+
if (scale < 0) {
|
|
955
|
+
coefficient *= 10n ** BigInt(-scale);
|
|
956
|
+
scale = 0;
|
|
957
|
+
}
|
|
958
|
+
while (scale > 0 && coefficient % 10n === 0n) {
|
|
959
|
+
coefficient /= 10n;
|
|
960
|
+
scale -= 1;
|
|
961
|
+
}
|
|
962
|
+
return { coefficient, scale };
|
|
963
|
+
}
|
|
964
|
+
function compareLiteral(left, right, type) {
|
|
965
|
+
return compareLogicalDomainValues(left, right, type);
|
|
966
|
+
}
|
|
967
|
+
function parseBoundedJson(value) {
|
|
968
|
+
let parsed = value;
|
|
969
|
+
if (typeof value === "string") {
|
|
970
|
+
if (TEXT_ENCODER.encode(value).byteLength > MAX_SEMANTIC_QUERY_BYTES) {
|
|
971
|
+
throw invalid("$", `exceeds the ${MAX_SEMANTIC_QUERY_BYTES}-byte bound`);
|
|
972
|
+
}
|
|
973
|
+
assertUniqueJsonObjectNames(value);
|
|
974
|
+
try {
|
|
975
|
+
parsed = JSON.parse(value);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
throw invalid("$", `is not valid JSON: ${errorMessage(error)}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const state = { nodes: 0, bytes: 0, ancestors: new WeakSet() };
|
|
982
|
+
const json = cloneBoundedJson(parsed, "$", 0, state);
|
|
983
|
+
if (TEXT_ENCODER.encode(JSON.stringify(json)).byteLength > MAX_SEMANTIC_QUERY_BYTES) {
|
|
984
|
+
throw invalid("$", `exceeds the ${MAX_SEMANTIC_QUERY_BYTES}-byte bound`);
|
|
985
|
+
}
|
|
986
|
+
return json;
|
|
987
|
+
}
|
|
988
|
+
function cloneBoundedJson(value, path, depth, state) {
|
|
989
|
+
state.nodes += 1;
|
|
990
|
+
consumeJsonBytes(state, 1, path);
|
|
991
|
+
if (state.nodes > MAX_SEMANTIC_QUERY_NODES)
|
|
992
|
+
throw invalid(path, "exceeds the semantic query node bound");
|
|
993
|
+
if (depth > MAX_SEMANTIC_QUERY_DEPTH)
|
|
994
|
+
throw invalid(path, "exceeds the semantic query depth bound");
|
|
995
|
+
if (value === null || typeof value === "boolean")
|
|
996
|
+
return value;
|
|
997
|
+
if (typeof value === "string") {
|
|
998
|
+
const byteLength = TEXT_ENCODER.encode(value).byteLength;
|
|
999
|
+
if (byteLength > MAX_SEMANTIC_QUERY_TEXT_BYTES)
|
|
1000
|
+
throw invalid(path, "text is too large");
|
|
1001
|
+
consumeJsonBytes(state, byteLength + 2, path);
|
|
1002
|
+
return value;
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof value === "number") {
|
|
1005
|
+
if (!Number.isFinite(value))
|
|
1006
|
+
throw invalid(path, "must be a finite number");
|
|
1007
|
+
consumeJsonBytes(state, String(value).length, path);
|
|
1008
|
+
return Object.is(value, -0) ? 0 : value;
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof value !== "object")
|
|
1011
|
+
throw invalid(path, `contains unsupported ${typeof value}`);
|
|
1012
|
+
if (state.ancestors.has(value))
|
|
1013
|
+
throw invalid(path, "must not contain cycles");
|
|
1014
|
+
state.ancestors.add(value);
|
|
1015
|
+
try {
|
|
1016
|
+
if (safeIsArray(value, path)) {
|
|
1017
|
+
const lengthDescriptor = safeOwnPropertyDescriptor(value, "length", path);
|
|
1018
|
+
if (!lengthDescriptor || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value)) {
|
|
1019
|
+
throw invalid(path, "must have an ordinary array length");
|
|
1020
|
+
}
|
|
1021
|
+
const length = lengthDescriptor.value;
|
|
1022
|
+
if (length > MAX_SEMANTIC_QUERY_COLLECTION_ITEMS)
|
|
1023
|
+
throw invalid(path, "array is too large");
|
|
1024
|
+
for (const key of safeOwnKeys(value, path)) {
|
|
1025
|
+
if (key === "length")
|
|
1026
|
+
continue;
|
|
1027
|
+
if (typeof key !== "string" || !isCanonicalArrayIndex(key, length)) {
|
|
1028
|
+
throw invalid(path, "array must not contain symbol, extra, or non-JSON properties");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const out = [];
|
|
1032
|
+
for (let index = 0; index < length; index += 1) {
|
|
1033
|
+
const descriptor = safeOwnPropertyDescriptor(value, String(index), `${path}[${index}]`);
|
|
1034
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1035
|
+
throw invalid(`${path}[${index}]`, "must be an enumerable own data value");
|
|
1036
|
+
}
|
|
1037
|
+
out.push(cloneBoundedJson(descriptor.value, `${path}[${index}]`, depth + 1, state));
|
|
1038
|
+
}
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
const prototype = safeGetPrototypeOf(value, path);
|
|
1042
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1043
|
+
throw invalid(path, "must be a plain JSON object");
|
|
1044
|
+
const ownKeys = safeOwnKeys(value, path);
|
|
1045
|
+
if (ownKeys.some((key) => typeof key === "symbol"))
|
|
1046
|
+
throw invalid(path, "must not contain symbol properties");
|
|
1047
|
+
const keys = ownKeys;
|
|
1048
|
+
if (keys.length > MAX_SEMANTIC_QUERY_COLLECTION_ITEMS)
|
|
1049
|
+
throw invalid(path, "object has too many properties");
|
|
1050
|
+
const out = Object.create(null);
|
|
1051
|
+
for (const key of keys) {
|
|
1052
|
+
const keyBytes = TEXT_ENCODER.encode(key).byteLength;
|
|
1053
|
+
if (keyBytes > MAX_SEMANTIC_QUERY_TEXT_BYTES)
|
|
1054
|
+
throw invalid(`${path}.${key}`, "property name is too large");
|
|
1055
|
+
consumeJsonBytes(state, keyBytes + 3, `${path}.${key}`);
|
|
1056
|
+
const descriptor = safeOwnPropertyDescriptor(value, key, `${path}.${key}`);
|
|
1057
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1058
|
+
throw invalid(`${path}.${key}`, "must be an enumerable own data value");
|
|
1059
|
+
}
|
|
1060
|
+
Object.defineProperty(out, key, {
|
|
1061
|
+
value: cloneBoundedJson(descriptor.value, `${path}.${key}`, depth + 1, state),
|
|
1062
|
+
enumerable: true,
|
|
1063
|
+
writable: true,
|
|
1064
|
+
configurable: true,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
return out;
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
state.ancestors.delete(value);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function safeIsArray(value, path) {
|
|
1074
|
+
try {
|
|
1075
|
+
return Array.isArray(value);
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
throw invalid(path, "could not be inspected safely as JSON data");
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function safeGetPrototypeOf(value, path) {
|
|
1082
|
+
try {
|
|
1083
|
+
return Object.getPrototypeOf(value);
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
throw invalid(path, "could not be inspected safely as JSON data");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function safeOwnKeys(value, path) {
|
|
1090
|
+
try {
|
|
1091
|
+
return Reflect.ownKeys(value);
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
throw invalid(path, "could not be inspected safely as JSON data");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function safeOwnPropertyDescriptor(value, key, path) {
|
|
1098
|
+
try {
|
|
1099
|
+
return Object.getOwnPropertyDescriptor(value, key);
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
throw invalid(path, "could not be inspected safely as JSON data");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function isCanonicalArrayIndex(key, length) {
|
|
1106
|
+
if (key.length === 0)
|
|
1107
|
+
return false;
|
|
1108
|
+
const index = Number(key);
|
|
1109
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length && String(index) === key;
|
|
1110
|
+
}
|
|
1111
|
+
/** JSON.parse keeps the last duplicate object member; reject ambiguity first. */
|
|
1112
|
+
function assertUniqueJsonObjectNames(text) {
|
|
1113
|
+
const state = { text, index: 0 };
|
|
1114
|
+
scanJsonValue(state, "$", 0);
|
|
1115
|
+
skipJsonWhitespace(state);
|
|
1116
|
+
if (state.index !== text.length)
|
|
1117
|
+
throw invalid("$", "is not valid JSON");
|
|
1118
|
+
}
|
|
1119
|
+
function scanJsonValue(state, path, depth) {
|
|
1120
|
+
if (depth > MAX_SEMANTIC_QUERY_DEPTH)
|
|
1121
|
+
throw invalid(path, "exceeds the semantic query depth bound");
|
|
1122
|
+
skipJsonWhitespace(state);
|
|
1123
|
+
const character = state.text[state.index];
|
|
1124
|
+
if (character === "{") {
|
|
1125
|
+
scanJsonObject(state, path, depth);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (character === "[") {
|
|
1129
|
+
scanJsonArray(state, path, depth);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (character === '"') {
|
|
1133
|
+
scanJsonString(state);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (character === "t") {
|
|
1137
|
+
scanJsonKeyword(state, "true");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (character === "f") {
|
|
1141
|
+
scanJsonKeyword(state, "false");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (character === "n") {
|
|
1145
|
+
scanJsonKeyword(state, "null");
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
scanJsonNumber(state);
|
|
1149
|
+
}
|
|
1150
|
+
function scanJsonObject(state, path, depth) {
|
|
1151
|
+
state.index += 1;
|
|
1152
|
+
skipJsonWhitespace(state);
|
|
1153
|
+
if (state.text[state.index] === "}") {
|
|
1154
|
+
state.index += 1;
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const names = new Set();
|
|
1158
|
+
while (state.index < state.text.length) {
|
|
1159
|
+
skipJsonWhitespace(state);
|
|
1160
|
+
if (state.text[state.index] !== '"')
|
|
1161
|
+
throw invalid(path, "is not valid JSON");
|
|
1162
|
+
const name = scanJsonString(state);
|
|
1163
|
+
if (names.has(name))
|
|
1164
|
+
throw invalid(path, `contains duplicate object name ${JSON.stringify(name)}`);
|
|
1165
|
+
names.add(name);
|
|
1166
|
+
skipJsonWhitespace(state);
|
|
1167
|
+
if (state.text[state.index] !== ":")
|
|
1168
|
+
throw invalid(path, "is not valid JSON");
|
|
1169
|
+
state.index += 1;
|
|
1170
|
+
scanJsonValue(state, `${path}.${name}`, depth + 1);
|
|
1171
|
+
skipJsonWhitespace(state);
|
|
1172
|
+
const delimiter = state.text[state.index];
|
|
1173
|
+
if (delimiter === "}") {
|
|
1174
|
+
state.index += 1;
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (delimiter !== ",")
|
|
1178
|
+
throw invalid(path, "is not valid JSON");
|
|
1179
|
+
state.index += 1;
|
|
1180
|
+
}
|
|
1181
|
+
throw invalid(path, "is not valid JSON");
|
|
1182
|
+
}
|
|
1183
|
+
function scanJsonArray(state, path, depth) {
|
|
1184
|
+
state.index += 1;
|
|
1185
|
+
skipJsonWhitespace(state);
|
|
1186
|
+
if (state.text[state.index] === "]") {
|
|
1187
|
+
state.index += 1;
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
let index = 0;
|
|
1191
|
+
while (state.index < state.text.length) {
|
|
1192
|
+
scanJsonValue(state, `${path}[${index}]`, depth + 1);
|
|
1193
|
+
index += 1;
|
|
1194
|
+
skipJsonWhitespace(state);
|
|
1195
|
+
const delimiter = state.text[state.index];
|
|
1196
|
+
if (delimiter === "]") {
|
|
1197
|
+
state.index += 1;
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (delimiter !== ",")
|
|
1201
|
+
throw invalid(path, "is not valid JSON");
|
|
1202
|
+
state.index += 1;
|
|
1203
|
+
}
|
|
1204
|
+
throw invalid(path, "is not valid JSON");
|
|
1205
|
+
}
|
|
1206
|
+
function scanJsonString(state) {
|
|
1207
|
+
const start = state.index;
|
|
1208
|
+
state.index += 1;
|
|
1209
|
+
while (state.index < state.text.length) {
|
|
1210
|
+
const code = state.text.charCodeAt(state.index);
|
|
1211
|
+
if (code <= 0x1f)
|
|
1212
|
+
throw invalid("$", "is not valid JSON");
|
|
1213
|
+
const character = state.text[state.index];
|
|
1214
|
+
if (character === '"') {
|
|
1215
|
+
state.index += 1;
|
|
1216
|
+
return JSON.parse(state.text.slice(start, state.index));
|
|
1217
|
+
}
|
|
1218
|
+
if (character === "\\") {
|
|
1219
|
+
state.index += 1;
|
|
1220
|
+
const escapeCode = state.text[state.index];
|
|
1221
|
+
if (escapeCode === "u") {
|
|
1222
|
+
const digits = state.text.slice(state.index + 1, state.index + 5);
|
|
1223
|
+
if (digits.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(digits))
|
|
1224
|
+
throw invalid("$", "is not valid JSON");
|
|
1225
|
+
state.index += 5;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (escapeCode === undefined || !'"\\/bfnrt'.includes(escapeCode))
|
|
1229
|
+
throw invalid("$", "is not valid JSON");
|
|
1230
|
+
}
|
|
1231
|
+
state.index += 1;
|
|
1232
|
+
}
|
|
1233
|
+
throw invalid("$", "is not valid JSON");
|
|
1234
|
+
}
|
|
1235
|
+
function scanJsonKeyword(state, keyword) {
|
|
1236
|
+
if (state.text.slice(state.index, state.index + keyword.length) !== keyword)
|
|
1237
|
+
throw invalid("$", "is not valid JSON");
|
|
1238
|
+
state.index += keyword.length;
|
|
1239
|
+
}
|
|
1240
|
+
function scanJsonNumber(state) {
|
|
1241
|
+
const start = state.index;
|
|
1242
|
+
if (state.text[state.index] === "-")
|
|
1243
|
+
state.index += 1;
|
|
1244
|
+
if (state.text[state.index] === "0") {
|
|
1245
|
+
state.index += 1;
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (!isJsonDigit(state.text[state.index], false))
|
|
1249
|
+
throw invalid("$", "is not valid JSON");
|
|
1250
|
+
while (isJsonDigit(state.text[state.index], true))
|
|
1251
|
+
state.index += 1;
|
|
1252
|
+
}
|
|
1253
|
+
if (state.text[state.index] === ".") {
|
|
1254
|
+
state.index += 1;
|
|
1255
|
+
if (!isJsonDigit(state.text[state.index], true))
|
|
1256
|
+
throw invalid("$", "is not valid JSON");
|
|
1257
|
+
while (isJsonDigit(state.text[state.index], true))
|
|
1258
|
+
state.index += 1;
|
|
1259
|
+
}
|
|
1260
|
+
if (state.text[state.index] === "e" || state.text[state.index] === "E") {
|
|
1261
|
+
state.index += 1;
|
|
1262
|
+
if (state.text[state.index] === "+" || state.text[state.index] === "-")
|
|
1263
|
+
state.index += 1;
|
|
1264
|
+
if (!isJsonDigit(state.text[state.index], true))
|
|
1265
|
+
throw invalid("$", "is not valid JSON");
|
|
1266
|
+
while (isJsonDigit(state.text[state.index], true))
|
|
1267
|
+
state.index += 1;
|
|
1268
|
+
}
|
|
1269
|
+
if (state.index === start)
|
|
1270
|
+
throw invalid("$", "is not valid JSON");
|
|
1271
|
+
}
|
|
1272
|
+
function isJsonDigit(value, allowZero) {
|
|
1273
|
+
if (value === undefined)
|
|
1274
|
+
return false;
|
|
1275
|
+
const code = value.charCodeAt(0);
|
|
1276
|
+
return code >= (allowZero ? 0x30 : 0x31) && code <= 0x39;
|
|
1277
|
+
}
|
|
1278
|
+
function skipJsonWhitespace(state) {
|
|
1279
|
+
while (state.index < state.text.length) {
|
|
1280
|
+
const code = state.text.charCodeAt(state.index);
|
|
1281
|
+
if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
|
|
1282
|
+
return;
|
|
1283
|
+
state.index += 1;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function consumeJsonBytes(state, amount, path) {
|
|
1287
|
+
state.bytes += amount;
|
|
1288
|
+
if (state.bytes > MAX_SEMANTIC_QUERY_BYTES) {
|
|
1289
|
+
throw invalid(path, `exceeds the ${MAX_SEMANTIC_QUERY_BYTES}-byte bound`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function node(value) {
|
|
1293
|
+
return deepFreeze(parseBoundedJson(value));
|
|
1294
|
+
}
|
|
1295
|
+
function deepFreeze(value) {
|
|
1296
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1297
|
+
Object.freeze(value);
|
|
1298
|
+
for (const child of Object.values(value))
|
|
1299
|
+
deepFreeze(child);
|
|
1300
|
+
}
|
|
1301
|
+
return value;
|
|
1302
|
+
}
|
|
1303
|
+
function expectObject(value, path) {
|
|
1304
|
+
if (value === null || value === undefined || Array.isArray(value) || typeof value !== "object") {
|
|
1305
|
+
throw invalid(path, "must be an object");
|
|
1306
|
+
}
|
|
1307
|
+
return value;
|
|
1308
|
+
}
|
|
1309
|
+
function expectArray(value, path) {
|
|
1310
|
+
if (!Array.isArray(value))
|
|
1311
|
+
throw invalid(path, "must be an array");
|
|
1312
|
+
if (value.length > MAX_SEMANTIC_QUERY_COLLECTION_ITEMS)
|
|
1313
|
+
throw invalid(path, "array is too large");
|
|
1314
|
+
return value;
|
|
1315
|
+
}
|
|
1316
|
+
function expectString(value, path) {
|
|
1317
|
+
if (typeof value !== "string")
|
|
1318
|
+
throw invalid(path, "must be a string");
|
|
1319
|
+
return value;
|
|
1320
|
+
}
|
|
1321
|
+
function expectBoundedText(value, path) {
|
|
1322
|
+
const text = expectString(value, path);
|
|
1323
|
+
if (TEXT_ENCODER.encode(text).byteLength > MAX_SEMANTIC_QUERY_TEXT_BYTES)
|
|
1324
|
+
throw invalid(path, "text is too large");
|
|
1325
|
+
return text;
|
|
1326
|
+
}
|
|
1327
|
+
function expectNonblankBoundedText(value, path) {
|
|
1328
|
+
const text = expectBoundedText(value, path);
|
|
1329
|
+
if (text.trim() === "")
|
|
1330
|
+
throw invalid(path, "must be a non-empty string");
|
|
1331
|
+
return text;
|
|
1332
|
+
}
|
|
1333
|
+
function expectBoolean(value, path) {
|
|
1334
|
+
if (typeof value !== "boolean")
|
|
1335
|
+
throw invalid(path, "must be a boolean");
|
|
1336
|
+
return value;
|
|
1337
|
+
}
|
|
1338
|
+
function expectFiniteNumber(value, path) {
|
|
1339
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1340
|
+
throw invalid(path, "must be a finite number");
|
|
1341
|
+
return value;
|
|
1342
|
+
}
|
|
1343
|
+
function expectPositiveSafeInteger(value, path) {
|
|
1344
|
+
const number = expectFiniteNumber(value, path);
|
|
1345
|
+
if (!Number.isSafeInteger(number) || number <= 0)
|
|
1346
|
+
throw invalid(path, "must be a positive safe integer");
|
|
1347
|
+
return number;
|
|
1348
|
+
}
|
|
1349
|
+
function expectNonnegativeSafeInteger(value, path) {
|
|
1350
|
+
const number = expectFiniteNumber(value, path);
|
|
1351
|
+
if (!Number.isSafeInteger(number) || number < 0)
|
|
1352
|
+
throw invalid(path, "must be a nonnegative safe integer");
|
|
1353
|
+
return number;
|
|
1354
|
+
}
|
|
1355
|
+
function expectEnum(value, path, values) {
|
|
1356
|
+
const text = expectString(value, path);
|
|
1357
|
+
if (!values.includes(text))
|
|
1358
|
+
throw invalid(path, `must be one of ${values.join(", ")}`);
|
|
1359
|
+
return text;
|
|
1360
|
+
}
|
|
1361
|
+
function expectExact(value, expected, path) {
|
|
1362
|
+
if (value !== expected)
|
|
1363
|
+
throw invalid(path, `must be ${JSON.stringify(expected)}`);
|
|
1364
|
+
}
|
|
1365
|
+
function parseStringArray(value, path, requireNonempty) {
|
|
1366
|
+
const array = expectArray(value, path);
|
|
1367
|
+
if (requireNonempty && array.length === 0)
|
|
1368
|
+
throw invalid(path, "must contain at least one field");
|
|
1369
|
+
return array.map((entry, index) => expectNonblankBoundedText(entry, `${path}[${index}]`));
|
|
1370
|
+
}
|
|
1371
|
+
function assertOnlyKeys(object, allowed, path) {
|
|
1372
|
+
const allowedSet = new Set(allowed);
|
|
1373
|
+
const unexpected = Object.keys(object).find((key) => !allowedSet.has(key));
|
|
1374
|
+
if (unexpected)
|
|
1375
|
+
throw invalid(`${path}.${unexpected}`, "is not part of the semantic query contract");
|
|
1376
|
+
}
|
|
1377
|
+
function assertUnique(values, path, label) {
|
|
1378
|
+
const seen = new Set();
|
|
1379
|
+
for (const value of values) {
|
|
1380
|
+
if (seen.has(value))
|
|
1381
|
+
throw invalid(path, `contains duplicate ${label} ${JSON.stringify(value)}`);
|
|
1382
|
+
seen.add(value);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function assertIsoDate(value, path) {
|
|
1386
|
+
if (!isIsoDate(value))
|
|
1387
|
+
throw invalid(path, "must be an RFC 3339 full-date");
|
|
1388
|
+
}
|
|
1389
|
+
function assertIsoInstant(value, path) {
|
|
1390
|
+
if (!isIsoInstant(value))
|
|
1391
|
+
throw invalid(path, "must be an RFC 3339 instant with an explicit offset");
|
|
1392
|
+
}
|
|
1393
|
+
function temporalEndpointKind(value) {
|
|
1394
|
+
if (isIsoDate(value))
|
|
1395
|
+
return "date";
|
|
1396
|
+
if (isIsoInstant(value))
|
|
1397
|
+
return "instant";
|
|
1398
|
+
return undefined;
|
|
1399
|
+
}
|
|
1400
|
+
function isIsoDate(value) {
|
|
1401
|
+
return isLogicalDomainValueCompatible(value, RFC3339_DATE_TYPE);
|
|
1402
|
+
}
|
|
1403
|
+
function isIsoInstant(value) {
|
|
1404
|
+
return isLogicalDomainValueCompatible(value, RFC3339_INSTANT_TYPE);
|
|
1405
|
+
}
|
|
1406
|
+
function isBuiltInDialect(value) {
|
|
1407
|
+
return BUILT_IN_DIALECTS.has(value);
|
|
1408
|
+
}
|
|
1409
|
+
function isLiteralNode(value) {
|
|
1410
|
+
return (value !== null &&
|
|
1411
|
+
typeof value === "object" &&
|
|
1412
|
+
!Array.isArray(value) &&
|
|
1413
|
+
value.kind === "literal" &&
|
|
1414
|
+
Object.hasOwn(value, "value"));
|
|
1415
|
+
}
|
|
1416
|
+
function invalid(path, message) {
|
|
1417
|
+
return new HonuaQueryPlanningError("invalid-query", `${path} ${message}`);
|
|
1418
|
+
}
|
|
1419
|
+
function errorMessage(error) {
|
|
1420
|
+
return error instanceof Error ? error.message : String(error);
|
|
1421
|
+
}
|