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.
- package/LICENSE +661 -0
- package/README.md +61 -0
- package/cli/import.js +142 -0
- package/cli/index.js +26 -0
- package/cli/io.js +105 -0
- package/cli/migrate.js +123 -0
- package/cli/mongoClient.js +11 -0
- package/docs/architecture.md +88 -0
- package/docs/db.md +38 -0
- package/docs/dev_iiif_compatibility.md +43 -0
- package/docs/endpoints.md +48 -0
- package/docs/progress.md +159 -0
- package/docs/specifications/0_w3c_open_annotations.md +332 -0
- package/docs/specifications/1_w3c_web_annotations.md +577 -0
- package/docs/specifications/2_iiif_apis.md +396 -0
- package/docs/specifications/3_iiif_annotations.md +103 -0
- package/docs/specifications/4_search_api.md +135 -0
- package/docs/specifications/5_sas.md +119 -0
- package/docs/specifications/6_mirador.md +119 -0
- package/docs/specifications/7_aikon.md +137 -0
- package/docs/specifications/include/presentation_2.0.webp +0 -0
- package/docs/specifications/include/presentation_2.0_white.png +0 -0
- package/docs/specifications/include/presentation_3.0.png +0 -0
- package/docs/specifications/include/presentation_3.0_resize.png +0 -0
- package/eslint.config.js +27 -0
- package/migrations/baseConfig.js +56 -0
- package/migrations/manageIndex.js +55 -0
- package/migrations/migrate-mongo-config-main.js +8 -0
- package/migrations/migrate-mongo-config-test.js +8 -0
- package/migrations/migrationScripts/20250825185706-collections.js +41 -0
- package/migrations/migrationScripts/20250826194832-annotations2-canvas-index.js +31 -0
- package/migrations/migrationScripts/20250904080710-annotations2-schema.js +42 -0
- package/migrations/migrationScripts/20251002141951-manifest2-schema.js +43 -0
- package/migrations/migrationScripts/20251006212110-manifest-unique-index.js +29 -0
- package/migrations/migrationScripts/20251028115614-annotations2-id-index.js +27 -0
- package/migrations/migrationTemplate.js +25 -0
- package/package.json +78 -0
- package/run.sh +70 -0
- package/scripts/_migrations.sh +79 -0
- package/scripts/_setup.js +31 -0
- package/scripts/setup_mongodb.sh +61 -0
- package/scripts/setup_mongodb_migrate.sh +17 -0
- package/scripts/setup_node.sh +15 -0
- package/scripts/utils.sh +192 -0
- package/setup.sh +20 -0
- package/src/app.js +113 -0
- package/src/config/.env.template +22 -0
- package/src/data/annotations/annotations2.js +419 -0
- package/src/data/annotations/annotations3.js +32 -0
- package/src/data/annotations/routes.js +271 -0
- package/src/data/annotations/routes.test.js +180 -0
- package/src/data/collectionAbstract.js +270 -0
- package/src/data/index.js +29 -0
- package/src/data/manifests/manifests2.js +305 -0
- package/src/data/manifests/manifests2.test.js +53 -0
- package/src/data/manifests/manifests3.js +23 -0
- package/src/data/manifests/routes.js +95 -0
- package/src/data/manifests/routes.test.js +69 -0
- package/src/data/routes.js +141 -0
- package/src/data/routes.test.js +117 -0
- package/src/data/utils/iiif2Utils.js +196 -0
- package/src/data/utils/iiif2Utils.test.js +98 -0
- package/src/data/utils/iiif3Utils.js +0 -0
- package/src/data/utils/iiifUtils.js +18 -0
- package/src/data/utils/routeUtils.js +109 -0
- package/src/data/utils/testUtils.js +253 -0
- package/src/data/utils/utils.js +231 -0
- package/src/db/index.js +48 -0
- package/src/fileServer/annotations.js +39 -0
- package/src/fileServer/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
- package/src/fileServer/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
- package/src/fileServer/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
- package/src/fileServer/data/annotations2Invalid.jsonld +39 -0
- package/src/fileServer/data/annotations2Valid.jsonld +39 -0
- package/src/fileServer/data/bnf_invalid_manifest.json +2806 -0
- package/src/fileServer/data/bnf_valid_manifest.json +2817 -0
- package/src/fileServer/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
- package/src/fileServer/index.js +64 -0
- package/src/fileServer/manifests.js +14 -0
- package/src/fileServer/utils.js +35 -0
- package/src/schemas/index.js +20 -0
- package/src/schemas/schemasBase.js +47 -0
- package/src/schemas/schemasPresentation2.js +417 -0
- package/src/schemas/schemasPresentation3.js +57 -0
- package/src/schemas/schemasResolver.js +71 -0
- package/src/schemas/schemasRoutes.js +277 -0
- package/src/server.js +22 -0
- 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
|
+
})
|