ochre-sdk 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.mjs +144 -88
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,7 @@ const result = await fetchItem("<item-uuid>", {
33
33
  });
34
34
 
35
35
  if (result.error != null) {
36
- throw new Error(result.error);
36
+ throw new Error("Failed to fetch item", { cause: result.error });
37
37
  }
38
38
 
39
39
  console.log(result.item.identification.label.getText("eng"));
package/dist/index.mjs CHANGED
@@ -487,7 +487,45 @@ var MultilingualString = class MultilingualString {
487
487
  };
488
488
  //#endregion
489
489
  //#region src/utils.ts
490
+ const MAX_SCHEMA_VALIDATION_ISSUES = 3;
490
491
  const PSEUDO_UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
492
+ function getSchemaValidationLeafIssues(issues, leaves = []) {
493
+ for (const issue of issues) {
494
+ if (issue.issues != null && issue.issues.length > 0) {
495
+ getSchemaValidationLeafIssues(issue.issues, leaves);
496
+ continue;
497
+ }
498
+ leaves.push(issue);
499
+ }
500
+ return leaves;
501
+ }
502
+ function formatSchemaValidationIssue(issue) {
503
+ const path = v.getDotPath(issue);
504
+ return `${path != null && path.length > 0 ? path : "(root)"}: ${issue.message}`;
505
+ }
506
+ /**
507
+ * Formats Valibot validation issues for compact error messages.
508
+ * @param issues - The validation issues to format
509
+ * @internal
510
+ */
511
+ function formatSchemaValidationIssues(issues) {
512
+ const leafIssues = getSchemaValidationLeafIssues(issues);
513
+ const issuesToFormat = leafIssues.length > 0 ? leafIssues : issues;
514
+ const formattedIssues = [];
515
+ for (const issue of issuesToFormat.slice(0, MAX_SCHEMA_VALIDATION_ISSUES)) formattedIssues.push(formatSchemaValidationIssue(issue));
516
+ const hiddenIssueCount = issuesToFormat.length - formattedIssues.length;
517
+ if (hiddenIssueCount > 0) formattedIssues.push(`+${hiddenIssueCount.toLocaleString("en-US")} more`);
518
+ return `Schema validation failed: ${formattedIssues.join("; ")}`;
519
+ }
520
+ /**
521
+ * Creates an Error whose message includes compact schema-validation details.
522
+ * @param message - The base error message
523
+ * @param issues - The validation issues to include
524
+ * @internal
525
+ */
526
+ function createSchemaValidationError(message, issues) {
527
+ return new Error(`${message}. ${formatSchemaValidationIssues(issues)}`, { cause: issues });
528
+ }
491
529
  /**
492
530
  * Logs Valibot validation issues to the console with detailed formatting
493
531
  * @param issues - The validation issues to log
@@ -1409,6 +1447,12 @@ function parseStringContent(value, options = FALLBACK_PARSER_OPTIONS) {
1409
1447
  }
1410
1448
  //#endregion
1411
1449
  //#region src/parsers/index.ts
1450
+ function isXMLContextGroup(context) {
1451
+ return "context" in context;
1452
+ }
1453
+ function isXMLContextItem(context) {
1454
+ return "project" in context;
1455
+ }
1412
1456
  const SET_ITEM_CATEGORIES = ["tree", ...[
1413
1457
  "bibliography",
1414
1458
  "concept",
@@ -1521,27 +1565,33 @@ function emptyContextItem() {
1521
1565
  }
1522
1566
  function parseContext(rawContext) {
1523
1567
  const nodes = [];
1524
- for (const rawContextOuterItem of rawContext) for (const rawContextItem of rawContextOuterItem.context) {
1525
- const node = {
1526
- tree: rawContextItem.tree[0] == null ? emptyContextItem() : parseContextItem$1(rawContextItem.tree[0]),
1527
- project: parseContextItem$1(rawContextItem.project),
1528
- heading: []
1529
- };
1530
- const rawContextValues = rawContextItem;
1531
- for (const heading of rawContextValues.heading ?? []) node.heading.push(parseContextItem$1(heading));
1532
- for (const { raw, parsed } of CONTEXT_CATEGORY_MAPPINGS) {
1533
- const contextValues = rawContextValues[raw] ?? [];
1534
- for (const contextValue of contextValues) {
1535
- const parsedItems = node[parsed] ?? [];
1536
- parsedItems.push(parseContextItem$1(contextValue));
1537
- node[parsed] = parsedItems;
1568
+ let displayPath = "";
1569
+ for (const rawContextOuterItem of rawContext) {
1570
+ if (!isXMLContextGroup(rawContextOuterItem)) continue;
1571
+ displayPath = displayPath || rawContextOuterItem.displayPath;
1572
+ for (const rawContextItem of rawContextOuterItem.context) {
1573
+ if (!isXMLContextItem(rawContextItem)) continue;
1574
+ const node = {
1575
+ tree: rawContextItem.tree[0] == null ? emptyContextItem() : parseContextItem$1(rawContextItem.tree[0]),
1576
+ project: parseContextItem$1(rawContextItem.project),
1577
+ heading: []
1578
+ };
1579
+ const rawContextValues = rawContextItem;
1580
+ for (const heading of rawContextValues.heading ?? []) node.heading.push(parseContextItem$1(heading));
1581
+ for (const { raw, parsed } of CONTEXT_CATEGORY_MAPPINGS) {
1582
+ const contextValues = rawContextValues[raw] ?? [];
1583
+ for (const contextValue of contextValues) {
1584
+ const parsedItems = node[parsed] ?? [];
1585
+ parsedItems.push(parseContextItem$1(contextValue));
1586
+ node[parsed] = parsedItems;
1587
+ }
1538
1588
  }
1589
+ nodes.push(node);
1539
1590
  }
1540
- nodes.push(node);
1541
1591
  }
1542
1592
  return {
1543
1593
  nodes,
1544
- displayPath: rawContext[0]?.displayPath ?? ""
1594
+ displayPath
1545
1595
  };
1546
1596
  }
1547
1597
  function parseEventReference(rawReference, options) {
@@ -1692,16 +1742,19 @@ function normalizeSetItemCategories(containedItemCategory) {
1692
1742
  return uniqueCategories;
1693
1743
  }
1694
1744
  function normalizeTreeItemCategory(containedItemCategory) {
1695
- if (containedItemCategory != null && typeof containedItemCategory !== "string") throw new Error("Tree containedItemCategory must be a single category");
1696
- if (containedItemCategory === "tree") throw new Error("Tree containedItemCategory cannot be \"tree\"");
1745
+ if (containedItemCategory != null && typeof containedItemCategory !== "string") throw new Error("Tree containedItemCategory must be a single category", { cause: containedItemCategory });
1746
+ if (containedItemCategory === "tree") throw new Error("Tree containedItemCategory cannot be \"tree\"", { cause: containedItemCategory });
1697
1747
  return containedItemCategory;
1698
1748
  }
1699
1749
  function resolveTreeItemCategory(rawTree, containedItemCategory) {
1700
1750
  const inferredCategories = inferItemCategories(rawTree.items);
1701
- if (inferredCategories.length > 1) throw new Error(`Expected Tree items to contain one category, received ${inferredCategories.join(", ")}`);
1751
+ if (inferredCategories.length > 1) throw new Error(`Expected Tree items to contain one category, received ${inferredCategories.join(", ")}`, { cause: inferredCategories });
1702
1752
  const inferredCategory = inferredCategories[0] ?? null;
1703
- if (inferredCategory === "tree") throw new Error("Tree items cannot contain category \"tree\"");
1704
- if (containedItemCategory != null && inferredCategory != null && containedItemCategory !== inferredCategory) throw new Error(`Tree containedItemCategory "${containedItemCategory}" does not match XML items category "${inferredCategory}"`);
1753
+ if (inferredCategory === "tree") throw new Error("Tree items cannot contain category \"tree\"", { cause: inferredCategory });
1754
+ if (containedItemCategory != null && inferredCategory != null && containedItemCategory !== inferredCategory) throw new Error(`Tree containedItemCategory "${containedItemCategory}" does not match XML items category "${inferredCategory}"`, { cause: {
1755
+ containedItemCategory,
1756
+ inferredCategory
1757
+ } });
1705
1758
  return containedItemCategory ?? inferredCategory;
1706
1759
  }
1707
1760
  function parseImage(rawImage, options) {
@@ -1849,12 +1902,13 @@ function parseNotes(rawNotes, options) {
1849
1902
  }
1850
1903
  function parsePropertyDataType(dataType) {
1851
1904
  if (dataType == null || dataType === "") return "string";
1852
- for (const propertyDataType of PROPERTY_DATA_TYPES) if (dataType === propertyDataType) return propertyDataType;
1853
- throw new Error(`Invalid property value data type: ${dataType}`);
1905
+ const normalizedDataType = dataType.startsWith("xs:") ? dataType.slice(3) : dataType;
1906
+ for (const propertyDataType of PROPERTY_DATA_TYPES) if (normalizedDataType === propertyDataType) return propertyDataType;
1907
+ throw new Error(`Invalid property value data type: ${dataType}`, { cause: dataType });
1854
1908
  }
1855
1909
  function parsePropertyValueContent(value, options) {
1856
1910
  const dataType = parsePropertyDataType(value.dataType);
1857
- const rawLabel = value.content == null ? null : parseRequiredContentLike(value, options);
1911
+ const rawLabel = value.content != null ? parseRequiredContentLike(value, options) : value.payload != null && value.payload !== "" ? multilingualFromText(value.payload, options) : null;
1858
1912
  const displayText = rawLabel?.getText() ?? value.payload ?? value.slug ?? "";
1859
1913
  const contentText = value.rawValue ?? value.payload ?? displayText;
1860
1914
  const common = {
@@ -2580,7 +2634,7 @@ function resolveLanguages(requestedLanguages, metadataLanguages) {
2580
2634
  if (requestedLanguages.length === 0) return metadataLanguages;
2581
2635
  const unsupportedLanguages = [];
2582
2636
  for (const requestedLanguage of requestedLanguages) if (!metadataLanguages.some((metadataLanguage) => metadataLanguage.toLocaleLowerCase("en-US") === requestedLanguage.toLocaleLowerCase("en-US"))) unsupportedLanguages.push(requestedLanguage);
2583
- if (unsupportedLanguages.length > 0) throw new Error(`The following language(s) are not supported by the dataset: ${unsupportedLanguages.toSorted((a, b) => a.localeCompare(b, "en-US")).join(", ")}. Available languages: ${metadataLanguages.toSorted((a, b) => a.localeCompare(b, "en-US")).join(", ")}`);
2637
+ if (unsupportedLanguages.length > 0) throw new Error(`The following language(s) are not supported by the dataset: ${unsupportedLanguages.toSorted((a, b) => a.localeCompare(b, "en-US")).join(", ")}. Available languages: ${metadataLanguages.toSorted((a, b) => a.localeCompare(b, "en-US")).join(", ")}`, { cause: unsupportedLanguages });
2584
2638
  return requestedLanguages;
2585
2639
  }
2586
2640
  function resolveDefaultLanguage(rawOchre, languages) {
@@ -2590,7 +2644,7 @@ function resolveDefaultLanguage(rawOchre, languages) {
2590
2644
  }
2591
2645
  for (const language of languages) if (language === DEFAULT_LANGUAGES[0]) return language;
2592
2646
  const firstLanguage = languages[0];
2593
- if (firstLanguage == null) throw new Error("Default language not found");
2647
+ if (firstLanguage == null) throw new Error("Default language not found", { cause: languages });
2594
2648
  return firstLanguage;
2595
2649
  }
2596
2650
  function parseMetadataPublisher(rawPublisher) {
@@ -2635,11 +2689,11 @@ function inferTopLevelCategory(rawOchre) {
2635
2689
  for (const category of SET_ITEM_CATEGORIES) if (category in rawOchre) return category;
2636
2690
  if ("variable" in rawOchre) return "propertyVariable";
2637
2691
  if ("value" in rawOchre) return "propertyValue";
2638
- throw new Error("Could not infer OCHRE item category");
2692
+ throw new Error("Could not infer OCHRE item category", { cause: rawOchre });
2639
2693
  }
2640
2694
  function getSingleTopLevelRawItem(items, category) {
2641
- if (items == null || items.length === 0) throw new Error(`${category} not found`);
2642
- if (items.length > 1) throw new Error(`Expected one ${category}, received ${items.length}`);
2695
+ if (items == null || items.length === 0) throw new Error(`${category} not found`, { cause: items });
2696
+ if (items.length > 1) throw new Error(`Expected one ${category}, received ${items.length}`, { cause: items });
2643
2697
  return items[0];
2644
2698
  }
2645
2699
  function parseTopLevelItem(rawOchre, category, options) {
@@ -2826,10 +2880,12 @@ const XMLContextItem = v.objectWithRest({
2826
2880
  tree: v.array(XMLContextValue),
2827
2881
  displayPath: v.string("XMLContextItem: displayPath is string and required")
2828
2882
  }, v.array(XMLContextValue), "XMLContextItem: Shape error");
2829
- const XMLContext = v.array(v.object({
2830
- context: v.array(XMLContextItem, "XMLContext: context is array of XMLContextItem"),
2831
- displayPath: v.string("XMLContext: displayPath is string and required")
2832
- }, "XMLContext: Shape error"));
2883
+ const XMLEmptyContext = v.object({ payload: v.string("XMLEmptyContext: payload is string and required") }, "XMLEmptyContext: Shape error");
2884
+ const XMLContextGroup = v.object({
2885
+ context: v.array(v.union([XMLContextItem, XMLEmptyContext]), "XMLContextGroup: context is array of XMLContextItem or XMLEmptyContext"),
2886
+ displayPath: v.string("XMLContextGroup: displayPath is string and required")
2887
+ }, "XMLContextGroup: Shape error");
2888
+ const XMLContext = v.array(v.union([XMLContextGroup, XMLEmptyContext], "XMLContext: item is XMLContextGroup or XMLEmptyContext"));
2833
2889
  const XMLEvent = v.object({
2834
2890
  dateTime: v.optional(customDateTime("XMLEvent: dateTime is not a valid datetime")),
2835
2891
  endDateTime: v.optional(customDateTime("XMLEvent: endDateTime is not a valid datetime")),
@@ -3612,13 +3668,13 @@ async function fetchGallery(params, options) {
3612
3668
  }),
3613
3669
  headers: { "Content-Type": "application/xquery" }
3614
3670
  });
3615
- if (!response.ok) throw new Error("Error fetching gallery items, please try again later.");
3671
+ if (!response.ok) throw new Error("Error fetching gallery items, please try again later.", { cause: response.statusText });
3616
3672
  const dataRaw = await response.text();
3617
3673
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
3618
3674
  const { success, issues, output } = v.safeParse(XMLGalleryData, data);
3619
3675
  if (!success) {
3620
3676
  logIssues(issues);
3621
- throw new Error("Failed to parse gallery XML");
3677
+ throw createSchemaValidationError("Failed to parse gallery XML", issues);
3622
3678
  }
3623
3679
  restoreXMLMetadata(output, data);
3624
3680
  return {
@@ -3722,13 +3778,13 @@ async function fetchItemLinks(uuid, options) {
3722
3778
  body: buildXQuery$2(parsedUuid),
3723
3779
  headers: { "Content-Type": "application/xquery" }
3724
3780
  });
3725
- if (!response.ok) throw new Error("Failed to fetch OCHRE item links");
3781
+ if (!response.ok) throw new Error("Failed to fetch OCHRE item links", { cause: response.statusText });
3726
3782
  const dataRaw = await response.text();
3727
3783
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
3728
3784
  const { success, issues, output } = v.safeParse(XMLItemLinksData, data);
3729
3785
  if (!success) {
3730
3786
  logIssues(issues);
3731
- throw new Error("Failed to parse OCHRE item links");
3787
+ throw createSchemaValidationError("Failed to parse OCHRE item links", issues);
3732
3788
  }
3733
3789
  restoreXMLMetadata(output, data);
3734
3790
  const languages = resolveItemLinksLanguages(output, requestedLanguages);
@@ -3788,7 +3844,7 @@ function inferFetchItemCategory(rawOchre) {
3788
3844
  if ("resource" in rawOchre) return "resource";
3789
3845
  if ("text" in rawOchre) return "text";
3790
3846
  if ("set" in rawOchre) return "set";
3791
- throw new Error("Could not infer OCHRE item category");
3847
+ throw new Error("Could not infer OCHRE item category", { cause: rawOchre });
3792
3848
  }
3793
3849
  /**
3794
3850
  * Validate language codes while preserving literal tuple inference.
@@ -3823,13 +3879,13 @@ async function fetchItem(uuid, options) {
3823
3879
  assertItemCategoryAllowed(options?.category, options?.containedItemCategory);
3824
3880
  const languages = options?.languages == null ? [] : parseLanguages$1(options.languages);
3825
3881
  const response = await (options?.fetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?uuid=${parsedUuid}&xsl=none&lang="*"`);
3826
- if (!response.ok) throw new Error("Failed to fetch OCHRE data");
3882
+ if (!response.ok) throw new Error("Failed to fetch OCHRE data", { cause: response.statusText });
3827
3883
  const dataRaw = await response.text();
3828
3884
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
3829
3885
  const { success, issues, output } = v.safeParse(XMLData, data);
3830
3886
  if (!success) {
3831
3887
  logIssues(issues);
3832
- throw new Error("Failed to parse OCHRE data");
3888
+ throw createSchemaValidationError("Failed to parse OCHRE data", issues);
3833
3889
  }
3834
3890
  restoreXMLMetadata(output, data);
3835
3891
  const category = options?.category ?? inferFetchItemCategory(output.result.ochre);
@@ -4514,7 +4570,7 @@ function buildLeafQueryExpression(context, query) {
4514
4570
  if (query.target === "property" && query.dataType !== "date" && query.dataType !== "dateTime" && !("value" in query) && query.propertyVariable != null) return buildPropertyPresenceQueryExpression({ propertyVariable: query.propertyVariable });
4515
4571
  if (query.target === "property" && (query.dataType === "date" || query.dataType === "dateTime") && query.value == null) return buildPropertyDateRangeQueryExpression(query);
4516
4572
  const searchValue = getLeafSearchValue(query);
4517
- if (searchValue == null) throw new Error("Missing searchable value for query leaf");
4573
+ if (searchValue == null) throw new Error("Missing searchable value for query leaf", { cause: query });
4518
4574
  const exactHelper = registerLeafHelper({
4519
4575
  context,
4520
4576
  query,
@@ -4589,9 +4645,9 @@ function getCompatibleIncludesGroupLeaves(query) {
4589
4645
  }
4590
4646
  function buildIncludesGroupQueryExpression(context, queries) {
4591
4647
  const firstQuery = queries[0];
4592
- if (firstQuery == null) throw new Error("Cannot build an includes group without queries");
4648
+ if (firstQuery == null) throw new Error("Cannot build an includes group without queries", { cause: queries });
4593
4649
  const groupValue = getGroupableIncludesValue(firstQuery);
4594
- if (groupValue == null) throw new Error("Cannot build an includes group without a search value");
4650
+ if (groupValue == null) throw new Error("Cannot build an includes group without a search value", { cause: firstQuery });
4595
4651
  const terms = tokenizeIncludesSearchValue({
4596
4652
  value: groupValue,
4597
4653
  isCaseSensitive: firstQuery.isCaseSensitive
@@ -4897,18 +4953,18 @@ async function fetchSetItems(params, containedItemCategories, options) {
4897
4953
  body: xquery,
4898
4954
  headers: { "Content-Type": "application/xquery" }
4899
4955
  });
4900
- if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
4956
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`, { cause: response.statusText });
4901
4957
  const dataRaw = await response.text();
4902
4958
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
4903
4959
  const { success, issues, output } = v.safeParse(XMLSetItemsData, data);
4904
4960
  if (!success) {
4905
4961
  logIssues(issues);
4906
- throw new Error("Failed to parse OCHRE Set items");
4962
+ throw createSchemaValidationError("Failed to parse OCHRE Set items", issues);
4907
4963
  }
4908
4964
  restoreXMLMetadata(output, data);
4909
4965
  if (containedItemCategories != null) {
4910
4966
  const missingCategories = containedItemCategories.filter((category) => !hasSetItemsCategory(output.result.ochre.items, category));
4911
- if (missingCategories.length > 0) throw new Error(`No Set items found for item categories: ${missingCategories.join(", ")}`);
4967
+ if (missingCategories.length > 0) throw new Error(`No Set items found for item categories: ${missingCategories.join(", ")}`, { cause: missingCategories });
4912
4968
  }
4913
4969
  const languages = resolveSetItemsLanguages(output, requestedLanguages);
4914
4970
  const items = parseSetItems(output.result.ochre.items, {
@@ -5374,13 +5430,13 @@ async function fetchSetPropertyValues(params, options) {
5374
5430
  body: xquery,
5375
5431
  headers: { "Content-Type": "application/xquery" }
5376
5432
  });
5377
- if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
5433
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`, { cause: response.statusText });
5378
5434
  const dataRaw = await response.text();
5379
5435
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
5380
5436
  const { success, issues, output } = v.safeParse(responseSchema, data);
5381
5437
  if (!success) {
5382
5438
  logIssues(issues);
5383
- throw new Error("Failed to parse OCHRE Set property values");
5439
+ throw createSchemaValidationError("Failed to parse OCHRE Set property values", issues);
5384
5440
  }
5385
5441
  const parsedPropertyValues = [];
5386
5442
  const parsedAttributeValues = [];
@@ -5794,7 +5850,7 @@ var WebsitePresentationReader = class WebsitePresentationReader {
5794
5850
  }
5795
5851
  requiredProperty(label, message) {
5796
5852
  const property = this.property(label);
5797
- if (property === null) throw new Error(message);
5853
+ if (property === null) throw new Error(message, { cause: this.sourceProperties });
5798
5854
  return property;
5799
5855
  }
5800
5856
  propertyByValue(label, value) {
@@ -5942,7 +5998,7 @@ function parseResponsiveCssStyles(properties) {
5942
5998
  */
5943
5999
  function parseBounds(bounds) {
5944
6000
  const [southWest, northEast] = bounds.trim().startsWith("[") ? parseJsonBounds(bounds) : bounds.split(";").map((pair) => pair.split(",").map((coordinate) => Number.parseFloat(coordinate.trim())));
5945
- if (southWest?.length !== 2 || northEast?.length !== 2 || southWest.some((coordinate) => Number.isNaN(coordinate)) || northEast.some((coordinate) => Number.isNaN(coordinate))) throw new Error(`Invalid bounds: ${bounds}`);
6001
+ if (southWest?.length !== 2 || northEast?.length !== 2 || southWest.some((coordinate) => Number.isNaN(coordinate)) || northEast.some((coordinate) => Number.isNaN(coordinate))) throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
5946
6002
  return [[southWest[0], southWest[1]], [northEast[0], northEast[1]]];
5947
6003
  }
5948
6004
  function parseJsonBounds(bounds) {
@@ -5950,9 +6006,9 @@ function parseJsonBounds(bounds) {
5950
6006
  try {
5951
6007
  parsed = JSON.parse(bounds);
5952
6008
  } catch {
5953
- throw new Error(`Invalid bounds: ${bounds}`);
6009
+ throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
5954
6010
  }
5955
- if (!isNumberPairArray(parsed)) throw new Error(`Invalid bounds: ${bounds}`);
6011
+ if (!isNumberPairArray(parsed)) throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
5956
6012
  return parsed;
5957
6013
  }
5958
6014
  function isNumberPairArray(value) {
@@ -6030,7 +6086,7 @@ function parseStylesheets(styles) {
6030
6086
  mobile: []
6031
6087
  };
6032
6088
  if (style.category === "propertyValue" || style.valueUuid != null) {
6033
- if (style.valueUuid == null) throw new Error(`Stylesheet property value "${style.variableUuid}" is missing a value UUID`);
6089
+ if (style.valueUuid == null) throw new Error(`Stylesheet property value "${style.variableUuid}" is missing a value UUID`, { cause: style });
6034
6090
  parsedStyles.push({
6035
6091
  uuid: style.valueUuid,
6036
6092
  category: "propertyValue",
@@ -6066,7 +6122,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6066
6122
  switch (componentName) {
6067
6123
  case "3d-viewer": {
6068
6124
  const resourceLink = findWebsiteLink(websiteLinks, "resource", (link) => link.fileFormat === "model/obj");
6069
- if (resourceLink == null) throw new Error(formatComponentError("Resource link not found", componentName, elementResource));
6125
+ if (resourceLink == null) throw new Error(formatComponentError("Resource link not found", componentName, elementResource), { cause: componentProperty });
6070
6126
  const isInteractive = componentReader.valueOr("is-interactive", true);
6071
6127
  const isControlsDisplayed = componentReader.valueOr("controls-displayed", true);
6072
6128
  properties = {
@@ -6081,7 +6137,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6081
6137
  case "advanced-search": {
6082
6138
  const boundElementPropertyUuid = componentReader.uuid("bound-element");
6083
6139
  const href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
6084
- if (boundElementPropertyUuid == null && href == null) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource));
6140
+ if (boundElementPropertyUuid == null && href == null) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource), { cause: componentProperty });
6085
6141
  properties = {
6086
6142
  component: "advanced-search",
6087
6143
  boundElementUuid: boundElementPropertyUuid,
@@ -6091,7 +6147,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6091
6147
  }
6092
6148
  case "annotated-document": {
6093
6149
  const documentLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "internalDocument");
6094
- if (documentLink == null) throw new Error(formatComponentError("Document link not found", componentName, elementResource));
6150
+ if (documentLink == null) throw new Error(formatComponentError("Document link not found", componentName, elementResource), { cause: componentProperty });
6095
6151
  properties = {
6096
6152
  component: "annotated-document",
6097
6153
  linkUuid: documentLink.uuid
@@ -6100,7 +6156,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6100
6156
  }
6101
6157
  case "annotated-image": {
6102
6158
  const imageLinks = getWebsiteLinks(websiteLinks, "resource").filter((link) => link.type === "image" || link.type === "IIIF");
6103
- if (imageLinks.length === 0) throw new Error(formatComponentError("Image link not found", componentName, elementResource));
6159
+ if (imageLinks.length === 0) throw new Error(formatComponentError("Image link not found", componentName, elementResource), { cause: componentProperty });
6104
6160
  const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
6105
6161
  const isOptionsDisplayed = componentReader.valueOr("options-displayed", true);
6106
6162
  const isAnnotationHighlightsDisplayed = componentReader.valueOr("annotation-highlights-displayed", true);
@@ -6117,7 +6173,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6117
6173
  }
6118
6174
  case "audio-player": {
6119
6175
  const audioLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "audio");
6120
- if (audioLink == null) throw new Error(formatComponentError("Audio link not found", componentName, elementResource));
6176
+ if (audioLink == null) throw new Error(formatComponentError("Audio link not found", componentName, elementResource), { cause: componentProperty });
6121
6177
  const isSpeedControlsDisplayed = componentReader.valueOr("speed-controls-displayed", true);
6122
6178
  const isVolumeControlsDisplayed = componentReader.valueOr("volume-controls-displayed", true);
6123
6179
  const isSeekBarDisplayed = componentReader.valueOr("seek-bar-displayed", true);
@@ -6133,7 +6189,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6133
6189
  case "bibliography": {
6134
6190
  const itemLinks = websiteLinks.filter((link) => link.category !== "bibliography");
6135
6191
  const bibliographies = parseBibliographyList(elementResource.bibliographies, options);
6136
- if (itemLinks.length === 0 && bibliographies.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource));
6192
+ if (itemLinks.length === 0 && bibliographies.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource), { cause: componentProperty });
6137
6193
  const layout = componentReader.valueOr("layout", "long");
6138
6194
  const isSourceDocumentDisplayed = componentReader.valueOr("source-document-displayed", true);
6139
6195
  properties = {
@@ -6151,7 +6207,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6151
6207
  let href = componentReader.linkTarget("navigate-to", transformPermanentIdentificationUrlToItemLink);
6152
6208
  if (href === null) {
6153
6209
  href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
6154
- if (href === null) throw new Error(formatComponentError("Properties “navigate-to” or “link-to” not found", componentName, elementResource));
6210
+ if (href === null) throw new Error(formatComponentError("Properties “navigate-to” or “link-to” not found", componentName, elementResource), { cause: componentProperty });
6155
6211
  else isExternal = true;
6156
6212
  }
6157
6213
  const startIcon = componentReader.value("start-icon");
@@ -6180,7 +6236,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6180
6236
  }
6181
6237
  case "collection": {
6182
6238
  const setLinks = getWebsiteLinks(websiteLinks, "set");
6183
- if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource));
6239
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource), { cause: componentProperty });
6184
6240
  const displayedProperties = componentReader.property("use-property");
6185
6241
  const variant = componentReader.valueOr("variant", "slide");
6186
6242
  const paginationVariant = componentReader.valueOr("pagination-variant", "default");
@@ -6229,7 +6285,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6229
6285
  break;
6230
6286
  case "entries": {
6231
6287
  const entriesLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
6232
- if (entriesLink == null) throw new Error(formatComponentError("Entries link not found", componentName, elementResource));
6288
+ if (entriesLink == null) throw new Error(formatComponentError("Entries link not found", componentName, elementResource), { cause: componentProperty });
6233
6289
  const variant = componentReader.valueOr("variant", "entry");
6234
6290
  const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", false);
6235
6291
  properties = {
@@ -6242,7 +6298,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6242
6298
  }
6243
6299
  case "iframe": {
6244
6300
  const webpageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "webpage");
6245
- if (webpageLink?.href == null) throw new Error(formatComponentError("URL not found", componentName, elementResource));
6301
+ if (webpageLink?.href == null) throw new Error(formatComponentError("URL not found", componentName, elementResource), { cause: componentProperty });
6246
6302
  properties = {
6247
6303
  component: "iframe",
6248
6304
  href: transformPermanentIdentificationUrlToItemLink(webpageLink.href),
@@ -6253,7 +6309,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6253
6309
  }
6254
6310
  case "iiif-viewer": {
6255
6311
  const manifestLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "IIIF");
6256
- if (manifestLink == null) throw new Error(formatComponentError("Manifest link not found", componentName, elementResource));
6312
+ if (manifestLink == null) throw new Error(formatComponentError("Manifest link not found", componentName, elementResource), { cause: componentProperty });
6257
6313
  const variant = componentReader.valueOr("variant", "universal-viewer");
6258
6314
  properties = {
6259
6315
  component: "iiif-viewer",
@@ -6263,7 +6319,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6263
6319
  break;
6264
6320
  }
6265
6321
  case "image": {
6266
- if (websiteLinks.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource));
6322
+ if (websiteLinks.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource), { cause: componentProperty });
6267
6323
  const imageQuality = componentReader.valueOr("image-quality", "high");
6268
6324
  const images = [];
6269
6325
  for (const link of websiteLinks) images.push({
@@ -6333,7 +6389,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6333
6389
  }
6334
6390
  case "image-gallery": {
6335
6391
  const galleryLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
6336
- if (galleryLink == null) throw new Error(formatComponentError("Image gallery link not found", componentName, elementResource));
6392
+ if (galleryLink == null) throw new Error(formatComponentError("Image gallery link not found", componentName, elementResource), { cause: componentProperty });
6337
6393
  const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
6338
6394
  properties = {
6339
6395
  component: "image-gallery",
@@ -6344,7 +6400,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6344
6400
  }
6345
6401
  case "map": {
6346
6402
  const mapLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
6347
- if (mapLink == null) throw new Error(formatComponentError("Map link not found", componentName, elementResource));
6403
+ if (mapLink == null) throw new Error(formatComponentError("Map link not found", componentName, elementResource), { cause: componentProperty });
6348
6404
  const isInteractive = componentReader.valueOr("is-interactive", true);
6349
6405
  const isClustered = componentReader.valueOr("is-clustered", false);
6350
6406
  const isUsingPins = componentReader.valueOr("is-using-pins", false);
@@ -6373,9 +6429,9 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6373
6429
  }
6374
6430
  case "query": {
6375
6431
  const setLinks = getWebsiteLinks(websiteLinks, "set");
6376
- if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource));
6432
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource), { cause: componentProperty });
6377
6433
  const items = [];
6378
- if (componentProperty.properties.length === 0) throw new Error(formatComponentError("Query properties not found", componentName, elementResource));
6434
+ if (componentProperty.properties.length === 0) throw new Error(formatComponentError("Query properties not found", componentName, elementResource), { cause: componentProperty });
6379
6435
  for (const queryItem of componentProperty.properties) {
6380
6436
  const queryReader = websitePresentationReader(queryItem.properties);
6381
6437
  const label = queryReader.multilingualValue("query-prompt", options);
@@ -6385,9 +6441,9 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6385
6441
  if (queryLanguage == null) throw new Error(formatComponentError("Query language not found", componentName, elementResource));
6386
6442
  const queries = [];
6387
6443
  for (const propertyVariable of propertyVariables) {
6388
- if (propertyVariable.uuid === null) throw new Error(formatComponentError("Property variable UUID not found", componentName, elementResource));
6444
+ if (propertyVariable.uuid === null) throw new Error(formatComponentError("Property variable UUID not found", componentName, elementResource), { cause: propertyVariable });
6389
6445
  const dataType = propertyVariable.dataType;
6390
- if (dataType === "coordinate") throw new Error(formatComponentError("Query prompts with data type \"coordinate\" are not supported", componentName, elementResource));
6446
+ if (dataType === "coordinate") throw new Error(formatComponentError("Query prompts with data type \"coordinate\" are not supported", componentName, elementResource), { cause: propertyVariable });
6391
6447
  queries.push({
6392
6448
  target: "property",
6393
6449
  propertyVariable: propertyVariable.uuid,
@@ -6406,7 +6462,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6406
6462
  endIcon
6407
6463
  });
6408
6464
  }
6409
- if (items.length === 0) throw new Error(formatComponentError("No queries found", componentName, elementResource));
6465
+ if (items.length === 0) throw new Error(formatComponentError("No queries found", componentName, elementResource), { cause: componentProperty });
6410
6466
  const componentOptions = parseWebsiteOptions(elementResource.options, options);
6411
6467
  const displayedProperties = componentReader.property("use-property");
6412
6468
  const variant = componentReader.valueOr("variant", "slide");
@@ -6433,7 +6489,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6433
6489
  }
6434
6490
  case "table": {
6435
6491
  const tableLink = findWebsiteLink(websiteLinks, "set");
6436
- if (tableLink == null) throw new Error(formatComponentError("Table link not found", componentName, elementResource));
6492
+ if (tableLink == null) throw new Error(formatComponentError("Table link not found", componentName, elementResource), { cause: componentProperty });
6437
6493
  properties = {
6438
6494
  component: "table",
6439
6495
  linkUuid: tableLink.uuid
@@ -6444,7 +6500,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6444
6500
  const queryVariant = componentReader.valueOr("query-variant", "submit");
6445
6501
  const boundElementUuid = componentReader.uuid("bound-element");
6446
6502
  const href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
6447
- if (!boundElementUuid && !href) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource));
6503
+ if (!boundElementUuid && !href) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource), { cause: componentProperty });
6448
6504
  properties = {
6449
6505
  component: "search-bar",
6450
6506
  queryVariant,
@@ -6457,7 +6513,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6457
6513
  }
6458
6514
  case "text": {
6459
6515
  const content = elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null;
6460
- if (content == null) throw new Error(formatComponentError("Content not found", componentName, elementResource));
6516
+ if (content == null) throw new Error(formatComponentError("Content not found", componentName, elementResource), { cause: componentProperty });
6461
6517
  let variantName = "block";
6462
6518
  let variant;
6463
6519
  const variantProperty = componentReader.property("variant");
@@ -6503,7 +6559,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6503
6559
  }
6504
6560
  case "timeline": {
6505
6561
  const timelineLink = findWebsiteLink(websiteLinks, "tree");
6506
- if (timelineLink == null) throw new Error(formatComponentError("Timeline link not found", componentName, elementResource));
6562
+ if (timelineLink == null) throw new Error(formatComponentError("Timeline link not found", componentName, elementResource), { cause: componentProperty });
6507
6563
  properties = {
6508
6564
  component: "timeline",
6509
6565
  linkUuid: timelineLink.uuid
@@ -6512,7 +6568,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6512
6568
  }
6513
6569
  case "video": {
6514
6570
  const videoLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "video");
6515
- if (videoLink == null) throw new Error(formatComponentError("Video link not found", componentName, elementResource));
6571
+ if (videoLink == null) throw new Error(formatComponentError("Video link not found", componentName, elementResource), { cause: componentProperty });
6516
6572
  const isChaptersDisplayed = componentReader.valueOr("chapters-displayed", true);
6517
6573
  properties = {
6518
6574
  component: "video",
@@ -6525,7 +6581,7 @@ function parseWebElementProperties(componentProperty, elementResource, options)
6525
6581
  console.warn(`Invalid or non-implemented component name “${unparsedComponentName.toString()}” for the following element: “${parseStringContent(elementResource.identification.label, options)}”`);
6526
6582
  break;
6527
6583
  }
6528
- if (properties === null) throw new Error(formatComponentError("Properties not found", componentName, elementResource));
6584
+ if (properties === null) throw new Error(formatComponentError("Properties not found", componentName, elementResource), { cause: componentProperty });
6529
6585
  return properties;
6530
6586
  }
