-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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:
/conformanceendpoint 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+ moreWith 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 mirrorparse()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
/conformanceendpoint 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
CSAPIServerclass 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)
-
SystemSerializerconverts data → SystemFeature -
DeploymentSerializerconverts data → DeploymentFeature -
ProcedureSerializerconverts data → ProcedureFeature -
DatastreamSerializerconverts data → DatastreamFeature -
ObservationSerializerconverts data → ObservationFeature -
CommandSerializerconverts 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 interfacessrc/ogc-api/csapi/geojson/features/*.ts- Feature typessrc/ogc-api/csapi/swe/*.ts- SWE Common typessrc/ogc-api/csapi/sensorml/*.ts- SensorML types
Validators (use directly):
src/ogc-api/csapi/validation/geojson-validator.tssrc/ogc-api/csapi/validation/swe-validator.tssrc/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:
- Issue Validate: OGC API Endpoint Integration (endpoint.ts) #20 (OGC Standards Compliance) - ✅ Complete (provides patterns)
- Issue Validate: Multi-Format Parsers (parsers/) #10 (Parsers) - ✅ Complete (inverse for serializers)
- Issue Validate: GeoJSON Validation System (validation/geojson-validator.ts) #12 (GeoJSON Validation) - ✅ Complete (reuse validators)
- Issue Validate: Request Body Builders (request-builders.ts) #14 (SWE Common Validation) - ✅ Complete (reuse validators)
Optional Integrations:
- Work Item Add more comprehensive edge case testing for parsers #42 (WebSocket Streaming) - Could add server-side WebSocket support
- Work Item Add test coverage for uncovered SWE Common validation paths #40 (Client Caching) - Server could provide Cache-Control headers
- Work Item Generate and publish coverage reports to GitHub Pages or repository documentation #41 (Cache Invalidation) - Server could push invalidation events
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):
- Complete database adapters for all databases
- WebSocket streaming server (see work item Add more comprehensive edge case testing for parsers #42)
- Authentication/authorization implementation (just helpers)
- Admin UI / management interface
- Monitoring and metrics
- Multi-tenancy support
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:
- Client Focus: Library is primarily a client library (98% compliant)
- Large Scope: Server helpers are substantial (~80-100 hours implementation)
- Different Audience: Server developers vs client developers
- External Dependencies: Requires database, web framework choices
- Ongoing Maintenance: Server code requires more maintenance than client
Why Still Valuable:
- Ecosystem Growth: Enables rapid OGC CSAPI server development
- Code Reuse: Leverages existing types, validators, parsers
- Standards Compliance: Ensures consistent OGC implementations
- Developer Experience: 95% time savings for server development
- 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.