gtfs 4.11.2 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
  strategy:
9
9
  matrix:
10
- node: [ 20, 21 ]
10
+ node: [ 20, 22 ]
11
11
  name: Use Node.js ${{ matrix.node }}
12
12
  steps:
13
13
  - uses: actions/checkout@v4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.12.0] - 2024-06-17
9
+
10
+ ### Added
11
+
12
+ - Support for `service_id` in `getRoutes`
13
+
14
+ ### Changed
15
+
16
+ - Use tempy for temporary directory creation
17
+
18
+ ## [4.11.3] - 2024-06-13
19
+
20
+ ### Fixed
21
+
22
+ - Better tests for locations.geojson
23
+ - Handle unknown filename extensions in models
24
+
8
25
  ## [4.11.2] - 2024-06-13
9
26
 
10
27
  ### Added
package/lib/export.js CHANGED
@@ -117,6 +117,10 @@ const exportGtfs = async (initialConfig) => {
117
117
  } else if (model.filenameExtension === 'geojson') {
118
118
  const fileText = lines?.[0].geojson ?? '';
119
119
  await writeFile(filePath, fileText);
120
+ } else {
121
+ throw new Error(
122
+ `Unexpected filename extension: ${model.filenameExtension}`,
123
+ );
120
124
  }
121
125
 
122
126
  log(`Exporting - ${model.filenameBase}.${model.filenameExtension}\r`);
@@ -17,14 +17,30 @@ function buildStoptimeSubquery(query) {
17
17
  }
18
18
 
19
19
  function buildTripSubquery(query) {
20
- return `SELECT DISTINCT route_id FROM trips WHERE trip_id IN (${buildStoptimeSubquery(
21
- query,
22
- )})`;
20
+ let whereClause = '';
21
+ const tripQuery = omit(query, ['stop_id']);
22
+ const stoptimeQuery = pick(query, ['stop_id']);
23
+
24
+ const whereClauses = Object.entries(tripQuery).map(([key, value]) =>
25
+ formatWhereClause(key, value),
26
+ );
27
+
28
+ if (Object.values(stoptimeQuery).length > 0) {
29
+ whereClauses.push(`trip_id IN (${buildStoptimeSubquery(stoptimeQuery)})`);
30
+ }
31
+
32
+ if (whereClauses.length > 0) {
33
+ whereClause = `WHERE ${whereClauses.join(' AND ')}`;
34
+ }
35
+
36
+ return `SELECT DISTINCT route_id FROM trips ${whereClause}`;
23
37
  }
24
38
 
25
39
  /*
26
40
  * Returns an array of routes that match the query parameters. A `stop_id`
27
41
  * query parameter may be passed to find all routes that contain that stop.
42
+ * A `service_id` query parameter may be passed to limit routes to specific
43
+ * calendars.
28
44
  */
