@honua/react 0.0.20-alpha.0 → 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/contract/index.d.ts +2 -0
  2. package/contract/index.d.ts.map +1 -1
  3. package/contract/index.js +1 -0
  4. package/contract/pmtiles.d.ts +162 -0
  5. package/contract/pmtiles.d.ts.map +1 -0
  6. package/contract/pmtiles.js +155 -0
  7. package/contract/source.d.ts +24 -0
  8. package/contract/source.d.ts.map +1 -1
  9. package/contract/source.js +115 -8
  10. package/contract/types.d.ts +31 -2
  11. package/contract/types.d.ts.map +1 -1
  12. package/contract/types.js +8 -0
  13. package/core/auth/client-credentials-provider.d.ts +51 -0
  14. package/core/auth/client-credentials-provider.d.ts.map +1 -0
  15. package/core/auth/client-credentials-provider.js +115 -0
  16. package/core/auth/credential-store.d.ts +52 -0
  17. package/core/auth/credential-store.d.ts.map +1 -0
  18. package/core/auth/credential-store.js +82 -0
  19. package/core/auth/index.d.ts +23 -0
  20. package/core/auth/index.d.ts.map +1 -0
  21. package/core/auth/index.js +20 -0
  22. package/core/auth/oauth2-provider.d.ts +114 -0
  23. package/core/auth/oauth2-provider.d.ts.map +1 -0
  24. package/core/auth/oauth2-provider.js +408 -0
  25. package/core/auth/pkce.d.ts +25 -0
  26. package/core/auth/pkce.d.ts.map +1 -0
  27. package/core/auth/pkce.js +46 -0
  28. package/core/auth/static-providers.d.ts +20 -0
  29. package/core/auth/static-providers.d.ts.map +1 -0
  30. package/core/auth/static-providers.js +26 -0
  31. package/core/auth/token-endpoint.d.ts +40 -0
  32. package/core/auth/token-endpoint.d.ts.map +1 -0
  33. package/core/auth/token-endpoint.js +133 -0
  34. package/core/auth/types.d.ts +144 -0
  35. package/core/auth/types.d.ts.map +1 -0
  36. package/core/auth/types.js +8 -0
  37. package/core/client.d.ts +31 -1
  38. package/core/client.d.ts.map +1 -1
  39. package/core/client.js +52 -0
  40. package/core/errors.d.ts +33 -1
  41. package/core/errors.d.ts.map +1 -1
  42. package/core/errors.js +20 -0
  43. package/core/geoparquet-sql.d.ts +162 -0
  44. package/core/geoparquet-sql.d.ts.map +1 -0
  45. package/core/geoparquet-sql.js +333 -0
  46. package/core/ogc-endpoint-layout.d.ts +70 -0
  47. package/core/ogc-endpoint-layout.d.ts.map +1 -0
  48. package/core/ogc-endpoint-layout.js +173 -0
  49. package/core/ogc-features.d.ts.map +1 -1
  50. package/core/ogc-features.js +32 -17
  51. package/core/stac-static.d.ts +51 -0
  52. package/core/stac-static.d.ts.map +1 -0
  53. package/core/stac-static.js +195 -0
  54. package/core/stac.d.ts +12 -0
  55. package/core/stac.d.ts.map +1 -1
  56. package/core/stac.js +45 -15
  57. package/core/surfaces.d.ts +13 -0
  58. package/core/surfaces.d.ts.map +1 -1
  59. package/core/surfaces.js +58 -11
  60. package/core/types.d.ts +49 -0
  61. package/core/types.d.ts.map +1 -1
  62. package/core/wfs-capabilities.d.ts +10 -0
  63. package/core/wfs-capabilities.d.ts.map +1 -1
  64. package/core/wfs-capabilities.js +14 -2
  65. package/core/wfs.d.ts +13 -0
  66. package/core/wfs.d.ts.map +1 -1
  67. package/core/wfs.js +25 -5
  68. package/esri-compat/identity-manager.d.ts +43 -0
  69. package/esri-compat/identity-manager.d.ts.map +1 -1
  70. package/esri-compat/identity-manager.js +131 -1
  71. package/esri-compat/oauth-info.d.ts +15 -0
  72. package/esri-compat/oauth-info.d.ts.map +1 -1
  73. package/esri-compat/oauth-info.js +27 -0
  74. package/package.json +6 -1
  75. package/runtime/index.d.ts +2 -2
  76. package/runtime/index.d.ts.map +1 -1
  77. package/runtime/index.js +1 -4
  78. package/runtime/load-package.d.ts.map +1 -1
  79. package/runtime/load-package.js +8 -0
  80. package/runtime/map-package-validation.d.ts +0 -13
  81. package/runtime/map-package-validation.d.ts.map +1 -1
  82. package/runtime/map-package-validation.js +0 -12
  83. package/runtime/map-package.d.ts +1 -1
  84. package/runtime/map-package.d.ts.map +1 -1
  85. package/runtime/pmtiles-protocol.d.ts +77 -0
  86. package/runtime/pmtiles-protocol.d.ts.map +1 -0
  87. package/runtime/pmtiles-protocol.js +120 -0
  88. package/runtime/source-bridge.d.ts.map +1 -1
  89. package/runtime/source-bridge.js +27 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Pure SQL compiler for the GeoParquet / DuckDB-WASM `Source`
