Compare commits
26 Commits
4059082416
...
v3.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 0474973588 | |||
| 7f2cb2c413 | |||
| 3c81b5225f | |||
| d10dabf604 | |||
| 26886f8a7d | |||
| 46dcf65de6 | |||
| d160ad87f9 | |||
| fc74e933d6 | |||
| ad355fe15e | |||
| 4dd8bd1108 | |||
| e2f4433042 | |||
| 7775abbbca | |||
| f340c57ffd | |||
| 6c6a603cd8 | |||
| 35167f1853 | |||
| 815ae473db | |||
| ff3052f3b0 | |||
| 0985e40ead | |||
| 23afe71752 | |||
| a77d661797 | |||
| bf46067a0c | |||
| ed4bbf2327 | |||
| 16bb7d3510 | |||
| edca2698a7 | |||
| 4fd95189ce | |||
| 4f13309cdc |
37
.gitea/workflows/publish.yaml
Normal file
37
.gitea/workflows/publish.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Publish Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
registry-url: 'https://git.fjla.uk/api/packages/owlboard/npm'
|
||||||
|
scope: '@OwlBoard'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish to Gitea Repo
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
npm version $VERSION --no-git-tag-version
|
||||||
|
npm config set //git.fjla.uk/api/packages/owlboard/npm/:_authToken ${{ secrets.REPO_TOKEN }}
|
||||||
|
npm config set //git.fjla.uk/api/packages/OwlBoard/npm/:_authToken ${{ secrets.REPO_TOKEN }}
|
||||||
|
npm publish --registry=https://git.fjla.uk
|
||||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "owlboard-ts",
|
"name": "@owlboard/owlboard-ts",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "owlboard-ts",
|
"name": "@owlboard/owlboard-ts",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@owlboard/api-schema-types": "^3.0.3-alpha1",
|
||||||
|
"latlon-geohash": "^2.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@owlboard/api-schema-types": "^3.0.1-alpha3",
|
"@types/latlon-geohash": "^2.0.4",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
@@ -500,10 +504,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@owlboard/api-schema-types": {
|
"node_modules/@owlboard/api-schema-types": {
|
||||||
"version": "3.0.1-alpha3",
|
"version": "3.0.3-alpha1",
|
||||||
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.1-alpha3/api-schema-types-3.0.1-alpha3.tgz",
|
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.3-alpha1/api-schema-types-3.0.3-alpha1.tgz",
|
||||||
"integrity": "sha512-5CVm1k/C++/VrtAw4NkvclDunH+RmYLnDZZMSWTM1mm+WlEVnmD+MVnTgC/FhcsAmsNHV8swm66RCqkCuhbOnA==",
|
"integrity": "sha512-UWe2nbJWb2B/LuZW1UXHJ2lpqOGwugiXTa4G6X5xLiaws3ISEdciweorX8kr2/JAz5+iFYIe1xXRFAWsFtpn/w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
@@ -534,6 +537,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/latlon-geohash": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/latlon-geohash/-/latlon-geohash-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-R/wb/V8lhhI0hyRGDTkT6F4C3lynkF/D29iMSN5B4bYswaa4R+wajgcTv9z73BotGArf4Q8BnDNtmLeI+WiKkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.3.0",
|
"version": "25.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
||||||
@@ -664,6 +674,12 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/latlon-geohash": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/latlon-geohash/-/latlon-geohash-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-OKBswTwrvTdtenV+9C9euBmvgGuqyjJNAzpQCarRz1m8/pYD2nz9fKkXmLs2S3jeXaLi3Ry76twQplKKUlgS/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "owlboard-ts",
|
"name": "@owlboard/owlboard-ts",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"description": "TypeScript API Library to interact with the OwlBoard API (> v3.0.0)",
|
"description": "TypeScript API Library to interact with the OwlBoard API (> v3.0.0)",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -13,7 +16,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc",
|
||||||
|
"test-pis": "NODE_TLS_REJECT_UNAUTHORIZED=0 tsx tests/manual/pis.ts"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -24,8 +29,12 @@
|
|||||||
],
|
],
|
||||||
"author": "Frederick Boniface",
|
"author": "Frederick Boniface",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@owlboard/api-schema-types": "^3.0.3-alpha1",
|
||||||
|
"latlon-geohash": "^2.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@owlboard/api-schema-types": "^3.0.1-alpha3",
|
"@types/latlon-geohash": "^2.0.4",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
|
|||||||
11
src/app.d.ts
vendored
Normal file
11
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { OwlBoardClient } from "./lib/client";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
interface Locals {
|
||||||
|
api: OwlBoardClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
14
src/index.ts
14
src/index.ts
@@ -1,5 +1,11 @@
|
|||||||
export * from './lib/base.js';
|
export { OwlBoardClient } from './lib/client.js';
|
||||||
export * from './lib/client.js';
|
export { ValidationError, ApiError } from './lib/errors.js';
|
||||||
export * from './lib/errors.js';
|
|
||||||
|
|
||||||
export * from './modules/pis.js';
|
// Re-export API Schema types
|
||||||
|
export type * from '@owlboard/api-schema-types'
|
||||||
|
|
||||||
|
// Useful exports for Type Hinting
|
||||||
|
export { PisModule } from './modules/pis.js';
|
||||||
|
export { LocationFilterModule } from './modules/locationFilter.js';
|
||||||
|
export { StationDataModule } from './modules/stationData.js';
|
||||||
|
export { TrainsModule } from './modules/trains.js';
|
||||||
|
|||||||
@@ -8,16 +8,36 @@ export interface ApiResult<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BaseClient {
|
export class BaseClient {
|
||||||
|
protected readonly baseUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly baseUrl: string,
|
baseUrl: string,
|
||||||
protected readonly apiKey?: string
|
protected readonly apiKey?: string
|
||||||
) {}
|
) {
|
||||||
|
const root = baseUrl.replace(/\/$/, '');
|
||||||
|
this.baseUrl = root.endsWith('/api/v3')
|
||||||
|
? root
|
||||||
|
: `${root}/api/v3`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ping(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fetch(this.baseUrl, {
|
||||||
|
method: 'HEAD',
|
||||||
|
signal: AbortSignal.timeout(3000)
|
||||||
|
});
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the Envelope logic
|
* Handles the Envelope logic
|
||||||
*/
|
*/
|
||||||
public async request<T>(path: string, options: RequestInit = {}): Promise<ApiResult<T>> {
|
public async request<T>(path: string, options: RequestInit = {}): Promise<ApiResult<T>> {
|
||||||
const url = `${this.baseUrl}${path}`;
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
console.debug(`[API DEBUG] Calling: ${url}`);
|
||||||
|
|
||||||
const headers = new Headers(options.headers);
|
const headers = new Headers(options.headers);
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import { BaseClient } from "./base.js";
|
import { BaseClient } from "./base.js";
|
||||||
import { PisModule } from "../modules/pis.js";
|
import { PisModule } from "../modules/pis.js";
|
||||||
|
import { LocationFilterModule } from "../modules/locationFilter.js";
|
||||||
|
import { StationDataModule } from "../modules/stationData.js";
|
||||||
|
import { TrainsModule } from "../modules/trains.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 stationData: StationDataModule;
|
||||||
|
public readonly trains: TrainsModule;
|
||||||
|
|
||||||
constructor(baseUrl: string, apiKey?: string) {
|
constructor(baseUrl: string, apiKey?: string) {
|
||||||
super(baseUrl, apiKey);
|
super(baseUrl, apiKey);
|
||||||
|
|
||||||
this.pis = new PisModule(this);
|
this.pis = new PisModule(this);
|
||||||
|
this.locationFilter = new LocationFilterModule(this);
|
||||||
|
this.stationData = new StationDataModule(this);
|
||||||
|
this.trains = new TrainsModule(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,3 +15,10 @@ export class ApiError extends Error {
|
|||||||
this.code = errorObj.code || ("SERVER" as ApiErrorCode);
|
this.code = errorObj.code || ("SERVER" as ApiErrorCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends Error {
|
||||||
|
constructor(public field: string, public reason: string) {
|
||||||
|
super(`validation failed for ${field}: ${reason}`);
|
||||||
|
this.name = "VALIDATIONERROR";
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/lib/helpers.ts
Normal file
20
src/lib/helpers.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Normalises input to YYYY-MM-DD string
|
||||||
|
* - Strings are passed through as is
|
||||||
|
* - Date objects are converted to UK time
|
||||||
|
*/
|
||||||
|
export const ensureDateString = (input: Date | string): string => {
|
||||||
|
if (typeof input === 'string') return input.trim();
|
||||||
|
|
||||||
|
const d = new Intl.DateTimeFormat('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
timeZone: 'Europe/London'
|
||||||
|
}).formatToParts(input);
|
||||||
|
|
||||||
|
const part = (type: string) => d.find(p => p.type === type)?.value;
|
||||||
|
|
||||||
|
return `${part('year')}-${part('month')}-${part('day')}`
|
||||||
|
|
||||||
|
}
|
||||||
125
src/lib/validation.ts
Normal file
125
src/lib/validation.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a character code is a digit
|
||||||
|
*/
|
||||||
|
const isDigit = (c: number) => c >= 48 && c <= 57;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a character code is an upper case letter
|
||||||
|
*/
|
||||||
|
const isUpper = (c: number) => c >= 65 && c <= 90;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid CRS (Syntactically Only)
|
||||||
|
* using byte level checking for max performance
|
||||||
|
* @param CRS The CRS Code to be validated
|
||||||
|
*/
|
||||||
|
export const IsValidCrs = (CRS: string): boolean => {
|
||||||
|
if (CRS.length !== 3) return false;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const char = CRS.charCodeAt(i);
|
||||||
|
if (!(
|
||||||
|
(char >= 65 && char <=90) ||
|
||||||
|
(char >= 97 && char <=122)
|
||||||
|
)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid TIPLOC (Syntactically Only)
|
||||||
|
* using byte level checking for max performance
|
||||||
|
* @param TIPLOC The TIPLOC to be validated
|
||||||
|
*/
|
||||||
|
export const IsValidTiploc = (TIPLOC: string): boolean => {
|
||||||
|
const l = TIPLOC.length;
|
||||||
|
if (l === 0 || l > 7) return false;
|
||||||
|
for (let i = 0; i > l; i++) {
|
||||||
|
const char = TIPLOC.charCodeAt(i);
|
||||||
|
if (!(
|
||||||
|
(char >= 48 && char <= 57) ||
|
||||||
|
(char >= 65 && char <=90) ||
|
||||||
|
(char >= 97 && char <=122)
|
||||||
|
)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid PIS Code (Syntactically Only)
|
||||||
|
* using byte level checking for max performance
|
||||||
|
* ONLY GWR (4-digit) CODES SUPPORTED
|
||||||
|
* @param PIS The PIS Code to be validated
|
||||||
|
*/
|
||||||
|
export const IsValidPis = (PIS: string): boolean => {
|
||||||
|
if (PIS.length !== 4) return false;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const char = PIS.charCodeAt(i);
|
||||||
|
if (!(char >= 48 && char <= 57)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is the correct format for a Headcode
|
||||||
|
* USes byte level checking for max performance
|
||||||
|
* @param Headcode The headcode to be validated
|
||||||
|
*/
|
||||||
|
export const IsValidHeadcode = (Headcode: string): boolean => {
|
||||||
|
if (Headcode.length !== 4) return false;
|
||||||
|
const c0 = Headcode.charCodeAt(0);
|
||||||
|
const c1 = Headcode.charCodeAt(1);
|
||||||
|
const c2 = Headcode.charCodeAt(2);
|
||||||
|
const c3 = Headcode.charCodeAt(3);
|
||||||
|
|
||||||
|
return isDigit(c0) && isUpper(c1) && isDigit(c2) && isDigit(c3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is the correct format for a TOC Code
|
||||||
|
*/
|
||||||
|
export const IsValidToc = (toc: string): boolean => {
|
||||||
|
if (toc.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c0 = toc.charCodeAt(0);
|
||||||
|
const c1 = toc.charCodeAt(1);
|
||||||
|
return isUpper(c0) && isUpper(c1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is the correct format, and is a valid date
|
||||||
|
*/
|
||||||
|
export const IsValidDateStr = (s: string): boolean => {
|
||||||
|
if (s.length !== 10) return false;
|
||||||
|
if (s[4] !== '-' || s[7] !== '-') return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (i === 4 || i === 7) continue;
|
||||||
|
const c = s.charCodeAt(i);
|
||||||
|
if (!isDigit(i)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const y = parseInt(s.substring(0, 4), 10);
|
||||||
|
const m = parseInt(s.substring(5, 7), 10) - 1; // JS months are 0-11
|
||||||
|
const d = parseInt(s.substring(8, 10), 10);
|
||||||
|
|
||||||
|
const date = new Date(y, m, d);
|
||||||
|
|
||||||
|
return (
|
||||||
|
date.getFullYear() === y &&
|
||||||
|
date.getMonth() === m &&
|
||||||
|
date.getDate() === d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates Geohash string against standard b32 alphabet
|
||||||
|
* (Syntactically Only validation)
|
||||||
|
* @param hash The geohash to be validated
|
||||||
|
* @param maxLen Defaults to 6 - which is enforced by the server, should not need changing
|
||||||
|
*/
|
||||||
|
export const IsValidGeoHash = (hash: string, maxLen: number = 6): boolean => {
|
||||||
|
const geoHashRegex = new RegExp(`^[0-9bcdefghjkmnpqrstuvwxyz]{1,${maxLen}}$`, 'i');
|
||||||
|
return geoHashRegex.test(hash);
|
||||||
|
}
|
||||||
15
src/modules/locationFilter.ts
Normal file
15
src/modules/locationFilter.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ApiLocationFilter } from "@owlboard/api-schema-types";
|
||||||
|
import type { BaseClient, ApiResult } from '../lib/base.js';
|
||||||
|
|
||||||
|
export class LocationFilterModule {
|
||||||
|
constructor(private client: BaseClient) {}
|
||||||
|
|
||||||
|
async getLocationFilterData(): Promise<ApiResult<ApiLocationFilter.LocationFilterObject[]>> {
|
||||||
|
const path = '/locationFilter/data';
|
||||||
|
|
||||||
|
return this.client.request<ApiLocationFilter.LocationFilterObject[]>(
|
||||||
|
path,
|
||||||
|
{method: "GET"}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,35 @@
|
|||||||
import type { ApiPisObject } from '@owlboard/api-schema-types';
|
import { ApiPisObject } from '@owlboard/api-schema-types';
|
||||||
import type { BaseClient, ApiResult } from '../lib/base.js';
|
import type { BaseClient, ApiResult } from '../lib/base.js';
|
||||||
|
import { IsValidCrs, IsValidPis } from '../lib/validation.js';
|
||||||
|
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): Promise<ApiResult<ApiPisObject.PisObjects[]>> {
|
||||||
|
if (!IsValidCrs(startCrs)) {
|
||||||
|
throw new ValidationError("startCrs", "Invalid CRS Format")
|
||||||
|
}
|
||||||
|
if (!IsValidCrs(endCrs)) {
|
||||||
|
throw new ValidationError("endCrs", "Invalid CRS Format")
|
||||||
|
}
|
||||||
|
|
||||||
const path = `/pis/route/${encodeURIComponent(startCrs.toUpperCase())}/${encodeURIComponent(endCrs.toUpperCase())}`;
|
const path = `/pis/route/${encodeURIComponent(startCrs.toUpperCase())}/${encodeURIComponent(endCrs.toUpperCase())}`;
|
||||||
|
|
||||||
return this.client.request<ApiPisObject.PisObjects>(path, {
|
return this.client.request<ApiPisObject.PisObjects[]>(path, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByCode(code: string): Promise<ApiResult<ApiPisObject.PisObjects[]>> {
|
||||||
|
if (!IsValidPis(code)) {
|
||||||
|
throw new ValidationError("code", "Invalid PIS Code Format")
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = `/pis/code/${encodeURIComponent(code)}`;
|
||||||
|
|
||||||
|
return this.client.request<ApiPisObject.PisObjects[]>(path, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
37
src/modules/stationData.ts
Normal file
37
src/modules/stationData.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ApiStationsNearestStations } from '@owlboard/api-schema-types';
|
||||||
|
import type { BaseClient, ApiResult } from '../lib/base.js';
|
||||||
|
import { IsValidGeoHash } from '../lib/validation.js';
|
||||||
|
import { ValidationError } from '../lib/errors.js';
|
||||||
|
import Geohash from 'latlon-geohash';
|
||||||
|
|
||||||
|
export class StationDataModule {
|
||||||
|
constructor(private client: BaseClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a 6-char Geohash for the given co-ordinates
|
||||||
|
* @param lt Latitude
|
||||||
|
* @param ln Longitude
|
||||||
|
* @returns Geohash as string
|
||||||
|
*/
|
||||||
|
generateGeohash(lt: number, ln: number): string {
|
||||||
|
return Geohash.encode(lt, ln, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param geohash Geohash as string (up to six characters), generate using this.generateGeohash()
|
||||||
|
* @returns Nearest Stations API Response (CRS, Name)
|
||||||
|
*/
|
||||||
|
async getNearestStations(geohash: string): Promise<ApiResult<ApiStationsNearestStations.StationsNearestStations[]>> {
|
||||||
|
if (!IsValidGeoHash(geohash)) {
|
||||||
|
throw new ValidationError("hash", "Invalid Geohash requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = `/stationData/nearest/${geohash}`;
|
||||||
|
return this.client.request<ApiStationsNearestStations.StationsNearestStations[]>(path, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStationSate(crs: string){}
|
||||||
|
}
|
||||||
34
src/modules/trains.ts
Normal file
34
src/modules/trains.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiTrainsTrainByHeadcode } from '@owlboard/api-schema-types';
|
||||||
|
import type { BaseClient, ApiResult } from '../lib/base.js';
|
||||||
|
import { IsValidHeadcode, IsValidToc, IsValidDateStr } from '../lib/validation.js';
|
||||||
|
import { ensureDateString } from '../lib/helpers.js';
|
||||||
|
import { ValidationError } from '../lib/errors.js';
|
||||||
|
|
||||||
|
export class TrainsModule {
|
||||||
|
constructor(private client: BaseClient) { }
|
||||||
|
|
||||||
|
async getByHeadcode(headcode: string, date: string | Date = new Date, toc: string = ""): Promise<ApiResult<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>> {
|
||||||
|
if (!IsValidHeadcode(headcode)) {
|
||||||
|
throw new ValidationError("headcode", "Invalid headcode format")
|
||||||
|
}
|
||||||
|
if (toc !== "" && !IsValidToc(toc)) {
|
||||||
|
throw new ValidationError("TOC", "Invalid TOC Format, needs to be two characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateStr = ensureDateString(date);
|
||||||
|
if (!IsValidDateStr(dateStr)) {
|
||||||
|
throw new ValidationError("Date", "Invalid date format, need YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.append('d', dateStr);
|
||||||
|
searchParams.append('h', headcode);
|
||||||
|
if (toc) searchParams.append('t', toc);
|
||||||
|
|
||||||
|
const path = `/trains?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
return this.client.request<ApiTrainsTrainByHeadcode.TrainByHeadcodeResponse[]>(path, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
tests/manual/pis.ts
Normal file
80
tests/manual/pis.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { OwlBoardClient } from '../../src/lib/client';
|
||||||
|
import { ApiError, ValidationError } from '../../src/lib/errors';
|
||||||
|
|
||||||
|
console.log("Manual PIS Tests are running")
|
||||||
|
|
||||||
|
const TEST_SERVER: string = "https://owlboard.rke2-gw.svc.fjla.net"
|
||||||
|
console.log(`Using TEST_SERVER: ${TEST_SERVER}`)
|
||||||
|
|
||||||
|
const api = new OwlBoardClient(TEST_SERVER);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Test server reachability
|
||||||
|
console.log("Testing server reachability")
|
||||||
|
if (!(await api.ping())) {
|
||||||
|
console.error('Server not reachable. Check it is running and is reachable at the provided address')
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Server is reachable. Running tests...")
|
||||||
|
|
||||||
|
// Run code here.
|
||||||
|
|
||||||
|
{
|
||||||
|
// Test PIS Route
|
||||||
|
const start = 'bri';
|
||||||
|
const end = 'svb';
|
||||||
|
|
||||||
|
console.log(`Querying PIS Route: ${start} > ${end}...`);
|
||||||
|
try {
|
||||||
|
const {data, producedAt} = await api.pis.getByStartEndCrs(start, end);
|
||||||
|
console.log('\n---Response Success---');
|
||||||
|
console.log(`Data Produced At: ${producedAt.toLocaleTimeString()}`);
|
||||||
|
console.log(`Number of entries in response: ${data.length}`);
|
||||||
|
console.log(JSON.stringify(data));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
console.error('\n--- API Error ---');
|
||||||
|
console.error(`Code: ${err.code}`);
|
||||||
|
console.error(`Msg: ${err.message}`);
|
||||||
|
console.error(`Status: ${err.status}`);
|
||||||
|
} else if (err instanceof ValidationError) {
|
||||||
|
console.error('\n--- Local Validation Error ---');
|
||||||
|
console.error(`Error Field: ${err.field}`);
|
||||||
|
console.error(`Message: ${err.reason}`);
|
||||||
|
console.error(`Request not sent`);
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown Error: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test PIS Code
|
||||||
|
{
|
||||||
|
const code = 5001;
|
||||||
|
console.log(`Querying PIS Code: ${code}`);
|
||||||
|
try {
|
||||||
|
const {data, producedAt} = await api.pis.getByCode(code.toString());
|
||||||
|
console.log("\n--- Response Success ---");
|
||||||
|
console.log(`Data produced at: ${producedAt.toLocaleString()}`);
|
||||||
|
console.log(`Number of results: ${data.length}`);
|
||||||
|
console.log(JSON.stringify(data));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
console.error('\n--- API Error ---');
|
||||||
|
console.error(`Code: ${err.code}`);
|
||||||
|
console.error(`Msg: ${err.message}`);
|
||||||
|
console.error(`Status: ${err.status}`);
|
||||||
|
} else if (err instanceof ValidationError) {
|
||||||
|
console.error('\n--- Local Validation Error ---');
|
||||||
|
console.error(`Error Field: ${err.field}`);
|
||||||
|
console.error(`Message: ${err.reason}`);
|
||||||
|
console.error(`Request not sent`);
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown Error: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
|
"composite": true,
|
||||||
|
|
||||||
// Stricter Typechecking Options
|
// Stricter Typechecking Options
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user