gtfs 4.4.3 → 4.5.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.5.1] - 2023-11-09
9
+
10
+ ### Updated
11
+
12
+ - Dependency updates
13
+ - Use path to types file instead of directory in package.json
14
+
15
+ ### Fixed
16
+
17
+ - Exclude agency_id from export if empty
18
+
19
+ ## [4.5.0] - 2023-08-23
20
+
21
+ ### Updated
22
+
23
+ - Increase `maxInsertVariables` to 32,000
24
+ - Dependency updates
25
+
8
26
  ## [4.4.3] - 2023-07-18
9
27
 
10
28
  ### Updated
package/README.md CHANGED
@@ -116,8 +116,8 @@ try {
116
116
  <td><a href="https://github.com/blinktaginc/gtfs-tts">GTFS-Text-to-Speech</a> app tests GTFS stop name pronunciation for text-to-speech. It uses `node-gtfs` for loading stop names from GTFS data.</td>
117
117
  </tr>
118
118
  <tr>
119
- <td><img src="https://github.com/BlinkTagInc/transit-arrivals-widget/raw/master/docs/images/transit-arrivals-widget-logo.svg" alt="Transit Arrivals Widget" width="200"></td>
120
- <td><a href="https://github.com/BlinkTagInc/transit-arrivals-widget">Transit Arrivals Widget</a> creates a realtime transit arrivals tool from GTFS and GTFS-RT data.</td>
119
+ <td><img src="https://raw.githubusercontent.com/BlinkTagInc/transit-departures-widget/main/docs/images/transit-departures-widget-logo.svg" alt="Transit Departures Widget" width="200"></td>
120
+ <td><a href="https://github.com/BlinkTagInc/transit-departures-widget">Transit Departures Widget</a> creates a realtime transit departures widget from GTFS and GTFS-Realtime data.</td>
121
121
  </tr>
122
122
  </table>
123
123
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "agencies": [
3
3
  {
4
- "url": "~/path/to/my/gtfs"
4
+ "path": "~/path/to/my/gtfs"
5
5
  }
6
6
  ],
7
7
  "sqlitePath": "/tmp/gtfs"
package/lib/export.js CHANGED
@@ -20,12 +20,12 @@ const getAgencies = (db, config) => {
20
20
  } catch (error) {
21
21
  if (config.sqlitePath === ':memory:') {
22
22
  throw new Error(
23
- 'No agencies found in SQLite. You are using an in-memory database - if running this from command line be sure to specify a value for `sqlitePath` in config.json other than ":memory:".'
23
+ 'No agencies found in SQLite. You are using an in-memory database - if running this from command line be sure to specify a value for `sqlitePath` in config.json other than ":memory:".',
24
24
  );
25
25
  }
26
26
 
27
27
  throw new Error(
28
- 'No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`'
28
+ 'No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`',
29
29
  );
30
30
  }
31
31
  };
