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 links
export function scanCsapiLinks(
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}/procedures
getSystemDeployments(systemId: string): string // → /systems/{id}/deployments
getDeploymentSystems(deploymentId: string): string // → /deployments/{id}/systems
getProcedureSystems(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 library
async function tryLinkFallback(resource: any, resourceType: string) {
const props = resource.properties ?? resource;
if (props['systemKind@link']?.href) {
const resp = await fetch(props['systemKind@link'].href, { headers });
if (resp.ok) {
const procedure = await resp.json();
// Display procedure info
}
}
if (props['platform@link']?.href) {
const resp = await fetch(props['platform@link'].href, { headers });
if (resp.ok) {
const platform = await resp.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)
export interface CSAPIResourceRef {
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
*/
export async function resolveResourceRef(
ref: CSAPIResourceRef,
apiRootUrl: string,
fetchOptions?: RequestInit,
): Promise {
const url = new URL(ref.href, apiRootUrl).toString();
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`Failed to resolve @link: ${response.status} ${url}`);
}
return response.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
*/
export function parseResourceRefHref(
href: string,
): { resourceType: string; resourceId: string } | null {
const segments = new URL(href, 'http://placeholder').pathname
.replace(/\/+$/, '')
.split('/');
const id = segments.pop();
const type = segments.pop();
if (!id || !type) return null;
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)
*/
export function extractCrossReferences(
raw: Record,
): Map {
const refs = new Map();
const props = (raw.properties as Record) ?? raw;
for (const [key, value] of Object.entries(props)) {
if (key.endsWith('@link') && typeof value === 'object' && value !== null) {
const obj = value as Record;
if (typeof obj.href === 'string') {
refs.set(key, {
href: obj.href,
...(typeof obj.uid === 'string' ? { uid: obj.uid } : {}),
...(typeof obj.title === 'string' ? { title: obj.title } : {}),
...(typeof obj.rt === 'string' ? { rt: obj.rt } : {}),
});
}
}
if (key.endsWith('@id') && typeof value === 'string') {
refs.set(key, value);
}
}
return refs;
}
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
*/
export async function resolveWithLinkFallback(
navigationUrl: string,
linkRef: CSAPIResourceRef | undefined,
apiRootUrl: string,
fetchOptions?: RequestInit,
): Promise {
// Step 1: Try server-side navigation
try {
const response = await fetch(navigationUrl, fetchOptions);
if (response.ok) return response.json();
} catch { /* fall through */ }
// Step 2: Fall back to @link
if (linkRef) {
try {
return await resolveResourceRef(linkRef, apiRootUrl, fetchOptions);
} catch { /* fall through */ }
}
return null;
}
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