gtfs 4.18.3 → 4.18.4

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/dist/index.js CHANGED
@@ -4078,6 +4078,104 @@ function untildify(pathWithTilde) {
4078
4078
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
4079
4079
  }
4080
4080
 
4081
+ // src/lib/errors.ts
4082
+ var GtfsErrorCategory = /* @__PURE__ */ ((GtfsErrorCategory2) => {
4083
+ GtfsErrorCategory2["CONFIG"] = "config";
4084
+ GtfsErrorCategory2["DOWNLOAD"] = "download";
4085
+ GtfsErrorCategory2["ZIP"] = "zip";
4086
+ GtfsErrorCategory2["VALIDATION"] = "validation";
4087
+ GtfsErrorCategory2["DATABASE"] = "database";
4088
+ GtfsErrorCategory2["PARSE"] = "parse";
4089
+ GtfsErrorCategory2["QUERY"] = "query";
4090
+ GtfsErrorCategory2["INTERNAL"] = "internal";
4091
+ return GtfsErrorCategory2;
4092
+ })(GtfsErrorCategory || {});
4093
+ var GtfsErrorCode = /* @__PURE__ */ ((GtfsErrorCode2) => {
4094
+ GtfsErrorCode2["GTFS_DOWNLOAD_HTTP"] = "GTFS_DOWNLOAD_HTTP";
4095
+ GtfsErrorCode2["GTFS_DOWNLOAD_FAILED"] = "GTFS_DOWNLOAD_FAILED";
4096
+ GtfsErrorCode2["GTFS_ZIP_INVALID"] = "GTFS_ZIP_INVALID";
4097
+ GtfsErrorCode2["GTFS_REQUIRED_FIELD_MISSING"] = "GTFS_REQUIRED_FIELD_MISSING";
4098
+ GtfsErrorCode2["GTFS_INVALID_DATE"] = "GTFS_INVALID_DATE";
4099
+ GtfsErrorCode2["GTFS_CONFIG_INVALID"] = "GTFS_CONFIG_INVALID";
4100
+ GtfsErrorCode2["DB_OPEN_FAILED"] = "DB_OPEN_FAILED";
4101
+ GtfsErrorCode2["GTFS_DB_OPERATION_FAILED"] = "GTFS_DB_OPERATION_FAILED";
4102
+ GtfsErrorCode2["GTFS_JSON_INVALID"] = "GTFS_JSON_INVALID";
4103
+ GtfsErrorCode2["GTFS_UNSUPPORTED_FILE_TYPE"] = "GTFS_UNSUPPORTED_FILE_TYPE";
4104
+ GtfsErrorCode2["GTFS_CSV_PARSE_FAILED"] = "GTFS_CSV_PARSE_FAILED";
4105
+ GtfsErrorCode2["GTFS_QUERY_INVALID"] = "GTFS_QUERY_INVALID";
4106
+ return GtfsErrorCode2;
4107
+ })(GtfsErrorCode || {});
4108
+ var GtfsWarningCode = /* @__PURE__ */ ((GtfsWarningCode2) => {
4109
+ GtfsWarningCode2["GTFS_DUPLICATE_PRIMARY_KEY"] = "GTFS_DUPLICATE_PRIMARY_KEY";
4110
+ return GtfsWarningCode2;
4111
+ })(GtfsWarningCode || {});
4112
+ var GtfsError = class extends Error {
4113
+ code;
4114
+ category;
4115
+ isOperational;
4116
+ statusCode;
4117
+ details;
4118
+ constructor(message, options) {
4119
+ super(message, { cause: options.cause });
4120
+ this.name = "GtfsError";
4121
+ this.code = options.code;
4122
+ this.category = options.category;
4123
+ this.isOperational = options.isOperational ?? true;
4124
+ this.statusCode = options.statusCode;
4125
+ this.details = options.details;
4126
+ }
4127
+ };
4128
+ function isGtfsError(error) {
4129
+ if (!error || typeof error !== "object") {
4130
+ return false;
4131
+ }
4132
+ const candidate = error;
4133
+ return candidate.name === "GtfsError" && typeof candidate.message === "string" && typeof candidate.code === "string" && typeof candidate.category === "string" && typeof candidate.isOperational === "boolean";
4134
+ }
4135
+ function isGtfsValidationError(error) {
4136
+ return isGtfsError(error) && error.category === "validation" /* VALIDATION */;
4137
+ }
4138
+ function toGtfsError(error, fallback) {
4139
+ if (isGtfsError(error)) {
4140
+ return error;
4141
+ }
4142
+ return new GtfsError(fallback.message, {
4143
+ ...fallback,
4144
+ cause: error
4145
+ });
4146
+ }
4147
+ function createImportReport() {
4148
+ return {
4149
+ errors: [],
4150
+ warnings: [],
4151
+ errorCountsByCode: {},
4152
+ warningCountsByCode: {}
4153
+ };
4154
+ }
4155
+ function addImportError(report, error) {
4156
+ report.errors.push(error);
4157
+ report.errorCountsByCode[error.code] = (report.errorCountsByCode[error.code] ?? 0) + 1;
4158
+ }
4159
+ function addImportWarning(report, warning) {
4160
+ report.warnings.push(warning);
4161
+ report.warningCountsByCode[warning.code] = (report.warningCountsByCode[warning.code] ?? 0) + 1;
4162
+ }
4163
+ function formatGtfsError(error, options = { verbosity: "developer" }) {
4164
+ if (!isGtfsError(error)) {
4165
+ const message = error instanceof Error ? error.message : String(error);
4166
+ return options.verbosity === "user" ? message : `UNKNOWN_ERROR: ${message}`;
4167
+ }
4168
+ if (options.verbosity === "user") {
4169
+ return error.message;
4170
+ }
4171
+ return [
4172
+ `${error.code}: ${error.message}`,
4173
+ `category=${error.category}`,
4174
+ error.statusCode !== void 0 ? `statusCode=${error.statusCode}` : null,
4175
+ error.details ? `details=${JSON.stringify(error.details)}` : null
4176
+ ].filter(Boolean).join(" | ");
4177
+ }
4178
+
4081
4179
  // src/lib/db.ts
