Skip to content

Create server-side helpers for Node.js CSAPI server implementation #66

@Sam-Bolling

Description

@Sam-Bolling

Problem

The ogc-client-CSAPI library is a client-only implementation focused on consuming OGC API - Connected Systems services. While it provides excellent client-side capabilities (98% OGC compliance), it does not provide server-side helpers for developers building OGC CSAPI servers in Node.js.

Current State:

  • Client Library: Complete implementation for consuming CSAPI services
  • Type Definitions: Comprehensive TypeScript types for all resources (4,159 lines)
  • Validators: GeoJSON, SWE Common, SensorML validators (73%+ coverage)
  • Parsers: Resource parsers with 97.63% test coverage
  • Server Helpers: Not implemented

Missing Server-Side Capabilities:

1. Request Validation Middleware

  • Missing: Express/Fastify middleware to validate incoming requests
  • Needed: Query parameter validation, body schema validation, authorization
  • Impact: Server developers must implement OGC CSAPI validation from scratch

2. Response Builders

  • Missing: Helper functions to build OGC-compliant responses
  • Needed: GeoJSON feature collections, link headers, pagination, conformance declarations
  • Impact: Server developers must manually construct OGC-compliant JSON structures

3. URL Route Handlers

  • Missing: Express/Fastify route helpers for CSAPI endpoints
  • Needed: Route patterns, parameter extraction, RESTful conventions
  • Impact: Server developers must define 50+ endpoint routes manually

4. Database Integration Helpers

  • Missing: Query builders for common CSAPI patterns
  • Needed: Spatial queries, temporal queries, pagination, filtering
  • Impact: Server developers must implement query logic for each database

5. OpenAPI Specification Generator

  • Missing: Generate OpenAPI spec from server configuration
  • Needed: Automatic API documentation, interactive testing (Swagger UI)
  • Impact: Server developers must manually maintain OpenAPI YAML files

6. Conformance Class Helpers

  • Missing: Conformance class registration and declaration
  • Needed: /conformance endpoint builder, capability negotiation
  • Impact: Servers cannot properly advertise OGC compliance

Real-World Development Scenario:

Without Server Helpers (Current):

// Developer must implement everything from scratch
import express from 'express';

const app = express();

// Manually define all routes
app.get('/csapi/systems', async (req, res) => {
  // Manually validate query parameters
  const limit = parseInt(req.query.limit as string) || 10;
  const bbox = req.query.bbox as string;
  
  // Manually parse bbox
  let bboxCoords: number[] | undefined;
  if (bbox) {
    bboxCoords = bbox.split(',').map(parseFloat);
    if (bboxCoords.length !== 4) {
      return res.status(400).json({ error: 'Invalid bbox' });
    }
  }
  
  // Manually build database query
  const query = db.select('*').from('systems').limit(limit);
  if (bboxCoords) {
    query.whereBetween('lon', [bboxCoords[0], bboxCoords[2]]);
    query.whereBetween('lat', [bboxCoords[1], bboxCoords[3]]);
  }
  
  const systems = await query;
  
  // Manually build GeoJSON FeatureCollection
  const features = systems.map(system => ({
    type: 'Feature',
    id: system.id,
    geometry: system.geometry,
    properties: {
      name: system.name,
      description: system.description,
      // ... manually map all properties
    }
  }));
  
  // Manually build response with links
  const response = {
    type: 'FeatureCollection',
    features,
    links: [
      { rel: 'self', href: req.originalUrl },
      { rel: 'collection', href: '/csapi/systems' },
      // ... manually build all links
    ],
    numberMatched: systems.length,
    numberReturned: features.length,
  };
  
  res.json(response);
});

// Repeat for 50+ more endpoints...
// - /systems/{id}
// - /systems/{id}/history
// - /systems/{id}/datastreams
// - /deployments
// - /procedures
// - /datastreams
// - /observations
// - ... and 40+ more

With Server Helpers (Desired):

import express from 'express';
import { CSAPIServer, createSystemsRouter, validateCSAPIRequest } from 'ogc-client-csapi/server';

const app = express();

// Initialize CSAPI server with configuration
const csapi = new CSAPIServer({
  baseUrl: 'https://example.com/csapi',
  title: 'My Sensor Network',
  database: db,
});

// Automatic request validation middleware
app.use(validateCSAPIRequest());

// Auto-generate all system routes
app.use('/csapi', createSystemsRouter({
  // Provide data access handlers
  async findSystems(query) {
    return db.systems.find(query);
  },
  async getSystem(id) {
    return db.systems.findById(id);
  },
  async createSystem(data) {
    return db.systems.create(data);
  },
  // ... handlers auto-map to OGC endpoints
}));

// Routes automatically created:
// - GET /csapi/systems (with query param validation)
// - GET /csapi/systems/{id}
// - POST /csapi/systems (with body validation)
// - PUT /csapi/systems/{id}
// - PATCH /csapi/systems/{id}
// - DELETE /csapi/systems/{id}
// - GET /csapi/systems/{id}/history
// - GET /csapi/systems/{id}/datastreams
// ... all with proper error handling, validation, response formatting

// Auto-generate OpenAPI spec
app.get('/csapi/api', (req, res) => {
  res.json(csapi.generateOpenAPISpec());
});

