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 @@
1
+ {"@type": "sc:AnnotationList", "resources": [{"@id": "http://iscd.huma-num.fr/sas/annotation/wit253_man253_anno253_c24_b2204564e1d74023bc38cbd2906e137b", "@type": "oa:Annotation", "dcterms:created": "2025-09-09T11:42:43", "dcterms:modified": "2025-09-09T11:42:43", "resource": [{"@type": "dctypes:Text", "https://iscd.huma-num.fr/sas/full_text": "", "format": "text/html", "chars": "<p></p>"}], "on": [{"@type": "oa:SpecificResource", "within": {"@id": "https://iscd.huma-num.fr/vhs/iiif/v2/wit253_man253_anno253/manifest.json", "@type": "sc:Manifest"}, "selector": {"@type": "oa:Choice", "default": {"@type": "oa:FragmentSelector", "value": "xywh=212,381,1021,1610"}, "item": {"@type": "oa:SvgSelector", "value": "<svg xmlns=\"http://www.w3.org/2000/svg\"><path xmlns='http://www.w3.org/2000/svg' d='M212 381 h 510 v 0 h 510 v 805 v 805 h -510 h -510 v -805Z' id='rectangle_wit253_man253_anno253_c24_b2204564e1d74023bc38cbd2906e137b' data-paper-data='{&quot;strokeWidth&quot;:1,&quot;rotation&quot;:0,&quot;annotation&quot;:null,&quot;nonHoverStrokeColor&quot;:[&quot;Color&quot;,0,1,0],&quot;editable&quot;:true,&quot;deleteIcon&quot;:null,&quot;rotationIcon&quot;:null,&quot;group&quot;:null}' fill-opacity='0' fill='#00ff00' fill-rule='nonzero' stroke='#00ff00' stroke-width='1' stroke-linecap='butt' stroke-linejoin='miter' stroke-miterlimit='10' stroke-dashoffset='0' style='mix-blend-mode: normal'/></svg>"}}, "full": "https://iscd.huma-num.fr/vhs/iiif/v2/wit253_man253_anno253/canvas/c24.json"}], "motivation": ["oa:commenting", "oa:tagging"], "@context": "http://iiif.io/api/presentation/2/context.json"}]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * the `fileServer` plugin makes test files available to the entire fastify app, mostly for testing purposes.
3
+ */
4
+ import fastifyPlugin from "fastify-plugin";
5
+
6
+ import { annotations2Invalid, annotations2Valid, annotationListUri, annotationListUriArray, annotationList, annotationListArray, annotationListUriInvalid, annotationListUriArrayInvalid } from "#src/fileServer/annotations.js";
7
+ import { manifest2Valid, manifest2ValidUri, manifest2Invalid, manifest2InvalidUri } from "#fileServer/manifests.js";
8
+ import { readFileToObject } from "#fileServer/utils.js";
9
+
10
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
11
+
12
+
13
+ /**
14
+ * NOTE: `done` musn't be used with async plugins. it raises an error `FST_ERR_PLUGIN_INVALID_ASYNC_HANDLER`
15
+ * @param {FastifyInstanceType} fastify Encapsulated Fastify Instance
16
+ * @param {object} options
17
+ */
18
+ async function fileServer(fastify, options) {
19
+
20
+ /** route to return a file in `dataDir` */
21
+ fastify.get(
22
+ "/fileServer/:fileName",
23
+ {
24
+ schema: {
25
+ params: {
26
+ type: "object",
27
+ properties: { fileName: { type: "string" } }
28
+ }
29
+ },
30
+ response: {
31
+ 200: {
32
+ type: "string"
33
+ },
34
+ 500: {
35
+ type: "object",
36
+ properties: {
37
+ error: { type: "string" }
38
+ }
39
+ }
40
+ }
41
+ },
42
+ (request, reply) => {
43
+ const { fileName } = request.params;
44
+ return readFileToObject(fileName);
45
+ }
46
+ )
47
+
48
+ fastify.decorate("fileServer", {
49
+ annotationListUri: annotationListUri,
50
+ annotationListUriArray: annotationListUriArray,
51
+ annotationList: annotationList,
52
+ annotationListArray: annotationListArray,
53
+ annotationListUriArrayInvalid: annotationListUriArrayInvalid,
54
+ annotationListUriInvalid: annotationListUriInvalid,
55
+ annotations2Invalid: annotations2Invalid,
56
+ annotations2Valid: annotations2Valid,
57
+ manifest2Valid: manifest2Valid,
58
+ manifest2ValidUri: manifest2ValidUri,
59
+ manifest2Invalid: manifest2Invalid,
60
+ manifest2InvalidUri: manifest2InvalidUri
61
+ });
62
+ }
63
+
64
+ export default fastifyPlugin(fileServer);
@@ -0,0 +1,14 @@
1
+ import { readFileToObject, toUrl } from "#fileServer/utils.js";
2
+
3
+ const manifest2ValidUri = { uri: toUrl("bnf_valid_manifest.json") };
4
+ const manifest2Valid = readFileToObject("bnf_valid_manifest.json");
5
+ const manifest2InvalidUri = { uri: toUrl("bnf_invalid_manifest.json") };
6
+ const manifest2Invalid = readFileToObject("bnf_invalid_manifest.json");
7
+
8
+
9
+ export {
10
+ manifest2ValidUri,
11
+ manifest2Valid,
12
+ manifest2InvalidUri,
13
+ manifest2Invalid
14
+ }
@@ -0,0 +1,35 @@
1
+ import url from "node:url";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+
5
+ // path to dirctory of curent file
6
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
7
+ // path to fileServer/data
8
+ const dataDir = path.join(__dirname, "data");
9
+
10
+ const availableFiles = fs.readdirSync(dataDir);
11
+
12
+ /**
13
+ * for simplicity, `readFileToObject` is synchronous. given that `fileServer` should only be used in tests, there is no performance downgrade for the prod server.
14
+ * @param {string} fn: the filename
15
+ * @returns {object}
16
+ */
17
+ const readFileToObject = (fn) => {
18
+ if ( !availableFiles.includes(fn) ) {
19
+ throw new Error(`file not found: ${fn}`);
20
+ }
21
+ return JSON.parse(fs.readFileSync(path.join(dataDir, fn), { encoding: "utf8" }));
22
+ }
23
+
24
+ /**
25
+ * @param {string} fn
26
+ * @returns {string}
27
+ */
28
+ const toUrl = (fn) => `${process.env.APP_BASE_URL}/fileServer/${fn}`;
29
+
30
+
31
+ export {
32
+ dataDir,
33
+ readFileToObject,
34
+ toUrl
35
+ }
@@ -0,0 +1,20 @@
1
+ import fastifyPlugin from "fastify-plugin";
2
+
3
+ import schemasBase from "#src/schemas/schemasBase.js";
4
+ import schemasPresentation2 from "#src/schemas/schemasPresentation2.js";
5
+ import schemasPresentation3 from "#src/schemas/schemasPresentation3.js";
6
+ import schemasResolver from "#schemas/schemasResolver.js";
7
+ import schemasRoutes from "#src/schemas/schemasRoutes.js";
8
+
9
+ function schemas(fastify, options, done) {
10
+
11
+ fastify.register(schemasResolver);
12
+ fastify.register(schemasBase);
13
+ fastify.register(schemasPresentation2);
14
+ fastify.register(schemasPresentation3);
15
+ fastify.register(schemasRoutes);
16
+
17
+ done()
18
+ }
19
+
20
+ export default fastifyPlugin(schemas);
@@ -0,0 +1,47 @@
1
+ import fastifyPlugin from "fastify-plugin"
2
+
3
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
4
+
5
+ /** @param {"search"|"presentation"} slug */
6
+ const makeSchemaUri = (slug) =>
7
+ `${process.env.APP_BASE_URL}/schemas/${slug}/version`;
8
+
9
+ /**
10
+ * @param {FastifyInstanceType} fastify
11
+ * @param {"search"|"presentation"} slug
12
+ */
13
+ const getSchema = (fastify, slug) =>
14
+ fastify.getSchema(makeSchemaUri(slug));
15
+
16
+ function addSchemas(fastify, options, done) {
17
+
18
+ // fastify.decorate("makeSchemaUri", makeSchemaUri);
19
+ // fastify.decorate("getSchema", (slug) => getSchema(fastify, slug));
20
+
21
+ // schemas are defined on the global `fastify` instance
22
+ fastify.addSchema({
23
+ $id: makeSchemaUri("presentation"),
24
+ type: "integer",
25
+ enum: [2, 3],
26
+ description: "IIIF presentation API versions"
27
+ });
28
+ fastify.addSchema({
29
+ $id: makeSchemaUri("search"),
30
+ type: "integer",
31
+ enum: [1, 2],
32
+ description: "IIIF search API versions"
33
+ });
34
+
35
+ // functions `makeSchemaUri` and `getSchema`
36
+ // are defined in an object that is used to decorate the global `fastify` instance,
37
+ // this namespacing the functions and allowing each plugin
38
+ // in this module to have functions with the same name.
39
+ fastify.decorate("schemasBase", {
40
+ makeSchemaUri: makeSchemaUri,
41
+ getSchema: (slug) => getSchema(fastify, slug)
42
+ }) ;
43
+
44
+ done()
45
+ }
46
+
47
+ export default fastifyPlugin(addSchemas);
@@ -0,0 +1,417 @@
1
+ import fastifyPlugin from "fastify-plugin";
2
+
3
+ import { IIIF_PRESENTATION_2, IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
4
+
5
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
6
+
7
+ // TODO: schemas are maybe wayyy too strict.
8
+ // convert all enums (the arrays below) to { type: string } ?
9
+
10
+ const oaSelectorTypes = [
11
+ "oa:FragmentSelector",
12
+ "oa:CssSelector",
13
+ "oa:XPathSelector",
14
+ "oa:TextQuoteSelector",
15
+ "oa:TextPositionSelector",
16
+ "oa:DataPositionSelector",
17
+ "oa:SvgSelector",
18
+ "oa:RangeSelector",
19
+ "cnt:ContentAsText", // this one is only described in IIIF Presentation API.
20
+ // also allow values without extension
21
+ "FragmentSelector",
22
+ "CssSelector",
23
+ "XPathSelector",
24
+ "TextQuoteSelector",
25
+ "TextPositionSelector",
26
+ "DataPositionSelector",
27
+ "SvgSelector",
28
+ "RangeSelector",
29
+ "ContentAsText"
30
+ ]
31
+
32
+ const motivationValues = [
33
+ "sc:painting",
34
+ "oa:commenting",
35
+ "oa:describing",
36
+ "oa:tagging",
37
+ "oa:linking",
38
+ "painting",
39
+ "commenting",
40
+ "describing",
41
+ "tagging",
42
+ "linking",
43
+ ]
44
+
45
+ const embeddedBodyTypeValues = [
46
+ "oa:TextualBody",
47
+ "cnt:ContentAsText",
48
+ "dctypes:Text",
49
+ "oa:Tag",
50
+ "TextualBody",
51
+ "ContentAsText",
52
+ "Text",
53
+ "Tag",
54
+ ]
55
+
56
+ /** @param {string} slug */
57
+ const makeSchemaUri = (slug) =>
58
+ `${process.env.APP_BASE_URL}/schemas/presentation/${IIIF_PRESENTATION_2}/${slug}`
59
+
60
+ /**
61
+ * @param {FastifyInstanceType} fastify
62
+ * @param {"search"|"presentation"} slug
63
+ */
64
+ const getSchema = (fastify, slug) =>
65
+ fastify.getSchema(makeSchemaUri(slug))
66
+
67
+
68
+ function addSchemas(fastify, options, done) {
69
+
70
+ /////////////////////////////////////////////
71
+ // GENERIC STUFF
72
+
73
+ fastify.addSchema({
74
+ $id: makeSchemaUri("context"),
75
+ type: "string",
76
+ enum: [ IIIF_PRESENTATION_2_CONTEXT["@context"] ]
77
+ });
78
+
79
+ /////////////////////////////////////////////
80
+ // SPECIFIC RESOURCES
81
+
82
+ // derived from: https://iiif.io/api/annex/openannotation/context.json
83
+ fastify.addSchema({
84
+ $id: makeSchemaUri("iiifImageApiSelector"),
85
+ type: "object",
86
+ required: [ "@id", "@type", "@context" ],
87
+ properties: {
88
+ "@id": { type: "string" },
89
+ "@type": {
90
+ type: "string",
91
+ enum: [ "iiif:ImageApiSelector" ]
92
+ },
93
+ "@context": { $ref: makeSchemaUri("context") },
94
+ region: { type: "string" },
95
+ size: { type: "string" },
96
+ rotation: { type: "string" },
97
+ format: { type: "string" },
98
+ quality: { type: "string" },
99
+ }
100
+ });
101
+
102
+ // NOTE: we don't support refinedBy and any other recursive selectors.
103
+ // https://github.com/Aikon-platform/aiiinotate/blob/main/docs/specifications/0_w3c_web_annotations.md#selectors-data-model
104
+ fastify.addSchema({
105
+ $id: makeSchemaUri("oaSelector"),
106
+ type: "object",
107
+ required: [ "@type" ], // could also add `value` or `chars` but one or the other may be used, not both.
108
+ properties: {
109
+ "@id": { type: "string" },
110
+ "@type": {
111
+ anyOf: [
112
+ {
113
+ type: "string",
114
+ enum: oaSelectorTypes
115
+ },
116
+ {
117
+ // IIIF 2.1 has examples with multiple `@types`: // `chars` is used by SvgSelector in: https://iiif.io/api/presentation/2.1/#non-rectangular-segments
118
+ type: "array",
119
+ items: {
120
+ type: "string",
121
+ enum: oaSelectorTypes
122
+ }
123
+ }
124
+ ]
125
+ },
126
+ value: { type: "string" },
127
+ chars: { type: "string" } // `chars` is used by SvgSelector in: https://iiif.io/api/presentation/2.1/#non-rectangular-segments
128
+ }
129
+ })
130
+
131
+ fastify.addSchema({
132
+ $id: makeSchemaUri("oaOrIiifSelector"),
133
+ type: "object",
134
+ oneOf: [
135
+ { $ref: makeSchemaUri("oaSelector") },
136
+ { $ref: makeSchemaUri("iiifImageApiSelector") },
137
+ ]
138
+ })
139
+
140
+ fastify.addSchema({
141
+ $id: makeSchemaUri("oaChoiceSelector"),
142
+ type: "object",
143
+ required: [ "@type", "default" ],
144
+ properties: {
145
+ "@type": { type: "string", enum: ["oa:Choice"] },
146
+ default: { $ref: makeSchemaUri("oaOrIiifSelector") },
147
+ item: { $ref: makeSchemaUri("oaOrIiifSelector") }
148
+ }
149
+ })
150
+
151
+ // selector is either a string, string[], iiifImageApiSelector, iiifImageApiSelector[]. oaSelector, oaSelector[]
152
+ fastify.addSchema({
153
+ $id: makeSchemaUri("selector"),
154
+ anyOf: [
155
+ { type: "string" },
156
+ { type: "array", items: { type: "string" } },
157
+ { $ref: makeSchemaUri("oaOrIiifSelector") },
158
+ { $ref: makeSchemaUri("oaChoiceSelector") },
159
+ {
160
+ type: "array",
161
+ items: { $ref: makeSchemaUri("oaOrIiifSelector") }
162
+ },
163
+ ]
164
+ })
165
+
166
+ fastify.addSchema({
167
+ $id: makeSchemaUri("specificResource"),
168
+ type: "object",
169
+ required: [ "@type", "full", "selector" ],
170
+ properties: {
171
+ "@id": { type: "string" },
172
+ "@type": {
173
+ type: "string",
174
+ enum: [ "oa:SpecificResource" ]
175
+ },
176
+ // NOTE: OA defines a `source` field for SpecificResources, but in IIIF `full` seems to have the same role
177
+ // https://github.com/Aikon-platform/aiiinotate/blob/main/docs/specifications/0_w3c_web_annotations.md#specific-resources-data-model
178
+ full: {
179
+ anyOf: [
180
+ // URI
181
+ { type: "string" },
182
+ // object describing an image
183
+ {
184
+ type: "object",
185
+ required: [ "@id", "@type" ],
186
+ properties: {
187
+ "@id": { type: "string" },
188
+ "@type": { type: "string" }
189
+ }
190
+ }
191
+ ],
192
+ },
193
+ selector: { $ref: makeSchemaUri("selector") },
194
+ purpose: { type: "string" }
195
+ }
196
+ })
197
+
198
+ /////////////////////////////////////////////
199
+ // ANNOTATIONS
200
+ // NOTE : annotations can define both painting and non-painting annotations.
201
+
202
+ fastify.addSchema({
203
+ $id: makeSchemaUri("annotationTarget"),
204
+ anyOf: [
205
+ // URI
206
+ { type: "string" },
207
+ // SpecificResource
208
+ { $ref: makeSchemaUri("specificResource") }
209
+ ]
210
+ })
211
+
212
+ fastify.addSchema({
213
+ $id: makeSchemaUri("motivation"),
214
+ anyOf: [
215
+ { type: "string", enum: motivationValues },
216
+ {
217
+ type: "array",
218
+ items : { type: "string", enum: motivationValues }
219
+ },
220
+ ],
221
+ })
222
+
223
+ fastify.addSchema({
224
+ $id: makeSchemaUri("referencedBody"),
225
+ type: "object",
226
+ required: [ "@id", "@type" ],
227
+ properties: {
228
+ "@id": { type: "string" },
229
+ "@type": { type: "string" }, // should match `dctypes:[a-zA-Z]+`. regex is disabled for performance reasons.
230
+ "format": { type: "string" } // should be a MimeType.
231
+ },
232
+ })
233
+
234
+ // embedded textual body
235
+ fastify.addSchema({
236
+ $id: makeSchemaUri("embeddedBody"),
237
+ type: "object",
238
+ required: [ "@type", "chars" ],
239
+ properties: {
240
+ "@type": {
241
+ anyOf: [
242
+ {
243
+ type: "string",
244
+ enum: embeddedBodyTypeValues
245
+ },
246
+ {
247
+ type: "array",
248
+ items: {
249
+ type: "string",
250
+ enum: embeddedBodyTypeValues
251
+ }
252
+ },
253
+ ]
254
+ },
255
+ chars: { type: "string" },
256
+ motivation: { type: "string" },
257
+ format: { type: "string" }, // should be a MimeType
258
+ }
259
+ })
260
+
261
+ fastify.addSchema({
262
+ $id: makeSchemaUri("body"),
263
+ oneOf: [
264
+ { $ref: makeSchemaUri("embeddedBody") },
265
+ { $ref: makeSchemaUri("referencedBody") },
266
+ {
267
+ type: "array",
268
+ items: {
269
+ type: "object",
270
+ anyOf: [
271
+ { $ref: makeSchemaUri("embeddedBody") },
272
+ { $ref: makeSchemaUri("referencedBody") },
273
+ ]
274
+ }
275
+ }
276
+ ]
277
+ })
278
+
279
+ // NOTE: maeData is used by `mirador-annotations-editor` (MAE) to store data specific to that mirador plugin. since MAE is external to the Aikon ecosystem, we don't define it further to avoid errors if/when its data format changes.
280
+ // we set additionalProperties: true, because otherwise, writing annotations to database will be fine, but sending responses will strip out the contents of maeData.
281
+ fastify.addSchema({
282
+ $id: makeSchemaUri("maeData"),
283
+ additionalProperties: true,
284
+ type: "object"
285
+ })
286
+
287
+ fastify.addSchema({
288
+ $id: makeSchemaUri("annotation"),
289
+ type: "object",
290
+ required: [ "@id", "@context", "@type", "motivation", "on" ],
291
+ properties: {
292
+ "@id": { type: "string" },
293
+ "@context": { $ref: makeSchemaUri("context") },
294
+ "@type": { type: "string", enum: [ "oa:Annotation" ] },
295
+ motivation: { $ref: makeSchemaUri("motivation") },
296
+ on: { $ref: makeSchemaUri("annotationTarget") },
297
+ // in OA, one OR the other should be use, but `oneOf` can't be used in `properties`.
298
+ resource: { $ref: makeSchemaUri("body") },
299
+ bodyValue: { type: "string" },
300
+ maeData: { $ref: makeSchemaUri("maeData") }
301
+ },
302
+ });
303
+
304
+ fastify.addSchema({
305
+ $id: makeSchemaUri("annotationList"),
306
+ type: "object",
307
+ required: ["@id", "@type", "@context", "resources"],
308
+ properties: {
309
+ "@id": { type: "string" },
310
+ "@context": { $ref: makeSchemaUri("context") },
311
+ "@type": {
312
+ type: "string",
313
+ enum: [ "sc:AnnotationList" ]
314
+ },
315
+ "resources": {
316
+ type: "array",
317
+ items: { $ref: makeSchemaUri("annotation") }
318
+ }
319
+ }
320
+ });
321
+
322
+ // NOTE : not sure it's needed.
323
+ fastify.addSchema({
324
+ $id: makeSchemaUri("annotationArray"),
325
+ type: "array",
326
+ items: { $ref: makeSchemaUri("annotation") }
327
+ })
328
+
329
+ /////////////////////////////////////////////
330
+ // MANIFESTS
331
+
332
+ // internal data model for IIIF manifests, containing just what we need.
333
+ // manifests are just stored as an @id, a short ID, an array of canvas Ids. we don't need more info.
334
+ fastify.addSchema({
335
+ $id: makeSchemaUri("manifestMongo"),
336
+ type: "object",
337
+ required: ["@id", "@type", "manifestShortId", "canvasIds"],
338
+ properties: {
339
+ "@id": { type: "string" },
340
+ "@type": { type: "string", enum: ["sc:Manifest"] },
341
+ manifestShortId: { type: "string" },
342
+ canvasIds: { type: "array", items: { type: "string" }}
343
+ }
344
+ })
345
+
346
+ // minimal structure we need to work with a IIIF 2.x manifest.
347
+ fastify.addSchema({
348
+ $id: makeSchemaUri("manifestPublic"),
349
+ type: "object",
350
+ required: ["@id", "sequences"],
351
+ properties: {
352
+ "@id": { type: "string" },
353
+ sequences: {
354
+ type: "array",
355
+ items: {
356
+ type: "object",
357
+ required: [ "@id", "canvases" ],
358
+ properties: {
359
+ "@id": { type: "string" },
360
+ canvases: {
361
+ type: "array",
362
+ items: {
363
+ type: "object",
364
+ required: [ "@id" ],
365
+ properties: {
366
+ "@id": { type: "string" }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ })
375
+
376
+ /////////////////////////////////////////////
377
+ // COLLETION
378
+
379
+ fastify.addSchema({
380
+ $id: makeSchemaUri("collection"),
381
+ type: "object",
382
+ required: [ "@id", "@type", "@context", "members" ],
383
+ properties: {
384
+ "@context": { $ref: makeSchemaUri("context") },
385
+ "@type": { type: "string", enum: [ "sc:Collection" ] },
386
+ "@id": { type: "string" },
387
+ label: { type: "string" },
388
+ members: {
389
+ type: "array",
390
+ items: {
391
+ type: "object",
392
+ required: ["@id"],
393
+ properties: {
394
+ "@id": { type: "string" },
395
+ "@type": { type: "string", enum: [ "sc:Manifest" ] },
396
+ }
397
+ }
398
+ }
399
+ }
400
+ });
401
+
402
+ /////////////////////////////////////////////
403
+ // DONE
404
+
405
+ fastify.decorate("schemasPresentation2", {
406
+ makeSchemaUri: makeSchemaUri,
407
+ getSchema: (slug) => getSchema(fastify, slug)
408
+ })
409
+
410
+ done();
411
+ }
412
+
413
+
414
+
415
+
416
+
417
+ export default fastifyPlugin(addSchemas)
@@ -0,0 +1,57 @@
1
+ import fastifyPlugin from "fastify-plugin";
2
+
3
+ import { IIIF_PRESENTATION_3, IIIF_PRESENTATION_3_CONTEXT } from "#utils/iiifUtils.js";
4
+
5
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
6
+
7
+ /** @param {string} slug */
8
+ const makeSchemaUri = (slug) =>
9
+ `${process.env.APP_BASE_URL}/schemas/presentation/${IIIF_PRESENTATION_3}/${slug}`
10
+
11
+ /**
12
+ * @param {FastifyInstanceType} fastify
13
+ * @param {"search"|"presentation"} slug
14
+ */
15
+ const getSchema = (fastify, slug) =>
16
+ fastify.getSchema(makeSchemaUri(slug))
17
+
18
+ function addSchemas(fastify, options, done) {
19
+
20
+ fastify.addSchema({
21
+ $id: makeSchemaUri("context"),
22
+ type: "string",
23
+ enum: [ IIIF_PRESENTATION_3_CONTEXT["@context"] ]
24
+ });
25
+
26
+ // minimal schema for IIIF manifests3, containing just what we need to process a manifest
27
+ fastify.addSchema({
28
+ $id: makeSchemaUri("manifestPublic"),
29
+ type: "object",
30
+ required: [ "@context", "id", "items" ],
31
+ properties: {
32
+ "@context": { $ref: makeSchemaUri("context") },
33
+ id: { type: "string" },
34
+ items: {
35
+ type: "array",
36
+ items: {
37
+ type: "object",
38
+ required: [ "id" ],
39
+ properties: {
40
+ id: { type: "string" }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ });
46
+
47
+ fastify.decorate("schemasPresentation3", {
48
+ makeSchemaUri: makeSchemaUri,
49
+ getSchema: (slug) => getSchema(fastify, slug)
50
+ });
51
+
52
+ done()
53
+ }
54
+
55
+ export default fastifyPlugin(addSchemas);
56
+
57
+