29
45
  export function getRoutes(query = {}, fields = [], orderBy = [], options = {}) {
30
46
  const db = options.db ?? openDb();
@@ -33,15 +49,15 @@ export function getRoutes(query = {}, fields = [], orderBy = [], options = {}) {
33
49
  let whereClause = '';
34
50
  const orderByClause = formatOrderByClause(orderBy);
35
51
 
36
- const routeQuery = omit(query, ['stop_id']);
37
- const stoptimeQuery = pick(query, ['stop_id']);
52
+ const routeQuery = omit(query, ['stop_id', 'service_id']);
53
+ const tripQuery = pick(query, ['stop_id', 'service_id']);
38
54
 
39
55
  const whereClauses = Object.entries(routeQuery).map(([key, value]) =>
40
56
  formatWhereClause(key, value),
41
57
  );
42
58
 
43
- if (Object.values(stoptimeQuery).length > 0) {
44
- whereClauses.push(`route_id IN (${buildTripSubquery(stoptimeQuery)})`);
59
+ if (Object.values(tripQuery).length > 0) {
60
+ whereClauses.push(`route_id IN (${buildTripSubquery(tripQuery)})`);
45
61
  }
46
62
 
47
63
  if (whereClauses.length > 0) {
package/lib/import.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { createReadStream, existsSync, lstatSync } from 'node:fs';
3
- import { cp, readdir, rename, readFile, writeFile } from 'node:fs/promises';
3
+ import { cp, readdir, rename, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import fetch from 'node-fetch';
5
5
  import { parse } from 'csv-parse';
6
6
  import pluralize from 'pluralize';
7
7
  import stripBomStream from 'strip-bom-stream';
8
- import { dir } from 'tmp-promise';
8
+ import { temporaryDirectory } from 'tempy';
9
9
  import untildify from 'untildify';
10
10
  import mapSeries from 'promise-map-series';
11
11
  import GtfsRealtimeBindings from 'gtfs-realtime-bindings';
@@ -604,8 +604,46 @@ const importFiles = (task) =>
604
604
  `Importing - ${model.filenameBase}.${model.filenameExtension}\r`,
605
605
  );
606
606
 
607
- // Handle geojson files
608
- if (model.filenameExtension === 'geojson') {
607
+ if (model.filenameExtension === 'txt') {
608
+ const parser = parse({
609
+ columns: true,
610
+ relax_quotes: true,
611
+ trim: true,
612
+ skip_empty_lines: true,
613
+ ...task.csvOptions,
614
+ });
615
+
616
+ parser.on('readable', () => {
617
+ let record;
618
+
619
+ while ((record = parser.read())) {
620
+ try {
621
+ totalLineCount += 1;
622
+ lines.push(formatLine(record, model, totalLineCount));
623
+ // If we have a bunch of lines ready to insert, then do it
624
+ if (lines.length >= maxInsertVariables / model.schema.length) {
625
+ importLines(task, lines, model, totalLineCount);
626
+ }
627
+ } catch (error) {
628
+ reject(error);
629
+ }
630
+ }
631
+ });
632
+
633
+ parser.on('end', () => {
634
+ try {
635
+ // Insert all remaining lines
636
+ importLines(task, lines, model, totalLineCount);
637
+ } catch (error) {
638
+ reject(error);
639
+ }
640
+ resolve();
641
+ });
642
+
643
+ parser.on('error', reject);
644
+
645
+ createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
646
+ } else if (model.filenameExtension === 'geojson') {
609
647
  readFile(filepath, 'utf8')
610
648
  .then((data) => {
611
649
  if (isValidJSON(data) === false) {
@@ -620,46 +658,11 @@ const importFiles = (task) =>
620
658
  resolve();
621
659
  })
622
660
  .catch(reject);
661
+ } else {
662
+ reject(
663
+ new Error(`Unsupported file type: ${model.filenameExtension}`),
664
+ );
623
665
  }
624
-
625
- const parser = parse({
626
- columns: true,
627
- relax_quotes: true,
628
- trim: true,
629
- skip_empty_lines: true,
630
- ...task.csvOptions,
631
- });
632
-
633
- parser.on('readable', () => {
634
- let record;
635
-
636
- while ((record = parser.read())) {
637
- try {
638
- totalLineCount += 1;
639
- lines.push(formatLine(record, model, totalLineCount));
640
- // If we have a bunch of lines ready to insert, then do it
641
- if (lines.length >= maxInsertVariables / model.schema.length) {
642
- importLines(task, lines, model, totalLineCount);
643
- }
644
- } catch (error) {
645
- reject(error);
646
- }
647
- }
648
- });
649
-
650
- parser.on('end', () => {
651
- try {
652
- // Insert all remaining lines
653
- importLines(task, lines, model, totalLineCount);
654
- } catch (error) {
655
- reject(error);
656
- }
657
- resolve();
658
- });
659
-
660
- parser.on('error', reject);
661
-
662
- createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
663
666
  }),
664
667
  );
665
668
 
@@ -684,7 +687,7 @@ export async function importGtfs(initialConfig) {
684
687
  createTables(db);
685
688
 
686
689
  await mapSeries(config.agencies, async (agency) => {
687
- const { path, cleanup } = await dir({ unsafeCleanup: true });
690
+ const tempPath = temporaryDirectory();
688
691
 
689
692
  const task = {
690
693
  exclude: agency.exclude,
@@ -692,7 +695,7 @@ export async function importGtfs(initialConfig) {
692
695
  headers: agency.headers || false,
693
696
  realtime_headers: agency.realtimeHeaders || false,
694
697
  realtime_urls: agency.realtimeUrls || false,
695
- downloadDir: path,
698
+ downloadDir: tempPath,
696
699
  downloadTimeout: config.downloadTimeout,
697
700
  path: agency.path,
698
701
  csvOptions: config.csvOptions || {},
@@ -716,7 +719,7 @@ export async function importGtfs(initialConfig) {
716
719
  await updateRealtimeData(task);
717
720
  }
718
721
 
719
- cleanup();
722
+ await rm(tempPath, { recursive: true });
720
723
  } catch (error) {
721
724
  if (config.ignoreErrors) {
722
725
  logError(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs",
3
- "version": "4.11.2",
3
+ "version": "4.12.0",
4
4
  "description": "Import GTFS transit data into SQLite and query routes, stops, times, fares and more",
5
5
  "keywords": [
6
6
  "transit",
@@ -91,7 +91,7 @@
91
91
  "sanitize-filename": "^1.6.3",
92
92
  "sqlstring-sqlite": "^0.1.1",
93
93
  "strip-bom-stream": "^5.0.0",
94
- "tmp-promise": "^3.0.3",
94
+ "tempy": "^3.1.0",
95
95
  "untildify": "^5.0.0",
96
96
  "yargs": "^17.7.2",
97
97
  "yoctocolors": "^2.0.2"
@@ -17,7 +17,6 @@ const locationsConfig = {
17
17
  },
18
18
  ],
19
19
  verbose: false,
20
- sqlitePath: '/tmp/locations',
21
20
  };
22
21
 
23
22
  describe('getLocations():', () => {
@@ -25,7 +24,6 @@ describe('getLocations():', () => {
25
24
  openDb(locationsConfig);
26
25
 
27
26
  // Add locations.geojson to test GTFS dataset
28
- const temporaryDir = path.join(import.meta.dirname, '../fixture/tmp2/');
29
27
  await prepDirectory(temporaryDir);
30
28
  await unzip(config.agencies[0].path, temporaryDir);
31
29