Merge branch 'main' into main

This commit is contained in:
McToel 2025-02-19 01:00:22 +01:00 committed by GitHub
commit 406d24a051
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 355 additions and 165 deletions

View file

@ -6,6 +6,20 @@ env:
npm_config_cache: /tmp/npm-cache npm_config_cache: /tmp/npm-cache
jobs: 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: unit-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -25,13 +39,12 @@ jobs:
- id: cache-npm - id: cache-npm
name: restore npm cache name: restore npm cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
key: npm-cache-${{ github.ref_name }} key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-unit-tests
path: ${{ env.npm_config_cache }} path: ${{ env.npm_config_cache }}
- run: npm install - run: npm install
- run: npm run lint
- run: npm run test-unit - run: npm run test-unit
integration-tests: integration-tests:
@ -53,9 +66,9 @@ jobs:
- id: cache-npm - id: cache-npm
name: restore npm cache name: restore npm cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
key: npm-cache-${{ github.ref_name }} key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-integration-tests
path: ${{ env.npm_config_cache }} path: ${{ env.npm_config_cache }}
- run: npm install - run: npm install
@ -66,7 +79,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [18.x]
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -77,9 +90,9 @@ jobs:
- id: cache-npm - id: cache-npm
name: restore npm cache name: restore npm cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
key: npm-cache-${{ github.ref_name }} key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-e2e-tests
path: ${{ env.npm_config_cache }} path: ${{ env.npm_config_cache }}
- run: npm install - 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. 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 {createClient} from './index.js';
import {profile as dbProfile} from './p/db/index.js'; import {profile as dbProfile} from './p/db/index.js';
import {profile as dbnavProfile} from './p/dbnav/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 {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 = { const config = {
hostname: process.env.HOSTNAME || 'localhost', hostname: process.env.HOSTNAME || 'localhost',
@ -45,10 +22,15 @@ const config = {
mapRouteParsers, mapRouteParsers,
}; };
const profiles = {
db: dbProfile,
dbnav: dbnavProfile,
dbweb: dbwebProfile,
};
const start = async () => { const start = async () => {
const vendo = createClient( 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', process.env.USER_AGENT || 'link-to-your-project-or-email',
config, config,
); );

View file

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

View file

@ -385,13 +385,14 @@ paths:
- bahncard-2nd-25 - bahncard-2nd-25
- bahncard-1st-50 - bahncard-1st-50
- bahncard-2nd-50 - bahncard-2nd-50
- bahncard-1st-100
- bahncard-2nd-100
- vorteilscard - vorteilscard
- halbtaxabo-railplus
- halbtaxabo - halbtaxabo
- voordeelurenabo-railplus - generalabonnement-1st
- voordeelurenabo - generalabonnement-2nd
- shcard - nl-40
- generalabonnement - at-klimaticket
- name: firstClass - name: firstClass
in: query in: query
description: Search for first-class options? description: Search for first-class options?
@ -2087,7 +2088,7 @@ components:
type: string type: string
format: date-time format: date-time
direction: direction:
description: only show departures heading to this station description: only show departures heading to this station, only supported for `dbweb` profile
default: undefined default: undefined
type: string type: string
line: line:
@ -2124,7 +2125,7 @@ components:
type: boolean type: boolean
includeRelatedStations: includeRelatedStations:
description: departures at related stations description: departures at related stations
default: false default: true
type: boolean type: boolean
products: products:
$ref: '#/components/schemas/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'), VOORDEELURENABO: Symbol('Voordeelurenabo'),
SHCARD: Symbol('SH-Card'), SHCARD: Symbol('SH-Card'),
GENERALABONNEMENT: Symbol('General-Abonnement'), GENERALABONNEMENT: Symbol('General-Abonnement'),
NL_40: Symbol('NL-40%'),
AT_KLIMATICKET: Symbol('AT-KlimaTicket'),
}; };
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
const formatLoyaltyCard = (data) => { const formatLoyaltyCard = (data) => {
if (!data) { if (!data) {
return { return {
@ -19,7 +20,7 @@ const formatLoyaltyCard = (data) => {
const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2'; const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2';
if (data.type.toString() === c.BAHNCARD.toString()) { if (data.type.toString() === c.BAHNCARD.toString()) {
return { return {
art: 'BAHNCARD' + data.discount, art: 'BAHNCARD' + (data.business ? 'BUSINESS' : '') + data.discount,
klasse: cls, klasse: cls,
}; };
} }
@ -35,13 +36,24 @@ const formatLoyaltyCard = (data) => {
klasse: 'KLASSENLOS', klasse: 'KLASSENLOS',
}; };
} }
// TODO Rest
if (data.type.toString() === c.GENERALABONNEMENT.toString()) { if (data.type.toString() === c.GENERALABONNEMENT.toString()) {
return { return {
art: 'CH-GENERAL-ABONNEMENT', art: 'CH-GENERAL-ABONNEMENT',
klasse: cls, 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 { return {
art: 'KEINE_ERMAESSIGUNG', art: 'KEINE_ERMAESSIGUNG',
klasse: 'KLASSENLOS', klasse: 'KLASSENLOS',

View file

@ -46,15 +46,23 @@ const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
}); });
}); });
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 = {}) => { const createClient = (profile, userAgent, opt = {}) => {
profile = Object.assign({}, defaultProfile, profile); profile = Object.assign({}, defaultProfile, profile);
validateProfile(profile); validateProfile(profile);
const common = {}; const common = {};
if (opt.enrichStations !== false) { let shouldLoadEnrichedStationData = false;
loadEnrichedStationData(profile) if (typeof opt.enrichStations === 'function') {
.then(locations => { profile.enrichStation = opt.enrichStations;
common.locations = locations; } else if (opt.enrichStations !== false) {
}); shouldLoadEnrichedStationData = true;
} }
if ('string' !== typeof userAgent) { if ('string' !== typeof userAgent) {
@ -65,6 +73,7 @@ const createClient = (profile, userAgent, opt = {}) => {
} }
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => { const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(station) && station.id) { if (isObj(station) && station.id) {
station = station.id; station = station.id;
} else if ('string' !== typeof station) { } else if ('string' !== typeof station) {
@ -107,9 +116,15 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req); const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res}; 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 .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 { return {
[resultsField]: results, [resultsField]: results,
realtimeDataUpdatedAt: null, // TODO realtimeDataUpdatedAt: null, // TODO
@ -124,6 +139,7 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const journeys = async (from, to, opt = {}) => { const journeys = async (from, to, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('earlierThan' in opt && 'laterThan' in opt) { if ('earlierThan' in opt && 'laterThan' in opt) {
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.'); throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.');
} }
@ -206,6 +222,8 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const refreshJourney = async (refreshToken, opt = {}) => { const refreshJourney = async (refreshToken, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('string' !== typeof refreshToken || !refreshToken) { if ('string' !== typeof refreshToken || !refreshToken) {
throw new TypeError('refreshToken must be a non-empty string.'); throw new TypeError('refreshToken must be a non-empty string.');
} }
@ -232,6 +250,8 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const locations = async (query, opt = {}) => { const locations = async (query, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(query)) { if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.'); throw new TypeError('query must be a non-empty string.');
} }
@ -258,6 +278,8 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const stop = async (stop, opt = {}) => { const stop = async (stop, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(stop) && stop.id) { if (isObj(stop) && stop.id) {
stop = stop.id; stop = stop.id;
} else if ('string' !== typeof stop) { } else if ('string' !== typeof stop) {
@ -279,6 +301,8 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const nearby = async (location, opt = {}) => { const nearby = async (location, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
validateLocation(location, 'location'); validateLocation(location, 'location');
opt = Object.assign({ opt = Object.assign({
@ -309,6 +333,8 @@ const createClient = (profile, userAgent, opt = {}) => {
}; };
const trip = async (id, opt = {}) => { const trip = async (id, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(id)) { if (!isNonEmptyString(id)) {
throw new TypeError('id must be a non-empty string.'); throw new TypeError('id must be a non-empty string.');
} }
@ -336,6 +362,8 @@ const createClient = (profile, userAgent, opt = {}) => {
// todo [breaking]: rename to trips()? // todo [breaking]: rename to trips()?
const tripsByName = async (_lineNameOrFahrtNr = '*', _opt = {}) => { const tripsByName = async (_lineNameOrFahrtNr = '*', _opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
throw new Error('not implemented'); throw new Error('not implemented');
}; };
@ -362,4 +390,5 @@ const createClient = (profile, userAgent, opt = {}) => {
export { export {
createClient, 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 {parseJourneyLeg} from '../parse/journey-leg.js';
import {parseJourney} from '../parse/journey.js'; import {parseJourney} from '../parse/journey.js';
import {parseLine} from '../parse/line.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 {parsePolyline} from '../parse/polyline.js';
import {parseOperator} from '../parse/operator.js'; import {parseOperator} from '../parse/operator.js';
import {parseRemarks, parseCancelled} from '../parse/remarks.js'; import {parseRemarks, parseCancelled} from '../parse/remarks.js';
@ -82,6 +82,7 @@ const defaultProfile = {
parseLine, parseLine,
parseStationName: id, parseStationName: id,
parseLocation, parseLocation,
enrichStation,
parsePolyline, parsePolyline,
parseOperator, parseOperator,
parseRemarks, parseRemarks,

View file

@ -8,7 +8,7 @@ const formatStationBoardReq = (ctx, station, type) => {
ortExtId: station, ortExtId: station,
zeit: profile.formatTimeOfDay(profile, opt.when), zeit: profile.formatTimeOfDay(profile, opt.when),
datum: profile.formatDate(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 || {}), verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}),
}, },
method: 'GET', method: 'GET',

116
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "db-vendo-client", "name": "db-vendo-client",
"version": "6.4.0", "version": "6.5.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "db-vendo-client", "name": "db-vendo-client",
"version": "6.4.0", "version": "6.5.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"content-type": "^1.0.4", "content-type": "^1.0.4",
@ -26,7 +26,7 @@
"@pollyjs/adapter-node-http": "^6.0.5", "@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5", "@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^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", "db-rest": "github:derhuerst/db-rest",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"globals": "^15.15.0", "globals": "^15.15.0",
@ -151,6 +151,19 @@
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
} }
}, },
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint-community/regexpp": { "node_modules/@eslint-community/regexpp": {
"version": "4.12.1", "version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
@ -976,8 +989,9 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@stylistic/eslint-plugin-js": "^1.8.1", "@typescript-eslint/utils": "^8.13.0",
"@types/eslint": "^8.56.10", "eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.2"
}, },
@ -1782,17 +1796,17 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "6.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.21.0", "@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "6.21.0" "@typescript-eslint/visitor-keys": "8.24.0"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -1800,13 +1814,13 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "6.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -1814,32 +1828,46 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "6.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.21.0", "@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "6.21.0", "@typescript-eslint/visitor-keys": "8.24.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"minimatch": "9.0.3", "minimatch": "^9.0.4",
"semver": "^7.5.4", "semver": "^7.6.0",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^2.0.1"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependenciesMeta": { "peerDependencies": {
"typescript": { "typescript": ">=4.8.4 <5.8.0"
"optional": true }
} },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
@ -1869,17 +1897,17 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "6.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.21.0", "@typescript-eslint/types": "8.24.0",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -3393,13 +3421,13 @@
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "3.4.3", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
@ -8015,16 +8043,16 @@
} }
}, },
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "1.4.3", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16" "node": ">=18.12"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": ">=4.2.0" "typescript": ">=4.8.4"
} }
}, },
"node_modules/tshy": { "node_modules/tshy": {

View file

@ -1,7 +1,7 @@
{ {
"name": "db-vendo-client", "name": "db-vendo-client",
"description": "Client for bahn.de public transport APIs.", "description": "Client for bahn.de public transport APIs.",
"version": "6.4.0", "version": "6.5.0",
"type": "module", "type": "module",
"main": "index.js", "main": "index.js",
"files": [ "files": [
@ -73,7 +73,7 @@
"@pollyjs/adapter-node-http": "^6.0.5", "@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5", "@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^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", "db-rest": "github:derhuerst/db-rest",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"globals": "^15.15.0", "globals": "^15.15.0",
@ -86,8 +86,8 @@
"validate-fptf": "^3.0.0" "validate-fptf": "^3.0.0"
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint",
"lint:fix": "eslint . --fix", "lint:fix": "eslint --fix",
"test-unit": "tap test/lib/*.js test/*.js test/format/*.js test/parse/*.js", "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": "VCR_MODE=playback tap test/e2e/*.js",
"test-integration:record": "VCR_MODE=record tap -t60 -j1 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); 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 const stopovers = d.ueber
.map(viaName => profile.parseStopover(ctx, {name: viaName}, null)); .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 stops = pt.halte?.length && pt.halte || pt.stops?.length && pt.stops || [];
const res = { const res = {
origin: stops.length && profile.parseLocation(ctx, stops[0].ort || stops[0].station || stops[0]) origin: stops.length && profile.parseLocation(ctx, stops[0].ort || stops[0].station || stops[0])
|| pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt) || pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt)
|| locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations), || 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]) 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) || pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt)
|| locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations), || locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
}; };
const cancelledDep = stops.length && profile.parseCancelled(stops[0]); 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 lid = parse(l.id || l.locationId, {delimiter: '@'});
const res = { let res = {
type: 'location', type: 'location',
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || l.bahnhofsId || '').replace(leadingZeros, '') || null, 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); stop.products = profile.parseProducts(ctx, l.products);
} }
if (common && common.locations && common.locations[stop.id]) { stop = profile.enrichStation(ctx, stop);
delete stop.type;
stop = {
...common.locations[stop.id],
...stop,
};
}
// TODO isMeta // TODO isMeta
// TODO entrances, lines // TODO entrances, lines
@ -70,6 +64,8 @@ const parseLocation = (ctx, l) => {
} }
res.name = name; res.name = name;
res = enrichStation(ctx, res);
if (l.type === ADDRESS || lid.A == '2') { if (l.type === ADDRESS || lid.A == '2') {
res.address = name; res.address = name;
} }
@ -80,6 +76,22 @@ const parseLocation = (ctx, l) => {
return res; 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 { export {
parseLocation, parseLocation,
enrichStation,
}; };

View file

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

View file

@ -7,7 +7,7 @@ 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'; 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 {profile} = client;
const opt = { const opt = {

View file

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

View file

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