3
+ * (`@honua/sdk-js/geoparquet`). Translates the protocol-neutral
4
+ * {@link Query} envelope into DuckDB SQL over `read_parquet(...)` with spatial
5
+ * predicate pushdown.
6
+ *
7
+ * This module has **no runtime dependencies** (not even `@duckdb/duckdb-wasm`)
8
+ * so it can be unit- and snapshot-tested in isolation and so the SQL text is a
9
+ * pure function of its inputs. The heavy DuckDB orchestration lives in
10
+ * `src/geoparquet/`.
11
+ *
12
+ * ## SQL-injection safety
13
+ *
14
+ * Every value this compiler interpolates is either (a) a numeric literal that
15
+ * is validated to be a finite JS number and rendered with {@link numberLiteral},
16
+ * (b) a string literal escaped by {@link stringLiteral} (single quotes doubled,
17
+ * embedded NULs rejected), or (c) a SQL identifier escaped by
18
+ * {@link quoteIdentifier} (double quotes doubled, embedded NULs / control chars
19
+ * rejected). The one exception is {@link Query.where}: like the GeoServices and
20
+ * OGC adapters, `where` is caller-authored filter SQL and is passed through
21
+ * verbatim (wrapped in parentheses). Callers are responsible for the trust
22
+ * boundary on `where`, exactly as they are for a GeoServices `where` clause.
23
+ *
24
+ * @module
25
+ */
26
+ import type { AggregationSpec, Query } from "../contract/types.js";
27
+ import type { SpatialFilter } from "./spatial-filter.js";
28
+ /**
29
+ * Quote a SQL identifier for DuckDB: wrap in double quotes and double any
30
+ * embedded double quote. Rejects NUL and other control characters, which
31
+ * cannot appear in a legitimate column name and are a classic smuggling
32
+ * vector. Overture column names (`id`, `names`, `geometry`, dotted struct
33
+ * paths accessed as whole columns) are all safe under this rule.
34
+ */
35
+ export declare function quoteIdentifier(name: string): string;
36
+ /**
37
+ * Render a string SQL literal (single quotes doubled). Rejects embedded NUL.
38
+ */
39
+ export declare function stringLiteral(value: string): string;
40
+ /**
41
+ * Render a numeric SQL literal, validating the input is a finite JS number.
42
+ * Non-finite values (NaN/Infinity) throw rather than emit `'NaN'` — a spatial
43
+ * predicate must never silently widen to an infinite envelope.
44
+ */
45
+ export declare function numberLiteral(value: number): string;
46
+ /**
47
+ * Render an integer SQL literal (offset / limit). Coerces via `Math.trunc` and
48
+ * validates non-negative finiteness.
49
+ */
50
+ export declare function integerLiteral(value: number): string;
51
+ /**
52
+ * Build the `read_parquet(...)` table expression from one or more file URLs /
53
+ * globs. A single source renders `read_parquet('url')`; multiple sources
54
+ * render `read_parquet(['a', 'b'])`. Every URL is escaped as a string literal.
55
+ * `union_by_name := true` and `hive_partitioning := true` are set so mixed
56
+ * schemas across a hive-partitioned Overture theme line up by column name.
57
+ */
58
+ export declare function parquetSourceExpr(urls: readonly string[]): string;
59
+ /**
60
+ * How the geometry column is physically stored, which decides the SQL used to
61
+ * (a) evaluate a spatial predicate and (b) project GeoJSON output.
62
+ *
63
+ * - `wkb`: GeoParquet 1.0/1.1 well-known-binary BLOB — wrap with
64
+ * `ST_GeomFromWKB(...)`.
65
+ * - `native`: Parquet-native `GEOMETRY` / `GEOGRAPHY` logical type (Parquet
66
+ * 2.11, Mar 2025) — DuckDB reads it as a `GEOMETRY`, used directly.
67
+ * - `geojson`: GeoParquet `GeoJSON` encoding (string) — wrap with
68
+ * `ST_GeomFromGeoJSON(...)`.
69
+ */
70
+ export type GeometryEncoding = "wkb" | "native" | "geojson";
71
+ export interface GeometryColumnPlan {
72
+ /** Geometry column name in the parquet file. */
73
+ readonly column: string;
74
+ /** Physical encoding of that column. */
75
+ readonly encoding: GeometryEncoding;
76
+ /**
77
+ * Optional GeoParquet 1.1 "bbox covering" struct column (typically `bbox`
78
+ * with `xmin`/`ymin`/`xmax`/`ymax` fields). When present, an envelope filter
79
+ * can prune row groups without decoding any geometry.
80
+ */
81
+ readonly bboxColumn?: string;
82
+ }
83
+ /**
84
+ * SQL expression that yields a DuckDB `GEOMETRY` from the stored geometry
85
+ * column, adapting to the physical encoding.
86
+ */
87
+ export declare function geometryExpr(plan: GeometryColumnPlan): string;
88
+ export interface BBox {
89
+ readonly xmin: number;
90
+ readonly ymin: number;
91
+ readonly xmax: number;
92
+ readonly ymax: number;
93
+ }
94
+ /**
95
+ * Reduce any Esri-shaped {@link SpatialFilter} geometry to its axis-aligned
96
+ * bounding box. Envelopes are exact; points, polylines (`paths`), polygons
97
+ * (`rings`), and multipoints (`points`) are reduced to their coordinate
98
+ * bounds. Returns `undefined` when the geometry carries no usable coordinates.
99
+ */
100
+ export declare function bboxFromSpatialFilter(filter: SpatialFilter): BBox | undefined;
101
+ /**
102
+ * True when the spatial filter is a plain envelope (exact bbox intersection).
103
+ * Anything else is reduced to its bbox and is therefore an approximation the
104
+ * caller should be told about via a `degraded` reason.
105
+ */
106
+ export declare function isEnvelopeFilter(filter: SpatialFilter): boolean;
107
+ /**
108
+ * Compile the spatial predicate for a bbox against a geometry plan. Prefers the
109
+ * GeoParquet 1.1 bbox-covering column (row-group-skippable) when present, then
110
+ * falls back to `ST_Intersects(<geom>, ST_MakeEnvelope(...))`.
111
+ */
112
+ export declare function spatialPredicate(bbox: BBox, plan: GeometryColumnPlan): string;
113
+ export interface CompileOptions {
114
+ /** Parquet file URL(s) / glob(s). */
115
+ readonly sources: readonly string[];
116
+ /** Geometry column plan (from metadata detection), if the file is spatial. */
117
+ readonly geometry?: GeometryColumnPlan;
118
+ /**
119
+ * All non-geometry column names available in the file, used to expand a
120
+ * `returnGeometry`-only projection and to validate that requested
121
+ * `outFields` are addressable. When omitted, `*` (minus geometry) is used.
122
+ */
123
+ readonly columns?: readonly string[];
124
+ /** Output alias for the projected GeoJSON geometry. Defaults to `geometry`. */
125
+ readonly geometryAlias?: string;
126
+ }
127
+ export interface CompiledSql {
128
+ readonly sql: string;
129
+ /** True when a non-envelope spatial filter was reduced to its bbox. */
130
+ readonly bboxApproximated: boolean;
131
+ }
132
+ /**
133
+ * Compile a feature {@link Query} into a DuckDB `SELECT`. The geometry column,
134
+ * when present and `returnGeometry !== false`, is projected as GeoJSON text via
135
+ * `ST_AsGeoJSON` under the alias `geometry` (overridable).
136
+ */
137
+ export declare function compileQuery(query: Query, options: CompileOptions): CompiledSql;
138
+ /**
139
+ * Compile an {@link AggregationSpec} into a GROUP BY `SELECT`. Group-by fields
140
+ * are echoed in the projection (so the returned rows carry the group key),
141
+ * followed by each metric expression aliased to its `alias` (or `fn_field`).
142
+ */
143
+ export declare function compileAggregate(query: Query & {
144
+ aggregation: AggregationSpec;
145
+ }, options: CompileOptions): CompiledSql;
146
+ /**
147
+ * SQL that reads the parquet footer schema without scanning rows.
148
+ */
149
+ export declare function describeSql(sources: readonly string[]): string;
150
+ /**
151
+ * Cheap row-count estimate from the parquet footer (`num_rows` per row group),
152
+ * summed across all files. Avoids a full `count(*)` table scan on large
153
+ * Overture extracts.
154
+ */
155
+ export declare function rowEstimateSql(sources: readonly string[]): string;
156
+ /**
157
+ * SQL that reads the GeoParquet `geo` key-value metadata (the JSON blob that
158
+ * carries `primary_column`, per-column `encoding`, and `crs`). Returns zero
159
+ * rows for Parquet-native geometry files, which carry no `geo` metadata.
160
+ */
161
+ export declare function geoMetadataSql(sources: readonly string[]): string;
162
+ //# sourceMappingURL=geoparquet-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geoparquet-sql.d.ts","sourceRoot":"","sources":["../../../src/core/geoparquet-sql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Pure SQL compiler for the GeoParquet / DuckDB-WASM `Source`
3
+ * (`@honua/sdk-js/geoparquet`). Translates the protocol-neutral
4
+ * {@link Query} envelope into DuckDB SQL over `read_parquet(...)` with spatial
5
+ * predicate pushdown.
6
+ *
7
+ * This module has **no runtime dependencies** (not even `@duckdb/duckdb-wasm`)
8
+ * so it can be unit- and snapshot-tested in isolation and so the SQL text is a
9
+ * pure function of its inputs. The heavy DuckDB orchestration lives in
10
+ * `src/geoparquet/`.
11
+ *
12
+ * ## SQL-injection safety
13
+ *
14
+ * Every value this compiler interpolates is either (a) a numeric literal that
15
+ * is validated to be a finite JS number and rendered with {@link numberLiteral},
16
+ * (b) a string literal escaped by {@link stringLiteral} (single quotes doubled,
17
+ * embedded NULs rejected), or (c) a SQL identifier escaped by
18
+ * {@link quoteIdentifier} (double quotes doubled, embedded NULs / control chars
19
+ * rejected). The one exception is {@link Query.where}: like the GeoServices and
20
+ * OGC adapters, `where` is caller-authored filter SQL and is passed through
21
+ * verbatim (wrapped in parentheses). Callers are responsible for the trust
22
+ * boundary on `where`, exactly as they are for a GeoServices `where` clause.
23
+ *
24
+ * @module
25
+ */
26
+ // ── Identifier / literal escaping ─────────────────────────────
27
+ /**
28
+ * Quote a SQL identifier for DuckDB: wrap in double quotes and double any
29
+ * embedded double quote. Rejects NUL and other control characters, which
30
+ * cannot appear in a legitimate column name and are a classic smuggling
31
+ * vector. Overture column names (`id`, `names`, `geometry`, dotted struct
32
+ * paths accessed as whole columns) are all safe under this rule.
33
+ */
34
+ export function quoteIdentifier(name) {
35
+ if (typeof name !== "string" || name.length === 0) {
36
+ throw new Error(`geoparquet: invalid SQL identifier ${JSON.stringify(name)}`);
37
+ }
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control chars in identifiers is the point
39
+ if (/[\u0000-\u001f\u007f]/.test(name)) {
40
+ throw new Error(`geoparquet: SQL identifier contains control characters: ${JSON.stringify(name)}`);
41
+ }
42
+ return `"${name.replace(/"/g, '""')}"`;
43
+ }
44
+ /**
45
+ * Render a string SQL literal (single quotes doubled). Rejects embedded NUL.
46
+ */
47
+ export function stringLiteral(value) {
48
+ if (typeof value !== "string") {
49
+ throw new Error(`geoparquet: expected a string literal, received ${typeof value}`);
50
+ }
51
+ if (value.includes("\u0000")) {
52
+ throw new Error("geoparquet: string literal contains a NUL byte");
53
+ }
54
+ return `'${value.replace(/'/g, "''")}'`;
55
+ }
56
+ /**
57
+ * Render a numeric SQL literal, validating the input is a finite JS number.
58
+ * Non-finite values (NaN/Infinity) throw rather than emit `'NaN'` — a spatial
59
+ * predicate must never silently widen to an infinite envelope.
60
+ */
61
+ export function numberLiteral(value) {
62
+ if (typeof value !== "number" || !Number.isFinite(value)) {
63
+ throw new Error(`geoparquet: expected a finite number, received ${String(value)}`);
64
+ }
65
+ return String(value);
66
+ }
67
+ /**
68
+ * Render an integer SQL literal (offset / limit). Coerces via `Math.trunc` and
69
+ * validates non-negative finiteness.
70
+ */
71
+ export function integerLiteral(value) {
72
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
73
+ throw new Error(`geoparquet: expected a non-negative integer, received ${String(value)}`);
74
+ }
75
+ return String(Math.trunc(value));
76
+ }
77
+ // ── read_parquet(...) source expression ───────────────────────
78
+ /**
79
+ * Build the `read_parquet(...)` table expression from one or more file URLs /
80
+ * globs. A single source renders `read_parquet('url')`; multiple sources
81
+ * render `read_parquet(['a', 'b'])`. Every URL is escaped as a string literal.
82
+ * `union_by_name := true` and `hive_partitioning := true` are set so mixed
83
+ * schemas across a hive-partitioned Overture theme line up by column name.
84
+ */
85
+ export function parquetSourceExpr(urls) {
86
+ if (!Array.isArray(urls) || urls.length === 0) {
87
+ throw new Error("geoparquet: at least one parquet URL is required");
88
+ }
89
+ const list = urls.map(stringLiteral);
90
+ const arg = list.length === 1 ? list[0] : `[${list.join(", ")}]`;
91
+ return `read_parquet(${arg}, union_by_name = true, hive_partitioning = true)`;
92
+ }
93
+ /**
94
+ * SQL expression that yields a DuckDB `GEOMETRY` from the stored geometry
95
+ * column, adapting to the physical encoding.
96
+ */
97
+ export function geometryExpr(plan) {
98
+ const col = quoteIdentifier(plan.column);
99
+ switch (plan.encoding) {
100
+ case "wkb":
101
+ return `ST_GeomFromWKB(${col})`;
102
+ case "geojson":
103
+ return `ST_GeomFromGeoJSON(${col})`;
104
+ case "native":
105
+ return col;
106
+ }
107
+ }
108
+ /**
109
+ * Reduce any Esri-shaped {@link SpatialFilter} geometry to its axis-aligned
110
+ * bounding box. Envelopes are exact; points, polylines (`paths`), polygons
111
+ * (`rings`), and multipoints (`points`) are reduced to their coordinate
112
+ * bounds. Returns `undefined` when the geometry carries no usable coordinates.
113
+ */
114
+ export function bboxFromSpatialFilter(filter) {
115
+ const g = filter.geometry;
116
+ if (typeof g.xmin === "number" &&
117
+ typeof g.ymin === "number" &&
118
+ typeof g.xmax === "number" &&
119
+ typeof g.ymax === "number") {
120
+ return { xmin: g.xmin, ymin: g.ymin, xmax: g.xmax, ymax: g.ymax };
121
+ }
122
+ if (typeof g.x === "number" && typeof g.y === "number") {
123
+ return { xmin: g.x, ymin: g.y, xmax: g.x, ymax: g.y };
124
+ }
125
+ const coordSets = [];
126
+ if (Array.isArray(g.rings))
127
+ coordSets.push(...g.rings);
128
+ if (Array.isArray(g.paths))
129
+ coordSets.push(...g.paths);
130
+ if (Array.isArray(g.points))
131
+ coordSets.push(g.points);
132
+ let xmin = Number.POSITIVE_INFINITY;
133
+ let ymin = Number.POSITIVE_INFINITY;
134
+ let xmax = Number.NEGATIVE_INFINITY;
135
+ let ymax = Number.NEGATIVE_INFINITY;
136
+ let seen = false;
137
+ for (const set of coordSets) {
138
+ if (!Array.isArray(set))
139
+ continue;
140
+ for (const pt of set) {
141
+ if (!Array.isArray(pt) || typeof pt[0] !== "number" || typeof pt[1] !== "number")
142
+ continue;
143
+ seen = true;
144
+ xmin = Math.min(xmin, pt[0]);
145
+ ymin = Math.min(ymin, pt[1]);
146
+ xmax = Math.max(xmax, pt[0]);
147
+ ymax = Math.max(ymax, pt[1]);
148
+ }
149
+ }
150
+ return seen ? { xmin, ymin, xmax, ymax } : undefined;
151
+ }
152
+ /**
153
+ * True when the spatial filter is a plain envelope (exact bbox intersection).
154
+ * Anything else is reduced to its bbox and is therefore an approximation the
155
+ * caller should be told about via a `degraded` reason.
156
+ */
157
+ export function isEnvelopeFilter(filter) {
158
+ return filter.geometryType === "esriGeometryEnvelope";
159
+ }
160
+ /**
161
+ * Compile the spatial predicate for a bbox against a geometry plan. Prefers the
162
+ * GeoParquet 1.1 bbox-covering column (row-group-skippable) when present, then
163
+ * falls back to `ST_Intersects(<geom>, ST_MakeEnvelope(...))`.
164
+ */
165
+ export function spatialPredicate(bbox, plan) {
166
+ const xmin = numberLiteral(bbox.xmin);
167
+ const ymin = numberLiteral(bbox.ymin);
168
+ const xmax = numberLiteral(bbox.xmax);
169
+ const ymax = numberLiteral(bbox.ymax);
170
+ if (plan.bboxColumn) {
171
+ const b = quoteIdentifier(plan.bboxColumn);
172
+ // Two envelopes intersect iff neither is strictly to one side of the other.
173
+ return `${b}.xmin <= ${xmax} AND ${b}.xmax >= ${xmin} AND ` + `${b}.ymin <= ${ymax} AND ${b}.ymax >= ${ymin}`;
174
+ }
175
+ return `ST_Intersects(${geometryExpr(plan)}, ST_MakeEnvelope(${xmin}, ${ymin}, ${xmax}, ${ymax}))`;
176
+ }
177
+ const DEFAULT_GEOMETRY_ALIAS = "geometry";
178
+ // ── WHERE + ORDER BY + LIMIT fragments ────────────────────────
179
+ function whereClause(query, spatial) {
180
+ const clauses = [];
181
+ if (typeof query.where === "string" && query.where.trim().length > 0) {
182
+ clauses.push(`(${query.where})`);
183
+ }
184
+ if (spatial)
185
+ clauses.push(`(${spatial})`);
186
+ return clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
187
+ }
188
+ function orderByClause(orderBy) {
189
+ if (!orderBy || orderBy.length === 0)
190
+ return "";
191
+ const parts = orderBy.map((s) => {
192
+ const dir = s.direction === "desc" ? " DESC" : " ASC";
193
+ return `${quoteIdentifier(s.field)}${dir}`;
194
+ });
195
+ return ` ORDER BY ${parts.join(", ")}`;
196
+ }
197
+ function limitOffsetClause(query) {
198
+ let out = "";
199
+ const limit = query.pagination?.limit;
200
+ const offset = query.pagination?.offset;
201
+ if (typeof limit === "number")
202
+ out += ` LIMIT ${integerLiteral(limit)}`;
203
+ if (typeof offset === "number" && offset > 0)
204
+ out += ` OFFSET ${integerLiteral(offset)}`;
205
+ return out;
206
+ }
207
+ // ── Projection ────────────────────────────────────────────────
208
+ function projection(query, options) {
209
+ const geom = options.geometry;
210
+ const geomAlias = quoteIdentifier(options.geometryAlias ?? DEFAULT_GEOMETRY_ALIAS);
211
+ const wantGeometry = query.returnGeometry !== false && geom !== undefined;
212
+ const geomSelect = wantGeometry ? `ST_AsGeoJSON(${geometryExpr(geom)}) AS ${geomAlias}` : undefined;
213
+ const outFields = query.outFields;
214
+ if (outFields && outFields.length > 0) {
215
+ const cols = outFields.filter((f) => !geom || f !== geom.column).map((f) => quoteIdentifier(f));
216
+ if (geomSelect)
217
+ cols.push(geomSelect);
218
+ return cols.length > 0 ? cols.join(", ") : "*";
219
+ }
220
+ // No explicit projection: select every non-geometry column, plus the
221
+ // GeoJSON-projected geometry when requested. When we do not know the column
222
+ // list, fall back to `* EXCLUDE (geom)` so raw WKB never rides along.
223
+ const nonGeom = options.columns?.filter((c) => !geom || c !== geom.column);
224
+ if (nonGeom && nonGeom.length > 0) {
225
+ const cols = nonGeom.map((c) => quoteIdentifier(c));
226
+ if (geomSelect)
227
+ cols.push(geomSelect);
228
+ return cols.join(", ");
229
+ }
230
+ if (geom) {
231
+ const base = `* EXCLUDE (${quoteIdentifier(geom.column)})`;
232
+ return geomSelect ? `${base}, ${geomSelect}` : base;
233
+ }
234
+ return "*";
235
+ }
236
+ /**
237
+ * Compile a feature {@link Query} into a DuckDB `SELECT`. The geometry column,
238
+ * when present and `returnGeometry !== false`, is projected as GeoJSON text via
239
+ * `ST_AsGeoJSON` under the alias `geometry` (overridable).
240
+ */
241
+ export function compileQuery(query, options) {
242
+ const from = parquetSourceExpr(options.sources);
243
+ let spatial;
244
+ let bboxApproximated = false;
245
+ if (query.spatialFilter) {
246
+ if (!options.geometry) {
247
+ throw new Error("geoparquet: spatialFilter supplied but the source has no detected geometry column");
248
+ }
249
+ const bbox = bboxFromSpatialFilter(query.spatialFilter);
250
+ if (bbox) {
251
+ spatial = spatialPredicate(bbox, options.geometry);
252
+ bboxApproximated = !isEnvelopeFilter(query.spatialFilter);
253
+ }
254
+ }
255
+ const sql = `SELECT ${projection(query, options)} FROM ${from}${whereClause(query, spatial)}${orderByClause(query.orderBy)}${limitOffsetClause(query)}`;
256
+ return { sql, bboxApproximated };
257
+ }
258
+ function aggregateFn(fn, field) {
259
+ const col = field === "*" ? "*" : quoteIdentifier(field);
260
+ switch (fn) {
261
+ case "count":
262
+ return `count(${col})`;
263
+ case "sum":
264
+ return `sum(${col})`;
265
+ case "avg":
266
+ return `avg(${col})`;
267
+ case "min":
268
+ return `min(${col})`;
269
+ case "max":
270
+ return `max(${col})`;
271
+ case "stddev":
272
+ return `stddev_samp(${col})`;
273
+ case "var":
274
+ return `var_samp(${col})`;
275
+ }
276
+ }
277
+ /**
278
+ * Compile an {@link AggregationSpec} into a GROUP BY `SELECT`. Group-by fields
279
+ * are echoed in the projection (so the returned rows carry the group key),
280
+ * followed by each metric expression aliased to its `alias` (or `fn_field`).
281
+ */
282
+ export function compileAggregate(query, options) {
283
+ const from = parquetSourceExpr(options.sources);
284
+ const agg = query.aggregation;
285
+ if (!agg.metrics || agg.metrics.length === 0) {
286
+ throw new Error("geoparquet: aggregation requires at least one metric");
287
+ }
288
+ const groupBy = agg.groupBy ?? [];
289
+ const selectParts = [];
290
+ for (const field of groupBy)
291
+ selectParts.push(quoteIdentifier(field));
292
+ for (const m of agg.metrics) {
293
+ const alias = quoteIdentifier(m.alias ?? `${m.fn}_${m.field === "*" ? "all" : m.field}`);
294
+ selectParts.push(`${aggregateFn(m.fn, m.field)} AS ${alias}`);
295
+ }
296
+ let spatial;
297
+ let bboxApproximated = false;
298
+ if (query.spatialFilter && options.geometry) {
299
+ const bbox = bboxFromSpatialFilter(query.spatialFilter);
300
+ if (bbox) {
301
+ spatial = spatialPredicate(bbox, options.geometry);
302
+ bboxApproximated = !isEnvelopeFilter(query.spatialFilter);
303
+ }
304
+ }
305
+ const groupByClause = groupBy.length > 0 ? ` GROUP BY ${groupBy.map((f) => quoteIdentifier(f)).join(", ")}` : "";
306
+ const sql = `SELECT ${selectParts.join(", ")} FROM ${from}${whereClause(query, spatial)}${groupByClause}${orderByClause(query.orderBy)}${limitOffsetClause(query)}`;
307
+ return { sql, bboxApproximated };
308
+ }
309
+ /**
310
+ * SQL that reads the parquet footer schema without scanning rows.
311
+ */
312
+ export function describeSql(sources) {
313
+ return `DESCRIBE SELECT * FROM ${parquetSourceExpr(sources)}`;
314
+ }
315
+ /**
316
+ * Cheap row-count estimate from the parquet footer (`num_rows` per row group),
317
+ * summed across all files. Avoids a full `count(*)` table scan on large
318
+ * Overture extracts.
319
+ */
320
+ export function rowEstimateSql(sources) {
321
+ const list = sources.map(stringLiteral);
322
+ const arg = list.length === 1 ? list[0] : `[${list.join(", ")}]`;
323
+ return `SELECT CAST(COALESCE(sum(num_rows), 0) AS BIGINT) AS row_estimate FROM parquet_file_metadata(${arg})`;
324
+ }
325
+ /**
326
+ * SQL that reads the GeoParquet `geo` key-value metadata (the JSON blob that
327
+ * carries `primary_column`, per-column `encoding`, and `crs`). Returns zero
328
+ * rows for Parquet-native geometry files, which carry no `geo` metadata.
329
+ */
330
+ export function geoMetadataSql(sources) {
331
+ const first = stringLiteral(sources[0] ?? "");
332
+ return `SELECT value FROM parquet_kv_metadata(${first}) WHERE decode(key) = 'geo'`;
333
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * OGC API endpoint-layout resolution.
3
+ *
4
+ * The SDK's headline claim is one typed contract against ANY standards
5
+ * server. For OGC API Features / Records that means the client must not
6
+ * assume a fixed path prefix: a Honua Server mounts the facade at
7
+ * `/ogc/features/...`, but pygeoapi, ldproxy, and GeoServer's OGC API each
8
+ * serve the landing page at their own root and advertise their collections
9
+ * and conformance URLs through the landing page's `links` array
10
+ * (OGC API - Common Requirement 5).
11
+ *
12
+ * This module produces an {@link OgcEndpointLayout} — the concrete set of
13
+ * resource paths a Features client addresses:
14
+ *
15
+ * - {@link honuaFacadeFeaturesLayout} is the fixed facade layout and the
16
+ * default. It performs no network round-trips, so existing callers see
17
+ * zero behaviour change.
18
+ * - {@link resolveOgcEndpointLayout} discovers the layout from the landing
19
+ * page (`ogc-api` mode) or auto-detects the facade first and falls back
20
+ * to discovery (`auto` mode).
21
+ *
22
+ * Per OGC API - Features Requirement 17 the items resource is always at
23
+ * `{collections}/{collectionId}/items`, so a single landing-page fetch is
24
+ * enough to derive every per-collection path from the discovered
25
+ * collections URL.
26
+ *
27
+ * @module
28
+ */
29
+ import type { HonuaProtocolTransport } from "./protocol-transport.js";
30
+ import type { OgcEndpointLayout } from "./types.js";
31
+ /** Discovery mode carried on a source locator. */
32
+ export type OgcApiLayoutMode = "honua-facade" | "ogc-api" | "auto";
33
+ /**
34
+ * The Honua Server facade layout (`/ogc/features/...`). No network access;
35
+ * this is the default fast path and preserves the pre-existing behaviour of
36
+ * every OGC Features caller that points at a Honua Server.
37
+ */
38
+ export declare function honuaFacadeFeaturesLayout(base?: string): OgcEndpointLayout;
39
+ /**
40
+ * A raw OGC API layout built from a discovered (or configured) landing +
41
+ * collections + conformance URL triple. Per-collection paths are templated
42
+ * off the collections URL per OGC API - Features Requirement 17.
43
+ */
44
+ export declare function ogcApiFeaturesLayout(options: {
45
+ landingUrl: string;
46
+ collectionsUrl: string;
47
+ conformanceUrl: string;
48
+ }): OgcEndpointLayout;
49
+ /**
50
+ * Find the first link whose `rel` matches one of `rels` (exact or as the
51
+ * final path segment of a full OGC relation URI, e.g.
52
+ * `http://www.opengis.net/def/rel/ogc/1.0/data`).
53
+ */
54
+ export declare function findOgcLink(links: ReadonlyArray<{
55
+ href?: string;
56
+ rel?: string;
57
+ }> | undefined, ...rels: string[]): string | undefined;
58
+ /**
59
+ * Resolve the endpoint layout for the requested `mode`:
60
+ *
61
+ * - `honua-facade` (default) — the fixed facade layout, no round-trips.
62
+ * - `ogc-api` — discover from the landing page at the client baseUrl.
63
+ * - `auto` — probe the facade landing; use it when valid, otherwise
64
+ * discover from the root.
65
+ *
66
+ * Discovery failures surface as {@link HonuaCapabilityNotSupportedError}
67
+ * (per the SDK convention of throwing rather than returning empty data).
68
+ */
69
+ export declare function resolveOgcEndpointLayout(transport: HonuaProtocolTransport, mode?: OgcApiLayoutMode): Promise<OgcEndpointLayout>;
70
+ //# sourceMappingURL=ogc-endpoint-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ogc-endpoint-layout.d.ts","sourceRoot":"","sources":["../../../src/core/ogc-endpoint-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAA2B,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE7E,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,SAAS,GAAG,MAAM,CAAC;AAcnE;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,GAAE,MAA4B,GAAG,iBAAiB,CAY/F;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,iBAAiB,CAiBpB;AAgBD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,aAAa,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,EACjE,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,MAAM,GAAG,SAAS,CAWpB;AAyCD;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,sBAAsB,EACjC,IAAI,GAAE,gBAAiC,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAe5B"}