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 +51 -2
- package/dist/fetchers/container-items.d.mts +52 -0
- package/dist/fetchers/container-items.mjs +171 -0
- package/dist/fetchers/set/items.mjs +9 -148
- package/dist/fetchers/set/property-values.mjs +4 -3
- package/dist/fetchers/tree/items.d.mts +53 -0
- package/dist/fetchers/tree/items.mjs +38 -0
- package/dist/getters.d.mts +11 -0
- package/dist/getters.mjs +11 -1
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +3 -2
- package/dist/parsers/multilingual.d.mts +101 -0
- package/dist/parsers/multilingual.mjs +149 -0
- package/dist/query.d.mts +42 -9
- package/dist/query.mjs +37 -18
- package/dist/schemas.d.mts +28 -4
- package/dist/schemas.mjs +14 -3
- package/dist/types/index.d.mts +39 -0
- package/dist/types/website.d.mts +6 -1
- package/dist/xml/schemas.d.mts +30 -10
- package/dist/xml/schemas.mjs +3 -2
- package/dist/xml/types.d.mts +2 -1
- package/package.json +6 -6
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`,
|
|
181
|
-
`isPropertyMatchingFilter` for reading
|
|
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 {
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 {
|
|
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
|
|
371
|
-
|
|
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 };
|
package/dist/getters.d.mts
CHANGED
|
@@ -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 };
|