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.
@@ -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
- - See section **Create/update an annotation** for more information and possible issues with canvas indexes.
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`): if there is an error fetching the related manifest, or getting a target canvas' index, throw an error.
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
- - Query:
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
- ## Appending 1: App logic: URL prefixes
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 2: IIIF URIs
361
+ ## Appendix 3: IIIF URIs
337
362
 
338
363
  IIIF URIs in the Presentation 2.1 API are:
339
364
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiiinotate",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "a fast IIIF-compliant annotation server",
5
5
  "main": "./cli/index.js",
6
6
  "type": "module",
@@ -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, inspectObj, visibleLog, memoize } from "#utils/utils.js";
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(annotation, update=false) {
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
- * @returns {object[]}
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=false) {
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?} throwOnCanvasIndexError
337
+ * @param {boolean} throwOnCanvasIndexError
338
+ * @param {boolean} throwOnXywhError
327
339
  * @returns {Promise<InsertResponseType>}
328
340
  */
329
- async insertAnnotation(annotation, throwOnCanvasIndexError=false) {
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, inspectObj, throwIfKeyUndefined, throwIfValueError, getFirstNonEmptyPair, visibleLog } from "#utils/utils.js";
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) => {
@@ -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 oaSelectorTypes = [
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: [ "@id", "@type", "@context" ],
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": { $ref: makeSchemaUri("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
- anyOf: [
103
+ oneOf: [
112
104
  {
113
105
  type: "string",
114
- enum: oaSelectorTypes
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: oaSelectorTypes
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
- type: "array",
208
- items: { type: "number" }
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": { $ref: makeSchemaUri("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": { $ref: makeSchemaUri("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": { $ref: makeSchemaUri("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": { $ref: makeSchemaUri("context") },
26
+ "@context": { type: "string" },
33
27
  id: { type: "string" },
34
28
  items: {
35
29
  type: "array",
@@ -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 SpecificResouece or an array of SpecificResources`, { info: target });
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 SpecificResouece
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
@@ -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.constructor === Object;
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
  }