// Auto-generate conformance endpoint
app.get('/csapi/conformance', (req, res) => {
  res.json(csapi.getConformance());
});

Development Time Comparison:

Task Without Helpers With Helpers Time Saved
Define all routes 8-12 hours 1 hour 90%
Request validation 6-8 hours 0 (automatic) 100%
Response formatting 4-6 hours 0 (automatic) 100%
Error handling 3-4 hours 0 (automatic) 100%
OpenAPI spec 8-10 hours 0 (automatic) 100%
Testing validation 6-8 hours 1 hour 87%
Total 35-48 hours 2 hours 95%

Ecosystem Impact:

Current Situation:

  • 1 client library (ogc-client-CSAPI) - Excellent ✅
  • 0 server libraries for Node.js - Missing ❌
  • Developers must build servers from scratch
  • Inconsistent implementations across projects
  • High barrier to entry for OGC CSAPI adoption

With Server Helpers:

  • 1 client library ✅
  • 1 server library ✅
  • Rapid server development (hours vs weeks)
  • Consistent OGC-compliant implementations
  • Lower barrier to entry
  • Stronger ecosystem adoption

Context

This issue was identified during the comprehensive validation conducted January 27-28, 2026.

Related Validation Issues: #20 (OGC Standards Compliance)

Work Item ID: 43 from Remaining Work Items

Repository: https://github.com/OS4CSAPI/ogc-client-CSAPI

Validated Commit: a71706b9592cad7a5ad06e6cf8ddc41fa5387732


Detailed Findings

