aiiinotate 0.5.1 → 0.6.0
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/config/.env.template +3 -0
- package/docs/endpoints.md +34 -18
- package/docs/specifications/2_iiif_apis.md +32 -0
- package/migrations/migrationScripts/20250825185706-collections.js +17 -10
- package/migrations/migrationScripts/20250904080710-annotations2-indexes.js +69 -0
- package/migrations/migrationScripts/20251006212110-manifests2-indexes.js +35 -0
- package/package.json +5 -2
- package/src/data/annotations/annotations2.js +121 -39
- package/src/data/annotations/routes.js +11 -7
- package/src/data/annotations/routes.test.js +38 -30
- package/src/data/manifests/routes.js +0 -1
- package/src/data/routes.js +7 -5
- package/src/data/routes.test.js +16 -1
- package/src/fixtures/annotations.js +2 -0
- package/src/fixtures/data/annotations2SvgValid.jsonld +81 -0
- package/src/fixtures/index.js +2 -1
- package/src/schemas/schemasPresentation2.js +7 -1
- package/src/utils/iiif2Utils.js +158 -49
- package/src/utils/iiif2Utils.test.js +65 -18
- package/src/utils/routeUtils.js +26 -2
- package/src/utils/svg.js +416 -0
- package/src/utils/testUtils.js +32 -4
- package/src/utils/utils.js +97 -1
- package/migrations/migrationScripts/20250826194832-annotations2-canvas-index.js +0 -31
- package/migrations/migrationScripts/20251006212110-manifest-unique-index.js +0 -29
- package/migrations/migrationScripts/20251028115614-annotations2-id-index.js +0 -27
- /package/migrations/migrationScripts/{20250904080710-annotations2-schema.js → 20250826194832-annotations2-schema.js} +0 -0
- /package/migrations/migrationScripts/{20251002141951-manifest2-schema.js → 20251002141951-manifests2-schema.js} +0 -0
package/config/.env.template
CHANGED
|
@@ -14,6 +14,9 @@ AIIINOTATE_HOST=127.0.0.1
|
|
|
14
14
|
# HTTP scheme: HTTP or HTTPS. should be HTTP in dev and in docker
|
|
15
15
|
AIIINOTATE_SCHEME=http
|
|
16
16
|
|
|
17
|
+
# max number of items to display per result page
|
|
18
|
+
PAGE_SIZE=5000
|
|
19
|
+
|
|
17
20
|
# IGNORE
|
|
18
21
|
AIIINOTATE_BASE_URL="$AIIINOTATE_SCHEME://$AIIINOTATE_HOST:$AIIINOTATE_PORT"
|
|
19
22
|
# IGNORE
|
package/docs/endpoints.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Introductory notes
|
|
4
4
|
|
|
5
|
+
### Terminology
|
|
6
|
+
|
|
7
|
+
In the docs below,
|
|
8
|
+
|
|
9
|
+
- `Parameters` describes route parameters (dynamic segments of a route's URL).
|
|
10
|
+
- `Query` describes the query string in a key-value format
|
|
11
|
+
|
|
12
|
+
### IIIF Version
|
|
13
|
+
|
|
5
14
|
**aiiinotate** is meant to be able to handle both IIIF presentation APIs: the most common [2.x](https://iiif.io/api/presentation/2.1) and the more recent [3.x](https://iiif.io/api/presentation/3.0). Both APIs define a data structure for manifests, annotations, lists of annotations and collections of manifests.
|
|
6
15
|
|
|
7
16
|
**HOWEVER, in aiiinotate, IIIF Presentation v2 and v3 data are isolated**: they form two separate collections, and no conversion is done between IIIF 2.x and 3.x data. This means that:
|
|
@@ -30,10 +39,10 @@ Implementation of the [IIIF Search API](https://iiif.io/api/search/2.0/), to sea
|
|
|
30
39
|
|
|
31
40
|
#### Request
|
|
32
41
|
|
|
33
|
-
-
|
|
42
|
+
- Parameters:
|
|
34
43
|
- `iiif_version` (`2 | 3`): the IIIF aearch API version. 2 is for IIIF Presentation API 3.x, 1 is for IIIF Presentation API 2.x
|
|
35
44
|
- `manifest_short_id` (`string`): the ID of the manifest. See the *IIIF URIs* section.
|
|
36
|
-
-
|
|
45
|
+
- Query:
|
|
37
46
|
- `q` (`string`): query string.
|
|
38
47
|
- if `iiif_version=1`, `q` is searched in the fields: `@id`, `resource.@id` or `resource.chars` fields
|
|
39
48
|
- `motivation` (`painting | non-painting | commenting | describing | tagging | linking`): values for the `motivation` field of an annotation
|
|
@@ -41,7 +50,9 @@ Implementation of the [IIIF Search API](https://iiif.io/api/search/2.0/), to sea
|
|
|
41
50
|
- `canvasMax` (`number`): a positive integer
|
|
42
51
|
- `canvasMax` must be greater than `canvasMin`
|
|
43
52
|
- if `canvasMax` is undefined, then we will only return the annotations that target a canvas at the `canvasMin` position in its manifest.
|
|
44
|
-
- `
|
|
53
|
+
- `page` (`number`): results are paginated. This specifies the page number
|
|
54
|
+
- `pageSize` (`number`): number of annotations to display per page. Defaults to `process.env.PAGE_SIZE`.
|
|
55
|
+
- `onlyIds` (`boolean`): return just the value of `@id` fields of matched annotations as a `string[]` instead of returning all the annotations. If `onlyIds=true`, there is no pagination, `page` and `pageSize` won't have any effect.
|
|
45
56
|
|
|
46
57
|
#### Response
|
|
47
58
|
|
|
@@ -73,10 +84,10 @@ DELETE /{collection_name}/{iiif_version}/delete
|
|
|
73
84
|
|
|
74
85
|
#### Request
|
|
75
86
|
|
|
76
|
-
-
|
|
87
|
+
- Parameters:
|
|
77
88
|
- `collection_name` (`annotations | manifests`): delete an annotation or a manifest
|
|
78
89
|
- `iiif_version` (`2 | 3`): IIIF presentation version
|
|
79
|
-
-
|
|
90
|
+
- Query:
|
|
80
91
|
- if `collection_name = manifests`:
|
|
81
92
|
- `uri`: the full URI of the manifest to delete
|
|
82
93
|
- `manifestShortId`: the manifest's identifier
|
|
@@ -105,7 +116,7 @@ Returns a Collection of all manifests in your **aiiinotate** instance.
|
|
|
105
116
|
|
|
106
117
|
#### Request
|
|
107
118
|
|
|
108
|
-
-
|
|
119
|
+
- Parameters:
|
|
109
120
|
- `iiif_version` (`2 | 3`): the IIIF Presentation API version
|
|
110
121
|
|
|
111
122
|
#### Response
|
|
@@ -122,7 +133,7 @@ POST /manifests/{iiif_version}/create
|
|
|
122
133
|
|
|
123
134
|
#### Request
|
|
124
135
|
|
|
125
|
-
-
|
|
136
|
+
- Parameters:
|
|
126
137
|
- `iiif_version` (`2 | 3`): the IIIF Presentation API version of your manifest
|
|
127
138
|
- Body (`JSON`): the manifest to index in the database
|
|
128
139
|
|
|
@@ -152,15 +163,20 @@ GET /annotations/{iiif_version}/search
|
|
|
152
163
|
|
|
153
164
|
#### Request
|
|
154
165
|
|
|
155
|
-
- Variables:
|
|
156
|
-
- `iiif_version` (`2 | 3`): the IIIF Presentation API of your manifests
|
|
157
166
|
- Parameters:
|
|
158
|
-
- `
|
|
159
|
-
|
|
167
|
+
- `iiif_version` (`2 | 3`): the IIIF Presentation API of your manifests
|
|
168
|
+
- Query:
|
|
169
|
+
- `canvasUri` (`string`): the URI of the target canvas
|
|
170
|
+
- `page` (`number`): results are paginated. Specifies the page number.
|
|
171
|
+
- `pageSize` (`number`): number of items per page. Defaults to `process.env.PAGE_SIZE`.
|
|
160
172
|
|
|
161
173
|
#### Response
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
Results are paginated.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
AnnotationList | AnnotationPage
|
|
179
|
+
```
|
|
164
180
|
|
|
165
181
|
---
|
|
166
182
|
|
|
@@ -172,9 +188,9 @@ GET /annotations/{iiif_version}/count
|
|
|
172
188
|
|
|
173
189
|
#### Request
|
|
174
190
|
|
|
175
|
-
- Variables:
|
|
176
|
-
- `iiif_version` (`2 | 3`): the IIIF Presentation API of your manifests
|
|
177
191
|
- Parameters:
|
|
192
|
+
- `iiif_version` (`2 | 3`): the IIIF Presentation API of your manifests
|
|
193
|
+
- Query:
|
|
178
194
|
- `uri` (`string`): the annotation's `@id`
|
|
179
195
|
- `canvasUri` (`string`): the annotation's target canvas (`on.full`)
|
|
180
196
|
- `manifestShortId` (`string`): the short ID of the annotation's target manifest (`on.manifestShortId`)
|
|
@@ -197,7 +213,7 @@ This route allows to query an annotation by its ID by defering its `@id | id` fi
|
|
|
197
213
|
|
|
198
214
|
#### Request
|
|
199
215
|
|
|
200
|
-
-
|
|
216
|
+
- Parameters:
|
|
201
217
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
202
218
|
- `manifest_short_id` (`string`): the identifier of the manifest the annotation is related to
|
|
203
219
|
- `annotation_short_id`: the unique part of the annotation URL
|
|
@@ -218,10 +234,10 @@ Create or update a single annotation
|
|
|
218
234
|
|
|
219
235
|
#### Request
|
|
220
236
|
|
|
221
|
-
-
|
|
237
|
+
- Parameters:
|
|
222
238
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
223
239
|
- `action` (`create | update`): the action to perform: create or update an annotation
|
|
224
|
-
-
|
|
240
|
+
- Query:
|
|
225
241
|
- `throwOnCanvasIndexError` (`boolean`): if there is an error fetching the related manifest, or getting a target canvas' index, throw an error.
|
|
226
242
|
- Body (`Object`): a IIIF annotation that follows the IIIF Presentation API 2 or 3 (depending on the value of `iiif_version`)
|
|
227
243
|
|
|
@@ -263,7 +279,7 @@ Batch insert multiple annotations.
|
|
|
263
279
|
|
|
264
280
|
#### Request
|
|
265
281
|
|
|
266
|
-
-
|
|
282
|
+
- Query:
|
|
267
283
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
268
284
|
- Body: either:
|
|
269
285
|
- a full `AnnotationList | AnnotationPage` embedded in the body (type must match `iiif_version`: AnnotationPage for IIIF 3, AnnotationList for IIIF 2).
|
|
@@ -394,3 +394,35 @@ Properties of the annotation list are:
|
|
|
394
394
|
}
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
+
### Pagination
|
|
398
|
+
|
|
399
|
+
A paginated resource can be for example a `Collection` or an `AnnotationList`.
|
|
400
|
+
|
|
401
|
+
- A paginated Resource (i.e., a `Collection`)
|
|
402
|
+
- MUST use `first` to link to the first page
|
|
403
|
+
- MUST NOT embed the corresponding list that is being pagiated
|
|
404
|
+
- A page (i.e., a single page of an `AnnotationList`)
|
|
405
|
+
- SHOULD use `within` to refer to its container resource
|
|
406
|
+
- MUST use `next` to provide a link to the next page, if it exists
|
|
407
|
+
- SHOULD use `prev` to provide a link to the previous page, if it exists
|
|
408
|
+
- MAY use `total` to list the total number of resources contained in all pages
|
|
409
|
+
- MAY use `startIndex` to document the index of the first item of the current page, starting from 0 (i.e., if you're on page 3 and there are 100 annotations per page, `startIndex: 300`)
|
|
410
|
+
- MAY inherit descriptive properties from the paged resource (i.e. the `logo` or `attribution`).
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
// the first page of an annotation list
|
|
414
|
+
{
|
|
415
|
+
"@context": "http://iiif.io/api/presentation/2/context.json",
|
|
416
|
+
"@id": "http://example.org/iiif/book1/list/l1",
|
|
417
|
+
"@type": "sc:AnnotationList",
|
|
418
|
+
|
|
419
|
+
"startIndex": 0,
|
|
420
|
+
"within": "http://example.org/iiif/book1/layer/transcription",
|
|
421
|
+
"next": "http://example.org/iiif/book1/list/l2",
|
|
422
|
+
|
|
423
|
+
"resources": [
|
|
424
|
+
// Annotations live here ...
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - manifests2 : IIIF manifests following the IIIF 2.1 (and 2.0) presentation API
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { inspectObj } from "#utils/utils.js";
|
|
10
|
+
|
|
9
11
|
const collectionNames = [
|
|
10
12
|
"annotations3",
|
|
11
13
|
"annotations2",
|
|
@@ -19,10 +21,16 @@ const collectionNames = [
|
|
|
19
21
|
* @returns {Promise<void>}
|
|
20
22
|
*/
|
|
21
23
|
export const up = async (db, client) => {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
db.
|
|
25
|
-
|
|
24
|
+
// check if a collection exists before recreating it, otherwise you get NameSpaceExists errors.
|
|
25
|
+
const existingCollectionNames =
|
|
26
|
+
( await db.listCollections().toArray() ).map(coll => coll.name);
|
|
27
|
+
|
|
28
|
+
// create a mongo collection: https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script
|
|
29
|
+
for (const colName of collectionNames ) {
|
|
30
|
+
if ( !existingCollectionNames.includes(colName) ) {
|
|
31
|
+
db.createCollection(colName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
26
34
|
};
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -31,11 +39,10 @@ export const up = async (db, client) => {
|
|
|
31
39
|
* @returns {Promise<void>}
|
|
32
40
|
*/
|
|
33
41
|
export const down = async (db, client) => {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
42
|
+
// NOTE : here, `down` does NOT revert the migration: it would delete the collections and their contents, which we don't want. it's ok since this is the first migration.
|
|
43
|
+
// collectionNames.forEach(async (colName) => {
|
|
44
|
+
// const collection = db.collection(colName);
|
|
45
|
+
// await collection.drop();
|
|
46
|
+
// })
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create and manage indexes for the collection `annotations2`
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {createIndex, removeIndex} from "../manageIndex.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// all filters on arrays are MultiKey index => it NOT a Sort index, but an Equality index (useful for filters)
|
|
9
|
+
const indexes = [
|
|
10
|
+
{
|
|
11
|
+
colName: "annotations2",
|
|
12
|
+
indexSpec: { "@id": 1 },
|
|
13
|
+
indexOptions: { name: "annotationIdIndex" }
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
colName: "annotations2",
|
|
17
|
+
indexSpec: { "on.full": 1 },
|
|
18
|
+
indexOptions: { name: "canvasIdIndex" }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
colName: "annotations2",
|
|
22
|
+
indexSpec: { "on.manifestUri": 1 },
|
|
23
|
+
indexOptions: { name: "manifestIdIndex" }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
colName: "annotations2",
|
|
27
|
+
indexSpec: { "on.manifestShortId": 1 },
|
|
28
|
+
indexOptions: { name: "manifestShortIdIndex" }
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
colName: "annotations2",
|
|
32
|
+
indexSpec: { "on.canvasIdx": 1 },
|
|
33
|
+
indexOptions: { name: "canvasIdxIndex" }
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
colName: "annotations2",
|
|
37
|
+
indexSpec: { "on.resource.@id": 1 },
|
|
38
|
+
indexOptions: { name: "resourceIdIndex" }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
colName: "annotations2",
|
|
42
|
+
indexSpec: { "on.resource.chars": 1 },
|
|
43
|
+
indexOptions: { name: "resourceCharsIndex" }
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('mongodb').Db} db
|
|
49
|
+
* @param {import('mongodb').MongoClient} client
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
export const up = async (db, client) => {
|
|
53
|
+
for ( const { colName, indexSpec, indexOptions } of indexes ) {
|
|
54
|
+
const result = await createIndex(db, colName, indexSpec, indexOptions);
|
|
55
|
+
console.log("created index:", result);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('mongodb').Db} db
|
|
61
|
+
* @param {import('mongodb').MongoClient} client
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export const down = async (db, client) => {
|
|
65
|
+
for ( const { colName, indexSpec, indexOptions } of indexes ) {
|
|
66
|
+
const result = await removeIndex(db, colName, indexOptions);
|
|
67
|
+
console.log("dropped index:", result);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** create indexes for manifests2 collection */
|
|
2
|
+
|
|
3
|
+
import {createIndex, removeIndex} from "../manageIndex.js";
|
|
4
|
+
|
|
5
|
+
const indexes = [
|
|
6
|
+
{
|
|
7
|
+
colName: "manifests2",
|
|
8
|
+
indexSpec: { "@id": 1 },
|
|
9
|
+
indexOptions: { name: "manifestIdIndex", unique: true }
|
|
10
|
+
},
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('mongodb').Db} db
|
|
15
|
+
* @param {import('mongodb').MongoClient} client
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
export const up = async (db, client) => {
|
|
19
|
+
for ( const { colName, indexSpec, indexOptions } of indexes ) {
|
|
20
|
+
const result = await createIndex(db, colName, indexSpec, indexOptions);
|
|
21
|
+
console.log("created index:", result);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('mongodb').Db} db
|
|
27
|
+
* @param {import('mongodb').MongoClient} client
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
export const down = async (db, client) => {
|
|
31
|
+
for ( const { colName, indexSpec, indexOptions } of indexes ) {
|
|
32
|
+
const result = await removeIndex(db, colName, indexOptions);
|
|
33
|
+
console.log("dropped index:", result);
|
|
34
|
+
}
|
|
35
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiiinotate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "a fast IIIF-compliant annotation server",
|
|
5
5
|
"main": "./cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test": "export AIIINOTATE_TESTING=true; sudo systemctl start mongod && dotenvx run -f ./config/.env -- node --test --test-isolation=none",
|
|
19
19
|
"lint": "npx eslint --fix",
|
|
20
20
|
"migrate": "npm run cli -- migrate",
|
|
21
|
-
"update_version": "python3 scripts/update_version.py"
|
|
21
|
+
"update_version": "python3 scripts/update_version.py",
|
|
22
|
+
"get_version": "python3 scripts/get_version.py"
|
|
22
23
|
},
|
|
23
24
|
"pre-commit": [
|
|
24
25
|
"lint"
|
|
@@ -61,9 +62,11 @@
|
|
|
61
62
|
"@fastify/mongodb": "^9.0.2",
|
|
62
63
|
"@fastify/swagger": "^9.5.2",
|
|
63
64
|
"commander": "^14.0.0",
|
|
65
|
+
"fast-xml-parser": "^5.3.3",
|
|
64
66
|
"fastify": "^5.5.0",
|
|
65
67
|
"migrate-mongo": "^12.1.3",
|
|
66
68
|
"mongodb": "^6.18.0",
|
|
69
|
+
"svg-path-bbox": "^2.1.0",
|
|
67
70
|
"swagger-markdown": "^3.0.0",
|
|
68
71
|
"uuid": "^11.1.0"
|
|
69
72
|
},
|
|
@@ -6,10 +6,11 @@ import fastifyPlugin from "fastify-plugin";
|
|
|
6
6
|
|
|
7
7
|
import CollectionAbstract from "#data/collectionAbstract.js";
|
|
8
8
|
import { IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
|
|
9
|
-
import { ajvCompile, objectHasKey, isNullish, maybeToArray, inspectObj, visibleLog } from "#utils/utils.js";
|
|
9
|
+
import { ajvCompile, objectHasKey, isNullish, maybeToArray, inspectObj, visibleLog, memoize } from "#utils/utils.js";
|
|
10
10
|
import { getManifestShortId, makeTarget, makeAnnotationId, toAnnotationList, canvasUriToManifestUri } from "#utils/iiif2Utils.js";
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
/** @typedef {import("mongodb").FindCursor} FindCursor */
|
|
13
14
|
/** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
|
|
14
15
|
/** @typedef {import("#types").MongoObjectId} MongoObjectId */
|
|
15
16
|
/** @typedef {import("#types").MongoInsertResultType} MongoInsertResultType */
|
|
@@ -58,6 +59,15 @@ class Annotations2 extends CollectionAbstract {
|
|
|
58
59
|
////////////////////////////////////////////////////////////////
|
|
59
60
|
// utils
|
|
60
61
|
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @type {() => Promise<number>}
|
|
65
|
+
* cache the number of documents corresponding to a paginated query in a JS cache
|
|
66
|
+
* a simple cache avoids rerunning a count to get the total number of documents for each page of a paginated query
|
|
67
|
+
* see: https://dev.to/codewithjohnson/the-power-of-a-simple-cache-system-with-javascript-map-3j01
|
|
68
|
+
*/
|
|
69
|
+
#memoizePaginationTotalCount = memoize((x) => this.collection.countDocuments(x), 2000);
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* expand a pair of `filterKey`, `filterVal` following the schema `routeAnnotationFilter` into a proper filter for the `annotations2` collection.
|
|
63
73
|
* @param {string} filterKey
|
|
@@ -93,7 +103,6 @@ class Annotations2 extends CollectionAbstract {
|
|
|
93
103
|
|
|
94
104
|
// OA stores Textual Body content in `cnt:chars`, IIIF uses `chars`. `value` is sometimes also used
|
|
95
105
|
resource.chars = resource.value || resource["cnt:chars"] || resource.chars; // may be undefined
|
|
96
|
-
// delete the alternate keys
|
|
97
106
|
[ "value", "cnt:chars" ].map((k) => {
|
|
98
107
|
if ( Object.keys(resource).includes(k) ) {
|
|
99
108
|
delete resource[k];
|
|
@@ -123,11 +132,11 @@ class Annotations2 extends CollectionAbstract {
|
|
|
123
132
|
* @param {boolean} update - set to `true` if performing an update instead of an insert.
|
|
124
133
|
* @returns {object}
|
|
125
134
|
*/
|
|
126
|
-
#cleanAnnotation(annotation, update=false) {
|
|
135
|
+
async #cleanAnnotation(annotation, update=false) {
|
|
127
136
|
// 1) extract ids and targets. convert the target to an array.
|
|
137
|
+
// we assume that all values of `annotationTargetArray` point to the same manifest => `manifestShortId` is extracted from the 1st target
|
|
128
138
|
const
|
|
129
|
-
annotationTargetArray = makeTarget(annotation),
|
|
130
|
-
// we assume that all values of `annotationTargetArray` point to the same manifest => take the manifest of the 1st target
|
|
139
|
+
annotationTargetArray = await makeTarget(annotation),
|
|
131
140
|
manifestShortId = annotationTargetArray[0].manifestShortId;
|
|
132
141
|
|
|
133
142
|
// in updates, "@id" has aldready been extracted
|
|
@@ -176,13 +185,17 @@ class Annotations2 extends CollectionAbstract {
|
|
|
176
185
|
* @param {object} annotationList
|
|
177
186
|
* @returns {object[]}
|
|
178
187
|
*/
|
|
179
|
-
#cleanAnnotationList(annotationList) {
|
|
188
|
+
async #cleanAnnotationList(annotationList) {
|
|
180
189
|
// NOTE: if `this.#cleanAnnotationList` can only be accessed from annotations routes, then this check is useless (has aldready been performed).
|
|
181
190
|
if ( this.validatorAnnotationList(annotationList) ) {
|
|
182
191
|
this.errorNoAction("Annotations2.#cleanAnnotationList: could not recognize AnnotationList. see: https://iiif.io/api/presentation/2.1/#annotation-list.", annotationList)
|
|
183
192
|
}
|
|
184
193
|
//NOTE: using an arrow function is necessary to avoid losing the scope of `this`. otherwise, `this` is undefined in `#cleanAnnotation`.
|
|
185
|
-
return
|
|
194
|
+
return await Promise.all(
|
|
195
|
+
annotationList.resources.map(async (ressource) =>
|
|
196
|
+
await this.#cleanAnnotation(ressource)
|
|
197
|
+
)
|
|
198
|
+
)
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
/**
|
|
@@ -214,10 +227,6 @@ class Annotations2 extends CollectionAbstract {
|
|
|
214
227
|
/** @type {string[]} concatenation of ids of newly inserted manifests and previously inserted manifests. */
|
|
215
228
|
insertedManifestsIds = insertResponse.insertedIds.concat(insertResponse.preExistingIds || []);
|
|
216
229
|
|
|
217
|
-
if ( throwOnCanvasIndexError && insertResponse.fetchErrorIds.length ) {
|
|
218
|
-
visibleLog("THIS SHOULD NOT HAPPEN")
|
|
219
|
-
}
|
|
220
|
-
|
|
221
230
|
// 3. update annotations with info on manifest and canvas.
|
|
222
231
|
// if canvasIdx is undefined, throw.
|
|
223
232
|
annotationData = await Promise.all(
|
|
@@ -252,6 +261,57 @@ class Annotations2 extends CollectionAbstract {
|
|
|
252
261
|
: annotationData;
|
|
253
262
|
}
|
|
254
263
|
|
|
264
|
+
/**
|
|
265
|
+
* taking a filter document `queryFilter`, return an annotationList with paginated results
|
|
266
|
+
*
|
|
267
|
+
* params:
|
|
268
|
+
* - queryUrl: the `@id` of the annotationList.
|
|
269
|
+
* MUST be an URL to a route that sjupports pagination, in order to set `prev` and `next` in the annotationList.
|
|
270
|
+
* - queryFilter: the filter to apply to the collection
|
|
271
|
+
* - page: current page number
|
|
272
|
+
* - pageSize: number of annotations per page
|
|
273
|
+
* - label: title of the AnnotationList.
|
|
274
|
+
*
|
|
275
|
+
* NOTE: other/more performant forms of pagination than offset: https://medium.com/mongodb/mongodb-pagination-offset-based-vs-keyset-vs-pre-generated-result-pages-4177e05d88ec
|
|
276
|
+
*
|
|
277
|
+
* @param {{
|
|
278
|
+
* queryUrl: string,
|
|
279
|
+
* queryFilter: object,
|
|
280
|
+
* page: number,
|
|
281
|
+
* pageSize: number,
|
|
282
|
+
* label: string?
|
|
283
|
+
* }}
|
|
284
|
+
* @returns {object} - paginated annotation list
|
|
285
|
+
*/
|
|
286
|
+
async #paginate({
|
|
287
|
+
queryUrl,
|
|
288
|
+
queryFilter,
|
|
289
|
+
page=1,
|
|
290
|
+
pageSize=process.env.PAGE_SIZE,
|
|
291
|
+
label=undefined
|
|
292
|
+
}) {
|
|
293
|
+
const totalCount = await this.#memoizePaginationTotalCount(queryFilter);
|
|
294
|
+
|
|
295
|
+
const skip = Math.max((page-1) * pageSize, 0); // number of queried items up until the previous page included.
|
|
296
|
+
|
|
297
|
+
const cursor = await this.find(queryFilter, {}, true);
|
|
298
|
+
const annotations = await cursor
|
|
299
|
+
.sort({ "@id": 1 })
|
|
300
|
+
.skip(skip)
|
|
301
|
+
.limit(pageSize)
|
|
302
|
+
.toArray();
|
|
303
|
+
|
|
304
|
+
const hasNext = page * pageSize <= totalCount;
|
|
305
|
+
|
|
306
|
+
return toAnnotationList({
|
|
307
|
+
resources: annotations,
|
|
308
|
+
annotationListId: queryUrl,
|
|
309
|
+
page: page,
|
|
310
|
+
hasNext: hasNext,
|
|
311
|
+
label: label
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
255
315
|
////////////////////////////////////////////////////////////////
|
|
256
316
|
// insert / updates
|
|
257
317
|
|
|
@@ -267,7 +327,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
267
327
|
* @returns {Promise<InsertResponseType>}
|
|
268
328
|
*/
|
|
269
329
|
async insertAnnotation(annotation, throwOnCanvasIndexError=false) {
|
|
270
|
-
annotation = this.#cleanAnnotation(annotation);
|
|
330
|
+
annotation = await this.#cleanAnnotation(annotation);
|
|
271
331
|
annotation = await this.#insertManifestsAndGetCanvasIdx(annotation, throwOnCanvasIndexError);
|
|
272
332
|
return this.insertOne(annotation);
|
|
273
333
|
}
|
|
@@ -280,7 +340,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
280
340
|
*/
|
|
281
341
|
async updateAnnotation(annotation) {
|
|
282
342
|
// necessary: on insert, the `@id` received is modified by `this.#cleanAnnotationList`.
|
|
283
|
-
annotation = this.#cleanAnnotation(annotation, true);
|
|
343
|
+
annotation = await this.#cleanAnnotation(annotation, true);
|
|
284
344
|
const
|
|
285
345
|
query = { "@id": annotation["@id"] },
|
|
286
346
|
update = { $set: annotation };
|
|
@@ -300,7 +360,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
300
360
|
*/
|
|
301
361
|
async insertAnnotationList(annotationList, throwOnCanvasIndexError) {
|
|
302
362
|
let annotationArray;
|
|
303
|
-
annotationArray = this.#cleanAnnotationList(annotationList);
|
|
363
|
+
annotationArray = await this.#cleanAnnotationList(annotationList);
|
|
304
364
|
annotationArray = await this.#insertManifestsAndGetCanvasIdx(annotationArray, throwOnCanvasIndexError);
|
|
305
365
|
return this.insertMany(annotationArray);
|
|
306
366
|
}
|
|
@@ -331,11 +391,12 @@ class Annotations2 extends CollectionAbstract {
|
|
|
331
391
|
* about projection: 0 removes the fields from the response, 1 incldes it (but exclude all others)
|
|
332
392
|
* see: https://www.mongodb.com/docs/drivers/node/current/crud/query/project/#std-label-node-project
|
|
333
393
|
* https://stackoverflow.com/questions/74447979/mongoservererror-cannot-do-exclusion-on-field-date-in-inclusion-projection
|
|
334
|
-
* @param {object} queryObj
|
|
394
|
+
* @param {object} queryObj - the filter document
|
|
335
395
|
* @param {object?} projectionObj - extra projection fields to tailor the reponse format
|
|
336
|
-
* @
|
|
396
|
+
* @param {boolean} asCursor - return a cursor instead of an array of results
|
|
397
|
+
* @returns {Promise<object[] | FindCursor>}
|
|
337
398
|
*/
|
|
338
|
-
async find(queryObj, projectionObj={}) {
|
|
399
|
+
async find(queryObj, projectionObj={}, asCursor=false) {
|
|
339
400
|
// 1. construct the final projection object, knowing that we can't mix exclusive and inclusive projectin.
|
|
340
401
|
// presence of `_id` will not cause projections to fail => remove it from values.
|
|
341
402
|
const projectionValues =
|
|
@@ -362,9 +423,12 @@ class Annotations2 extends CollectionAbstract {
|
|
|
362
423
|
}
|
|
363
424
|
|
|
364
425
|
// 2. find, project and return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.toArray()
|
|
426
|
+
const cursor = this.collection.find(queryObj, { projection: projectionObj });
|
|
427
|
+
if ( !asCursor ) {
|
|
428
|
+
return cursor.toArray()
|
|
429
|
+
} else {
|
|
430
|
+
return cursor;
|
|
431
|
+
};
|
|
368
432
|
}
|
|
369
433
|
|
|
370
434
|
/**
|
|
@@ -400,6 +464,8 @@ class Annotations2 extends CollectionAbstract {
|
|
|
400
464
|
* canvasMin: number?,
|
|
401
465
|
* canvasMax: number?,
|
|
402
466
|
* onlyIds: boolean
|
|
467
|
+
* page: number
|
|
468
|
+
* pageSize: number
|
|
403
469
|
* }}
|
|
404
470
|
* @returns {object} annotationList containing results
|
|
405
471
|
*/
|
|
@@ -410,15 +476,17 @@ class Annotations2 extends CollectionAbstract {
|
|
|
410
476
|
motivation=undefined,
|
|
411
477
|
canvasMin=undefined,
|
|
412
478
|
canvasMax=undefined,
|
|
413
|
-
onlyIds=false
|
|
479
|
+
onlyIds=false,
|
|
480
|
+
page=1,
|
|
481
|
+
pageSize=process.env.PAGE_SIZE
|
|
414
482
|
}) {
|
|
415
483
|
const
|
|
416
|
-
|
|
417
|
-
|
|
484
|
+
filtersBase = { "on.manifestShortId": manifestShortId },
|
|
485
|
+
filtersExtra = { $and: [] };
|
|
418
486
|
|
|
419
487
|
// expand query parameters
|
|
420
488
|
if ( q ) {
|
|
421
|
-
|
|
489
|
+
filtersExtra.$and.push({
|
|
422
490
|
$or: [
|
|
423
491
|
{ "@id": q },
|
|
424
492
|
{ "resource.@id": q },
|
|
@@ -427,7 +495,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
427
495
|
});
|
|
428
496
|
};
|
|
429
497
|
if ( motivation ) {
|
|
430
|
-
|
|
498
|
+
filtersExtra.$and.push(
|
|
431
499
|
motivation === "non-painting"
|
|
432
500
|
? { motivation: { $ne: "sc:painting" } }
|
|
433
501
|
: motivation === "painting"
|
|
@@ -438,10 +506,10 @@ class Annotations2 extends CollectionAbstract {
|
|
|
438
506
|
if ( !isNaN(canvasMin) ) {
|
|
439
507
|
// if canvasMax is undefined, then search for canvasIdx===canvasMin
|
|
440
508
|
if ( !canvasMax ) {
|
|
441
|
-
|
|
509
|
+
filtersExtra.$and.push({ "on.canvasIdx": canvasMin })
|
|
442
510
|
// if canvasMin and canvasMax, canvasIdx must be in [canvasMin, canvasMax] (inclusive).
|
|
443
511
|
} else {
|
|
444
|
-
|
|
512
|
+
filtersExtra.$and.push({
|
|
445
513
|
$and: [
|
|
446
514
|
{ "on.canvasIdx": { $gte: canvasMin } },
|
|
447
515
|
{ "on.canvasIdx": { $lte: canvasMax } }
|
|
@@ -449,16 +517,22 @@ class Annotations2 extends CollectionAbstract {
|
|
|
449
517
|
})
|
|
450
518
|
}
|
|
451
519
|
}
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
? { ...
|
|
455
|
-
:
|
|
520
|
+
const queryFilter =
|
|
521
|
+
filtersExtra.$and.length
|
|
522
|
+
? { ...filtersBase, ...filtersExtra }
|
|
523
|
+
: filtersBase;
|
|
456
524
|
|
|
457
525
|
if ( !onlyIds ) {
|
|
458
|
-
|
|
459
|
-
|
|
526
|
+
return await this.#paginate({
|
|
527
|
+
queryUrl,
|
|
528
|
+
queryFilter,
|
|
529
|
+
page,
|
|
530
|
+
pageSize,
|
|
531
|
+
label: `Paginated annotation List (page: ${page}, ${pageSize} items per page)`
|
|
532
|
+
})
|
|
460
533
|
} else {
|
|
461
|
-
|
|
534
|
+
// NOTE: there is no pagination if `onlyIds` is true
|
|
535
|
+
return ( await this.find(queryFilter, { "@id": 1 }) )
|
|
462
536
|
.map((ann) => ann["@id"]);
|
|
463
537
|
}
|
|
464
538
|
}
|
|
@@ -469,11 +543,19 @@ class Annotations2 extends CollectionAbstract {
|
|
|
469
543
|
* @param {boolean} asAnnotationList
|
|
470
544
|
* @returns
|
|
471
545
|
*/
|
|
472
|
-
async findByCanvasUri(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
546
|
+
async findByCanvasUri({
|
|
547
|
+
queryUrl,
|
|
548
|
+
canvasUri,
|
|
549
|
+
page=1,
|
|
550
|
+
pageSize=process.env.PAGE_SIZE
|
|
551
|
+
}) {
|
|
552
|
+
return this.#paginate({
|
|
553
|
+
queryUrl,
|
|
554
|
+
queryFilter: { "on.full": canvasUri },
|
|
555
|
+
label: `annotations targeting canvas ${canvasUri}`,
|
|
556
|
+
page,
|
|
557
|
+
pageSize
|
|
558
|
+
});
|
|
477
559
|
}
|
|
478
560
|
|
|
479
561
|
/**
|