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,71 @@
1
+ import fastifyPlugin from "fastify-plugin";
2
+
3
+ /**
4
+ * resolve schemas: convert all references to external schemas to embedded JsonSchemas.
5
+ *
6
+ * mongo doesn't implement the whole JSONSchema standard, and in some places in fastify (i.e., routes response schemas), references (`$ref`) are not allowed.
7
+ * in the app and in this module, our schemas are defined according to the full Fastify implementation, to use the schemas everywhere, we need to convert them.
8
+ *
9
+ * conversion primarily means converting all `$ref` references to external schemas to the JsonSchemas referenced.
10
+ *
11
+ * JSONSchema has a recursive data model in which a root schema contains nested schemas in a tree structure. in turn, our conversion function is recursive.
12
+ *
13
+ * NOTE: our conversion function DOES NOT DO ALL CONVERSIONS, only what we need.
14
+ * for example, we suppose that our schemas are built using $ref's to external schemas, and not defining sub-schemas in the `definitions` field.
15
+ *
16
+ * see: https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/#omissions
17
+ */
18
+ function schemasResolver (fastify, schema) {
19
+ const funcName = schemasResolver.name;
20
+
21
+ // no need to process strings, booleans or numbers
22
+ if ( typeof schema==="string" || typeof schema==="number" || typeof schema==="boolean" ) {
23
+ return schema
24
+ }
25
+ // convert each item in arrays
26
+ else if ( Array.isArray(schema) ) {
27
+ return schema.map((item) => schemasResolver(fastify, item));
28
+ }
29
+ // convert each key-value pair. this is actually the main part that will delta to the above branches.
30
+ else if ( schema.constructor === Object ) {
31
+ const out = {};
32
+
33
+ for ( let [k,v] of Object.entries(schema) ) {
34
+ // $id is not allowed => remove it
35
+ if ( k==="$id" ) {
36
+ continue
37
+ }
38
+ // $refs must be resolved => replace the kv pair { $ref: schemaUri } with the corresponding schemas and process convert those schemas to mongo.
39
+ else if ( k==="$ref" ) {
40
+ return schemasResolver(fastify, fastify.getSchema(v))
41
+ }
42
+ // convert JsonSchema integer types to bsonType: int
43
+ else if ( k==="type" && v==="integer" )
44
+ out.bsonType = "int";
45
+ // failsafe for other JsonSchema fields that are unimplemented by Mongo.
46
+ // NOTE: "format" is also a JsonSchema keyword, but the "format" keyword
47
+ // is used in our annotations, so we don't raise if it is encounetered.
48
+ // this filter could be fine-tuned to work only at top-level of schemas. for now, we allow "format¨.
49
+ else if ( ["$schema", "$default", "definitions"].includes(k) ) {
50
+ throw new Error(`${funcName}: JSONSchema field '${k}' conversion to Mongo schema is not implemented ! in schema: ${schema}`);
51
+ }
52
+ // it's a "normal" value => process it.
53
+ else {
54
+ out[k] = schemasResolver(fastify, v);
55
+ }
56
+ }
57
+
58
+ return out;
59
+ }
60
+ else {
61
+ throw new Error(`${funcName}: cannot process unexpected type: '${typeof k}' in schema ${schema}`);
62
+ }
63
+ }
64
+
65
+ function schemasResolverPlugin(fastify, options, done) {
66
+ fastify.decorate("schemasResolver", (schema) => schemasResolver(fastify, schema));
67
+ done();
68
+ }
69
+
70
+ export default fastifyPlugin(schemasResolverPlugin);
71
+
@@ -0,0 +1,277 @@
1
+ /**
2
+ * schemas for routes. these schemas are more permissive than the database schemas, defined for example in `presentation2`.
3
+ */
4
+
5
+ import fastifyPlugin from "fastify-plugin";
6
+
7
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
8
+
9
+
10
+ /** @param {string} slug */
11
+ const makeSchemaUri = (slug) =>
12
+ `${process.env.APP_BASE_URL}/schemas/routes/${slug}`;
13
+
14
+ /**
15
+ * @param {FastifyInstanceType} fastify
16
+ * @param {"search"|"presentation"} slug
17
+ */
18
+ const getSchema = (fastify, slug) =>
19
+ fastify.getSchema(makeSchemaUri(slug));
20
+
21
+
22
+ /**
23
+ *
24
+ * @param {FastifyInstanceType} fastify
25
+ * @param {object?} options
26
+ * @param {function} done
27
+ */
28
+ function addSchemas(fastify, options, done) {
29
+
30
+ ////////////////////////////////////////////////////////
31
+ // ANNOTATIONS ROUTES
32
+
33
+ fastify.addSchema({
34
+ $id: makeSchemaUri("routeAnnotation2"),
35
+ type: "object",
36
+ required: [ "@id", "@type", "motivation", "on" ],
37
+ properties: {
38
+ "@id": { type: "string" },
39
+ "@type": { type: "string" },
40
+ motivation: { anyOf: [
41
+ { type: "string", enum: [ "oa:Annotation" ] },
42
+ { type: "array", items: { type: "string" }}
43
+ ]},
44
+ on: { anyOf: [{ type: "string" }, { type: "object" }]}
45
+ }
46
+ });
47
+
48
+ fastify.addSchema({
49
+ $id: makeSchemaUri("routeAnnotation3"),
50
+ type: "object",
51
+ required: [ "id", "type", "motivation", "target" ],
52
+ properties: {
53
+ id: { type: "string" },
54
+ type: { type: "string", enum: ["Annotation"] },
55
+ motivation: { anyOf: [
56
+ { type: "string" },
57
+ { type: "array", items: { type: "string" } },
58
+ ]},
59
+ target: { anyOf: [{ type: "string" }, { type: "object" }]}
60
+ }
61
+ })
62
+
63
+ fastify.addSchema({
64
+ $id: makeSchemaUri("routeAnnotation2Or3"),
65
+ anyOf: [
66
+ { $ref: makeSchemaUri("routeAnnotation2") },
67
+ { $ref: makeSchemaUri("routeAnnotation3") }
68
+ ]
69
+ });
70
+
71
+ fastify.addSchema({
72
+ $id: makeSchemaUri("routeAnnotationListOrPageUri"),
73
+ type: "object",
74
+ required: ["uri"],
75
+ properties: {
76
+ "uri": { type: "string" },
77
+ }
78
+ });
79
+
80
+ fastify.addSchema({
81
+ $id: makeSchemaUri("routeAnnotationListOrPageUriArray"),
82
+ type: "array",
83
+ items: { $ref: "routeAnnotationListOrPageUri" }
84
+ });
85
+
86
+ fastify.addSchema({
87
+ $id: makeSchemaUri("routeAnnotationList"),
88
+ type: "object",
89
+ required: ["@id", "@type", "resources"],
90
+ properties: {
91
+ "@context": { type: "string" }, // i don't specify the value because @context may be an URI that points to a JSON that contains several namespaces other than "http://iiif.io/api/presentation/2/context.json"
92
+ "@id": { type: "string" },
93
+ "@type": { type: "string", enum: ["sc:AnnotationList"] },
94
+ "resources": {
95
+ type: "array",
96
+ items: { $ref: makeSchemaUri("routeAnnotation2") }
97
+ }
98
+ }
99
+ })
100
+
101
+ fastify.addSchema({
102
+ $id: makeSchemaUri("routeAnnotationPage"),
103
+ type: "object",
104
+ required: ["@id", "@type", "items"],
105
+ properties: {
106
+ "@context": { type: "string" }, // i don't specify the value because @context may be an URI that points to a JSON that contains several namespaces other than "http://iiif.io/api/presentation/2/context.json"
107
+ "id": { type: "string" },
108
+ "type": { type: "string", enum: ["AnnotationPage"] },
109
+ "items": {
110
+ type: "array",
111
+ items: { $ref: makeSchemaUri("routeAnnotation3") }
112
+ }
113
+ }
114
+ });
115
+
116
+ fastify.addSchema({
117
+ $id: makeSchemaUri("routeAnnotationListArray"),
118
+ type: "array",
119
+ items: { $ref: makeSchemaUri("routeAnnotationList") }
120
+ });
121
+
122
+ fastify.addSchema({
123
+ $id: makeSchemaUri("routeAnnotationPageArray"),
124
+ type: "array",
125
+ items: { $ref: makeSchemaUri("routeAnnotationPage") }
126
+ })
127
+
128
+ fastify.addSchema({
129
+ $id: makeSchemaUri("routeAnnotationCreateMany"),
130
+ anyOf: [
131
+ { $ref: makeSchemaUri("routeAnnotationList") },
132
+ { $ref: makeSchemaUri("routeAnnotationPage") },
133
+ { $ref: makeSchemaUri("routeAnnotationListArray") },
134
+ { $ref: makeSchemaUri("routeAnnotationPageArray") },
135
+ { $ref: makeSchemaUri("routeAnnotationListOrPageUri") },
136
+ { $ref: makeSchemaUri("routeAnnotationListOrPageUriArray") },
137
+ ]
138
+ })
139
+
140
+ fastify.addSchema({
141
+ $id: makeSchemaUri("routeAnnotationDelete"),
142
+ oneOf: [
143
+ {
144
+ type: "object",
145
+ required: ["uri"],
146
+ properties: { uri: { type: "string", description: "delete the annotation with this '@id'" } }
147
+ },
148
+ {
149
+ type: "object",
150
+ required: ["manifestShortId"],
151
+ properties: { manifestShortId: { type: "string", description: "delete all annotations for a single manifest" } }
152
+ },
153
+ {
154
+ type: "object",
155
+ required: ["canvasUri"],
156
+ properties: { canvasUri: { type: "string", description: "delete all annotations for a single canvas" } }
157
+ }
158
+ ]
159
+ })
160
+
161
+ ////////////////////////////////////////////////////////
162
+ // MANIFESTS ROUTES
163
+
164
+ fastify.addSchema({
165
+ $id: makeSchemaUri("routeManifest2Or3"),
166
+ anyOf: [
167
+ {
168
+ type: "object",
169
+ required: ["uri"],
170
+ properties: { uri: { type: "string" } }
171
+ },
172
+ { $ref: fastify.schemasPresentation2.makeSchemaUri("manifestPublic") },
173
+ { $ref: fastify.schemasPresentation3.makeSchemaUri("manifestPublic") },
174
+ ]
175
+ });
176
+
177
+ fastify.addSchema({
178
+ $id: makeSchemaUri("routeManifestDelete"),
179
+ type: "object",
180
+ oneOf: [
181
+ {
182
+ type: "object",
183
+ required: ["uri"],
184
+ properties: { uri: { type: "string" } }
185
+ },
186
+ {
187
+ type: "object",
188
+ required: ["manifestShortId"],
189
+ properties: { manifestShortId: { type: "string" } }
190
+ }
191
+ ]
192
+ });
193
+
194
+ ////////////////////////////////////////////////////////
195
+ // GENERIC STUFF
196
+
197
+ fastify.addSchema({
198
+ $id: makeSchemaUri("routeDelete"),
199
+ type: "object",
200
+ oneOf: [
201
+ { $ref: makeSchemaUri("routeAnnotationDelete") },
202
+ { $ref: makeSchemaUri("routeManifestDelete") },
203
+ ]
204
+ });
205
+
206
+ fastify.addSchema({
207
+ $id: makeSchemaUri("routeResponseInsert"),
208
+ type: "object",
209
+ required: [ "insertedCount" ],
210
+ properties: {
211
+ insertedCount: { type: "integer", minimum: 0 },
212
+ insertedIds: {
213
+ type: "array",
214
+ items: { type: "string" }
215
+ },
216
+ preExistingIds: {
217
+ type: "array",
218
+ items: { type: "string" }
219
+ },
220
+ fetchErrorIds: {
221
+ type: "array",
222
+ items: { type: "string" }
223
+ },
224
+ rejectedIds: {
225
+ type: "array",
226
+ items: { type: "string" }
227
+ }
228
+ }
229
+ });
230
+
231
+ fastify.addSchema({
232
+ $id: makeSchemaUri("routeResponseUpdate"),
233
+ type: "object",
234
+ required: ["matchedCount", "modifiedCount", "upsertedCount"],
235
+ properties: {
236
+ matchedCount: { type: "integer" },
237
+ modifiedCount: { type: "integer" },
238
+ upsertedCount: { type: "integer" },
239
+ upsertedId: { type: [ "string", "null" ] } // string|null => the field is nullable !
240
+ }
241
+ });
242
+
243
+ fastify.addSchema({
244
+ $id: makeSchemaUri("routeResponseDelete"),
245
+ type: "object",
246
+ required: [ "deletedCount" ],
247
+ properties: {
248
+ deletedCount: { type: "integer", minimum: 0 }
249
+ }
250
+ });
251
+
252
+ fastify.addSchema({
253
+ $id: makeSchemaUri("routeResponseError"),
254
+ type: "object",
255
+ required: [],// [ "message", "info", "method", "url" ],
256
+ properties: {
257
+ message: { type: "string" },
258
+ info: {}, // only using `{}` equates to JS "Any" type
259
+ method: { type: "string" },
260
+ url: { type: "string" },
261
+ requestBody: {}
262
+ }
263
+ });
264
+
265
+ ////////////////////////////////////////////////////////
266
+
267
+ fastify.decorate("schemasRoutes", {
268
+ makeSchemaUri: makeSchemaUri,
269
+ getSchema: (slug) => getSchema(fastify, slug)
270
+ });
271
+
272
+ done();
273
+ }
274
+
275
+
276
+ export default fastifyPlugin(addSchemas);
277
+
package/src/server.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * serve a fastify app
3
+ */
4
+
5
+ import build from "#src/app.js";
6
+
7
+ /**
8
+ * @param {object} options
9
+ */
10
+ async function start (options) {
11
+
12
+ const fastify = await build();
13
+
14
+ try {
15
+ fastify.listen({ port: process.env.APP_PORT });
16
+ } catch(err) {
17
+ fastify.log.error(err);
18
+ process.exit(1);
19
+ }
20
+ };
21
+
22
+ start();
package/src/types.js ADDED
@@ -0,0 +1,93 @@
1
+ /** @typedef {import("node:test")} NodeTestType */
2
+
3
+ /** @typedef {import("fastify").FastifyInstance} FastifyInstanceType */
4
+ /** @typedef {import("fastify").FastifyReply} FastifyReplyType */
5
+
6
+ /** @typedef {import("mongodb").Db} MongoDbType */
7
+ /** @typedef {import("mongodb").ObjectId } MongoObjectId */
8
+ /** @typedef {import("mongodb").Collection} MongoCollectionType */
9
+ /** @typedef {import("mongodb").InsertManyResult} MongoInsertManyResultType */
10
+ /** @typedef {import("mongodb").InsertOneResult} MongoInsertOneResultType */
11
+ /** @typedef {MongoInsertManyResultType | MongoInsertOneResultType} MongoInsertResultType */
12
+ /** @typedef {import("mongodb").UpdateResult} MongoUpdateResultType */
13
+ /** @typedef {import("mongodb").DeleteResult} MongoDeleteResultType */
14
+ /** @typedef {import("mongodb").MongoClient} MongoClientType */
15
+
16
+ /** @typedef {import("ajv").ValidateFunction} AjvValidateFunctionType */
17
+
18
+ /** @typedef {"uri"|"manifestShortId"|"canvasUri"} AnnotationsDeleteKeyType */
19
+
20
+ /** @typedef {2|3} IiifPresentationVersionType */
21
+ /** @typedef {"manifests2"|"manifests3"|"annotations2"|"annotations3"} CollectionNamesType */
22
+ /**
23
+ * @typedef InsertResponseType
24
+ * @type {object}
25
+ * @property {number} insertedCount - the number of documents that were properly inserted
26
+ * @property {Array<string|MongoObjectId>} insertedIds - the ids of the documents that were properly inserted
27
+ * @property {Array<string|MongoObjectId>} [preExistingIds] - the ids of documents that were aldready inserted in a collection with a UNIQUE clause. USED ONLY BY `manifests2` and `manifests3`
28
+ * @property {Array<string>} [fetchErrorIds] - the ids of referenced documents that could not be fetched. USED ONLY BY `manifests2` and `manifests3`
29
+ * @property {{ [x: string]: string }} [rejectedIds] - the ids of the documents that did not pass validation, mapped to validation errors. USED ONLY BY `manifests2` and `manifests3`
30
+ */
31
+
32
+ /**
33
+ * @typedef {Array<InsertResponseType>} InsertResponseArrayType
34
+ */
35
+
36
+ /**
37
+ * @typedef UpdateResponseType
38
+ * @type {object}
39
+ * @property {number} matchedCount
40
+ * @property {number} modifiedCount
41
+ * @property {number} upsertedCount
42
+ * @property {string?} [upsertedId]
43
+ */
44
+
45
+ /**
46
+ * @typedef DeleteResponseType
47
+ * @type {object}
48
+ * @property {number} deletedCount
49
+ */
50
+
51
+ /**
52
+ * @typedef {"read"|"insert"|"update"|"delete"} DataOperationsType
53
+ * the allowed mongo operations.
54
+ */
55
+
56
+ /**
57
+ * @typedef Manifest2InternalType
58
+ * app and database-side structure of IIIF manifests
59
+ * @type {object}
60
+ * @property {string} ["@id"] - the manifest's '@id'
61
+ * @property {string} manifestShortId
62
+ * @property {string[]} canvasIds
63
+ */
64
+
65
+ /**
66
+ * @typedef IiifCollection2Type
67
+ * IIIF manifests collection
68
+ * @type {object}
69
+ * @property {string} ["@context"]
70
+ * @property {string} ["@id"]
71
+ * @property {"sc:Collection"} ["@type"]
72
+ * @property {string?} label
73
+ * @property {Array<{ "@type": "sc:Manifest", "@id": string }>} members
74
+ */
75
+
76
+ /**
77
+ * @typedef {import("#manifests/manifests2.js").Manifests2InstanceType} Manifests2InstanceType
78
+ * type of an instance of the Manifests2 class.
79
+ */
80
+ /**
81
+ * @typedef {import("#annotations/annotations2.js").Annotations2InstanceType} Annotations2InstanceType
82
+ * type of an instance of the Annotations2 class.
83
+ */
84
+ /**
85
+ * @typedef {import("#manifests/manifests3.js").Manifests3InstanceType} Manifests3InstanceType
86
+ * type of an instance of the Manifests3 class.
87
+ */
88
+ /**
89
+ * @typedef {import("#annotations/annotations3.js").Annotations3InstanceType} Annotations3InstanceType
90
+ * type of an instance of the Annotations3 class.
91
+ */
92
+
93
+ export {}