-
Notifications
You must be signed in to change notification settings - Fork 1
DEFERRED — No @link / @id resolution utilities for cross-resource reference following #110
Description
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
hrefvalues against the API root - Extracting the resource type and ID from an
href - Falling back to
@linkwhen 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
@linkobject'shrefinto a resource type + ID - Resolves a relative
hrefagainst the API root URL - Fetches a resource from an
@linkreference - Extracts all
@link/@idfields from a resource - Falls back from failed navigation endpoints to
@linkdata
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
hrefis 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
- File location: New file
src/ogc-api/csapi/link-resolution.ts(parallels existinglink-utils.tswhich handles HATEOAS links) - Independent of Part 1 (GeoJSON) TypeScript interfaces omit all
@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 updated - Composable: Each utility is independently useful — consumers can use just
parseResourceRefHref()without the fetch-based functions - No new dependencies: Uses only the standard
fetchAPI andURLconstructor - Mirrors
CSAPIQueryBuilderpattern: Just as the query builder constructs navigation URLs, these utilities resolve@linkURLs — two complementary approaches to association discovery
OGC Spec References
- OGC 23-001 §16 — JSON encoding for Part 1 resources, defines
@linkinline property format:{ href, uid?, title?, rt? } - OGC 23-002 §16.1 — JSON encoding for Part 2 resources, defines
@idinline property format (scalar string) - OGC 23-001 §8.3, §8.5, §8.9 — Resource association tables defining which
@linkfields exist per resource type
Related Issues
- Part 1 (GeoJSON) TypeScript interfaces omit all
@linkassociation properties #108 — Part 1 interfaces: addsCSAPIResourceReftype and@linkfields to interfaces - Part 1
extractCSAPIFeature()silently drops all@linkproperties during parsing #109 — Part 1 parser: extracts@linkproperties inextractCSAPIFeature() - Parsed Part 2 models discard all cross-reference fields — parent resource navigation impossible without raw JSON #103 — Part 2 parsers: preserves
@id/@linkfields in Part 2 parse functions - Gap analysis report — Full audit documenting all
@linkgaps in the library - ogc-csapi-explorer ad06b52 — Explorer
tryLinkFallback()workaround that this utility would replace