Summary
The library provides no utility functions for resolving @link or @id cross-reference fields into usable resources or URLs. Even once #103, #108, and #109 are resolved and @link fields survive parsing, consumers will have a typed CSAPIResourceRef object (e.g., { href: "http://.../procedures/abc123", uid: "urn:...", title: "..." }) but no library support for:
- Fetching the referenced resource
- Resolving relative
href values against the API root - Extracting the resource type and ID from an
href - Falling back to
@link when server navigation endpoints fail
Every consumer must independently implement these operations — as ogc-csapi-explorer had to do with its tryLinkFallback() workaround (~105 lines).
What Exists Today
scanCsapiLinks() — Collection-Level Only
scanCsapiLinks() (helpers.ts ~L131–172) scans the document-level HATEOAS links[] array for resource type navigation URLs. It does not operate on inline @link properties within individual resources:
// helpers.ts ~L131 — operates on collection/root document linksexportfunctionscanCsapiLinks(links: Array<{rel?: string;href?: string}>): Map{// Looks for rel="ogc-cs:systems", rel="items", etc.// Does NOT handle systemKind@link, platform@link, etc.}CSAPIQueryBuilder — Server-Dependent Navigation
CSAPIQueryBuilder (url_builder.ts, 2329 lines) provides complete URL construction for server-side navigation endpoints:
getSystemProcedures(systemId: string): string// → /systems/{id}/proceduresgetSystemDeployments(systemId: string): string// → /systems/{id}/deploymentsgetDeploymentSystems(deploymentId: string): string// → /deployments/{id}/systemsgetProcedureSystems(procedureId: string): string// → /procedures/{id}/systems// ... etc.These work only when the server implements the endpoints. OSH SensorHub returns 400 Bad Request for all cross-resource navigation endpoints, making these methods useless for association discovery on that server.
Gap: No @link Utilities
There is no code anywhere in the library that:
- Parses an
@link object's href into a resource type + ID - Resolves a relative
href against the API root URL - Fetches a resource from an
@link reference - Extracts all
@link / @id fields from a resource - Falls back from failed navigation endpoints to
@link data
Why This Matters
1. @link Is the Universal Fallback
Server-side navigation (the CSAPIQueryBuilder approach) requires the server to implement every cross-resource endpoint. The OGC spec defines many optional endpoints, and no server implements all of them. @link fields are the baseline mechanism — they're embedded in the resource JSON by servers that provide them, regardless of which navigation endpoints are available.
A library that can construct navigation URLs but cannot resolve @link references is missing half of the association-discovery story.
2. Real-World Impact: ogc-csapi-explorer
The ogc-csapi-explorer had to implement tryLinkFallback() in ResourceDetail.vue (commit ad06b52) — approximately 105 lines that manually:
- Check if raw resource properties contain
systemKind@link, platform@link, deployedSystems@link, etc. - Validate the
href is a string - Fetch the referenced resource directly via the
href - Parse and display the result
// ResourceDetail.vue — ~105 lines of boilerplate that should be in the libraryasyncfunctiontryLinkFallback(resource: any,resourceType: string){constprops=resource.properties??resource;if(props['systemKind@link']?.href){constresp=awaitfetch(props['systemKind@link'].href,{ headers });if(resp.ok){constprocedure=awaitresp.json();// Display procedure info}}if(props['platform@link']?.href){constresp=awaitfetch(props['platform@link'].href,{ headers });if(resp.ok){constplatform=awaitresp.json();// Display platform info}}// ... repeat for every @link field type}This pattern will need to be independently reimplemented by every library consumer that encounters a server with incomplete navigation endpoint support.
3. Graceful Degradation Pattern
The ideal consumer workflow is:
1. Try server-side navigation → /systems/{id}/deployments
2. If 4xx/5xx → fall back to @link fields from the parsed resource
3. If no @link fields → report "association unknown"
The library provides step 1 (CSAPIQueryBuilder) but not step 2. Consumers must implement step 2 from scratch.
Proposed Utilities
CSAPIResourceRef Type
(Defined in #108)
exportinterfaceCSAPIResourceRef{href: string;uid?: string;title?: string;rt?: string;}resolveResourceRef() — Fetch a Referenced Resource
/** * Fetches the resource referenced by a `@link` property. * * Handles both absolute and relative hrefs by resolving against the * provided API root URL. * * @param ref - The `@link` reference object (e.g., from `systemKindLink`) * @param apiRootUrl - The CS API root URL for resolving relative hrefs * @param fetchOptions - Optional fetch configuration (headers, auth, etc.) * @returns The fetched resource as parsed JSON */exportasyncfunctionresolveResourceRef(ref: CSAPIResourceRef,apiRootUrl: string,fetchOptions?: RequestInit,): Promise{consturl=newURL(ref.href,apiRootUrl).toString();constresponse=awaitfetch(url,fetchOptions);if(!response.ok){thrownewError(`Failed to resolve @link: ${response.status}${url}`);}returnresponse.json();}parseResourceRefHref() — Extract Type and ID from href
/** * Extracts the resource type and ID from a `@link` href. * * Handles hrefs like: * - "http://server/api/procedures/abc123" → { type: 'procedures', id: 'abc123' } * - "/api/systems/xyz" → { type: 'systems', id: 'xyz' } * * @param href - The href string from a `@link` object * @returns Parsed resource type and ID, or null if the href doesn't match a known pattern */exportfunctionparseResourceRefHref(href: string,): {resourceType: string;resourceId: string}|null{constsegments=newURL(href,'http://placeholder').pathname.replace(/\/+$/,'').split('/');constid=segments.pop();consttype=segments.pop();if(!id||!type)returnnull;return{resourceType: type,resourceId: id};}extractCrossReferences() — Collect All @link / @id Fields
/** * Extracts all `@link` and `@id` cross-reference fields from a raw * resource object. * * Useful for discovering what associations a server provided, regardless * of whether the typed model includes them. * * @param raw - Raw JSON object from the server * @returns Map of field name → value (CSAPIResourceRef for @link, string for @id) */exportfunctionextractCrossReferences(raw: Record,): Map{constrefs=newMap();constprops=(raw.propertiesasRecord)??raw;for(const[key,value]ofObject.entries(props)){if(key.endsWith('@link')&&typeofvalue==='object'&&value!==null){constobj=valueasRecord;if(typeofobj.href==='string'){refs.set(key,{href: obj.href,
...(typeofobj.uid==='string' ? {uid: obj.uid} : {}),
...(typeofobj.title==='string' ? {title: obj.title} : {}),
...(typeofobj.rt==='string' ? {rt: obj.rt} : {}),});}}if(key.endsWith('@id')&&typeofvalue==='string'){refs.set(key,value);}}returnrefs;}resolveWithLinkFallback() — Try Navigation, Fall Back to @link
/** * Attempts server-side navigation first, then falls back to `@link` data * if the server returns an error. * * This is the recommended pattern for consumers that need to discover * associations on servers with varying endpoint support. * * @param navigationUrl - The server-side navigation endpoint URL * @param linkRef - The `@link` reference to use as fallback (may be undefined) * @param apiRootUrl - The CS API root URL for resolving relative hrefs * @param fetchOptions - Optional fetch configuration * @returns The fetched resource(s), or null if both approaches fail */exportasyncfunctionresolveWithLinkFallback(navigationUrl: string,linkRef: CSAPIResourceRef|undefined,apiRootUrl: string,fetchOptions?: RequestInit,): Promise{// Step 1: Try server-side navigationtry{constresponse=awaitfetch(navigationUrl,fetchOptions);if(response.ok)returnresponse.json();}catch{/* fall through */}// Step 2: Fall back to @linkif(linkRef){try{returnawaitresolveResourceRef(linkRef,apiRootUrl,fetchOptions);}catch{/* fall through */}}returnnull;}Design Notes
OGC Spec References
- OGC 23-001 §16 — JSON encoding for Part 1 resources, defines
@link inline property format: { href, uid?, title?, rt? } - OGC 23-002 §16.1 — JSON encoding for Part 2 resources, defines
@id inline property format (scalar string) - OGC 23-001 §8.3, §8.5, §8.9 — Resource association tables defining which
@link fields exist per resource type
Related Issues
Summary
The library provides no utility functions for resolving
@linkor@idcross-reference fields into usable resources or URLs. Even once #103, #108, and #109 are resolved and@linkfields survive parsing, consumers will have a typedCSAPIResourceRefobject (e.g.,{ href: "http://.../procedures/abc123", uid: "urn:...", title: "..." }) but no library support for:hrefvalues against the API roothref@linkwhen server navigation endpoints failEvery consumer must independently implement these operations — as ogc-csapi-explorer had to do with its
tryLinkFallback()workaround (~105 lines).What Exists Today
scanCsapiLinks()— Collection-Level OnlyscanCsapiLinks()(helpers.ts ~L131–172) scans the document-level HATEOASlinks[]array for resource type navigation URLs. It does not operate on inline@linkproperties within individual resources:CSAPIQueryBuilder— Server-Dependent NavigationCSAPIQueryBuilder(url_builder.ts, 2329 lines) provides complete URL construction for server-side navigation endpoints:These work only when the server implements the endpoints. OSH SensorHub returns
400 Bad Requestfor all cross-resource navigation endpoints, making these methods useless for association discovery on that server.Gap: No
@linkUtilitiesThere is no code anywhere in the library that:
@linkobject'shrefinto a resource type + IDhrefagainst the API root URL@linkreference@link/@idfields from a resource@linkdataWhy This Matters
1.
@linkIs the Universal FallbackServer-side navigation (the
CSAPIQueryBuilderapproach) requires the server to implement every cross-resource endpoint. The OGC spec defines many optional endpoints, and no server implements all of them.@linkfields are the baseline mechanism — they're embedded in the resource JSON by servers that provide them, regardless of which navigation endpoints are available.A library that can construct navigation URLs but cannot resolve
@linkreferences is missing half of the association-discovery story.2. Real-World Impact: ogc-csapi-explorer
The ogc-csapi-explorer had to implement
tryLinkFallback()inResourceDetail.vue(commit ad06b52) — approximately 105 lines that manually:systemKind@link,platform@link,deployedSystems@link, etc.hrefis a stringhrefThis pattern will need to be independently reimplemented by every library consumer that encounters a server with incomplete navigation endpoint support.
3. Graceful Degradation Pattern
The ideal consumer workflow is:
The library provides step 1 (
CSAPIQueryBuilder) but not step 2. Consumers must implement step 2 from scratch.Proposed Utilities
CSAPIResourceRefType(Defined in #108)
resolveResourceRef()— Fetch a Referenced ResourceparseResourceRefHref()— Extract Type and ID from hrefextractCrossReferences()— Collect All@link/@idFieldsresolveWithLinkFallback()— Try Navigation, Fall Back to@linkDesign Notes
src/ogc-api/csapi/link-resolution.ts(parallels existinglink-utils.tswhich handles HATEOAS links)@linkassociation properties #108/Part 1extractCSAPIFeature()silently drops all@linkproperties during parsing #109:extractCrossReferences()operates on raw JSON and can be used immediately, even before interfaces/parsers are updatedparseResourceRefHref()without the fetch-based functionsfetchAPI andURLconstructorCSAPIQueryBuilderpattern: Just as the query builder constructs navigation URLs, these utilities resolve@linkURLs — two complementary approaches to association discoveryOGC Spec References
@linkinline property format:{ href, uid?, title?, rt? }@idinline property format (scalar string)@linkfields exist per resource typeRelated Issues
@linkassociation properties #108 — Part 1 interfaces: addsCSAPIResourceReftype and@linkfields to interfacesextractCSAPIFeature()silently drops all@linkproperties during parsing #109 — Part 1 parser: extracts@linkproperties inextractCSAPIFeature()@id/@linkfields in Part 2 parse functions@linkgaps in the librarytryLinkFallback()workaround that this utility would replace