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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions