Compare commits

..

4 Commits

Author SHA1 Message Date
0dd11f7b3b Add BoardModule and 'getByLocation' method
All checks were successful
Publish Package / build-and-publish (push) Successful in 13s
2026-05-09 21:00:39 +01:00
70e64e8d64 Bump api-schema version
All checks were successful
Publish Package / build-and-publish (push) Successful in 12s
2026-05-03 09:47:07 +01:00
935dc271a2 Add getByRid to TrainsModule
All checks were successful
Publish Package / build-and-publish (push) Successful in 13s
2026-05-03 00:51:03 +01:00
ed2f8527d6 Implement use of a custom fetch fn 2026-05-02 10:09:18 +01:00
11 changed files with 1853 additions and 21 deletions

1784
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,8 +30,10 @@
"author": "Frederick Boniface", "author": "Frederick Boniface",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@owlboard/api-schema-types": "^3.0.3-alpha1", "@owlboard/api-schema-types": "^3.0.3-alpha17",
"latlon-geohash": "^2.0.0" "install": "^0.13.0",
"latlon-geohash": "^2.0.0",
"npm": "^11.13.0"
}, },
"devDependencies": { "devDependencies": {
"@types/latlon-geohash": "^2.0.4", "@types/latlon-geohash": "^2.0.4",

View File

@@ -9,3 +9,4 @@ export { PisModule } from './modules/pis.js';
export { LocationFilterModule } from './modules/locationFilter.js'; export { LocationFilterModule } from './modules/locationFilter.js';
export { StationDataModule } from './modules/stationData.js'; export { StationDataModule } from './modules/stationData.js';
export { TrainsModule } from './modules/trains.js'; export { TrainsModule } from './modules/trains.js';
export { BoardModule } from './modules/board.js';

View File

@@ -34,8 +34,9 @@ export class BaseClient {
/** /**
* Handles the Envelope logic * Handles the Envelope logic
* @param fetcher - Optional custom fetch instance (e.g SvelteKit load fetch)
*/ */
public async request<T>(path: string, options: RequestInit = {}): Promise<ApiResult<T>> { public async request<T>(path: string, options: RequestInit = {}, fetcher: typeof fetch = fetch): Promise<ApiResult<T>> {
const url = `${this.baseUrl}${path}`; const url = `${this.baseUrl}${path}`;
console.debug(`[API DEBUG] Calling: ${url}`); console.debug(`[API DEBUG] Calling: ${url}`);
@@ -43,7 +44,7 @@ export class BaseClient {
headers.set('Content-Type', 'application/json'); headers.set('Content-Type', 'application/json');
if (this.apiKey) headers.set('X-OWL-KEY', this.apiKey); if (this.apiKey) headers.set('X-OWL-KEY', this.apiKey);
const response = await fetch(url, { ...options, headers }); const response = await fetcher(url, { ...options, headers });
if (!response.ok && !response.headers.get('content-type')?.includes('application/json')) { if (!response.ok && !response.headers.get('content-type')?.includes('application/json')) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);

View File

@@ -3,12 +3,14 @@ import { PisModule } from "../modules/pis.js";
import { LocationFilterModule } from "../modules/locationFilter.js"; import { LocationFilterModule } from "../modules/locationFilter.js";
import { StationDataModule } from "../modules/stationData.js"; import { StationDataModule } from "../modules/stationData.js";
import { TrainsModule } from "../modules/trains.js"; import { TrainsModule } from "../modules/trains.js";
import { BoardModule } from "../modules/board.js";
export class OwlBoardClient extends BaseClient { export class OwlBoardClient extends BaseClient {
public readonly pis: PisModule; public readonly pis: PisModule;
public readonly locationFilter: LocationFilterModule; public readonly locationFilter: LocationFilterModule;
public readonly stationData: StationDataModule; public readonly stationData: StationDataModule;
public readonly trains: TrainsModule; public readonly trains: TrainsModule;
public readonly board: BoardModule
constructor(baseUrl: string, apiKey?: string) { constructor(baseUrl: string, apiKey?: string) {
super(baseUrl, apiKey); super(baseUrl, apiKey);
@@ -17,5 +19,6 @@ export class OwlBoardClient extends BaseClient {
this.locationFilter = new LocationFilterModule(this); this.locationFilter = new LocationFilterModule(this);
this.stationData = new StationDataModule(this); this.stationData = new StationDataModule(this);
this.trains = new TrainsModule(this); this.trains = new TrainsModule(this);
this.board = new BoardModule(this);
} }
} }

View File

@@ -25,6 +25,24 @@ export const IsValidCrs = (CRS: string): boolean => {
return true; return true;
} }
/**
* Checks if a string is a valid RID, with 15-16 numeric characters
* @param rid The RID to validate
* @returns True/False - Whether valid or not
*/
export const IsValidRid = (rid: string): boolean => {
if (rid.length > 16 || rid.length < 15) {
return false
}
for (let i = 0; i < rid.length; i++) {
const char = rid.charCodeAt(i);
if (!isDigit(char)) {
return false
}
}
return true
}
/** /**
* Checks if a string is a valid TIPLOC (Syntactically Only) * Checks if a string is a valid TIPLOC (Syntactically Only)
* using byte level checking for max performance * using byte level checking for max performance

21
src/modules/board.ts Normal file
View File

@@ -0,0 +1,21 @@
import { ApiStationsBoard } from '@owlboard/api-schema-types';
import type { BaseClient, ApiResult } from '../lib/base.js';
import { IsValidCrs, IsValidTiploc } from '../lib/validation.js';
import { ValidationError } from '../lib/errors.js';
export class BoardModule {
constructor(private client: BaseClient) { }
async getByLocation(location: string, customFetch?: typeof fetch): Promise<ApiResult<ApiStationsBoard.StationsBoard>> {
if (!IsValidCrs(location) && !IsValidTiploc(location)) {
throw new ValidationError("Location", "Location must be provided as CRS or TIPLOC");
}
const path = `/board/${location.toUpperCase()}`;
return this.client.request<ApiStationsBoard.StationsBoard>(path, {
method: 'GET',
}, customFetch);
}
}

View File

@@ -4,12 +4,13 @@ import type { BaseClient, ApiResult } from '../lib/base.js';
export class LocationFilterModule { export class LocationFilterModule {
constructor(private client: BaseClient) {} constructor(private client: BaseClient) {}
async getLocationFilterData(): Promise<ApiResult<ApiLocationFilter.LocationFilterObject[]>> { async getLocationFilterData(customFetch?: typeof fetch): Promise<ApiResult<ApiLocationFilter.LocationFilterObject[]>> {
const path = '/locationFilter/data'; const path = '/locationFilter/data';
return this.client.request<ApiLocationFilter.LocationFilterObject[]>( return this.client.request<ApiLocationFilter.LocationFilterObject[]>(
path, path,
{method: "GET"} {method: "GET"},
customFetch,
); );
} }
} }

View File

@@ -6,7 +6,7 @@ import { ValidationError } from '../lib/errors.js';
export class PisModule { export class PisModule {
constructor(private client: BaseClient) {} constructor(private client: BaseClient) {}
async getByStartEndCrs(startCrs: string, endCrs: string): Promise<ApiResult<ApiPisObject.PisObjects[]>> { async getByStartEndCrs(startCrs: string, endCrs: string, customFetch?: typeof fetch): Promise<ApiResult<ApiPisObject.PisObjects[]>> {
if (!IsValidCrs(startCrs)) { if (!IsValidCrs(startCrs)) {
throw new ValidationError("startCrs", "Invalid CRS Format") throw new ValidationError("startCrs", "Invalid CRS Format")
} }
@@ -18,10 +18,10 @@ export class PisModule {
return this.client.request<ApiPisObject.PisObjects[]>(path, { return this.client.request<ApiPisObject.PisObjects[]>(path, {
method: 'GET', method: 'GET',
}); }, customFetch);
} }
async getByCode(code: string): Promise<ApiResult<ApiPisObject.PisObjects[]>> { async getByCode(code: string, customFetch?: typeof fetch): Promise<ApiResult<ApiPisObject.PisObjects[]>> {
if (!IsValidPis(code)) { if (!IsValidPis(code)) {
throw new ValidationError("code", "Invalid PIS Code Format") throw new ValidationError("code", "Invalid PIS Code Format")
} }
@@ -30,6 +30,6 @@ export class PisModule {
return this.client.request<ApiPisObject.PisObjects[]>(path, { return this.client.request<ApiPisObject.PisObjects[]>(path, {
method: 'GET', method: 'GET',
}) }, customFetch)
} }
} }

View File

@@ -22,7 +22,7 @@ export class StationDataModule {
* @param geohash Geohash as string (up to six characters), generate using this.generateGeohash() * @param geohash Geohash as string (up to six characters), generate using this.generateGeohash()
* @returns Nearest Stations API Response (CRS, Name) * @returns Nearest Stations API Response (CRS, Name)
*/ */
async getNearestStations(geohash: string): Promise<ApiResult<ApiStationsNearestStations.StationsNearestStations[]>> { async getNearestStations(geohash: string, customFetch?: typeof fetch): Promise<ApiResult<ApiStationsNearestStations.StationsNearestStations[]>> {
if (!IsValidGeoHash(geohash)) { if (!IsValidGeoHash(geohash)) {
throw new ValidationError("hash", "Invalid Geohash requested"); throw new ValidationError("hash", "Invalid Geohash requested");
} }
@@ -30,7 +30,7 @@ export class StationDataModule {
const path = `/stationData/nearest/${geohash}`; const path = `/stationData/nearest/${geohash}`;
return this.client.request<ApiStationsNearestStations.StationsNearestStations[]>(path, { return this.client.request<ApiStationsNearestStations.StationsNearestStations[]>(path, {
method: 'GET', method: 'GET',
}) }, customFetch)
} }
// getStationSate(crs: string){} // getStationSate(crs: string){}

View File

@@ -1,13 +1,13 @@
import { ApiTrainsTrainByHeadcode } from '@owlboard/api-schema-types'; import { ApiTrainsTrainByHeadcode, ApiTrainsTrainDetails } from '@owlboard/api-schema-types';
import type { BaseClient, ApiResult } from '../lib/base.js'; import type { BaseClient, ApiResult } from '../lib/base.js';
import { IsValidHeadcode, IsValidToc, IsValidDateStr } from '../lib/validation.js'; import { IsValidHeadcode, IsValidRid, IsValidToc, IsValidDateStr } from '../lib/validation.js';
import { ensureDateString } from '../lib/helpers.js'; import { ensureDateString } from '../lib/helpers.js';
import { ValidationError } from '../lib/errors.js'; import { ValidationError } from '../lib/errors.js';
export class TrainsModule { export class TrainsModule {
constructor(private client: BaseClient) { } constructor(private client: BaseClient) { }
async getByHeadcode(headcode: string, date: string | Date = new Date, toc: string = ""): Promise<ApiResult<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>> { async getByHeadcode(headcode: string, date: string | Date = new Date, toc: string = "", customFetch?: typeof fetch): Promise<ApiResult<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>> {
if (!IsValidHeadcode(headcode)) { if (!IsValidHeadcode(headcode)) {
throw new ValidationError("headcode", "Invalid headcode format") throw new ValidationError("headcode", "Invalid headcode format")
} }
@@ -29,6 +29,17 @@ export class TrainsModule {
return this.client.request<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>(path, { return this.client.request<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>(path, {
method: 'GET', method: 'GET',
}); }, customFetch);
}
async getByRid(rid: string, customFetch?: typeof fetch): Promise<ApiResult<ApiTrainsTrainDetails.TrainDetailsResponse>> {
if (!IsValidRid(rid)) {
throw new ValidationError("RID", "Invalid RID format, must be 15-16 numerals")
}
const path = `/train/${rid}`
return this.client.request<ApiTrainsTrainDetails.TrainDetailsResponse>(path, {
method: 'GET',
}, customFetch);
} }
} }