gtfs 4.7.1 → 4.8.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.
package/@types/index.d.ts CHANGED
@@ -63,6 +63,11 @@ export interface DbConfig {
63
63
  * A path to a SQLite database. Defaults to using an in-memory database.
64
64
  */
65
65
  sqlitePath?: string;
66
+
67
+ /**
68
+ * A better-sqlite3 database object. If provided, sqlitePath will be ignored.
69
+ */
70
+ db?: Database.Database;
66
71
  }
67
72
 
68
73
  export interface ExportConfig extends DbConfig, VerboseConfig {
@@ -114,6 +119,16 @@ export interface ImportConfig extends DbConfig, VerboseConfig {
114
119
  * Options passed to csv-parse for parsing GTFS CSV files.
115
120
  */
116
121
  csvOptions?: CsvParse.Options;
122
+
123
+ /**
124
+ * A timeout in milliseconds for downloading GTFS files. Defaults to no timeout.
125
+ */
126
+ downloadTimeout?: number;
127
+
128
+ /**
129
+ * Whether or not to ignore errors during import. Defaults to false.
130
+ */
131
+ ignoreErrors?: boolean;
117
132
  }
118
133
 
119
134
  export interface QueryOptions {
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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.8.0] - 2024-03-10
9
+
10
+ ### Added
11
+
12
+ - `ignoreErrors` config option
13
+ - `downloadTimeout` config option
14
+ - `db` config option
15
+
16
+ ## [4.7.2] - 2024-03-05
17
+
18
+ ### Updated
19
+
20
+ - Show info about ignoreDuplicates option when primary key constraint issue happens
21
+ - Dependency updates
22
+
8
23
  ## [4.7.1] - 2024-02-18
9
24
 
10
25
  ### Updated
package/README.md CHANGED
@@ -161,14 +161,17 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
161
161
 
162
162
  cp config-sample.json config.json
163
163
 
164
- | option | type | description |
165
- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
166
- | [`agencies`](#agencies) | array | An array of GTFS files to be imported, and which files to exclude. |
167
- | [`csvOptions`](#csvOptions) | object | Options passed to `csv-parse` for parsing GTFS CSV files. Optional. |
168
- | [`exportPath`](#exportPath) | string | A path to a directory to put exported GTFS files. Optional, defaults to `gtfs-export/<agency_name>`. |
169
- | [`ignoreDuplicates`](#ignoreduplicates) | boolean | Whether or not to ignore unique constraints on ids when importing GTFS, such as `trip_id`, `calendar_id`. Optional, defaults to false. |
170
- | [`sqlitePath`](#sqlitePath) | string | A path to an SQLite database. Optional, defaults to using an in-memory database. |
171
- | [`verbose`](#verbose) | boolean | Whether or not to print output to the console. Optional, defaults to true. |
164
+ | option | type | description |
165
+ | --------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
+ | [`agencies`](#agencies) | array | An array of GTFS files to be imported, and which files to exclude. |
167
+ | [`csvOptions`](#csvOptions) | object | Options passed to `csv-parse` for parsing GTFS CSV files. Optional. |
168
+ | [`db`](#db) | database instance | An existing database instance to use instead of relying on node-gtfs to connect. Optional. |
169
+ | [`downloadTimeout`](#downloadtimeout) | integer | The number of milliseconds to wait before throwing an error when downloading GTFS. Optional. |
170
+ | [`exportPath`](#exportPath) | string | A path to a directory to put exported GTFS files. Optional, defaults to `gtfs-export/<agency_name>`. |
171
+ | [`ignoreDuplicates`](#ignoreduplicates) | boolean | Whether or not to ignore unique constraints on ids when importing GTFS, such as `trip_id`, `calendar_id`. Optional, defaults to false. |
172
+ | [`ignoreErrors`](#ignoreerrors) | boolean | Whether or not to ignore errors during the import process. If true, when importing multiple agencies, failed agencies will be skipped. Optional, defaults to false. |
173
+ | [`sqlitePath`](#sqlitePath) | string | A path to an SQLite database. Optional, defaults to using an in-memory database. |
174
+ | [`verbose`](#verbose) | boolean | Whether or not to print output to the console. Optional, defaults to true. |
172
175
 
173
176
  ### agencies
174
177
 
@@ -320,6 +323,60 @@ For instance, if you wanted to skip importing invalid lines in the GTFS file:
320
323
 
321
324
  See [full list of options](https://csv.js.org/parse/options/).
322
325
 
326
+ ### db
327
+
328
+ {Database Instance} When passing configuration to `importGtfs` in javascript, you can pass a `db` parameter with an existing database instance. This is not possible using a json configuration file Optional.
329
+
330
+ ```js
331
+ // Using better-sqlite3 to open database
332
+ import { importGtfs } from 'gtfs';
333
+ import Database from 'better-sqlite3';
334
+
335
+ const db = new Database('/path/to/database');
336
+
337
+ importGtfs({
338
+ agencies: [
339
+ {
340
+ path: '/path/to/the/unzipped/gtfs/',
341
+ },
342
+ ],
343
+ db: db,
344
+ });
345
+ ```
346
+
347
+ ```js
348
+ // Using `openDb` from node-gtfs to open database
349
+ import { importGtfs, openDb } from 'gtfs';
350
+
351
+ const db = openDb({
352
+ sqlitePath: '/path/to/database',
353
+ });
354
+
355
+ importGtfs({
356
+ agencies: [
357
+ {
358
+ path: '/path/to/the/unzipped/gtfs/',
359
+ },
360
+ ],
361
+ db: db,
362
+ });
363
+ ```
364
+
365
+ ### downloadTimeout
366
+
367
+ {Integer} A number of milliseconds to wait when downloading GTFS before throwing an error. Optional.
368
+
369
+ ```json
370
+ {
371
+ "agencies": [
372
+ {
373
+ "path": "/path/to/the/unzipped/gtfs/"
374
+ }
375
+ ],
376
+ "downloadTimeout": 5000
377
+ }
378
+ ```
379
+
323
380
  ### exportPath
324
381
 
325
382
  {String} A path to a directory to put exported GTFS files. If the directory does not exist, it will be created. Used when running `gtfs-export` script or `exportGtfs()`. Optional, defaults to `gtfs-export/<agency_name>` where `<agency_name>` is a sanitized, [snake-cased](https://en.wikipedia.org/wiki/Snake_case) version of the first `agency_name` in `agency.txt`.
@@ -350,6 +407,21 @@ See [full list of options](https://csv.js.org/parse/options/).
350
407
  }
351
408
  ```
352
409
 
410
+ ### ignoreErrors
411
+
412
+ {Boolean} When importing GTFS from multiple agencies, if you don't want node-GTFS to throw an error and instead skip failed GTFS importants proceed to the next agency. Defaults to `false`.
413
+
414
+ ```json
415
+ {
416
+ "agencies": [
417
+ {
418
+ "path": "/path/to/the/unzipped/gtfs/"
419
+ }
420
+ ],
421
+ "ignoreErrors": true
422
+ }
423
+ ```
424
+
353
425
  ### sqlitePath
354
426
 
355
427
  {String} A path to an SQLite database. Optional, defaults to using an in-memory database with a value of `:memory:`.
@@ -12,7 +12,9 @@
12
12
  "csvOptions": {
13
13
  "skip_lines_with_error": true
14
14
  },
15
+ "downloadTimeout": 5000,
15
16
  "ignoreDuplicates": false,
17
+ "ignoreErrors": false,
16
18
  "sqlitePath": "/tmp/gtfs",
17
19
  "exportPath": "~/path/to/export/gtfs"
18
20
  }
package/lib/db.js CHANGED
@@ -17,7 +17,10 @@ function setupDb(sqlitePath) {
17
17
  export function openDb(config) {
18
18
  // If config is passed, use that to open or return db
19
19
  if (config) {
20
- const { sqlitePath } = setDefaultConfig(config);
20
+ const { sqlitePath, db } = setDefaultConfig(config);
21
+ if (db) {
22
+ return db;
23
+ }
21
24
 
22
25
  if (dbs[sqlitePath]) {
23
26
  return dbs[sqlitePath];
@@ -34,7 +37,7 @@ export function openDb(config) {
34
37
 
35
38
  if (Object.keys(dbs).length > 1) {
36
39
  throw new Error(
37
- 'Multiple databases open, please specify which one to use.'
40
+ 'Multiple databases open, please specify which one to use.',
38
41
  );
39
42
  }
40
43
 
@@ -44,14 +47,14 @@ export function openDb(config) {
44
47
  export function closeDb(db) {
45
48
  if (Object.keys(dbs).length === 0) {
46
49
  throw new Error(
47
- 'No database connection. Call `openDb(config)` before using any methods.'
50
+ 'No database connection. Call `openDb(config)` before using any methods.',
48
51
  );
49
52
  }
50
53
 
51
54
  if (!db) {
52
55
  if (Object.keys(dbs).length > 1) {
53
56
  throw new Error(
54
- 'Multiple database connections. Pass the db you want to close as a parameter to `closeDb`.'
57
+ 'Multiple database connections. Pass the db you want to close as a parameter to `closeDb`.',
55
58
  );
56
59
  }
57
60
 
package/lib/import.js CHANGED
@@ -36,6 +36,9 @@ const downloadFiles = async (task) => {
36
36
  const response = await fetch(task.agency_url, {
37
37
  method: 'GET',
38
38
  headers: task.headers || {},
39
+ signal: task.downloadTimeout
40
+ ? AbortSignal.timeout(task.downloadTimeout)
41
+ : undefined,
39
42
  });
40
43
 
41
44
  if (response.status !== 200) {
@@ -509,6 +512,13 @@ const importLines = (task, lines, model, totalLineCount) => {
509
512
  .join(', ')}) VALUES ${placeholders.join(',')}`,
510
513
  ).run(...values);
511
514
  } catch (error) {
515
+ if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
516
+ const primaryColumns = model.schema.filter((column) => column.primary);
517
+ task.warn(
518
+ `Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(', ')}) found in ${model.filenameBase}.txt. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`,
519
+ );
520
+ }
521
+
512
522
  task.warn(
513
523
  `Check ${model.filenameBase}.txt for invalid data between lines ${
514
524
  totalLineCount - linesToImportCount
@@ -587,8 +597,12 @@ const importFiles = (task) =>
587
597
  });
588
598
 
589
599
  parser.on('end', () => {
590
- // Insert all remaining lines
591
- importLines(task, lines, model, totalLineCount);
600
+ try {
601
+ // Insert all remaining lines
602
+ importLines(task, lines, model, totalLineCount);
603
+ } catch (error) {
604
+ reject(error);
605
+ }
592
606
  resolve();
593
607
  });
594
608
 
@@ -628,6 +642,7 @@ export async function importGtfs(initialConfig) {
628
642
  realtime_headers: agency.realtimeHeaders || false,
629
643
  realtime_urls: agency.realtimeUrls || false,
630
644
  downloadDir: path,
645
+ downloadTimeout: config.downloadTimeout,
631
646
  path: agency.path,
632
647
  csvOptions: config.csvOptions || {},
633
648
  ignoreDuplicates: config.ignoreDuplicates,
@@ -638,18 +653,26 @@ export async function importGtfs(initialConfig) {
638
653
  error: logError,
639
654
  };
640
655
 
641
- if (task.agency_url) {
642
- await downloadFiles(task);
643
- }
656
+ try {
657
+ if (task.agency_url) {
658
+ await downloadFiles(task);
659
+ }
644
660
 
645
- await readFiles(task);
646
- await importFiles(task);
661
+ await readFiles(task);
662
+ await importFiles(task);
647
663
 
648
- if (task.realtime_urls) {
649
- await updateRealtimeData(task);
650
- }
664
+ if (task.realtime_urls) {
665
+ await updateRealtimeData(task);
666
+ }
651
667
 
652
- cleanup();
668
+ cleanup();
669
+ } catch (error) {
670
+ if (config.ignoreErrors) {
671
+ logError(error.message);
672
+ } else {
673
+ throw error;
674
+ }
675
+ }
653
676
  });
654
677
 
655
678
  log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs",
3
- "version": "4.7.1",
3
+ "version": "4.8.0",
4
4
  "description": "Import GTFS transit data into SQLite and query routes, stops, times, fares and more",
5
5
  "keywords": [
6
6
  "transit",
@@ -60,7 +60,8 @@
60
60
  "David Abell",
61
61
  "Matthias Feist <matze@matf.de>",
62
62
  "Oliv4945",
63
- "Kyle Ramey"
63
+ "Kyle Ramey",
64
+ "Anton Bracke"
64
65
  ],
65
66
  "type": "module",
66
67
  "main": "index.js",
@@ -75,9 +76,9 @@
75
76
  },
76
77
  "dependencies": {
77
78
  "@turf/helpers": "^6.5.0",
78
- "better-sqlite3": "^9.4.1",
79
- "csv-parse": "^5.5.3",
80
- "csv-stringify": "^6.4.5",
79
+ "better-sqlite3": "^9.4.3",
80
+ "csv-parse": "^5.5.5",
81
+ "csv-stringify": "^6.4.6",
81
82
  "gtfs-realtime-bindings": "^1.1.1",
82
83
  "lodash-es": "^4.17.21",
83
84
  "long": "^5.2.3",
@@ -93,7 +94,7 @@
93
94
  "tmp-promise": "^3.0.3",
94
95
  "untildify": "^5.0.0",
95
96
  "yargs": "^17.7.2",
96
- "yoctocolors": "^1.0.0"
97
+ "yoctocolors": "^2.0.0"
97
98
  },
98
99
  "devDependencies": {
99
100
  "husky": "^9.0.11",
@@ -30,7 +30,7 @@ const agenciesFixturesLocal = [
30
30
  {
31
31
  path: path.join(
32
32
  path.dirname(fileURLToPath(import.meta.url)),
33
- '../fixture/caltrain_20160406.zip'
33
+ '../fixture/caltrain_20160406.zip',
34
34
  ),
35
35
  },
36
36
  ];
@@ -58,6 +58,18 @@ describe('importGtfs():', function () {
58
58
  routes.length.should.equal(4);
59
59
  });
60
60
 
61
+ it('should be able to download and import from HTTP with a downloadTimeout', async () => {
62
+ try {
63
+ await importGtfs({
64
+ ...config,
65
+ agencies: agenciesFixturesRemote,
66
+ downloadTimeout: 1,
67
+ });
68
+ } catch (error) {
69
+ error.name.should.equal('AbortError');
70
+ }
71
+ });
72
+
61
73
  it('should be able to download and import from local filesystem', async () => {
62
74
  await importGtfs({
63
75
  ...config,
@@ -105,7 +117,7 @@ describe('importGtfs():', function () {
105
117
  const countData = {};
106
118
  const temporaryDir = path.join(
107
119
  path.dirname(fileURLToPath(import.meta.url)),
108
- '../fixture/tmp/'
120
+ '../fixture/tmp/',
109
121
  );
110
122
 
111
123
  before(async () => {
@@ -135,7 +147,7 @@ describe('importGtfs():', function () {
135
147
  }
136
148
 
137
149
  countData[model.filenameBase] = data.length;
138
- }
150
+ },
139
151
  );
140
152
 
141
153
  return createReadStream(filePath)
@@ -144,7 +156,7 @@ describe('importGtfs():', function () {
144
156
  countData[model.collection] = 0;
145
157
  throw new Error(error);
146
158
  });
147
- })
159
+ }),
148
160
  );
149
161
 
150
162
  await importGtfs({
@@ -7,14 +7,25 @@ import config from '../test-config.js';
7
7
  import { openDb, closeDb, importGtfs, getShapes } from '../../index.js';
8
8
 
9
9
  const db2Config = {
10
- ...config,
11
10
  agencies: [
12
11
  {
13
12
  ...config.agencies[0],
14
13
  exclude: ['shapes'],
15
14
  },
16
15
  ],
17
- sqlitePath: './tmpdb',
16
+ verbose: false,
17
+ sqlitePath: './tmpdb2',
18
+ };
19
+
20
+ const db3Config = {
21
+ agencies: [
22
+ {
23
+ ...config.agencies[0],
24
+ exclude: ['shapes'],
25
+ },
26
+ ],
27
+ verbose: false,
28
+ sqlitePath: './tmpdb3',
18
29
  };
19
30
 
20
31
  describe('openDb():', () => {
@@ -31,6 +42,11 @@ describe('openDb():', () => {
31
42
  const db2 = openDb(db2Config);
32
43
  closeDb(db2);
33
44
  fs.unlinkSync(db2Config.sqlitePath);
45
+
46
+ // Close db3 and then delete it
47
+ const db3 = openDb(db3Config);
48
+ closeDb(db3);
49
+ fs.unlinkSync(db3Config.sqlitePath);
34
50
  });
35
51
 
36
52
  it('should allow raw db queries: calendar_dates', () => {
@@ -40,7 +56,7 @@ describe('openDb():', () => {
40
56
  .prepare(
41
57
  `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds
42
58
  .map((serviceId) => `'${serviceId}'`)
43
- .join(', ')})`
59
+ .join(', ')})`,
44
60
  )
45
61
  .all();
46
62
 
@@ -55,7 +71,7 @@ describe('openDb():', () => {
55
71
  const db = openDb();
56
72
  const results = db
57
73
  .prepare(
58
- 'SELECT * from trips where trips.trip_id IN (SELECT start_stop_times.trip_id FROM stop_times as start_stop_times WHERE stop_id = ? AND start_stop_times.stop_sequence < (SELECT end_stop_times.stop_sequence FROM stop_times as end_stop_times WHERE end_stop_times.stop_sequence > start_stop_times.stop_sequence AND end_stop_times.trip_id = start_stop_times.trip_id AND end_stop_times.stop_id = ? ))'
74
+ 'SELECT * from trips where trips.trip_id IN (SELECT start_stop_times.trip_id FROM stop_times as start_stop_times WHERE stop_id = ? AND start_stop_times.stop_sequence < (SELECT end_stop_times.stop_sequence FROM stop_times as end_stop_times WHERE end_stop_times.stop_sequence > start_stop_times.stop_sequence AND end_stop_times.trip_id = start_stop_times.trip_id AND end_stop_times.stop_id = ? ))',
59
75
  )
60
76
  .all(startStopId, endStopId);
61
77
  should.exists(results);
@@ -69,7 +85,7 @@ describe('openDb():', () => {
69
85
  const db1 = openDb(config);
70
86
 
71
87
  db1.name.should.equal(':memory:');
72
- db2.name.should.equal('./tmpdb');
88
+ db2.name.should.equal('./tmpdb2');
73
89
 
74
90
  // Query db1 for shapes
75
91
  const shapeId = 'cal_sf_tam';
@@ -79,7 +95,7 @@ describe('openDb():', () => {
79
95
  },
80
96
  [],
81
97
  [],
82
- { db: db1 }
98
+ { db: db1 },
83
99
  );
84
100
 
85
101
  const expectedResult = {
@@ -101,10 +117,33 @@ describe('openDb():', () => {
101
117
  },
102
118
  [],
103
119
  [],
104
- { db: db2 }
120
+ { db: db2 },
105
121
  );
106
122
 
107
123
  should.exist(results2);
108
124
  results2.length.should.equal(0);
109
125
  });
126
+
127
+ it('should allow `db` configuration option', async () => {
128
+ const db3 = openDb(db3Config);
129
+
130
+ await importGtfs({
131
+ ...db3Config,
132
+ db: db3,
133
+ });
134
+
135
+ // Query db3 for shapes, none should exist
136
+ const shapeId = 'cal_sf_tam';
137
+ const results = getShapes(
138
+ {
139
+ shape_id: shapeId,
140
+ },
141
+ [],
142
+ [],
143
+ { db: db3 },
144
+ );
145
+
146
+ should.exist(results);
147
+ results.length.should.equal(0);
148
+ });
110
149
  });