ochre-sdk 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -66,6 +66,9 @@ present and `error` is `null`; on failure, the parsed value is `null` and
66
66
  style, collection, and item-page configuration.
67
67
  - `fetchSetItems(params, containedItemCategories, options)` fetches paginated
68
68
  Set search results with typed query and sort support.
69
+ - `fetchTreeItems(params, containedItemCategories, options)` is the same for a
70
+ Tree, and is how to read a Tree too large to fetch whole. Items nested under
71
+ headings come back flat, in document order.
69
72
  - `fetchSetPropertyValues(params, options)` fetches Set property-value facets
70
73
  and optional bibliography/period attribute facets for the same query model.
71
74
 
@@ -84,6 +87,34 @@ title.getExactText("tur");
84
87
  title.getAvailableLanguages();
85
88
  ```
86
89
 
90
+ Reads come in three widths. `getText` and `getRichText` resolve one entry through the language fallback, `getExactText` and `getExactRichText` skip the fallback, and `getEntries`, `getTexts` and `getExactTexts` return every entry when OCHRE carried more than one for a language.
91
+
92
+ Two questions look alike and are not the same. `isEmpty()` asks whether any language carries an entry at all. `hasContent()` asks whether any entry in any language holds text that is not whitespace, which is what a caller deciding whether to render something wants, because OCHRE does serve fields holding a single blank entry.
93
+
94
+ ```ts
95
+ title.isEmpty();
96
+ title.hasContent();
97
+ title.hasLanguage("tur");
98
+ title.hasAliases();
99
+
100
+ title.getAvailableLanguages(); // languages that carry content
101
+ title.getSupportedLanguages(); // languages the string was built to hold
102
+ ```
103
+
104
+ Writes return a new string and never mutate the original.
105
+
106
+ ```ts
107
+ title.with("tur", "Başlık");
108
+ title.with("tur", "another entry", { shouldAppend: true });
109
+ title.without("tur");
110
+ title.withAliases(["ABC"]);
111
+
112
+ title.mapText((text) => text.trim());
113
+ title.filterEntries((entry) => entry.isPrimary);
114
+ ```
115
+
116
+ `mapText` runs against plain text and rebuilds each entry's rich text from the result, so a transform cannot corrupt the markup OCHRE wrote. A transform that needs to keep or rewrite markup returns `{ text, richText }` instead of a string.
117
+
87
118
  For reusable language tuples, use `defineLanguages` to keep runtime validation
88
119
  and literal TypeScript inference together.
89
120
 
@@ -121,6 +152,23 @@ const result = await fetchSetItems(
121
152
  Use `fetchSetPropertyValues` with the same query shape when you need facet data
122
153
  for a filtered result set.
123
154
 
155
+ `fetchTreeItems` takes the same `Query` tree and the same sort options, because
156
+ both fetchers compile the same query model against their container's searchable
157
+ path. The difference is what the payload carries: OCHRE publishes Tree items as
158
+ identification-only stubs unless the Tree is published with item properties, so
159
+ a `property` query or a `propertyValue` sort only works on a Tree that carries
160
+ them.
161
+
162
+ ```ts
163
+ import { fetchTreeItems } from "ochre-sdk";
164
+
165
+ const result = await fetchTreeItems(
166
+ { treeScopeUuids: ["<tree-uuid>"], sort: { target: "title" }, page: 1 },
167
+ ["concept"],
168
+ { languages: ["eng"] },
169
+ );
170
+ ```
171
+
124
172
  ### OCR Text Queries
125
173
 
126
174
  The `ocr` target searches the OCR text layer of the Resource items in a Set. It takes no `language`, because OCR text carries none.
@@ -177,8 +225,9 @@ types, query types, property getters, and small data helpers:
177
225
  - `Item`, `SetItem`, `ItemLink`, `Website`, `WebElementOf`,
178
226
  `WebElementComponentOf`, `WebBlockByLayout`, `Query`, and related types.
179
227
  - `getProperty`, `getPropertyValues`, `getPropertyValue`,
180
- `getUniqueProperties`, `getUniquePropertyVariableLabels`, and
181
- `isPropertyMatchingFilter` for reading properties off a parsed item.
228
+ `getUniqueProperties`, `getUniquePropertyVariableLabels`,
229
+ `isPropertyMatchingFilter`, and `getLeafPropertyValues` for reading
230
+ properties off a parsed item.
182
231
  - `flattenItemProperties` and `DEFAULT_PAGE_SIZE` for common collection UI
183
232
  workflows.
184
233
 
@@ -0,0 +1,52 @@
1
+ import { ItemCategory, Query, SetItem, SetItemsSort } from "../types/index.mjs";
2
+ import { XMLSetItems } from "../xml/types.mjs";
3
+ import { FetchRuntimeOptions } from "../parsers/helpers.mjs";
4
+ //#region src/fetchers/container-items.d.ts
5
+ /**
6
+ * Whether a container payload carries any item of a category
7
+ * @param items - The parsed `<items>` payload
8
+ * @param category - The category to look for
9
+ * @returns True when the payload carries at least one item of it
10
+ * @internal
11
+ */
12
+ export declare function hasItemsCategory(items: XMLSetItems, category: ItemCategory): boolean;
13
+ /**
14
+ * Fetch one page of the items a Set or a Tree holds
15
+ *
16
+ * The only thing that differs between the two containers is the searchable
17
+ * path {@link compileContainerItemsQuery} picks, so counting, sorting,
18
+ * paginating, validating and parsing are written once here and the public
19
+ * fetchers are thin wrappers that name their own scope parameter.
20
+ * @param parameters - The request parameters
21
+ * @param parameters.container - Whether the scope UUIDs name Sets or Trees
22
+ * @param parameters.scopeUuids - The container UUIDs to search within
23
+ * @param parameters.belongsToCollectionScopeUuids - Collection scope UUIDs to narrow to
24
+ * @param parameters.queries - The query tree to filter by, or null to match every item
25
+ * @param parameters.sort - The sort applied before pagination
26
+ * @param parameters.page - The page number, 1-indexed
27
+ * @param parameters.pageSize - The number of items per page
28
+ * @param parameters.containedItemCategories - Categories the payload must carry
29
+ * @param parameters.label - What is being fetched, used in failure messages
30
+ * @param parameters.options - Fetch and parser options
31
+ * @returns The page of items, deduplicated by UUID
32
+ * @throws When the request fails, validation fails, or a category is missing
33
+ * @internal
34
+ */
35
+ export declare function fetchContainerItems(parameters: {
36
+ container: "set" | "tree";
37
+ scopeUuids: Array<string>;
38
+ belongsToCollectionScopeUuids: Array<string>;
39
+ queries: Query | null;
40
+ sort: SetItemsSort;
41
+ page: number;
42
+ pageSize: number;
43
+ containedItemCategories?: ReadonlyArray<ItemCategory>;
44
+ label: string;
45
+ options?: FetchRuntimeOptions;
46
+ }): Promise<{
47
+ totalCount: number;
48
+ page: number;
49
+ pageSize: number;
50
+ items: Array<SetItem<ItemCategory, ReadonlyArray<string>>>;
51
+ }>;
52
+ //#endregion
@@ -0,0 +1,171 @@
1
+ import { requestOchre } from "./request.mjs";
2
+ import { parseRequestedLanguages, resolveContentLanguages } from "../parsers/languages.mjs";
3
+ import { parseSetItems } from "../parsers/index.mjs";
4
+ import { XMLSetItemsData } from "../xml/schemas.mjs";
5
+ import { stringLiteral } from "../xquery.mjs";
6
+ import { compileContainerItemsQuery } from "../query.mjs";
7
+ //#region src/fetchers/container-items.ts
8
+ function hasArray(items) {
9
+ return items != null && items.length > 0;
10
+ }
11
+ /**
12
+ * Whether a container payload carries any item of a category
13
+ * @param items - The parsed `<items>` payload
14
+ * @param category - The category to look for
15
+ * @returns True when the payload carries at least one item of it
16
+ * @internal
17
+ */
18
+ function hasItemsCategory(items, category) {
19
+ switch (category) {
20
+ case "tree": return hasArray(items.tree);
21
+ case "bibliography": return hasArray(items.bibliography);
22
+ case "concept": return hasArray(items.concept);
23
+ case "spatialUnit": return hasArray(items.spatialUnit);
24
+ case "period": return hasArray(items.period);
25
+ case "person": return hasArray(items.person);
26
+ case "propertyVariable": return hasArray(items.propertyVariable) || hasArray(items.variable);
27
+ case "propertyValue": return hasArray(items.propertyValue) || hasArray(items.value);
28
+ case "resource": return hasArray(items.resource);
29
+ case "text": return hasArray(items.text);
30
+ case "set": return hasArray(items.set);
31
+ }
32
+ }
33
+ function mapSortDirectionToXQuery(direction) {
34
+ return direction === "desc" ? "descending" : "ascending";
35
+ }
36
+ function buildStringOrderByClause(direction) {
37
+ return `($sortKey = "") ascending, lower-case($sortKey) ${direction}, $position ascending`;
38
+ }
39
+ function buildTypedOrderByClause(direction) {
40
+ return `empty($sortKey) ascending, $sortKey ${direction}, $position ascending`;
41
+ }
42
+ function buildPropertyValueValuePath(sort) {
43
+ return `$item//properties//property[label/@uuid=${stringLiteral(sort.propertyVariableUuid)}]/value[not(@i)]`;
44
+ }
45
+ function buildPropertyValueStringSortKeyExpression(sort) {
46
+ const languageLiteral = stringLiteral(sort.language ?? "eng");
47
+ return `string((for $v in ${buildPropertyValueValuePath(sort)}
48
+ let $candidate := string-join($v/content[@xml:lang=${languageLiteral}]/string, "")
49
+ where string-length($candidate) gt 0
50
+ return $candidate)[1])`;
51
+ }
52
+ function buildPropertyValueTypedSortKeyExpression(parameters) {
53
+ const { sort, dataType } = parameters;
54
+ const propertyValuePath = buildPropertyValueValuePath(sort);
55
+ switch (dataType) {
56
+ case "integer": return `(for $v in ${propertyValuePath}
57
+ let $candidate := normalize-space(string($v/@rawValue))
58
+ where $candidate castable as xs:integer
59
+ return xs:integer($candidate))[1]`;
60
+ case "decimal":
61
+ case "time": return `(for $v in ${propertyValuePath}
62
+ let $candidate := normalize-space(string($v/@rawValue))
63
+ where $candidate castable as xs:decimal
64
+ return xs:decimal($candidate))[1]`;
65
+ case "boolean": return `(for $v in ${propertyValuePath}
66
+ let $candidate := lower-case(normalize-space(string($v/@rawValue)))
67
+ where $candidate = ("true", "false", "1", "0")
68
+ return if ($candidate = ("true", "1")) then 1 else 0)[1]`;
69
+ case "date": return `(for $v in ${propertyValuePath}
70
+ let $candidate := normalize-space(string($v/@rawValue))
71
+ where $candidate castable as xs:date
72
+ return xs:date($candidate))[1]`;
73
+ case "dateTime": return `(for $v in ${propertyValuePath}
74
+ let $candidate := normalize-space(string($v/@rawValue))
75
+ where $candidate castable as xs:dateTime
76
+ return xs:dateTime($candidate))[1]`;
77
+ }
78
+ }
79
+ function buildPropertyValueOrderByClause(parameters) {
80
+ const { dataType, direction } = parameters;
81
+ return (dataType === "string" || dataType === "IDREF" ? buildStringOrderByClause : buildTypedOrderByClause)(direction);
82
+ }
83
+ function buildOrderedItemsClause(sort) {
84
+ if (sort.target === "none") return "let $orderedItems := $items";
85
+ const direction = mapSortDirectionToXQuery(sort.direction);
86
+ if (sort.target === "title") return `let $orderedItems :=
87
+ for $item at $position in $items
88
+ let $sortKey := ${`string-join($item/identification/label/content[@xml:lang=${stringLiteral(sort.language ?? "eng")}]/string, "")`}
89
+ stable order by ${buildStringOrderByClause(direction)}
90
+ return $item`;
91
+ if (sort.target === "date") return `let $orderedItems :=
92
+ for $item at $position in $items
93
+ let $sortKey := string(($item/@date, $item/interpretations/interpretation/@date)[1])
94
+ stable order by ${buildStringOrderByClause(direction)}
95
+ return $item`;
96
+ return `let $orderedItems :=
97
+ for $item at $position in $items
98
+ let $sortKey := ${sort.dataType === "string" || sort.dataType === "IDREF" ? buildPropertyValueStringSortKeyExpression(sort) : buildPropertyValueTypedSortKeyExpression({
99
+ sort,
100
+ dataType: sort.dataType
101
+ })}
102
+ stable order by ${buildPropertyValueOrderByClause({
103
+ dataType: sort.dataType,
104
+ direction
105
+ })}
106
+ return $item`;
107
+ }
108
+ /**
109
+ * Fetch one page of the items a Set or a Tree holds
110
+ *
111
+ * The only thing that differs between the two containers is the searchable
112
+ * path {@link compileContainerItemsQuery} picks, so counting, sorting,
113
+ * paginating, validating and parsing are written once here and the public
114
+ * fetchers are thin wrappers that name their own scope parameter.
115
+ * @param parameters - The request parameters
116
+ * @param parameters.container - Whether the scope UUIDs name Sets or Trees
117
+ * @param parameters.scopeUuids - The container UUIDs to search within
118
+ * @param parameters.belongsToCollectionScopeUuids - Collection scope UUIDs to narrow to
119
+ * @param parameters.queries - The query tree to filter by, or null to match every item
120
+ * @param parameters.sort - The sort applied before pagination
121
+ * @param parameters.page - The page number, 1-indexed
122
+ * @param parameters.pageSize - The number of items per page
123
+ * @param parameters.containedItemCategories - Categories the payload must carry
124
+ * @param parameters.label - What is being fetched, used in failure messages
125
+ * @param parameters.options - Fetch and parser options
126
+ * @returns The page of items, deduplicated by UUID
127
+ * @throws When the request fails, validation fails, or a category is missing
128
+ * @internal
129
+ */
130
+ async function fetchContainerItems(parameters) {
131
+ const { container, scopeUuids, belongsToCollectionScopeUuids, queries, sort, page, pageSize, containedItemCategories, label, options } = parameters;
132
+ const requestedLanguages = parseRequestedLanguages(options?.languages);
133
+ const startPosition = (page - 1) * pageSize + 1;
134
+ const output = await requestOchre({
135
+ xquery: compileContainerItemsQuery({
136
+ container,
137
+ scopeUuids,
138
+ belongsToCollectionScopeUuids,
139
+ queries,
140
+ body: ({ items, omitSupplemental }) => ` let $totalCount := count(${items})
141
+ ${buildOrderedItemsClause(sort)}
142
+ let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
143
+
144
+ return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
145
+ ${omitSupplemental("$pagedItems")}
146
+ }</items>`
147
+ }),
148
+ schema: XMLSetItemsData,
149
+ label,
150
+ options
151
+ });
152
+ if (containedItemCategories != null) {
153
+ const missingCategories = containedItemCategories.filter((category) => !hasItemsCategory(output.result.ochre.items, category));
154
+ if (missingCategories.length > 0) throw new Error(`No items found for item categories: ${missingCategories.join(", ")}`, { cause: missingCategories });
155
+ }
156
+ const parserOptions = { languages: resolveContentLanguages(output.result.ochre.items, requestedLanguages) };
157
+ const items = parseSetItems(output.result.ochre.items, {
158
+ containedItemCategories,
159
+ languages: parserOptions.languages
160
+ });
161
+ const itemsByUuid = /* @__PURE__ */ new Map();
162
+ for (const item of items) if (!itemsByUuid.has(item.uuid)) itemsByUuid.set(item.uuid, item);
163
+ return {
164
+ totalCount: output.result.ochre.items.totalCount,
165
+ page: output.result.ochre.items.page,
166
+ pageSize: output.result.ochre.items.pageSize,
167
+ items: itemsByUuid.values().toArray()
168
+ };
169
+ }
170
+ //#endregion
171
+ export { fetchContainerItems, hasItemsCategory };
@@ -1,163 +1,24 @@
1
1
  import { getErrorOutput } from "../../errors.mjs";