@@ -42,11 +42,11 @@ const exportGtfs = async (initialConfig) => {
42
42
  const agencyCount = agencies.length;
43
43
  if (agencyCount === 0) {
44
44
  throw new Error(
45
- 'No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`'
45
+ 'No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`',
46
46
  );
47
47
  } else if (agencyCount > 1) {
48
48
  logWarning(
49
- 'More than one agency is defined in config.json. Export will merge all into one GTFS file.'
49
+ 'More than one agency is defined in config.json. Export will merge all into one GTFS file.',
50
50
  );
51
51
  }
52
52
 
@@ -54,8 +54,8 @@ const exportGtfs = async (initialConfig) => {
54
54
  `Starting GTFS export for ${pluralize(
55
55
  'agency',
56
56
  agencyCount,
57
- true
58
- )} using SQLite database at ${config.sqlitePath}`
57
+ true,
58
+ )} using SQLite database at ${config.sqlitePath}`,
59
59
  );
60
60
 
61
61
  const folderName = generateFolderName(agencies[0].agency_name);
@@ -92,9 +92,19 @@ const exportGtfs = async (initialConfig) => {
92
92
  'ridership_end_timestamp',
93
93
  ];
94
94
 
95
+ // If no routes have values for agency_id, add it to the excludeColumns list
96
+ if (model.filenameBase === 'routes') {
97
+ const routesWithAgencyId = db
98
+ .prepare('SELECT agency_id FROM routes WHERE agency_id IS NOT NULL;')
99
+ .all();
100
+ if (!routesWithAgencyId || routesWithAgencyId.length === 0) {
101
+ excludeColumns.push('agency_id');
102
+ }
103
+ }
104
+
95
105
  const columns = without(
96
106
  model.schema.map((column) => column.name),
97
- ...excludeColumns
107
+ ...excludeColumns,
98
108
  );
99
109
  const fileText = await stringify(lines, { columns, header: true });
100
110
  await writeFile(filepath, fileText);
package/lib/import.js CHANGED
@@ -112,15 +112,15 @@ const updateRealtimeData = async (task) => {
112
112
 
113
113
  const model = {
114
114
  vehicle_positions: models.find(
115
- (x) => x.filenameBase === 'vehicle_positions'
115
+ (x) => x.filenameBase === 'vehicle_positions',
116
116
  ),
117
117
  trip_updates: models.find((x) => x.filenameBase === 'trip_updates'),
118
118
  stop_times_updates: models.find(
119
- (x) => x.filenameBase === 'stop_times_updates'
119
+ (x) => x.filenameBase === 'stop_times_updates',
120
120
  ),
121
121
  service_alerts: models.find((x) => x.filenameBase === 'service_alerts'),
122
122
  service_alert_targets: models.find(
123
- (x) => x.filenameBase === 'service_alert_targets'
123
+ (x) => x.filenameBase === 'service_alert_targets',
124
124
  ),
125
125
  };
126
126
 
@@ -143,7 +143,7 @@ const updateRealtimeData = async (task) => {
143
143
  };
144
144
 
145
145
  task.log(
146
- `Starting GTFS-Realtime import from ${task.realtime_urls.length} urls`
146
+ `Starting GTFS-Realtime import from ${task.realtime_urls.length} urls`,
147
147
  );
148
148
 
149
149
  for (const realtimeUrl of task.realtime_urls) {
@@ -151,7 +151,7 @@ const updateRealtimeData = async (task) => {
151
151
  // eslint-disable-next-line no-await-in-loop
152
152
  const tripUpdateData = await downloadGtfsRealtimeData(
153
153
  realtimeUrl,
154
- task.realtime_headers
154
+ task.realtime_headers,
155
155
  );
156
156
  task.log(`Download successful`);
157
157
 
@@ -178,15 +178,15 @@ const updateRealtimeData = async (task) => {
178
178
  // Do base processing
179
179
  const fieldValues = model[gtfsRealtimeType].schema.map((column) =>
180
180
  sqlString.escape(
181
- getDescendantProp(entity, column.source, column.default)
182
- )
181
+ getDescendantProp(entity, column.source, column.default),
182
+ ),
183
183
  );
184
184
 
185
185
  try {
186
186
  db.prepare(
187
187
  `REPLACE INTO ${model[gtfsRealtimeType].filenameBase} (${
188
188
  fields[gtfsRealtimeType]
189
- }) VALUES (${fieldValues.join(', ')})`
189
+ }) VALUES (${fieldValues.join(', ')})`,
190
190
  ).run();
