gtfs 4.17.7 → 4.18.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.
@@ -4158,7 +4158,7 @@ function isValidJSON(string) {
4158
4158
  try {
4159
4159
  JSON.parse(string);
4160
4160
  return true;
4161
- } catch (error) {
4161
+ } catch {
4162
4162
  return false;
4163
4163
  }
4164
4164
  }
@@ -4190,7 +4190,8 @@ function setDefaultConfig(initialConfig) {
4190
4190
  ignoreDuplicates: false,
4191
4191
  ignoreErrors: false,
4192
4192
  gtfsRealtimeExpirationSeconds: 0,
4193
- verbose: true
4193
+ verbose: true,
4194
+ downloadTimeout: 3e4
4194
4195
  };
4195
4196
  return {
4196
4197
  ...defaults,
@@ -4224,36 +4225,9 @@ function pluralize(singularWord, pluralWord, count) {
4224
4225
  }
4225
4226
 
4226
4227
  // src/lib/import-gtfs-realtime.ts
4227
- async function fetchGtfsRealtimeData(urlConfig, task) {
4228
- task.log(`Downloading GTFS-Realtime from ${urlConfig.url}`);
4229
- const response = await fetch(urlConfig.url, {
4230
- method: "GET",
4231
- headers: {
4232
- ...urlConfig.headers ?? {},
4233
- "Accept-Encoding": "gzip"
4234
- },
4235
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4236
- });
4237
- if (response.status !== 200) {
4238
- task.logWarning(
4239
- `Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
4240
- );
4241
- return null;
4242
- }
4243
- const buffer = await response.arrayBuffer();
4244
- const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
4245
- new Uint8Array(buffer)
4246
- );
4247
- return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
4248
- enums: String,
4249
- longs: String,
4250
- bytes: String,
4251
- defaults: false,
4252
- arrays: true,
4253
- objects: true,
4254
- oneofs: true
4255
- });
4256
- }
4228
+ var BATCH_SIZE = 1e3;
4229
+ var MAX_RETRIES = 3;
4230
+ var RETRY_DELAY = 1e3;
4257
4231
  function prepareRealtimeFieldValue(entity, column, task) {
4258
4232
  if (column.name === "created_timestamp") {
4259
4233
  return task.currentTimestamp;
@@ -4270,163 +4244,246 @@ function prepareRealtimeFieldValue(entity, column, task) {
4270
4244
  );
4271
4245
  return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
4272
4246
  }
4273
- async function processRealtimeAlerts(db, gtfsRealtimeData, task) {
4274
- const alertStmt = db.prepare(
4275
- `REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(
4276
- ", "
4277
- )}) VALUES (${serviceAlerts.schema.map(() => "?").join(", ")})`
4278
- );
4279
- const informedEntityStmt = db.prepare(
4280
- `REPLACE INTO ${serviceAlertInformedEntities.filenameBase} (${serviceAlertInformedEntities.schema.map((column) => column.name).join(
4281
- ", "
4282
- )}) VALUES (${serviceAlertInformedEntities.schema.map(() => "?").join(", ")})`
4247
+ function createPreparedStatement(db, model) {
4248
+ const columns = model.schema.map((column) => column.name);
4249
+ const placeholders = model.schema.map(() => "?").join(", ");
4250
+ return db.prepare(
4251
+ `REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
4283
4252
  );
4284
- let totalLineCount = 0;
4285
- db.transaction(() => {
4286
- for (const entity of gtfsRealtimeData.entity) {
4287
- const fieldValues = serviceAlerts.schema.map(
4288
- (column) => prepareRealtimeFieldValue(entity, column, task)
4253
+ }
4254
+ async function processBatch(items, batchSize, processor) {
4255
+ let totalRecordCount = 0;
4256
+ let totalErrorCount = 0;
4257
+ for (let i = 0; i < items.length; i += batchSize) {
4258
+ const batch = items.slice(i, i + batchSize);
4259
+ try {
4260
+ const result = await processor(batch);
4261
+ totalRecordCount += result.recordCount;
4262
+ totalErrorCount += result.errorCount;
4263
+ } catch (error) {
4264
+ const errorMessage = error instanceof Error ? error.message : String(error);
4265
+ totalErrorCount += batch.length;
4266
+ console.error(`Batch processing error: ${errorMessage}`);
4267
+ }
4268
+ }
4269
+ return { recordCount: totalRecordCount, errorCount: totalErrorCount };
4270
+ }
4271
+ async function fetchGtfsRealtimeData(type, task) {
4272
+ const urlConfig = getUrlConfig(type, task);
4273
+ if (!urlConfig) {
4274
+ return null;
4275
+ }
4276
+ task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
4277
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
4278
+ try {
4279
+ const response = await fetch(urlConfig.url, {
4280
+ method: "GET",
4281
+ headers: {
4282
+ ...urlConfig.headers ?? {},
4283
+ "Accept-Encoding": "gzip"
4284
+ },
4285
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4286
+ });
4287
+ if (response.status !== 200) {
4288
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4289
+ }
4290
+ const buffer = await response.arrayBuffer();
4291
+ const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
4292
+ new Uint8Array(buffer)
4289
4293
  );
4290
- try {
4291
- alertStmt.run(fieldValues);
4292
- if (entity.alert.informedEntity?.length) {
4293
- const informedEntities = entity.alert.informedEntity.map(
4294
- (informedEntity) => {
4294
+ const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
4295
+ enums: String,
4296
+ longs: String,
4297
+ bytes: String,
4298
+ defaults: false,
4299
+ arrays: true,
4300
+ objects: true,
4301
+ oneofs: true
4302
+ });
4303
+ return feedMessage;
4304
+ } catch (error) {
4305
+ const errorMessage = error instanceof Error ? error.message : String(error);
4306
+ if (attempt === MAX_RETRIES) {
4307
+ if (task.ignoreErrors) {
4308
+ task.logError(
4309
+ `Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
4310
+ );
4311
+ return null;
4312
+ }
4313
+ throw error;
4314
+ }
4315
+ task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
4316
+ await new Promise(
4317
+ (resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
4318
+ );
4319
+ }
4320
+ }
4321
+ return null;
4322
+ }
4323
+ function getUrlConfig(type, task) {
4324
+ switch (type) {
4325
+ case "alerts":
4326
+ return task.realtimeAlerts;
4327
+ case "tripupdates":
4328
+ return task.realtimeTripUpdates;
4329
+ case "vehiclepositions":
4330
+ return task.realtimeVehiclePositions;
4331
+ default:
4332
+ return void 0;
4333
+ }
4334
+ }
4335
+ function createServiceAlertsProcessor(db, task) {
4336
+ const alertStmt = createPreparedStatement(db, serviceAlerts);
4337
+ const informedEntityStmt = createPreparedStatement(
4338
+ db,
4339
+ serviceAlertInformedEntities
4340
+ );
4341
+ return async (batch) => {
4342
+ let recordCount = 0;
4343
+ let errorCount = 0;
4344
+ db.transaction(() => {
4345
+ for (const entity of batch) {
4346
+ try {
4347
+ const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4348
+ alertStmt.run(alertValues);
4349
+ recordCount++;
4350
+ if (entity.alert?.informedEntity?.length) {
4351
+ for (const informedEntity of entity.alert.informedEntity) {
4295
4352
  informedEntity.parent = entity;
4296
- return serviceAlertInformedEntities.schema.map(
4353
+ const entityValues = serviceAlertInformedEntities.schema.map(
4297
4354
  (column) => prepareRealtimeFieldValue(informedEntity, column, task)
4298
4355
  );
4356
+ informedEntityStmt.run(entityValues);
4357
+ recordCount++;
4299
4358
  }
4300
- );
4301
- for (const values of informedEntities) {
4302
- informedEntityStmt.run(values);
4303
4359
  }
4360
+ } catch (error) {
4361
+ const errorMessage = error instanceof Error ? error.message : String(error);
4362
+ errorCount++;
4363
+ task.logWarning(`Alert processing error: ${errorMessage}`);
4304
4364
  }
4305
- totalLineCount++;
4306
- } catch (error) {
4307
- task.logWarning(`Import error: ${error.message}`);
4308
4365
  }
4309
- }
4310
- task.log(
4311
- `Importing - GTFS-Realtime service alerts - ${totalLineCount} entries imported\r`,
4312
- true
4313
- );
4314
- })();
4366
+ })();
4367
+ return { recordCount, errorCount };
4368
+ };
4315
4369
  }
4316
- async function processRealtimeTripUpdates(db, gtfsRealtimeData, task) {
4317
- let totalLineCount = 0;
4318
- const tripUpdateStmt = db.prepare(
4319
- `REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(
4320
- ", "
4321
- )}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
4370
+ function createTripUpdatesProcessor(db, task) {
4371
+ const tripUpdateStmt = createPreparedStatement(
4372
+ db,
4373
+ tripUpdates
4322
4374
  );
4323
- const stopTimeStmt = db.prepare(
4324
- `REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(
4325
- ", "
4326
- )}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
4375
+ const stopTimeStmt = createPreparedStatement(
4376
+ db,
4377
+ stopTimeUpdates
4327
4378
  );
4328
- db.transaction(() => {
4329
- for (const entity of gtfsRealtimeData.entity) {
4330
- try {
4331
- const fieldValues = tripUpdates.schema.map(
4332
- (column) => prepareRealtimeFieldValue(entity, column, task)
4333
- );
4334
- tripUpdateStmt.run(fieldValues);
4335
- for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
4336
- stopTimeUpdate.parent = entity;
4337
- const values = stopTimeUpdates.schema.map(
4338
- (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
4339
- );
4340
- stopTimeStmt.run(values);
4379
+ return async (batch) => {
4380
+ let recordCount = 0;
4381
+ let errorCount = 0;
4382
+ db.transaction(() => {
4383
+ for (const entity of batch) {
4384
+ try {
4385
+ const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4386
+ tripUpdateStmt.run(tripUpdateValues);
4387
+ recordCount++;
4388
+ if (entity.tripUpdate?.stopTimeUpdate?.length) {
4389
+ for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
4390
+ stopTimeUpdate.parent = entity;
4391
+ const stopTimeValues = stopTimeUpdates.schema.map(
4392
+ (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
4393
+ );
4394
+ stopTimeStmt.run(stopTimeValues);
4395
+ recordCount++;
4396
+ }
4397
+ }
4398
+ } catch (error) {
4399
+ const errorMessage = error instanceof Error ? error.message : String(error);
4400
+ errorCount++;
4401
+ task.logWarning(`Trip update processing error: ${errorMessage}`);
4341
4402
  }
4342
- totalLineCount++;
4343
- } catch (error) {
4344
- task.logWarning(`Import error: ${error.message}`);
4345
4403
  }
4346
- }
4347
- task.log(
4348
- `Importing - GTFS-Realtime trip updates - ${totalLineCount} entries imported\r`,
4349
- true
4350
- );
4351
- })();
4404
+ })();
4405
+ return { recordCount, errorCount };
4406
+ };
4352
4407
  }
4353
- async function processRealtimeVehiclePositions(db, gtfsRealtimeData, task) {
4354
- let totalLineCount = 0;
4355
- const vehiclePositionStmt = db.prepare(
4356
- `REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(
4357
- ", "
4358
- )}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
4408
+ function createVehiclePositionsProcessor(db, task) {
4409
+ const vehiclePositionStmt = createPreparedStatement(
4410
+ db,
4411
+ vehiclePositions
4359
4412
  );
4360
- db.transaction(() => {
4361
- for (const entity of gtfsRealtimeData.entity) {
4362
- try {
4363
- const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4364
- vehiclePositionStmt.run(fieldValues);
4365
- totalLineCount++;
4366
- } catch (error) {
4367
- task.logWarning(`Import error: ${error.message}`);
4413
+ return async (batch) => {
4414
+ let recordCount = 0;
4415
+ let errorCount = 0;
4416
+ db.transaction(() => {
4417
+ for (const entity of batch) {
4418
+ try {
4419
+ const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4420
+ vehiclePositionStmt.run(fieldValues);
4421
+ recordCount++;
4422
+ } catch (error) {
4423
+ const errorMessage = error instanceof Error ? error.message : String(error);
4424
+ errorCount++;
4425
+ task.logWarning(`Vehicle position processing error: ${errorMessage}`);
4426
+ }
4368
4427
  }
4369
- }
4370
- task.log(
4371
- `Importing - GTFS-Realtime vehicle positions - ${totalLineCount} entries imported\r`,
4372
- true
4373
- );
4374
- })();
4428
+ })();
4429
+ return { recordCount, errorCount };
4430
+ };
4375
4431
  }
4376
4432
  async function updateGtfsRealtimeData(task) {
4377
- if (task.realtimeAlerts === void 0 && task.realtimeTripUpdates === void 0 && task.realtimeVehiclePositions === void 0) {
4433
+ if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
4378
4434
  return;
4379
4435
  }
4436
+ const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
4437
+ [
4438
+ task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
4439
+ task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
4440
+ task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
4441
+ ]
4442
+ );
4380
4443
  const db = openDb({ sqlitePath: task.sqlitePath });
4381
- if (task.realtimeAlerts?.url) {
4382
- try {
4383
- const alertsData = await fetchGtfsRealtimeData(task.realtimeAlerts, task);
4384
- if (alertsData?.entity) {
4385
- await processRealtimeAlerts(db, alertsData, task);
4386
- }
4387
- } catch (error) {
4388
- if (task.ignoreErrors) {
4389
- task.logError(error.message);
4390
- } else {
4391
- throw error;
4392
- }
4393
- }
4444
+ const recordCounts = {
4445
+ alerts: 0,
4446
+ tripupdates: 0,
4447
+ vehiclepositions: 0
4448
+ };
4449
+ const processingPromises = [];
4450
+ if (alertsData?.entity?.length) {
4451
+ processingPromises.push(
4452
+ processBatch(
4453
+ alertsData.entity,
4454
+ BATCH_SIZE,
4455
+ createServiceAlertsProcessor(db, task)
4456
+ ).then((result) => {
4457
+ recordCounts.alerts = result.recordCount;
4458
+ })
4459
+ );
4394
4460
  }
4395
- if (task.realtimeTripUpdates?.url) {
4396
- try {
4397
- const tripUpdatesData = await fetchGtfsRealtimeData(
4398
- task.realtimeTripUpdates,
4399
- task
4400
- );
4401
- if (tripUpdatesData?.entity) {
4402
- await processRealtimeTripUpdates(db, tripUpdatesData, task);
4403
- }
4404
- } catch (error) {
4405
- if (task.ignoreErrors) {
4406
- task.logError(error.message);
4407
- } else {
4408
- throw error;
4409
- }
4410
- }
4461
+ if (tripUpdatesData?.entity?.length) {
4462
+ processingPromises.push(
4463
+ processBatch(
4464
+ tripUpdatesData.entity,
4465
+ BATCH_SIZE,
4466
+ createTripUpdatesProcessor(db, task)
4467
+ ).then((result) => {
4468
+ recordCounts.tripupdates = result.recordCount;
4469
+ })
4470
+ );
4411
4471
  }
4412
- if (task.realtimeVehiclePositions?.url) {
4413
- try {
4414
- const vehiclePositionsData = await fetchGtfsRealtimeData(
4415
- task.realtimeVehiclePositions,
4416
- task
4417
- );
4418
- if (vehiclePositionsData?.entity) {
4419
- await processRealtimeVehiclePositions(db, vehiclePositionsData, task);
4420
- }
4421
- } catch (error) {
4422
- if (task.ignoreErrors) {
4423
- task.logError(error.message);
4424
- } else {
4425
- throw error;
4426
- }
4427
- }
4472
+ if (vehiclePositionsData?.entity?.length) {
4473
+ processingPromises.push(
4474
+ processBatch(
4475
+ vehiclePositionsData.entity,
4476
+ BATCH_SIZE,
4477
+ createVehiclePositionsProcessor(db, task)
4478
+ ).then((result) => {
4479
+ recordCounts.vehiclepositions = result.recordCount;
4480
+ })
4481
+ );
4428
4482
  }
4429
- task.log(`GTFS-Realtime data import complete`);
4483
+ await Promise.all(processingPromises);
4484
+ task.log(
4485
+ `GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
4486
+ );
4430
4487
  }
4431
4488
 
4432
4489
  // src/lib/import-gtfs.ts
@@ -4459,7 +4516,7 @@ var extractGtfsFiles = async (task) => {
4459
4516
  throw new Error("No `path` specified in config");
4460
4517
  }
4461
4518
  const gtfsPath = untildify(task.path);
4462
- task.log(`Importing GTFS from ${task.path}\r`);
4519
+ task.log(`Importing static GTFS from ${task.path}\r`);
4463
4520
  if (path2.extname(gtfsPath) === ".zip") {
4464
4521
  try {
4465
4522
  await unzip(gtfsPath, task.downloadDir);
@@ -4542,11 +4599,11 @@ var createGtfsTables = (db) => {
4542
4599
  if (column.type === "time") {
4543
4600
  sqlColumnCreateStatements.push(
4544
4601
  `${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
4545
- CASE
4546
- WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
4602
+ CASE
4603
+ WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
4547
4604
  ELSE CAST(
4548
- substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
4549
- substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
4605
+ substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
4606
+ substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
4550
4607
  substr(${column.name}, -2) AS INTEGER
4551
4608
  )
4552
4609
  END
@@ -4603,7 +4660,7 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4603
4660
  continue;
4604
4661
  }
4605
4662
  if (type === "date") {
4606
- value = value.replace(/-/g, "");
4663
+ value = value?.toString().replace(/-/g, "");
4607
4664
  if (value.length !== 8) {
4608
4665
  throw new Error(
4609
4666
  `Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
@@ -4619,203 +4676,212 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4619
4676
  }
4620
4677
  return formattedLine;
4621
4678
  };
4622
- var BATCH_SIZE = 1e5;
4623
- var importGtfsFiles = (db, task) => mapSeries2(
4624
- Object.values(models_exports),
4625
- (model) => new Promise((resolve, reject) => {
4626
- let totalLineCount = 0;
4627
- const filename = `${model.filenameBase}.${model.filenameExtension}`;
4628
- if (task.exclude && task.exclude.includes(model.filenameBase)) {
4629
- task.log(`Skipping - ${filename}\r`);
4630
- resolve();
4631
- return;
4632
- }
4633
- if (model.extension === "gtfs-realtime") {
4634
- resolve();
4635
- return;
4636
- }
4637
- const filepath = path2.join(task.downloadDir, `${filename}`);
4638
- if (!existsSync2(filepath)) {
4639
- if (!model.nonstandard) {
4640
- task.log(`Importing - ${filename} - No file found\r`);
4679
+ var BATCH_SIZE2 = 1e5;
4680
+ var importGtfsFiles = async (db, task) => {
4681
+ await mapSeries2(
4682
+ Object.values(models_exports),
4683
+ (model) => new Promise((resolve, reject) => {
4684
+ let totalLineCount = 0;
4685
+ const filename = `${model.filenameBase}.${model.filenameExtension}`;
4686
+ if (task.exclude && task.exclude.includes(model.filenameBase)) {
4687
+ task.log(`Skipping - ${filename}\r`);
4688
+ resolve();
4689
+ return;
4641
4690
  }
4642
- resolve();
4643
- return;
4644
- }
4645
- task.log(`Importing - ${filename}\r`);
4646
- const columns = model.schema;
4647
- const prefixedColumns = new Set(
4648
- columns.filter((column) => column.prefix).map((column) => column.name)
4649
- );
4650
- const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
4651
- const insert = db.prepare(prepareStatement);
4652
- const insertLines = db.transaction((lines) => {
4653
- for (const [rowNumber, line] of Object.entries(lines)) {
4654
- try {
4655
- if (task.prefix === void 0) {
4656
- insert.run(line);
4657
- } else {
4658
- const prefixedLine = Object.fromEntries(
4659
- Object.entries(
4660
- line
4661
- ).map(([columnName, value]) => [
4662
- columnName,
4663
- applyPrefixToValue(
4664
- value,
4665
- prefixedColumns.has(columnName),
4666
- task.prefix
4667
- )
4668
- ])
4669
- );
4670
- insert.run(prefixedLine);
4671
- }
4672
- } catch (error) {
4673
- if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
4674
- const primaryColumns = columns.filter(
4675
- (column) => column.primary
4676
- );
4677
- task.logWarning(
4678
- `Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${filename}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`
4679
- );
4680
- }
4681
- task.logWarning(
4682
- `Check ${filename} for invalid data on line ${rowNumber + 1}.`
4683
- );
4684
- throw error;
4691
+ if (model.extension === "gtfs-realtime") {
4692
+ resolve();
4693
+ return;
4694
+ }
4695
+ const filepath = path2.join(task.downloadDir, `${filename}`);
4696
+ if (!existsSync2(filepath)) {
4697
+ if (!model.nonstandard) {
4698
+ task.log(`Importing - ${filename} - No file found\r`);
4685
4699
  }
4700
+ resolve();
4701
+ return;
4686
4702
  }
4687
- });
4688
- if (model.filenameExtension === "txt") {
4689
- const parser = parse({
4690
- columns: true,
4691
- relax_quotes: true,
4692
- trim: true,
4693
- skip_empty_lines: true,
4694
- ...task.csvOptions
4695
- });
4696
- let lines = [];
4697
- parser.on("readable", () => {
4698
- try {
4699
- let record;
4700
- while (record = parser.read()) {
4701
- totalLineCount += 1;
4702
- lines.push(formatGtfsLine(record, model, totalLineCount));
4703
- if (lines.length >= BATCH_SIZE) {
4704
- insertLines(lines);
4705
- lines = [];
4706
- task.log(
4707
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4708
- true
4703
+ task.log(`Importing - ${filename}\r`);
4704
+ const columns = model.schema;
4705
+ const prefixedColumns = new Set(
4706
+ columns.filter((column) => column.prefix).map((column) => column.name)
4707
+ );
4708
+ const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
4709
+ const insert = db.prepare(prepareStatement);
4710
+ const insertLines = db.transaction((lines) => {
4711
+ for (const [rowNumber, line] of Object.entries(lines)) {
4712
+ try {
4713
+ if (task.prefix === void 0) {
4714
+ insert.run(line);
4715
+ } else {
4716
+ const prefixedLine = Object.fromEntries(
4717
+ Object.entries(
4718
+ line
4719
+ ).map(([columnName, value]) => [
4720
+ columnName,
4721
+ applyPrefixToValue(
4722
+ value,
4723
+ prefixedColumns.has(columnName),
4724
+ task.prefix
4725
+ )
4726
+ ])
4709
4727
  );
4728
+ insert.run(prefixedLine);
4710
4729
  }
4711
- }
4712
- } catch (error) {
4713
- if (task.ignoreErrors) {
4714
- task.logError(`Error processing ${filename}: ${error.message}`);
4715
- resolve();
4716
- } else {
4717
- reject(error);
4730
+ } catch (error) {
4731
+ if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
4732
+ const primaryColumns = columns.filter(
4733
+ (column) => column.primary
4734
+ );
4735
+ task.logWarning(
4736
+ `Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${filename}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`
4737
+ );
4738
+ }
4739
+ task.logWarning(
4740
+ `Check ${filename} for invalid data on line ${rowNumber + 1}.`
4741
+ );
4742
+ throw error;
4718
4743
  }
4719
4744
  }
4720
4745
  });
4721
- parser.on("end", () => {
4722
- try {
4723
- if (lines.length > 0) {
4724
- try {
4725
- insertLines(lines);
4726
- } catch (error) {
4727
- if (task.ignoreErrors) {
4728
- task.logError(
4729
- `Error inserting data for ${filename}: ${error.message}`
4746
+ if (model.filenameExtension === "txt") {
4747
+ const parser = parse({
4748
+ columns: true,
4749
+ relax_quotes: true,
4750
+ trim: true,
4751
+ skip_empty_lines: true,
4752
+ ...task.csvOptions
4753
+ });
4754
+ let lines = [];
4755
+ parser.on("readable", () => {
4756
+ try {
4757
+ let record;
4758
+ while (record = parser.read()) {
4759
+ totalLineCount += 1;
4760
+ lines.push(formatGtfsLine(record, model, totalLineCount));
4761
+ if (lines.length >= BATCH_SIZE2) {
4762
+ insertLines(lines);
4763
+ lines = [];
4764
+ task.log(
4765
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4766
+ true
4730
4767
  );
4731
- resolve();
4732
- return;
4733
- } else {
4734
- reject(error);
4735
- return;
4736
4768
  }
4737
4769
  }
4770
+ } catch (error) {
4771
+ if (task.ignoreErrors) {
4772
+ const errorMessage = error instanceof Error ? error.message : String(error);
4773
+ task.logError(`Error processing ${filename}: ${errorMessage}`);
4774
+ resolve();
4775
+ } else {
4776
+ reject(error);
4777
+ }
4778
+ }
4779
+ });
4780
+ parser.on("end", () => {
4781
+ try {
4782
+ if (lines.length > 0) {
4783
+ try {
4784
+ insertLines(lines);
4785
+ } catch (error) {
4786
+ if (task.ignoreErrors) {
4787
+ const errorMessage = error instanceof Error ? error.message : String(error);
4788
+ task.logError(
4789
+ `Error inserting data for ${filename}: ${errorMessage}`
4790
+ );
4791
+ resolve();
4792
+ return;
4793
+ } else {
4794
+ reject(error);
4795
+ return;
4796
+ }
4797
+ }
4798
+ }
4799
+ task.log(
4800
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4801
+ true
4802
+ );
4803
+ resolve();
4804
+ } catch (error) {
4805
+ if (task.ignoreErrors) {
4806
+ const errorMessage = error instanceof Error ? error.message : String(error);
4807
+ task.logError(`Error finalizing ${filename}: ${errorMessage}`);
4808
+ resolve();
4809
+ } else {
4810
+ reject(error);
4811
+ }
4738
4812
  }
4739
- task.log(
4740
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4741
- true
4742
- );
4743
- resolve();
4744
- } catch (error) {
4813
+ });
4814
+ parser.on("error", (error) => {
4745
4815
  if (task.ignoreErrors) {
4746
- task.logError(`Error finalizing ${filename}: ${error.message}`);
4816
+ const errorMessage = error instanceof Error ? error.message : String(error);
4817
+ task.logError(`Parser error for ${filename}: ${errorMessage}`);
4747
4818
  resolve();
4748
4819
  } else {
4749
4820
  reject(error);
4750
4821
  }
4751
- }
4752
- });
4753
- parser.on("error", (error) => {
4754
- if (task.ignoreErrors) {
4755
- task.logError(`Parser error for ${filename}: ${error.message}`);
4756
- resolve();
4757
- } else {
4758
- reject(error);
4759
- }
4760
- });
4761
- createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
4762
- } else if (model.filenameExtension === "geojson") {
4763
- readFile2(filepath, "utf8").then((data) => {
4764
- if (isValidJSON(data) === false) {
4765
- if (task.ignoreErrors) {
4766
- task.logError(`Invalid JSON in ${filename}`);
4767
- resolve();
4768
- return;
4769
- } else {
4770
- reject(new Error(`Invalid JSON in ${filename}`));
4771
- return;
4822
+ });
4823
+ createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
4824
+ } else if (model.filenameExtension === "geojson") {
4825
+ readFile2(filepath, "utf8").then((data) => {
4826
+ if (isValidJSON(data) === false) {
4827
+ if (task.ignoreErrors) {
4828
+ task.logError(`Invalid JSON in ${filename}`);
4829
+ resolve();
4830
+ return;
4831
+ } else {
4832
+ reject(new Error(`Invalid JSON in ${filename}`));
4833
+ return;
4834
+ }
4772
4835
  }
4773
- }
4774
- totalLineCount += 1;
4775
- const line = formatGtfsLine(
4776
- { geojson: data },
4777
- model,
4778
- totalLineCount
4779
- );
4780
- try {
4781
- insertLines([line]);
4782
- task.log(
4783
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4784
- true
4836
+ totalLineCount += 1;
4837
+ const line = formatGtfsLine(
4838
+ { geojson: data },
4839
+ model,
4840
+ totalLineCount
4785
4841
  );
4786
- resolve();
4787
- } catch (error) {
4788
- if (task.ignoreErrors) {
4789
- task.logError(
4790
- `Error inserting data for ${filename}: ${error.message}`
4842
+ try {
4843
+ insertLines([line]);
4844
+ task.log(
4845
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4846
+ true
4791
4847
  );
4792
4848
  resolve();
4849
+ } catch (error) {
4850
+ if (task.ignoreErrors) {
4851
+ const errorMessage = error instanceof Error ? error.message : String(error);
4852
+ task.logError(
4853
+ `Error inserting data for ${filename}: ${errorMessage}`
4854
+ );
4855
+ resolve();
4856
+ } else {
4857
+ reject(error);
4858
+ }
4859
+ }
4860
+ }).catch((error) => {
4861
+ if (task.ignoreErrors) {
4862
+ const errorMessage = error instanceof Error ? error.message : String(error);
4863
+ task.logError(`Error reading ${filename}: ${errorMessage}`);
4864
+ resolve();
4793
4865
  } else {
4794
4866
  reject(error);
4795
4867
  }
4796
- }
4797
- }).catch((error) => {
4868
+ });
4869
+ } else {
4798
4870
  if (task.ignoreErrors) {
4799
- task.logError(`Error reading ${filename}: ${error.message}`);
4871
+ task.logError(
4872
+ `Unsupported file type: ${model.filenameExtension} for ${filename}`
4873
+ );
4800
4874
  resolve();
4801
4875
  } else {
4802
- reject(error);
4876
+ reject(
4877
+ new Error(`Unsupported file type: ${model.filenameExtension}`)
4878
+ );
4803
4879
  }
4804
- });
4805
- } else {
4806
- if (task.ignoreErrors) {
4807
- task.logError(
4808
- `Unsupported file type: ${model.filenameExtension} for ${filename}`
4809
- );
4810
- resolve();
4811
- } else {
4812
- reject(
4813
- new Error(`Unsupported file type: ${model.filenameExtension}`)
4814
- );
4815
4880
  }
4816
- }
4817
- })
4818
- );
4881
+ })
4882
+ );
4883
+ task.log(`Static GTFS import complete`);
4884
+ };
4819
4885
  async function importGtfs(initialConfig) {
4820
4886
  const startTime = process.hrtime.bigint();
4821
4887
  const config = setDefaultConfig(initialConfig);
@@ -4832,7 +4898,6 @@ async function importGtfs(initialConfig) {
4832
4898
  const tempPath = temporaryDirectory();
4833
4899
  const task = {
4834
4900
  exclude: agency2.exclude,
4835
- url: agency2.url,
4836
4901
  headers: agency2.headers,
4837
4902
  realtimeAlerts: agency2.realtimeAlerts,
4838
4903
  realtimeTripUpdates: agency2.realtimeTripUpdates,
@@ -4840,7 +4905,6 @@ async function importGtfs(initialConfig) {
4840
4905
  downloadDir: tempPath,
4841
4906
  downloadTimeout: config.downloadTimeout,
4842
4907
  gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
4843
- path: agency2.path,
4844
4908
  csvOptions: config.csvOptions || {},
4845
4909
  ignoreDuplicates: config.ignoreDuplicates,
4846
4910
  ignoreErrors: config.ignoreErrors,
@@ -4851,8 +4915,13 @@ async function importGtfs(initialConfig) {
4851
4915
  logWarning: logWarning(config),
4852
4916
  logError: logError(config)
4853
4917
  };
4854
- if (task.url) {
4918
+ if ("url" in agency2) {
4919
+ Object.assign(task, { url: agency2.url });
4855
4920
  await downloadGtfsFiles(task);
4921
+ } else {
4922
+ Object.assign(task, {
4923
+ path: agency2.path
4924
+ });
4856
4925
  }
4857
4926
  await extractGtfsFiles(task);
4858
4927
  await importGtfsFiles(db, task);
@@ -4860,7 +4929,8 @@ async function importGtfs(initialConfig) {
4860
4929
  await rm2(tempPath, { recursive: true });
4861
4930
  } catch (error) {
4862
4931
  if (config.ignoreErrors) {
4863
- logError(config)(error.message);
4932
+ const errorMessage = error instanceof Error ? error.message : String(error);
4933
+ logError(config)(errorMessage);
4864
4934
  } else {
4865
4935
  throw error;
4866
4936
  }
@@ -4875,7 +4945,7 @@ async function importGtfs(initialConfig) {
4875
4945
  `
4876
4946
  );
4877
4947
  } catch (error) {
4878
- if (error?.code === "SQLITE_CANTOPEN") {
4948
+ if (error.code === "SQLITE_CANTOPEN") {
4879
4949
  logError(config)(
4880
4950
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4881
4951
  );