forked from camptocamp/ogc-client
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathurl_builder.ts
More file actions
2943 lines (2823 loc) · 110 KB
/
url_builder.ts
File metadata and controls
2943 lines (2823 loc) · 110 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import type {
QueryOptions,
SystemQueryOptions,
DeploymentQueryOptions,
ProcedureQueryOptions,
SamplingFeatureQueryOptions,
PropertyQueryOptions,
DatastreamQueryOptions,
ObservationQueryOptions,
ControlStreamQueryOptions,
CommandQueryOptions,
CommandStatusQueryOptions,
CSAPICollectionRef,
CSAPIResourceType,
} from './model.js';
import { EndpointError } from '../../shared/errors.js';
import {
encodeResourceId,
formatDateTimeParameter,
scanCsapiLinks,
validateLimit,
validateBbox,
} from './helpers.js';
/**
* Compile-time-constrained set of sub-path segments that may be appended
* after a resource ID in CSAPI URLs.
*
* Derived from OGC API — Connected Systems Parts 1 & 2 path definitions.
* Restricting the type prevents accidental path traversal or typos.
*/
type ResourceSubPath =
| 'history'
| 'subsystems'
| 'subdeployments'
| 'systems'
| 'deployments'
| 'samplingFeatures'
| 'procedures'
| 'datastreams'
| 'controlstreams'
| 'observations'
| 'commands'
| 'schema'
| 'feasibility'
| 'datastream'
| 'samplingFeature'
| 'system'
| 'status'
| 'result'
| 'cancel';
/**
* Builds query URLs for the OGC API - Connected Systems specification.
*
* Constructs canonical and nested resource endpoint URLs for all 9 CSAPI
* resource types (Part 1: systems, deployments, procedures, samplingFeatures,
* properties; Part 2: datastreams, observations, controlStreams, commands).
*
* **URL builder, NOT an HTTP client.** Every `get*()` method on this class
* returns a URL **string** — no network request is made. The consumer is
* responsible for issuing the `fetch()` call (auth headers, timeouts,
* retries, `AbortSignal`, error handling, content-negotiation) and for
* passing the parsed JSON body to the matching parser function
* (`parseDatastream`, `parseObservation`, `parseSystem`, …). This mirrors
* the design of `EDRQueryBuilder` from the sibling `ogc-api/edr` module —
* same pattern, same rationale. See the {@link module:csapi | csapi module
* docblock} for the full 5-step request pattern and a complete worked
* example.
*
* ## Resource Discovery
*
* Available resources are discovered automatically from the collection's link
* relations. Attempting to build a URL for an unavailable resource throws an
* {@link EndpointError}. Check `availableResources` to inspect what is available.
*
* ## Pagination
*
* All list methods (`get*` returning collection URLs) follow the
* [OGC API Common](https://docs.ogc.org/is/19-072/19-072.html#_pagination)
* pagination contract:
*
* - **The server chooses the default page size** if `limit` is unspecified.
* Defaults vary by implementation — `connected-systems-go` defaults to
* `limit=10`; OpenSensorHub defaults to `limit=100`. Code that processes
* only the first response may silently lose data on low-default servers.
*
* - **The server returns `next` HATEOAS links** in the response body's
* `links` array (`rel: "next"`) when more pages are available. The
* consumer is responsible for following them; this library does not
* auto-paginate.
*
* - **A future enhancement** (deferred — see issue
* [#170](https://github.com/OS4CSAPI/ogc-client-CSAPI_2/issues/170))
* may add an opt-in async-iterator / `followNext` helper. Until then,
* consumer code MUST follow `next` links explicitly to avoid data loss.
*
* Every list method on this class carries a `@remarks` Pagination block
* that points back at this section.
*
* ## Error Handling
*
* All URL-building methods throw {@link EndpointError} when the requested
* resource type is not available on the collection. Wrap calls in try/catch
* or check `builder.availableResources.has('systems')` before calling.
*
* ```ts
* try {
* const url = builder.getSystems();
* } catch (e) {
* if (e instanceof EndpointError) {
* console.warn('Systems not available:', e.message);
* }
* }
* ```
*
* ## Migration from Direct API Access
*
* Instead of manually constructing CSAPI URLs:
* ```ts
* // Before (manual URL construction):
* const url = `${baseUrl}/collections/${collectionId}/systems?limit=50&bbox=-180,-90,180,90`;
*
* // After (using CSAPIQueryBuilder):
* const endpoint = await new OgcApiEndpoint(baseUrl);
* const builder = await createCSAPIBuilder(endpoint, collectionId);
* const url = builder.getSystems({ limit: 50, bbox: [-180, -90, 180, 90] });
* ```
*
* The builder handles URL encoding, parameter validation, resource
* availability checks, and supports both collection-scoped and
* root-level API resource URLs automatically.
*
* @example Complete workflow — list, filter, and navigate CSAPI resources:
* ```ts
* import { OgcApiEndpoint } from '@camptocamp/ogc-client';
* import { createCSAPIBuilder } from '@camptocamp/ogc-client/csapi';
*
* const endpoint = await new OgcApiEndpoint('https://api.example.com');
* const builder = await createCSAPIBuilder(endpoint, 'weather-stations');
*
* // List systems with spatial and text filters
* const systemsUrl = builder.getSystems({
* bbox: [-105, 39, -104, 40],
* q: 'temperature',
* limit: 25,
* });
*
* // Get a specific system
* const systemUrl = builder.getSystem('sys-001');
*
* // List observations for a datastream with temporal filter
* const obsUrl = builder.getDatastreamObservations('ds-001', {
* phenomenonTime: { start: new Date('2024-01-01') },
* limit: 100,
* });
*
* // Create a new system (returns the POST URL)
* const createUrl = builder.createSystem();
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html — OGC API - Connected Systems Part 1
* @see https://docs.ogc.org/is/23-002/23-002.html — OGC API - Connected Systems Part 2
*/
/**
* Maps internal resource type keys to their OGC API URL path segments.
*
* NOTE: The `controlStreams → controlstreams` mapping affects all `@example`
* JSDoc blocks that show `/controlstreams` in output URLs. If this map is
* updated, the corresponding `@example` outputs must be updated to match.
*/
const RESOURCE_PATH_OVERRIDES: Readonly<Record<string, string>> = {
controlStreams: 'controlstreams',
};
function toUrlPathSegment(resourceType: string): string {
return RESOURCE_PATH_OVERRIDES[resourceType] ?? resourceType;
}
export default class CSAPIQueryBuilder {
/**
* The set of CSAPI resource types available on this collection,
* discovered from the collection's link relations via
* {@link scanCsapiLinks}.
*
* This reflects link scanning results, **not** actual server capability.
* Resources may exist at standard well-known paths even if they are not
* listed here. Consumers who supply `resourceUrls` to the constructor
* will also see those keys appear in this set.
*
* @see {@link scanCsapiLinks} for the recognized link conventions
*/
public readonly availableResources: ReadonlySet<CSAPIResourceType>;
/** Base URL for resource endpoints, derived from collection links. */
private baseUrl: string;
/**
* Optional map of resource type → absolute URL, supplied when the
* server advertises top-level (non-collection-scoped) resource URLs
* in the root API document. When present, `buildResourceUrl()` uses
* these absolute URLs instead of computing paths relative to the
* collection self link.
*/
private resourceUrls_: Map<string, string>;
/**
* @param collection_ - The OGC API collection metadata object.
* Must contain a `links` array; CSAPI resource availability is
* discovered from link relations matching `ogc-cs:{resourceType}`,
* plain resource names, or `items` links with resource hrefs.
* @param resourceUrls - Optional map of resource type names to absolute
* URLs. When provided (e.g., from the root API document), these URLs
* are used as the base for resource endpoints instead of the
* collection-scoped self link. This supports servers that expose
* CSAPI resources at the API root (e.g., `/api/systems`) rather than
* under a collection path (e.g., `/collections/{id}/systems`).
*
* @remarks
* Resource availability (`availableResources`) is populated by scanning
* link relations in the collection document via {@link scanCsapiLinks},
* **not** by probing the server with HTTP requests. Some servers
* (e.g., 52North CSA) do not advertise CSAPI resources via standard link
* relations, which results in an empty `availableResources` set and
* causes {@link assertResourceAvailable} to throw for every resource type.
*
* The `resourceUrls` parameter is the recommended workaround for such
* servers: when provided, its keys are merged into `availableResources`,
* and its values are used as endpoint base URLs.
*
* @example
* // For servers that don't advertise CSAPI links, provide explicit resource URLs:
* const resourceUrls = new Map(
* CSAPIResourceTypes.map(t => [t, `${baseUrl}/${t}`])
* );
* const builder = new CSAPIQueryBuilder(collection, resourceUrls);
*
* @see {@link scanCsapiLinks} for the link conventions recognized during discovery
* @see {@link assertResourceAvailable} for the validation that guards every query method
* @see https://docs.ogc.org/is/23-001/23-001.html
*/
constructor(
private collection_: CSAPICollectionRef,
resourceUrls?: Map<string, string>
) {
this.resourceUrls_ = resourceUrls ?? new Map();
this.baseUrl = this.extractBaseUrl();
this.availableResources = this.extractAvailableResources();
}
// ========================================
// PRIVATE HELPERS
// ========================================
/**
* Extracts the base URL for CSAPI resource endpoints from collection links.
* Looks for a self link or falls back to the first available href.
*/
private extractBaseUrl(): string {
const links = this.collection_.links;
if (!Array.isArray(links) || links.length === 0) {
return '';
}
const selfLink = links.find(
(l: { rel?: string; href?: string }) => l.rel === 'self'
);
if (selfLink?.href) {
return selfLink.href.replace(/\/$/, '');
}
// Fall back to first link with an href
const first = links.find(
(l: { href?: string }) => typeof l.href === 'string'
);
return first?.href?.replace(/\/$/, '') ?? '';
}
/**
* Discovers available CSAPI resource types from collection link relations.
*
* Recognizes three link relation conventions, in priority order:
*
* 1. **`ogc-cs:` prefixed** — `rel: "ogc-cs:systems"` → resource `"systems"`
* 2. **Plain resource name** — `rel: "systems"` where the value is a known
* {@link CSAPIResourceTypes} member → resource `"systems"`
* 3. **`items` with resource href** — `rel: "items"` where the `href` path
* ends with a known resource type name → resource extracted from href
*
* All three conventions populate the same Set. Duplicate entries are
* deduplicated automatically.
*
* @returns Set of available resource type names (e.g., 'systems', 'datastreams').
* @see https://docs.ogc.org/is/23-001/23-001.html
*/
private extractAvailableResources(): Set<CSAPIResourceType> {
const links = this.collection_.links;
if (!Array.isArray(links)) {
return new Set<CSAPIResourceType>();
}
// scanCsapiLinks emits only CSAPIResourceType keys (Conventions 2 & 3
// gate by `knownTypes`; Convention 1's `ogc-cs:` prefix is reserved for
// spec resource types). The cast aligns the static type with this
// runtime invariant without changing behavior.
return new Set(scanCsapiLinks(links).keys()) as Set<CSAPIResourceType>;
}
/**
* Core URL construction helper.
* Handles canonical, nested, and top-level resource endpoints.
*
* If the constructor received a `resourceUrls` map containing an
* absolute URL for the given `resourceType`, that URL is used as the
* base (top-level pattern). Otherwise, the URL is built relative to
* the collection self link (collection-scoped pattern).
*
* @param resourceType - Resource type (systems, deployments, etc.)
* @param id - Optional resource ID.
* @param subPath - Optional sub-path (subsystems, datastreams, etc.)
* @param options - Query parameters.
* @returns Fully constructed URL string.
* @see https://docs.ogc.org/is/23-001/23-001.html
*/
private buildResourceUrl(
resourceType: string,
id?: string,
subPath?: ResourceSubPath,
options?: QueryOptions
): string {
// Use the absolute resource URL when available (top-level pattern),
// otherwise fall back to collection-scoped base URL.
const topLevelUrl = this.resourceUrls_.get(resourceType);
const resourceBase = topLevelUrl
? topLevelUrl.replace(/\/+$/, '')
: `${this.baseUrl}/${toUrlPathSegment(resourceType)}`;
let url = resourceBase;
if (id) url += `/${encodeResourceId(id)}`;
if (subPath) url += `/${subPath}`;
return url + this.buildQueryString(options);
}
/**
* Builds a nested resource URL for servers that only expose child resources
* as sub-resources under their parent.
*
* Produces: `/{parentType}/{parentId}/{childSegment}/{childId}[/{subPath}][?query]`
*
* @param parentType - Parent resource type (e.g., 'controlStreams', 'datastreams').
* @param parentId - Parent resource identifier.
* @param childSegment - URL path segment for the child collection (e.g., 'commands').
* @param childId - Child resource identifier.
* @param subPath - Optional sub-path after the child ID (e.g., 'status').
* @param options - Optional query parameters.
* @returns Fully constructed nested URL string.
*
* @see https://docs.ogc.org/is/23-002/23-002.html — §7.5 (nested observations), §7.9 (nested commands)
*/
private buildNestedResourceUrl(
parentType: string,
parentId: string,
childSegment: string,
childId: string,
subPath?: ResourceSubPath,
options?: QueryOptions
): string {
const topLevelUrl = this.resourceUrls_.get(parentType);
const parentBase = topLevelUrl
? topLevelUrl.replace(/\/+$/, '')
: `${this.baseUrl}/${toUrlPathSegment(parentType)}`;
let url = `${parentBase}/${encodeResourceId(
parentId
)}/${childSegment}/${encodeResourceId(childId)}`;
if (subPath) url += `/${subPath}`;
return url + this.buildQueryString(options);
}
/**
* Serializes query options into a URL query string.
* Handles undefined/null skipping, array joining, temporal formatting,
* and bbox validation.
* @param options - Query parameter object.
* @returns Query string with leading '?', or empty string if no params.
*/
/**
* Temporal parameter keys that require ISO 8601 date/interval formatting.
* Used by `buildQueryString` to detect parameters needing `formatDateTimeParameter`.
* @see https://docs.ogc.org/is/23-001/23-001.html
* @see https://docs.ogc.org/is/23-002/23-002.html
*/
private static readonly TEMPORAL_KEYS: ReadonlySet<string> = new Set([
'datetime',
'phenomenonTime',
'resultTime',
'issueTime',
'executionTime',
]);
/**
* Maps TypeScript query-option property names to the OGC-spec wire names
* used in URL query strings. Properties not listed here are serialized
* as-is (the TypeScript name already matches the spec name).
*
* @see https://docs.ogc.org/is/23-001/23-001.html#clause-advanced-filtering
* @see https://docs.ogc.org/is/23-002/23-002.html#clause-advanced-filtering
*/
private static readonly PARAM_NAME_MAP: Readonly<Record<string, string>> = {
currentStatus: 'statusCode',
systemId: 'system',
observedPropertyId: 'observedProperty',
controlledPropertyId: 'controlledProperty',
foiId: 'foi',
procedureId: 'procedure',
};
private buildQueryString(options?: QueryOptions): string {
if (!options) return '';
const params = new URLSearchParams();
for (const [key, value] of Object.entries(options)) {
if (value === undefined || value === null) {
continue;
}
// Resolve the OGC-spec wire name (falls back to the TypeScript key).
const wireName = CSAPIQueryBuilder.PARAM_NAME_MAP[key] ?? key;
if (key === 'bbox') {
validateBbox(value);
params.append(wireName, value.join(','));
} else if (CSAPIQueryBuilder.TEMPORAL_KEYS.has(key)) {
params.append(wireName, formatDateTimeParameter(value));
} else if (key === 'limit') {
validateLimit(value);
params.append(wireName, String(value));
} else if (Array.isArray(value)) {
// Use plain join — URLSearchParams.append() handles percent-encoding.
// Previously used encodeArrayParameter() here, which pre-encoded values
// before URLSearchParams encoded them again (double-encoding bug F5).
params.append(wireName, value.join(','));
} else {
params.append(wireName, String(value));
}
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Validates that a resource type is available on this collection.
* @param resourceType - The resource type to validate.
* @throws {EndpointError} If the resource type is not available.
*
* @see The constructor's `resourceUrls` parameter for a workaround when
* a resource type exists on the server but was not discovered via links.
* @see {@link scanCsapiLinks} for the link conventions used during discovery
*/
private assertResourceAvailable(resourceType: string): void {
// Widen the `has` lookup to accept arbitrary string inputs from internal
// call sites; the public type of `availableResources` remains
// `ReadonlySet<CSAPIResourceType>`. See Phase 8 / Task A3 / Finding 023.
if (!(this.availableResources as ReadonlySet<string>).has(resourceType)) {
throw new EndpointError(
`Collection '${this.collection_.id}' does not support '${resourceType}' resource. ` +
`Available resources: ${Array.from(this.availableResources).join(
', '
)}`
);
}
}
/**
* Guards resource availability for collection-level requests and constructs
* the URL. When `id` is provided (per-resource request), the guard is
* skipped — per-ID methods do not require top-level endpoint discovery
* (see #100).
*
* @param resourceType - The resource type key (e.g., 'systems', 'datastreams').
* @param id - Optional resource ID. When absent, `assertResourceAvailable` is called.
* @param subPath - Optional sub-path (e.g., 'schema', 'observations').
* @param options - Optional query parameters.
* @returns Fully constructed URL string.
* @throws {EndpointError} If `id` is absent and the resource type is not available.
*/
private build(
resourceType: string,
id?: string,
subPath?: ResourceSubPath,
options?: QueryOptions
): string {
if (!id) {
this.assertResourceAvailable(resourceType);
}
return this.buildResourceUrl(resourceType, id, subPath, options);
}
// ========================================
// SYSTEMS METHODS
// ========================================
/**
* Returns the URL for listing systems.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param options - Optional query parameters for filtering systems.
* @returns URL string for the systems list endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystems({ limit: 10 });
* // => "https://example.com/collections/iot/systems?limit=10"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
getSystems(options?: SystemQueryOptions): string {
return this.build('systems', undefined, undefined, options);
}
/**
* Returns the URL for retrieving a single system by ID.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters.
* @returns URL string for the individual system endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystem('abc123');
* // => "https://example.com/collections/iot/systems/abc123"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
getSystem(id: string, options?: QueryOptions): string {
return this.build('systems', id, undefined, options);
}
/**
* Returns the URL for creating a new system (POST target).
*
* @returns URL string for the systems collection endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createSystem();
* // POST to => "https://example.com/collections/iot/systems"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
createSystem(): string {
return this.build('systems');
}
/**
* Returns the URL for updating an existing system (PUT target).
*
* @remarks
* **uid strictness (P4-F2):** The server rejects PUT requests with
* `400 "Feature UID cannot be changed"` if the `uid` in the request body
* does not byte-for-byte match the server-stored value. Consumers must
* preserve the exact `uid` from the original creation response or GET the
* resource before PUT to read the current uid.
*
* Recommended patterns:
* - **Preserve from creation:** Store the `uid` returned in the POST
* `Location` header or GET response; reuse it unchanged in the PUT body.
* - **GET-then-PUT:** Fetch the current resource, merge your changes into
* the fetched body, then PUT it back (see example below).
*
* @param id - The system resource identifier to update.
* @returns URL string for the individual system endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* // Safe update pattern: GET then PUT
* const getUrl = builder.getSystem('abc123');
* const current = await fetch(getUrl).then(r => r.json());
* current.properties.name = 'Updated Name';
* // uid is preserved from GET response
* await fetch(builder.updateSystem('abc123'), {
* method: 'PUT',
* headers: { 'Content-Type': 'application/geo+json' },
* body: JSON.stringify(current),
* });
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
updateSystem(id: string): string {
return this.build('systems', id);
}
/**
* Returns the URL for deleting a system (DELETE target).
*
* @param id - The system resource identifier to delete.
* @returns URL string for the individual system endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.deleteSystem('abc123');
* // DELETE to => "https://example.com/collections/iot/systems/abc123"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
deleteSystem(id: string): string {
return this.build('systems', id);
}
/**
* Returns the URL for retrieving a system's version history.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering history entries.
* @returns URL string for the system history endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemHistory('abc123', { limit: 5 });
* // => "https://example.com/collections/iot/systems/abc123/history?limit=5"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_history
*/
getSystemHistory(id: string, options?: QueryOptions): string {
return this.build('systems', id, 'history', options);
}
/**
* Returns the URL for listing subsystems of a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The parent system resource identifier.
* @param options - Optional query parameters. Supports `recursive` parameter
* to include nested subsystems at all levels.
* @returns URL string for the system's subsystems endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemSubsystems('abc123', { recursive: true });
* // => "https://example.com/collections/iot/systems/abc123/subsystems?recursive=true"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
getSystemSubsystems(id: string, options?: SystemQueryOptions): string {
return this.build('systems', id, 'subsystems', options);
}
/**
* Returns the URL for creating a subsystem within a parent system.
*
* The request body (not part of the URL) must describe the new child system.
*
* @param parentId - The parent system resource identifier.
* @returns URL string for the subsystem creation endpoint (POST).
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createSubsystem('sys-parent');
* // => "https://example.com/collections/iot/systems/sys-parent/subsystems"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_system_resources
*/
createSubsystem(parentId: string): string {
return this.build('systems', parentId, 'subsystems');
}
/**
* Returns the URL for listing datastreams associated with a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering datastreams.
* @returns URL string for the system's datastreams endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemDatastreams('abc123');
* // => "https://example.com/collections/iot/systems/abc123/datastreams"
* ```
*
* @see https://docs.ogc.org/is/23-002/23-002.html#_datastream_resources
*/
getSystemDatastreams(id: string, options?: DatastreamQueryOptions): string {
return this.build('systems', id, 'datastreams', options);
}
/**
* Returns the URL for creating a datastream within a system.
*
* OGC 23-002r1 §7.2 requires datastreams to be created as nested
* sub-resources of a System. The request body (not part of the URL)
* must include the result schema, observed properties, and system association.
*
* @param systemId - The parent system resource identifier.
* @returns URL string for the nested datastream creation endpoint (POST).
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createDatastreamForSystem('sys-001');
* // => "https://example.com/collections/iot/systems/sys-001/datastreams"
* ```
*
* @see https://docs.ogc.org/is/23-002/23-002.html#_datastream_resources
*/
createDatastreamForSystem(systemId: string): string {
return this.build('systems', systemId, 'datastreams');
}
/**
* Returns the URL for listing control streams associated with a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering control streams.
* @returns URL string for the system's control streams endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemControlStreams('abc123');
* // => "https://example.com/collections/iot/systems/abc123/controlstreams"
* ```
*
* @see https://docs.ogc.org/is/23-002/23-002.html#_controlstream_resources
*/
getSystemControlStreams(
id: string,
options?: ControlStreamQueryOptions
): string {
return this.build('systems', id, 'controlstreams', options);
}
/**
* Returns the URL for creating a control stream within a system.
*
* The request body (not part of the URL) must include the parameter schema
* and controlled properties.
*
* @param systemId - The parent system resource identifier.
* @returns URL string for the nested control stream creation endpoint (POST).
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createControlStreamForSystem('sys-001');
* // => "https://example.com/collections/iot/systems/sys-001/controlstreams"
* ```
*
* @see https://docs.ogc.org/is/23-002/23-002.html#_controlstream_resources
*/
createControlStreamForSystem(systemId: string): string {
return this.build('systems', systemId, 'controlstreams');
}
/**
* Returns the URL for listing sampling features associated with a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering sampling features.
* @returns URL string for the system's sampling features endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemSamplingFeatures('abc123');
* // => "https://example.com/collections/iot/systems/abc123/samplingFeatures"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_sampling_feature_resources
*/
getSystemSamplingFeatures(id: string, options?: QueryOptions): string {
return this.build('systems', id, 'samplingFeatures', options);
}
/**
* Returns the URL for creating a sampling feature within a system.
*
* The request body (not part of the URL) must include the feature type,
* geometry, and sampled feature link relation.
*
* @param systemId - The parent system resource identifier.
* @returns URL string for the nested sampling feature creation endpoint (POST).
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createSamplingFeatureForSystem('sys-001');
* // => "https://example.com/collections/iot/systems/sys-001/samplingFeatures"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_sampling_feature_resources
*/
createSamplingFeatureForSystem(systemId: string): string {
return this.build('systems', systemId, 'samplingFeatures');
}
/**
* Returns the URL for listing deployments associated with a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering deployments.
* @returns URL string for the system's deployments endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemDeployments('abc123');
* // => "https://example.com/collections/iot/systems/abc123/deployments"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_deployment_resources
*/
getSystemDeployments(id: string, options?: DeploymentQueryOptions): string {
return this.build('systems', id, 'deployments', options);
}
/**
* Returns the URL for listing procedures associated with a system.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param id - The system resource identifier.
* @param options - Optional query parameters for filtering procedures.
* @returns URL string for the system's procedures endpoint.
* @throws {EndpointError} If 'systems' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getSystemProcedures('abc123');
* // => "https://example.com/collections/iot/systems/abc123/procedures"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_procedure_resources
*/
getSystemProcedures(id: string, options?: QueryOptions): string {
return this.build('systems', id, 'procedures', options);
}
// ========================================
// DEPLOYMENTS METHODS
// ========================================
/**
* Returns the URL for querying the deployments collection.
*
* @remarks
* **Pagination:** server picks the default `limit` if unspecified; the
* consumer must follow `next` HATEOAS links from the response body to
* retrieve subsequent pages. See the Pagination section of the
* {@link CSAPIQueryBuilder} class docblock.
*
* @param options - Optional query parameters for filtering, pagination, bbox,
* datetime, sorting, and deployment-specific filters.
* @returns URL string for the deployments collection endpoint.
* @throws {EndpointError} If 'deployments' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getDeployments({ limit: 10, bbox: [-180, -90, 180, 90] });
* // => "https://example.com/collections/iot/deployments?limit=10&bbox=-180%2C-90%2C180%2C90"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_deployment_resources
*/
getDeployments(options?: DeploymentQueryOptions): string {
return this.build('deployments', undefined, undefined, options);
}
/**
* Returns the URL for retrieving a single deployment by ID.
*
* @param id - The deployment resource identifier.
* @param options - Optional query parameters.
* @returns URL string for the individual deployment endpoint.
* @throws {EndpointError} If 'deployments' is not available on this collection.
*
* @example
* ```ts
* const url = builder.getDeployment('dep-001');
* // => "https://example.com/collections/iot/deployments/dep-001"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_deployment_resources
*/
getDeployment(id: string, options?: QueryOptions): string {
return this.build('deployments', id, undefined, options);
}
/**
* Returns the URL for creating a new deployment (POST target).
*
* @returns URL string for the deployments collection endpoint.
* @throws {EndpointError} If 'deployments' is not available on this collection.
*
* @example
* ```ts
* const url = builder.createDeployment();
* // POST to => "https://example.com/collections/iot/deployments"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_deployment_resources
*/
createDeployment(): string {
return this.build('deployments');
}
/**
* Returns the URL for updating an existing deployment (PUT target).
*
* @remarks
* **uid strictness (P4-F2):** The server rejects PUT requests with
* `400 "Feature UID cannot be changed"` if the `uid` in the request body
* does not byte-for-byte match the server-stored value. Consumers must
* preserve the exact `uid` from the original creation response or GET the
* resource before PUT to read the current uid.
* See {@link updateSystem} for a full GET-then-PUT example.
*
* @param id - The deployment resource identifier to update.
* @returns URL string for the individual deployment endpoint.
* @throws {EndpointError} If 'deployments' is not available on this collection.
*
* @example
* ```ts
* const url = builder.updateDeployment('dep-001');
* // PUT to => "https://example.com/collections/iot/deployments/dep-001"
* ```
*
* @see https://docs.ogc.org/is/23-001/23-001.html#_deployment_resources
*/
updateDeployment(id: string): string {
return this.build('deployments', id);
}
/**
* Returns the URL for deleting a deployment (DELETE target).
*
* @param id - The deployment resource identifier to delete.
* @returns URL string for the individual deployment endpoint.
* @throws {EndpointError} If 'deployments' is not available on this collection.
*
* @example