Compare commits

..

14 commits

Author SHA1 Message Date
McToel
5e502a6f44 removed duplicated eslint config 2025-02-19 01:06:35 +01:00
McToel
406d24a051
Merge branch 'main' into main 2025-02-19 01:00:22 +01:00
McToel
1f95ca06c1 Bumped eslint to v9 and ecmaScript to 2025 2025-02-19 00:25:25 +01:00
McToel
c951466597 No async in new Promise 2025-02-19 00:17:48 +01:00
McToel
f1d226e9c8 Linting 2025-02-19 00:16:08 +01:00
Kristjan ESPERANTO
6d1d0c626f Upgrade ESLint and fix linting issues
Some checks failed
test / lint (push) Has been cancelled
test / unit-tests (16.x) (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (16.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-02-17 21:41:20 +01:00
Kristjan ESPERANTO
afa99b0742
Review test workflow (#16)
Some checks are pending
test / lint (push) Waiting to run
test / unit-tests (16.x) (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (16.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
* Run linting only once

* Update actions/cache to v4

* Run e2e-tests with the same matrix as the others

* Change cache keys to avoid cache collisions

* Run e2e-tests only once
Since they go against the live DB APIs #16
2025-02-17 19:09:14 +01:00
Kristjan ESPERANTO
229dbac93e
Update copyright information (#15)
Some checks failed
test / unit-tests (16.x) (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (16.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (16.x) (push) Has been cancelled
2025-02-14 22:33:50 +01:00
Traines
7a1e513fa2 6.5.0
Some checks are pending
test / unit-tests (16.x) (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (16.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (16.x) (push) Blocked by required conditions
2025-02-13 23:34:39 +00:00
Traines
f1302b0a7b docs 2025-02-13 23:33:45 +00:00
Traines
177a3cab3f migrate and update loyaltyCard parsing from db-rest 2025-02-13 23:32:01 +00:00
Traines
71d1a4f1a9 refactor enrichStations, only load on first request 2025-02-13 22:16:22 +00:00
Traines
6ff406ea79 artificially filter for includeRelatedStations and direction 2025-02-13 21:53:49 +00:00
Traines
2a23e1ad9b make dbweb selectable in docker img 2025-02-13 19:41:46 +00:00
36 changed files with 1919 additions and 1373 deletions

View file

@ -1,54 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends"],
"ignorePatterns": ["node_modules", "*example.js"],
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"curly": "error",
"no-implicit-coercion": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
],
"@stylistic/array-bracket-newline": ["error", "consistent"],
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": "off",
"@stylistic/comma-dangle": ["error", "always-multiline"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-paren-newline": "off",
"@stylistic/indent": ["error", "tab"],
"@stylistic/indent-binary-ops": ["error", "tab"],
"@stylistic/max-len": "off",
"@stylistic/multiline-ternary": ["error", "always-multiline"],
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 1 }],
"@stylistic/no-mixed-operators": "off",
"@stylistic/no-tabs": "off",
"@stylistic/object-property-newline": "off",
"@stylistic/one-var-declaration-per-line": "off",
"@stylistic/operator-linebreak": ["error", "before"],
"@stylistic/padded-blocks": "off",
"@stylistic/quote-props": ["error", "consistent-as-needed"],
"@stylistic/quotes": ["error", "single"]
},
"overrides": [
{
"files": [
"test/**"
],
"rules": {
"no-unused-vars": "off"
}
}
]
}

View file

@ -6,6 +6,20 @@ env:
npm_config_cache: /tmp/npm-cache
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Use Node.js"
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- run: npm install
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
strategy:
@ -25,13 +39,12 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-unit-tests
path: ${{ env.npm_config_cache }}
- run: npm install
- run: npm run lint
- run: npm run test-unit
integration-tests:
@ -53,9 +66,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-integration-tests
path: ${{ env.npm_config_cache }}
- run: npm install
@ -66,7 +79,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- name: checkout
uses: actions/checkout@v4
@ -77,9 +90,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-e2e-tests
path: ${{ env.npm_config_cache }}
- run: npm install

View file

@ -1,4 +1,7 @@
Copyright (c) 2024, Jannis R
# ISC License
- Copyright © 2024 Jannis R
- Copyright © 2025 traines-source
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

34
api.js
View file

@ -1,32 +1,9 @@
import {createClient} from './index.js';
import {profile as dbProfile} from './p/db/index.js';
import {profile as dbnavProfile} from './p/dbnav/index.js';
import {profile as dbwebProfile} from './p/dbweb/index.js';
import {mapRouteParsers} from './lib/api-parsers.js';
import {createHafasRestApi as createApi} from 'hafas-rest-api';
import {loyaltyCardParser} from 'db-rest/lib/loyalty-cards.js';
import {parseBoolean, parseInteger} from 'hafas-rest-api/lib/parse.js';
// TODO product support for nearby etc?
const mapRouteParsers = (route, parsers) => {
if (!route.includes('journey')) {
return parsers;
}
return {
...parsers,
loyaltyCard: loyaltyCardParser,
firstClass: {
description: 'Search for first-class options?',
type: 'boolean',
default: 'false',
parse: parseBoolean,
},
age: {
description: 'Age of traveller',
type: 'integer',
defaultStr: '*adult*',
parse: parseInteger,
},
};
};
const config = {
hostname: process.env.HOSTNAME || 'localhost',
@ -45,10 +22,15 @@ const config = {
mapRouteParsers,
};
const profiles = {
db: dbProfile,
dbnav: dbnavProfile,
dbweb: dbwebProfile,
};
const start = async () => {
const vendo = createClient(
process.env.DB_PROFILE == 'db' ? dbProfile : dbnavProfile,
profiles[process.env.DB_PROFILE] || dbnavProfile,
process.env.USER_AGENT || 'link-to-your-project-or-email',
config,
);

7
debug.js Normal file
View file

@ -0,0 +1,7 @@
import {createClient} from './index.js';
import {profile} from './p/dbnav/index.js';
const client = createClient(profile, 'hafas-client-debug');
const journeys = await client.journeys('8000105', '8000261', {results: 1});
console.log(journeys);

View file

@ -24,7 +24,7 @@ With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
direction: null, // not supported
direction: null, // only supported in `dbweb` and with `enrichStations=true` (experimental)
line: null, // not supported
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
@ -35,7 +35,7 @@ With `opt`, you can override the default options, which look like this:
stopovers: false, // fetch & parse previous/next stopovers?, only supported with `dbweb` profile
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true, // only true supported
includeRelatedStations: true,
language: 'en' // language to get results in
}
```

View file

@ -385,13 +385,14 @@ paths:
- bahncard-2nd-25
- bahncard-1st-50
- bahncard-2nd-50
- bahncard-1st-100
- bahncard-2nd-100
- vorteilscard
- halbtaxabo-railplus
- halbtaxabo
- voordeelurenabo-railplus
- voordeelurenabo
- shcard
- generalabonnement
- generalabonnement-1st
- generalabonnement-2nd
- nl-40
- at-klimaticket
- name: firstClass
in: query
description: Search for first-class options?
@ -2087,7 +2088,7 @@ components:
type: string
format: date-time
direction:
description: only show departures heading to this station
description: only show departures heading to this station, only supported for `dbweb` profile
default: undefined
type: string
line:
@ -2124,7 +2125,7 @@ components:
type: boolean
includeRelatedStations:
description: departures at related stations
default: false
default: true
type: boolean
products:
$ref: '#/components/schemas/Products'

61
eslint.config.js Normal file
View file

@ -0,0 +1,61 @@
import eslintPluginJs from '@eslint/js';
import eslintPluginStylistic from '@stylistic/eslint-plugin';
import globals from 'globals';
const config = [
eslintPluginJs.configs.recommended,
eslintPluginStylistic.configs['all-flat'],
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
globals: {
...globals.node,
},
sourceType: 'module',
},
rules: {
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/array-element-newline': ['error', 'consistent'],
'@stylistic/arrow-parens': 'off',
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
'@stylistic/function-paren-newline': 'off',
'@stylistic/indent': ['error', 'tab'],
'@stylistic/indent-binary-ops': ['error', 'tab'],
'@stylistic/max-len': 'off',
'@stylistic/multiline-comment-style': 'off',
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
'@stylistic/newline-per-chained-call': ['error', {ignoreChainWithDepth: 1}],
'@stylistic/no-mixed-operators': 'off',
'@stylistic/no-tabs': 'off',
'@stylistic/object-property-newline': 'off',
'@stylistic/one-var-declaration-per-line': 'off',
'@stylistic/operator-linebreak': ['error', 'before'],
'@stylistic/padded-blocks': 'off',
'@stylistic/quote-props': ['error', 'consistent-as-needed'],
'@stylistic/quotes': ['error', 'single'],
'curly': 'error',
'no-implicit-coercion': 'error',
'no-unused-vars': [
'error',
{
vars: 'all',
args: 'none',
ignoreRestSiblings: false,
},
],
},
},
{
files: ['test/**', '**/example.js'],
rules: {
'no-unused-vars': 'off',
'@stylistic/semi': 'off',
},
},
];
export default config;

View file

@ -6,9 +6,10 @@ const c = {
VOORDEELURENABO: Symbol('Voordeelurenabo'),
SHCARD: Symbol('SH-Card'),
GENERALABONNEMENT: Symbol('General-Abonnement'),
NL_40: Symbol('NL-40%'),
AT_KLIMATICKET: Symbol('AT-KlimaTicket'),
};
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
const formatLoyaltyCard = (data) => {
if (!data) {
return {
@ -19,7 +20,7 @@ const formatLoyaltyCard = (data) => {
const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2';
if (data.type.toString() === c.BAHNCARD.toString()) {
return {
art: 'BAHNCARD' + data.discount,
art: 'BAHNCARD' + (data.business ? 'BUSINESS' : '') + data.discount,
klasse: cls,
};
}
@ -35,13 +36,24 @@ const formatLoyaltyCard = (data) => {
klasse: 'KLASSENLOS',
};
}
// TODO Rest
if (data.type.toString() === c.GENERALABONNEMENT.toString()) {
return {
art: 'CH-GENERAL-ABONNEMENT',
klasse: cls,
};
}
if (data.type.toString() === c.NL_40.toString()) {
return {
art: 'NL-40_OHNE_RAILPLUS',
klasse: 'KLASSENLOS',
};
}
if (data.type.toString() === c.AT_KLIMATICKET.toString()) {
return {
art: 'KLIMATICKET_OE',
klasse: 'KLASSENLOS',
};
}
return {
art: 'KEINE_ERMAESSIGUNG',
klasse: 'KLASSENLOS',

View file

@ -26,34 +26,43 @@ const validateLocation = (loc, name = 'location') => {
}
};
const loadEnrichedStationData = (profile) => new Promise(async (resolve, reject) => {
const { default: readStations} = await import('db-hafas-stations');
const items = {};
readStations.full()
.on('data', (station) => {
items[station.id] = station;
items[station.name] = station;
})
.once('end', () => {
if (profile.DEBUG) {
console.log('Loaded station index.');
}
resolve(items);
})
.once('error', (err) => {
reject(err);
});
const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
import('db-hafas-stations').then(m => {
const items = {};
m.default.full()
.on('data', (station) => {
items[station.id] = station;
items[station.name] = station;
})
.once('end', () => {
if (profile.DEBUG) {
console.log('Loaded station index.');
}
resolve(items);
})
.once('error', (err) => {
reject(err);
});
});
});
const applyEnrichedStationData = async (ctx, shouldLoadEnrichedStationData) => {
const {profile, common} = ctx;
if (shouldLoadEnrichedStationData && !common.locations) {
const locations = await loadEnrichedStationData(profile);
common.locations = locations;
}
};
const createClient = (profile, userAgent, opt = {}) => {
profile = Object.assign({}, defaultProfile, profile);
validateProfile(profile);
const common = {};
if (opt.enrichStations !== false) {
loadEnrichedStationData(profile)
.then(locations => {
common.locations = locations;
});
let shouldLoadEnrichedStationData = false;
if (typeof opt.enrichStations === 'function') {
profile.enrichStation = opt.enrichStations;
} else if (opt.enrichStations !== false) {
shouldLoadEnrichedStationData = true;
}
if ('string' !== typeof userAgent) {
@ -64,6 +73,7 @@ const createClient = (profile, userAgent, opt = {}) => {
}
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(station) && station.id) {
station = station.id;
} else if ('string' !== typeof station) {
@ -106,9 +116,15 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res};
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries)
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries)
.map(res => parse(ctx, res)); // TODO sort?, slice
if (!opt.includeRelatedStations) {
results = results.filter(r => !r.stop?.id || r.stop.id == station);
}
if (opt.direction) {
results = results.filter(r => !r.nextStopovers || r.nextStopovers.find(s => s.stop?.id == opt.direction || s.stop?.name == opt.direction));
}
return {
[resultsField]: results,
realtimeDataUpdatedAt: null, // TODO
@ -123,6 +139,7 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const journeys = async (from, to, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('earlierThan' in opt && 'laterThan' in opt) {
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.');
}
@ -205,6 +222,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const refreshJourney = async (refreshToken, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('string' !== typeof refreshToken || !refreshToken) {
throw new TypeError('refreshToken must be a non-empty string.');
}
@ -231,6 +250,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const locations = async (query, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.');
}
@ -257,6 +278,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const stop = async (stop, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(stop) && stop.id) {
stop = stop.id;
} else if ('string' !== typeof stop) {
@ -278,6 +301,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const nearby = async (location, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
validateLocation(location, 'location');
opt = Object.assign({
@ -308,6 +333,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const trip = async (id, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(id)) {
throw new TypeError('id must be a non-empty string.');
}
@ -335,6 +362,8 @@ const createClient = (profile, userAgent, opt = {}) => {
// todo [breaking]: rename to trips()?
const tripsByName = async (_lineNameOrFahrtNr = '*', _opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
throw new Error('not implemented');
};
@ -361,4 +390,5 @@ const createClient = (profile, userAgent, opt = {}) => {
export {
createClient,
loadEnrichedStationData,
};

74
lib/api-parsers.js Normal file
View file

@ -0,0 +1,74 @@
import {data as cards} from '../format/loyalty-cards.js';
import {parseBoolean, parseInteger} from 'hafas-rest-api/lib/parse.js';
const typesByName = new Map([
['bahncard-1st-25', {type: cards.BAHNCARD, discount: 25, class: 1}],
['bahncard-2nd-25', {type: cards.BAHNCARD, discount: 25, class: 2}],
['bahncard-1st-50', {type: cards.BAHNCARD, discount: 50, class: 1}],
['bahncard-2nd-50', {type: cards.BAHNCARD, discount: 50, class: 2}],
['bahncard-1st-100', {type: cards.BAHNCARD, discount: 100, class: 1}],
['bahncard-2nd-100', {type: cards.BAHNCARD, discount: 100, class: 2}],
['vorteilscard', {type: cards.VORTEILSCARD}],
['halbtaxabo-railplus', {type: cards.HALBTAXABO}],
['halbtaxabo', {type: cards.HALBTAXABO}],
['voordeelurenabo-railplus', {type: cards.VOORDEELURENABO}],
['voordeelurenabo', {type: cards.VOORDEELURENABO}],
['shcard', {type: cards.SHCARD}],
['generalabonnement-1st', {type: cards.GENERALABONNEMENT, class: 1}],
['generalabonnement-2nd', {type: cards.GENERALABONNEMENT, class: 2}],
['generalabonnement', {type: cards.GENERALABONNEMENT}],
['nl-40', {type: cards.NL_40}],
['at-klimaticket', {type: cards.AT_KLIMATICKET}],
]);
const types = Array.from(typesByName.keys());
const parseLoyaltyCard = (key, val) => {
if (typesByName.has(val)) {
return typesByName.get(val);
}
if (!val) {
return null;
}
throw new Error(key + ' must be one of ' + types.join(', '));
};
const parseArrayOr = (parseEntry) => {
return (key, val) => {
if (Array.isArray(val)) {
return val.map(e => parseEntry(key, e));
}
return parseEntry(key, val);
};
};
const mapRouteParsers = (route, parsers) => {
if (route !== 'journeys') {
return parsers;
}
return {
...parsers,
firstClass: {
description: 'Search for first-class options?',
type: 'boolean',
default: 'false',
parse: parseBoolean,
},
loyaltyCard: {
description: 'Type of loyalty card in use.',
type: 'string',
enum: types,
defaultStr: '*none*',
parse: parseArrayOr(parseLoyaltyCard),
},
age: {
description: 'Age of traveller',
type: 'integer',
defaultStr: '*adult*',
parse: parseArrayOr(parseInteger),
},
};
};
export {
mapRouteParsers,
};

View file

@ -16,7 +16,7 @@ import {parseTrip} from '../parse/trip.js';
import {parseJourneyLeg} from '../parse/journey-leg.js';
import {parseJourney} from '../parse/journey.js';
import {parseLine} from '../parse/line.js';
import {parseLocation} from '../parse/location.js';
import {parseLocation, enrichStation} from '../parse/location.js';
import {parsePolyline} from '../parse/polyline.js';
import {parseOperator} from '../parse/operator.js';
import {parseRemarks, parseCancelled} from '../parse/remarks.js';
@ -37,7 +37,7 @@ import {formatTravellers} from '../format/travellers.js';
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
import {formatTransfers} from '../format/transfers.js';
const DEBUG = (/(^|,)hafas-client(,|$)/).test((typeof process !== 'undefined') ? (process.env.DEBUG || '') : '');
const DEBUG = (/(^|,)hafas-client(,|$)/).test(typeof process !== 'undefined' ? process.env.DEBUG || '' : '');
const logRequest = DEBUG
? (_, req, reqId) => console.error(String(req.body))
: () => { };
@ -82,6 +82,7 @@ const defaultProfile = {
parseLine,
parseStationName: id,
parseLocation,
enrichStation,
parsePolyline,
parseOperator,
parseRemarks,

View file

@ -7,9 +7,10 @@ const randomBytesHex = (nBytes = 8) => {
const array = new Uint8Array(nBytes);
crypto.getRandomValues(array);
return Array.from(array)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
.map((byte) => byte.toString(16)
.padStart(2, '0'))
.join('');
};
const checkIfResponseIsOk = (_) => {
const {

View file

@ -1,5 +1,5 @@
import dbnavBase from '../dbnav/base.json' with { type: "json" };
import dbregioguideBase from '../dbregioguide/base.json' with { type: "json" };
import dbnavBase from '../dbnav/base.json' with { type: 'json' };
import dbregioguideBase from '../dbregioguide/base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
// journeys()

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: "json" };
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatTripReq} from './trip-req.js';

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: "json" };
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatTripReq} from './trip-req.js';

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: "json" };
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatLocationFilter} from './location-filter.js';

View file

@ -8,7 +8,7 @@ const formatStationBoardReq = (ctx, station, type) => {
ortExtId: station,
zeit: profile.formatTimeOfDay(profile, opt.when),
datum: profile.formatDate(profile, opt.when),
mitVias: opt.stopovers || undefined,
mitVias: opt.stopovers || Boolean(opt.direction) || undefined,
verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}),
},
method: 'GET',

2748
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "db-vendo-client",
"description": "Client for bahn.de public transport APIs.",
"version": "6.4.0",
"version": "6.5.0",
"type": "module",
"main": "index.js",
"files": [
@ -68,12 +68,15 @@
"uuid": "^11.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^6.0.5",
"@stylistic/eslint-plugin": "^1.5.1",
"@stylistic/eslint-plugin": "^3.1.0",
"db-rest": "github:derhuerst/db-rest",
"eslint": "^8.56.0",
"eslint": "^9.20.1",
"globals": "^15.15.0",
"hafas-rest-api": "^5.1.3",
"is-coordinates": "^2.0.2",
"is-roughly-equal": "^0.1.0",
@ -83,8 +86,8 @@
"validate-fptf": "^3.0.0"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test-unit": "tap test/lib/*.js test/*.js test/format/*.js test/parse/*.js",
"test-integration": "VCR_MODE=playback tap test/e2e/*.js",
"test-integration:record": "VCR_MODE=record tap -t60 -j1 test/e2e/*.js",

View file

@ -40,7 +40,7 @@ const createParseArrOrDep = (prefix) => {
res.remarks = profile.parseRemarks(ctx, d);
}
if (opt.stopovers && Array.isArray(d.ueber)) {
if ((opt.stopovers || opt.direction) && Array.isArray(d.ueber)) {
const stopovers = d.ueber
.map(viaName => profile.parseStopover(ctx, {name: viaName}, null));

View file

@ -16,11 +16,11 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
const stops = pt.halte?.length && pt.halte || pt.stops?.length && pt.stops || [];
const res = {
origin: stops.length && profile.parseLocation(ctx, stops[0].ort || stops[0].station || stops[0])
|| pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt)
|| locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
|| pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt)
|| locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
destination: stops.length && profile.parseLocation(ctx, stops[stops.length - 1].ort || stops[stops.length - 1].station || stops[stops.length - 1])
|| pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt)
|| locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
|| pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt)
|| locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
};
const cancelledDep = stops.length && profile.parseCancelled(stops[0]);

View file

@ -14,7 +14,7 @@ const parseLocation = (ctx, l) => {
}
const lid = parse(l.id || l.locationId, {delimiter: '@'});
const res = {
let res = {
type: 'location',
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || l.bahnhofsId || '').replace(leadingZeros, '') || null,
};
@ -46,13 +46,7 @@ const parseLocation = (ctx, l) => {
stop.products = profile.parseProducts(ctx, l.products);
}
if (common && common.locations && common.locations[stop.id]) {
delete stop.type;
stop = {
...common.locations[stop.id],
...stop,
};
}
stop = profile.enrichStation(ctx, stop);
// TODO isMeta
// TODO entrances, lines
@ -70,6 +64,8 @@ const parseLocation = (ctx, l) => {
}
res.name = name;
res = enrichStation(ctx, res);
if (l.type === ADDRESS || lid.A == '2') {
res.address = name;
}
@ -80,6 +76,22 @@ const parseLocation = (ctx, l) => {
return res;
};
const enrichStation = (ctx, stop, locations) => {
const {common} = ctx;
const locs = locations || common?.locations;
const rich = locs && (locs[stop.id] || locs[stop.name]);
if (rich) {
delete stop.type;
delete stop.id;
stop = {
...rich,
...stop,
};
}
return stop;
};
export {
parseLocation,
enrichStation,
};

View file

@ -37,11 +37,11 @@ const parseRemarks = (ctx, ref) => {
let res = {
code: remark.code || remark.key || remark.id,
summary: remark.nachrichtKurz || remark.value || remark.ueberschrift || remark.text || remark.shortText
|| Object.values(remark.descriptions || {})
.shift()?.textShort,
|| Object.values(remark.descriptions || {})
.shift()?.textShort,
text: remark.nachrichtLang || remark.value || remark.text || remark.caption
|| Object.values(remark.descriptions || {})
.shift()?.text,
|| Object.values(remark.descriptions || {})
.shift()?.text,
type: type,
};
if (remark.modDateTime || remark.letzteAktualisierung) {
@ -208,9 +208,9 @@ const parseCancelled = (ref) => {
|| ref.journeyCancelled
|| (ref.risNotizen || ref.echtzeitNotizen || ref.meldungen) && Boolean(
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|| r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt'
|| r.text == 'Stop cancelled',
|| r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt'
|| r.text == 'Stop cancelled',
),
);
};

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-departures.json' with { type: "json" };
import res from './fixtures/dbnav-departures.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-refresh-journey.json' with { type: "json" };
import res from './fixtures/dbnav-refresh-journey.json' with { type: 'json' };
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-stop.json' with { type: "json" };
import res from './fixtures/dbnav-stop.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-trip.json' with { type: "json" };
import res from './fixtures/dbnav-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbnav-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbregioguide/index.js';
import res from './fixtures/dbregioguide-trip.json' with { type: "json" };
import res from './fixtures/dbregioguide-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbregioguide-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbris-arrivals.json' with { type: "json" };
import res from './fixtures/dbris-arrivals.json' with { type: 'json' };
import {dbArrivals as expected} from './fixtures/dbris-arrivals.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,10 +4,10 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-departures.json' with { type: "json" };
import res from './fixtures/dbweb-departures.json' with { type: 'json' };
import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: true});
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-journey.json' with { type: "json" };
import res from './fixtures/dbweb-journey.json' with { type: 'json' };
import {dbwebJourney as expected} from './fixtures/dbweb-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-refresh-journey.json' with { type: "json" };
import res from './fixtures/dbweb-refresh-journey.json' with { type: 'json' };
import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-trip.json' with { type: "json" };
import res from './fixtures/dbweb-trip.json' with { type: 'json' };
import {dbwebTrip as expected} from './fixtures/dbweb-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -395,17 +395,8 @@ tap.test('trip details', async (t) => {
});
tap.test('departures at Berlin Schwedter Str.', async (t) => {
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
});
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
});
await testDepartures({
@ -418,42 +409,24 @@ tap.test('departures at Berlin Schwedter Str.', async (t) => {
});
tap.test('departures with station object', async (t) => {
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
const res = await client.departures({
type: 'station',
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34,
},
}, {when});
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
});
const res = await client.departures({
type: 'station',
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34,
},
}, {when});
validate(t, res, 'departuresResponse', 'res');
t.end();
});
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `arrivals()` until stations are enriched
const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when,
});
if (res.arrivals[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when,
});
await testArrivals({

View file

@ -4,6 +4,7 @@ import {parseProducts} from '../../parse/products.js';
const profile = {
parseLocation: parse,
enrichStation: (ctx, stop) => stop,
parseStationName: (_, name) => name.toLowerCase(),
parseProducts,
products: [{