aiiinotate 0.3.2 → 0.4.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/docs/endpoints.md +253 -10
- package/package.json +1 -1
- package/scripts/get_version.py +12 -0
- package/scripts/update_version.py +1 -1
- package/src/data/annotations/annotations2.js +41 -13
- package/src/data/annotations/routes.js +33 -1
- package/src/data/annotations/routes.test.js +34 -0
- package/src/data/routes.js +2 -2
- package/src/schemas/schemasRoutes.js +18 -4
package/docs/endpoints.md
CHANGED
|
@@ -1,8 +1,250 @@
|
|
|
1
1
|
# Endpoints
|
|
2
2
|
|
|
3
|
+
## Introductory notes
|
|
4
|
+
|
|
5
|
+
**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
|
+
|
|
7
|
+
**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:
|
|
8
|
+
- **when communicating with aiiinotate**, you must specify a **IIIF presentation version in the query URL**. In the docs, this is described by the `iiif_version` keyword.
|
|
9
|
+
- **when inserting/updating data**, the data structure you provide must match the URL's `iiif_version`: you can't insert an annotation in v3 if your `iiif_version` is `2`.
|
|
10
|
+
- **when searching for data**, if you inserted an annotation in v3, you must search for it with `iiif_version = 3`.
|
|
11
|
+
- **TLDR**:
|
|
12
|
+
- your data must match the `iiif_version` argument
|
|
13
|
+
- if you insert an Annotation following the API v3.x, you can't search for it using `iiif_version=2`.
|
|
14
|
+
|
|
15
|
+
This is because
|
|
16
|
+
- the IIIF standard is quite complex and there are breaking changes between v2 and v3
|
|
17
|
+
- handling conversions between v2 and v3 is error prone, would increase calculations and slow the app down
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Generic routes
|
|
22
|
+
|
|
23
|
+
### IIIF search API
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
GET /search-api/{iiif_version}/manifests/{manifest_short_id}/search
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Implementation of the [IIIF Search API](https://iiif.io/api/search/2.0/), to search one or several annotations within a manifest.
|
|
30
|
+
|
|
31
|
+
#### Request
|
|
32
|
+
|
|
33
|
+
- Variables:
|
|
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.
|
|
36
|
+
- Parameters:
|
|
37
|
+
- `q` (`string`): query string.
|
|
38
|
+
- if `iiif_version=1`, `q` is searched in the fields: `@id`, `resource.@id` or `resource.chars` fields
|
|
39
|
+
- `motivation` (`painting | non-painting | commenting | describing | tagging | linking`): values for the `motivation` field of an annotation
|
|
40
|
+
|
|
41
|
+
#### Reply
|
|
42
|
+
|
|
43
|
+
Returns a JSON. If `iiif_version` is `1`, an `AnnotationList` is returned. Otherwise, an `AnnotationPage` is returned.
|
|
44
|
+
|
|
45
|
+
#### Notes
|
|
46
|
+
|
|
47
|
+
- if `q` and `motivation` are unused, it will return all annotations for the manifest
|
|
48
|
+
- only exact matches are allowed for `q` and `motivation`
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### Delete an annotation or a manifest
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
DELETE /{collection_name}/{iiif_version}/delete
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Request
|
|
59
|
+
|
|
60
|
+
- Variables
|
|
61
|
+
- `collection_name` (`annotations | manifests`): delete an annotation or a manifest
|
|
62
|
+
- `iiif_version` (`2 | 3`): IIIF presentation version
|
|
63
|
+
- Parameters:
|
|
64
|
+
- if `collection_name = manifests`:
|
|
65
|
+
- `uri`: the full URI of the manifest to delete
|
|
66
|
+
- `manifestShortId`: the manifest's identifier
|
|
67
|
+
- if `collection_name = annotation`:
|
|
68
|
+
- `uri`: the full URI of the annotation to delete
|
|
69
|
+
- `manifestShortId`: a manifest's identifier, to delete all annotations for a manifest
|
|
70
|
+
- `canvasUri`: the full URI to an annotation's target canvas, to delete all annotatons for the canvas
|
|
71
|
+
|
|
72
|
+
#### Reply
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
{ deletedCount: <integer> }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Manifests routes
|
|
81
|
+
|
|
82
|
+
### Get an index of all manifests
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
GET /manifests/{iiif_version}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Returns a Collection of all manifests in your **aiiinotate** instance.
|
|
89
|
+
|
|
90
|
+
#### Request
|
|
91
|
+
|
|
92
|
+
- Variables:
|
|
93
|
+
- `iiif_version` (`2 | 3`): the IIIF Presentation API version
|
|
94
|
+
|
|
95
|
+
#### Reply
|
|
96
|
+
|
|
97
|
+
A IIIF `Collection`, following the IIIF Presentation API 2 or 3, depending of the value of `iiif_version`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Insert a manifest
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
POST /manifests/{iiif_version}/create
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Request
|
|
108
|
+
|
|
109
|
+
- Variable:
|
|
110
|
+
- `iiif_version` (`2 | 3`): the IIIF Presentation API version of your manifest
|
|
111
|
+
- Body (`JSON`): the manifest to index in the database
|
|
112
|
+
|
|
113
|
+
#### Reply
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
{
|
|
117
|
+
insertedIds: string[],
|
|
118
|
+
preExistingIds: string[],
|
|
119
|
+
rejectedIds: []
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `insertedIds`: the list of IDs of inserted manifests
|
|
124
|
+
- `preExisingIds`: the IDs of manifests that were aldready in the database
|
|
125
|
+
- `rejectedIds`: the IDs of manifests on which an error occurred
|
|
126
|
+
|
|
3
127
|
---
|
|
4
128
|
|
|
5
|
-
##
|
|
129
|
+
## Annotation routes
|
|
130
|
+
|
|
131
|
+
### Get all annotations for a canvas
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
GET /annotations/{iiif_version}/search
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Request
|
|
138
|
+
|
|
139
|
+
- Variables:
|
|
140
|
+
- `iiif_version` (`2 | 3`): the IIIF Presentation API of your manifests
|
|
141
|
+
- Parameters:
|
|
142
|
+
- `uri` (`string`): the URI of the target canvas
|
|
143
|
+
- `asAnnotationList` (`true | false`): format of the response
|
|
144
|
+
|
|
145
|
+
#### Reply
|
|
146
|
+
|
|
147
|
+
`Object[] | Object`: if `true`, return an array of annotations. Otherwise, return an `AnnotationList`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Get a single annotation
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
GET /data/{iiif_version}/{manifest_short_id}/annotation/{annotation_short_id}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This route allows to query an annotation by its ID by defering its `@id | id` field. This URL follows the IIIF specification
|
|
158
|
+
|
|
159
|
+
#### Request
|
|
160
|
+
|
|
161
|
+
- Variables:
|
|
162
|
+
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
163
|
+
- `manifest_short_id` (`string`): the identifier of the manifest the annotation is related to
|
|
164
|
+
- `annotation_short_id`: the unique part of the annotation URL
|
|
165
|
+
|
|
166
|
+
#### Reply
|
|
167
|
+
|
|
168
|
+
`Object`: the annotation. Its format follows the IIIF Presentation specification 2 or 3, based on the value of `iiif_version`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Create/update an annotation
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
POST /annotations/{iiif_version}/{action}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Create or update a single annotation
|
|
179
|
+
|
|
180
|
+
#### Request
|
|
181
|
+
|
|
182
|
+
- Variables:
|
|
183
|
+
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
184
|
+
- `action` (`create | update`): the action to perform: create or update an annotation
|
|
185
|
+
- Body (`Object`): a IIIF annotation that follows the IIIF Presentation API 2 or 3 (depending on the value of `iiif_version`)
|
|
186
|
+
|
|
187
|
+
#### Reply
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
{
|
|
191
|
+
insertedIds: string[],
|
|
192
|
+
preExistingIds: string[],
|
|
193
|
+
rejectedIds: []
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Notes
|
|
198
|
+
|
|
199
|
+
- A side effect of inserting annotations is inserting the related manifests.
|
|
200
|
+
- When inserting an annotation, the annotation's target manifest is also fetched and inserted in the database
|
|
201
|
+
- Annotations in `aiiinotate` contain 3 nonstandard fields. In IIIF presentation 2.x,
|
|
202
|
+
- `annotation.on[0].manifestUri`: the URI of the manifest on which is an annotation
|
|
203
|
+
- `annotation.on[0].manifestShortId`: the unique identifier of the manifest on which is an annotation
|
|
204
|
+
- `annotation.on[0].canvasIdx`: the position of an annotation's target canvas within the target manifest, as an integer
|
|
205
|
+
- this depends on reconstructing an annotation's target manifest URL and fetching it. If this process fails, the fields above will be `undefined`.
|
|
206
|
+
- the annotation's target's manifest is fetched and inserted in the database, if possible, and stored in `annotation.on[0].manifestShortId`
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Insert several annotations
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
POST /annotations/{iiif_version}/createMany
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Batch insert multiple annotations.
|
|
218
|
+
|
|
219
|
+
#### Request
|
|
220
|
+
|
|
221
|
+
- Parameters:
|
|
222
|
+
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
223
|
+
- Body: either:
|
|
224
|
+
- a full `AnnotationList | AnnotationPage` embedded in the body (type must match `iiif_version`: AnnotationPage for IIIF 3, AnnotationList for IIIF 2).
|
|
225
|
+
- `AnnotationList[] | AnnotationPage[]` (type must match `iiif_version`): an array of annotation lists or pages
|
|
226
|
+
- `{ uri: string }`: an object containing a reference to an `AnnotationList` or `AnnotationPage`
|
|
227
|
+
- `{ uri: string }[]`: an array of objects containing a reference to an `AnnotationList` or `AnnotationPage`.
|
|
228
|
+
|
|
229
|
+
#### Reply
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
{
|
|
233
|
+
insertedIds: string[],
|
|
234
|
+
preExistingIds: string[],
|
|
235
|
+
rejectedIds: []
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Notes
|
|
240
|
+
|
|
241
|
+
- Be wary of maximum body size, especially when sending AnnotationLists in your body. If possible, using `{ uri: string }` is better.
|
|
242
|
+
- All annotations within a single AnnotationList/Page may have different target canvases or manifests.
|
|
243
|
+
- See **Create/update an annotation**.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Appending 1: App logic: URL prefixes
|
|
6
248
|
|
|
7
249
|
URL anatomy is a mix of [SAS endpoints](./specifications/4_sas.md) and IIIF specifications. In turn, we define the following prefixes:
|
|
8
250
|
|
|
@@ -27,22 +269,23 @@ Where:
|
|
|
27
269
|
|
|
28
270
|
There is an extra URL prefix: `schemas`. It is only used internally (not accessible to clients or accessible through HTTP) to define the IDs of all JsonSchemas, so we won't talk about it here.
|
|
29
271
|
|
|
272
|
+
|
|
30
273
|
---
|
|
31
274
|
|
|
32
|
-
## IIIF URIs
|
|
275
|
+
## Appendix 2: IIIF URIs
|
|
33
276
|
|
|
34
277
|
IIIF URIs in the Presentation 2.1 API are:
|
|
35
278
|
|
|
36
279
|
```
|
|
37
280
|
Collection {scheme}://{host}/{prefix}/collection/{name}
|
|
38
|
-
Manifest {scheme}://{host}/{prefix}/{
|
|
39
|
-
Sequence {scheme}://{host}/{prefix}/{
|
|
40
|
-
Canvas {scheme}://{host}/{prefix}/{
|
|
41
|
-
Annotation (incl images) {scheme}://{host}/{prefix}/{
|
|
42
|
-
AnnotationList {scheme}://{host}/{prefix}/{
|
|
43
|
-
Range {scheme}://{host}/{prefix}/{
|
|
44
|
-
Layer {scheme}://{host}/{prefix}/{
|
|
45
|
-
Content {scheme}://{host}/{prefix}/{
|
|
281
|
+
Manifest {scheme}://{host}/{prefix}/{manifest_short_id}/manifest
|
|
282
|
+
Sequence {scheme}://{host}/{prefix}/{manifest_short_id}/sequence/{name}
|
|
283
|
+
Canvas {scheme}://{host}/{prefix}/{manifest_short_id}/canvas/{name}
|
|
284
|
+
Annotation (incl images) {scheme}://{host}/{prefix}/{manifest_short_id}/annotation/{name}
|
|
285
|
+
AnnotationList {scheme}://{host}/{prefix}/{manifest_short_id}/list/{name}
|
|
286
|
+
Range {scheme}://{host}/{prefix}/{manifest_short_id}/range/{name}
|
|
287
|
+
Layer {scheme}://{host}/{prefix}/{manifest_short_id}/layer/{name}
|
|
288
|
+
Content {scheme}://{host}/{prefix}/{manifest_short_id}/res/{name}.{format}
|
|
46
289
|
```
|
|
47
290
|
|
|
48
291
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
curdir = pathlib.Path(__file__).parent.resolve()
|
|
8
|
+
pkg_file = curdir.parent.joinpath("package.json").resolve()
|
|
9
|
+
|
|
10
|
+
with open(pkg_file, mode="r") as fh:
|
|
11
|
+
data = json.load(fh)
|
|
12
|
+
print(f"\naiiinotate current version: {data['version']}\n")
|
|
@@ -58,6 +58,24 @@ class Annotations2 extends CollectionAbstract {
|
|
|
58
58
|
////////////////////////////////////////////////////////////////
|
|
59
59
|
// utils
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* expand a pair of `filterKey`, `filterVal` following the schema `routeAnnotationFilter` into a proper filter for the `annotations2` collection.
|
|
63
|
+
* @param {string} filterKey
|
|
64
|
+
* @param {string} filterVal
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
#expandRouteAnnotationFilter(filterKey, filterVal) {
|
|
68
|
+
const allowedFilterKeys = [ "uri", "manifestShortId", "canvasUri" ];
|
|
69
|
+
if ( !allowedFilterKeys.includes(filterKey) ) {
|
|
70
|
+
throw new Error(`${this.funcname(this.#expandRouteAnnotationFilter)}: expected one of ${allowedFilterKeys} for param 'deleteKey', got '${filterKey}'`)
|
|
71
|
+
}
|
|
72
|
+
return filterKey==="uri"
|
|
73
|
+
? { "@id": filterVal }
|
|
74
|
+
: filterKey==="canvasUri"
|
|
75
|
+
? { "on.full": filterVal }
|
|
76
|
+
: { "on.manifestShortId": filterVal };
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
/**
|
|
62
80
|
* clean the body of an annotation (annotation.resource).
|
|
63
81
|
* if `annotation.resource` is an array (there are several bodies associated to that annotation), this function must be called on each item of the array
|
|
@@ -275,19 +293,12 @@ class Annotations2 extends CollectionAbstract {
|
|
|
275
293
|
* @returns {Promise<DeleteResponseType>}
|
|
276
294
|
*/
|
|
277
295
|
async deleteAnnotations(deleteKey, deleteVal) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
try {
|
|
297
|
+
const deleteFilter = this.#expandRouteAnnotationFilter(deleteKey, deleteVal);
|
|
298
|
+
return this.delete(deleteFilter);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
throw this.deleteError(`${this.funcName(this.deleteAnnotations)}: ${err.message}`)
|
|
282
301
|
}
|
|
283
|
-
|
|
284
|
-
const deleteFilter =
|
|
285
|
-
deleteKey==="uri"
|
|
286
|
-
? { "@id": deleteVal }
|
|
287
|
-
: deleteKey==="canvasUri"
|
|
288
|
-
? { "on.full": deleteVal }
|
|
289
|
-
: { "on.manifestShortId": deleteVal };
|
|
290
|
-
return this.delete(deleteFilter);
|
|
291
302
|
}
|
|
292
303
|
|
|
293
304
|
////////////////////////////////////////////////////////////////
|
|
@@ -340,7 +351,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
340
351
|
*
|
|
341
352
|
* NOTE:
|
|
342
353
|
* - only `motivation` and `q` search params are implemented
|
|
343
|
-
* - to increase search execution, ONLY EXACT STRING MACHES are
|
|
354
|
+
* - to increase search execution speed, ONLY EXACT STRING MACHES are
|
|
344
355
|
* implemented for `q` and `motivation` (in the IIIF specs, you can supply
|
|
345
356
|
* multiple space-separated values and the server should return all partial
|
|
346
357
|
* matches to any of those strings.)
|
|
@@ -410,6 +421,23 @@ class Annotations2 extends CollectionAbstract {
|
|
|
410
421
|
return this.collection.findOne({ "@id": annotationUri })
|
|
411
422
|
}
|
|
412
423
|
|
|
424
|
+
/**
|
|
425
|
+
* count number of annotations.
|
|
426
|
+
* @param {string} filterKey
|
|
427
|
+
* @param {string} filterVal
|
|
428
|
+
* @returns
|
|
429
|
+
*/
|
|
430
|
+
async count(filterKey, filterVal) {
|
|
431
|
+
try {
|
|
432
|
+
const
|
|
433
|
+
countFilter = this.#expandRouteAnnotationFilter(filterKey, filterVal),
|
|
434
|
+
count = await this.collection.countDocuments(countFilter);
|
|
435
|
+
return { count: count }
|
|
436
|
+
} catch (err) {
|
|
437
|
+
throw this.readError(`${this.funcName(this.count)}: ${err.message}`)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
413
441
|
}
|
|
414
442
|
|
|
415
443
|
export default fastifyPlugin((fastify, options, done) => {
|
|
@@ -64,9 +64,11 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
64
64
|
annotations2 = fastify.annotations2,
|
|
65
65
|
/** @type {Annotations3InstanceType} */
|
|
66
66
|
annotations3 = fastify.annotations3,
|
|
67
|
-
iiifPresentationVersionSchema = fastify.schemasBase.getSchema("presentation"),
|
|
68
67
|
routeAnnotation2Or3Schema = fastify.schemasRoutes.getSchema("routeAnnotation2Or3"),
|
|
69
68
|
routeAnnotationCreateManySchema = fastify.schemasRoutes.getSchema("routeAnnotationCreateMany"),
|
|
69
|
+
routeAnnotationFilterSchema = fastify.schemasRoutes.getSchema("routeAnnotationFilter"),
|
|
70
|
+
routeResponseCountSchema = fastify.schemasRoutes.getSchema("routeResponseCount"),
|
|
71
|
+
iiifPresentationVersionSchema = fastify.schemasBase.getSchema("presentation"),
|
|
70
72
|
iiifAnnotationListSchema = fastify.schemasPresentation2.getSchema("annotationList"),
|
|
71
73
|
iiifAnnotation2ArraySchema = fastify.schemasPresentation2.getSchema("annotationArray"),
|
|
72
74
|
iiifAnnotation2Schema = fastify.schemasPresentation2.getSchema("annotation"),
|
|
@@ -122,6 +124,36 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
122
124
|
}
|
|
123
125
|
);
|
|
124
126
|
|
|
127
|
+
fastify.get(
|
|
128
|
+
"/annotations/:iiifPresentationVersion/count",
|
|
129
|
+
{
|
|
130
|
+
schema: {
|
|
131
|
+
params: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
iiifPresentationVersion: iiifPresentationVersionSchema
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
querystring: routeAnnotationFilterSchema,
|
|
138
|
+
response: makeResponseSchema(
|
|
139
|
+
fastify, routeResponseCountSchema
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
async (request, reply) => {
|
|
144
|
+
const
|
|
145
|
+
{ iiifPresentationVersion } = request.params,
|
|
146
|
+
[ filterKey, filterVal ] = getFirstNonEmptyPair(request.query);
|
|
147
|
+
try {
|
|
148
|
+
return iiifPresentationVersion === 2
|
|
149
|
+
? await annotations2.count(filterKey, filterVal)
|
|
150
|
+
: annotations3.notImplementedError();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
returnError(request, reply, err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
125
157
|
/** retrieve a single annotation by its "@id"|"id". this route defers an annotation */
|
|
126
158
|
fastify.get(
|
|
127
159
|
"/data/:iiifPresentationVersion/:manifestShortId/annotation/:annotationShortId",
|
|
@@ -176,5 +176,39 @@ test("test annotation Routes", async (t) => {
|
|
|
176
176
|
)
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
await t.test("test route /annotations/:iiifPresentationVersion/count", async (t) => {
|
|
180
|
+
const expectedOnCount = (annotationArray, onKey, expectedOnVal) =>
|
|
181
|
+
annotationArray.filter((anno) => anno.on.some(x => x[onKey] === expectedOnVal)).length
|
|
182
|
+
|
|
183
|
+
await injectTestAnnotations(fastify, t, annotationList);
|
|
184
|
+
const
|
|
185
|
+
annotationArray = await fastify.mongo.db.collection("annotations2").find().toArray(),
|
|
186
|
+
annotationId = getRandomItem(annotationArray)["@id"],
|
|
187
|
+
canvasUri = getRandomItem(annotationArray).on[0].full,
|
|
188
|
+
manifestShortId = getRandomItem(annotationArray).on[0].manifestShortId,
|
|
189
|
+
expectedAnnotationIdCount = 1,
|
|
190
|
+
expectedCanvasUriCount = expectedOnCount(annotationArray, "full", canvasUri),
|
|
191
|
+
expectedManifestShortIdCount = expectedOnCount(annotationArray, "manifestShortId", manifestShortId),
|
|
192
|
+
mapper = [
|
|
193
|
+
["uri", annotationId, expectedAnnotationIdCount],
|
|
194
|
+
["canvasUri", canvasUri, expectedCanvasUriCount],
|
|
195
|
+
["manifestShortId", manifestShortId, expectedManifestShortIdCount]
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
await Promise.all(
|
|
199
|
+
mapper.map(async ([filterKey, filterVal, expectedCount]) => {
|
|
200
|
+
const
|
|
201
|
+
r = await fastify.inject({
|
|
202
|
+
method: "GET",
|
|
203
|
+
url: `/annotations/2/count?${filterKey}=${filterVal}`
|
|
204
|
+
}),
|
|
205
|
+
body = await r.json();
|
|
206
|
+
t.assert.deepStrictEqual(r.statusCode, 200);
|
|
207
|
+
t.assert.deepStrictEqual(body.count, expectedCount);
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
})
|
|
212
|
+
|
|
179
213
|
return
|
|
180
214
|
})
|
package/src/data/routes.js
CHANGED
|
@@ -29,10 +29,10 @@ function commonRoutes(fastify, options, done) {
|
|
|
29
29
|
routeDeleteSchema = fastify.schemasRoutes.getSchema("routeDelete"),
|
|
30
30
|
responsePostSchema = makeResponsePostSchema(fastify),
|
|
31
31
|
validatorRouteAnnotationDeleteSchema = ajvCompile(fastify.schemasResolver(
|
|
32
|
-
fastify.schemasRoutes.getSchema("
|
|
32
|
+
fastify.schemasRoutes.getSchema("routeAnnotationFilter")
|
|
33
33
|
)),
|
|
34
34
|
validatorRouteManifestDeleteSchema = ajvCompile(fastify.schemasResolver(
|
|
35
|
-
fastify.schemasRoutes.getSchema("
|
|
35
|
+
fastify.schemasRoutes.getSchema("routeManifestFilter")
|
|
36
36
|
));
|
|
37
37
|
|
|
38
38
|
fastify.get(
|
|
@@ -163,8 +163,9 @@ function addSchemas(fastify, options, done) {
|
|
|
163
163
|
]
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
// for annotations: key-value pairs to filter a manifests collection by
|
|
166
167
|
fastify.addSchema({
|
|
167
|
-
$id: makeSchemaUri("
|
|
168
|
+
$id: makeSchemaUri("routeAnnotationFilter"),
|
|
168
169
|
oneOf: [
|
|
169
170
|
{
|
|
170
171
|
type: "object",
|
|
@@ -200,8 +201,9 @@ function addSchemas(fastify, options, done) {
|
|
|
200
201
|
]
|
|
201
202
|
});
|
|
202
203
|
|
|
204
|
+
// for manifests: key-value pairs to filter a manifests collection by
|
|
203
205
|
fastify.addSchema({
|
|
204
|
-
$id: makeSchemaUri("
|
|
206
|
+
$id: makeSchemaUri("routeManifestFilter"),
|
|
205
207
|
type: "object",
|
|
206
208
|
oneOf: [
|
|
207
209
|
{
|
|
@@ -224,8 +226,8 @@ function addSchemas(fastify, options, done) {
|
|
|
224
226
|
$id: makeSchemaUri("routeDelete"),
|
|
225
227
|
type: "object",
|
|
226
228
|
oneOf: [
|
|
227
|
-
{ $ref: makeSchemaUri("
|
|
228
|
-
{ $ref: makeSchemaUri("
|
|
229
|
+
{ $ref: makeSchemaUri("routeAnnotationFilter") },
|
|
230
|
+
{ $ref: makeSchemaUri("routeManifestFilter") },
|
|
229
231
|
]
|
|
230
232
|
});
|
|
231
233
|
|
|
@@ -288,6 +290,18 @@ function addSchemas(fastify, options, done) {
|
|
|
288
290
|
}
|
|
289
291
|
});
|
|
290
292
|
|
|
293
|
+
fastify.addSchema({
|
|
294
|
+
$id: makeSchemaUri("routeResponseCount"),
|
|
295
|
+
type: "object",
|
|
296
|
+
required: [ "count" ],
|
|
297
|
+
properties: {
|
|
298
|
+
count: {
|
|
299
|
+
type: "integer",
|
|
300
|
+
minimum: 0
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
291
305
|
////////////////////////////////////////////////////////
|
|
292
306
|
|
|
293
307
|
fastify.decorate("schemasRoutes", {
|