1. Existing Client Infrastructure Can Be Reused (Issue #20)

From Issue #20 Validation Report:

The OGC-Client-CSAPI implementation demonstrates excellent compliance with OGC API - Connected Systems standards (98% compliance).

Key Assets for Server-Side Reuse:

Type Definitions (4,159 lines):

  • ✅ All CSAPI resource types defined
  • ✅ GeoJSON feature types (7 core + 2 Part 2)
  • ✅ SWE Common types (12 files)
  • ✅ SensorML types (6 files)
  • ✅ Query option interfaces (10 resource types)

Validators (97%+ coverage):

  • ✅ GeoJSON validator (61 tests, 97.4% coverage)
  • ✅ SWE Common validator (50 tests, 100% coverage)
  • ✅ SensorML validator (exists, integration pending)

URL Patterns (from navigator.ts):

  • ✅ All 50+ OGC CSAPI endpoint patterns defined
  • ✅ Query parameter handling
  • ✅ Resource relationship patterns

Evidence: Server helpers can directly leverage these existing assets, avoiding duplication.


2. Comprehensive Endpoint Patterns Available (Issue #20)

From Issue #20 Validation Report:

Part 1 (Core) - 85/85 Compliance:

GET    /systems                    ✅
GET    /systems/{id}               ✅
POST   /systems                    ✅
PUT    /systems/{id}               ✅
PATCH  /systems/{id}               ✅
DELETE /systems/{id}               ✅
GET    /systems/{id}/history       ✅
GET    /systems/{id}/subsystems    ✅
GET    /systems/{id}/procedures    ✅
GET    /systems/{id}/deployments   ✅
GET    /systems/{id}/samplingFeatures  ✅

# Same pattern for 7 core resources

Part 2 (Advanced) - 85/85 Compliance:

GET    /datastreams/{id}/observations       ✅
POST   /datastreams/{id}/observations       ✅
GET    /controlStreams/{id}/commands        ✅
POST   /controlStreams/{id}/commands        ✅
GET    /commands/{id}/status                ✅
GET    /commands/{id}/result                ✅
POST   /systems/{id}/feasibility            ✅
GET    /systems/{id}/feasibility/{requestId}  ✅

Server Helper Opportunity: All endpoint patterns are known and validated. Can auto-generate route handlers for Express/Fastify.


3. Query Parameter Handling (Issue #20)

From Issue #20 Validation Report:

Systems Query Parameters:

export interface SystemsQueryOptions {
  limit?: number;                    // Pagination
  bbox?: BoundingBox;                // Spatial filter
  datetime?: DateTimeParameter;      // Temporal filter
  q?: string;                        // Full-text search
  id?: string | string[];            // ID filter
  geom?: string;                     // WKT geometry filter
  foi?: string | string[];           // Feature of interest
  parent?: string;                   // Hierarchical filter
  recursive?: boolean;               // Recursive subsystems
  procedure?: string;                // Related procedures
  observedProperty?: string;         // Property filter
  controlledProperty?: string;       // Property filter
  systemKind?: string;               // Type filter
  select?: string;                   // Property path
}

All 10 resource types have similar comprehensive query interfaces.

Server Helper Opportunity:

  • Request validation middleware can use these interfaces
  • Query builder can translate options to database queries
  • Already validated and tested (186 tests for Navigator)

4. Response Format Patterns (Issue #20)

From Issue #20 Validation Report:

GeoJSON Feature Collections:

export interface SystemFeatureCollection extends CSAPIFeatureCollection<SystemFeature> {
  type: 'FeatureCollection';
  features: SystemFeature[];
  links: Link[];
  numberMatched?: number;
  numberReturned: number;
}

Required Link Relations:

links: [
  { rel: 'self', href: '...' },
  { rel: 'collection', href: '...' },
  { rel: 'next', href: '...' },  // Pagination
  { rel: 'prev', href: '...' },  // Pagination
]

Server Helper Opportunity:

  • Response builder can auto-generate feature collections
  • Link builder can construct proper rel/href pairs
  • Pagination helper can add next/prev links

5. Existing Parsers Can Inform Serializers

From Issue #10 Validation (via Issue #20 cross-reference):

Parsers Implemented:

  • ObservationParser (79 tests, 97.63% coverage)
  • SystemParser
  • DeploymentParser
  • ProcedureParser
  • DatastreamParser
  • ControlStreamParser

Server Helper Opportunity:

  • Serializers are inverse of parsers
  • Parser logic shows expected structure
  • Can create serialize() methods that mirror parse() methods

Example:

// Current (client-side)
class ObservationParser {
  parse(json: unknown): ParseResult<ObservationFeature> {
    // Validate and parse JSON → ObservationFeature
  }
}

// Future (server-side)
class ObservationSerializer {
  serialize(observation: ObservationData): ObservationFeature {
    // Convert database record → OGC-compliant ObservationFeature
  }
}

6. Conformance Class Patterns (Issue #20)

From Issue #20 Validation Report:

Conformance Detection:

// endpoint.ts, Lines 296-302
get hasConnectedSystems(): Promise<boolean> {
  return this.conformance.then(checkHasConnectedSystems);
}

// info.ts, Lines 105-117
export function checkHasConnectedSystems([conformance]: [ConformanceClass[]]): boolean {
  return (
    conformance.indexOf('http://www.opengis.net/spec/ogcapi-connected-systems-1/1.0/conf/core') > -1 ||
    conformance.indexOf('http://www.opengis.net/spec/ogcapi-cs-part1/1.0/conf/core') > -1
  );
}

Server Helper Opportunity:

  • Server needs to advertise conformance classes
  • Helper can build /conformance endpoint response
  • Track which features are implemented

Conformance Classes:

Part 1 (Core):
- http://www.opengis.net/spec/ogcapi-cs-part1/1.0/conf/core
- http://www.opengis.net/spec/ogcapi-cs-part1/1.0/conf/geojson
- http://www.opengis.net/spec/ogcapi-cs-part1/1.0/conf/sensorml

Part 2 (Advanced):
- http://www.opengis.net/spec/ogcapi-cs-part2/1.0/conf/datastreams
- http://www.opengis.net/spec/ogcapi-cs-part2/1.0/conf/observations
- http://www.opengis.net/spec/ogcapi-cs-part2/1.0/conf/commands
- http://www.opengis.net/spec/ogcapi-cs-part2/1.0/conf/system-events
- http://www.opengis.net/spec/ogcapi-cs-part2/1.0/conf/feasibility

Proposed Solution

1. Server-Side Package Structure

Create new package: ogc-client-csapi/server (or separate ogc-server-csapi package)

src/
  server/
    index.ts                    # Main exports
    
    core/
      csapi-server.ts           # Main server class
      conformance.ts            # Conformance class registry
      openapi-generator.ts      # OpenAPI spec generator
    
    middleware/
      validate-request.ts       # Request validation middleware
      error-handler.ts          # OGC-compliant error responses
      cors.ts                   # CORS headers
      auth.ts                   # Authentication helpers
    
    routers/
      systems-router.ts         # Systems CRUD router factory
      deployments-router.ts     # Deployments router factory
      procedures-router.ts      # Procedures router factory
      datastreams-router.ts     # Datastreams router factory
      observations-router.ts    # Observations router factory
      commands-router.ts        # Commands router factory
      # ... routers for all 10 resource types
    
    builders/
      response-builder.ts       # Build OGC-compliant responses
      link-builder.ts           # Build link arrays
      pagination-builder.ts     # Pagination helpers
      collection-builder.ts     # FeatureCollection builder
    
    serializers/
      system-serializer.ts      # System → SystemFeature
      observation-serializer.ts # Observation → ObservationFeature
      # ... serializers for all resource types
    
    query/
      query-builder.ts          # Translate OGC params to DB queries
      spatial-query.ts          # Spatial query helpers (bbox, geom)
      temporal-query.ts         # Temporal query helpers (datetime)
      filter-builder.ts         # Build WHERE clauses
    
    adapters/
      database-adapter.ts       # Database interface
      postgres-adapter.ts       # PostgreSQL/PostGIS adapter
      mongodb-adapter.ts        # MongoDB adapter
      sqlite-adapter.ts         # SQLite adapter
    
tests/
  server/
    integration/
      express-server.spec.ts    # Test Express integration
      fastify-server.spec.ts    # Test Fastify integration
    unit/
      # Unit tests for each component

2. Core Server Class

CSAPIServer Class:

// src/server/core/csapi-server.ts

export interface CSAPIServerConfig {
  baseUrl: string;
  title: string;
  description?: string;
  database: DatabaseAdapter;
  conformance?: ConformanceClass[];
  enableCORS?: boolean;
  enableAuth?: boolean;
  authProvider?: AuthProvider;
}

export class CSAPIServer {
  private config: CSAPIServerConfig;
  private conformance: ConformanceRegistry;
  
  constructor(config: CSAPIServerConfig) {
    this.config = config;
    this.conformance = new ConformanceRegistry();
    
    // Register default conformance classes
    this.conformance.register('http://www.opengis.net/spec/ogcapi-cs-part1/1.0/conf/core');
  }
  
  // Get conformance endpoint response
  getConformance(): ConformanceResponse {
    return {
      conformsTo: this.conformance.getAll()
    };
  }
  
  // Generate OpenAPI specification
  generateOpenAPISpec(): OpenAPISpec {
    return generateOpenAPI({
      baseUrl: this.config.baseUrl,
      title: this.config.title,
      description: this.config.description,
      conformance: this.conformance.getAll(),
    });
  }
  
  // Get landing page
  getLandingPage(): LandingPage {
    return {
      title: this.config.title,
      description: this.config.description,
      links: [
        { rel: 'self', href: this.config.baseUrl },
        { rel: 'service-desc', href: `${this.config.baseUrl}/api` },
        { rel: 'conformance', href: `${this.config.baseUrl}/conformance` },
        { rel: 'data', href: `${this.config.baseUrl}/collections` },
      ]
    };
  }
}

3. Router Factories

Systems Router Factory:

// src/server/routers/systems-router.ts

import { Router } from 'express';
import { validateSystemsQuery, validateSystemBody } from '../middleware/validate-request';
import { ResponseBuilder } from '../builders/response-builder';
import { SystemSerializer } from '../serializers/system-serializer';

export interface SystemsRouterHandlers {
  findSystems(query: SystemsQueryOptions): Promise<SystemData[]>;
  getSystem(id: string): Promise<SystemData | null>;
  createSystem(data: SystemData): Promise<SystemData>;
  updateSystem(id: string, data: SystemData): Promise<SystemData>;
  patchSystem(id: string, data: Partial<SystemData>): Promise<SystemData>;
  deleteSystem(id: string, cascade?: boolean): Promise<void>;
  getSystemHistory(id: string, query: HistoryQueryOptions): Promise<SystemData[]>;
}

export function createSystemsRouter(handlers: SystemsRouterHandlers): Router {
  const router = Router();
  const serializer = new SystemSerializer();
  const responseBuilder = new ResponseBuilder();
  
  // GET /systems - List systems with filtering
  router.get('/', validateSystemsQuery(), async (req, res, next) => {
    try {
      const query = req.query as SystemsQueryOptions;
      const systems = await handlers.findSystems(query);
      
      // Serialize to GeoJSON features
      const features = systems.map(s => serializer.serialize(s));
      
      // Build OGC-compliant response
      const response = responseBuilder.buildFeatureCollection({
        features,
        links: [
          { rel: 'self', href: req.originalUrl },
          { rel: 'collection', href: '/systems' },
        ],
        numberMatched: systems.length,
        numberReturned: features.length,
      });
      
      res.json(response);
    } catch (error) {
      next(error);
    }
  });
  
  // GET /systems/{id} - Get single system
  router.get('/:id', async (req, res, next) => {
    try {
      const system = await handlers.getSystem(req.params.id);
      
      if (!system) {
        return res.status(404).json({
          code: 'NotFound',
          description: `System ${req.params.id} not found`
        });
      }
      
      const feature = serializer.serialize(system);
      res.json(feature);
    } catch (error) {
      next(error);
    }
  });
  
  // POST /systems - Create system
  router.post('/', validateSystemBody(), async (req, res, next) => {
    try {
      const system = await handlers.createSystem(req.body);
      const feature = serializer.serialize(system);
      res.status(201)
        .location(`/systems/${system.id}`)
        .json(feature);
    } catch (error) {
      next(error);
    }
  });
  
  // PUT /systems/{id} - Update system
  router.put('/:id', validateSystemBody(), async (req, res, next) => {
    try {
      const system = await handlers.updateSystem(req.params.id, req.body);
      const feature = serializer.serialize(system);
      res.json(feature);
    } catch (error) {
      next(error);
    }
  });
  
  // PATCH /systems/{id} - Partial update
  router.patch('/:id', async (req, res, next) => {
    try {
      const system = await handlers.patchSystem(req.params.id, req.body);
      const feature = serializer.serialize(system);
      res.json(feature);
    } catch (error) {
      next(error);
    }
  });
  
  // DELETE /systems/{id} - Delete system
  router.delete('/:id', async (req, res, next) => {
    try {
      const cascade = req.query.cascade === 'true';
      await handlers.deleteSystem(req.params.id, cascade);
      res.status(204).send();
    } catch (error) {
      next(error);
    }
  });
  
  // GET /systems/{id}/history - System history
  router.get('/:id/history', validateHistoryQuery(), async (req, res, next) => {
    try {
      const query = req.query as HistoryQueryOptions;
      const history = await handlers.getSystemHistory(req.params.id, query);
      
      const features = history.map(s => serializer.serialize(s));
      const response = responseBuilder.buildFeatureCollection({
        features,
        links: [
          { rel: 'self', href: req.originalUrl },
          { rel: 'collection', href: `/systems/${req.params.id}/history` },
        ],
        numberMatched: history.length,
        numberReturned: features.length,
      });
      
      res.json(response);
    } catch (error) {
      next(error);
    }
  });
  
  return router;
}

4. Request Validation Middleware

Validate Request Middleware:

// src/server/middleware/validate-request.ts

import { Request, Response, NextFunction } from 'express';
import type { SystemsQueryOptions } from '../../ogc-api/csapi/model';

export function validateSystemsQuery() {
  return (req: Request, res: Response, next: NextFunction) => {
    const errors: string[] = [];
    
    // Validate limit
    if (req.query.limit) {
      const limit = parseInt(req.query.limit as string);
      if (isNaN(limit) || limit < 1 || limit > 10000) {
        errors.push('limit must be between 1 and 10000');
      }
    }
    
    // Validate bbox
    if (req.query.bbox) {
      const bbox = (req.query.bbox as string).split(',').map(parseFloat);
      if (bbox.length !== 4 || bbox.some(isNaN)) {
        errors.push('bbox must be four numbers: minLon,minLat,maxLon,maxLat');
      }
    }
    
    // Validate datetime
    if (req.query.datetime) {
      const datetime = req.query.datetime as string;
      // ISO 8601 validation
      if (!isValidISO8601(datetime)) {
        errors.push('datetime must be valid ISO 8601 format');
      }
    }
    
    if (errors.length > 0) {
      return res.status(400).json({
        code: 'InvalidParameter',
        description: 'Invalid query parameters',
        errors
      });
    }
    
    next();
  };
}

export function validateSystemBody() {
  return (req: Request, res: Response, next: NextFunction) => {
    const errors: string[] = [];
    
    // Validate GeoJSON structure
    if (!req.body.type || req.body.type !== 'Feature') {
      errors.push('Request body must be a GeoJSON Feature');
    }
    
    if (!req.body.properties) {
      errors.push('Feature must have properties');
    }
    
    // Use existing GeoJSON validator
    const validator = new GeoJSONValidator();
    const result = validator.validateSystemFeature(req.body);
    
    if (!result.isValid) {
      errors.push(...result.errors.map(e => e.message));
    }
    
    if (errors.length > 0) {
      return res.status(400).json({
        code: 'InvalidBody',
        description: 'Invalid request body',
        errors
      });
    }
    
    next();
  };
}

5. Response Builder

Response Builder:

// src/server/builders/response-builder.ts

export class ResponseBuilder {
  buildFeatureCollection(options: {
    features: Feature[];
    links: Link[];
    numberMatched?: number;
    numberReturned: number;
  }): FeatureCollection {
    return {
      type: 'FeatureCollection',
      features: options.features,
      links: options.links,
      numberMatched: options.numberMatched,
      numberReturned: options.numberReturned,
    };
  }
  
  buildPaginationLinks(options: {
    baseUrl: string;
    currentPage: number;
    pageSize: number;
    totalItems: number;
  }): Link[] {
    const { baseUrl, currentPage, pageSize, totalItems } = options;
    const totalPages = Math.ceil(totalItems / pageSize);
    
    const links: Link[] = [
      { rel: 'self', href: `${baseUrl}?page=${currentPage}&limit=${pageSize}` },
    ];
    
    if (currentPage > 1) {
      links.push({
        rel: 'prev',
        href: `${baseUrl}?page=${currentPage - 1}&limit=${pageSize}`
      });
    }
    
    if (currentPage < totalPages) {
      links.push({
        rel: 'next',
        href: `${baseUrl}?page=${currentPage + 1}&limit=${pageSize}`
      });
    }
    
    return links;
  }
  
  buildErrorResponse(options: {
    code: string;
    description: string;
    errors?: string[];
  }): ErrorResponse {
    return {
      code: options.code,
      description: options.description,
      errors: options.errors,
    };
  }
}

6. Database Query Builder

Query Builder:

// src/server/query/query-builder.ts

export class QueryBuilder {
  constructor(private adapter: DatabaseAdapter) {}
  
  buildSystemsQuery(options: SystemsQueryOptions): DatabaseQuery {
    let query = this.adapter.select('*').from('systems');
    
    // Pagination
    if (options.limit) {
      query = query.limit(options.limit);
    }
    
    // Spatial filter (bbox)
    if (options.bbox) {
      const [minLon, minLat, maxLon, maxLat] = options.bbox;
      query = query.where('ST_Intersects', [
        'geometry',
        `ST_MakeEnvelope(${minLon}, ${minLat}, ${maxLon}, ${maxLat}, 4326)`
      ]);
    }
    
    // Temporal filter (datetime)
    if (options.datetime) {
      query = query.whereBetween('valid_time', [
        options.datetime.start || '-infinity',
        options.datetime.end || 'infinity'
      ]);
    }
    
    // Full-text search
    if (options.q) {
      query = query.where('name', 'ILIKE', `%${options.q}%`)
        .orWhere('description', 'ILIKE', `%${options.q}%`);
    }
    
    // ID filter
    if (options.id) {
      const ids = Array.isArray(options.id) ? options.id : [options.id];
      query = query.whereIn('id', ids);
    }
    
    // Hierarchical filter
    if (options.parent) {
      query = query.where('parent_id', options.parent);
    }
    
    return query;
  }
}

7. OpenAPI Spec Generator

OpenAPI Generator:

// src/server/core/openapi-generator.ts

export function generateOpenAPI(config: {
  baseUrl: string;
  title: string;
  description?: string;
  conformance: ConformanceClass[];
}): OpenAPISpec {
  return {
    openapi: '3.0.3',
    info: {
      title: config.title,
      description: config.description,
      version: '1.0.0',
    },
    servers: [
      { url: config.baseUrl }
    ],
    paths: {
      '/systems': {
        get: {
          summary: 'List systems',
          operationId: 'getSystems',
          parameters: [
            {
              name: 'limit',
              in: 'query',
              schema: { type: 'integer', minimum: 1, maximum: 10000 }
            },
            {
              name: 'bbox',
              in: 'query',
              schema: { type: 'string', pattern: '^[-+]?\\d+(\\.\\d+)?,[-+]?\\d+(\\.\\d+)?,[-+]?\\d+(\\.\\d+)?,[-+]?\\d+(\\.\\d+)?$' }
            },
            // ... all query parameters
          ],
          responses: {
            '200': {
              description: 'Systems feature collection',
              content: {
                'application/geo+json': {
                  schema: { $ref: '#/components/schemas/SystemFeatureCollection' }
                }
              }
            }
          }
        },
        post: {
          summary: 'Create system',
          operationId: 'createSystem',
          requestBody: {
            content: {
              'application/geo+json': {
                schema: { $ref: '#/components/schemas/SystemFeature' }
              }
            }
          },
          responses: {
            '201': {
              description: 'System created',
              content: {
                'application/geo+json': {
                  schema: { $ref: '#/components/schemas/SystemFeature' }
                }
              }
            }
          }
        }
      },
      // ... all other endpoints
    },
    components: {
      schemas: {
        // Generate from TypeScript types
        SystemFeature: { /* ... */ },
        SystemFeatureCollection: { /* ... */ },
        // ... all other schemas
      }
    }
  };
}

8. Complete Usage Example

Complete Server Implementation:

import express from 'express';
import { CSAPIServer, createSystemsRouter, createDatastreamsRouter } from 'ogc-client-csapi/server';
import { PostgresAdapter } from 'ogc-client-csapi/server/adapters';

const app = express();
app.use(express.json());

// Initialize database adapter
const db = new PostgresAdapter({
  host: 'localhost',
  port: 5432,
  database: 'sensors',
  user: 'postgres',
  password: 'password',
});

// Initialize CSAPI server
const csapi = new CSAPIServer({
  baseUrl: 'https://api.example.com/csapi',
  title: 'Example Sensor Network',
  description: 'OGC API - Connected Systems implementation',
  database: db,
});

// Landing page
app.get('/csapi', (req, res) => {
  res.json(csapi.getLandingPage());
});

// Conformance
app.get('/csapi/conformance', (req, res) => {
  res.json(csapi.getConformance());
});

// OpenAPI spec
app.get('/csapi/api', (req, res) => {
  res.json(csapi.generateOpenAPISpec());
});

// Systems endpoints (auto-generated)
app.use('/csapi/systems', createSystemsRouter({
  async findSystems(query) {
    return db.systems.find(query);
  },
  async getSystem(id) {
    return db.systems.findById(id);
  },
  async createSystem(data) {
    return db.systems.create(data);
  },
  async updateSystem(id, data) {
    return db.systems.update(id, data);
  },
  async patchSystem(id, data) {
    return db.systems.patch(id, data);
  },
  async deleteSystem(id, cascade) {
    return db.systems.delete(id, cascade);
  },
  async getSystemHistory(id, query) {
    return db.systems.findHistory(id, query);
  },
}));

// Datastreams endpoints (auto-generated)
app.use('/csapi/datastreams', createDatastreamsRouter({
  // Similar handlers for datastreams
}));

// Start server
app.listen(3000, () => {
  console.log('OGC CSAPI server running on http://localhost:3000/csapi');
});

Result: 50+ OGC-compliant endpoints in ~100 lines of code (vs 2000+ lines manual implementation).


Acceptance Criteria

Core Server Infrastructure (10 criteria)

  • Implement CSAPIServer class with configuration
  • getConformance() method returns conformance classes
  • generateOpenAPISpec() generates valid OpenAPI 3.0 spec
  • getLandingPage() returns OGC landing page
  • Support Express framework
  • Support Fastify framework (optional)
  • ConformanceRegistry for tracking implemented features
  • TypeScript types for all configuration options
  • Error handling with OGC-compliant error responses
  • CORS middleware (optional)

Router Factories (20 criteria)

  • createSystemsRouter() generates all 7+ system endpoints
  • createDeploymentsRouter() generates all deployment endpoints
  • createProceduresRouter() generates all procedure endpoints
  • createSamplingFeaturesRouter() generates sampling feature endpoints
  • createPropertiesRouter() generates property endpoints
  • createDatastreamsRouter() generates datastream endpoints
  • createObservationsRouter() generates observation endpoints
  • createControlStreamsRouter() generates control stream endpoints
  • createCommandsRouter() generates command endpoints
  • createSystemEventsRouter() generates system event endpoints
  • All routers support GET (list and individual)
  • All routers support POST (create)
  • All routers support PUT (update)
  • All routers support PATCH (partial update)
  • All routers support DELETE (with cascade)
  • All routers support history endpoints
  • Relationship endpoints (e.g., /systems/{id}/datastreams)
  • Router factories accept handler functions (data access)
  • Automatic route registration
  • Proper HTTP status codes (200, 201, 204, 400, 404, 500)

Request Validation Middleware (12 criteria)

  • Validate query parameters (limit, bbox, datetime, etc.)
  • Validate request body (GeoJSON features)
  • Use existing validators (GeoJSON, SWE Common)
  • Return 400 Bad Request with error details
  • Validate data types (numbers, strings, dates)
  • Validate ranges (limit 1-10000)
  • Validate formats (ISO 8601 dates, WKT geometry)
  • Validate required fields
  • Middleware for all 10 resource types
  • Reusable validation functions
  • TypeScript type guards
  • Clear error messages

Response Builders (8 criteria)

  • buildFeatureCollection() creates OGC feature collections
  • buildPaginationLinks() generates next/prev links
  • buildErrorResponse() creates OGC error responses
  • Build proper link arrays (rel, href, type)
  • Include numberMatched and numberReturned
  • Support custom link relations
  • Proper Content-Type headers
  • Location header for 201 Created responses

Serializers (10 criteria)

  • SystemSerializer converts data → SystemFeature
  • DeploymentSerializer converts data → DeploymentFeature
  • ProcedureSerializer converts data → ProcedureFeature
  • DatastreamSerializer converts data → DatastreamFeature
  • ObservationSerializer converts data → ObservationFeature
  • CommandSerializer converts data → CommandFeature
  • All serializers handle null/undefined gracefully
  • Serializers validate output (optional)
  • Serializers support custom mappings
  • Serializers reuse type definitions from client library

Query Builders (12 criteria)

  • buildSystemsQuery() translates SystemsQueryOptions to SQL
  • Similar query builders for all 10 resource types
  • Support pagination (limit, offset)
  • Support spatial queries (bbox, geom)
  • Support temporal queries (datetime, phenomenonTime)
  • Support full-text search (q parameter)
  • Support ID filtering
  • Support hierarchical queries (parent, recursive)
  • Support property filtering (observedProperty, etc.)
  • Database adapter abstraction (PostgreSQL, MongoDB, SQLite)
  • SQL injection prevention
  • Query optimization hints

OpenAPI Generator (6 criteria)

  • Generate valid OpenAPI 3.0.3 specification
  • Include all implemented endpoints
  • Generate schemas from TypeScript types
  • Include query parameters with validation rules
  • Include request/response examples
  • Update spec based on registered routers

Testing (15 criteria)

Unit Tests (8 tests):

  • Test CSAPIServer initialization
  • Test conformance generation
  • Test OpenAPI generation
  • Test router factory creation
  • Test request validation middleware
  • Test response builders
  • Test serializers
  • Test query builders

Integration Tests (7 tests):

  • Test complete Express server with all routers
  • Test CRUD operations (create, read, update, delete)
  • Test query filtering (bbox, datetime, q)
  • Test pagination
  • Test error handling
  • Test relationship endpoints
  • Test with in-memory database

Documentation (8 criteria)

  • Add "Server-Side Helpers" section to README
  • Document server setup and configuration
  • Provide complete server example
  • Document router factory pattern
  • Document database adapter interface
  • Document custom handler implementation
  • Document OpenAPI spec generation
  • Document deployment to production

Implementation Notes

Files to Create

Server Infrastructure (~2000-2500 lines total):

src/server/
  index.ts                          # Main exports (~50 lines)
  core/
    csapi-server.ts                 # CSAPIServer class (~200 lines)
    conformance.ts                  # ConformanceRegistry (~100 lines)
    openapi-generator.ts            # OpenAPI generator (~400 lines)
  middleware/
    validate-request.ts             # Validation middleware (~300 lines)
    error-handler.ts                # Error handling (~100 lines)
  routers/
    systems-router.ts               # Systems router factory (~250 lines)
    deployments-router.ts           # Deployments router factory (~250 lines)
    procedures-router.ts            # Procedures router factory (~200 lines)
    datastreams-router.ts           # Datastreams router factory (~250 lines)
    observations-router.ts          # Observations router factory (~200 lines)
    commands-router.ts              # Commands router factory (~200 lines)
    # ... (4 more routers)
  builders/
    response-builder.ts             # Response builders (~150 lines)
    link-builder.ts                 # Link builders (~100 lines)
  serializers/
    system-serializer.ts            # SystemSerializer (~100 lines)
    observation-serializer.ts       # ObservationSerializer (~100 lines)
    # ... (8 more serializers)
  query/
    query-builder.ts                # Query builder base (~200 lines)
    spatial-query.ts                # Spatial helpers (~100 lines)
    temporal-query.ts               # Temporal helpers (~100 lines)
  adapters/
    database-adapter.ts             # Abstract adapter (~100 lines)
    postgres-adapter.ts             # PostgreSQL adapter (~200 lines)

Tests (~1500-2000 lines total):

tests/server/
  unit/
    csapi-server.spec.ts            # CSAPIServer tests (~150 lines)
    routers.spec.ts                 # Router tests (~300 lines)
    validation.spec.ts              # Validation tests (~200 lines)
    serializers.spec.ts             # Serializer tests (~200 lines)
    query-builder.spec.ts           # Query builder tests (~200 lines)
  integration/
    express-server.spec.ts          # Express integration (~300 lines)
    crud-operations.spec.ts         # CRUD tests (~200 lines)

Documentation:

README.md                           # Add "Server-Side" section (~300 lines)
docs/
  server-guide.md                   # Server development guide (~500 lines)
  database-adapters.md              # Database adapter guide (~200 lines)
  deployment.md                     # Deployment guide (~150 lines)

Files to Reuse from Existing Library

Type Definitions (no changes needed):

  • src/ogc-api/csapi/model.ts - Query option interfaces
  • src/ogc-api/csapi/geojson/features/*.ts - Feature types
  • src/ogc-api/csapi/swe/*.ts - SWE Common types
  • src/ogc-api/csapi/sensorml/*.ts - SensorML types

Validators (use directly):

  • src/ogc-api/csapi/validation/geojson-validator.ts
  • src/ogc-api/csapi/validation/swe-validator.ts
  • src/ogc-api/csapi/validation/sensorml-validator.ts

URL Patterns (reference for routes):

  • src/ogc-api/csapi/navigator.ts - All endpoint patterns

Implementation Phases

Phase 1: Core Infrastructure (10-12 hours)

  • CSAPIServer class
  • ConformanceRegistry
  • OpenAPI generator (basic)
  • Response builders
  • Link builders

Phase 2: Router Factories (20-25 hours)

  • Systems router (complete implementation)
  • Template for other routers
  • Implement all 10 router factories
  • Relationship endpoints

Phase 3: Request Validation (8-10 hours)

  • Validation middleware for all resource types
  • Integrate existing validators
  • Error handling middleware

Phase 4: Serializers (10-12 hours)

  • Implement all 10 serializers
  • Validation integration
  • Custom mapping support

Phase 5: Query Builders (12-15 hours)

  • QueryBuilder base class
  • Spatial query helpers
  • Temporal query helpers
  • PostgreSQL adapter

Phase 6: Testing (15-20 hours)

  • Unit tests for all components
  • Integration tests with Express
  • In-memory database for tests
  • Example server implementation

Phase 7: Documentation (8-10 hours)

  • README updates
  • Server development guide
  • API documentation
  • Deployment guide

Total Estimated Effort: 83-104 hours (~2-2.5 weeks for one developer)

Dependencies

Requires:

  • Node.js 18+ (for native fetch, WebSocket)
  • Express or Fastify web framework
  • Database (PostgreSQL recommended for spatial support)

Leverages Existing Work:

Optional Integrations:

Caveats

Scope Considerations:

In Scope:

  • Router factories for Express/Fastify
  • Request validation middleware
  • Response builders and serializers
  • Basic query builders
  • OpenAPI spec generation
  • PostgreSQL adapter (example)

Out of Scope (Future Work):

Database Requirements:

  • Spatial database recommended (PostGIS, MongoDB with geospatial)
  • Support for temporal queries
  • Full-text search capability (optional)
  • JSONB support for flexible properties (optional)

Performance Considerations:

  • Query builders generate SQL (not optimized for all cases)
  • Developers should add database indexes
  • Consider caching at server level (Redis, etc.)
  • Pagination required for large result sets

Testing Challenges:

  • Integration tests need database setup
  • Use in-memory SQLite for tests (no PostGIS)
  • Mock spatial queries in tests
  • WebSocket testing needs mock clients

Maintenance:

  • Server helpers must stay in sync with OGC specs
  • Breaking changes require versioning
  • Documentation must stay current

Priority Justification

Priority: Low

Why Low Priority:

  1. Client Focus: Library is primarily a client library (98% compliant)
  2. Large Scope: Server helpers are substantial (~80-100 hours implementation)
  3. Different Audience: Server developers vs client developers
  4. External Dependencies: Requires database, web framework choices
  5. Ongoing Maintenance: Server code requires more maintenance than client

Why Still Valuable:

  1. Ecosystem Growth: Enables rapid OGC CSAPI server development
  2. Code Reuse: Leverages existing types, validators, parsers
  3. Standards Compliance: Ensures consistent OGC implementations
  4. Developer Experience: 95% time savings for server development
  5. Completeness: Makes library a full-stack solution (client + server)

Impact if Not Addressed:

  • ⚠️ Developers must build servers from scratch (35-48 hours)
  • ⚠️ Inconsistent OGC implementations across projects
  • ⚠️ Higher barrier to entry for OGC CSAPI adoption
  • ⚠️ Duplicate validation/serialization logic in every project
  • Client library still excellent (no impact on client usage)

When to Prioritize Higher:

  • Building OGC CSAPI server in Node.js
  • Need rapid prototyping of CSAPI endpoints
  • Want reference implementation for OGC compliance
  • Building SaaS platform with multi-tenant servers
  • Community requests server support

Effort Estimate: 83-104 hours (2-2.5 weeks)

  • Core infrastructure: 10-12 hours
  • Router factories: 20-25 hours
  • Request validation: 8-10 hours
  • Serializers: 10-12 hours
  • Query builders: 12-15 hours
  • Testing: 15-20 hours
  • Documentation: 8-10 hours

ROI Analysis:

  • High ROI for developers building OGC CSAPI servers in Node.js (95% time savings)
  • Medium ROI for library adoption (grows ecosystem)
  • Low ROI for existing client users (no direct benefit)
  • Best ROI when multiple teams building servers (reusable foundation)

Recommendation: Consider as separate project (ogc-server-csapi) or as future enhancement after client library is fully mature and stable. Prioritize when there's demonstrated demand from server developers or when building reference implementation.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions