gtfs 4.18.3 → 4.18.5

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