2
- import { requestOchre } from "../request.mjs";
3
2
  import { setItemsParametersSchema } from "../../schemas.mjs";
4
- import { parseRequestedLanguages, resolveContentLanguages } from "../../parsers/languages.mjs";
5
- import { parseSetItems } from "../../parsers/index.mjs";
6
- import { XMLSetItemsData } from "../../xml/schemas.mjs";
7
- import { stringLiteral } from "../../xquery.mjs";
8
- import { compileSetItemsQuery } from "../../query.mjs";
3
+ import { fetchContainerItems } from "../container-items.mjs";
9
4
  import * as v from "valibot";
10
5
  //#region src/fetchers/set/items.ts
11
- function hasArray(items) {
12
- return items != null && items.length > 0;
13
- }
14
- function hasSetItemsCategory(items, category) {
15
- switch (category) {
16
- case "tree": return hasArray(items.tree);
17
- case "bibliography": return hasArray(items.bibliography);
18
- case "concept": return hasArray(items.concept);
19
- case "spatialUnit": return hasArray(items.spatialUnit);
20
- case "period": return hasArray(items.period);
21
- case "person": return hasArray(items.person);
22
- case "propertyVariable": return hasArray(items.propertyVariable) || hasArray(items.variable);
23
- case "propertyValue": return hasArray(items.propertyValue) || hasArray(items.value);
24
- case "resource": return hasArray(items.resource);
25
- case "text": return hasArray(items.text);
26
- case "set": return hasArray(items.set);
27
- }
28
- }
29
- function mapSortDirectionToXQuery(direction) {
30
- return direction === "desc" ? "descending" : "ascending";
31
- }
32
- function buildStringOrderByClause(direction) {
33
- return `($sortKey = "") ascending, lower-case($sortKey) ${direction}, $position ascending`;
34
- }
35
- function buildTypedOrderByClause(direction) {
36
- return `empty($sortKey) ascending, $sortKey ${direction}, $position ascending`;
37
- }
38
- function buildPropertyValueValuePath(sort) {
39
- return `$item//properties//property[label/@uuid=${stringLiteral(sort.propertyVariableUuid)}]/value[not(@i)]`;
40
- }
41
- function buildPropertyValueStringSortKeyExpression(sort) {
42
- const languageLiteral = stringLiteral(sort.language ?? "eng");
43
- return `string((for $v in ${buildPropertyValueValuePath(sort)}
44
- let $candidate := string-join($v/content[@xml:lang=${languageLiteral}]/string, "")
45
- where string-length($candidate) gt 0
46
- return $candidate)[1])`;
47
- }
48
- function buildPropertyValueTypedSortKeyExpression(parameters) {
49
- const { sort, dataType } = parameters;
50
- const propertyValuePath = buildPropertyValueValuePath(sort);
51
- switch (dataType) {
52
- case "integer": return `(for $v in ${propertyValuePath}
53
- let $candidate := normalize-space(string($v/@rawValue))
54
- where $candidate castable as xs:integer
55
- return xs:integer($candidate))[1]`;
56
- case "decimal":
57
- case "time": return `(for $v in ${propertyValuePath}
58
- let $candidate := normalize-space(string($v/@rawValue))
59
- where $candidate castable as xs:decimal
60
- return xs:decimal($candidate))[1]`;
61
- case "boolean": return `(for $v in ${propertyValuePath}
62
- let $candidate := lower-case(normalize-space(string($v/@rawValue)))
63
- where $candidate = ("true", "false", "1", "0")
64
- return if ($candidate = ("true", "1")) then 1 else 0)[1]`;
65
- case "date": return `(for $v in ${propertyValuePath}
66
- let $candidate := normalize-space(string($v/@rawValue))
67
- where $candidate castable as xs:date
68
- return xs:date($candidate))[1]`;
69
- case "dateTime": return `(for $v in ${propertyValuePath}
70
- let $candidate := normalize-space(string($v/@rawValue))
71
- where $candidate castable as xs:dateTime
72
- return xs:dateTime($candidate))[1]`;
73
- }
74
- }
75
- function buildPropertyValueOrderByClause(parameters) {
76
- const { dataType, direction } = parameters;
77
- return (dataType === "string" || dataType === "IDREF" ? buildStringOrderByClause : buildTypedOrderByClause)(direction);
78
- }
79
- function buildOrderedItemsClause(sort) {
80
- if (sort.target === "none") return "let $orderedItems := $items";
81
- const direction = mapSortDirectionToXQuery(sort.direction);
82
- if (sort.target === "title") return `let $orderedItems :=
83
- for $item at $position in $items
84
- let $sortKey := ${`string-join($item/identification/label/content[@xml:lang=${stringLiteral(sort.language ?? "eng")}]/string, "")`}
85
- stable order by ${buildStringOrderByClause(direction)}
86
- return $item`;
87
- return `let $orderedItems :=
88
- for $item at $position in $items
89
- let $sortKey := ${sort.dataType === "string" || sort.dataType === "IDREF" ? buildPropertyValueStringSortKeyExpression(sort) : buildPropertyValueTypedSortKeyExpression({
90
- sort,
91
- dataType: sort.dataType
92
- })}
93
- stable order by ${buildPropertyValueOrderByClause({
94
- dataType: sort.dataType,
95
- direction
96
- })}
97
- return $item`;
98
- }
99
- /**
100
- * Build an XQuery string to fetch Set items from the OCHRE API
101
- * @param parameters - The parameters for the fetch
102
- * @param parameters.setScopeUuids - An array of Set scope UUIDs to filter by
103
- * @param parameters.belongsToCollectionScopeUuids - An array of collection scope UUIDs to filter by
104
- * @param parameters.queries - Recursive query tree used to filter matching items
105
- * @param parameters.sort - Optional sorting configuration applied before pagination.
106
- * For propertyValue sorting, dataType is required and the sort key uses the first valid leaf value (value[not(@i)]).
107
- * @param parameters.page - The page number (1-indexed)
108
- * @param parameters.pageSize - The number of items per page
109
- * @returns An XQuery string
110
- */
111
- function buildXQuery(parameters) {
112
- const { queries, sort, setScopeUuids, belongsToCollectionScopeUuids, page, pageSize } = parameters;
113
- const startPosition = (page - 1) * pageSize + 1;
114
- return compileSetItemsQuery({
115
- setScopeUuids,
116
- belongsToCollectionScopeUuids,
117
- queries,
118
- body: ({ items, omitSupplemental }) => ` let $totalCount := count(${items})
119
- ${buildOrderedItemsClause(sort)}
120
- let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
121
-
122
- return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
123
- ${omitSupplemental("$pagedItems")}
124
- }</items>`
125
- });
126
- }
127
6
  async function fetchSetItems(parameters, containedItemCategories, options) {
128
7
  try {
129
8
  const { setScopeUuids, belongsToCollectionScopeUuids, queries, sort, page, pageSize } = v.parse(setItemsParametersSchema, parameters);
130
- const requestedLanguages = parseRequestedLanguages(options?.languages);
131
- const output = await requestOchre({
132
- xquery: buildXQuery({
133
- setScopeUuids,
9
+ return {
10
+ ...await fetchContainerItems({
11
+ container: "set",
12
+ scopeUuids: setScopeUuids,
134
13
  belongsToCollectionScopeUuids,
135
14
  queries,
136
15
  sort,
137
16
  page,
138
- pageSize
17
+ pageSize,
18
+ containedItemCategories,
19
+ label: "OCHRE Set items",
20
+ options
139
21
  }),
140
- schema: XMLSetItemsData,
141
- label: "OCHRE Set items",
142
- options
143
- });
144
- if (containedItemCategories != null) {
145
- const missingCategories = containedItemCategories.filter((category) => !hasSetItemsCategory(output.result.ochre.items, category));
146
- if (missingCategories.length > 0) throw new Error(`No Set items found for item categories: ${missingCategories.join(", ")}`, { cause: missingCategories });
147
- }
148
- const languages = resolveContentLanguages(output.result.ochre.items, requestedLanguages);
149
- const items = parseSetItems(output.result.ochre.items, {
150
- containedItemCategories,
151
- languages
152
- });
153
- const itemsByUuid = /* @__PURE__ */ new Map();
154
- for (const item of items) if (!itemsByUuid.has(item.uuid)) itemsByUuid.set(item.uuid, item);
155
- const uniqueItems = itemsByUuid.values().toArray();
156
- return {
157
- totalCount: output.result.ochre.items.totalCount,
158
- page: output.result.ochre.items.page,
159
- pageSize: output.result.ochre.items.pageSize,
160
- items: uniqueItems,
161
22
  error: null,
162
23
  detailedError: null
163
24
  };
@@ -5,7 +5,7 @@ import { MultilingualString } from "../../parsers/multilingual.mjs";
5
5
  import { setPropertyValuesParametersSchema } from "../../schemas.mjs";
6
6
  import { parseXMLContent } from "../../parsers/string.mjs";
7
7
  import { stringLiteral } from "../../xquery.mjs";
8
- import { compileSetItemsQuery, getItemFilterQueries, getPropertyFacetSelectors } from "../../query.mjs";
8
+ import { compileContainerItemsQuery, getItemFilterQueries, getPropertyFacetSelectors } from "../../query.mjs";
9
9
  import * as v from "valibot";
10
10
  //#region src/fetchers/set/property-values.ts
11
11
  function getLabelContentLanguages(content) {
@@ -367,8 +367,9 @@ let $period-values :=
367
367
  }
368
368
  return queryBlocks;
369
369
  }
370
- return compileSetItemsQuery({
371
- setScopeUuids,
370
+ return compileContainerItemsQuery({
371
+ container: "set",
372
+ scopeUuids: setScopeUuids,
372
373
  belongsToCollectionScopeUuids,
373
374
  queries: getItemFilterQueries(queries),
374
375
  declarations: xqueryDeclarations,
@@ -0,0 +1,53 @@
1
+ import { Query, SetItem, SetItemsSort, TreeItemCategory } from "../../types/index.mjs";
2
+ import { FetchBaseOptions, FetchLanguages } from "../../parsers/helpers.mjs";
3
+ //#region src/fetchers/tree/items.d.ts
4
+ type FetchTreeItemsCategory<TContainedItemCategories extends ReadonlyArray<TreeItemCategory> | undefined> = TContainedItemCategories extends ReadonlyArray<infer U> ? Extract<U, TreeItemCategory> : TreeItemCategory;
5
+ /**
6
+ * Fetches and parses Tree items from the OCHRE API
7
+ *
8
+ * The counterpart of {@link fetchSetItems} for a Tree, and the way to read a
9
+ * Tree too large to fetch whole: a Tree of tens of thousands of items is one
10
+ * OCHRE document, so counting, filtering and sorting run inside it rather than
11
+ * over one document per item.
12
+ *
13
+ * Items nested under headings are returned flat, in document order, because a
14
+ * heading groups items for display rather than identifying them. What the
15
+ * returned items carry is whatever the Tree payload carries: OCHRE publishes
16
+ * Tree items as identification-only stubs unless the Tree is published with
17
+ * item properties, so a `property` query or a `propertyValue` sort only works
18
+ * on a Tree that carries them.
19
+ *
20
+ * @param parameters - The parameters for the fetch
21
+ * @param parameters.treeScopeUuids - The Tree scope UUIDs to filter by
22
+ * @param parameters.queries - Recursive query tree used to filter matching items
23
+ * @param parameters.sort - Optional sorting configuration applied before pagination.
24
+ * For propertyValue sorting, dataType is required and the sort key uses the first valid leaf value (value[not(@i)]).
25
+ * @param parameters.page - The page number (1-indexed)
26
+ * @param parameters.pageSize - The number of items per page
27
+ * @param containedItemCategories - The categories of the items to fetch
28
+ * @param options - Options for the fetch
29
+ * @param options.fetch - The fetch function to use
30
+ * @returns The parsed Tree items or null if the fetch/parse fails
31
+ */
32
+ export declare function fetchTreeItems<const TContainedItemCategories extends ReadonlyArray<TreeItemCategory> | undefined = undefined, const TLanguages extends ReadonlyArray<string> | undefined = undefined>(parameters: {
33
+ treeScopeUuids: Array<string>;
34
+ queries?: Query | null;
35
+ sort?: SetItemsSort;
36
+ page: number;
37
+ pageSize?: number;
38
+ }, containedItemCategories?: TContainedItemCategories, options?: FetchBaseOptions<TLanguages>): Promise<{
39
+ totalCount: number;
40
+ page: number;
41
+ pageSize: number;
42
+ items: Array<SetItem<FetchTreeItemsCategory<TContainedItemCategories>, FetchLanguages<TLanguages>>>;
43
+ error: null;
44
+ detailedError: null;
45
+ } | {
46
+ totalCount: null;
47
+ page: null;
48
+ pageSize: null;
49
+ items: null;
50
+ error: string;
51
+ detailedError: string;
52
+ }>;
53
+ //#endregion
@@ -0,0 +1,38 @@
1
+ import { getErrorOutput } from "../../errors.mjs";
2
+ import { treeItemsParametersSchema } from "../../schemas.mjs";
3
+ import { fetchContainerItems } from "../container-items.mjs";
4
+ import * as v from "valibot";
5
+ //#region src/fetchers/tree/items.ts
6
+ async function fetchTreeItems(parameters, containedItemCategories, options) {
7
+ try {
8
+ const { treeScopeUuids, belongsToCollectionScopeUuids, queries, sort, page, pageSize } = v.parse(treeItemsParametersSchema, parameters);
9
+ const { items, ...pagination } = await fetchContainerItems({
10
+ container: "tree",
11
+ scopeUuids: treeScopeUuids,
12
+ belongsToCollectionScopeUuids,
13
+ queries,
14
+ sort,
15
+ page,
16
+ pageSize,
17
+ containedItemCategories,
18
+ label: "OCHRE Tree items",
19
+ options
20
+ });
21
+ return {
22
+ ...pagination,
23
+ items,
24
+ error: null,
25
+ detailedError: null
26
+ };
27
+ } catch (error) {
28
+ return {
29
+ totalCount: null,
30
+ page: null,
31
+ pageSize: null,
32
+ items: null,
33
+ ...getErrorOutput(error, "Failed to fetch Tree items")
34
+ };
35
+ }
36
+ }
37
+ //#endregion
38
+ export { fetchTreeItems };
@@ -53,6 +53,17 @@ export type PropertySelector<T extends LanguageCodes = LanguageCodes> = {
53
53
  * @returns The normalized label
54
54
  */
55
55
  export declare function normalizePropertyVariableLabel(value: string): string;
56
+ /**
57
+ * Keep only the leaf values from an array of property values
58
+ *
59
+ * OCHRE property values form a hierarchy, and a value with children is usually
60
+ * a grouping rather than something to show. This asks that of a values array
61
+ * the caller already holds; the same filter is available on the lookups
62
+ * through `limitToLeafPropertyValues`.
63
+ * @param propertyValues - The values to filter
64
+ * @returns The values that have no children
65
+ */
66
+ export declare function getLeafPropertyValues<T extends LanguageCodes = LanguageCodes>(propertyValues: ReadonlyArray<PropertyValueContent<T>>): Array<PropertyValueContent<T>>;
56
67
  /**
57
68
  * Find the property a selector names
58
69
  *
package/dist/getters.mjs CHANGED
@@ -88,6 +88,16 @@ function findProperty(properties, isMatch, shouldIncludeNestedProperties) {
88
88
  }
89
89
  return null;
90
90
  }
91
+ /**
92
+ * Keep only the leaf values from an array of property values
93
+ *
94
+ * OCHRE property values form a hierarchy, and a value with children is usually
95
+ * a grouping rather than something to show. This asks that of a values array
96
+ * the caller already holds; the same filter is available on the lookups
97
+ * through `limitToLeafPropertyValues`.
98
+ * @param propertyValues - The values to filter
99
+ * @returns The values that have no children
100
+ */
91
101
  function getLeafPropertyValues(propertyValues) {
92
102
  const leafPropertyValues = [];
93
103
  for (const value of propertyValues) if (value.hierarchy.isLeaf) leafPropertyValues.push(value);
@@ -224,4 +234,4 @@ function isPropertyMatchingFilter(property, filter, options = DEFAULT_OPTIONS) {
224
234
  return false;
225
235
  }
226
236
  //#endregion
227
- export { getProperty, getPropertyValue, getPropertyValues, getUniqueProperties, getUniquePropertyVariableLabels, isPropertyMatchingFilter, normalizePropertyVariableLabel };
237
+ export { getLeafPropertyValues, getProperty, getPropertyValue, getPropertyValues, getUniqueProperties, getUniquePropertyVariableLabels, isPropertyMatchingFilter, normalizePropertyVariableLabel };