From 40590824169ce389035a04608d8c8d0ae9eb4377 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Sat, 21 Feb 2026 00:04:25 +0000 Subject: [PATCH] Add initial PIS route --- package.json | 11 ++++++++++- src/index.ts | 5 +++++ src/lib/base.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib/client.ts | 12 ++++++++++++ src/lib/errors.ts | 17 +++++++++++++++++ src/modules/pis.ts | 14 ++++++++++++++ tsconfig.json | 11 +++++++---- 7 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/index.ts create mode 100644 src/lib/base.ts create mode 100644 src/lib/client.ts create mode 100644 src/lib/errors.ts create mode 100644 src/modules/pis.ts diff --git a/package.json b/package.json index fcf1905..dcc75b4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,16 @@ "name": "owlboard-ts", "version": "3.0.0", "description": "TypeScript API Library to interact with the OwlBoard API (> v3.0.0)", - "main": "index.js", + "main": "./dist/index.js", + "type": "module", + "sideEffects": false, + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b8e6587 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export * from './lib/base.js'; +export * from './lib/client.js'; +export * from './lib/errors.js'; + +export * from './modules/pis.js'; \ No newline at end of file diff --git a/src/lib/base.ts b/src/lib/base.ts new file mode 100644 index 0000000..98b61c3 --- /dev/null +++ b/src/lib/base.ts @@ -0,0 +1,46 @@ +import { ApiError } from "./errors.js"; +import type { ApiErrorCode } from "./errors.js"; +import type { ApiEnvelope } from "@owlboard/api-schema-types"; + +export interface ApiResult { + data: T; + producedAt: Date; +} + +export class BaseClient { + constructor( + protected readonly baseUrl: string, + protected readonly apiKey?: string + ) {} + + /** + * Handles the Envelope logic + */ + public async request(path: string, options: RequestInit = {}): Promise> { + const url = `${this.baseUrl}${path}`; + + const headers = new Headers(options.headers); + headers.set('Content-Type', 'application/json'); + if (this.apiKey) headers.set('X-OWL-KEY', this.apiKey); + + const response = await fetch(url, { ...options, headers }); + + if (!response.ok && !response.headers.get('content-type')?.includes('application/json')) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // Parse raw envelope + const envelope = (await response.json()) as ApiEnvelope.Envelope; + + if (envelope.e !== undefined && envelope.e !== null) { + throw new ApiError(envelope.e || {}, response.status); + } + + return { + data: envelope.d as T, + + // Convert seconds to milliseconds before parsing date + producedAt: new Date(Number(envelope.t) * 1000) + }; + } +} \ No newline at end of file diff --git a/src/lib/client.ts b/src/lib/client.ts new file mode 100644 index 0000000..db81eb2 --- /dev/null +++ b/src/lib/client.ts @@ -0,0 +1,12 @@ +import { BaseClient } from "./base.js"; +import { PisModule } from "../modules/pis.js"; + +export class OwlBoardClient extends BaseClient { + public readonly pis: PisModule; + + constructor(baseUrl: string, apiKey?: string) { + super(baseUrl, apiKey); + + this.pis = new PisModule(this); + } +} \ No newline at end of file diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..8acef77 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,17 @@ +import type { ApiEnvelope } from "@owlboard/api-schema-types"; + +type RawErrorObject = NonNullable; +export type ApiErrorCode = NonNullable + +export class ApiError extends Error { + public readonly code: ApiErrorCode; + public readonly status: number; + + constructor(errorObj: RawErrorObject, status: number) { + super(errorObj.msg || 'An unknown error occurred'); + this.name = 'ApiError'; + this.status = status; + // cast fallback to ensure it satisfies union + this.code = errorObj.code || ("SERVER" as ApiErrorCode); + } +} \ No newline at end of file diff --git a/src/modules/pis.ts b/src/modules/pis.ts new file mode 100644 index 0000000..46f8137 --- /dev/null +++ b/src/modules/pis.ts @@ -0,0 +1,14 @@ +import type { ApiPisObject } from '@owlboard/api-schema-types'; +import type { BaseClient, ApiResult } from '../lib/base.js'; + +export class PisModule { + constructor(private client: BaseClient) {} + + async getByStartEndCrs(startCrs: string, endCrs: string): Promise> { + const path = `/pis/route/${encodeURIComponent(startCrs.toUpperCase())}/${encodeURIComponent(endCrs.toUpperCase())}`; + + return this.client.request(path, { + method: 'GET', + }); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index cec4a3a..d56b9ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,15 @@ // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { // File Layout - // "rootDir": "./src", - // "outDir": "./dist", + "rootDir": "./src", + "outDir": "./dist", // Environment Settings // See also https://aka.ms/tsconfig/module - "module": "nodenext", + "module": "esnext", "target": "esnext", + "moduleResolution": "bundler", + "baseUrl": ".", "types": [], // For nodejs: // "lib": ["esnext"], @@ -40,5 +42,6 @@ "noUncheckedSideEffectImports": true, "moduleDetection": "force", "skipLibCheck": true, - } + }, + "include": ["src/**/*"] }