aiiinotate 0.4.3 → 0.5.0

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