gtfs 3.1.1 → 3.2.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/.eslintrc.json +14 -19
- package/.husky/pre-commit +4 -0
- package/@types/index.d.ts +269 -0
- package/@types/tests.ts +26 -0
- package/@types/tsconfig.json +17 -0
- package/CHANGELOG.md +182 -1
- package/README.md +168 -149
- package/bin/gtfs-export.js +4 -5
- package/bin/gtfs-import.js +6 -7
- package/config-sample-full.json +2 -7
- package/docs/images/node-gtfs-logo.svg +18 -0
- package/lib/db.js +63 -12
- package/lib/export.js +27 -13
- package/lib/file-utils.js +26 -9
- package/lib/geojson-utils.js +51 -38
- package/lib/gtfs/agencies.js +15 -4
- package/lib/gtfs/attributions.js +15 -4
- package/lib/gtfs/calendar-dates.js +15 -4
- package/lib/gtfs/calendars.js +15 -4
- package/lib/gtfs/fare-attributes.js +15 -4
- package/lib/gtfs/fare-rules.js +15 -4
- package/lib/gtfs/feed-info.js +15 -4
- package/lib/gtfs/frequencies.js +15 -4
- package/lib/gtfs/levels.js +15 -4
- package/lib/gtfs/pathways.js +15 -4
- package/lib/gtfs/routes.js +22 -6
- package/lib/gtfs/shapes.js +59 -23
- package/lib/gtfs/stop-times.js +15 -4
- package/lib/gtfs/stops.js +57 -23
- package/lib/gtfs/transfers.js +15 -4
- package/lib/gtfs/translations.js +15 -4
- package/lib/gtfs/trips.js +15 -4
- package/lib/gtfs-ride/board-alights.js +15 -4
- package/lib/gtfs-ride/ride-feed-infos.js +15 -4
- package/lib/gtfs-ride/rider-trips.js +15 -4
- package/lib/gtfs-ride/riderships.js +15 -4
- package/lib/gtfs-ride/trip-capacities.js +15 -4
- package/lib/import.js +203 -128
- package/lib/log-utils.js +8 -4
- package/lib/non-standard/directions.js +15 -4
- package/lib/non-standard/stop-attributes.js +15 -4
- package/lib/non-standard/timetable-notes-references.js +15 -4
- package/lib/non-standard/timetable-notes.js +15 -4
- package/lib/non-standard/timetable-pages.js +15 -4
- package/lib/non-standard/timetable-stop-order.js +15 -4
- package/lib/non-standard/timetables.js +15 -4
- package/lib/utils.js +26 -12
- package/models/gtfs/agency.js +11 -11
- package/models/gtfs/attributions.js +14 -14
- package/models/gtfs/calendar-dates.js +7 -7
- package/models/gtfs/calendar.js +12 -12
- package/models/gtfs/fare-attributes.js +9 -9
- package/models/gtfs/fare-rules.js +8 -8
- package/models/gtfs/feed-info.js +12 -12
- package/models/gtfs/frequencies.js +10 -10
- package/models/gtfs/levels.js +5 -5
- package/models/gtfs/pathways.js +14 -14
- package/models/gtfs/routes.js +14 -14
- package/models/gtfs/shapes.js +8 -8
- package/models/gtfs/stop-times.js +17 -17
- package/models/gtfs/stops.js +17 -17
- package/models/gtfs/transfers.js +7 -7
- package/models/gtfs/translations.js +10 -10
- package/models/gtfs/trips.js +12 -12
- package/models/gtfs-ride/board-alight.js +24 -24
- package/models/gtfs-ride/ride-feed-info.js +8 -8
- package/models/gtfs-ride/rider-trip.js +21 -21
- package/models/gtfs-ride/ridership.js +23 -23
- package/models/gtfs-ride/trip-capacity.js +10 -10
- package/models/models.js +1 -1
- package/models/non-standard/directions.js +6 -6
- package/models/non-standard/stop-attributes.js +5 -5
- package/models/non-standard/timetable-notes-references.js +9 -9
- package/models/non-standard/timetable-notes.js +5 -5
- package/models/non-standard/timetable-pages.js +5 -5
- package/models/non-standard/timetable-stop-order.js +6 -6
- package/models/non-standard/timetables.js +27 -27
- package/package.json +35 -13
- package/test/mocha/export-gtfs.js +74 -44
- package/test/mocha/get-agencies.js +20 -14
- package/test/mocha/get-attributions.js +10 -4
- package/test/mocha/get-board-alights.js +10 -4
- package/test/mocha/get-calendar-dates.js +31 -24
- package/test/mocha/get-calendars.js +17 -11
- package/test/mocha/get-db.js +71 -5
- package/test/mocha/get-directions.js +10 -4
- package/test/mocha/get-fare-attributes.js +12 -6
- package/test/mocha/get-fare-rules.js +17 -13
- package/test/mocha/get-feed-info.js +10 -4
- package/test/mocha/get-frequencies.js +10 -4
- package/test/mocha/get-levels.js +4 -4
- package/test/mocha/get-pathways.js +10 -4
- package/test/mocha/get-ride-feed-infos.js +9 -3
- package/test/mocha/get-rider-trips.js +10 -4
- package/test/mocha/get-riderships.js +10 -4
- package/test/mocha/get-routes.js +12 -16
- package/test/mocha/get-shapes-as-geojson.js +12 -6
- package/test/mocha/get-shapes.js +31 -39
- package/test/mocha/get-stop-attributes.js +10 -4
- package/test/mocha/get-stops-as-geojson.js +11 -5
- package/test/mocha/get-stops.js +62 -51
- package/test/mocha/get-stoptimes.js +18 -10
- package/test/mocha/get-timetable-pages.js +10 -4
- package/test/mocha/get-timetable-stop-orders.js +10 -4
- package/test/mocha/get-timetables.js +10 -4
- package/test/mocha/get-transfers.js +10 -4
- package/test/mocha/get-translations.js +10 -4
- package/test/mocha/get-trip-capacities.js +10 -4
- package/test/mocha/get-trips.js +6 -6
- package/test/mocha/import-gtfs.js +63 -46
- package/test/test-config.js +9 -4
|
@@ -2,18 +2,29 @@ import sqlString from 'sqlstring-sqlite';
|
|
|
2
2
|
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatOrderByClause,
|
|
7
|
+
formatSelectClause,
|
|
8
|
+
formatWhereClauses,
|
|
9
|
+
} from '../utils.js';
|
|
6
10
|
import boardAlights from '../../models/gtfs-ride/board-alight.js';
|
|
7
11
|
|
|
8
12
|
/*
|
|
9
13
|
* Returns an array of all board-alights that match the query parameters.
|
|
10
14
|
*/
|
|
11
|
-
export async function getBoardAlights(
|
|
12
|
-
|
|
15
|
+
export async function getBoardAlights(
|
|
16
|
+
query = {},
|
|
17
|
+
fields = [],
|
|
18
|
+
orderBy = [],
|
|
19
|
+
options = {}
|
|
20
|
+
) {
|
|
21
|
+
const db = options.db ?? (await getDb());
|
|
13
22
|
const tableName = sqlString.escapeId(boardAlights.filenameBase);
|
|
14
23
|
const selectClause = formatSelectClause(fields);
|
|
15
24
|
const whereClause = formatWhereClauses(query);
|
|
16
25
|
const orderByClause = formatOrderByClause(orderBy);
|
|
17
26
|
|
|
18
|
-
return db.all(
|
|
27
|
+
return db.all(
|
|
28
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`
|
|
29
|
+
);
|
|
19
30
|
}
|
|
@@ -2,18 +2,29 @@ import sqlString from 'sqlstring-sqlite';
|
|
|
2
2
|
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatOrderByClause,
|
|
7
|
+
formatSelectClause,
|
|
8
|
+
formatWhereClauses,
|
|
9
|
+
} from '../utils.js';
|
|
6
10
|
import rideFeedInfo from '../../models/gtfs-ride/ride-feed-info.js';
|
|
7
11
|
|
|
8
12
|
/*
|
|
9
13
|
* Returns an array of all ride-feed-info that match the query parameters.
|
|
10
14
|
*/
|
|
11
|
-
export async function getRideFeedInfos(
|
|
12
|
-
|
|
15
|
+
export async function getRideFeedInfos(
|
|
16
|
+
query = {},
|
|
17
|
+
fields = [],
|
|
18
|
+
orderBy = [],
|
|
19
|
+
options = {}
|
|
20
|
+
) {
|
|
21
|
+
const db = options.db ?? (await getDb());
|
|
13
22
|
const tableName = sqlString.escapeId(rideFeedInfo.filenameBase);
|
|
14
23
|
const selectClause = formatSelectClause(fields);
|
|
15
24
|
const whereClause = formatWhereClauses(query);
|
|
16
25
|
const orderByClause = formatOrderByClause(orderBy);
|
|
17
26
|
|
|
18
|
-
return db.all(
|
|
27
|
+
return db.all(
|
|
28
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`
|
|
29
|
+
);
|
|
19
30
|
}
|
|
@@ -2,18 +2,29 @@ import sqlString from 'sqlstring-sqlite';
|
|
|
2
2
|
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatOrderByClause,
|
|
7
|
+
formatSelectClause,
|
|
8
|
+
formatWhereClauses,
|
|
9
|
+
} from '../utils.js';
|
|
6
10
|
import riderTrip from '../../models/gtfs-ride/rider-trip.js';
|
|
7
11
|
|
|
8
12
|
/*
|
|
9
13
|
* Returns an array of all rider trips that match the query parameters.
|
|
10
14
|
*/
|
|
11
|
-
export async function getRiderTrips(
|
|
12
|
-
|
|
15
|
+
export async function getRiderTrips(
|
|
16
|
+
query = {},
|
|
17
|
+
fields = [],
|
|
18
|
+
orderBy = [],
|
|
19
|
+
options = {}
|
|
20
|
+
) {
|
|
21
|
+
const db = options.db ?? (await getDb());
|
|
13
22
|
const tableName = sqlString.escapeId(riderTrip.filenameBase);
|
|
14
23
|
const selectClause = formatSelectClause(fields);
|
|
15
24
|
const whereClause = formatWhereClauses(query);
|
|
16
25
|
const orderByClause = formatOrderByClause(orderBy);
|
|
17
26
|
|
|
18
|
-
return db.all(
|
|
27
|
+
return db.all(
|
|
28
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`
|
|
29
|
+
);
|
|
19
30
|
}
|
|
@@ -2,18 +2,29 @@ import sqlString from 'sqlstring-sqlite';
|
|
|
2
2
|
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatOrderByClause,
|
|
7
|
+
formatSelectClause,
|
|
8
|
+
formatWhereClauses,
|
|
9
|
+
} from '../utils.js';
|
|
6
10
|
import riderships from '../../models/gtfs-ride/ridership.js';
|
|
7
11
|
|
|
8
12
|
/*
|
|
9
13
|
* Returns an array of all riderships that match the query parameters.
|
|
10
14
|
*/
|
|
11
|
-
export async function getRiderships(
|
|
12
|
-
|
|
15
|
+
export async function getRiderships(
|
|
16
|
+
query = {},
|
|
17
|
+
fields = [],
|
|
18
|
+
orderBy = [],
|
|
19
|
+
options = {}
|
|
20
|
+
) {
|
|
21
|
+
const db = options.db ?? (await getDb());
|
|
13
22
|
const tableName = sqlString.escapeId(riderships.filenameBase);
|
|
14
23
|
const selectClause = formatSelectClause(fields);
|
|
15
24
|
const whereClause = formatWhereClauses(query);
|
|
16
25
|
const orderByClause = formatOrderByClause(orderBy);
|
|
17
26
|
|
|
18
|
-
return db.all(
|
|
27
|
+
return db.all(
|
|
28
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`
|
|
29
|
+
);
|
|
19
30
|
}
|
|
@@ -2,18 +2,29 @@ import sqlString from 'sqlstring-sqlite';
|
|
|
2
2
|
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatOrderByClause,
|
|
7
|
+
formatSelectClause,
|
|
8
|
+
formatWhereClauses,
|
|
9
|
+
} from '../utils.js';
|
|
6
10
|
import tripCapacity from '../../models/gtfs-ride/trip-capacity.js';
|
|
7
11
|
|
|
8
12
|
/*
|
|
9
13
|
* Returns an array of all trip-capacities that match the query parameters.
|
|
10
14
|
*/
|
|
11
|
-
export async function getTripCapacities(
|
|
12
|
-
|
|
15
|
+
export async function getTripCapacities(
|
|
16
|
+
query = {},
|
|
17
|
+
fields = [],
|
|
18
|
+
orderBy = [],
|
|
19
|
+
options = {}
|
|
20
|
+
) {
|
|
21
|
+
const db = options.db ?? (await getDb());
|
|
13
22
|
const tableName = sqlString.escapeId(tripCapacity.filenameBase);
|
|
14
23
|
const selectClause = formatSelectClause(fields);
|
|
15
24
|
const whereClause = formatWhereClauses(query);
|
|
16
25
|
const orderByClause = formatOrderByClause(orderBy);
|
|
17
26
|
|
|
18
|
-
return db.all(
|
|
27
|
+
return db.all(
|
|
28
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`
|
|
29
|
+
);
|
|
19
30
|
}
|
package/lib/import.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createReadStream, existsSync, lstatSync } from 'node:fs';
|
|
3
3
|
import { readdir, rename, writeFile } from 'node:fs/promises';
|
|
4
|
-
import
|
|
4
|
+
import copy from 'recursive-copy';
|
|
5
5
|
import fetch from 'node-fetch';
|
|
6
|
-
import parse from 'csv-parse';
|
|
6
|
+
import { parse } from 'csv-parse';
|
|
7
7
|
import pluralize from 'pluralize';
|
|
8
8
|
import stripBomStream from 'strip-bom-stream';
|
|
9
9
|
import { dir } from 'tmp-promise';
|
|
@@ -13,32 +13,43 @@ import mapSeries from 'promise-map-series';
|
|
|
13
13
|
import models from '../models/models.js';
|
|
14
14
|
import { openDb, setupDb } from './db.js';
|
|
15
15
|
import { unzip } from './file-utils.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import {
|
|
17
|
+
log as _log,
|
|
18
|
+
logError as _logError,
|
|
19
|
+
logWarning as _logWarning,
|
|
20
|
+
} from './log-utils.js';
|
|
21
|
+
import {
|
|
22
|
+
calculateSecondsFromMidnight,
|
|
23
|
+
setDefaultConfig,
|
|
24
|
+
validateConfigForImport,
|
|
25
|
+
} from './utils.js';
|
|
26
|
+
|
|
27
|
+
const downloadFiles = async (task) => {
|
|
20
28
|
task.log(`Downloading GTFS from ${task.agency_url}`);
|
|
21
29
|
|
|
22
30
|
task.path = `${task.downloadDir}/gtfs.zip`;
|
|
23
31
|
|
|
24
|
-
const response = await fetch(task.agency_url, {
|
|
32
|
+
const response = await fetch(task.agency_url, {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: task.agency_headers || {},
|
|
35
|
+
});
|
|
25
36
|
|
|
26
37
|
if (response.status !== 200) {
|
|
27
38
|
throw new Error('Couldn’t download files');
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
const buffer = await response.
|
|
41
|
+
const buffer = await response.arrayBuffer();
|
|
31
42
|
|
|
32
|
-
await writeFile(task.path, buffer);
|
|
43
|
+
await writeFile(task.path, Buffer.from(buffer));
|
|
33
44
|
task.log('Download successful');
|
|
34
45
|
};
|
|
35
46
|
|
|
36
|
-
const getTextFiles = async folderPath => {
|
|
47
|
+
const getTextFiles = async (folderPath) => {
|
|
37
48
|
const files = await readdir(folderPath);
|
|
38
|
-
return files.filter(filename => filename.slice(-3) === 'txt');
|
|
49
|
+
return files.filter((filename) => filename.slice(-3) === 'txt');
|
|
39
50
|
};
|
|
40
51
|
|
|
41
|
-
const readFiles = async task => {
|
|
52
|
+
const readFiles = async (task) => {
|
|
42
53
|
const gtfsPath = untildify(task.path);
|
|
43
54
|
task.log(`Importing GTFS from ${task.path}\r`);
|
|
44
55
|
if (path.extname(gtfsPath) === '.zip') {
|
|
@@ -49,22 +60,37 @@ const readFiles = async task => {
|
|
|
49
60
|
// If no .txt files in this directory, check for subdirectories and copy them here
|
|
50
61
|
if (textFiles.length === 0) {
|
|
51
62
|
const files = await readdir(task.downloadDir);
|
|
52
|
-
const folders = files
|
|
63
|
+
const folders = files
|
|
64
|
+
.map((filename) => path.join(task.downloadDir, filename))
|
|
65
|
+
.filter((source) => lstatSync(source).isDirectory());
|
|
53
66
|
|
|
54
67
|
if (folders.length > 1) {
|
|
55
|
-
throw new Error(
|
|
68
|
+
throw new Error(
|
|
69
|
+
`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.`
|
|
70
|
+
);
|
|
56
71
|
} else if (folders.length === 0) {
|
|
57
|
-
throw new Error(
|
|
72
|
+
throw new Error(
|
|
73
|
+
`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.`
|
|
74
|
+
);
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
const subfolderName = folders[0];
|
|
61
78
|
const directoryTextFiles = await getTextFiles(subfolderName);
|
|
62
79
|
|
|
63
80
|
if (directoryTextFiles.length === 0) {
|
|
64
|
-
throw new Error(
|
|
81
|
+
throw new Error(
|
|
82
|
+
`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.`
|
|
83
|
+
);
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
await Promise.all(
|
|
86
|
+
await Promise.all(
|
|
87
|
+
directoryTextFiles.map(async (fileName) =>
|
|
88
|
+
rename(
|
|
89
|
+
path.join(subfolderName, fileName),
|
|
90
|
+
path.join(task.downloadDir, fileName)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
);
|
|
68
94
|
}
|
|
69
95
|
} catch (error) {
|
|
70
96
|
task.error(error);
|
|
@@ -74,52 +100,66 @@ const readFiles = async task => {
|
|
|
74
100
|
} else {
|
|
75
101
|
// Local file is unzipped, just copy it from there.
|
|
76
102
|
try {
|
|
77
|
-
|
|
103
|
+
await copy(gtfsPath, task.downloadDir);
|
|
78
104
|
} catch {
|
|
79
|
-
throw new Error(
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`
|
|
107
|
+
);
|
|
80
108
|
}
|
|
81
109
|
}
|
|
82
110
|
};
|
|
83
111
|
|
|
84
|
-
const deleteTables = db =>
|
|
85
|
-
|
|
86
|
-
))
|
|
87
|
-
|
|
88
|
-
const createTables = db => Promise.all(models.map(async model => {
|
|
89
|
-
if (!model.schema) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const columns = model.schema.map(column => {
|
|
94
|
-
let check = '';
|
|
95
|
-
if (column.min !== undefined && column.max) {
|
|
96
|
-
check = `CHECK( ${column.name} >= ${column.min} AND ${column.name} <= ${column.max} )`;
|
|
97
|
-
} else if (column.min) {
|
|
98
|
-
check = `CHECK( ${column.name} >= ${column.min} )`;
|
|
99
|
-
} else if (column.max) {
|
|
100
|
-
check = `CHECK( ${column.name} <= ${column.max} )`;
|
|
101
|
-
}
|
|
112
|
+
const deleteTables = (db) =>
|
|
113
|
+
Promise.all(
|
|
114
|
+
models.map((model) => db.run(`DROP TABLE IF EXISTS ${model.filenameBase};`))
|
|
115
|
+
);
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
const createTables = (db) =>
|
|
118
|
+
Promise.all(
|
|
119
|
+
models.map(async (model) => {
|
|
120
|
+
if (!model.schema) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
const columns = model.schema.map((column) => {
|
|
125
|
+
let check = '';
|
|
126
|
+
if (column.min !== undefined && column.max) {
|
|
127
|
+
check = `CHECK( ${column.name} >= ${column.min} AND ${column.name} <= ${column.max} )`;
|
|
128
|
+
} else if (column.min) {
|
|
129
|
+
check = `CHECK( ${column.name} >= ${column.min} )`;
|
|
130
|
+
} else if (column.max) {
|
|
131
|
+
check = `CHECK( ${column.name} <= ${column.max} )`;
|
|
132
|
+
}
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
const primary = column.primary ? 'PRIMARY KEY' : '';
|
|
135
|
+
const required = column.required ? 'NOT NULL' : '';
|
|
136
|
+
const columnDefault = column.default ? 'DEFAULT ' + column.default : '';
|
|
137
|
+
return `${column.name} ${column.type} ${check} ${primary} ${required} ${columnDefault}`;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await db.run(
|
|
141
|
+
`CREATE TABLE ${model.filenameBase} (${columns.join(', ')});`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await Promise.all(
|
|
145
|
+
model.schema.map(async (column) => {
|
|
146
|
+
if (column.index) {
|
|
147
|
+
const unique = column.index === 'unique' ? 'UNIQUE' : '';
|
|
148
|
+
await db.run(
|
|
149
|
+
`CREATE ${unique} INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
})
|
|
155
|
+
);
|
|
118
156
|
|
|
119
157
|
const formatLine = (line, model, totalLineCount) => {
|
|
120
158
|
const lineNumber = totalLineCount + 1;
|
|
121
159
|
for (const fieldName of Object.keys(line)) {
|
|
122
|
-
const columnSchema = model.schema.find(
|
|
160
|
+
const columnSchema = model.schema.find(
|
|
161
|
+
(schema) => schema.name === fieldName
|
|
162
|
+
);
|
|
123
163
|
|
|
124
164
|
// Remove columns not part of model
|
|
125
165
|
if (!columnSchema) {
|
|
@@ -155,18 +195,27 @@ const formatLine = (line, model, totalLineCount) => {
|
|
|
155
195
|
}
|
|
156
196
|
|
|
157
197
|
// Validate required
|
|
158
|
-
if (
|
|
159
|
-
|
|
198
|
+
if (
|
|
199
|
+
columnSchema.required === true &&
|
|
200
|
+
(line[fieldName] === undefined || line[fieldName] === '')
|
|
201
|
+
) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Missing required value in ${model.filenameBase}.txt for ${fieldName} on line ${lineNumber}.`
|
|
204
|
+
);
|
|
160
205
|
}
|
|
161
206
|
|
|
162
207
|
// Validate minimum
|
|
163
208
|
if (columnSchema.min !== undefined && line[fieldName] < columnSchema.min) {
|
|
164
|
-
throw new Error(
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Invalid value in ${model.filenameBase}.txt for ${fieldName} on line ${lineNumber}: below minimum value of ${columnSchema.min}.`
|
|
211
|
+
);
|
|
165
212
|
}
|
|
166
213
|
|
|
167
214
|
// Validate maximum
|
|
168
215
|
if (columnSchema.max !== undefined && line[fieldName] > columnSchema.max) {
|
|
169
|
-
throw new Error(
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Invalid value in ${model.filenameBase}.txt for ${fieldName} on line ${lineNumber}: above maximum value of ${columnSchema.max}.`
|
|
218
|
+
);
|
|
170
219
|
}
|
|
171
220
|
}
|
|
172
221
|
|
|
@@ -175,7 +224,7 @@ const formatLine = (line, model, totalLineCount) => {
|
|
|
175
224
|
'start_time',
|
|
176
225
|
'end_time',
|
|
177
226
|
'arrival_time',
|
|
178
|
-
'departure_time'
|
|
227
|
+
'departure_time',
|
|
179
228
|
];
|
|
180
229
|
|
|
181
230
|
for (const fieldName of timestampFormat) {
|
|
@@ -193,7 +242,7 @@ const importLines = async (task, lines, model, totalLineCount) => {
|
|
|
193
242
|
}
|
|
194
243
|
|
|
195
244
|
const linesToImportCount = lines.length;
|
|
196
|
-
const fieldNames = model.schema.map(column => column.name);
|
|
245
|
+
const fieldNames = model.schema.map((column) => column.name);
|
|
197
246
|
const placeholders = [];
|
|
198
247
|
const values = [];
|
|
199
248
|
|
|
@@ -206,103 +255,129 @@ const importLines = async (task, lines, model, totalLineCount) => {
|
|
|
206
255
|
}
|
|
207
256
|
|
|
208
257
|
try {
|
|
209
|
-
await task.db.run(
|
|
258
|
+
await task.db.run(
|
|
259
|
+
`INSERT INTO ${model.filenameBase}(${fieldNames.join(
|
|
260
|
+
', '
|
|
261
|
+
)}) VALUES${placeholders.join(',')}`,
|
|
262
|
+
values
|
|
263
|
+
);
|
|
210
264
|
} catch (error) {
|
|
211
|
-
task.warn(
|
|
265
|
+
task.warn(
|
|
266
|
+
`Check ${model.filenameBase}.txt for invalid data between lines ${
|
|
267
|
+
totalLineCount - linesToImportCount
|
|
268
|
+
} and ${totalLineCount}.`
|
|
269
|
+
);
|
|
212
270
|
throw error;
|
|
213
271
|
}
|
|
214
272
|
|
|
215
|
-
task.log(
|
|
273
|
+
task.log(
|
|
274
|
+
`Importing - ${model.filenameBase}.txt - ${totalLineCount} lines imported\r`,
|
|
275
|
+
true
|
|
276
|
+
);
|
|
216
277
|
};
|
|
217
278
|
|
|
218
|
-
const importFiles = task
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!existsSync(filepath)) {
|
|
230
|
-
if (!model.nonstandard) {
|
|
231
|
-
task.log(`Importing - ${model.filenameBase}.txt - No file found\r`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
resolve();
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
task.log(`Importing - ${model.filenameBase}.txt\r`);
|
|
239
|
-
|
|
240
|
-
const lines = [];
|
|
241
|
-
let totalLineCount = 0;
|
|
242
|
-
const maxInsertVariables = 800;
|
|
243
|
-
const parser = parse({
|
|
244
|
-
columns: true,
|
|
245
|
-
relax: true,
|
|
246
|
-
trim: true,
|
|
247
|
-
skip_empty_lines: true,
|
|
248
|
-
...task.csvOptions
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
parser.on('readable', async () => {
|
|
252
|
-
let record;
|
|
253
|
-
/* eslint-disable-next-line no-cond-assign */
|
|
254
|
-
while (record = parser.read()) {
|
|
255
|
-
try {
|
|
256
|
-
totalLineCount += 1;
|
|
257
|
-
lines.push(formatLine(record, model, totalLineCount));
|
|
258
|
-
|
|
259
|
-
// If we have a bunch of lines ready to insert, then do it
|
|
260
|
-
if (lines.length >= maxInsertVariables / model.schema.length) {
|
|
261
|
-
/* eslint-disable-next-line no-await-in-loop */
|
|
262
|
-
await importLines(task, lines, model, totalLineCount);
|
|
279
|
+
const importFiles = (task) =>
|
|
280
|
+
mapSeries(
|
|
281
|
+
models,
|
|
282
|
+
(model) =>
|
|
283
|
+
new Promise((resolve, reject) => {
|
|
284
|
+
// Loop through each GTFS file
|
|
285
|
+
// Filter out excluded files from config
|
|
286
|
+
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
287
|
+
task.log(`Skipping - ${model.filenameBase}.txt\r`);
|
|
288
|
+
resolve();
|
|
289
|
+
return;
|
|
263
290
|
}
|
|
264
|
-
} catch (error) {
|
|
265
|
-
reject(error);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
291
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
292
|
+
const filepath = path.join(
|
|
293
|
+
task.downloadDir,
|
|
294
|
+
`${model.filenameBase}.txt`
|
|
295
|
+
);
|
|
275
296
|
|
|
276
|
-
|
|
297
|
+
if (!existsSync(filepath)) {
|
|
298
|
+
if (!model.nonstandard) {
|
|
299
|
+
task.log(`Importing - ${model.filenameBase}.txt - No file found\r`);
|
|
300
|
+
}
|
|
277
301
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}));
|
|
302
|
+
resolve();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
282
305
|
|
|
283
|
-
|
|
306
|
+
task.log(`Importing - ${model.filenameBase}.txt\r`);
|
|
307
|
+
|
|
308
|
+
const lines = [];
|
|
309
|
+
let totalLineCount = 0;
|
|
310
|
+
const maxInsertVariables = 800;
|
|
311
|
+
const parser = parse({
|
|
312
|
+
columns: true,
|
|
313
|
+
relax: true,
|
|
314
|
+
trim: true,
|
|
315
|
+
skip_empty_lines: true,
|
|
316
|
+
...task.csvOptions,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
parser.on('readable', async () => {
|
|
320
|
+
let record;
|
|
321
|
+
|
|
322
|
+
while ((record = parser.read())) {
|
|
323
|
+
try {
|
|
324
|
+
totalLineCount += 1;
|
|
325
|
+
lines.push(formatLine(record, model, totalLineCount));
|
|
326
|
+
|
|
327
|
+
// If we have a bunch of lines ready to insert, then do it
|
|
328
|
+
if (lines.length >= maxInsertVariables / model.schema.length) {
|
|
329
|
+
/* eslint-disable-next-line no-await-in-loop */
|
|
330
|
+
await importLines(task, lines, model, totalLineCount);
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
reject(error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
parser.on('end', async () => {
|
|
339
|
+
// Insert all remaining lines
|
|
340
|
+
await importLines(task, lines, model, totalLineCount).catch(reject);
|
|
341
|
+
resolve();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
parser.on('error', reject);
|
|
345
|
+
|
|
346
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const importGtfs = async (initialConfig) => {
|
|
284
351
|
const config = setDefaultConfig(initialConfig);
|
|
285
352
|
validateConfigForImport(config);
|
|
286
353
|
const log = _log(config);
|
|
287
354
|
const logError = _logError(config);
|
|
288
355
|
const logWarning = _logWarning(config);
|
|
289
|
-
const db = await openDb(config).catch(error => {
|
|
356
|
+
const db = await openDb(config).catch((error) => {
|
|
290
357
|
if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
|
|
291
|
-
logError(
|
|
358
|
+
logError(
|
|
359
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
360
|
+
);
|
|
292
361
|
}
|
|
293
362
|
|
|
294
363
|
throw error;
|
|
295
364
|
});
|
|
296
365
|
|
|
297
366
|
const agencyCount = config.agencies.length;
|
|
298
|
-
log(
|
|
367
|
+
log(
|
|
368
|
+
`Starting GTFS import for ${pluralize(
|
|
369
|
+
'file',
|
|
370
|
+
agencyCount,
|
|
371
|
+
true
|
|
372
|
+
)} using SQLite database at ${config.sqlitePath}`
|
|
373
|
+
);
|
|
299
374
|
|
|
300
375
|
await deleteTables(db);
|
|
301
376
|
await createTables(db);
|
|
302
377
|
|
|
303
378
|
await setupDb(db);
|
|
304
379
|
|
|
305
|
-
await mapSeries(config.agencies, async agency => {
|
|
380
|
+
await mapSeries(config.agencies, async (agency) => {
|
|
306
381
|
const { path, cleanup } = await dir({ unsafeCleanup: true });
|
|
307
382
|
|
|
308
383
|
const task = {
|
|
@@ -316,12 +391,12 @@ const importGtfs = async initialConfig => {
|
|
|
316
391
|
log: (message, overwrite) => {
|
|
317
392
|
log(message, overwrite);
|
|
318
393
|
},
|
|
319
|
-
warn: message => {
|
|
394
|
+
warn: (message) => {
|
|
320
395
|
logWarning(message);
|
|
321
396
|
},
|
|
322
|
-
error: message => {
|
|
397
|
+
error: (message) => {
|
|
323
398
|
logError(message);
|
|
324
|
-
}
|
|
399
|
+
},
|
|
325
400
|
};
|
|
326
401
|
|
|
327
402
|
if (task.agency_url) {
|
package/lib/log-utils.js
CHANGED
|
@@ -38,7 +38,7 @@ export function logWarning(config) {
|
|
|
38
38
|
return config.logFunction;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
return text => {
|
|
41
|
+
return (text) => {
|
|
42
42
|
process.stdout.write(`\n${formatWarning(text)}\n`);
|
|
43
43
|
};
|
|
44
44
|
}
|
|
@@ -51,7 +51,7 @@ export function logError(config) {
|
|
|
51
51
|
return config.logFunction;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
return text => {
|
|
54
|
+
return (text) => {
|
|
55
55
|
process.stdout.write(`\n${formatError(text)}\n`);
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -60,7 +60,9 @@ export function logError(config) {
|
|
|
60
60
|
* Format console warning text
|
|
61
61
|
*/
|
|
62
62
|
export function formatWarning(text) {
|
|
63
|
-
return `${chalk.yellow.underline('Warning')}${chalk.yellow(
|
|
63
|
+
return `${chalk.yellow.underline('Warning')}${chalk.yellow(
|
|
64
|
+
':'
|
|
65
|
+
)} ${chalk.yellow(text)}`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/*
|
|
@@ -68,5 +70,7 @@ export function formatWarning(text) {
|
|
|
68
70
|
*/
|
|
69
71
|
export function formatError(error) {
|
|
70
72
|
const message = error instanceof Error ? error.message : error;
|
|
71
|
-
return `${chalk.red.underline('Error')}${chalk.red(':')} ${chalk.red(
|
|
73
|
+
return `${chalk.red.underline('Error')}${chalk.red(':')} ${chalk.red(
|
|
74
|
+
message.replace('Error: ', '')
|
|
75
|
+
)}`;
|
|
72
76
|
}
|