191
191
  } catch (error) {
192
192
  task.warn('Import error: ' + error.message);
@@ -199,8 +199,8 @@ const updateRealtimeData = async (task) => {
199
199
  stopUpdate.parent = entity;
200
200
  const subValues = model.stop_times_updates.schema.map((column) =>
201
201
  sqlString.escape(
202
- getDescendantProp(stopUpdate, column.source, column.default)
203
- )
202
+ getDescendantProp(stopUpdate, column.source, column.default),
203
+ ),
204
204
  );
205
205
  stopUpdateArray.push(`(${subValues.join(', ')})`);
206
206
  totalLineCount++;
@@ -210,7 +210,7 @@ const updateRealtimeData = async (task) => {
210
210
  db.prepare(
211
211
  `REPLACE INTO ${model.stop_times_updates.filenameBase} (${
212
212
  fields.stop_times_updates
213
- }) VALUES ${stopUpdateArray.join(', ')}`
213
+ }) VALUES ${stopUpdateArray.join(', ')}`,
214
214
  ).run();
215
215
  } catch (error) {
216
216
  task.warn('Import error: ' + error.message);
@@ -224,8 +224,8 @@ const updateRealtimeData = async (task) => {
224
224
  informedEntity.parent = entity;
225
225
  const subValues = model.service_alert_targets.schema.map((column) =>
226
226
  sqlString.escape(
227
- getDescendantProp(informedEntity, column.source, column.default)
228
- )
227
+ getDescendantProp(informedEntity, column.source, column.default),
228
+ ),
229
229
  );
230
230
  alertTargetArray.push(`(${subValues.join(', ')})`);
231
231
  totalLineCount++;
@@ -235,7 +235,7 @@ const updateRealtimeData = async (task) => {
235
235
  db.prepare(
236
236
  `REPLACE INTO ${model.service_alert_targets.filenameBase} (${
237
237
  fields.service_alert_targets
238
- }) VALUES ${alertTargetArray.join(', ')}`
238
+ }) VALUES ${alertTargetArray.join(', ')}`,
239
239
  ).run();
240
240
  } catch (error) {
241
241
  task.warn('Import error: ' + error.message);
@@ -273,11 +273,11 @@ const readFiles = async (task) => {
273
273
 
274
274
  if (folders.length > 1) {
275
275
  throw new Error(
276
- `More than one subfolder found in zip file at \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`
276
+ `More than one subfolder found in zip file at \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
277
277
  );
278
278
  } else if (folders.length === 0) {
279
279
  throw new Error(
280
- `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`
280
+ `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
281
281
  );
282
282
  }
283
283
 
@@ -286,7 +286,7 @@ const readFiles = async (task) => {
286
286
 
287
287
  if (directoryTextFiles.length === 0) {
288
288
  throw new Error(
289
- `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`
289
+ `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
290
290
  );
291
291
  }
292
292
 
@@ -294,9 +294,9 @@ const readFiles = async (task) => {
294
294
  directoryTextFiles.map(async (fileName) =>
295
295
  rename(
296
296
  path.join(subfolderName, fileName),
297
- path.join(task.downloadDir, fileName)
298
- )
299
- )
297
+ path.join(task.downloadDir, fileName),
298
+ ),
299
+ ),
300
300
  );
301
301
  }
302
302
  } catch (error) {
@@ -310,7 +310,7 @@ const readFiles = async (task) => {
310
310
  await copy(gtfsPath, task.downloadDir);
311
311
  } catch {
312
312
  throw new Error(
313
- `Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`
313
+ `Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`,
314
314
  );
315
315
  }
316
316
  }
@@ -345,20 +345,20 @@ const createTables = (db) => {
345
345
  columns.push(
346
346
  `PRIMARY KEY (${primaryColumns
347
347
  .map((column) => column.name)
348
- .join(', ')})`
348
+ .join(', ')})`,
349
349
  );
350
350
  }
351
351
 
352
352
  db.prepare(`DROP TABLE IF EXISTS ${model.filenameBase};`).run();
353
353
 
354
354
  db.prepare(
355
- `CREATE TABLE ${model.filenameBase} (${columns.join(', ')});`
355
+ `CREATE TABLE ${model.filenameBase} (${columns.join(', ')});`,
356
356
  ).run();
357
357
 
358
358
  for (const column of model.schema.filter((column) => column.index)) {
359
359
  const unique = column.index === 'unique' ? 'UNIQUE' : '';
360
360
  db.prepare(
361
- `CREATE ${unique} INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`
361
+ `CREATE ${unique} INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`,
362
362
  ).run();
363
363
  }
364
364
  }
@@ -398,7 +398,7 @@ const formatLine = (line, model, totalLineCount) => {
398
398
  formattedLine[columnSchema.name] === null
399
399
  ) {
400
400
  throw new Error(
401
- `Missing required value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}.`
401
+ `Missing required value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}.`,
402
402
  );
403
403
  }
404
404
 
@@ -408,7 +408,7 @@ const formatLine = (line, model, totalLineCount) => {
408
408
  formattedLine[columnSchema.name] < columnSchema.min
409
409
  ) {
410
410
  throw new Error(
411
- `Invalid value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}: below minimum value of ${columnSchema.min}.`
411
+ `Invalid value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}: below minimum value of ${columnSchema.min}.`,
412
412
  );
413
413
  }
414
414
 
@@ -418,7 +418,7 @@ const formatLine = (line, model, totalLineCount) => {
418
418
  formattedLine[columnSchema.name] > columnSchema.max
419
419
  ) {
420
420
  throw new Error(
421
- `Invalid value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}: above maximum value of ${columnSchema.max}.`
421
+ `Invalid value in ${model.filenameBase}.txt for ${columnSchema.name} on line ${lineNumber}: above maximum value of ${columnSchema.max}.`,
422
422
  );
423
423
  }
424
424
  }
@@ -438,7 +438,7 @@ const formatLine = (line, model, totalLineCount) => {
438
438
 
439
439
  // Ensure leading zeros for time columns
440
440
  formattedLine[timestampColumnName] = padLeadingZeros(
441
- formattedLine[timestampColumnName]
441
+ formattedLine[timestampColumnName],
442
442
  );
443
443
  }
444
444
  }
