2022-12-19 13:23:13 +01:00
import computeEtag from 'etag'
import serveBuffer from 'serve-buffer'
import autocomplete from 'db-stations-autocomplete'
import _cliNative from 'cli-native'
const { to : parse } = _cliNative
import createFilter from 'db-stations/create-filter.js'
import { pStations } from '../lib/db-stations.js'
2020-05-01 18:10:21 +02:00
const JSON _MIME = 'application/json'
const NDJSON _MIME = 'application/x-ndjson'
2022-11-22 16:07:33 +01:00
// todo: DRY with vbb-rest/routes/stations.js?
2020-05-01 18:10:21 +02:00
const toNdjsonBuf = ( data ) => {
const chunks = [ ]
let i = 0 , bytes = 0
for ( const id in data ) {
const sep = i ++ === 0 ? '' : '\n'
const buf = Buffer . from ( sep + JSON . stringify ( data [ id ] ) , 'utf8' )
chunks . push ( buf )
bytes += buf . length
}
return Buffer . concat ( chunks , bytes )
}
2022-12-19 13:23:13 +01:00
const pAllStations = pStations . then ( ( { data , timeModified } ) => {
2020-05-01 18:10:21 +02:00
const asJson = Buffer . from ( JSON . stringify ( data ) , 'utf8' )
const asNdjson = toNdjsonBuf ( data )
return {
stations : data ,
timeModified ,
asJson : { data : asJson , etag : computeEtag ( asJson ) } ,
asNdjson : { data : asNdjson , etag : computeEtag ( asNdjson ) } ,
}
} )
. catch ( ( err ) => {
console . error ( err )
process . exit ( 1 )
} )
const err = ( msg , statusCode = 500 ) => {
const err = new Error ( msg )
err . statusCode = statusCode
return err
}
const complete = ( req , res , next , q , allStations , onStation , onEnd ) => {
const limit = q . results && parseInt ( q . results ) || 3
const fuzzy = parse ( q . fuzzy ) === true
const completion = parse ( q . completion ) !== false
const results = autocomplete ( q . query , limit , fuzzy , completion )
for ( const result of results ) {
const station = allStations [ result . id ]
if ( ! station ) continue
Object . assign ( result , station )
onStation ( result )
}
onEnd ( )
}
const filter = ( req , res , next , q , allStations , onStation , onEnd ) => {
const selector = Object . create ( null )
for ( const prop in q ) selector [ prop ] = parse ( q [ prop ] )
const filter = createFilter ( selector )
for ( const id in allStations ) {
const station = allStations [ id ]
if ( filter ( station ) ) onStation ( station )
}
onEnd ( )
}
const stationsRoute = ( req , res , next ) => {
const t = req . accepts ( [ JSON _MIME , NDJSON _MIME ] )
if ( t !== JSON _MIME && t !== NDJSON _MIME ) {
return next ( err ( JSON + ' or ' + NDJSON _MIME , 406 ) )
}
const head = t === JSON _MIME ? '{\n' : ''
const sep = t === JSON _MIME ? ',\n' : '\n'
const tail = t === JSON _MIME ? '\n}\n' : '\n'
let i = 0
const onStation = ( s ) => {
const j = JSON . stringify ( s )
const field = t === JSON _MIME ? ` " ${ s . id } ": ` : ''
res . write ( ` ${ i ++ === 0 ? head : sep } ${ field } ${ j } ` )
}
const onEnd = ( ) => {
if ( i > 0 ) res . end ( tail )
else res . end ( head + tail )
}
const q = req . query
pAllStations
. then ( ( { stations , timeModified , asJson , asNdjson } ) => {
res . setHeader ( 'Last-Modified' , timeModified . toUTCString ( ) )
2022-11-22 16:01:29 +01:00
res . setHeader ( 'Content-Type' , t )
2022-11-22 15:58:43 +01:00
if ( Object . keys ( q ) . length === 0 ) {
2020-05-01 18:10:21 +02:00
const data = t === JSON _MIME ? asJson . data : asNdjson . data
const etag = t === JSON _MIME ? asJson . etag : asNdjson . etag
2022-11-22 16:01:29 +01:00
serveBuffer ( req , res , data , {
timeModified ,
etag ,
contentType : t ,
} )
2020-05-01 18:10:21 +02:00
} else if ( q . query ) {
complete ( req , res , next , q , stations , onStation , onEnd )
} else {
filter ( req , res , next , q , stations , onStation , onEnd )
}
} )
. catch ( next )
}
2021-02-04 19:24:13 +01:00
stationsRoute . openapiPaths = {
'/stations' : {
get : {
summary : 'Autocompletes stops/stations by name or filters stops/stations.' ,
description : ` \
2022-11-22 15:38:58 +01:00
If the \ ` query \` parameter is used, it will use [ \` db-stations-autocomplete@2 \` ](https://github.com/derhuerst/db-stations-autocomplete/tree/2.2.0) to autocomplete *Deutsche Bahn*-operated stops/stations by name. Otherwise, it will filter the stops/stations in [ \` db-stations@3 \` ](https://github.com/derhuerst/db-stations/tree/3.0.1).
2021-02-04 19:24:13 +01:00
Instead of receiving a JSON response , you can request [ newline - delimited JSON ] ( http : //ndjson.org) by sending \`Accept: application/x-ndjson\`.`,
parameters : [ {
name : 'query' ,
in : 'query' ,
2022-11-22 15:38:58 +01:00
description : 'Find stations by name using [`db-stations-autocomplete@2`](https://github.com/derhuerst/db-stations-autocomplete/tree/2.2.0).' ,
2021-02-04 19:24:13 +01:00
schema : {
type : 'string' ,
} ,
} , {
name : 'limit' ,
in : 'query' ,
description : '*If `query` is used:* Return at most `n` stations.' ,
schema : {
type : 'integer' ,
default : 3 ,
} ,
} , {
name : 'fuzzy' ,
in : 'query' ,
description : '*If `query` is used:* Find stations despite typos.' ,
schema : {
type : 'boolean' ,
default : false ,
} ,
} , {
name : 'completion' ,
in : 'query' ,
description : '*If `query` is used:* Autocomplete stations.' ,
schema : {
type : 'boolean' ,
default : true ,
} ,
} ] ,
responses : {
'2XX' : {
2022-11-22 15:58:43 +01:00
description : 'An object of stops/stations in the [`db-stations@3` format](https://github.com/derhuerst/db-stations/blob/3.0.1/readme.md), with their IDs as the keys.' ,
2021-02-04 19:24:13 +01:00
content : {
'application/json' : {
schema : {
2022-11-22 15:58:43 +01:00
// todo
type : 'object' ,
2021-02-04 19:24:13 +01:00
} ,
// todo: example(s)
} ,
2022-12-19 13:07:00 +01:00
'application/x-ndjson' : {
schema : {
type : 'string' ,
} ,
} ,
2021-02-04 19:24:13 +01:00
} ,
} ,
2022-12-19 13:07:00 +01:00
// todo: non-2xx response
2021-02-04 19:24:13 +01:00
} ,
} ,
} ,
}
2020-04-30 15:48:06 +02:00
stationsRoute . queryParameters = {
query : {
2022-11-22 15:38:58 +01:00
description : 'Find stations by name using [`db-stations-autocomplete@2`](https://github.com/derhuerst/db-stations-autocomplete/tree/2.2.0).' ,
2020-04-30 15:48:06 +02:00
type : 'string' ,
defaultStr : '– ' ,
} ,
limit : {
description : '*If `query` is used:* Return at most `n` stations.' ,
type : 'number' ,
default : 3 ,
} ,
fuzzy : {
description : '*If `query` is used:* Find stations despite typos.' ,
type : 'boolean' ,
default : false ,
} ,
completion : {
description : '*If `query` is used:* Autocomplete stations.' ,
type : 'boolean' ,
default : true ,
} ,
}
2022-12-19 13:23:13 +01:00
export {
stationsRoute as route ,
}