diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ef0b1b3..b60dff02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/license.md b/LICENSE.md similarity index 89% rename from license.md rename to LICENSE.md index 732e7f50..f9dc0fcf 100644 --- a/license.md +++ b/LICENSE.md @@ -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. diff --git a/api.js b/api.js index 398bb69a..2712e7de 100644 --- a/api.js +++ b/api.js @@ -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, ); diff --git a/docs/departures.md b/docs/departures.md index 41954a96..07f647b8 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -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 } ``` diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 8599d8df..3ff865d7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -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' diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..275cd9fa --- /dev/null +++ b/eslint.config.js @@ -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; diff --git a/format/loyalty-cards.js b/format/loyalty-cards.js index 03180eaa..bbd30465 100644 --- a/format/loyalty-cards.js +++ b/format/loyalty-cards.js @@ -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', diff --git a/index.js b/index.js index 8ee0fddd..a74a4c0f 100644 --- a/index.js +++ b/index.js @@ -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 = {}) => { 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) { @@ -65,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) { @@ -107,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 @@ -124,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.'); } @@ -206,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.'); } @@ -232,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.'); } @@ -258,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) { @@ -279,6 +301,8 @@ const createClient = (profile, userAgent, opt = {}) => { }; const nearby = async (location, opt = {}) => { + await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData); + validateLocation(location, 'location'); opt = Object.assign({ @@ -309,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.'); } @@ -336,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'); }; @@ -362,4 +390,5 @@ const createClient = (profile, userAgent, opt = {}) => { export { createClient, + loadEnrichedStationData, }; diff --git a/lib/api-parsers.js b/lib/api-parsers.js new file mode 100644 index 00000000..cfa41da0 --- /dev/null +++ b/lib/api-parsers.js @@ -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, +}; diff --git a/lib/default-profile.js b/lib/default-profile.js index ee44a85a..5f3fd2da 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -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'; @@ -82,6 +82,7 @@ const defaultProfile = { parseLine, parseStationName: id, parseLocation, + enrichStation, parsePolyline, parseOperator, parseRemarks, diff --git a/p/dbweb/station-board-req.js b/p/dbweb/station-board-req.js index 7a8c3101..7196e62b 100644 --- a/p/dbweb/station-board-req.js +++ b/p/dbweb/station-board-req.js @@ -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', diff --git a/package-lock.json b/package-lock.json index 263e212c..3d890710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "db-vendo-client", - "version": "6.4.0", + "version": "6.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "db-vendo-client", - "version": "6.4.0", + "version": "6.5.0", "license": "ISC", "dependencies": { "content-type": "^1.0.4", @@ -26,7 +26,7 @@ "@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": "^9.20.1", "globals": "^15.15.0", @@ -151,6 +151,19 @@ "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": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -976,8 +989,9 @@ "dev": true, "license": "MIT", "dependencies": { - "@stylistic/eslint-plugin-js": "^1.8.1", - "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, @@ -1782,17 +1796,17 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", + "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1800,13 +1814,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", + "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1814,32 +1828,46 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", + "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "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": { @@ -1869,17 +1897,17 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", + "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.24.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3393,13 +3421,13 @@ } }, "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==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -8015,16 +8043,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/tshy": { diff --git a/package.json b/package.json index 971d4ffb..a48f7f4d 100644 --- a/package.json +++ b/package.json @@ -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": [ @@ -73,7 +73,7 @@ "@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": "^9.20.1", "globals": "^15.15.0", @@ -86,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", diff --git a/parse/arrival-or-departure.js b/parse/arrival-or-departure.js index 9259a105..2dab416c 100644 --- a/parse/arrival-or-departure.js +++ b/parse/arrival-or-departure.js @@ -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)); diff --git a/parse/journey-leg.js b/parse/journey-leg.js index 910c9dd0..a62f2cb0 100644 --- a/parse/journey-leg.js +++ b/parse/journey-leg.js @@ -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]); diff --git a/parse/location.js b/parse/location.js index d15b9302..6a1f5e10 100644 --- a/parse/location.js +++ b/parse/location.js @@ -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, }; diff --git a/parse/remarks.js b/parse/remarks.js index b6a53345..cd3d6ae7 100644 --- a/parse/remarks.js +++ b/parse/remarks.js @@ -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', ), ); }; diff --git a/test/dbweb-departures.js b/test/dbweb-departures.js index b2c5f0e9..749a113e 100644 --- a/test/dbweb-departures.js +++ b/test/dbweb-departures.js @@ -7,7 +7,7 @@ import {profile as rawProfile} from '../p/dbweb/index.js'; 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 = { diff --git a/test/e2e/dbweb.js b/test/e2e/dbweb.js index 0c818635..4d2acbe8 100644 --- a/test/e2e/dbweb.js +++ b/test/e2e/dbweb.js @@ -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({ diff --git a/test/parse/location.js b/test/parse/location.js index f2eb4fed..14cad45b 100644 --- a/test/parse/location.js +++ b/test/parse/location.js @@ -4,6 +4,7 @@ import {parseProducts} from '../../parse/products.js'; const profile = { parseLocation: parse, + enrichStation: (ctx, stop) => stop, parseStationName: (_, name) => name.toLowerCase(), parseProducts, products: [{