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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import util from "node:util";
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} obj
|
|
7
|
+
* @param {string} key
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
const objectHasKey = (obj, key) =>
|
|
11
|
+
Object.keys(obj).includes(key);
|
|
12
|
+
|
|
13
|
+
const addKeyValueToObj = (obj, key, value) => {
|
|
14
|
+
obj[key] = value;
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isNullOrUndefined = (v) => v == null;
|
|
19
|
+
|
|
20
|
+
const isNullish = (v) => v == null || !v.length;
|
|
21
|
+
|
|
22
|
+
/** o is an object but not an array. https://stackoverflow.com/a/44556453 */
|
|
23
|
+
const isObject = (o) => o.constructor === Object;
|
|
24
|
+
|
|
25
|
+
const isNonEmptyArray = (a) => Array.isArray(a) && a.length;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* extend objOut with a key-value pair fron objIn if a key is in objIn
|
|
29
|
+
* @param {object} objIn: the object that should contain key
|
|
30
|
+
* @param {object} objOut: the object to extend
|
|
31
|
+
* @param {string|number} key: the key in objIn.
|
|
32
|
+
* @param {string?} newKey: (optional) the name of the new key in objOut. if undefined, key is used.
|
|
33
|
+
*/
|
|
34
|
+
const addKeyValueToObjIfHasKey = (objIn, objOut, key, newKey) =>
|
|
35
|
+
Object.keys(objIn).includes(key)
|
|
36
|
+
? addKeyValueToObj(
|
|
37
|
+
objOut,
|
|
38
|
+
isNullOrUndefined(newKey) ? key : newKey,
|
|
39
|
+
objIn[key]
|
|
40
|
+
)
|
|
41
|
+
: objOut;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* hash generating function, copied from: https://stackoverflow.com/a/52171480
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
const getHash = (str, seed=0) => {
|
|
48
|
+
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
|
49
|
+
for(let i = 0, ch; i < str.length; i++) {
|
|
50
|
+
ch = str.charCodeAt(i);
|
|
51
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
52
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
53
|
+
}
|
|
54
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
55
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
56
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
57
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
58
|
+
return String(4294967296 * (2097151 & h2) + (h1 >>> 0));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `obj` is an object of form `{ key1: val1, key2: val2, key3: val3 }`, where only a single key-value pair can be defined at the same time.
|
|
63
|
+
* return the only key-value pair that is not undefined.
|
|
64
|
+
*
|
|
65
|
+
* use case: in a route's querystring, there may be different options that are mutually exclusive (defined using JSONschema oneOf). in that case, return the only key-value pair that is not undefined.
|
|
66
|
+
* see: src/data/annotations2: '/annotations/:iiifPresentationVersion/delete'
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<object>} obj
|
|
69
|
+
* @returns {Array<string, any>?}
|
|
70
|
+
*/
|
|
71
|
+
const getFirstNonEmptyPair = (obj) => {
|
|
72
|
+
let [ key,val ] = [ undefined, undefined ];
|
|
73
|
+
[ key, val ] = Object.entries(obj).find(([k,v]) => v != null);
|
|
74
|
+
if ( key!== undefined && val!==undefined ) {
|
|
75
|
+
return [key, val];
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* if obj[typeKey] !== expectedTypeVal, throw
|
|
82
|
+
* @param {object} obj
|
|
83
|
+
* @param {2|3} iiifPresentationVersion
|
|
84
|
+
* @param {string|number} typeKey
|
|
85
|
+
* @param {any} expectedTypeVal
|
|
86
|
+
*/
|
|
87
|
+
const throwIfValueError = (obj, typeKey, expectedTypeVal) => {
|
|
88
|
+
if ( obj[typeKey] !== expectedTypeVal ) {
|
|
89
|
+
throw new Error(`expected value '${expectedTypeVal}' for key '${typeKey}', got: '${obj[typeKey]}' in object ${inspectObj(obj)}`);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* if obj[key] is undefined, throw
|
|
95
|
+
* @param {object} obj
|
|
96
|
+
* @param {string|number} key
|
|
97
|
+
*/
|
|
98
|
+
const throwIfKeyUndefined = (obj, key) => {
|
|
99
|
+
if ( !objectHasKey(obj, key) ) {
|
|
100
|
+
throw new Error(`key '${key}' not found in object ${inspectObj(obj)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* shallow-compare 2 arrays.
|
|
106
|
+
* @param {Array} a1
|
|
107
|
+
* @param {Array} a2
|
|
108
|
+
* @param {boolean} sort: if `true`, sort before comparing
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
const arrayEqualsShallow = (a1, a2, sort=false) => {
|
|
112
|
+
if ( !Array.isArray(a1) || !Array.isArray(a2) ) {
|
|
113
|
+
throw new Error(`Incorrect type: 'a1', 'a2' must be arrays, got '${typeof a1}' and '${typeof a2}' on a1='${a1}' and a2='${a2}'`)
|
|
114
|
+
}
|
|
115
|
+
if ( sort ) {
|
|
116
|
+
a1 = a1.sort();
|
|
117
|
+
a2 = a2.sort();
|
|
118
|
+
}
|
|
119
|
+
if ( a1.length!==a2.length ) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return a1.every((el, i) => a2[i]===el);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* convert object `x` to an array if it is not aldready an array.
|
|
127
|
+
* if `convertedFlag`, return [ obj, converted ]. `converted` is `true` if the object was converted to an array, false otherwise.
|
|
128
|
+
* @param {any} x
|
|
129
|
+
* @param {boolean} convertedFlag
|
|
130
|
+
* @returns {Array | Array<Array, boolean> }
|
|
131
|
+
*/
|
|
132
|
+
const maybeToArray = (x, convertedFlag=false) =>
|
|
133
|
+
convertedFlag
|
|
134
|
+
? Array.isArray(x) ? [x, false] : [[x], true]
|
|
135
|
+
: Array.isArray(x) ? x : [x];
|
|
136
|
+
|
|
137
|
+
const pathToUrl = (path) =>
|
|
138
|
+
`${process.env.APP_BASE_URL}${path}`
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* display a detailed and nested view of an object. to be used with console.log.
|
|
142
|
+
* @param {any} obj - object to inspect
|
|
143
|
+
* @param {number|Infinity} maxLines - maximum number of lines in string output. defaults to 100.
|
|
144
|
+
* @returns
|
|
145
|
+
*/
|
|
146
|
+
const inspectObj = (obj, maxLines=100) => {
|
|
147
|
+
const
|
|
148
|
+
str = util.inspect(obj, {showHidden: false, depth: null, colors: true}),
|
|
149
|
+
strArr = str.split("\n"),
|
|
150
|
+
strLen = strArr.length;
|
|
151
|
+
// remove the middle lines if `str` is too long
|
|
152
|
+
if ( strLen > maxLines ) {
|
|
153
|
+
const
|
|
154
|
+
startProportion = 0.8,
|
|
155
|
+
startSlice = strArr.slice(0, Math.round(0.8 * maxLines)),
|
|
156
|
+
endSlice = strArr.slice(-Math.round((1-startProportion) * maxLines), -1);
|
|
157
|
+
return (
|
|
158
|
+
startSlice.join("\n")
|
|
159
|
+
+ `\n ... inspectObj: ${strLen - maxLines} lines omitted ... \n`
|
|
160
|
+
+ endSlice.join("\n")
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
return str;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* return a random item from an array.
|
|
168
|
+
* @param {any[]} arr
|
|
169
|
+
* @returns {any}
|
|
170
|
+
*/
|
|
171
|
+
const getRandomItem = (arr) =>
|
|
172
|
+
arr.at(Math.floor(Math.random() * arr.length));
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* AJV instance to run JsonSchema compilation/validation anywhere in the app
|
|
176
|
+
* (not just in Fastify route definition and Mongo interactions).
|
|
177
|
+
* NOTE: this is a workaround since i could not get to access fastify's AJV instance, although fastify uses AJV internally.
|
|
178
|
+
*/
|
|
179
|
+
const ajv = new Ajv({
|
|
180
|
+
removeAdditional: false,
|
|
181
|
+
useDefaults: true,
|
|
182
|
+
coerceTypes: true,
|
|
183
|
+
allErrors: true
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* wrapper for `ajv.compile`. `schema` must be a schema resolved using `fastify.schemasResolver()`
|
|
188
|
+
* NOTE: this function exists because using the native `ajv.compile` on fastify schemas (with `$id`, `$ref`...) causes tests to fail WITHOUT launching an error., making it very hard to debug
|
|
189
|
+
* @param {object} schema - jsonSchema
|
|
190
|
+
* @returns {import("#types").AjvValidateFunctionType}
|
|
191
|
+
*/
|
|
192
|
+
const ajvCompile = (schema) => {
|
|
193
|
+
if ( objectHasKey(schema, "$id") || objectHasKey(schema, "$ref") ) {
|
|
194
|
+
const err = new Error(`ajvCompile: 'schema' has not been resolved. use 'fastify.schemasResolver()' to resolve the schema before compiling it, on schema: ${inspectObj(schema)}`);
|
|
195
|
+
// `console.error` is necessary to be sure that the error will be displayed
|
|
196
|
+
console.error(err);
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
return ajv.compile(schema);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* print in a box for debug purposes
|
|
204
|
+
* @param {any} data
|
|
205
|
+
* @param {string} prefix
|
|
206
|
+
*/
|
|
207
|
+
const visibleLog = (data, prefix) => {
|
|
208
|
+
console.log("<".repeat(100));
|
|
209
|
+
if ( prefix ) console.log(prefix);
|
|
210
|
+
console.log(inspectObj(data));
|
|
211
|
+
console.log(">".repeat(100));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export {
|
|
215
|
+
maybeToArray,
|
|
216
|
+
pathToUrl,
|
|
217
|
+
getHash,
|
|
218
|
+
isNullish,
|
|
219
|
+
isObject,
|
|
220
|
+
objectHasKey,
|
|
221
|
+
addKeyValueToObjIfHasKey,
|
|
222
|
+
getFirstNonEmptyPair,
|
|
223
|
+
inspectObj,
|
|
224
|
+
getRandomItem,
|
|
225
|
+
arrayEqualsShallow,
|
|
226
|
+
throwIfKeyUndefined,
|
|
227
|
+
throwIfValueError,
|
|
228
|
+
ajvCompile,
|
|
229
|
+
visibleLog,
|
|
230
|
+
isNonEmptyArray
|
|
231
|
+
}
|
package/src/db/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fastifyPlugin from "fastify-plugin"
|
|
2
|
+
import fastifyMongo from "@fastify/mongodb"
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('fastify').FastifyInstance} FastifyInstance */
|
|
5
|
+
/** @typedef {import('mongodb').Db} MongoDB */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* empty all collections. can only be used on the test database for obvious reasons.
|
|
9
|
+
* @param {MongoDB} db
|
|
10
|
+
*/
|
|
11
|
+
const emptyCollections = async (db) => {
|
|
12
|
+
if ( db.databaseName !== process.env.MONGODB_DB_TEST ) {
|
|
13
|
+
throw new Error(`'emptyCollections' may only be used on test database defined by .env variable 'MONGODB_DB_TEST'. expected test database '${process.env.MONGODB_DB_TEST}' but working on database '${db.databaseName}'`);
|
|
14
|
+
}
|
|
15
|
+
await Promise.all(
|
|
16
|
+
[
|
|
17
|
+
"annotations3",
|
|
18
|
+
"annotations2",
|
|
19
|
+
"manifests3",
|
|
20
|
+
"manifests2"
|
|
21
|
+
].map(async (collectionName) =>
|
|
22
|
+
await db.collection(collectionName).deleteMany({})
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {FastifyInstance} fastify
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
*/
|
|
31
|
+
async function dbConnector(fastify, options) {
|
|
32
|
+
const connString =
|
|
33
|
+
options.test
|
|
34
|
+
? process.env.MONGODB_CONNSTRING_TEST
|
|
35
|
+
: process.env.MONGODB_CONNSTRING;
|
|
36
|
+
|
|
37
|
+
await fastify.register(fastifyMongo, {
|
|
38
|
+
forceClose: true,
|
|
39
|
+
url: connString
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if ( options.test ) {
|
|
43
|
+
fastify.decorate("emptyCollections", () => emptyCollections(fastify.mongo.db));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// wrapping a plugin function with fastify-plugin exposes what is declared inside the plugin to the parent scope.
|
|
48
|
+
export default fastifyPlugin(dbConnector)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// test data for the annotations create and createMany functions.
|
|
2
|
+
import { readFileToObject, toUrl } from "#fileServer/utils.js";
|
|
3
|
+
|
|
4
|
+
const annotations2Invalid = readFileToObject("annotations2Invalid.jsonld");
|
|
5
|
+
const annotations2Valid = readFileToObject("annotations2Valid.jsonld");
|
|
6
|
+
|
|
7
|
+
const annotationListUri = {
|
|
8
|
+
uri: toUrl("annotationList_aikon_wit9_man11_anno165_all.jsonld")
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const annotationListUriArray = [
|
|
12
|
+
{ uri: toUrl("annotationList_vhs_wit250_man250_anno250_all.jsonld") },
|
|
13
|
+
{ uri: toUrl("annotationList_vhs_wit253_man253_anno253_all.jsonld") }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// will trigger an error because the path doesn't exist
|
|
17
|
+
const annotationListUriInvalid = { uri: "/fileServer/annotationList_that_does_not_exist.jsonld" };
|
|
18
|
+
|
|
19
|
+
const annotationListUriArrayInvalid = [
|
|
20
|
+
{ uri: "/fileServer/annotationList_that_does_not_exist.jsonld" }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const annotationList = readFileToObject("annotationList_aikon_wit9_man11_anno165_all.jsonld");
|
|
24
|
+
|
|
25
|
+
const annotationListArray = [
|
|
26
|
+
readFileToObject("annotationList_vhs_wit250_man250_anno250_all.jsonld"),
|
|
27
|
+
readFileToObject("annotationList_vhs_wit253_man253_anno253_all.jsonld")
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
annotations2Invalid,
|
|
32
|
+
annotations2Valid,
|
|
33
|
+
annotationListUri,
|
|
34
|
+
annotationListUriArray,
|
|
35
|
+
annotationListUriInvalid,
|
|
36
|
+
annotationList,
|
|
37
|
+
annotationListArray,
|
|
38
|
+
annotationListUriArrayInvalid
|
|
39
|
+
}
|