@@ -469,7 +469,7 @@ const importLines = (task, lines, model, totalLineCount) => {
469
469
  }
470
470
 
471
471
  return line[column.name];
472
- })
472
+ }),
473
473
  );
474
474
  }
475
475
 
@@ -479,20 +479,20 @@ const importLines = (task, lines, model, totalLineCount) => {
479
479
  model.filenameBase
480
480
  } (${columns
481
481
  .map((column) => column.name)
482
- .join(', ')}) VALUES ${placeholders.join(',')}`
482
+ .join(', ')}) VALUES ${placeholders.join(',')}`,
483
483
  ).run(...values);
484
484
  } catch (error) {
485
485
  task.warn(
486
486
  `Check ${model.filenameBase}.txt for invalid data between lines ${
487
487
  totalLineCount - linesToImportCount
488
- } and ${totalLineCount}.`
488
+ } and ${totalLineCount}.`,
489
489
  );
490
490
  throw error;
491
491
  }
492
492
 
493
493
  task.log(
494
494
  `Importing - ${model.filenameBase}.txt - ${totalLineCount} lines imported\r`,
495
- true
495
+ true,
496
496
  );
497
497
  };
498
498
 
@@ -517,7 +517,7 @@ const importFiles = (task) =>
517
517
 
518
518
  const filepath = path.join(
519
519
  task.downloadDir,
520
- `${model.filenameBase}.txt`
520
+ `${model.filenameBase}.txt`,
521
521
  );
522
522
 
523
523
  if (!existsSync(filepath)) {
@@ -533,7 +533,7 @@ const importFiles = (task) =>
533
533
 
534
534
  const lines = [];
535
535
  let totalLineCount = 0;
536
- const maxInsertVariables = 800;
536
+ const maxInsertVariables = 32_000;
537
537
  const parser = parse({
538
538
  columns: true,
539
539
  relax_quotes: true,
@@ -568,7 +568,7 @@ const importFiles = (task) =>
568
568
  parser.on('error', reject);
569
569
 
570
570
  createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
571
- })
571
+ }),
572
572
  );
573
573
 
574
574
  export async function importGtfs(initialConfig) {
@@ -585,8 +585,8 @@ export async function importGtfs(initialConfig) {
585
585
  `Starting GTFS import for ${pluralize(
586
586
  'file',
587
587
  agencyCount,
588
- true
589
- )} using SQLite database at ${config.sqlitePath}`
588
+ true,
589
+ )} using SQLite database at ${config.sqlitePath}`,
590
590
  );
