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
@@ -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
+ }
@@ -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
+ }