aiiinotate 0.4.3 → 0.5.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/README.md +5 -1
- package/docs/endpoints.md +36 -14
- package/docs/todo.md +19 -0
- package/package.json +8 -6
- package/src/data/annotations/annotations2.js +74 -30
- package/src/data/annotations/routes.js +27 -3
- package/src/data/annotations/routes.test.js +30 -2
- package/src/data/collectionAbstract.js +3 -3
- package/src/data/manifests/manifests2.js +76 -33
- package/src/data/manifests/routes.js +28 -0
- package/src/data/routes.js +31 -10
- package/src/data/routes.test.js +46 -11
- package/src/fixtures/generate.js +180 -0
- package/src/fixtures/index.js +6 -2
- package/src/schemas/schemasPresentation2.js +13 -0
- package/src/{data/utils → utils}/routeUtils.js +7 -6
- package/src/{data/utils → utils}/testUtils.js +9 -1
- package/test.json +0 -77
- /package/src/{data/utils → utils}/iiif2Utils.js +0 -0
- /package/src/{data/utils → utils}/iiif2Utils.test.js +0 -0
- /package/src/{data/utils → utils}/iiif3Utils.js +0 -0
- /package/src/{data/utils → utils}/iiifUtils.js +0 -0
- /package/src/{data/utils → utils}/utils.js +0 -0
package/README.md
CHANGED
|
@@ -105,7 +105,11 @@ Remember to have your `mongodb` service running: `sudo systemctl start mongod` !
|
|
|
105
105
|
- **Start the app**
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
|
|
108
|
+
# reload enabled
|
|
109
|
+
npm run dev
|
|
110
|
+
|
|
111
|
+
# reload disabled
|
|
112
|
+
npm run prod
|
|
109
113
|
```
|
|
110
114
|
|
|
111
115
|
- **Test the app**
|
package/docs/endpoints.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- your data must match the `iiif_version` argument
|
|
13
13
|
- if you insert an Annotation following the API v3.x, you can't search for it using `iiif_version=2`.
|
|
14
14
|
|
|
15
|
-
This is because
|
|
15
|
+
This is because
|
|
16
16
|
- the IIIF standard is quite complex and there are breaking changes between v2 and v3
|
|
17
17
|
- handling conversions between v2 and v3 is error prone, would increase calculations and slow the app down
|
|
18
18
|
|
|
@@ -32,20 +32,36 @@ Implementation of the [IIIF Search API](https://iiif.io/api/search/2.0/), to sea
|
|
|
32
32
|
|
|
33
33
|
- Variables:
|
|
34
34
|
- `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
|
-
- `manifest_short_id` (`string`): the ID of the manifest. See the *IIIF URIs* section.
|
|
35
|
+
- `manifest_short_id` (`string`): the ID of the manifest. See the *IIIF URIs* section.
|
|
36
36
|
- Parameters:
|
|
37
|
-
- `q` (`string`): query string.
|
|
37
|
+
- `q` (`string`): query string.
|
|
38
38
|
- if `iiif_version=1`, `q` is searched in the fields: `@id`, `resource.@id` or `resource.chars` fields
|
|
39
39
|
- `motivation` (`painting | non-painting | commenting | describing | tagging | linking`): values for the `motivation` field of an annotation
|
|
40
|
+
- `canvasMin` (`number`): a positive integer
|
|
41
|
+
- `canvasMax` (`number`): a positive integer
|
|
42
|
+
- `canvasMax` must be greater than `canvasMin`
|
|
43
|
+
- if `canvasMax` is undefined, then we will only return the annotations that target a canvas at the `canvasMin` position in its manifest.
|
|
44
|
+
- `onlyIds` (`boolean`): return just the value of `@id` fields of matched annotations as a `string[]` instead of returning all the annotations,
|
|
40
45
|
|
|
41
46
|
#### Response
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
```
|
|
49
|
+
AnnotationList | AnnotationPage | string[]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- if `onlyIds=true`, return a `string[]` (array of the IDs of all matched annotations)
|
|
53
|
+
- otherwise,
|
|
54
|
+
- if `iiif_version` is `1`, return an `AnnotationList`
|
|
55
|
+
- else, return an `AnnotationPage`.
|
|
44
56
|
|
|
45
57
|
#### Notes
|
|
46
58
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
59
|
+
- `canvasMin`, `canvasMax` and `onlyIds` are non-standard query parameters that are NOT part of the IIIF Search API specification.
|
|
60
|
+
- If `q` and `motivation` are unused, it will return all annotations for the manifest
|
|
61
|
+
- Only exact matches are allowed for `q` and `motivation`
|
|
62
|
+
- About `canvasMin` and `canvasMax`:
|
|
63
|
+
- they are used to search for annotations that target a range of canvases: for example, fetch all anotations between pages 3 and 30 of a manuscript.
|
|
64
|
+
- See section **Create/update an annotation** for more information and possible issues with canvas indexes.
|
|
49
65
|
|
|
50
66
|
---
|
|
51
67
|
|
|
@@ -75,7 +91,7 @@ DELETE /{collection_name}/{iiif_version}/delete
|
|
|
75
91
|
{ deletedCount: <integer> }
|
|
76
92
|
```
|
|
77
93
|
|
|
78
|
-
---
|
|
94
|
+
---
|
|
79
95
|
|
|
80
96
|
## Manifests routes
|
|
81
97
|
|
|
@@ -106,14 +122,14 @@ POST /manifests/{iiif_version}/create
|
|
|
106
122
|
|
|
107
123
|
#### Request
|
|
108
124
|
|
|
109
|
-
- Variable:
|
|
125
|
+
- Variable:
|
|
110
126
|
- `iiif_version` (`2 | 3`): the IIIF Presentation API version of your manifest
|
|
111
127
|
- Body (`JSON`): the manifest to index in the database
|
|
112
128
|
|
|
113
129
|
#### Response
|
|
114
130
|
|
|
115
131
|
```
|
|
116
|
-
{
|
|
132
|
+
{
|
|
117
133
|
insertedIds: string[],
|
|
118
134
|
preExistingIds: string[],
|
|
119
135
|
rejectedIds: []
|
|
@@ -205,15 +221,18 @@ Create or update a single annotation
|
|
|
205
221
|
- Variables:
|
|
206
222
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
207
223
|
- `action` (`create | update`): the action to perform: create or update an annotation
|
|
224
|
+
- Parameters:
|
|
225
|
+
- `throwOnCanvasIndexError` (`boolean`): if there is an error fetching the related manifest, or getting a target canvas' index, throw an error.
|
|
208
226
|
- Body (`Object`): a IIIF annotation that follows the IIIF Presentation API 2 or 3 (depending on the value of `iiif_version`)
|
|
209
227
|
|
|
210
228
|
#### Response
|
|
211
229
|
|
|
212
230
|
```
|
|
213
|
-
{
|
|
231
|
+
{
|
|
214
232
|
insertedIds: string[],
|
|
215
233
|
preExistingIds: string[],
|
|
216
|
-
rejectedIds: []
|
|
234
|
+
rejectedIds: string[],
|
|
235
|
+
fetchErrorIds: string[]
|
|
217
236
|
}
|
|
218
237
|
```
|
|
219
238
|
|
|
@@ -227,7 +246,10 @@ Create or update a single annotation
|
|
|
227
246
|
- `annotation.on[0].canvasIdx`: the position of an annotation's target canvas within the target manifest, as an integer
|
|
228
247
|
- this depends on reconstructing an annotation's target manifest URL and fetching it. If this process fails, the fields above will be `undefined`.
|
|
229
248
|
- the annotation's target's manifest is fetched and inserted in the database, if possible, and stored in `annotation.on[0].manifestShortId`
|
|
230
|
-
|
|
249
|
+
- If `throwOnCanvasIndexError`, an error will be thrown if an error appears anywhere in the proicess of fetching the target manifest or populating the `canvasIdx` field.
|
|
250
|
+
- fetching an annotation's target manfest is error prone: it depends on the manifest being available through HTTP, which is not in our control.
|
|
251
|
+
- in turn, normally, if there's an error, we will just add the issue to `fetchErrorIds` and not throw.
|
|
252
|
+
- in controlled environments where you know your manifests WILL be available and where you rely heavily on the `canvasIdx` field (like AIKON), throwing an error will ensure that the `canvasIdx` field is always defined.
|
|
231
253
|
|
|
232
254
|
---
|
|
233
255
|
|
|
@@ -241,7 +263,7 @@ Batch insert multiple annotations.
|
|
|
241
263
|
|
|
242
264
|
#### Request
|
|
243
265
|
|
|
244
|
-
- Parameters:
|
|
266
|
+
- Parameters:
|
|
245
267
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
246
268
|
- Body: either:
|
|
247
269
|
- a full `AnnotationList | AnnotationPage` embedded in the body (type must match `iiif_version`: AnnotationPage for IIIF 3, AnnotationList for IIIF 2).
|
|
@@ -252,7 +274,7 @@ Batch insert multiple annotations.
|
|
|
252
274
|
#### Response
|
|
253
275
|
|
|
254
276
|
```
|
|
255
|
-
{
|
|
277
|
+
{
|
|
256
278
|
insertedIds: string[],
|
|
257
279
|
preExistingIds: string[],
|
|
258
280
|
rejectedIds: []
|
package/docs/todo.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Short term
|
|
4
|
+
|
|
5
|
+
- **pagination**
|
|
6
|
+
- add pagination to responses (`next` field in AnnotationLists)
|
|
7
|
+
- add `pageNumber` and `limit` parameters to routes to be able to specify page numbers and number of items in page from GET queries
|
|
8
|
+
- **update Mongo indexes**
|
|
9
|
+
- to reflect database changes (`annotation.on` is now an array)
|
|
10
|
+
- on frequently queried fields (`annotation.on.canvasIdx`, `annotation.on.manifestShortId`... => see what else is used in GET routes)
|
|
11
|
+
- **sort annotations collections**
|
|
12
|
+
- by `canvasIdx`
|
|
13
|
+
- by position on the page (y-offset then x-offset, see `xywh`)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Mid term
|
|
17
|
+
|
|
18
|
+
- **aiiinotate CLI** to import and export data easily
|
|
19
|
+
- **IIIF presentation 3**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiiinotate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "a fast IIIF-compliant annotation server",
|
|
5
5
|
"main": "./cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"cli": "node ./cli/index.js --env=./config/.env",
|
|
15
15
|
"setup": "sudo systemctl start mongod && npm run cli migrate apply",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
16
|
+
"dev": "sudo systemctl start mongod && nodemon --watch ./src --exec \"npm run cli serve dev\"",
|
|
17
|
+
"prod": "sudo systemctl start mongod && npm run cli serve prod",
|
|
18
|
+
"test": "export AIIINOTATE_TESTING=true; sudo systemctl start mongod && dotenvx run -f ./config/.env -- node --test --test-isolation=none",
|
|
18
19
|
"lint": "npx eslint --fix",
|
|
19
20
|
"migrate": "npm run cli -- migrate",
|
|
20
21
|
"update_version": "python3 scripts/update_version.py"
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"#schemas/*.js": "./src/schemas/*.js",
|
|
48
49
|
"#config/*.js": "./config/*.js",
|
|
49
50
|
"#data/*.js": "./src/data/*.js",
|
|
50
|
-
"#utils/*.js": "./src/
|
|
51
|
+
"#utils/*.js": "./src/utils/*.js",
|
|
51
52
|
"#fixtures/*.js": "./src/fixtures/*.js",
|
|
52
53
|
"#fixtures/*.json": "./src/fixtures/*.json",
|
|
53
54
|
"#manifests/*.js": "./src/data/manifests/*.js",
|
|
@@ -70,9 +71,10 @@
|
|
|
70
71
|
"@eslint/css": "^0.10.0",
|
|
71
72
|
"@eslint/js": "^9.33.0",
|
|
72
73
|
"@eslint/json": "^0.13.1",
|
|
74
|
+
"@fastify/pre-commit": "^2.2.1",
|
|
73
75
|
"@stylistic/eslint-plugin": "^5.2.3",
|
|
74
76
|
"eslint": "^9.33.0",
|
|
75
77
|
"globals": "^16.3.0",
|
|
76
|
-
"
|
|
78
|
+
"nodemon": "^3.1.11"
|
|
77
79
|
}
|
|
78
|
-
}
|
|
80
|
+
}
|
|
@@ -191,11 +191,12 @@ class Annotations2 extends CollectionAbstract {
|
|
|
191
191
|
* - set a key `canvasIdx` in all values of `annotation.on`, containing the position of the annotation's target canvas in the manifest,
|
|
192
192
|
* (or undefined if the manifest or canvas were not found).
|
|
193
193
|
* @param {object|object[]} annotationData - an annotation, or array of annotations.
|
|
194
|
+
* @param {boolean} throwOnCanvasIndexError - if canvasIdx can't be found, raise an error.
|
|
194
195
|
*/
|
|
195
|
-
async #insertManifestsAndGetCanvasIdx(annotationData) {
|
|
196
|
-
//
|
|
196
|
+
async #insertManifestsAndGetCanvasIdx(annotationData, throwOnCanvasIndexError=false) {
|
|
197
|
+
// NOTE: instead of propagating `throwOnCanvasIndexError` to `insertManifestsFromUriArray`, we could just check if `insertResponse.fetchErrorIds.length > 0` and return an error then.
|
|
197
198
|
// convert objects to array to get a uniform interface.
|
|
198
|
-
let converted
|
|
199
|
+
let converted;
|
|
199
200
|
[ annotationData, converted ] = maybeToArray(annotationData, true);
|
|
200
201
|
|
|
201
202
|
// 1. get all distinct manifest URIs
|
|
@@ -209,22 +210,28 @@ class Annotations2 extends CollectionAbstract {
|
|
|
209
210
|
// 2. insert the manifests
|
|
210
211
|
// NOTE: PERFORMANCE significantly drops because of this: test running for the entire app goes from ~1000ms to ~2600ms
|
|
211
212
|
const
|
|
212
|
-
insertResponse = await this.manifestsPlugin.insertManifestsFromUriArray(manifestUris,
|
|
213
|
+
insertResponse = await this.manifestsPlugin.insertManifestsFromUriArray(manifestUris, throwOnCanvasIndexError),
|
|
213
214
|
/** @type {string[]} concatenation of ids of newly inserted manifests and previously inserted manifests. */
|
|
214
215
|
insertedManifestsIds = insertResponse.insertedIds.concat(insertResponse.preExistingIds || []);
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
if ( throwOnCanvasIndexError && insertResponse.fetchErrorIds.length ) {
|
|
218
|
+
visibleLog("THIS SHOULD NOT HAPPEN")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 3. update annotations with info on manifest and canvas.
|
|
222
|
+
// if canvasIdx is undefined, throw.
|
|
219
223
|
annotationData = await Promise.all(
|
|
220
224
|
annotationData.map(async (ann) => {
|
|
221
225
|
ann.on = await Promise.all(
|
|
222
226
|
ann.on.map(async (target) => {
|
|
227
|
+
// a. where manifest insertion has failed, set `manifestUri` to undefined on all values of `annotation.on`
|
|
223
228
|
target.manifestUri =
|
|
224
229
|
// has the insertion of `manifestUri` worked ? (has it returned a valid response, woth `insertedIds` key).
|
|
225
230
|
insertedManifestsIds.find((x) => x === target.manifestUri)
|
|
226
231
|
? target.manifestUri
|
|
227
232
|
: undefined;
|
|
233
|
+
|
|
234
|
+
// b. set `annotation.on.canvasIdx`: the position of the target canvas within the manifest, or undefined if it cound not be found.
|
|
228
235
|
target.canvasIdx =
|
|
229
236
|
target.manifestUri
|
|
230
237
|
? await this.manifestsPlugin.getCanvasIdx(target.manifestUri, target.full)
|
|
@@ -232,6 +239,9 @@ class Annotations2 extends CollectionAbstract {
|
|
|
232
239
|
return target;
|
|
233
240
|
})
|
|
234
241
|
)
|
|
242
|
+
if ( throwOnCanvasIndexError && ann.on.some((target) => target.canvasIdx === undefined) ) {
|
|
243
|
+
throw new this.insertError(`${this.funcName(this.deleteAnnotations)}: could not get canvasIdx for annotation`);
|
|
244
|
+
}
|
|
235
245
|
return ann;
|
|
236
246
|
})
|
|
237
247
|
);
|
|
@@ -247,12 +257,18 @@ class Annotations2 extends CollectionAbstract {
|
|
|
247
257
|
|
|
248
258
|
/**
|
|
249
259
|
* validate and insert a single annotation.
|
|
250
|
-
*
|
|
260
|
+
*
|
|
261
|
+
* about `throwOnCanvasIndexError`:
|
|
262
|
+
* when inserting, aiiinotate attempts to fetch the target manifest of an annotation and to add the canvas number of the annotation to `annotation.on`.
|
|
263
|
+
* this may fail for a number of reasons (manifest URL and JSON structure, server storing the manifest is inaccessible...). if `throwOnCanvasIndexError`, it will raise.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} annotation
|
|
266
|
+
* @param {boolean?} throwOnCanvasIndexError
|
|
251
267
|
* @returns {Promise<InsertResponseType>}
|
|
252
268
|
*/
|
|
253
|
-
async insertAnnotation(annotation) {
|
|
269
|
+
async insertAnnotation(annotation, throwOnCanvasIndexError=false) {
|
|
254
270
|
annotation = this.#cleanAnnotation(annotation);
|
|
255
|
-
annotation = await this.#insertManifestsAndGetCanvasIdx(annotation);
|
|
271
|
+
annotation = await this.#insertManifestsAndGetCanvasIdx(annotation, throwOnCanvasIndexError);
|
|
256
272
|
return this.insertOne(annotation);
|
|
257
273
|
}
|
|
258
274
|
|
|
@@ -273,13 +289,19 @@ class Annotations2 extends CollectionAbstract {
|
|
|
273
289
|
|
|
274
290
|
/**
|
|
275
291
|
* validate and insert annotations from an annotation list.
|
|
292
|
+
*
|
|
293
|
+
* about `throwOnCanvasIndexError`:
|
|
294
|
+
* when inserting, aiiinotate attempts to fetch the target manifest of an annotation and to add the canvas number of the annotation to `annotation.on`.
|
|
295
|
+
* this may fail for a number of reasons (manifest URL and JSON structure, server storing the manifest is inaccessible...). if `throwOnCanvasIndexError`, it will raise.
|
|
296
|
+
*
|
|
276
297
|
* @param {object} annotationList
|
|
298
|
+
* @param {boolean?} throwOnCanvasIndexError
|
|
277
299
|
* @returns {Promise<InsertResponseType>}
|
|
278
300
|
*/
|
|
279
|
-
async insertAnnotationList(annotationList) {
|
|
301
|
+
async insertAnnotationList(annotationList, throwOnCanvasIndexError) {
|
|
280
302
|
let annotationArray;
|
|
281
303
|
annotationArray = this.#cleanAnnotationList(annotationList);
|
|
282
|
-
annotationArray = await this.#insertManifestsAndGetCanvasIdx(annotationArray);
|
|
304
|
+
annotationArray = await this.#insertManifestsAndGetCanvasIdx(annotationArray, throwOnCanvasIndexError);
|
|
283
305
|
return this.insertMany(annotationArray);
|
|
284
306
|
}
|
|
285
307
|
|
|
@@ -349,27 +371,47 @@ class Annotations2 extends CollectionAbstract {
|
|
|
349
371
|
* implementation of the IIIF Search API 1.0.
|
|
350
372
|
* function arguments have been validated by JSONSchemas at route-level so they're clean.
|
|
351
373
|
*
|
|
374
|
+
* parameters:
|
|
375
|
+
* - queryUrl - the request URL (/search-api...)
|
|
376
|
+
* - manifestShortId - the manifest's identifier
|
|
377
|
+
* - q: query string (content to search for in annotations)
|
|
378
|
+
* - motivation: filter by annotation motivation
|
|
379
|
+
* - canvasMin - minimum value of `on.canvasIdx`, inclusive
|
|
380
|
+
* - canvasMax - maximum value of `on.canvasIdx`, inclusive
|
|
381
|
+
* - onlyIds - return only the @ids of matched annotations instead of the entire annotations
|
|
382
|
+
*
|
|
352
383
|
* NOTE:
|
|
353
384
|
* - only `motivation` and `q` search params are implemented
|
|
354
385
|
* - to increase search execution speed, ONLY EXACT STRING MACHES are
|
|
355
386
|
* implemented for `q` and `motivation` (in the IIIF specs, you can supply
|
|
356
387
|
* multiple space-separated values and the server should return all partial
|
|
357
388
|
* matches to any of those strings.)
|
|
358
|
-
* - non-standard `canvasMin` and `
|
|
389
|
+
* - non-standard `canvasMin`, `canvasMax` and `onlyIds` parameters are implemented
|
|
359
390
|
*
|
|
360
391
|
* see:
|
|
361
392
|
* https://iiif.io/api/search/1.0/
|
|
362
393
|
* https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/specifications/4_search_api.md
|
|
363
394
|
*
|
|
364
|
-
* @param {
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
395
|
+
* @param {{
|
|
396
|
+
* queryUrl: string,
|
|
397
|
+
* manifestShortId: string,
|
|
398
|
+
* q: string?,
|
|
399
|
+
* motivation: ("painting"|"non-painting"|"commenting"|"describing"|"tagging"|"linking")?,
|
|
400
|
+
* canvasMin: number?,
|
|
401
|
+
* canvasMax: number?,
|
|
402
|
+
* onlyIds: boolean
|
|
403
|
+
* }}
|
|
370
404
|
* @returns {object} annotationList containing results
|
|
371
405
|
*/
|
|
372
|
-
async search(
|
|
406
|
+
async search({
|
|
407
|
+
queryUrl,
|
|
408
|
+
manifestShortId,
|
|
409
|
+
q,
|
|
410
|
+
motivation=undefined,
|
|
411
|
+
canvasMin=undefined,
|
|
412
|
+
canvasMax=undefined,
|
|
413
|
+
onlyIds=false
|
|
414
|
+
}) {
|
|
373
415
|
const
|
|
374
416
|
queryBase = { "on.manifestShortId": manifestShortId },
|
|
375
417
|
queryFilters = { $and: [] };
|
|
@@ -393,20 +435,17 @@ class Annotations2 extends CollectionAbstract {
|
|
|
393
435
|
: { motivation: `oa:${motivation}` }
|
|
394
436
|
);
|
|
395
437
|
};
|
|
396
|
-
|
|
397
|
-
if ( canvasMin ) {
|
|
438
|
+
if ( !isNaN(canvasMin) ) {
|
|
398
439
|
// if canvasMax is undefined, then search for canvasIdx===canvasMin
|
|
399
440
|
if ( !canvasMax ) {
|
|
400
441
|
queryFilters.$and.push({ "on.canvasIdx": canvasMin })
|
|
401
442
|
// if canvasMin and canvasMax, canvasIdx must be in [canvasMin, canvasMax] (inclusive).
|
|
402
443
|
} else {
|
|
403
444
|
queryFilters.$and.push({
|
|
404
|
-
|
|
405
|
-
$
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
]
|
|
409
|
-
}
|
|
445
|
+
$and: [
|
|
446
|
+
{ "on.canvasIdx": { $gte: canvasMin } },
|
|
447
|
+
{ "on.canvasIdx": { $lte: canvasMax } }
|
|
448
|
+
]
|
|
410
449
|
})
|
|
411
450
|
}
|
|
412
451
|
}
|
|
@@ -415,8 +454,13 @@ class Annotations2 extends CollectionAbstract {
|
|
|
415
454
|
? { ...queryBase, ...queryFilters }
|
|
416
455
|
: queryBase;
|
|
417
456
|
|
|
418
|
-
|
|
419
|
-
|
|
457
|
+
if ( !onlyIds ) {
|
|
458
|
+
const annotations = await this.find(query);
|
|
459
|
+
return toAnnotationList(annotations, queryUrl, `search results for query ${queryUrl}`);
|
|
460
|
+
} else {
|
|
461
|
+
return ( await this.find(query, { "@id": 1 }) )
|
|
462
|
+
.map((ann) => ann["@id"]);
|
|
463
|
+
}
|
|
420
464
|
}
|
|
421
465
|
|
|
422
466
|
/**
|
|
@@ -176,7 +176,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
176
176
|
async (request, reply) => {
|
|
177
177
|
const
|
|
178
178
|
annotationUri = pathToUrl(request.url),
|
|
179
|
-
{ iiifPresentationVersion} = request.params;
|
|
179
|
+
{ iiifPresentationVersion } = request.params;
|
|
180
180
|
try {
|
|
181
181
|
return iiifPresentationVersion === 2
|
|
182
182
|
? annotations2.findById(annotationUri)
|
|
@@ -202,6 +202,22 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
202
202
|
action: { type: "string", enum: [ "create", "update" ] }
|
|
203
203
|
}
|
|
204
204
|
},
|
|
205
|
+
// NOTE: throwOnCanvasIndexError is only implemented if `action==="create"` (see preValidation)
|
|
206
|
+
querystring: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
throwOnCanvasIndexError: { type: "boolean" },
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
preValidation: async (request, reply) => {
|
|
213
|
+
const
|
|
214
|
+
{ action } = request.params,
|
|
215
|
+
{ throwOnCanvasIndexError } = request.querystring;
|
|
216
|
+
if ( action==="update" && throwOnCanvasIndexError ) {
|
|
217
|
+
returnError(request, reply, "'throwOnCanvasIndexError' is only allowed when ':action' is 'create'.")
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
},
|
|
205
221
|
body: routeAnnotation2Or3Schema,
|
|
206
222
|
response: responsePostSchema
|
|
207
223
|
},
|
|
@@ -209,6 +225,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
209
225
|
async (request, reply) => {
|
|
210
226
|
const
|
|
211
227
|
{ iiifPresentationVersion, action } = request.params,
|
|
228
|
+
{ throwOnCanvasIndexError } = request.query,
|
|
212
229
|
annotation = request.body;
|
|
213
230
|
|
|
214
231
|
try {
|
|
@@ -216,7 +233,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
216
233
|
// insert or update
|
|
217
234
|
if ( iiifPresentationVersion === 2 ) {
|
|
218
235
|
return action==="create"
|
|
219
|
-
? await annotations2.insertAnnotation(annotation)
|
|
236
|
+
? await annotations2.insertAnnotation(annotation, throwOnCanvasIndexError)
|
|
220
237
|
: await annotations2.updateAnnotation(annotation);
|
|
221
238
|
} else {
|
|
222
239
|
annotations3.notImplementedError();
|
|
@@ -251,6 +268,12 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
251
268
|
iiifPresentationVersion: iiifPresentationVersionSchema
|
|
252
269
|
}
|
|
253
270
|
},
|
|
271
|
+
querystring: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
throwOnCanvasIndexError: { type: "boolean", },
|
|
275
|
+
}
|
|
276
|
+
},
|
|
254
277
|
body: routeAnnotationCreateManySchema,
|
|
255
278
|
response: responsePostSchema
|
|
256
279
|
}
|
|
@@ -258,6 +281,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
258
281
|
async (request, reply) => {
|
|
259
282
|
const
|
|
260
283
|
{ iiifPresentationVersion } = request.params,
|
|
284
|
+
{ throwOnCanvasIndexError } = request.query,
|
|
261
285
|
body = maybeToArray(request.body), // convert to an array to have a homogeneous data structure
|
|
262
286
|
insertResponseArray = [];
|
|
263
287
|
|
|
@@ -282,7 +306,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
282
306
|
if ( iiifPresentationVersion === 2 ) {
|
|
283
307
|
await Promise.all(annotationsArray.map(
|
|
284
308
|
async (annotationList) => {
|
|
285
|
-
const r = await annotations2.insertAnnotationList(annotationList);
|
|
309
|
+
const r = await annotations2.insertAnnotationList(annotationList, throwOnCanvasIndexError);
|
|
286
310
|
insertResponseArray.push(r);
|
|
287
311
|
}
|
|
288
312
|
));
|
|
@@ -55,8 +55,9 @@ test("test annotation Routes", async (t) => {
|
|
|
55
55
|
return a;
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
// test basic inserts
|
|
58
59
|
//NOTE: we can't do Promise.all because it causes a data race that can cause a failure of unique constraints (i.e., on manifests '@id')
|
|
59
|
-
|
|
60
|
+
let data = [
|
|
60
61
|
[[ annotationListUri, annotationListUriArray, annotationList, annotationListArrayLimit ], testPostRouteCreateSuccess],
|
|
61
62
|
[[ annotationListUriInvalid, annotationListUriArrayInvalid ], testPostRouteCreateFailure]
|
|
62
63
|
];
|
|
@@ -66,11 +67,29 @@ test("test annotation Routes", async (t) => {
|
|
|
66
67
|
await func(t, "/annotations/2/createMany", testData.at(i));
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
// test that `throwOnCanvasIndexError` throws errors when it's supposed to
|
|
72
|
+
const annotationListWithTargetErrors = structuredClone(annotationList);
|
|
73
|
+
// replace the annotation.on with an uuid => should trigger an error.https://www.cinefil.com/film/mulholland-drive
|
|
74
|
+
annotationListWithTargetErrors.resources =
|
|
75
|
+
annotationListWithTargetErrors
|
|
76
|
+
.resources
|
|
77
|
+
.map((annotation) => {
|
|
78
|
+
// if annotation.on is a string, the annotation should have a fragment
|
|
79
|
+
annotation.on = `https://test/${uuid4()}#xywh=100,100,300,300`;
|
|
80
|
+
return annotation
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
data = [[testPostRouteCreateFailure, true], [testPostRouteCreateSuccess, false]];
|
|
84
|
+
for (let i=0; i<data.length; i++) {
|
|
85
|
+
const [func, v] = data.at(i);
|
|
86
|
+
func(t, `/annotations/2/createMany?throwOnCanvasIndexError=${v}`, annotationListWithTargetErrors)
|
|
87
|
+
}
|
|
69
88
|
})
|
|
70
89
|
|
|
71
90
|
await t.test("test route /annotations/:iiifPresentationVersion/create", async (t) => {
|
|
72
91
|
//NOTE: we can't do Promise.all because it causes a data race that can cause a failure of unique constraints (i.e., on manifests '@id')
|
|
73
|
-
|
|
92
|
+
let data = [
|
|
74
93
|
[fastify.fixtures.annotations2Valid, testPostRouteCreateSuccess],
|
|
75
94
|
[fastify.fixtures.annotations2Invalid, testPostRouteCreateFailure],
|
|
76
95
|
]
|
|
@@ -80,6 +99,15 @@ test("test annotation Routes", async (t) => {
|
|
|
80
99
|
await func(t, "/annotations/2/create", testData.at(i));
|
|
81
100
|
}
|
|
82
101
|
};
|
|
102
|
+
|
|
103
|
+
// test throwOnCanvasIndexError
|
|
104
|
+
const annotationWithTargetError = structuredClone(fastify.fixtures.annotations2Valid.at(0));
|
|
105
|
+
annotationWithTargetError.on = `https://test/${uuid4()}#xywh=100,100,300,300`;
|
|
106
|
+
data = [[testPostRouteCreateFailure, true], [testPostRouteCreateSuccess, false]];
|
|
107
|
+
for (let i=0; i<data.length; i++) {
|
|
108
|
+
const [func, v] = data.at(i);
|
|
109
|
+
await func(t, `/annotations/2/create?throwOnCanvasIndexError=${v}`, annotationWithTargetError)
|
|
110
|
+
}
|
|
83
111
|
})
|
|
84
112
|
|
|
85
113
|
await t.test("test route /annotations/:iiifPresentationVersion/update", async (t) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { maybeToArray, inspectObj } from "#utils/utils.js"
|
|
2
|
-
import { formatInsertResponse, formatDeleteResponse, formatUpdateResponse } from "#
|
|
2
|
+
import { formatInsertResponse, formatDeleteResponse, formatUpdateResponse } from "#utils/routeUtils.js";
|
|
3
3
|
|
|
4
4
|
/** @typedef {import("#types").MongoDbType} MongoDbType */
|
|
5
5
|
/** @typedef {import("#types").MongoClientType} MongoClientType */
|
|
@@ -94,7 +94,7 @@ class CollectionAbstract {
|
|
|
94
94
|
this.errorConstructor = errorConstructor(collectionName);
|
|
95
95
|
/** @type {Function(string, object?) => Error} */
|
|
96
96
|
this.errorNoAction = this.errorConstructor(undefined);
|
|
97
|
-
// create this.
|
|
97
|
+
// create this.(read|insert|update|delete)Error, properties that will be used to throw the proper error.
|
|
98
98
|
[ "read", "insert", "update", "delete" ].forEach((op) =>
|
|
99
99
|
/** @type {Function(string,object?) => Error} */
|
|
100
100
|
this[`${op}Error`] = errorConstructor(collectionName)(op)
|
|
@@ -148,7 +148,7 @@ class CollectionAbstract {
|
|
|
148
148
|
// MongoInsertOneResultType and MongoInsertManyResultType have a different structureex
|
|
149
149
|
mongoRes.insertedId || Object.values(mongoRes.insertedIds)
|
|
150
150
|
);
|
|
151
|
-
return formatInsertResponse(insertedIds);
|
|
151
|
+
return formatInsertResponse({ insertedIds });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|