Skip to content

DEFERRED — No @link / @id resolution utilities for cross-resource reference following #110

@Sam-Bolling

Description

@Sam-Bolling

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:

  1. Parses an @link object's href into a resource type + ID
  2. Resolves a relative href against the API root URL
  3. Fetches a resource from an @link reference
  4. Extracts all @link / @id fields from a resource
  5. 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:

  1. Check if raw resource properties contain systemKind@link, platform@link, deployedSystems@link, etc.
  2. Validate the href is a string
  3. Fetch the referenced resource directly via the href
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions