aiiinotate 0.2.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 (88) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +61 -0
  3. package/cli/import.js +142 -0
  4. package/cli/index.js +26 -0
  5. package/cli/io.js +105 -0
  6. package/cli/migrate.js +123 -0
  7. package/cli/mongoClient.js +11 -0
  8. package/docs/architecture.md +88 -0
  9. package/docs/db.md +38 -0
  10. package/docs/dev_iiif_compatibility.md +43 -0
  11. package/docs/endpoints.md +48 -0
  12. package/docs/progress.md +159 -0
  13. package/docs/specifications/0_w3c_open_annotations.md +332 -0
  14. package/docs/specifications/1_w3c_web_annotations.md +577 -0
  15. package/docs/specifications/2_iiif_apis.md +396 -0
  16. package/docs/specifications/3_iiif_annotations.md +103 -0
  17. package/docs/specifications/4_search_api.md +135 -0
  18. package/docs/specifications/5_sas.md +119 -0
  19. package/docs/specifications/6_mirador.md +119 -0
  20. package/docs/specifications/7_aikon.md +137 -0
  21. package/docs/specifications/include/presentation_2.0.webp +0 -0
  22. package/docs/specifications/include/presentation_2.0_white.png +0 -0
  23. package/docs/specifications/include/presentation_3.0.png +0 -0
  24. package/docs/specifications/include/presentation_3.0_resize.png +0 -0
  25. package/eslint.config.js +27 -0
  26. package/migrations/baseConfig.js +56 -0
  27. package/migrations/manageIndex.js +55 -0
  28. package/migrations/migrate-mongo-config-main.js +8 -0
  29. package/migrations/migrate-mongo-config-test.js +8 -0
  30. package/migrations/migrationScripts/20250825185706-collections.js +41 -0
  31. package/migrations/migrationScripts/20250826194832-annotations2-canvas-index.js +31 -0
  32. package/migrations/migrationScripts/20250904080710-annotations2-schema.js +42 -0
  33. package/migrations/migrationScripts/20251002141951-manifest2-schema.js +43 -0
  34. package/migrations/migrationScripts/20251006212110-manifest-unique-index.js +29 -0
  35. package/migrations/migrationScripts/20251028115614-annotations2-id-index.js +27 -0
  36. package/migrations/migrationTemplate.js +25 -0
  37. package/package.json +78 -0
  38. package/run.sh +70 -0
  39. package/scripts/_migrations.sh +79 -0
  40. package/scripts/_setup.js +31 -0
  41. package/scripts/setup_mongodb.sh +61 -0
  42. package/scripts/setup_mongodb_migrate.sh +17 -0
  43. package/scripts/setup_node.sh +15 -0
  44. package/scripts/utils.sh +192 -0
  45. package/setup.sh +20 -0
  46. package/src/app.js +113 -0
  47. package/src/config/.env.template +22 -0
  48. package/src/data/annotations/annotations2.js +419 -0
  49. package/src/data/annotations/annotations3.js +32 -0
  50. package/src/data/annotations/routes.js +271 -0
  51. package/src/data/annotations/routes.test.js +180 -0
  52. package/src/data/collectionAbstract.js +270 -0
  53. package/src/data/index.js +29 -0
  54. package/src/data/manifests/manifests2.js +305 -0
  55. package/src/data/manifests/manifests2.test.js +53 -0
  56. package/src/data/manifests/manifests3.js +23 -0
  57. package/src/data/manifests/routes.js +95 -0
  58. package/src/data/manifests/routes.test.js +69 -0
  59. package/src/data/routes.js +141 -0
  60. package/src/data/routes.test.js +117 -0
  61. package/src/data/utils/iiif2Utils.js +196 -0
  62. package/src/data/utils/iiif2Utils.test.js +98 -0
  63. package/src/data/utils/iiif3Utils.js +0 -0
  64. package/src/data/utils/iiifUtils.js +18 -0
  65. package/src/data/utils/routeUtils.js +109 -0
  66. package/src/data/utils/testUtils.js +253 -0
  67. package/src/data/utils/utils.js +231 -0
  68. package/src/db/index.js +48 -0
  69. package/src/fileServer/annotations.js +39 -0
  70. package/src/fileServer/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
  71. package/src/fileServer/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
  72. package/src/fileServer/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
  73. package/src/fileServer/data/annotations2Invalid.jsonld +39 -0
  74. package/src/fileServer/data/annotations2Valid.jsonld +39 -0
  75. package/src/fileServer/data/bnf_invalid_manifest.json +2806 -0
  76. package/src/fileServer/data/bnf_valid_manifest.json +2817 -0
  77. package/src/fileServer/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
  78. package/src/fileServer/index.js +64 -0
  79. package/src/fileServer/manifests.js +14 -0
  80. package/src/fileServer/utils.js +35 -0
  81. package/src/schemas/index.js +20 -0
  82. package/src/schemas/schemasBase.js +47 -0
  83. package/src/schemas/schemasPresentation2.js +417 -0
  84. package/src/schemas/schemasPresentation3.js +57 -0
  85. package/src/schemas/schemasResolver.js +71 -0
  86. package/src/schemas/schemasRoutes.js +277 -0
  87. package/src/server.js +22 -0
  88. package/src/types.js +93 -0
package/setup.sh ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/env bash
2
+
3
+ source "./scripts/utils.sh";
4
+
5
+ # SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
6
+
7
+ color_echo blue "\nInstalling prompt utility fzy..."
8
+ if [ "$OS" = "Linux" ]; then
9
+ sudo apt install fzy
10
+ elif [ "$OS" = "Mac" ]; then
11
+ brew install fzy
12
+ else
13
+ color_echo red "Unsupported OS: $OS"
14
+ exit 1
15
+ fi
16
+
17
+ #NOTE node needs to be installed for mongodb to run, so order is important
18
+ run_script "setup_node.sh" "Node and webapp packages installation"
19
+ run_script "setup_mongodb.sh" "MongoDB installation"
20
+ run_script "setup_mongodb_migrate.sh" "MongoDB database creation"
package/src/app.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * build a fastify app
3
+ */
4
+
5
+ import Fastify from "fastify";
6
+ import cors from "@fastify/cors";
7
+ // import swagger from "@fastify/swagger";
8
+
9
+ import fileServer from "#fileServer/index.js";
10
+ import schemas from "#schemas/index.js";
11
+ import data from "#data/index.js";
12
+ import db from "#db/index.js";
13
+
14
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
15
+
16
+ const fastifyConfigCommon = {
17
+ bodyLimit: 100 * 1048576 // 100 MiB
18
+ }
19
+
20
+ const testConfig = {
21
+ fastify: {
22
+ ...fastifyConfigCommon
23
+ },
24
+ mongo: {
25
+ test: true,
26
+ }
27
+ }
28
+
29
+ const defaultConfig = {
30
+ fastify: {
31
+ logger: true,
32
+ ...fastifyConfigCommon
33
+ },
34
+ mongo: { }
35
+ }
36
+
37
+ //NOTE: couldn´t get fastify/swagger to work for now...
38
+ // const swaggerConfig = {
39
+ // openapi: {
40
+ // openapi: "3.0.0",
41
+ // info: {
42
+ // title: "Aiiinotate",
43
+ // description: "A fast and lightweight IIIF annotations server",
44
+ // version: "1.0.0",
45
+ // },
46
+ // servers: [
47
+ // {
48
+ // url: process.env.APP_BASE_URL,
49
+ // description: "Aiiinotate URL"
50
+ // }
51
+ // ],
52
+ // // tags: [],
53
+ // // components: {},
54
+ // // externalDocs: {
55
+ // // url: 'https://swagger.io',
56
+ // // description: 'Find more info here'
57
+ // // }
58
+ // },
59
+ //
60
+ // // swagger: {
61
+ // // info: {
62
+ // // title: "Aiiinotate",
63
+ // // description: "A fast and lightweight IIIF annotations server",
64
+ // // version: "1.0.0",
65
+ // // },
66
+ // // externalDocs: {
67
+ // // url: 'https://swagger.io',
68
+ // // description: 'Find more info here'
69
+ // // },
70
+ // // host: process.env.APP_BASE_URL.replace(/^http(s)?\:\/\//g, ""), // process.env.APP_BASE_URL,
71
+ // // schemes: [ "http", "https" ],
72
+ // // consumes: ['application/json'],
73
+ // // produces: ['application/json'],
74
+ // // tags: [ Object ],
75
+ // // },
76
+ // hideUntagged: false,
77
+ // exposeRoute: true,
78
+ // }
79
+
80
+ const allowedModes = ["test", "default"];
81
+
82
+ /**
83
+ * @param {"test"|"default"} mode
84
+ * @returns {Promise<FastifyInstanceType>}
85
+ */
86
+ async function build(mode="default") {
87
+
88
+ if ( ! allowedModes.includes(mode) ) {
89
+ throw new Error(`app.build: 'mode' param expected one of ${allowedModes}, got ${mode}`)
90
+ }
91
+
92
+ const
93
+ mongoConfig = mode==="test" ? testConfig.mongo : defaultConfig.mongo,
94
+ fastifyConfig = mode==="test" ? testConfig.fastify : defaultConfig.fastify,
95
+ fastify = Fastify(fastifyConfig);
96
+
97
+ // NOTE: we allow all origins => restrict ?
98
+ fastify.register(cors, {
99
+ origin: "*",
100
+ methods: ["GET", "HEAD", "POST", "DELETE"]
101
+ });
102
+
103
+ await fastify.register(db, mongoConfig);
104
+ await fastify.register(fileServer);
105
+ fastify.register(schemas);
106
+ fastify.register(data);
107
+ await fastify.ready();
108
+
109
+ return fastify
110
+ }
111
+
112
+ export default build;
113
+
@@ -0,0 +1,22 @@
1
+ #!/bin/env bash
2
+
3
+ # MongoDB host
4
+ MONGODB_HOST=127.0.0.1
5
+ # MongoDB port
6
+ MONGODB_PORT=27017
7
+ # MongoDB database name
8
+ MONGODB_DB=aiiinotate
9
+
10
+ # HTTP port for the app
11
+ APP_PORT=4000
12
+ # URL for the app, inclding port
13
+ APP_HOST=http://127.0.0.1
14
+
15
+ # IGNORE
16
+ APP_BASE_URL="$APP_HOST:$APP_PORT"
17
+ # IGNORE
18
+ MONGODB_CONNSTRING="mongodb://$MONGODB_HOST:$MONGODB_PORT/$MONGODB_DB"
19
+ # IGNORE
20
+ MONGODB_DB_TEST="${MONGODB_DB}_test"
21
+ # IGNORE
22
+ MONGODB_CONNSTRING_TEST="mongodb://$MONGODB_HOST:$MONGODB_PORT/$MONGODB_DB_TEST"
@@ -0,0 +1,419 @@
1
+ /**
2
+ * IIIF presentation 2.1 annotation internals: convert incoming data, interct with the database, return data.
3
+ * exposes an `Annotations2` class that should contain everything you need to interact with the annotations2 collection.
4
+ */
5
+ import fastifyPlugin from "fastify-plugin";
6
+
7
+ import CollectionAbstract from "#data/collectionAbstract.js";
8
+ import { IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
9
+ import { ajvCompile, objectHasKey, isNullish, maybeToArray, inspectObj, visibleLog } from "#utils/utils.js";
10
+ import { getManifestShortId, makeTarget, makeAnnotationId, toAnnotationList, canvasUriToManifestUri } from "#utils/iiif2Utils.js";
11
+
12
+
13
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
14
+ /** @typedef {import("#types").MongoObjectId} MongoObjectId */
15
+ /** @typedef {import("#types").MongoInsertResultType} MongoInsertResultType */
16
+ /** @typedef {import("#types").MongoUpdateResultType} MongoUpdateResultType */
17
+ /** @typedef {import("#types").MongoDeleteResultType} MongoDeleteResultType */
18
+ /** @typedef {import("#types").InsertResponseType} InsertResponseType */
19
+ /** @typedef {import("#types").UpdateResponseType} UpdateResponseType */
20
+ /** @typedef {import("#types").DeleteResponseType} DeleteResponseType */
21
+ /** @typedef {import("#types").DataOperationsType } DataOperationsType */
22
+ /** @typedef {import("#types").AnnotationsDeleteKeyType } AnnotationsDeleteKeyType */
23
+ /** @typedef {import("#types").Manifests2InstanceType} Manifests2InstanceType */
24
+ /** @typedef {import("#types").AjvValidateFunctionType} AjvValidateFunctionType */
25
+
26
+ /** @typedef {Annotations2} Annotations2InstanceType */
27
+
28
+ // RECOMMENDED URI PATTERNS https://iiif.io/api/presentation/2.1/#a-summary-of-recommended-uri-patterns
29
+ //
30
+ // Collection {scheme}://{host}/{prefix}/collection/{name}
31
+ // Manifest {scheme}://{host}/{prefix}/{identifier}/manifest
32
+ // Sequence {scheme}://{host}/{prefix}/{identifier}/sequence/{name}
33
+ // Canvas {scheme}://{host}/{prefix}/{identifier}/canvas/{name}
34
+ // Annotation (incl images) {scheme}://{host}/{prefix}/{identifier}/annotation/{name}
35
+ // AnnotationList {scheme}://{host}/{prefix}/{identifier}/list/{name}
36
+ // Range {scheme}://{host}/{prefix}/{identifier}/range/{name}
37
+ // Layer {scheme}://{host}/{prefix}/{identifier}/layer/{name}
38
+ // Content {scheme}://{host}/{prefix}/{identifier}/res/{name}.{format}
39
+
40
+ /**
41
+ * @extends {CollectionAbstract}
42
+ */
43
+ class Annotations2 extends CollectionAbstract {
44
+
45
+ /**
46
+ * @param {FastifyInstanceType} fastify
47
+ */
48
+ constructor(fastify) {
49
+ super(fastify, "annotations2");
50
+ /** @type {Manifests2InstanceType} */
51
+ this.manifestsPlugin = this.fastify.manifests2;
52
+ /** @type {AjvValidateFunctionType} */
53
+ this.validatorAnnotationList = ajvCompile(fastify.schemasResolver(
54
+ fastify.schemasPresentation2.getSchema("annotationList")
55
+ ));
56
+ }
57
+
58
+ ////////////////////////////////////////////////////////////////
59
+ // utils
60
+
61
+ /**
62
+ * clean the body of an annotation (annotation.resource).
63
+ * if `annotation.resource` is an array (there are several bodies associated to that annotation), this function must be called on each item of the array
64
+ * @param {object} resource
65
+ * @returns {object | null} - the resource, or `null` if the resource is either an empty Embedded Textual Body or has no `@id`
66
+ */
67
+ #cleanAnnotationResource(resource) {
68
+ if ( resource ) {
69
+ // 1) uniformize embedded textual body keys
70
+ // OA allows `cnt:ContentAsText` or `dctypes:Text` for Embedded Textual Bodies, IIIF only uses `dctypes:Text`
71
+ resource["@type"] =
72
+ resource["@type"] === "cnt:ContentAsText"
73
+ ? "dctypes:Text"
74
+ : resource["@type"];
75
+
76
+ // OA stores Textual Body content in `cnt:chars`, IIIF uses `chars`. `value` is sometimes also used
77
+ resource.chars = resource.value || resource["cnt:chars"] || resource.chars; // may be undefined
78
+ // delete the alternate keys
79
+ [ "value", "cnt:chars" ].map((k) => {
80
+ if ( Object.keys(resource).includes(k) ) {
81
+ delete resource[k];
82
+ }
83
+ })
84
+
85
+ // 2) return `null` if resource is empty. a body is empty if
86
+ // - it's got no `@id` (=> it's not a referenced textaul body)
87
+ // - it's not an Embedded Textual Body, or it's an empty Embedded Textual Body.
88
+ // see: https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/specifications/0_w3c_open_annotations.md#embedded-textual-body-etb
89
+ const
90
+ hasTextualBody = objectHasKey(resource, "chars"),
91
+ emptyBody = isNullish(resource.chars) || resource.chars === "<p></p>";
92
+ if ( isNullish(resource["@id"]) && (emptyBody || !hasTextualBody) ) {
93
+ return null
94
+ }
95
+ }
96
+ return resource;
97
+ }
98
+
99
+ /**
100
+ * clean an annotation before saving it to database.
101
+ * some of the work consists of translating what is defined by the OpenAnnotations standard to what is actually used by IIIF annotations.
102
+ * if `update`, some cleaning will be skipped (especially the redefinition of "@id"), otherwise updates would fail.
103
+ *
104
+ * @param {object} annotation
105
+ * @param {boolean} update - set to `true` if performing an update instead of an insert.
106
+ * @returns {object}
107
+ */
108
+ #cleanAnnotation(annotation, update=false) {
109
+ // 1) extract ids and targets
110
+ const
111
+ annotationTarget = makeTarget(annotation),
112
+ manifestShortId = getManifestShortId(annotationTarget.full);
113
+
114
+ // in updates, "@id" has aldready been extracted
115
+ if ( !update ) {
116
+ annotation["@id"] = makeAnnotationId(annotation, manifestShortId);
117
+ }
118
+ annotation["@context"] = IIIF_PRESENTATION_2_CONTEXT["@context"];
119
+ annotation.on = annotationTarget;
120
+ annotation.on.manifestShortId = manifestShortId;
121
+
122
+ // 2) process motivations.
123
+ // - motivations are an array of strings
124
+ // - open annotation specifies that motivations should be described by the `oa:Motivation`, while IIIF 2.1 examples uses the `motivation` field => uniformizwe
125
+ // - all values must be `sc:painting` or prefixed by `oa:`: IIIF presentation API indicates that the only allowed values are open annotation values (prefixed by `oa:`) or `sc:painting`.
126
+ if ( objectHasKey(annotation, "oa:Motivation") ) {
127
+ annotation.motivation = annotation["oa:Motivation"];
128
+ delete annotation["oa:motivation"];
129
+ }
130
+ annotation.motivation =
131
+ maybeToArray(annotation.motivation || [])
132
+ .map(String)
133
+ .map((motiv) =>
134
+ motiv.startsWith("oa:") || motiv.startsWith("sc:")
135
+ ? motiv
136
+ : `oa:${motiv}`
137
+ );
138
+
139
+ // 3) process the resource. Resource can be either undefined, an array of objects or a single object. process all objects and, if there's no resource content, delete `annotation.resource`.
140
+ let resource = annotation.resource || undefined;
141
+ if ( resource ) {
142
+ resource =
143
+ Array.isArray(resource)
144
+ ? resource.map((r) => this.#cleanAnnotationResource(r)).filter((r) => r !== null)
145
+ : this.#cleanAnnotationResource(resource);
146
+ }
147
+ if ( resource === null || resource === undefined || (Array.isArray(resource) && !resource.length) ) {
148
+ delete annotation.resource;
149
+ } else {
150
+ annotation.resource = resource;
151
+ }
152
+ return annotation;
153
+ }
154
+
155
+ /**
156
+ * take an annotationList, clean it and return it as a array of annotations.
157
+ * see: https://iiif.io/api/presentation/2.1/#annotation-list
158
+ * @param {object} annotationList
159
+ * @returns {object[]}
160
+ */
161
+ #cleanAnnotationList(annotationList) {
162
+ // NOTE: if `this.#cleanAnnotationList` can only be accessed from annotations routes, then this check is useless (has aldready been performed).
163
+ if ( this.validatorAnnotationList(annotationList) ) {
164
+ this.errorNoAction("Annotations2.#cleanAnnotationList: could not recognize AnnotationList. see: https://iiif.io/api/presentation/2.1/#annotation-list.", annotationList)
165
+ }
166
+ //NOTE: using an arrow function is necessary to avoid losing the scope of `this`. otherwise, `this` is undefined in `#cleanAnnotation`.
167
+ return annotationList.resources.map((ressource) => this.#cleanAnnotation(ressource))
168
+ }
169
+
170
+ /**
171
+ * handle all side effects on the `manifests2` collection. this does 2 things:
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,
174
+ * (or undefined if the manifest or canvas were not found).
175
+ * @param {object|object[]} annotationData - an annotation, or array of annotations.
176
+ */
177
+ async #insertManifestsAndGetCanvasIdx(annotationData) {
178
+ // TODO : extract all canvas Ids, reconstruct manifest IDs from it. if they're valid, insert the manifests into the db.
179
+ // convert objects to array to get a uniform interface.
180
+ let converted, manifestUris;
181
+ [ annotationData, converted ] = maybeToArray(annotationData, true);
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
+ })
188
+
189
+ // get all distinct manifest URIs, and insert them.
190
+ manifestUris = [...new Set(
191
+ annotationData.map((ann) => ann.on.manifestUri)
192
+ )];
193
+ // NOTE: PERFORMANCE significantly drops because of this: test running for the entire app goes from ~1000ms to ~2600ms
194
+ const
195
+ insertResponse = await this.manifestsPlugin.insertManifestsFromUriArray(manifestUris, false),
196
+ /** @type {string[]} concatenation of ids of newly inserted manifests and previously inserted manifests. */
197
+ insertedManifestsIds = insertResponse.insertedIds.concat(insertResponse.preExistingIds || []);
198
+
199
+ // 3. update annotations with 2 things:
200
+ // - where manifest insertion has failed, set `annotation.on.manifestUri` to undefined
201
+ // - set `annotation.on.canvasIdx`: the position of the target canvas within the manifest, or undefined if it cound not be found.
202
+ annotationData = await Promise.all(
203
+ 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;
213
+ return ann;
214
+ })
215
+ );
216
+
217
+ // retroconvert array to single object, if single object was converted.
218
+ return converted
219
+ ? annotationData[0]
220
+ : annotationData;
221
+ }
222
+
223
+ ////////////////////////////////////////////////////////////////
224
+ // insert / updates
225
+
226
+ /**
227
+ * validate and insert a single annotation.
228
+ * @param {object} annotationArray
229
+ * @returns {Promise<InsertResponseType>}
230
+ */
231
+ async insertAnnotation(annotation) {
232
+ annotation = this.#cleanAnnotation(annotation);
233
+ annotation = await this.#insertManifestsAndGetCanvasIdx(annotation);
234
+ return this.insertOne(annotation);
235
+ }
236
+
237
+ /**
238
+ * TODO: handle side effects when changing `annotation.on`: changes that can affect `manifestShortId`, `manifestUri` and `canvasIdx`
239
+ * (for example, updating `annotation.on.full` would ask to change `canvasIdx`).
240
+ * @param {object} annotation
241
+ * @returns {Promise<UpdateResponseType>}
242
+ */
243
+ async updateAnnotation(annotation) {
244
+ // necessary: on insert, the `@id` received is modified by `this.#cleanAnnotationList`.
245
+ annotation = this.#cleanAnnotation(annotation, true);
246
+ const
247
+ query = { "@id": annotation["@id"] },
248
+ update = { $set: annotation };
249
+ return this.updateOne(query, update);
250
+ }
251
+
252
+ /**
253
+ * validate and insert annotations from an annotation list.
254
+ * @param {object} annotationList
255
+ * @returns {Promise<InsertResponseType>}
256
+ */
257
+ async insertAnnotationList(annotationList) {
258
+ let annotationArray;
259
+ annotationArray = this.#cleanAnnotationList(annotationList);
260
+ annotationArray = await this.#insertManifestsAndGetCanvasIdx(annotationArray);
261
+ return this.insertMany(annotationArray);
262
+ }
263
+
264
+ ////////////////////////////////////////////////////////////////
265
+ // delete
266
+
267
+ /**
268
+ * @param {AnnotationsDeleteKeyType} deleteKey - what deleteVal describes: an annotation's '@id', a manifest's URI...
269
+ * @param {string} deleteVal - deletion key
270
+ * @returns {Promise<DeleteResponseType>}
271
+ */
272
+ async deleteAnnotations(deleteKey, deleteVal) {
273
+
274
+ const allowedDeleteKey = [ "uri", "manifestShortId", "canvasUri" ];
275
+ if ( !allowedDeleteKey.includes(deleteKey) ) {
276
+ throw this.deleteError(`${this.funcName(this.deleteAnnotations)}: expected one of ${allowedDeleteKey} for param 'deleteKey', got '${deleteKey}'`)
277
+ }
278
+
279
+ const deleteFilter =
280
+ deleteKey==="uri"
281
+ ? { "@id": deleteVal }
282
+ : deleteKey==="canvasUri"
283
+ ? { "on.full": deleteVal }
284
+ : { "on.manifestShortId": deleteVal };
285
+
286
+ return this.delete(deleteFilter);
287
+ }
288
+
289
+ ////////////////////////////////////////////////////////////////
290
+ // get
291
+
292
+ /**
293
+ * find documents based on a `queryObj` and project them to `projectionObj`.
294
+ *
295
+ * about projection: 0 removes the fields from the response, 1 incldes it (but exclude all others)
296
+ * see: https://www.mongodb.com/docs/drivers/node/current/crud/query/project/#std-label-node-project
297
+ * https://stackoverflow.com/questions/74447979/mongoservererror-cannot-do-exclusion-on-field-date-in-inclusion-projection
298
+ * @param {object} queryObj
299
+ * @param {object?} projectionObj - extra projection fields to tailor the reponse format
300
+ * @returns {Promise<object[]>}
301
+ */
302
+ async find(queryObj, projectionObj={}) {
303
+ // 1. construct the final projection object, knowing that we can't mix exclusive and inclusive projectin.
304
+ // presence of `_id` will not cause projections to fail => remove it from values.
305
+ const projectionValues =
306
+ Object.entries(projectionObj)
307
+ .filter(([k,v]) => k !== "_id")
308
+ .map(([k,v]) => v);
309
+
310
+ // if there are projection values defined and if they're not 0 or 1, then they're invalid => throw
311
+ if ( projectionValues.length && projectionValues.find((x) => ![0,1].includes(x)) ) {
312
+ throw this.readError(`Annotations2.find: only allowed values for projection are 0 and 1. got: ${[...new Set(projectionValues)]}`)
313
+ }
314
+ // mongo projection can be either inclusive (define only fields that will be included) or negative (define only fields that will be excluded), but not a mix of the 2. if you have more than 1 distinct values, you mixed inclusion and exclusion => throw
315
+ const distinctProjectionValues = [...new Set(projectionValues)]
316
+ if ( distinctProjectionValues.length > 1 ) {
317
+ throw this.readError(`Annotations2.find: can't mix insertion and exclusion projection in 'projectionObj'. all values must be either 0 or 1. got: ${distinctProjectionValues}`, projectionObj)
318
+ }
319
+ // negative projection: all fields will be included except for those specified.
320
+ // in this case, negate other fields that we don't ant exposed.
321
+ // in case of positive projection, no specific processing is required: only the explicitly required fields are included.
322
+ // in all cases, `_id` should not be included unless we explicitly ask for it.
323
+ projectionObj._id = projectionObj._id || 0;
324
+ if ( distinctProjectionValues[0] === 0 ) {
325
+ projectionObj["on.manifestShortId"] = projectionObj["on.manifestShortId"] || 0;
326
+ }
327
+
328
+ // 2. find, project and return
329
+ return this.collection
330
+ .find(queryObj, { projection: projectionObj })
331
+ .toArray();
332
+ }
333
+
334
+ /**
335
+ * implementation of the IIIF Search API 1.0
336
+ *
337
+ * NOTE:
338
+ * - only `motivation` and `q` search params are implemented
339
+ * - to increase search execution, ONLY EXACT STRING MACHES are
340
+ * implemented for `q` and `motivation` (in the IIIF specs, you can supply
341
+ * multiple space-separated values and the server should return all partial
342
+ * matches to any of those strings.)
343
+ *
344
+ * see:
345
+ * https://iiif.io/api/search/1.0/
346
+ * https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/specifications/4_search_api.md
347
+ *
348
+ * @param {string} queryUrl
349
+ * @param {string} manifestShortId
350
+ * @param {string} q
351
+ * @param {"painting"|"non-painting"|"commenting"|"describing"|"tagging"|"linking"} motivation
352
+ * @returns {object} annotationList containing results
353
+ */
354
+ async search(queryUrl, manifestShortId, q, motivation) {
355
+ const
356
+ queryBase = { "on.manifestShortId": manifestShortId },
357
+ queryFilters = { $and: [] };
358
+
359
+ // expand query parameters
360
+ if ( q ) {
361
+ queryFilters.$and.push({
362
+ $or: [
363
+ { "@id": q },
364
+ { "resource.@id": q },
365
+ { "resource.chars": q }
366
+ ]
367
+ });
368
+ }
369
+ if ( motivation ) {
370
+ queryFilters.$and.push(
371
+ motivation === "non-painting"
372
+ ? { motivation: { $ne: "sc:painting" } }
373
+ : motivation === "painting"
374
+ ? { motivation: "sc:painting" }
375
+ : { motivation: `oa:${motivation}` }
376
+ );
377
+ }
378
+ const query =
379
+ queryFilters.$and.length
380
+ ? { ...queryBase, ...queryFilters }
381
+ : queryBase;
382
+
383
+ const annotations = await this.find(query);
384
+ return toAnnotationList(annotations, queryUrl, `search results for query ${queryUrl}`);
385
+ }
386
+
387
+ /**
388
+ * find all annotations whose target (`on.full`) is `canvasUri`.
389
+ * @param {string} canvasUri
390
+ * @param {boolean} asAnnotationList
391
+ * @returns
392
+ */
393
+ async findByCanvasUri(queryUrl, canvasUri, asAnnotationList=false) {
394
+ const annotations = await this.find({
395
+ "on.full": canvasUri
396
+ });
397
+ return asAnnotationList
398
+ ? toAnnotationList(annotations, queryUrl, `annotations targeting canvas ${canvasUri}`)
399
+ : annotations;
400
+ }
401
+
402
+ /**
403
+ * find an annotation by its "@id"
404
+ * @param {string} annotationUri
405
+ * @returns {Promise<object>} the annotation, or `{}` if none was found
406
+ */
407
+ async findById(annotationUri) {
408
+ return this.collection.findOne({ "@id": annotationUri })
409
+ }
410
+
411
+ }
412
+
413
+ export default fastifyPlugin((fastify, options, done) => {
414
+ fastify.decorate("annotations2", new Annotations2(fastify));
415
+ done();
416
+ }, {
417
+ name: "annotations2",
418
+ dependencies: ["manifests2"]
419
+ })
@@ -0,0 +1,32 @@
1
+ import fastifyPlugin from "fastify-plugin";
2
+
3
+ import CollectionAbstract from "#data/collectionAbstract.js";
4
+
5
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
6
+
7
+ /** @typedef {Annnotations3} Annotations3InstanceType */
8
+
9
+ /**
10
+ * @extends {CollectionAbstract}
11
+ */
12
+ class Annnotations3 extends CollectionAbstract {
13
+ /**
14
+ * @param {FastifyInstanceType} fastify
15
+ */
16
+ constructor(fastify) {
17
+ super(fastify, "annotations3");
18
+ }
19
+
20
+ notImplementedError() {
21
+ throw this.errorNoAction(`${this.constructor.name}: not implemented`);
22
+ }
23
+
24
+ }
25
+
26
+ export default fastifyPlugin((fastify, options, done) => {
27
+ fastify.decorate("annnotations3", new Annnotations3(fastify));
28
+ done();
29
+ }, {
30
+ name: "annotations3",
31
+ dependencies: ["manifests3"]
32
+ })