4082
4180
  var dbs = {};
4083
4181
  function setupDb(sqlitePath) {
@@ -4107,22 +4205,39 @@ function openDb(config = null) {
4107
4205
  return dbs[filename];
4108
4206
  }
4109
4207
  if (Object.keys(dbs).length > 1) {
4110
- throw new Error(
4111
- "Multiple databases open, please specify which one to use."
4208
+ throw new GtfsError(
4209
+ "Multiple databases open, please specify which one to use.",
4210
+ {
4211
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4212
+ category: "database" /* DATABASE */,
4213
+ details: { openDatabaseCount: Object.keys(dbs).length }
4214
+ }
4112
4215
  );
4113
4216
  }
4114
- throw new Error("Unable to find database connection.");
4217
+ throw new GtfsError("Unable to find database connection.", {
4218
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4219
+ category: "database" /* DATABASE */
4220
+ });
4115
4221
  }
4116
4222
  function closeDb(db = null) {
4117
4223
  if (Object.keys(dbs).length === 0) {
4118
- throw new Error(
4119
- "No database connection. Call `openDb(config)` before using any methods."
4224
+ throw new GtfsError(
4225
+ "No database connection. Call `openDb(config)` before using any methods.",
4226
+ {
4227
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4228
+ category: "database" /* DATABASE */
4229
+ }
4120
4230
  );
4121
4231
  }
4122
4232
  if (!db) {
4123
4233
  if (Object.keys(dbs).length > 1) {
4124
- throw new Error(
4125
- "Multiple database connections. Pass the db you want to close as a parameter to `closeDb`."
4234
+ throw new GtfsError(
4235
+ "Multiple database connections. Pass the db you want to close as a parameter to `closeDb`.",
4236
+ {
4237
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4238
+ category: "database" /* DATABASE */,
4239
+ details: { openDatabaseCount: Object.keys(dbs).length }
4240
+ }
4126
4241
  );
4127
4242
  }
4128
4243
  db = dbs[Object.keys(dbs)[0]];
@@ -4132,14 +4247,23 @@ function closeDb(db = null) {
4132
4247
  }
4133
4248
  function deleteDb(db = null) {
4134
4249
  if (Object.keys(dbs).length === 0) {
4135
- throw new Error(
4136
- "No database connection. Call `openDb(config)` before using any methods."
4250
+ throw new GtfsError(
4251
+ "No database connection. Call `openDb(config)` before using any methods.",
4252
+ {
4253
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4254
+ category: "database" /* DATABASE */
4255
+ }
4137
4256
  );
4138
4257
  }
4139
4258
  if (!db) {
4140
4259
  if (Object.keys(dbs).length > 1) {
4141
- throw new Error(
4142
- "Multiple database connections. Pass the db you want to delete as a parameter to `deleteDb`."
4260
+ throw new GtfsError(
4261
+ "Multiple database connections. Pass the db you want to delete as a parameter to `deleteDb`.",
4262
+ {
4263
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4264
+ category: "database" /* DATABASE */,
4265
+ details: { openDatabaseCount: Object.keys(dbs).length }
4266
+ }
4143
4267
  );
4144
4268
  }
4145
4269
  db = dbs[Object.keys(dbs)[0]];
@@ -4278,12 +4402,21 @@ import sqlString from "sqlstring-sqlite";
4278
4402
  import Long from "long";
4279
4403
  function validateConfigForImport(config) {
4280
4404
  if (!config.agencies || config.agencies.length === 0) {
4281
- throw new Error("No `agencies` specified in config");
4405
+ throw new GtfsError("No `agencies` specified in config", {
4406
+ code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
4407
+ category: "config" /* CONFIG */,
4408
+ details: { field: "agencies" }
4409
+ });
4282
4410
  }
4283
4411
  for (const [index, agency2] of config.agencies.entries()) {
4284
4412
  if (!agency2.path && !agency2.url) {
4285
- throw new Error(
4286
- `No Agency \`url\` or \`path\` specified in config for agency index ${index}.`
4413
+ throw new GtfsError(
4414
+ `No Agency \`url\` or \`path\` specified in config for agency index ${index}.`,
4415
+ {
4416
+ code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
4417
+ category: "config" /* CONFIG */,
4418
+ details: { agencyIndex: index }
4419
+ }
4287
4420
  );
4288
4421
  }
4289
4422
  }
@@ -4354,7 +4487,11 @@ function formatWhereClauseBoundingBox(latitudeDegree, longitudeDegree, boundingB
4354
4487
  const lat = Number(latitudeDegree);
4355
4488
  const lon = Number(longitudeDegree);
4356
4489
  if (isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
4357
- throw new Error("Invalid latitude or longitude values");
4490
+ throw new GtfsError("Invalid latitude or longitude values", {
4491
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
4492
+ category: "query" /* QUERY */,
4493
+ details: { latitudeDegree, longitudeDegree, boundingBoxSideMeters }
4494
+ });
4358
4495
  }
4359
4496
  const latitudeRadian = degree2radian(lat);
4360
4497
  const radiusFromLatitude = Math.cos(latitudeRadian) * EARTH_RADIUS_METERS;
@@ -4410,14 +4547,22 @@ function getDayOfWeekFromDate(date) {
4410
4547
  "saturday"
4411
4548
  ];
4412
4549
  if (!Number.isInteger(date) || date.toString().length !== 8) {
4413
- throw new Error("Date must be in YYYYMMDD format");
4550
+ throw new GtfsError("Date must be in YYYYMMDD format", {
4551
+ code: "GTFS_INVALID_DATE" /* GTFS_INVALID_DATE */,
4552
+ category: "validation" /* VALIDATION */,
4553
+ details: { value: date }
4554
+ });
4414
4555
  }
4415
4556
  const year = Math.floor(date / 1e4);
4416
4557
  const month = Math.floor(date % 1e4 / 100);
4417
4558
  const day = date % 100;
4418
4559
  const dateObj = new Date(year, month - 1, day);
4419
4560
  if (dateObj.toString() === "Invalid Date") {
4420
- throw new Error("Invalid date");
4561
+ throw new GtfsError("Invalid date", {
4562
+ code: "GTFS_INVALID_DATE" /* GTFS_INVALID_DATE */,
4563
+ category: "validation" /* VALIDATION */,
4564
+ details: { value: date }
4565
+ });
4421
4566
  }
4422
4567
  return DAYS_OF_WEEK[dateObj.getDay()];
4423
4568
  }
@@ -4504,7 +4649,16 @@ async function fetchGtfsRealtimeData(type, task) {
4504
4649
  signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4505
4650
  });
4506
4651
  if (response.status !== 200) {
4507
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4652
+ throw new GtfsError(`HTTP ${response.status}: ${response.statusText}`, {
4653
+ code: "GTFS_DOWNLOAD_HTTP" /* GTFS_DOWNLOAD_HTTP */,
4654
+ category: "download" /* DOWNLOAD */,
4655
+ statusCode: response.status,
4656
+ details: {
4657
+ url: urlConfig.url,
4658
+ status: response.status,
4659
+ statusText: response.statusText
4660
+ }
4661
+ });
4508
4662
  }
4509
4663
  const buffer = await response.arrayBuffer();
4510
4664
  const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
@@ -4521,17 +4675,27 @@ async function fetchGtfsRealtimeData(type, task) {
4521
4675
  });
4522
4676
  return feedMessage;
4523
4677
  } catch (error) {
4524
- const errorMessage = error instanceof Error ? error.message : String(error);
4678
+ const gtfsError = toGtfsError(error, {
4679
+ message: error instanceof Error ? error.message : String(error),
4680
+ code: "GTFS_DOWNLOAD_FAILED" /* GTFS_DOWNLOAD_FAILED */,
4681
+ category: "download" /* DOWNLOAD */,
4682
+ details: { type, url: urlConfig.url }
4683
+ });
4525
4684
  if (attempt === MAX_RETRIES) {
4526
4685
  if (task.ignoreErrors) {
4527
4686
  task.logError(
4528
- `Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
4687
+ `Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${gtfsError.message}`
4529
4688
  );
4689
+ if (task.report) {
4690
+ addImportError(task.report, gtfsError);
4691
+ }
4530
4692
  return null;
4531
4693
  }
4532
- throw error;
4694
+ throw gtfsError;
4533
4695
  }
4534
- task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
4696
+ task.logWarning(
4697
+ `Attempt ${attempt} failed for ${type}: ${gtfsError.message}`
4698
+ );
4535
4699
  await new Promise(
4536
4700
  (resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
4537
4701
  );
@@ -4738,8 +4902,9 @@ async function updateGtfsRealtime(initialConfig) {
4738
4902
  );
4739
4903
  removeExpiredRealtimeData(config);
4740
4904
  await mapSeries(config.agencies, async (agency2) => {
4905
+ let task;
4741
4906
  try {
4742
- const task = {
4907
+ task = {
4743
4908
  realtimeAlerts: agency2.realtimeAlerts,
4744
4909
  realtimeTripUpdates: agency2.realtimeTripUpdates,
4745
4910
  realtimeVehiclePositions: agency2.realtimeVehiclePositions,
@@ -4755,11 +4920,19 @@ async function updateGtfsRealtime(initialConfig) {
4755
4920
  };
4756
4921
  await updateGtfsRealtimeData(task);
4757
4922
  } catch (error) {
4758
- const errorMessage = error instanceof Error ? error.message : String(error);
4923
+ const gtfsError = toGtfsError(error, {
4924
+ message: error instanceof Error ? error.message : String(error),
4925
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4926
+ category: "database" /* DATABASE */,
4927
+ details: { sqlitePath: task?.sqlitePath ?? config.sqlitePath }
4928
+ });
4759
4929
  if (config.ignoreErrors) {
4760
- logError(config)(errorMessage);
4930
+ logError(config)(formatGtfsError(gtfsError));
4931
+ if (task?.report) {
4932
+ addImportError(task.report, gtfsError);
4933
+ }
4761
4934
  } else {
4762
- throw error;
4935
+ throw gtfsError;
4763
4936
  }
4764
4937
  }
4765
4938
  });
@@ -4773,42 +4946,88 @@ async function updateGtfsRealtime(initialConfig) {
4773
4946
  );
4774
4947
  } catch (error) {
4775
4948
  if (error.code === "SQLITE_CANTOPEN") {
4776
- logError(config)(
4777
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4949
+ const dbOpenError = new GtfsError(
4950
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
4951
+ {
4952
+ code: "DB_OPEN_FAILED" /* DB_OPEN_FAILED */,
4953
+ category: "database" /* DATABASE */,
4954
+ details: {
4955
+ sqlitePath: config.sqlitePath,
4956
+ dbCode: error.code
4957
+ },
4958
+ cause: error
4959
+ }
4778
4960
  );
4961
+ logError(config)(dbOpenError.message);
4962
+ throw dbOpenError;
4779
4963
  }
4780
- throw error;
4964
+ throw toGtfsError(error, {
4965
+ message: error instanceof Error ? error.message : String(error),
4966
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
4967
+ category: "database" /* DATABASE */
4968
+ });
4781
4969
  }
4782
4970
  }
4783
4971
 
4784
4972
  // src/lib/import-gtfs.ts
4973
+ function reportTaskError(task, error) {
4974
+ if (task.report) {
4975
+ addImportError(task.report, error);
4976
+ }
4977
+ }
4785
4978
  var getTextFiles = async (folderPath) => {
4786
4979
  const files = await readdir(folderPath);
4787
4980
  return files.filter((filename) => filename.slice(-3) === "txt");
4788
4981
  };
4789
4982
  var downloadGtfsFiles = async (task) => {
4790
4983
  if (!task.url) {
4791
- throw new Error("No `url` specified in config");
4984
+ throw new GtfsError("No `url` specified in config", {
4985
+ code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
4986
+ category: "config" /* CONFIG */
4987
+ });
4792
4988
  }
4793
4989
  task.log(`Downloading GTFS from ${task.url}`);
4794
4990
  task.path = `${task.downloadDir}/gtfs.zip`;
4795
- const response = await fetch(task.url, {
4796
- method: "GET",
4797
- headers: task.headers || {},
4798
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4799
- });
4800
- if (response.status !== 200) {
4801
- throw new Error(
4802
- `Unable to download GTFS from ${task.url}. Got status ${response.status}.`
4803
- );
4991
+ try {
4992
+ const response = await fetch(task.url, {
4993
+ method: "GET",
4994
+ headers: task.headers || {},
4995
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4996
+ });
4997
+ if (response.status !== 200) {
4998
+ throw new GtfsError(
4999
+ `Unable to download GTFS from ${task.url}. Got status ${response.status}.`,
5000
+ {
5001
+ code: "GTFS_DOWNLOAD_HTTP" /* GTFS_DOWNLOAD_HTTP */,
5002
+ category: "download" /* DOWNLOAD */,
5003
+ statusCode: response.status,
5004
+ details: {
5005
+ url: task.url,
5006
+ status: response.status,
5007
+ statusText: response.statusText
5008
+ }
5009
+ }
5010
+ );
5011
+ }
5012
+ const buffer = await response.arrayBuffer();
5013
+ await writeFile(task.path, Buffer.from(buffer));
5014
+ task.log("Download successful");
5015
+ } catch (error) {
5016
+ throw toGtfsError(error, {
5017
+ message: `Unable to download GTFS from ${task.url}.`,
5018
+ code: "GTFS_DOWNLOAD_FAILED" /* GTFS_DOWNLOAD_FAILED */,
5019
+ category: "download" /* DOWNLOAD */,
5020
+ details: { url: task.url }
5021
+ });
4804
5022
  }
4805
- const buffer = await response.arrayBuffer();
4806
- await writeFile(task.path, Buffer.from(buffer));
4807
- task.log("Download successful");
4808
5023
  };
4809
5024
  var extractGtfsFiles = async (task) => {
4810
5025
  if (!task.path) {
4811
- throw new Error("No `path` specified in config");
5026
+ throw new GtfsError("No `path` specified in config", {
5027
+ code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
5028
+ category: "config" /* CONFIG */,
5029
+ details: { field: "path" }
5030
+ });
4812
5031
  }
4813
5032
  const gtfsPath = untildify(task.path);
4814
5033
  task.log(`Importing static GTFS from ${task.path}\r`);
@@ -4820,19 +5039,34 @@ var extractGtfsFiles = async (task) => {
4820
5039
  const files = await readdir(task.downloadDir);
4821
5040
  const folders = files.filter((filename) => !["__MACOSX"].includes(filename)).map((filename) => path2.join(task.downloadDir, filename)).filter((source) => lstatSync(source).isDirectory());
4822
5041
  if (folders.length > 1) {
4823
- throw new Error(
4824
- `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.`
5042
+ throw new GtfsError(
5043
+ `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.`,
5044
+ {
5045
+ code: "GTFS_ZIP_INVALID" /* GTFS_ZIP_INVALID */,
5046
+ category: "zip" /* ZIP */,
5047
+ details: { path: task.path, folderCount: folders.length }
5048
+ }
4825
5049
  );
4826
5050
  } else if (folders.length === 0) {
4827
- throw new Error(
4828
- `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.`
5051
+ throw new GtfsError(
5052
+ `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.`,
5053
+ {
5054
+ code: "GTFS_ZIP_INVALID" /* GTFS_ZIP_INVALID */,
5055
+ category: "zip" /* ZIP */,
5056
+ details: { path: task.path }
5057
+ }
4829
5058
  );
4830
5059
  }
4831
5060
  const subfolderName = folders[0];
4832
5061
  const directoryTextFiles = await getTextFiles(subfolderName);
4833
5062
  if (directoryTextFiles.length === 0) {
4834
- throw new Error(
4835
- `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.`
5063
+ throw new GtfsError(
5064
+ `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.`,
5065
+ {
5066
+ code: "GTFS_ZIP_INVALID" /* GTFS_ZIP_INVALID */,
5067
+ category: "zip" /* ZIP */,
5068
+ details: { path: task.path, subfolderName }
5069
+ }
4836
5070
  );
4837
5071
  }
4838
5072
  await Promise.all(
@@ -4845,15 +5079,27 @@ var extractGtfsFiles = async (task) => {
4845
5079
  );
4846
5080
  }
4847
5081
  } catch (error) {
4848
- task.logError(error);
4849
- throw new Error(`Unable to unzip file ${task.path}`);
5082
+ const wrappedError = toGtfsError(error, {
5083
+ message: `Unable to unzip file ${task.path}`,
5084
+ code: "GTFS_ZIP_INVALID" /* GTFS_ZIP_INVALID */,
5085
+ category: "zip" /* ZIP */,
5086
+ details: { path: task.path }
5087
+ });
5088
+ task.logError(formatGtfsError(wrappedError));
5089
+ throw wrappedError;
4850
5090
  }
4851
5091
  } else {
4852
5092
  try {
4853
5093
  await cp(gtfsPath, task.downloadDir, { recursive: true });
4854
- } catch {
4855
- throw new Error(
4856
- `Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`
5094
+ } catch (error) {
5095
+ throw new GtfsError(
5096
+ `Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`,
5097
+ {
5098
+ code: "GTFS_DOWNLOAD_FAILED" /* GTFS_DOWNLOAD_FAILED */,
5099
+ category: "download" /* DOWNLOAD */,
5100
+ details: { path: gtfsPath },
5101
+ cause: error
5102
+ }
4857
5103
  );
4858
5104
  }
4859
5105
  }
@@ -4948,8 +5194,17 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4948
5194
  if (value === "" || value === void 0 || value === null) {
4949
5195
  formattedLine[name] = null;
4950
5196
  if (required) {
4951
- throw new Error(
4952
- `Missing required value in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
5197
+ throw new GtfsError(
5198
+ `Missing required value in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`,
5199
+ {
5200
+ code: "GTFS_REQUIRED_FIELD_MISSING" /* GTFS_REQUIRED_FIELD_MISSING */,
5201
+ category: "validation" /* VALIDATION */,
5202
+ details: {
5203
+ file: `${filenameBase}.${filenameExtension}`,
5204
+ line: lineNumber,
5205
+ column: name
5206
+ }
5207
+ }
4953
5208
  );
4954
5209
  }
4955
5210
  continue;
@@ -4957,8 +5212,18 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4957
5212
  if (type === "date") {
4958
5213
  value = value?.toString().replace(/-/g, "");
4959
5214
  if (value.length !== 8) {
4960
- throw new Error(
4961
- `Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
5215
+ throw new GtfsError(
5216
+ `Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`,
5217
+ {
5218
+ code: "GTFS_INVALID_DATE" /* GTFS_INVALID_DATE */,
5219
+ category: "validation" /* VALIDATION */,
5220
+ details: {
5221
+ file: `${filenameBase}.${filenameExtension}`,
5222
+ line: lineNumber,
5223
+ column: name,
5224
+ value
5225
+ }
5226
+ }
4962
5227
  );
4963
5228
  }
4964
5229
  } else if (type === "time") {
@@ -5030,11 +5295,32 @@ var importGtfsFiles = async (db, task) => {
5030
5295
  task.logWarning(
5031
5296
  `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`
5032
5297
  );
5298
+ if (task.report) {
5299
+ addImportWarning(task.report, {
5300
+ code: "GTFS_DUPLICATE_PRIMARY_KEY" /* GTFS_DUPLICATE_PRIMARY_KEY */,
5301
+ message: `Duplicate values for primary key found in ${filename}.`,
5302
+ details: {
5303
+ file: filename,
5304
+ line: Number(rowNumber) + 1,
5305
+ columns: primaryColumns.map((column) => column.name)
5306
+ }
5307
+ });
5308
+ }
5033
5309
  }
5034
5310
  task.logWarning(
5035
5311
  `Check ${filename} for invalid data on line ${rowNumber + 1}.`
5036
5312
  );
5037
- throw error;
5313
+ throw toGtfsError(error, {
5314
+ message: error instanceof Error ? error.message : String(error),
5315
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
5316
+ category: "database" /* DATABASE */,
5317
+ details: {
5318
+ file: filename,
5319
+ line: Number(rowNumber) + 1,
5320
+ sqlitePath: task.sqlitePath,
5321
+ dbCode: error.code
5322
+ }
5323
+ });
5038
5324
  }
5039
5325
  }
5040
5326
  });
@@ -5063,12 +5349,20 @@ var importGtfsFiles = async (db, task) => {
5063
5349
  }
5064
5350
  }
5065
5351
  } catch (error) {
5352
+ const gtfsError = toGtfsError(error, {
5353
+ message: error instanceof Error ? error.message : String(error),
5354
+ code: "GTFS_CSV_PARSE_FAILED" /* GTFS_CSV_PARSE_FAILED */,
5355
+ category: "parse" /* PARSE */,
5356
+ details: { file: filename }
5357
+ });
5066
5358
  if (task.ignoreErrors) {
5067
- const errorMessage = error instanceof Error ? error.message : String(error);
5068
- task.logError(`Error processing ${filename}: ${errorMessage}`);
5359
+ reportTaskError(task, gtfsError);
5360
+ task.logError(
5361
+ `Error processing ${filename}: ${gtfsError.message}`
5362
+ );
5069
5363
  resolve();
5070
5364
  } else {
5071
- reject(error);
5365
+ reject(gtfsError);
5072
5366
  }
5073
5367
  }
5074
5368
  });
@@ -5078,15 +5372,21 @@ var importGtfsFiles = async (db, task) => {
5078
5372
  try {
5079
5373
  insertLines(lines);
5080
5374
  } catch (error) {
5375
+ const gtfsError = toGtfsError(error, {
5376
+ message: error instanceof Error ? error.message : String(error),
5377
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
5378
+ category: "database" /* DATABASE */,
5379
+ details: { file: filename, sqlitePath: task.sqlitePath }
5380
+ });
5081
5381
  if (task.ignoreErrors) {
5082
- const errorMessage = error instanceof Error ? error.message : String(error);
5083
5382
  task.logError(
5084
- `Error inserting data for ${filename}: ${errorMessage}`
5383
+ `Error inserting data for ${filename}: ${gtfsError.message}`
5085
5384
  );
5385
+ reportTaskError(task, gtfsError);
5086
5386
  resolve();
5087
5387
  return;
5088
5388
  } else {
5089
- reject(error);
5389
+ reject(gtfsError);
5090
5390
  return;
5091
5391
  }
5092
5392
  }
@@ -5097,22 +5397,38 @@ var importGtfsFiles = async (db, task) => {
5097
5397
  );
5098
5398
  resolve();
5099
5399
  } catch (error) {
5400
+ const gtfsError = toGtfsError(error, {
5401
+ message: error instanceof Error ? error.message : String(error),
5402
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
5403
+ category: "database" /* DATABASE */,
5404
+ details: { file: filename, sqlitePath: task.sqlitePath }
5405
+ });
5100
5406
  if (task.ignoreErrors) {
5101
- const errorMessage = error instanceof Error ? error.message : String(error);
5102
- task.logError(`Error finalizing ${filename}: ${errorMessage}`);
5407
+ task.logError(
5408
+ `Error finalizing ${filename}: ${gtfsError.message}`
5409
+ );
5410
+ reportTaskError(task, gtfsError);
5103
5411
  resolve();
5104
5412
  } else {
5105
- reject(error);
5413
+ reject(gtfsError);
5106
5414
  }
5107
5415
  }
5108
5416
  });
5109
5417
  parser.on("error", (error) => {
5418
+ const gtfsError = toGtfsError(error, {
5419
+ message: error instanceof Error ? error.message : String(error),
5420
+ code: "GTFS_CSV_PARSE_FAILED" /* GTFS_CSV_PARSE_FAILED */,
5421
+ category: "parse" /* PARSE */,
5422
+ details: { file: filename }
5423
+ });
5110
5424
  if (task.ignoreErrors) {
5111
- const errorMessage = error instanceof Error ? error.message : String(error);
5112
- task.logError(`Parser error for ${filename}: ${errorMessage}`);
5425
+ task.logError(
5426
+ `Parser error for ${filename}: ${gtfsError.message}`
5427
+ );
5428
+ reportTaskError(task, gtfsError);
5113
5429
  resolve();
5114
5430
  } else {
5115
- reject(error);
5431
+ reject(gtfsError);
5116
5432
  }
5117
5433
  });
5118
5434
  createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
@@ -5121,10 +5437,24 @@ var importGtfsFiles = async (db, task) => {
5121
5437
  if (isValidJSON(data) === false) {
5122
5438
  if (task.ignoreErrors) {
5123
5439
  task.logError(`Invalid JSON in ${filename}`);
5440
+ reportTaskError(
5441
+ task,
5442
+ new GtfsError(`Invalid JSON in ${filename}`, {
5443
+ code: "GTFS_JSON_INVALID" /* GTFS_JSON_INVALID */,
5444
+ category: "parse" /* PARSE */,
5445
+ details: { file: filename }
5446
+ })
5447
+ );
5124
5448
  resolve();
5125
5449
  return;
5126
5450
  } else {
5127
- reject(new Error(`Invalid JSON in ${filename}`));
5451
+ reject(
5452
+ new GtfsError(`Invalid JSON in ${filename}`, {
5453
+ code: "GTFS_JSON_INVALID" /* GTFS_JSON_INVALID */,
5454
+ category: "parse" /* PARSE */,
5455
+ details: { file: filename }
5456
+ })
5457
+ );
5128
5458
  return;
5129
5459
  }
5130
5460
  }
@@ -5142,23 +5472,37 @@ var importGtfsFiles = async (db, task) => {
5142
5472
  );
5143
5473
  resolve();
5144
5474
  } catch (error) {
5475
+ const gtfsError = toGtfsError(error, {
5476
+ message: error instanceof Error ? error.message : String(error),
5477
+ code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
5478
+ category: "database" /* DATABASE */,
5479
+ details: { file: filename, sqlitePath: task.sqlitePath }
5480
+ });
5145
5481
  if (task.ignoreErrors) {
5146
- const errorMessage = error instanceof Error ? error.message : String(error);
5147
5482
  task.logError(
5148
- `Error inserting data for ${filename}: ${errorMessage}`
5483
+ `Error inserting data for ${filename}: ${gtfsError.message}`
5149
5484
  );
5485
+ reportTaskError(task, gtfsError);
5150
5486
  resolve();
5151
5487
  } else {
5152
- reject(error);
5488
+ reject(gtfsError);
5153
5489
  }
5154
5490
  }
5155
5491
  }).catch((error) => {
5492
+ const gtfsError = toGtfsError(error, {
5493
+ message: error instanceof Error ? error.message : String(error),
5494
+ code: "GTFS_CSV_PARSE_FAILED" /* GTFS_CSV_PARSE_FAILED */,
5495
+ category: "parse" /* PARSE */,
5496
+ details: { file: filename }
5497
+ });
5156
5498
  if (task.ignoreErrors) {
5157
- const errorMessage = error instanceof Error ? error.message : String(error);
5158
- task.logError(`Error reading ${filename}: ${errorMessage}`);
5499
+ task.logError(
5500
+ `Error reading ${filename}: ${gtfsError.message}`
5501
+ );
5502
+ reportTaskError(task, gtfsError);
5159
5503
  resolve();
5160
5504
  } else {
5161
- reject(error);
5505
+ reject(gtfsError);
5162
5506
  }
5163
5507
  });
5164
5508
  } else {
@@ -5169,7 +5513,17 @@ var importGtfsFiles = async (db, task) => {
5169
5513
  resolve();
5170
5514
  } else {
5171
5515
  reject(
5172
- new Error(`Unsupported file type: ${model.filenameExtension}`)
5516
+ new GtfsError(
5517
+ `Unsupported file type: ${model.filenameExtension}`,
5518
+ {
5519
+ code: "GTFS_UNSUPPORTED_FILE_TYPE" /* GTFS_UNSUPPORTED_FILE_TYPE */,
5520
+ category: "parse" /* PARSE */,
5521
+ details: {
5522
+ file: filename,
5523
+ extension: model.filenameExtension
5524
+ }
5525
+ }
5526
+ )
5173
5527
  );
5174
5528
  }
5175
5529
  }
@@ -5181,6 +5535,7 @@ async function importGtfs(initialConfig) {
5181
5535
  const startTime = process.hrtime.bigint();
5182
5536
  const config = setDefaultConfig(initialConfig);
5183
5537
  validateConfigForImport(config);
5538
+ const report = config.includeImportReport ? createImportReport() : void 0;
5184
5539
  try {
5185
5540
  const db = openDb(config);
5186
5541
  const agencyCount = config.agencies.length;
@@ -5208,7 +5563,8 @@ async function importGtfs(initialConfig) {
5208
5563
  currentTimestamp: Math.floor(Date.now() / 1e3),
5209
5564
  log: log(config),
5210
5565
  logWarning: logWarning(config),
5211
- logError: logError(config)
5566
+ logError: logError(config),
5567
+ report
5212
5568
  };
5213
5569
  if ("url" in agency2) {
5214
5570
  Object.assign(task, { url: agency2.url });
@@ -5223,11 +5579,18 @@ async function importGtfs(initialConfig) {
5223
5579
  await updateGtfsRealtimeData(task);
5224
5580
  await rm2(tempPath, { recursive: true });
5225
5581
  } catch (error) {
5582
+ const wrappedError = toGtfsError(error, {
5583
+ message: error instanceof Error ? error.message : String(error),
5584
+ code: "GTFS_CSV_PARSE_FAILED" /* GTFS_CSV_PARSE_FAILED */,
5585
+ category: "parse" /* PARSE */
5586
+ });
5226
5587
  if (config.ignoreErrors) {
5227
- const errorMessage = error instanceof Error ? error.message : String(error);
5228
- logError(config)(errorMessage);
5588
+ logError(config)(formatGtfsError(wrappedError));
5589
+ if (report) {
5590
+ addImportError(report, wrappedError);
5591
+ }
5229
5592
  } else {
5230
- throw error;
5593
+ throw wrappedError;
5231
5594
  }
5232
5595
  }
5233
5596
  });
@@ -5241,11 +5604,29 @@ async function importGtfs(initialConfig) {
5241
5604
  );
5242
5605
  } catch (error) {
5243
5606
  if (error.code === "SQLITE_CANTOPEN") {
5244
- logError(config)(
5245
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
5607
+ const dbOpenError = new GtfsError(
5608
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
5609
+ {
5610
+ code: "DB_OPEN_FAILED" /* DB_OPEN_FAILED */,
5611
+ category: "database" /* DATABASE */,
5612
+ details: {
5613
+ sqlitePath: config.sqlitePath,
5614
+ dbCode: error.code
5615
+ },
5616
+ cause: error
5617
+ }
5246
5618
  );
5619
+ logError(config)(dbOpenError.message);
5620
+ throw dbOpenError;
5247
5621
  }
5248
- throw error;
5622
+ throw toGtfsError(error, {
5623
+ message: error instanceof Error ? error.message : String(error),
5624
+ code: "GTFS_CSV_PARSE_FAILED" /* GTFS_CSV_PARSE_FAILED */,
5625
+ category: "parse" /* PARSE */
5626
+ });
5627
+ }
5628
+ if (report) {
5629
+ return report;
5249
5630
  }
5250
5631
  }
5251
5632
 
@@ -5468,7 +5849,11 @@ function getCalendars(query = {}, fields = [], orderBy2 = [], options = {}) {
5468
5849
  function getServiceIdsByDate(date, options = {}) {
5469
5850
  const db = options.db ?? openDb();
5470
5851
  if (!date) {
5471
- throw new Error("`date` is a required query parameter");
5852
+ throw new GtfsError("`date` is a required query parameter", {
5853
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
5854
+ category: "query" /* QUERY */,
5855
+ details: { field: "date" }
5856
+ });
5472
5857
  }
5473
5858
  const dayOfWeek = getDayOfWeekFromDate(date);
5474
5859
  const results = db.prepare(
@@ -5937,7 +6322,11 @@ function getStoptimes(query = {}, fields = [], orderBy2 = [], options = {}) {
5937
6322
  );
5938
6323
  if (query.date) {
5939
6324
  if (typeof query.date !== "number") {
5940
- throw new Error("`date` must be a number in yyyymmdd format");
6325
+ throw new GtfsError("`date` must be a number in yyyymmdd format", {
6326
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
6327
+ category: "query" /* QUERY */,
6328
+ details: { field: "date", value: query.date }
6329
+ });
5941
6330
  }
5942
6331
  const serviceIds = getServiceIdsByDate(query.date, options);
5943
6332
  const tripSubquery = `SELECT DISTINCT trip_id FROM trips WHERE service_id IN (${serviceIds.map((id) => sqlString4.escape(id)).join(",")})`;
@@ -5945,7 +6334,11 @@ function getStoptimes(query = {}, fields = [], orderBy2 = [], options = {}) {
5945
6334
  }
5946
6335
  if (query.start_time) {
5947
6336
  if (typeof query.start_time !== "string") {
5948
- throw new Error("`start_time` must be a string in HH:mm:ss format");
6337
+ throw new GtfsError("`start_time` must be a string in HH:mm:ss format", {
6338
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
6339
+ category: "query" /* QUERY */,
6340
+ details: { field: "start_time", value: query.start_time }
6341
+ });
5949
6342
  }
5950
6343
  whereClauses.push(
5951
6344
  `arrival_timestamp >= ${calculateSecondsFromMidnight(query.start_time)}`
@@ -5953,7 +6346,11 @@ function getStoptimes(query = {}, fields = [], orderBy2 = [], options = {}) {
5953
6346
  }
5954
6347
  if (query.end_time) {
5955
6348
  if (typeof query.end_time !== "string") {
5956
- throw new Error("`end_time` must be a string in HH:mm:ss format");
6349
+ throw new GtfsError("`end_time` must be a string in HH:mm:ss format", {
6350
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
6351
+ category: "query" /* QUERY */,
6352
+ details: { field: "end_time", value: query.end_time }
6353
+ });
5957
6354
  }
5958
6355
  whereClauses.push(
5959
6356
  `departure_timestamp <= ${calculateSecondsFromMidnight(query.end_time)}`
@@ -6019,7 +6416,11 @@ function getTrips(query = {}, fields = [], orderBy2 = [], options = {}) {
6019
6416
  );
6020
6417
  if (query.date) {
6021
6418
  if (typeof query.date !== "number") {
6022
- throw new Error("`date` must be a number in yyyymmdd format");
6419
+ throw new GtfsError("`date` must be a number in yyyymmdd format", {
6420
+ code: "GTFS_QUERY_INVALID" /* GTFS_QUERY_INVALID */,
6421
+ category: "query" /* QUERY */,
6422
+ details: { field: "date", value: query.date }
6423
+ });
6023
6424
  }
6024
6425
  const serviceIds = getServiceIdsByDate(query.date, options);
6025
6426
  whereClauses.push(
@@ -6299,10 +6700,15 @@ function getRunsPieces(query = {}, fields = [], orderBy2 = [], options = {}) {
6299
6700
  ).all();
6300
6701
  }
6301
6702
  export {
6703
+ GtfsError,
6704
+ GtfsErrorCategory,
6705
+ GtfsErrorCode,
6706
+ GtfsWarningCode,
6302
6707
  advancedQuery,
6303
6708
  closeDb,
6304
6709
  deleteDb,
6305
6710
  exportGtfs,
6711
+ formatGtfsError,
6306
6712
  generateFolderName,
6307
6713
  getAgencies2 as getAgencies,
6308
6714
  getAreas,
@@ -6363,6 +6769,8 @@ export {
6363
6769
  getTripsDatedVehicleJourneys,
6364
6770
  getVehiclePositions,
6365
6771
  importGtfs,
6772
+ isGtfsError,
6773
+ isGtfsValidationError,
6366
6774
  openDb,
6367
6775
  prepDirectory,
6368
6776
  untildify,