aiiinotate 0.2.15 → 0.3.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.
Files changed (28) hide show
  1. package/docker/docker.sh +1 -1
  2. package/docs/architecture.md +12 -12
  3. package/package.json +3 -3
  4. package/scripts/setup_mongodb.sh +10 -1
  5. package/scripts/utils.sh +65 -0
  6. package/src/app.js +2 -2
  7. package/src/data/annotations/annotations2.js +32 -30
  8. package/src/data/annotations/routes.js +2 -1
  9. package/src/data/annotations/routes.test.js +4 -4
  10. package/src/data/manifests/manifests2.test.js +1 -1
  11. package/src/data/manifests/routes.test.js +1 -1
  12. package/src/data/routes.test.js +5 -5
  13. package/src/data/utils/iiif2Utils.js +38 -30
  14. package/src/data/utils/iiif2Utils.test.js +1 -1
  15. package/src/{fileServer → fixtures}/annotations.js +3 -3
  16. package/src/{fileServer → fixtures}/index.js +8 -8
  17. package/src/{fileServer → fixtures}/manifests.js +1 -1
  18. package/src/{fileServer → fixtures}/utils.js +3 -3
  19. package/src/schemas/schemasPresentation2.js +9 -6
  20. package/src/schemas/schemasRoutes.js +28 -2
  21. /package/src/{fileServer → fixtures}/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +0 -0
  22. /package/src/{fileServer → fixtures}/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +0 -0
  23. /package/src/{fileServer → fixtures}/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +0 -0
  24. /package/src/{fileServer → fixtures}/data/annotations2Invalid.jsonld +0 -0
  25. /package/src/{fileServer → fixtures}/data/annotations2Valid.jsonld +0 -0
  26. /package/src/{fileServer → fixtures}/data/bnf_invalid_manifest.json +0 -0
  27. /package/src/{fileServer → fixtures}/data/bnf_valid_manifest.json +0 -0
  28. /package/src/{fileServer → fixtures}/data/vhs_wit253_man253_anno253_anno-24.json +0 -0
package/docker/docker.sh CHANGED
@@ -45,7 +45,7 @@ env_to_docker() {
45
45
  # NOTE that the MongoDB host in Docker MUST BE the name of the Mongo docker service (defined in docker-compose)
46
46
  cp "$ENV_CONFIG" "$ENV_DOCKER";
47
47
  sed_repl s~^MONGODB_HOST=.*$~MONGODB_HOST=mongo~ "$ENV_DOCKER";
48
- sed_repl s~^AIIINOTATE_HOST=.*~AIIINOTATE_HOST=0.0.0.0~ "$ENV_DOCKER";
48
+ sed_repl s~^AIIINOTATE_HOST=.*~AIIINOTATE_HOST=0.0.0.0~ "$ENV_DOCKER"
49
49
  }
50
50
  env_to_docker;
51
51
 
@@ -6,19 +6,19 @@ This is a high-level overview of the Aiiinotate and a good place to start if you
6
6
 
7
7
  ## Top level plugins
8
8
 
9
- Fastify uses a [**plugin system**](https://fastify.dev/docs/latest/Guides/Plugins-Guide/). In short, it means that,
9
+ Fastify uses a [**plugin system**](https://fastify.dev/docs/latest/Guides/Plugins-Guide/). In short, it means that,
10
10
  - to access the global `fastify` instance, your code needs to be a plugin
11
11
  - all plugins must export a single function that registers the plugin on the global fastify instance
12
12
 
13
- This can be quite convoluted and conflict with a "normal" module architecture, so we do a mix of the two:
13
+ This can be quite convoluted and conflict with a "normal" module architecture, so we do a mix of the two:
14
14
  - **all modules at the root** of `src/` are plugins (except `config`)
15
- - **nested plugins** within each of the above are defined when they need to access the fastify instance.
15
+ - **nested plugins** within each of the above are defined when they need to access the fastify instance.
16
16
 
17
17
  ```
18
- ./src/fileServer/ - module storing and serving data files for test fixtures
18
+ ./src/fixtures/ - module storing and serving data files for test fixtures
19
19
  ./src/schemas/ - JsonSchemas definition and validation
20
20
  ./src/db/ - connects the app to the mongojs database
21
- ./src/data/ - routes and modules to read/write data
21
+ ./src/data/ - routes and modules to read/write data
22
22
 
23
23
  ```
24
24
 
@@ -38,13 +38,13 @@ Each `route` and `class` is a fastify plugin. Since the `data` plugin is registe
38
38
 
39
39
  ```
40
40
  ├── index.js // `data` plugin root
41
- ├── routes.js // generic routes
41
+ ├── routes.js // generic routes
42
42
  ├── annotations
43
43
  │   ├── annotations2.js // plugin for IIIF presentation 2 annotations
44
- │   ├── annotations3.js // plugin for IIIF presentation 3 annotations
44
+ │   ├── annotations3.js // plugin for IIIF presentation 3 annotations
45
45
  │   ├── routes.js // routes for both plugins
46
46
  ├── collectionAbstract.js // abstract class with functionnalities for all plugins
47
- ├── manifests
47
+ ├── manifests
48
48
     ├── manifests2.js // plugin for IIIF presentation 2 manifests
49
49
     ├── manifests3.js // plugin for IIIF presentation 3 manifests
50
50
     ├── routes.js // routes for both plugins
@@ -57,9 +57,9 @@ At a high level, `route` files receive data and relegate internal mongoJS intera
57
57
 
58
58
  Our 5 `collection classes` (`collectionAbstract`, `annotations2`, `annotations3`, `manifests2`, `manifests3`) use class inheritence: all classes inherit from `collectionAbstract`
59
59
  - `collectionAbstract` defines collection-agnostic processes that can be used by all other classes.
60
- - other classes implement functionnalities for a specific data type and collection.
60
+ - other classes implement functionnalities for a specific data type and collection.
61
61
 
62
- ---
62
+ ---
63
63
 
64
64
  ## Details: why plugins are complicated ?
65
65
 
@@ -69,13 +69,13 @@ If using a plugin-only architecture, **you should not do "local" imports** (impo
69
69
  // here, we define a root plugin with 2 subplugins (1st is a decorator, 2nd a normal plugin).
70
70
  fastify.register((instance, opts, done) => {
71
71
 
72
- // decorator
72
+ // decorator
73
73
  instance.decorate('util', (a, b) => a + b)
74
74
  console.log(instance.util('that is ', 'awesome'))
75
75
 
76
76
  // plugin
77
77
  fastify.register((instance, opts, done) => {
78
- console.log(instance.util('that is ', 'awesome'))
78
+ console.log(instance.util('that is ', 'awesome'))
79
79
  done()
80
80
  })
81
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiiinotate",
3
- "version": "0.2.15",
3
+ "version": "0.3.0",
4
4
  "description": "a fast IIIF-compliant annotation server",
5
5
  "main": "./cli/index.js",
6
6
  "type": "module",
@@ -47,8 +47,8 @@
47
47
  "#config/*.js": "./config/*.js",
48
48
  "#data/*.js": "./src/data/*.js",
49
49
  "#utils/*.js": "./src/data/utils/*.js",
50
- "#fileServer/*.js": "./src/fileServer/*.js",
51
- "#fileServer/*.json": "./src/fileServer/*.json",
50
+ "#fixtures/*.js": "./src/fixtures/*.js",
51
+ "#fixtures/*.json": "./src/fixtures/*.json",
52
52
  "#manifests/*.js": "./src/data/manifests/*.js",
53
53
  "#annotations/*.js": "./src/data/annotations/*.js"
54
54
  },
@@ -1,11 +1,20 @@
1
1
  #!/bin/env bash
2
2
 
3
+ set -e
4
+
3
5
  # mongo install guide: https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/#std-label-install-mdb-community-ubuntu
4
6
 
5
7
  #NOTE only the mongodb installation is done here. for database creation, see `setup_mongodb_populate.sh`
6
8
 
7
9
  SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
8
- source "$SCRIPT_DIR/utils.sh";
10
+ source "$SCRIPT_DIR/utils.sh"
11
+
12
+ # float arithmetic comparison is not supported by bash and we need to use `bc`
13
+ # usage: if float_comparison "a >= b"; then... ; fi
14
+ float_comparison () {
15
+ expr="$1"
16
+ (( $(echo "$expr" |bc -l) ));
17
+ }
9
18
 
10
19
  install_mongodb_ubuntu () {
11
20
 
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+
3
+ color_echo() {
4
+ Color_Off="\033[0m"
5
+ Red="\033[1;91m" # Red
6
+ Green="\033[1;92m" # Green
7
+ Yellow="\033[1;93m" # Yellow
8
+ Blue="\033[1;94m" # Blue
9
+ Purple="\033[1;95m" # Purple
10
+ Cyan="\033[1;96m" # Cyan
11
+
12
+ case "$1" in
13
+ "green") echo -e "$Green$2$Color_Off";;
14
+ "red") echo -e "$Red$2$Color_Off";;
15
+ "blue") echo -e "$Blue$2$Color_Off";;
16
+ "yellow") echo -e "$Yellow$2$Color_Off";;
17
+ "purple") echo -e "$Purple$2$Color_Off";;
18
+ "cyan") echo -e "$Cyan$2$Color_Off";;
19
+ *) echo "$2";;
20
+ esac
21
+ }
22
+
23
+ echo_title(){
24
+ sep_line="========================================"
25
+ len_title=${#1}
26
+
27
+ if [ "$len_title" -gt 40 ]; then
28
+ sep_line=$(printf "%0.s=" $(seq 1 $len_title))
29
+ title="$1"
30
+ else
31
+ diff=$((38 - len_title))
32
+ half_diff=$((diff / 2))
33
+ sep=$(printf "%0.s=" $(seq 1 $half_diff))
34
+
35
+ if [ $((diff % 2)) -ne 0 ]; then
36
+ title="$sep $1 $sep="
37
+ else
38
+ title="$sep $1 $sep"
39
+ fi
40
+ fi
41
+
42
+ color_echo purple "\n\n$sep_line\n$title\n$sep_line"
43
+ }
44
+
45
+ get_os() {
46
+ uname_out="$(uname -s)"
47
+ case "${uname_out}" in
48
+ Linux*) os=Linux;;
49
+ Darwin*) os=Mac;;
50
+ CYGWIN*) os=Cygwin;;
51
+ MINGW*) os=MinGw;;
52
+ MSYS_NT*) os=Git;;
53
+ *) os="UNKNOWN:${unameOut}"
54
+ esac
55
+ echo "${os}"
56
+ }
57
+ export OS
58
+ OS=$(get_os)
59
+
60
+ # float arithmetic comparison is not supported by bash and we need to use `bc`
61
+ # usage: if float_comparison "a >= b"; then... ; fi
62
+ float_comparison () {
63
+ expr="$1"
64
+ (( $(echo "$expr" |bc -l) ));
65
+ }
package/src/app.js CHANGED
@@ -6,7 +6,7 @@ import Fastify from "fastify";
6
6
  import cors from "@fastify/cors";
7
7
  // import swagger from "@fastify/swagger";
8
8
 
9
- import fileServer from "#fileServer/index.js";
9
+ import fixtures from "#fixtures/index.js";
10
10
  import schemas from "#schemas/index.js";
11
11
  import data from "#data/index.js";
12
12
  import db from "#db/index.js";
@@ -101,7 +101,7 @@ async function build(mode="default") {
101
101
  });
102
102
 
103
103
  await fastify.register(db, mongoConfig);
104
- await fastify.register(fileServer);
104
+ await fastify.register(fixtures);
105
105
  fastify.register(schemas);
106
106
  fastify.register(data);
107
107
  await fastify.ready();
@@ -106,18 +106,18 @@ class Annotations2 extends CollectionAbstract {
106
106
  * @returns {object}
107
107
  */
108
108
  #cleanAnnotation(annotation, update=false) {
109
- // 1) extract ids and targets
109
+ // 1) extract ids and targets. convert the target to an array.
110
110
  const
111
- annotationTarget = makeTarget(annotation),
112
- manifestShortId = getManifestShortId(annotationTarget.full);
111
+ annotationTargetArray = makeTarget(annotation),
112
+ // we assume that all values of `annotationTargetArray` point to the same manifest => take the manifest of the 1st target
113
+ manifestShortId = annotationTargetArray[0].manifestShortId;
113
114
 
114
115
  // in updates, "@id" has aldready been extracted
115
116
  if ( !update ) {
116
117
  annotation["@id"] = makeAnnotationId(annotation, manifestShortId);
117
118
  }
118
119
  annotation["@context"] = IIIF_PRESENTATION_2_CONTEXT["@context"];
119
- annotation.on = annotationTarget;
120
- annotation.on.manifestShortId = manifestShortId;
120
+ annotation.on = annotationTargetArray;
121
121
 
122
122
  // 2) process motivations.
123
123
  // - motivations are an array of strings
@@ -170,26 +170,25 @@ class Annotations2 extends CollectionAbstract {
170
170
  /**
171
171
  * handle all side effects on the `manifests2` collection. this does 2 things:
172
172
  * - insert all manifests referenced by `annotationData`, and set a key `on.manifestId` on all annotations.
173
- * - set a key `on.canvasIdx`, containing the position of the annotation's target canvas in the manifest,
173
+ * - set a key `canvasIdx` in all values of `annotation.on`, containing the position of the annotation's target canvas in the manifest,
174
174
  * (or undefined if the manifest or canvas were not found).
175
175
  * @param {object|object[]} annotationData - an annotation, or array of annotations.
176
176
  */
177
177
  async #insertManifestsAndGetCanvasIdx(annotationData) {
178
178
  // TODO : extract all canvas Ids, reconstruct manifest IDs from it. if they're valid, insert the manifests into the db.
179
179
  // convert objects to array to get a uniform interface.
180
- let converted, manifestUris;
180
+ let converted
181
181
  [ annotationData, converted ] = maybeToArray(annotationData, true);
182
182
 
183
- // extract all manifest URIs and add them to `annotationData`
184
- annotationData = annotationData.map((ann) => {
185
- ann.on.manifestUri = canvasUriToManifestUri(ann.on.full);
186
- return ann;
187
- })
183
+ // 1. get all distinct manifest URIs
184
+ const manifestUris = [];
185
+ annotationData.map((ann) => ann.on.map((target) => {
186
+ if ( target.manifestUri !== undefined && !manifestUris.includes(target.manifestUri) ) {
187
+ manifestUris.push(target.manifestUri);
188
+ }
189
+ }));
188
190
 
189
- // get all distinct manifest URIs, and insert them.
190
- manifestUris = [...new Set(
191
- annotationData.map((ann) => ann.on.manifestUri)
192
- )];
191
+ // 2. insert the manifests
193
192
  // NOTE: PERFORMANCE significantly drops because of this: test running for the entire app goes from ~1000ms to ~2600ms
194
193
  const
195
194
  insertResponse = await this.manifestsPlugin.insertManifestsFromUriArray(manifestUris, false),
@@ -197,19 +196,25 @@ class Annotations2 extends CollectionAbstract {
197
196
  insertedManifestsIds = insertResponse.insertedIds.concat(insertResponse.preExistingIds || []);
198
197
 
199
198
  // 3. update annotations with 2 things:
200
- // - where manifest insertion has failed, set `annotation.on.manifestUri` to undefined
199
+ // - where manifest insertion has failed, set `manifestUri` to undefined on all values of `annotation.on`
201
200
  // - set `annotation.on.canvasIdx`: the position of the target canvas within the manifest, or undefined if it cound not be found.
202
201
  annotationData = await Promise.all(
203
202
  annotationData.map(async (ann) => {
204
- ann.on.manifestUri =
205
- // has the insertion of `manifestUri` worked ? (has it returned a valid response, woth `insertedIds` key).
206
- insertedManifestsIds.find((x) => x === ann.on.manifestUri)
207
- ? ann.on.manifestUri
208
- : undefined;
209
- ann.on.canvasIdx =
210
- ann.on.manifestUri
211
- ? await this.manifestsPlugin.getCanvasIdx(ann.on.manifestUri, ann.on.full)
212
- : undefined;
203
+ ann.on = await Promise.all(
204
+ ann.on.map(async (target) => {
205
+ target.manifestUri =
206
+ // has the insertion of `manifestUri` worked ? (has it returned a valid response, woth `insertedIds` key).
207
+ insertedManifestsIds.find((x) => x === target.manifestUri)
208
+ ? target.manifestUri
209
+ : undefined;
210
+ target.canvasIdx =
211
+ target.manifestUri
212
+ ? await this.manifestsPlugin.getCanvasIdx(target.manifestUri, target.full)
213
+ : undefined;
214
+ // console.log(">>> #insertManifestsAndGetCanvasIdx: target", target);
215
+ return target;
216
+ })
217
+ )
213
218
  return ann;
214
219
  })
215
220
  );
@@ -282,7 +287,6 @@ class Annotations2 extends CollectionAbstract {
282
287
  : deleteKey==="canvasUri"
283
288
  ? { "on.full": deleteVal }
284
289
  : { "on.manifestShortId": deleteVal };
285
-
286
290
  return this.delete(deleteFilter);
287
291
  }
288
292
 
@@ -391,9 +395,7 @@ class Annotations2 extends CollectionAbstract {
391
395
  * @returns
392
396
  */
393
397
  async findByCanvasUri(queryUrl, canvasUri, asAnnotationList=false) {
394
- const annotations = await this.find({
395
- "on.full": canvasUri
396
- });
398
+ const annotations = await this.find({ "on.full": canvasUri });
397
399
  return asAnnotationList
398
400
  ? toAnnotationList(annotations, queryUrl, `annotations targeting canvas ${canvasUri}`)
399
401
  : annotations;
@@ -65,6 +65,7 @@ function annotationsRoutes(fastify, options, done) {
65
65
  /** @type {Annotations3InstanceType} */
66
66
  annotations3 = fastify.annotations3,
67
67
  iiifPresentationVersionSchema = fastify.schemasBase.getSchema("presentation"),
68
+ routeAnnotation2Or3Schema = fastify.schemasRoutes.getSchema("routeAnnotation2Or3"),
68
69
  routeAnnotationCreateManySchema = fastify.schemasRoutes.getSchema("routeAnnotationCreateMany"),
69
70
  iiifAnnotationListSchema = fastify.schemasPresentation2.getSchema("annotationList"),
70
71
  iiifAnnotation2ArraySchema = fastify.schemasPresentation2.getSchema("annotationArray"),
@@ -169,7 +170,7 @@ function annotationsRoutes(fastify, options, done) {
169
170
  action: { type: "string", enum: [ "create", "update" ] }
170
171
  }
171
172
  },
172
- body: { type: "object" } /* routeAnnotations2Or3Schema */,
173
+ body: routeAnnotation2Or3Schema,
173
174
  response: responsePostSchema
174
175
  },
175
176
  },
@@ -31,7 +31,7 @@ test("test annotation Routes", async (t) => {
31
31
  annotationListArray,
32
32
  annotationListUriInvalid,
33
33
  annotationListUriArrayInvalid
34
- } = fastify.fileServer;
34
+ } = fastify.fixtures;
35
35
 
36
36
  await fastify.ready();
37
37
  // close the app after running the tests
@@ -71,8 +71,8 @@ test("test annotation Routes", async (t) => {
71
71
  await t.test("test route /annotations/:iiifPresentationVersion/create", async (t) => {
72
72
  //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
73
  const data = [
74
- [fastify.fileServer.annotations2Valid, testPostRouteCreateSuccess],
75
- [fastify.fileServer.annotations2Invalid, testPostRouteCreateFailure],
74
+ [fastify.fixtures.annotations2Valid, testPostRouteCreateSuccess],
75
+ [fastify.fixtures.annotations2Invalid, testPostRouteCreateFailure],
76
76
  ]
77
77
  for ( let i=0; i<data.length; i++ ) {
78
78
  let [ testData, func ] = data.at(i);
@@ -126,7 +126,7 @@ test("test annotation Routes", async (t) => {
126
126
  annotation = await getRandomItem(
127
127
  await fastify.mongo.db.collection("annotations2").find().toArray()
128
128
  ),
129
- canvasId = annotation.on.full,
129
+ canvasId = getRandomItem(annotation.on).full,
130
130
  r = await fastify.inject({
131
131
  method: "GET",
132
132
  url: `/annotations/2/search?uri=${canvasId}&asAnnotationList=${asAnnotationList}`
@@ -17,7 +17,7 @@ test("test Manifests2 module", async (t) => {
17
17
  manifest2ValidUri,
18
18
  manifest2Invalid,
19
19
  manifest2InvalidUri
20
- } = fastify.fileServer;
20
+ } = fastify.fixtures;
21
21
 
22
22
  await fastify.ready();
23
23
  // close the app after running the tests
@@ -20,7 +20,7 @@ test("test manifests Routes", async (t) => {
20
20
  manifest2Invalid,
21
21
  manifest2ValidUri,
22
22
  manifest2InvalidUri,
23
- } = fastify.fileServer;
23
+ } = fastify.fixtures;
24
24
 
25
25
  await fastify.ready();
26
26
  // close the app after running the tests
@@ -16,7 +16,7 @@ test("test common routes", async (t) => {
16
16
  fastify = await build("test"),
17
17
  testPostRoute = testPostRouteCurry(fastify),
18
18
  testDeleteRoute = testDeleteRouteCurry(fastify),
19
- { manifest2Valid, annotationList } = fastify.fileServer;
19
+ { manifest2Valid, annotationList } = fastify.fixtures;
20
20
 
21
21
  await fastify.ready();
22
22
  // close the app after running the tests
@@ -88,16 +88,16 @@ test("test common routes", async (t) => {
88
88
  ? deleteBy==="uri"
89
89
  ? getRandomItem(annotations.map((a) => a["@id"]))
90
90
  : deleteBy==="canvasUri"
91
- ? getRandomItem(annotations.map((a) => a.on.full))
92
- : getRandomItem(annotations.map((a) => a.on.manifestShortId))
91
+ ? getRandomItem(annotations.map((a) => getRandomItem(a.on).full))
92
+ : getRandomItem(annotations.map((a) => getRandomItem(a.on).manifestShortId))
93
93
  : `invalid-filter-${uuid4()}`,
94
94
  expectedDeletedCount =
95
95
  validFilter
96
96
  ? deleteBy==="uri"
97
97
  ? annotations.filter((a) => a["@id"]===deleteKey).length
98
98
  : deleteBy==="canvasUri"
99
- ? annotations.filter((a) => a.on.full===deleteKey).length
100
- : annotations.filter((a) => a.on.manifestShortId===deleteKey).length
99
+ ? annotations.filter((a) => a.on.some((x) => x.full===deleteKey)).length
100
+ : annotations.filter((a) => a.on.some((x) => x.manifestShortId===deleteKey)).length
101
101
  : 0;
102
102
 
103
103
  await testDeleteRoute(t, `/annotations/2/delete?${deleteBy}=${deleteKey}`, expectedDeletedCount);
@@ -1,6 +1,6 @@
1
1
  import { v4 as uuid4 } from "uuid";
2
2
 
3
- import { maybeToArray, getHash, isNullish, isObject } from "#utils/utils.js";
3
+ import { maybeToArray, getHash, isNullish, isObject, objectHasKey, visibleLog } from "#utils/utils.js";
4
4
  import { IIIF_PRESENTATION_2, IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
5
5
 
6
6
  /** @typedef {import("#types").MongoCollectionType} MongoCollectionType */
@@ -84,44 +84,52 @@ const getAnnotationTarget = (annotation) => {
84
84
  /**
85
85
  * convert the annotation's `on` to a SpecificResource
86
86
  * reimplemented from SAS: https://github.com/glenrobson/SimpleAnnotationServer/blob/dc7c8c6de9f4693c678643db2a996a49eebfcbb0/src/main/java/uk/org/llgc/annotation/store/AnnotationUtils.java#L123-L135
87
+ * @param {object} annotation
88
+ * @returns {object[]} - the array of targets extracted from 'annotation.on'
87
89
  */
88
90
  const makeTarget = (annotation) => {
89
- const
90
- // must be either string or SpecificResource
91
- target = annotation.on,
92
- // error that will raise is `target` can't be processed
93
- err = new Error(`${makeTarget.name}: could not make target for annotation: 'annotation.on' must be an URI or an object with 'annotation.on.@type==="oa:SpecificResource"' and 'annotation.on.@id' must be a string URI`, { info: annotation });
94
-
95
- let specificResource;
96
-
97
- // convert to SpecificResource if it's not aldready the case
98
- if ( typeof(target) === "string" && !isNullish(target) ) {
99
- let [full, fragment] = target.split("#");
100
- specificResource = {
101
- "@type": "oa:SpecificResource",
102
- full: full,
103
- selector: {
104
- "@type": "oa:FragmentSelector",
105
- value: fragment
91
+ const err = new Error(`${makeTarget.name}: could not make target for annotation: 'annotation.on' must be an URI, an object or an array of objects containing { on: {@id: "<string URI>", @type: "oa:SpecificResource"} }`, { info: annotation });
92
+
93
+ /** annotation.on is converted to an array => this function creates a target from a single item of that array */
94
+ const makeSingleTarget = (_target) => {
95
+ let specificResource;
96
+
97
+ // convert to SpecificResource if it's not aldready the case
98
+ if ( typeof(_target) === "string" && !isNullish(_target) ) {
99
+ let [full, fragment] = _target.split("#");
100
+ specificResource = {
101
+ "@id": _target,
102
+ "@type": "oa:SpecificResource",
103
+ full: full,
104
+ selector: {
105
+ "@type": "oa:FragmentSelector",
106
+ value: fragment
107
+ }
106
108
  }
107
- }
108
- } else if ( isObject(target) ) {
109
- // if 'target' is an object but not a specificresource, raise.
110
- if ( target["@type"] === "oa:SpecificResource" && !isNullish(target["full"]) ) {
111
- specificResource = target;
112
- // the received specificResource `selector` may have its type specified using the key `type`. correct it to `@type`.
113
- if ( isObject(target.selector) && Object.keys(target.selector).includes("type") ) {
114
- target.selector["@type"] = target.selector.type;
115
- delete target.selector.type;
109
+ } else if ( isObject(_target) ) {
110
+ if ( _target["@type"] === "oa:SpecificResource" && !isNullish(_target["full"]) ) {
111
+ specificResource = _target;
112
+ // the received specificResource `selector` may have its type specified using the key `type`. correct it to `@type`.
113
+ if ( specificResource.selector !== undefined && isObject(specificResource.selector) && Object.keys(specificResource.selector).includes("type") ) {
114
+ specificResource.selector["@type"] = specificResource.selector.type;
115
+ delete specificResource.selector.type;
116
+ }
117
+ // if '_target' is an object but not a specificresource, raise.
118
+ } else {
119
+ throw err
116
120
  }
121
+ // if _target is neither a string nor an object, raise
117
122
  } else {
118
123
  throw err
119
124
  }
120
- } else {
121
- throw err
125
+ if ( objectHasKey(specificResource, "full") ) {
126
+ specificResource.manifestShortId = getManifestShortId(specificResource.full);
127
+ specificResource.manifestUri = manifestUri(specificResource.manifestShortId);
128
+ }
129
+ return specificResource
122
130
  }
123
131
 
124
- return specificResource
132
+ return maybeToArray(annotation.on).map(makeSingleTarget);
125
133
  }
126
134
 
127
135
  /**
@@ -12,7 +12,7 @@ const
12
12
  test("test 'iiif2Utils' functions", async (t) => {
13
13
  const
14
14
  fastify = await build("test"),
15
- { annotations2Valid, annotations2Invalid } = fastify.fileServer;
15
+ { annotations2Valid, annotations2Invalid } = fastify.fixtures;
16
16
 
17
17
  await fastify.ready();
18
18
 
@@ -1,5 +1,5 @@
1
1
  // test data for the annotations create and createMany functions.
2
- import { readFileToObject, toUrl } from "#fileServer/utils.js";
2
+ import { readFileToObject, toUrl } from "#fixtures/utils.js";
3
3
 
4
4
  const annotations2Invalid = readFileToObject("annotations2Invalid.jsonld");
5
5
  const annotations2Valid = readFileToObject("annotations2Valid.jsonld");
@@ -14,10 +14,10 @@ const annotationListUriArray = [
14
14
  ];
15
15
 
16
16
  // will trigger an error because the path doesn't exist
17
- const annotationListUriInvalid = { uri: "/fileServer/annotationList_that_does_not_exist.jsonld" };
17
+ const annotationListUriInvalid = { uri: "/fixtures/annotationList_that_does_not_exist.jsonld" };
18
18
 
19
19
  const annotationListUriArrayInvalid = [
20
- { uri: "/fileServer/annotationList_that_does_not_exist.jsonld" }
20
+ { uri: "/fixtures/annotationList_that_does_not_exist.jsonld" }
21
21
  ];
22
22
 
23
23
  const annotationList = readFileToObject("annotationList_aikon_wit9_man11_anno165_all.jsonld");
@@ -1,11 +1,11 @@
1
1
  /**
2
- * the `fileServer` plugin makes test files available to the entire fastify app, mostly for testing purposes.
2
+ * the `fixtures` plugin makes test files available to the entire fastify app, mostly for testing purposes.
3
3
  */
4
4
  import fastifyPlugin from "fastify-plugin";
5
5
 
6
- import { annotations2Invalid, annotations2Valid, annotationListUri, annotationListUriArray, annotationList, annotationListArray, annotationListUriInvalid, annotationListUriArrayInvalid } from "#src/fileServer/annotations.js";
7
- import { manifest2Valid, manifest2ValidUri, manifest2Invalid, manifest2InvalidUri } from "#fileServer/manifests.js";
8
- import { readFileToObject } from "#fileServer/utils.js";
6
+ import { annotations2Invalid, annotations2Valid, annotationListUri, annotationListUriArray, annotationList, annotationListArray, annotationListUriInvalid, annotationListUriArrayInvalid } from "#src/fixtures/annotations.js";
7
+ import { manifest2Valid, manifest2ValidUri, manifest2Invalid, manifest2InvalidUri } from "#fixtures/manifests.js";
8
+ import { readFileToObject } from "#fixtures/utils.js";
9
9
 
10
10
  /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
11
11
 
@@ -15,11 +15,11 @@ import { readFileToObject } from "#fileServer/utils.js";
15
15
  * @param {FastifyInstanceType} fastify Encapsulated Fastify Instance
16
16
  * @param {object} options
17
17
  */
18
- async function fileServer(fastify, options) {
18
+ async function fixtures(fastify, options) {
19
19
 
20
20
  /** route to return a file in `dataDir` */
21
21
  fastify.get(
22
- "/fileServer/:fileName",
22
+ "/fixtures/:fileName",
23
23
  {
24
24
  schema: {
25
25
  params: {
@@ -45,7 +45,7 @@ async function fileServer(fastify, options) {
45
45
  }
46
46
  )
47
47
 
48
- fastify.decorate("fileServer", {
48
+ fastify.decorate("fixtures", {
49
49
  annotationListUri: annotationListUri,
50
50
  annotationListUriArray: annotationListUriArray,
51
51
  annotationList: annotationList,
@@ -61,4 +61,4 @@ async function fileServer(fastify, options) {
61
61
  });
62
62
  }
63
63
 
64
- export default fastifyPlugin(fileServer);
64
+ export default fastifyPlugin(fixtures);
@@ -1,4 +1,4 @@
1
- import { readFileToObject, toUrl } from "#fileServer/utils.js";
1
+ import { readFileToObject, toUrl } from "#fixtures/utils.js";
2
2
 
3
3
  const manifest2ValidUri = { uri: toUrl("bnf_valid_manifest.json") };
4
4
  const manifest2Valid = readFileToObject("bnf_valid_manifest.json");
@@ -4,13 +4,13 @@ import fs from "node:fs";
4
4
 
5
5
  // path to dirctory of curent file
6
6
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
7
- // path to fileServer/data
7
+ // path to fixtures/data
8
8
  const dataDir = path.join(__dirname, "data");
9
9
 
10
10
  const availableFiles = fs.readdirSync(dataDir);
11
11
 
12
12
  /**
13
- * for simplicity, `readFileToObject` is synchronous. given that `fileServer` should only be used in tests, there is no performance downgrade for the prod server.
13
+ * for simplicity, `readFileToObject` is synchronous. given that `fixtures` should only be used in tests, there is no performance downgrade for the prod server.
14
14
  * @param {string} fn: the filename
15
15
  * @returns {object}
16
16
  */
@@ -25,7 +25,7 @@ const readFileToObject = (fn) => {
25
25
  * @param {string} fn
26
26
  * @returns {string}
27
27
  */
28
- const toUrl = (fn) => `${process.env.AIIINOTATE_BASE_URL}/fileServer/${fn}`;
28
+ const toUrl = (fn) => `${process.env.AIIINOTATE_BASE_URL}/fixtures/${fn}`;
29
29
 
30
30
 
31
31
  export {
@@ -201,12 +201,15 @@ function addSchemas(fastify, options, done) {
201
201
 
202
202
  fastify.addSchema({
203
203
  $id: makeSchemaUri("annotationTarget"),
204
- anyOf: [
205
- // URI
206
- { type: "string" },
207
- // SpecificResource
208
- { $ref: makeSchemaUri("specificResource") }
209
- ]
204
+ type: "array",
205
+ items: {
206
+ anyOf: [
207
+ // URI
208
+ { type: "string" },
209
+ // SpecificResource
210
+ { $ref: makeSchemaUri("specificResource") }
211
+ ]
212
+ }
210
213
  })
211
214
 
212
215
  fastify.addSchema({
@@ -41,7 +41,20 @@ function addSchemas(fastify, options, done) {
41
41
  { type: "string", enum: [ "oa:Annotation" ] },
42
42
  { type: "array", items: { type: "string" }}
43
43
  ]},
44
- on: { anyOf: [{ type: "string" }, { type: "object" }]}
44
+ on: {
45
+ anyOf: [
46
+ { type: "string" },
47
+ { type: "object" },
48
+ {
49
+ type: "array",
50
+ items: [
51
+ { type: "string" },
52
+ { type: "object" }
53
+ ],
54
+ minItems: 1
55
+ }
56
+ ]
57
+ }
45
58
  }
46
59
  });
47
60
 
@@ -56,7 +69,20 @@ function addSchemas(fastify, options, done) {
56
69
  { type: "string" },
57
70
  { type: "array", items: { type: "string" } },
58
71
  ]},
59
- target: { anyOf: [{ type: "string" }, { type: "object" }]}
72
+ target: {
73
+ anyOf: [
74
+ { type: "string" },
75
+ { type: "object" },
76
+ {
77
+ type: "array",
78
+ items: [
79
+ { type: "string" },
80
+ { type: "object" }
81
+ ],
82
+ minItems: 1
83
+ }
84
+ ]
85
+ }
60
86
  }
61
87
  })
62
88