aiiinotate 0.6.2 → 0.7.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 +2 -0
- package/docs/endpoints.md +45 -20
- package/package.json +1 -1
- package/src/data/annotations/annotations2.js +29 -16
- package/src/data/annotations/routes.js +12 -10
- package/src/data/annotations/routes.test.js +30 -11
- package/src/data/routes.test.js +0 -45
- package/src/schemas/schemasPresentation2.js +16 -19
- package/src/schemas/schemasPresentation3.js +1 -7
- package/src/utils/iiif2Utils.js +3 -2
- package/src/utils/utils.js +5 -2
package/config/.env.template
CHANGED
|
@@ -16,6 +16,8 @@ AIIINOTATE_SCHEME=http
|
|
|
16
16
|
|
|
17
17
|
# max number of items to display per result page
|
|
18
18
|
AIIINOTATE_PAGE_SIZE=5000
|
|
19
|
+
# "true"|"false". strict error throwing when inserting annotations. equivalent to `throwOnCanvasIndexError=true` (raise an error if an annotation's target manifest can't be found) and `throwOnXywhError=true` (raise an error if you can't extract a bounding box for an annotation's target). use in controlled environments where you know precisely the structure of your annotations and where you know that your manifests are accessible through HTTP.
|
|
20
|
+
AIIINOTATE_STRICT_MODE=false
|
|
19
21
|
|
|
20
22
|
# IGNORE
|
|
21
23
|
AIIINOTATE_BASE_URL="$AIIINOTATE_SCHEME://$AIIINOTATE_HOST:$AIIINOTATE_PORT"
|
package/docs/endpoints.md
CHANGED
|
@@ -72,7 +72,8 @@ AnnotationList | AnnotationPage | string[]
|
|
|
72
72
|
- Only exact matches are allowed for `q` and `motivation`
|
|
73
73
|
- About `canvasMin` and `canvasMax`:
|
|
74
74
|
- 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.
|
|
75
|
-
-
|
|
75
|
+
- they are **0-indexed**: the 1st canvas of a manifest is indexed `0`, the 2nd is indexed `1`...
|
|
76
|
+
- See section [Create/update an annotation](#createupdate-an-annotation) for more information and possible issues with canvas indexes.
|
|
76
77
|
|
|
77
78
|
---
|
|
78
79
|
|
|
@@ -238,7 +239,8 @@ Create or update a single annotation
|
|
|
238
239
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
239
240
|
- `action` (`create | update`): the action to perform: create or update an annotation
|
|
240
241
|
- Query:
|
|
241
|
-
- `throwOnCanvasIndexError` (`boolean`):
|
|
242
|
+
- `throwOnCanvasIndexError` (`boolean`): throw an error if there's a problem fetching the annotation's target canvas index. See [Appendix 1](#appendix-1-annotation-canvas-index-and-bounding-box-calculation)
|
|
243
|
+
- `throwOnXywhError` (`boolean`): throw an error if target bounding box calculation fails. See [Appendix 1](#appendix-1-annotation-canvas-index-and-bounding-box-calculation)
|
|
242
244
|
- Body (`Object`): a IIIF annotation that follows the IIIF Presentation API 2 or 3 (depending on the value of `iiif_version`)
|
|
243
245
|
|
|
244
246
|
#### Response
|
|
@@ -252,21 +254,6 @@ Create or update a single annotation
|
|
|
252
254
|
}
|
|
253
255
|
```
|
|
254
256
|
|
|
255
|
-
#### Notes
|
|
256
|
-
|
|
257
|
-
- A side effect of inserting annotations is inserting the related manifests.
|
|
258
|
-
- When inserting an annotation, the annotation's target manifest is also fetched and inserted in the database
|
|
259
|
-
- Annotations in `aiiinotate` contain 3 nonstandard fields. In IIIF presentation 2.x,
|
|
260
|
-
- `annotation.on[0].manifestUri`: the URI of the manifest on which is an annotation
|
|
261
|
-
- `annotation.on[0].manifestShortId`: the unique identifier of the manifest on which is an annotation
|
|
262
|
-
- `annotation.on[0].canvasIdx`: the position of an annotation's target canvas within the target manifest, as an integer
|
|
263
|
-
- this depends on reconstructing an annotation's target manifest URL and fetching it. If this process fails, the fields above will be `undefined`.
|
|
264
|
-
- the annotation's target's manifest is fetched and inserted in the database, if possible, and stored in `annotation.on[0].manifestShortId`
|
|
265
|
-
- 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.
|
|
266
|
-
- fetching an annotation's target manfest is error prone: it depends on the manifest being available through HTTP, which is not in our control.
|
|
267
|
-
- in turn, normally, if there's an error, we will just add the issue to `fetchErrorIds` and not throw.
|
|
268
|
-
- 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.
|
|
269
|
-
|
|
270
257
|
---
|
|
271
258
|
|
|
272
259
|
### Insert several annotations
|
|
@@ -279,8 +266,11 @@ Batch insert multiple annotations.
|
|
|
279
266
|
|
|
280
267
|
#### Request
|
|
281
268
|
|
|
282
|
-
-
|
|
269
|
+
- Parameters:
|
|
283
270
|
- `iiif_version` (`2 | 3`): the IIIF version of the annotation
|
|
271
|
+
- Query:
|
|
272
|
+
- `throwOnCanvasIndexError` (`boolean`): throw an error if there's a problem fetching the annotation's target canvas index. See [Appendix 1](#appendix-1-annotation-canvas-index-and-bounding-box-calculation)
|
|
273
|
+
- `throwOnXywhError` (`boolean`): throw an error if target bounding box calculation fails. See [Appendix 1](#appendix-1-annotation-canvas-index-and-bounding-box-calculation)
|
|
284
274
|
- Body: either:
|
|
285
275
|
- a full `AnnotationList | AnnotationPage` embedded in the body (type must match `iiif_version`: AnnotationPage for IIIF 3, AnnotationList for IIIF 2).
|
|
286
276
|
- `AnnotationList[] | AnnotationPage[]` (type must match `iiif_version`): an array of annotation lists or pages
|
|
@@ -305,7 +295,42 @@ Batch insert multiple annotations.
|
|
|
305
295
|
|
|
306
296
|
---
|
|
307
297
|
|
|
308
|
-
##
|
|
298
|
+
## Appendix 1: annotation canvas index and bounding box calculation
|
|
299
|
+
|
|
300
|
+
Two non-standard side-effects happen when inserting an annotation:
|
|
301
|
+
- the annotation's target manifest is also fetched and inserted in the database. The annotation's target canvas index is fetched and added to the annotation.
|
|
302
|
+
- calculating the XYWH bounding box of an annotation.
|
|
303
|
+
|
|
304
|
+
Annotations in `aiiinotate` contain nonstandard fields. In IIIF presentation 2.x,
|
|
305
|
+
- `manifestUri`, `manifestShortId`, `canvasIdx`:
|
|
306
|
+
- `annotation.on.manifestUri`: the URI of the manifest on which is an annotation
|
|
307
|
+
- `annotation.on.manifestShortId`: the unique identifier of the manifest on which is an annotation
|
|
308
|
+
- `annotation.on.canvasIdx`: the position of an annotation's target canvas within the target manifest, as an integer. `canvasIdx` is **0-indexed**: the 1st canvas in a manifest is indexed from `0`
|
|
309
|
+
- this relies on reconstructing an annotation's target manifest URL and fetching it. If this process fails, the fields above will be `undefined`.
|
|
310
|
+
- fetching an annotation's target manfest is error prone: it depends on the manifest being available through HTTP, which is not in our control.
|
|
311
|
+
- **you can control error throwing** (throw an error if a canvas index can't be found) using:
|
|
312
|
+
- at route-level, the `throwOnCanvasIndexError` query parameter
|
|
313
|
+
- at app-level, the `AIIINOTATE_STRICT_MODE` env variable.
|
|
314
|
+
- if `AIIINOTATE_STRICT_MODE` or `throwOnCanvasIndexError`, an error will be thrown if an error appears anywhere in the process of fetching the target manifest and populating the `canvasIdx` field.
|
|
315
|
+
- `annotation.on.xywh`: the annotation's bounding box on its target canvas.
|
|
316
|
+
- when inserting an annotation, aiiinotate will attempt to extract its XYWH bounding box and store it as an `[number,number,number,number]`.
|
|
317
|
+
- this only works for `oa:SvgSelectors`, `oa:FragmentSeletors`, `oa:Choice` containing either an `SvgSelector` or a `FragmentSelector`, or a string-target (an URI with a `#xywh=` fragment selector).
|
|
318
|
+
- **you can control error-throwing** (throw an error if a bounding-box can't be calculated) using:
|
|
319
|
+
- at route-level, the `throwOnXywhError` query parameter
|
|
320
|
+
- at app-level, the `AIIINOTATE_STRICT_MODE` env variable.
|
|
321
|
+
|
|
322
|
+
About `throwOnCanvasIndexError`, `throwOnXywhError` and `AIIINOTATE_STRICT_MODE`:
|
|
323
|
+
- as indicated above, fetching an annotation's target canvas index and calculating an annotation's target bounding boxc is error prone.
|
|
324
|
+
- in turn, you can decide wether or not to throw errors using:
|
|
325
|
+
- at route-level, the `throwOnCanvasIndexError` (for canvas index) and `throwOnXywhError` (for bounding box) query parameters
|
|
326
|
+
- at app-level, the `AIIINOTATE_STRICT_MODE` env variable. Setting this to `true` will set the value of `throwOnCanvasIndexError` and `throwOnXywhError` to `true` by default.
|
|
327
|
+
- the value of `AIIINOTATE_STRICT_MODE` can be overridden by setting a value to query parameters `throwOnCanvasIndexError` and `throwOnXywhError`.
|
|
328
|
+
- if those are set to `true`, an error will be thrown if a canvas index can't be found. Otherwise, the values will be set to `undefined`.
|
|
329
|
+
- 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.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Appendix 2: App logic: URL prefixes
|
|
309
334
|
|
|
310
335
|
URL anatomy is a mix of [SAS endpoints](./specifications/4_sas.md) and IIIF specifications. In turn, we define the following prefixes:
|
|
311
336
|
|
|
@@ -333,7 +358,7 @@ There is an extra URL prefix: `schemas`. It is only used internally (not accessi
|
|
|
333
358
|
|
|
334
359
|
---
|
|
335
360
|
|
|
336
|
-
## Appendix
|
|
361
|
+
## Appendix 3: IIIF URIs
|
|
337
362
|
|
|
338
363
|
IIIF URIs in the Presentation 2.1 API are:
|
|
339
364
|
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ 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,
|
|
9
|
+
import { ajvCompile, objectHasKey, isNullish, maybeToArray, visibleLog, memoize, STRICT_MODE } from "#utils/utils.js";
|
|
10
10
|
import { getManifestShortId, makeTarget, makeAnnotationId, toAnnotationList, canvasUriToManifestUri } from "#utils/iiif2Utils.js";
|
|
11
11
|
|
|
12
12
|
|
|
@@ -128,17 +128,24 @@ class Annotations2 extends CollectionAbstract {
|
|
|
128
128
|
* some of the work consists of translating what is defined by the OpenAnnotations standard to what is actually used by IIIF annotations.
|
|
129
129
|
* if `update`, some cleaning will be skipped (especially the redefinition of "@id"), otherwise updates would fail.
|
|
130
130
|
*
|
|
131
|
-
* @param {object} annotation
|
|
132
|
-
* @param {boolean} update - set to `true` if performing an update instead of an insert.
|
|
131
|
+
* @param {{ annotation: object, update: boolean, throwOnXywhError: boolean }} annotation
|
|
133
132
|
* @returns {object}
|
|
134
133
|
*/
|
|
135
|
-
async #cleanAnnotation(
|
|
134
|
+
async #cleanAnnotation({
|
|
135
|
+
annotation,
|
|
136
|
+
update=false,
|
|
137
|
+
throwOnXywhError=STRICT_MODE
|
|
138
|
+
}) {
|
|
136
139
|
// 1) extract ids and targets. convert the target to an array.
|
|
137
140
|
// we assume that all values of `annotationTargetArray` point to the same manifest => `manifestShortId` is extracted from the 1st target
|
|
138
141
|
const
|
|
139
142
|
annotationTargetArray = await makeTarget(annotation),
|
|
140
143
|
manifestShortId = annotationTargetArray[0].manifestShortId;
|
|
141
144
|
|
|
145
|
+
if ( throwOnXywhError && annotationTargetArray[0]?.xywh.some(x => isNaN(x)) ) {
|
|
146
|
+
throw this.insertError("annotations2.#cleanAnnotation: could not extract bounding box for annotation target", annotation.on);
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
// in updates, "@id" has aldready been extracted
|
|
143
150
|
if ( !update ) {
|
|
144
151
|
annotation["@id"] = makeAnnotationId(annotation, manifestShortId);
|
|
@@ -183,17 +190,17 @@ class Annotations2 extends CollectionAbstract {
|
|
|
183
190
|
* take an annotationList, clean it and return it as a array of annotations.
|
|
184
191
|
* see: https://iiif.io/api/presentation/2.1/#annotation-list
|
|
185
192
|
* @param {object} annotationList
|
|
186
|
-
* @
|
|
193
|
+
* @param {boolean} throwOnXywhError
|
|
194
|
+
* @returns {Promise<object[]>}
|
|
187
195
|
*/
|
|
188
|
-
async #cleanAnnotationList(annotationList) {
|
|
196
|
+
async #cleanAnnotationList(annotationList, throwOnXywhError=STRICT_MODE) {
|
|
189
197
|
// NOTE: if `this.#cleanAnnotationList` can only be accessed from annotations routes, then this check is useless (has aldready been performed).
|
|
190
198
|
if ( this.validatorAnnotationList(annotationList) ) {
|
|
191
199
|
this.errorNoAction("Annotations2.#cleanAnnotationList: could not recognize AnnotationList. see: https://iiif.io/api/presentation/2.1/#annotation-list.", annotationList)
|
|
192
200
|
}
|
|
193
|
-
//NOTE: using an arrow function is necessary to avoid losing the scope of `this`. otherwise, `this` is undefined in `#cleanAnnotation`.
|
|
194
201
|
return await Promise.all(
|
|
195
202
|
annotationList.resources.map(async (ressource) =>
|
|
196
|
-
await this.#cleanAnnotation(ressource)
|
|
203
|
+
await this.#cleanAnnotation({ annotation: ressource, throwOnXywhError })
|
|
197
204
|
)
|
|
198
205
|
)
|
|
199
206
|
}
|
|
@@ -206,7 +213,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
206
213
|
* @param {object|object[]} annotationData - an annotation, or array of annotations.
|
|
207
214
|
* @param {boolean} throwOnCanvasIndexError - if canvasIdx can't be found, raise an error.
|
|
208
215
|
*/
|
|
209
|
-
async #insertManifestsAndGetCanvasIdx(annotationData, throwOnCanvasIndexError=
|
|
216
|
+
async #insertManifestsAndGetCanvasIdx(annotationData, throwOnCanvasIndexError=STRICT_MODE) {
|
|
210
217
|
// NOTE: instead of propagating `throwOnCanvasIndexError` to `insertManifestsFromUriArray`, we could just check if `insertResponse.fetchErrorIds.length > 0` and return an error then.
|
|
211
218
|
// convert objects to array to get a uniform interface.
|
|
212
219
|
let converted;
|
|
@@ -322,12 +329,17 @@ class Annotations2 extends CollectionAbstract {
|
|
|
322
329
|
* when inserting, aiiinotate attempts to fetch the target manifest of an annotation and to add the canvas number of the annotation to `annotation.on`.
|
|
323
330
|
* this may fail for a number of reasons (manifest URL and JSON structure, server storing the manifest is inaccessible...). if `throwOnCanvasIndexError`, it will raise.
|
|
324
331
|
*
|
|
332
|
+
* about `throwOnXywhError`: XYWH bounding-box extraction of an annotation is not supported for all selectors (see: `selectorToXywh()`).
|
|
333
|
+
* by default, no error is raised if a bounding box can't be extracted.
|
|
334
|
+
* if `throwOnXywhError`, an error will be thrown. this is for controlled environments where you know exactly what you'll be sending aiiinotate and must rely on `xywh`
|
|
335
|
+
*
|
|
325
336
|
* @param {object} annotation
|
|
326
|
-
* @param {boolean
|
|
337
|
+
* @param {boolean} throwOnCanvasIndexError
|
|
338
|
+
* @param {boolean} throwOnXywhError
|
|
327
339
|
* @returns {Promise<InsertResponseType>}
|
|
328
340
|
*/
|
|
329
|
-
async insertAnnotation(annotation, throwOnCanvasIndexError=
|
|
330
|
-
annotation = await this.#cleanAnnotation(annotation);
|
|
341
|
+
async insertAnnotation(annotation, throwOnCanvasIndexError=STRICT_MODE, throwOnXywhError=STRICT_MODE) {
|
|
342
|
+
annotation = await this.#cleanAnnotation({ annotation, update: false, throwOnXywhError });
|
|
331
343
|
annotation = await this.#insertManifestsAndGetCanvasIdx(annotation, throwOnCanvasIndexError);
|
|
332
344
|
return this.insertOne(annotation);
|
|
333
345
|
}
|
|
@@ -336,11 +348,12 @@ class Annotations2 extends CollectionAbstract {
|
|
|
336
348
|
* TODO: handle side effects when changing `annotation.on`: changes that can affect `manifestShortId`, `manifestUri` and `canvasIdx`
|
|
337
349
|
* (for example, updating `annotation.on.full` would ask to change `canvasIdx`).
|
|
338
350
|
* @param {object} annotation
|
|
351
|
+
* @param {boolean} throwOnXywhError
|
|
339
352
|
* @returns {Promise<UpdateResponseType>}
|
|
340
353
|
*/
|
|
341
|
-
async updateAnnotation(annotation) {
|
|
354
|
+
async updateAnnotation(annotation, throwOnXywhError=STRICT_MODE) {
|
|
342
355
|
// necessary: on insert, the `@id` received is modified by `this.#cleanAnnotationList`.
|
|
343
|
-
annotation = await this.#cleanAnnotation(annotation, true);
|
|
356
|
+
annotation = await this.#cleanAnnotation({ annotation, update: true, throwOnXywhError });
|
|
344
357
|
const
|
|
345
358
|
query = { "@id": annotation["@id"] },
|
|
346
359
|
update = { $set: annotation };
|
|
@@ -358,9 +371,9 @@ class Annotations2 extends CollectionAbstract {
|
|
|
358
371
|
* @param {boolean?} throwOnCanvasIndexError
|
|
359
372
|
* @returns {Promise<InsertResponseType>}
|
|
360
373
|
*/
|
|
361
|
-
async insertAnnotationList(annotationList, throwOnCanvasIndexError) {
|
|
374
|
+
async insertAnnotationList(annotationList, throwOnCanvasIndexError=STRICT_MODE, throwOnXywhError=STRICT_MODE) {
|
|
362
375
|
let annotationArray;
|
|
363
|
-
annotationArray = await this.#cleanAnnotationList(annotationList);
|
|
376
|
+
annotationArray = await this.#cleanAnnotationList(annotationList, throwOnXywhError);
|
|
364
377
|
annotationArray = await this.#insertManifestsAndGetCanvasIdx(annotationArray, throwOnCanvasIndexError);
|
|
365
378
|
return this.insertMany(annotationArray);
|
|
366
379
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fastifyPlugin from "fastify-plugin"
|
|
2
2
|
|
|
3
|
-
import { pathToUrl, objectHasKey, maybeToArray,
|
|
3
|
+
import { pathToUrl, objectHasKey, maybeToArray, throwIfKeyUndefined, throwIfValueError, getFirstNonEmptyPair, visibleLog, STRICT_MODE } from "#utils/utils.js";
|
|
4
4
|
import { makeResponseSchema, makeResponsePostSchema, returnError, addPagination } from "#utils/routeUtils.js";
|
|
5
5
|
|
|
6
6
|
|
|
@@ -206,11 +206,12 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
206
206
|
action: { type: "string", enum: [ "create", "update" ] }
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
|
-
// NOTE: throwOnCanvasIndexError is only implemented if `action==="create"` (see preValidation)
|
|
209
|
+
// NOTE: `throwOnCanvasIndexError` is only implemented if `action==="create"` (see preValidation)
|
|
210
210
|
querystring: {
|
|
211
211
|
type: "object",
|
|
212
212
|
properties: {
|
|
213
|
-
throwOnCanvasIndexError: { type: "boolean" },
|
|
213
|
+
throwOnCanvasIndexError: { type: "boolean", default: STRICT_MODE },
|
|
214
|
+
throwOnXywhError: { type: "boolean", default: STRICT_MODE },
|
|
214
215
|
}
|
|
215
216
|
},
|
|
216
217
|
preValidation: async (request, reply) => {
|
|
@@ -218,7 +219,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
218
219
|
{ action } = request.params,
|
|
219
220
|
{ throwOnCanvasIndexError } = request.querystring;
|
|
220
221
|
if ( action==="update" && throwOnCanvasIndexError ) {
|
|
221
|
-
returnError(request, reply, "'throwOnCanvasIndexError' is only allowed when ':action' is 'create'.")
|
|
222
|
+
returnError(request, reply, "'throwOnCanvasIndexError' is only allowed when ':action' is 'create'.");
|
|
222
223
|
}
|
|
223
224
|
return;
|
|
224
225
|
},
|
|
@@ -229,7 +230,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
229
230
|
async (request, reply) => {
|
|
230
231
|
const
|
|
231
232
|
{ iiifPresentationVersion, action } = request.params,
|
|
232
|
-
{ throwOnCanvasIndexError } = request.query,
|
|
233
|
+
{ throwOnCanvasIndexError, throwOnXywhError } = request.query,
|
|
233
234
|
annotation = request.body;
|
|
234
235
|
|
|
235
236
|
try {
|
|
@@ -237,8 +238,8 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
237
238
|
// insert or update
|
|
238
239
|
if ( iiifPresentationVersion === 2 ) {
|
|
239
240
|
return action==="create"
|
|
240
|
-
? await annotations2.insertAnnotation(annotation, throwOnCanvasIndexError)
|
|
241
|
-
: await annotations2.updateAnnotation(annotation);
|
|
241
|
+
? await annotations2.insertAnnotation(annotation, throwOnCanvasIndexError, throwOnXywhError)
|
|
242
|
+
: await annotations2.updateAnnotation(annotation, throwOnXywhError);
|
|
242
243
|
} else {
|
|
243
244
|
annotations3.notImplementedError();
|
|
244
245
|
}
|
|
@@ -275,7 +276,8 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
275
276
|
querystring: {
|
|
276
277
|
type: "object",
|
|
277
278
|
properties: {
|
|
278
|
-
throwOnCanvasIndexError: { type: "boolean", },
|
|
279
|
+
throwOnCanvasIndexError: { type: "boolean", default: STRICT_MODE },
|
|
280
|
+
throwOnXywhError: { type: "boolean", default: STRICT_MODE },
|
|
279
281
|
}
|
|
280
282
|
},
|
|
281
283
|
body: routeAnnotationCreateManySchema,
|
|
@@ -285,7 +287,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
285
287
|
async (request, reply) => {
|
|
286
288
|
const
|
|
287
289
|
{ iiifPresentationVersion } = request.params,
|
|
288
|
-
{ throwOnCanvasIndexError } = request.query,
|
|
290
|
+
{ throwOnCanvasIndexError, throwOnXywhError } = request.query,
|
|
289
291
|
body = maybeToArray(request.body), // convert to an array to have a homogeneous data structure
|
|
290
292
|
insertResponseArray = [];
|
|
291
293
|
|
|
@@ -310,7 +312,7 @@ function annotationsRoutes(fastify, options, done) {
|
|
|
310
312
|
if ( iiifPresentationVersion === 2 ) {
|
|
311
313
|
await Promise.all(annotationsArray.map(
|
|
312
314
|
async (annotationList) => {
|
|
313
|
-
const r = await annotations2.insertAnnotationList(annotationList, throwOnCanvasIndexError);
|
|
315
|
+
const r = await annotations2.insertAnnotationList(annotationList, throwOnCanvasIndexError, throwOnXywhError);
|
|
314
316
|
insertResponseArray.push(r);
|
|
315
317
|
}
|
|
316
318
|
));
|
|
@@ -64,7 +64,7 @@ test("test annotation Routes", async (t) => {
|
|
|
64
64
|
for ( let i=0; i<data.length; i++ ) {
|
|
65
65
|
let [ testData, func ] = data.at(i);
|
|
66
66
|
for ( let i=0; i<testData.length; i++ ) {
|
|
67
|
-
await func(t, "/annotations/2/createMany", testData.at(i));
|
|
67
|
+
await func(t, "/annotations/2/createMany?throwOnXywhError=true", testData.at(i));
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -95,19 +95,10 @@ test("test annotation Routes", async (t) => {
|
|
|
95
95
|
]
|
|
96
96
|
for ( const [ testData, func ] of data ) {
|
|
97
97
|
for ( let i=0; i<testData.length; i++ ) {
|
|
98
|
-
await func(t, "/annotations/2/create", testData.at(i));
|
|
98
|
+
await func(t, "/annotations/2/create?throwOnXywhError=true", testData.at(i));
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// test throwOnCanvasIndexError
|
|
103
|
-
const annotationWithTargetError = structuredClone(fastify.fixtures.annotations2Valid.at(0));
|
|
104
|
-
annotationWithTargetError.on = `https://test/${uuid4()}#xywh=100,100,300,300`;
|
|
105
|
-
data = [[testPostRouteCreateFailure, true], [testPostRouteCreateSuccess, false]];
|
|
106
|
-
for (let i=0; i<data.length; i++) {
|
|
107
|
-
const [func, v] = data.at(i);
|
|
108
|
-
await func(t, `/annotations/2/create?throwOnCanvasIndexError=${v}`, annotationWithTargetError)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
102
|
// test SVG to XYWH conversion
|
|
112
103
|
// 1. insert and assert there was no mistake
|
|
113
104
|
// 2. retrieve the inserted annotation and check its xywh coordinates.
|
|
@@ -125,6 +116,34 @@ test("test annotation Routes", async (t) => {
|
|
|
125
116
|
t.assert.deepStrictEqual(target.xywh.every((i) => !isNaN(i)), true);
|
|
126
117
|
})
|
|
127
118
|
}
|
|
119
|
+
|
|
120
|
+
// test throwOnCanvasIndexError
|
|
121
|
+
let annotationError;
|
|
122
|
+
annotationError = structuredClone(fastify.fixtures.annotations2Valid.at(0));
|
|
123
|
+
annotationError.on = `https://test/${uuid4()}#xywh=100,100,300,300`;
|
|
124
|
+
data = [[testPostRouteCreateFailure, true], [testPostRouteCreateSuccess, false]];
|
|
125
|
+
for (let i=0; i<data.length; i++) {
|
|
126
|
+
const [func, v] = data.at(i);
|
|
127
|
+
await func(t, `/annotations/2/create?throwOnCanvasIndexError=${v}`, annotationError)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// test throwOnXywhError. SvgSelectors are not supported, so this should fail
|
|
131
|
+
annotationError = structuredClone(fastify.fixtures.annotations2Valid.at(0));
|
|
132
|
+
// NOTE : this is an SVG selector and not a CSS selector !!!!
|
|
133
|
+
annotationError.on = {
|
|
134
|
+
"@type": "oa:SpecificResource",
|
|
135
|
+
"full": "https://aikon.enpc.fr/aikon/iiif/v2/wit9_man11_anno165/canvas/c11.json",
|
|
136
|
+
selector: {
|
|
137
|
+
"@context": "http://iiif.io/api/annex/openannotation/context.json",
|
|
138
|
+
"@type": "iiif:ImageApiSelector",
|
|
139
|
+
"region": "50,50,1250,1850" }
|
|
140
|
+
}
|
|
141
|
+
data = [[testPostRouteCreateFailure, true], [testPostRouteCreateSuccess, false]];
|
|
142
|
+
for (let i=0; i<data.length; i++) {
|
|
143
|
+
const [func, v] = data.at(i);
|
|
144
|
+
await func(t, `/annotations/2/create?throwOnXywhError=${v}`, annotationError);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
})
|
|
129
148
|
|
|
130
149
|
await t.test("test route /annotations/:iiifPresentationVersion/update", async (t) => {
|
package/src/data/routes.test.js
CHANGED
|
@@ -35,51 +35,6 @@ test("test common routes", async (t) => {
|
|
|
35
35
|
////////////////////////////////////////////////
|
|
36
36
|
// GET routes
|
|
37
37
|
|
|
38
|
-
// NOTE: testing canvasMin/canvasMax parameters on the `search-api` would be good but is too complicated (would require storing the manifests on a live server so they can be queried through HTTP). so we drop the tests for now
|
|
39
|
-
// await t.test("test route /search-api/:iiifSearchVersion/manifests/:manifestShortId/search", async (t) => {
|
|
40
|
-
// const [manifest, annotationList] = fastify.fixtures.generateIiif2ManifestAndAnnotationsList(1000,1000);
|
|
41
|
-
// const rm = await injectPost(fastify, "/manifests/2/create", manifest);
|
|
42
|
-
// t.assert.deepStrictEqual(rm.statusCode, 200);
|
|
43
|
-
// const ra = await injectPost(fastify, "/annotations/2/createMany", annotationList);
|
|
44
|
-
// t.assert.deepStrictEqual(ra.statusCode, 200);
|
|
45
|
-
//
|
|
46
|
-
// // within annotationList, the number of annotations that are between canvases canvasMin and canvasMax
|
|
47
|
-
// const
|
|
48
|
-
// manifestShortId = getManifestShortId(manifest["@id"]),
|
|
49
|
-
// canvasMin = 400,
|
|
50
|
-
// canvasMax = 600,
|
|
51
|
-
// rangeCanvasIds =
|
|
52
|
-
// manifest.sequences[0].canvases
|
|
53
|
-
// .slice(canvasMin, canvasMax)
|
|
54
|
-
// .map((canvas) => canvas["@id"]),
|
|
55
|
-
// annotationCount = annotationList.resources.filter((annotation) =>
|
|
56
|
-
// rangeCanvasIds.includes(annotation.on.split("#")[0])
|
|
57
|
-
// ).length;
|
|
58
|
-
//
|
|
59
|
-
// const x = await injectGet(fastify, `/search-api/1/manifests/${manifestShortId}/search`);
|
|
60
|
-
// console.log(visibleLog(await x.json()));
|
|
61
|
-
// console.log(manifestShortId);
|
|
62
|
-
//
|
|
63
|
-
// // now, check that search-api with canvasMin / canvasMax has the same result as annotationCount
|
|
64
|
-
// const
|
|
65
|
-
// rCanvasRange = await injectGet(fastify, `/search-api/1/manifests/${manifestShortId}/search?canvasMin=${canvasMin}&canvasMax=${canvasMax}`),
|
|
66
|
-
// rCanvasRangeBody = await rCanvasRange.json(),
|
|
67
|
-
// rCanvasRangeCount = rCanvasRangeBody.length;
|
|
68
|
-
// console.log(rCanvasRangeBody);
|
|
69
|
-
// t.assert.deepStrictEqual(rCanvasRange.statusCode, 200);
|
|
70
|
-
// t.assert.deepStrictEqual(rCanvasRangeCount, annotationCount);
|
|
71
|
-
//
|
|
72
|
-
// // NOTE I THINK I'M GONNA HAVE TO DROP THE preExistingIds TESTING.
|
|
73
|
-
// // it's wayyyy to convoluted to handle dummy URLs pointing to nopn existant files.
|
|
74
|
-
// // in real life, canvasMin/canvasMax do seem to work.
|
|
75
|
-
//
|
|
76
|
-
// // q
|
|
77
|
-
// // motivation
|
|
78
|
-
// // canvasMin
|
|
79
|
-
// // canvasMax
|
|
80
|
-
//
|
|
81
|
-
// })
|
|
82
|
-
|
|
83
38
|
await t.test("test route /search-api/:iiifPresentationVersion/manifests/:manifestShortId/search", async (t) => {
|
|
84
39
|
await injectTestAnnotations(fastify, t, annotationList);
|
|
85
40
|
|
|
@@ -7,7 +7,7 @@ import { IIIF_PRESENTATION_2, IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUti
|
|
|
7
7
|
// TODO: schemas are maybe wayyy too strict.
|
|
8
8
|
// convert all enums (the arrays below) to { type: string } ?
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const oaSelectorValues = [
|
|
11
11
|
"oa:FragmentSelector",
|
|
12
12
|
"oa:CssSelector",
|
|
13
13
|
"oa:XPathSelector",
|
|
@@ -67,14 +67,6 @@ const getSchema = (fastify, slug) =>
|
|
|
67
67
|
|
|
68
68
|
function addSchemas(fastify, options, done) {
|
|
69
69
|
|
|
70
|
-
/////////////////////////////////////////////
|
|
71
|
-
// GENERIC STUFF
|
|
72
|
-
|
|
73
|
-
fastify.addSchema({
|
|
74
|
-
$id: makeSchemaUri("context"),
|
|
75
|
-
type: "string",
|
|
76
|
-
enum: [ IIIF_PRESENTATION_2_CONTEXT["@context"] ]
|
|
77
|
-
});
|
|
78
70
|
|
|
79
71
|
/////////////////////////////////////////////
|
|
80
72
|
// SPECIFIC RESOURCES
|
|
@@ -83,14 +75,14 @@ function addSchemas(fastify, options, done) {
|
|
|
83
75
|
fastify.addSchema({
|
|
84
76
|
$id: makeSchemaUri("iiifImageApiSelector"),
|
|
85
77
|
type: "object",
|
|
86
|
-
required: [ "@
|
|
78
|
+
required: [ "@type", "@context" ],
|
|
87
79
|
properties: {
|
|
88
80
|
"@id": { type: "string" },
|
|
89
81
|
"@type": {
|
|
90
82
|
type: "string",
|
|
91
83
|
enum: [ "iiif:ImageApiSelector" ]
|
|
92
84
|
},
|
|
93
|
-
"@context": {
|
|
85
|
+
"@context": { type: "string" },
|
|
94
86
|
region: { type: "string" },
|
|
95
87
|
size: { type: "string" },
|
|
96
88
|
rotation: { type: "string" },
|
|
@@ -108,17 +100,17 @@ function addSchemas(fastify, options, done) {
|
|
|
108
100
|
properties: {
|
|
109
101
|
"@id": { type: "string" },
|
|
110
102
|
"@type": {
|
|
111
|
-
|
|
103
|
+
oneOf: [
|
|
112
104
|
{
|
|
113
105
|
type: "string",
|
|
114
|
-
enum:
|
|
106
|
+
enum: oaSelectorValues
|
|
115
107
|
},
|
|
116
108
|
{
|
|
117
109
|
// IIIF 2.1 has examples with multiple `@types`: // `chars` is used by SvgSelector in: https://iiif.io/api/presentation/2.1/#non-rectangular-segments
|
|
118
110
|
type: "array",
|
|
119
111
|
items: {
|
|
120
112
|
type: "string",
|
|
121
|
-
enum:
|
|
113
|
+
enum: oaSelectorValues
|
|
122
114
|
}
|
|
123
115
|
}
|
|
124
116
|
]
|
|
@@ -204,8 +196,13 @@ function addSchemas(fastify, options, done) {
|
|
|
204
196
|
]
|
|
205
197
|
},
|
|
206
198
|
xywh: {
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
oneOf: [
|
|
200
|
+
{
|
|
201
|
+
type: "array",
|
|
202
|
+
items: { type: "number" }
|
|
203
|
+
},
|
|
204
|
+
{ type: "null" }
|
|
205
|
+
]
|
|
209
206
|
},
|
|
210
207
|
selector: { $ref: makeSchemaUri("selector") },
|
|
211
208
|
purpose: { type: "string" }
|
|
@@ -310,7 +307,7 @@ function addSchemas(fastify, options, done) {
|
|
|
310
307
|
required: [ "@id", "@context", "@type", "motivation", "on" ],
|
|
311
308
|
properties: {
|
|
312
309
|
"@id": { type: "string" },
|
|
313
|
-
"@context": {
|
|
310
|
+
"@context": { type: "string" },
|
|
314
311
|
"@type": { type: "string", enum: [ "oa:Annotation" ] },
|
|
315
312
|
motivation: { $ref: makeSchemaUri("motivation") },
|
|
316
313
|
on: { $ref: makeSchemaUri("annotationTarget") },
|
|
@@ -327,7 +324,7 @@ function addSchemas(fastify, options, done) {
|
|
|
327
324
|
required: ["@id", "@type", "@context", "resources"],
|
|
328
325
|
properties: {
|
|
329
326
|
"@id": { type: "string" },
|
|
330
|
-
"@context": {
|
|
327
|
+
"@context": { type: "string" },
|
|
331
328
|
"@type": {
|
|
332
329
|
type: "string",
|
|
333
330
|
enum: [ "sc:AnnotationList" ]
|
|
@@ -403,7 +400,7 @@ function addSchemas(fastify, options, done) {
|
|
|
403
400
|
type: "object",
|
|
404
401
|
required: [ "@id", "@type", "@context", "members" ],
|
|
405
402
|
properties: {
|
|
406
|
-
"@context": {
|
|
403
|
+
"@context": { type: "string" },
|
|
407
404
|
"@type": { type: "string", enum: [ "sc:Collection" ] },
|
|
408
405
|
"@id": { type: "string" },
|
|
409
406
|
label: { type: "string" },
|
|
@@ -17,19 +17,13 @@ const getSchema = (fastify, slug) =>
|
|
|
17
17
|
|
|
18
18
|
function addSchemas(fastify, options, done) {
|
|
19
19
|
|
|
20
|
-
fastify.addSchema({
|
|
21
|
-
$id: makeSchemaUri("context"),
|
|
22
|
-
type: "string",
|
|
23
|
-
enum: [ IIIF_PRESENTATION_3_CONTEXT["@context"] ]
|
|
24
|
-
});
|
|
25
|
-
|
|
26
20
|
// minimal schema for IIIF manifests3, containing just what we need to process a manifest
|
|
27
21
|
fastify.addSchema({
|
|
28
22
|
$id: makeSchemaUri("manifestPublic"),
|
|
29
23
|
type: "object",
|
|
30
24
|
required: [ "@context", "id", "items" ],
|
|
31
25
|
properties: {
|
|
32
|
-
"@context": {
|
|
26
|
+
"@context": { type: "string" },
|
|
33
27
|
id: { type: "string" },
|
|
34
28
|
items: {
|
|
35
29
|
type: "array",
|
package/src/utils/iiif2Utils.js
CHANGED
|
@@ -193,16 +193,17 @@ const stringToSpecificResource = (target) => {
|
|
|
193
193
|
* @returns {Promise<object>}
|
|
194
194
|
*/
|
|
195
195
|
const makeSingleTarget = async (target) => {
|
|
196
|
-
const err = new Error(`${makeSingleTarget.name}: could not make target for annotation: 'annotation.on' must be an URI, a
|
|
196
|
+
const err = new Error(`${makeSingleTarget.name}: could not make target for annotation: 'annotation.on' must be an URI, a SpecificResource or an array of SpecificResources. The key 'SpecificResource.full' MUST be defined.`, { info: target });
|
|
197
197
|
|
|
198
198
|
let specificResource;
|
|
199
199
|
|
|
200
|
-
// 1. convert to
|
|
200
|
+
// 1. convert to SpecificResource
|
|
201
201
|
if ( typeof(target) === "string" && !isNullish(target) ) {
|
|
202
202
|
specificResource = stringToSpecificResource(target);
|
|
203
203
|
} else if ( isObject(target) && target["@type"] === "oa:SpecificResource" && !isNullish(target["full"]) ) {
|
|
204
204
|
specificResource = target;
|
|
205
205
|
specificResource.selector = normalizeSelectorType(specificResource.selector);
|
|
206
|
+
|
|
206
207
|
// if target is neither a string nor a SpecificResource, raise
|
|
207
208
|
} else {
|
|
208
209
|
throw err
|
package/src/utils/utils.js
CHANGED
|
@@ -20,7 +20,7 @@ const isNullOrUndefined = (v) => v == null;
|
|
|
20
20
|
const isNullish = (v) => v == null || !v.length;
|
|
21
21
|
|
|
22
22
|
/** o is an object but not an array. https://stackoverflow.com/a/44556453 */
|
|
23
|
-
const isObject = (o) => o
|
|
23
|
+
const isObject = (o) => o?.constructor === Object;
|
|
24
24
|
|
|
25
25
|
const isNonEmptyArray = (a) => Array.isArray(a) && a.length;
|
|
26
26
|
|
|
@@ -305,6 +305,8 @@ const memoize = (fn, timeout = 2000) => {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
const STRICT_MODE = process.env.AIIINOTATE_STRICT_MODE?.toLowerCase() === "true";
|
|
309
|
+
|
|
308
310
|
export {
|
|
309
311
|
maybeToArray,
|
|
310
312
|
pathToUrl,
|
|
@@ -323,5 +325,6 @@ export {
|
|
|
323
325
|
visibleLog,
|
|
324
326
|
isNonEmptyArray,
|
|
325
327
|
mergeObjects,
|
|
326
|
-
memoize
|
|
328
|
+
memoize,
|
|
329
|
+
STRICT_MODE
|
|
327
330
|
}
|