591
591
 
592
592
  createTables(db);
@@ -626,12 +626,12 @@ export async function importGtfs(initialConfig) {
626
626
  });
627
627
 
628
628
  log(
629
- `Completed GTFS import for ${pluralize('agency', agencyCount, true)}\n`
629
+ `Completed GTFS import for ${pluralize('agency', agencyCount, true)}\n`,
630
630
  );
631
631
  } catch (error) {
632
632
  if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
633
633
  logError(
634
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
634
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
635
635
  );
636
636
  }
637
637
 
@@ -654,8 +654,8 @@ export async function updateGtfsRealtime(initialConfig) {
654
654
  `Starting GTFS-Realtime refresh for ${pluralize(
655
655
  'agencies',
656
656
  agencyCount,
657
- true
658
- )} using SQLite database at ${config.sqlitePath}`
657
+ true,
658
+ )} using SQLite database at ${config.sqlitePath}`,
659
659
  );
660
660
 
661
661
  markRealtimeDataStale(config, log);
@@ -676,7 +676,7 @@ export async function updateGtfsRealtime(initialConfig) {
676
676
  };
677
677
 
678
678
  await updateRealtimeData(task);
679
- })
679
+ }),
680
680
  );
681
681
 
682
682
  cleanStaleRealtimeData(config, log);
@@ -684,13 +684,13 @@ export async function updateGtfsRealtime(initialConfig) {
684
684
  `Completed GTFS-Realtime refresh for ${pluralize(
685
685
  'agencies',
686
686
  agencyCount,
687
- true
688
- )}\n`
687
+ true,
688
+ )}\n`,
689
689
  );
690
690
  } catch (error) {
691
691
  if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
692
692
  logError(
693
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
693
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
694
694
  );
695
695
  }
696
696
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs",
3
- "version": "4.4.3",
3
+ "version": "4.5.1",
4
4
  "description": "Import GTFS transit data into SQLite and query routes, stops, times, fares and more",
5
5
  "keywords": [
6
6
  "transit",
@@ -67,19 +67,19 @@
67
67
  "gtfs-import": "bin/gtfs-import.js",
68
68
  "gtfsrealtime-update": "bin/gtfsrealtime-update.js"
69
69
  },
70
- "types": "@types",
70
+ "types": "@types/index.d.ts",
71
71
  "scripts": {
72
72
  "test": "NODE_ENV=test mocha ./test/mocha/**/*.js --timeout 2000"
73
73
  },
74
74
  "dependencies": {
75
75
  "@turf/helpers": "^6.5.0",
76
- "better-sqlite3": "^8.4.0",
77
- "csv-parse": "^5.4.0",
78
- "csv-stringify": "^6.4.0",
76
+ "better-sqlite3": "^9.1.1",
77
+ "csv-parse": "^5.5.2",
78
+ "csv-stringify": "^6.4.4",
79
79
  "gtfs-realtime-bindings": "^1.1.1",
80
80
  "lodash-es": "^4.17.21",
81
81
  "long": "^5.2.3",
82
- "node-fetch": "^3.3.1",
82
+ "node-fetch": "^3.3.2",
83
83
  "node-stream-zip": "^1.15.0",
84
84
  "pluralize": "^8.0.0",
85
85
  "pretty-error": "^4.0.0",
@@ -95,9 +95,9 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "husky": "^8.0.3",
98
- "lint-staged": "^13.2.3",
98
+ "lint-staged": "^15.0.2",
99
99
  "mocha": "^10.2.0",
100
- "prettier": "^3.0.0",
100
+ "prettier": "^3.0.3",
101
101
  "should": "^13.2.3"
102
102
  },
103
103
  "engines": {