6531
6587
  function parseWebTitle(properties, identification, overrides) {
@@ -6586,7 +6642,7 @@ function parseWebpage(webpageResource, options, context, slugPrefix) {
6586
6642
  if (webpageReader.value("presentation") !== "page") return null;
6587
6643
  const identification = parseIdentification(webpageResource.identification, options);
6588
6644
  const slug = webpageResource.slug?.replace(SEGMENT_UNIQUE_SLUG_PREFIX_REGEX, "") ?? null;
6589
- if (slug == null) throw new Error(`Slug not found for page (${formatXMLWebsiteResourceMetadata(webpageResource)})`);
6645
+ if (slug == null) throw new Error(`Slug not found for page (${formatXMLWebsiteResourceMetadata(webpageResource)})`, { cause: webpageResource });
6590
6646
  const returnWebpage = {
6591
6647
  uuid: webpageResource.uuid,
6592
6648
  type: "page",
@@ -6673,7 +6729,7 @@ function parseWebsiteSegments(resources, context, options, slugPrefix) {
6673
6729
  if (!("segments" in resource)) continue;
6674
6730
  for (const tree of resource.segments.tree) {
6675
6731
  const segmentSlug = tree.identification.abbreviation == null ? null : parseStringContent(tree.identification.abbreviation, options);
6676
- if (segmentSlug == null) throw new Error(`Slug not found for segment website (website uuid “${tree.uuid}”)`);
6732
+ if (segmentSlug == null) throw new Error(`Slug not found for segment website (website uuid “${tree.uuid}”)`, { cause: tree });
6677
6733
  segments.push(parseWebsiteTree(tree, context, "segment", options, prefixSlug(segmentSlug, slugPrefix)));
6678
6734
  }
6679
6735
  }
@@ -6854,9 +6910,9 @@ function parseWebBlock(blockResource, options) {
6854
6910
  for (const resource of blockResources) {
6855
6911
  const resourceReader = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []);
6856
6912
  const resourceType = resourceReader.value("presentation");
6857
- if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” (${formatXMLWebsiteResourceMetadata(resource)})`);
6913
+ if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” (${formatXMLWebsiteResourceMetadata(resource)})`, { cause: resource });
6858
6914
  const componentType = resourceReader.nestedByValue("presentation", "element").value("component");
6859
- if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” (${formatXMLWebsiteResourceMetadata(resource)})`);
6915
+ if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” (${formatXMLWebsiteResourceMetadata(resource)})`, { cause: resource });
6860
6916
  const element = parseWebElementForAccordion(resource, options);
6861
6917
  accordionItems.push(element);
6862
6918
  }
@@ -6950,7 +7006,7 @@ function parseWebsiteProperties(properties, websiteTree, sidebar, options) {
6950
7006
  name: contactContent[0],
6951
7007
  email: contactContent[1] ?? null
6952
7008
  };
6953
- else throw new Error(`Contact property must use “name;email”, got “${contactProperty.values[0]?.content}” (website uuid “${websiteTree.uuid}”)`);
7009
+ else throw new Error(`Contact property must use “name;email”, got “${contactProperty.values[0]?.content}” (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
6954
7010
  }
6955
7011
  returnProperties.loadingVariant = websiteReader.valueOr("loading-variant", "spinner");
6956
7012
  returnProperties.theme.isThemeToggleDisplayed = websiteReader.valueOr("supports-theme-toggle", true);
@@ -7056,8 +7112,8 @@ function parseFilterContexts(filterContextLevels, options) {
7056
7112
  return filterContextTreeLevels;
7057
7113
  }
7058
7114
  function parseWebsiteTree(websiteTree, context, type, options, slugPrefix) {
7059
- if (!websiteTree.properties) throw new Error(`Website properties not found (website uuid “${websiteTree.uuid}”)`);
7060
- if (type === "website" && websiteTree.items?.resource == null) throw new Error(`Website pages not found (website uuid “${websiteTree.uuid}”)`);
7115
+ if (!websiteTree.properties) throw new Error(`Website properties not found (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
7116
+ if (type === "website" && websiteTree.items?.resource == null) throw new Error(`Website pages not found (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
7061
7117
  const resources = normalizeWebsiteResources(websiteTree.items?.resource);
7062
7118
  const sidebar = parseSidebar(resources, options);
7063
7119
  const properties = parseWebsiteProperties(websiteTree.properties.property, websiteTree, sidebar, options);
@@ -7081,7 +7137,7 @@ function parseWebsite(data, options) {
7081
7137
  const parserOptions = { languages };
7082
7138
  const defaultLanguage = resolveDefaultLanguage(rawOchre, languages);
7083
7139
  const websiteTree = rawOchre.tree[0];
7084
- if (websiteTree == null) throw new Error("Website tree not found");
7140
+ if (websiteTree == null) throw new Error("Website tree not found", { cause: data });
7085
7141
  return parseWebsiteTree(websiteTree, {
7086
7142
  belongsTo: {
7087
7143
  uuid: rawOchre.uuidBelongsTo,
@@ -7102,13 +7158,13 @@ async function fetchWebsite(abbreviation, options) {
7102
7158
  try {
7103
7159
  const cleanAbbreviation = abbreviation.trim().toLocaleLowerCase("en-US");
7104
7160
  const response = await (options?.fetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery=${encodeURIComponent(`collection('ochre/tree')/ochre[tree/identification/abbreviation/content/string='${cleanAbbreviation}']`)}&xsl=none&lang="*"`);
7105
- if (!response.ok) throw new Error("Failed to fetch website");
7161
+ if (!response.ok) throw new Error("Failed to fetch website", { cause: response.statusText });
7106
7162
  const dataRaw = await response.text();
7107
7163
  const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
7108
7164
  const { success, issues, output } = v.safeParse(XMLWebsiteData, data);
7109
7165
  if (!success) {
7110
7166
  logIssues(issues);
7111
- throw new Error("Failed to parse website XML");
7167
+ throw createSchemaValidationError("Failed to parse website XML", issues);
7112
7168
  }
7113
7169
  restoreXMLMetadata(output, data);
7114
7170
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ochre-sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Node.js library for working with OCHRE (Online Cultural and Historical Research Environment) data",