@paulhectork/aiiinotate 0.12.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 +206 -0
- package/cli/export.js +118 -0
- package/cli/import.js +111 -0
- package/cli/index.js +45 -0
- package/cli/migrate.js +131 -0
- package/cli/serve.js +29 -0
- package/cli/utils/fastifyClient.js +86 -0
- package/cli/utils/io.js +225 -0
- package/cli/utils/mongoClient.js +21 -0
- package/cli/utils/progressbar.js +86 -0
- package/cli/xywhToInt.js +99 -0
- package/config/.env.template +48 -0
- package/docker/Dockerfile +39 -0
- package/docker/README.md +5 -0
- package/docker/docker-compose.yaml +49 -0
- package/docker/docker.sh +43 -0
- package/docker/docker_aiiinotate_import.sh +62 -0
- package/docs/api.md +381 -0
- package/docs/cli.md +132 -0
- package/docs/dev_documentation/dev_architecture.md +88 -0
- package/docs/dev_documentation/dev_db.md +58 -0
- package/docs/dev_documentation/dev_iiif_compatibility.md +43 -0
- package/docs/dev_documentation/dev_notes_quirks_and_troubleshooting.md +143 -0
- package/docs/docker.md +98 -0
- package/docs/includes/report_benchmark_aiiinotate_2026-05-28-02:50:48_7steps.png +0 -0
- package/docs/scalability.md +34 -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 +428 -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 +30 -0
- package/migrations/baseConfig.js +57 -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 +48 -0
- package/migrations/migrationScripts/20250826194832-annotations2-schema.js +42 -0
- package/migrations/migrationScripts/20250904080710-annotations2-indexes.js +69 -0
- package/migrations/migrationScripts/20251002141951-manifests2-schema.js +43 -0
- package/migrations/migrationScripts/20251006212110-manifests2-indexes.js +35 -0
- package/migrations/migrationTemplate.js +25 -0
- package/package.json +82 -0
- package/scripts/get_version.py +12 -0
- package/scripts/run.sh +36 -0
- package/scripts/setup_mongodb.sh +70 -0
- package/scripts/setup_node.sh +15 -0
- package/scripts/update_version.py +30 -0
- package/scripts/utils.sh +65 -0
- package/src/app.js +116 -0
- package/src/constants.js +73 -0
- package/src/data/annotations/annotations2.js +681 -0
- package/src/data/annotations/annotations3.js +28 -0
- package/src/data/annotations/routes.js +335 -0
- package/src/data/annotations/routes.test.js +271 -0
- package/src/data/collectionAbstract.js +283 -0
- package/src/data/index.js +29 -0
- package/src/data/manifests/manifests2.js +378 -0
- package/src/data/manifests/manifests2.test.js +53 -0
- package/src/data/manifests/manifests3.js +23 -0
- package/src/data/manifests/routes.js +122 -0
- package/src/data/manifests/routes.test.js +70 -0
- package/src/data/routes.js +181 -0
- package/src/data/routes.test.js +166 -0
- package/src/db/index.js +50 -0
- package/src/fixtures/annotations.js +41 -0
- package/src/fixtures/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
- package/src/fixtures/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
- package/src/fixtures/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
- package/src/fixtures/data/annotations2Invalid.jsonld +39 -0
- package/src/fixtures/data/annotations2SvgValid.jsonld +81 -0
- package/src/fixtures/data/annotations2Valid.jsonld +39 -0
- package/src/fixtures/data/bnf_invalid_manifest.json +2806 -0
- package/src/fixtures/data/bnf_valid_manifest.json +2817 -0
- package/src/fixtures/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
- package/src/fixtures/generate.js +181 -0
- package/src/fixtures/index.js +69 -0
- package/src/fixtures/manifests.js +14 -0
- package/src/fixtures/utils.js +37 -0
- package/src/schemas/index.js +100 -0
- package/src/schemas/schemasBase.js +19 -0
- package/src/schemas/schemasPresentation2.js +410 -0
- package/src/schemas/schemasPresentation3.js +33 -0
- package/src/schemas/schemasResolver.js +71 -0
- package/src/schemas/schemasRoutes.js +318 -0
- package/src/server.js +25 -0
- package/src/types.js +97 -0
- package/src/utils/iiif2Utils.js +332 -0
- package/src/utils/iiif2Utils.test.js +146 -0
- package/src/utils/iiif3Utils.js +0 -0
- package/src/utils/iiifUtils.js +18 -0
- package/src/utils/logger.js +119 -0
- package/src/utils/routeUtils.js +137 -0
- package/src/utils/svg.js +417 -0
- package/src/utils/testUtils.js +289 -0
- package/src/utils/utils.js +403 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
|
|
2
|
+
|
|
3
|
+
// TODO: schemas are maybe wayyy too strict.
|
|
4
|
+
// convert all enums (the arrays below) to { type: string } ?
|
|
5
|
+
|
|
6
|
+
const oaSelectorValues = [
|
|
7
|
+
"oa:FragmentSelector",
|
|
8
|
+
"oa:CssSelector",
|
|
9
|
+
"oa:XPathSelector",
|
|
10
|
+
"oa:TextQuoteSelector",
|
|
11
|
+
"oa:TextPositionSelector",
|
|
12
|
+
"oa:DataPositionSelector",
|
|
13
|
+
"oa:SvgSelector",
|
|
14
|
+
"oa:RangeSelector",
|
|
15
|
+
"cnt:ContentAsText", // this one is only described in IIIF Presentation API.
|
|
16
|
+
// also allow values without extension
|
|
17
|
+
"FragmentSelector",
|
|
18
|
+
"CssSelector",
|
|
19
|
+
"XPathSelector",
|
|
20
|
+
"TextQuoteSelector",
|
|
21
|
+
"TextPositionSelector",
|
|
22
|
+
"DataPositionSelector",
|
|
23
|
+
"SvgSelector",
|
|
24
|
+
"RangeSelector",
|
|
25
|
+
"ContentAsText"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const motivationValues = [
|
|
29
|
+
"sc:painting",
|
|
30
|
+
"oa:commenting",
|
|
31
|
+
"oa:describing",
|
|
32
|
+
"oa:tagging",
|
|
33
|
+
"oa:linking",
|
|
34
|
+
"painting",
|
|
35
|
+
"commenting",
|
|
36
|
+
"describing",
|
|
37
|
+
"tagging",
|
|
38
|
+
"linking",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const embeddedBodyTypeValues = [
|
|
42
|
+
"oa:TextualBody",
|
|
43
|
+
"cnt:ContentAsText",
|
|
44
|
+
"dctypes:Text",
|
|
45
|
+
"oa:Tag",
|
|
46
|
+
"TextualBody",
|
|
47
|
+
"ContentAsText",
|
|
48
|
+
"Text",
|
|
49
|
+
"Tag",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
function addSchemas(fastify, makeSchemaUri) {
|
|
53
|
+
|
|
54
|
+
/////////////////////////////////////////////
|
|
55
|
+
// SPECIFIC RESOURCES
|
|
56
|
+
|
|
57
|
+
// derived from: https://iiif.io/api/annex/openannotation/context.json
|
|
58
|
+
fastify.addSchema({
|
|
59
|
+
$id: makeSchemaUri("iiifImageApiSelector"),
|
|
60
|
+
type: "object",
|
|
61
|
+
required: [ "@type", "@context" ],
|
|
62
|
+
properties: {
|
|
63
|
+
"@id": { type: "string" },
|
|
64
|
+
"@type": {
|
|
65
|
+
type: "string",
|
|
66
|
+
enum: [ "iiif:ImageApiSelector" ]
|
|
67
|
+
},
|
|
68
|
+
"@context": { type: "string" },
|
|
69
|
+
region: { type: "string" },
|
|
70
|
+
size: { type: "string" },
|
|
71
|
+
rotation: { type: "string" },
|
|
72
|
+
format: { type: "string" },
|
|
73
|
+
quality: { type: "string" },
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// NOTE: we don't support refinedBy and any other recursive selectors.
|
|
78
|
+
// https://github.com/Aikon-platform/aiiinotate/blob/main/docs/specifications/0_w3c_web_annotations.md#selectors-data-model
|
|
79
|
+
fastify.addSchema({
|
|
80
|
+
$id: makeSchemaUri("oaSelector"),
|
|
81
|
+
type: "object",
|
|
82
|
+
required: [ "@type" ], // could also add `value` or `chars` but one or the other may be used, not both.
|
|
83
|
+
properties: {
|
|
84
|
+
"@id": { type: "string" },
|
|
85
|
+
"@type": {
|
|
86
|
+
oneOf: [
|
|
87
|
+
{
|
|
88
|
+
type: "string",
|
|
89
|
+
enum: oaSelectorValues
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
// IIIF 2.1 has examples with multiple `@types`: // `chars` is used by SvgSelector in: https://iiif.io/api/presentation/2.1/#non-rectangular-segments
|
|
93
|
+
type: "array",
|
|
94
|
+
items: {
|
|
95
|
+
type: "string",
|
|
96
|
+
enum: oaSelectorValues
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
value: { type: "string" },
|
|
102
|
+
chars: { type: "string" } // `chars` is used by SvgSelector in: https://iiif.io/api/presentation/2.1/#non-rectangular-segments
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
fastify.addSchema({
|
|
107
|
+
$id: makeSchemaUri("oaOrIiifSelector"),
|
|
108
|
+
type: "object",
|
|
109
|
+
oneOf: [
|
|
110
|
+
{ $ref: makeSchemaUri("oaSelector") },
|
|
111
|
+
{ $ref: makeSchemaUri("iiifImageApiSelector") },
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
fastify.addSchema({
|
|
116
|
+
$id: makeSchemaUri("oaChoiceSelector"),
|
|
117
|
+
type: "object",
|
|
118
|
+
required: [ "@type", "default" ],
|
|
119
|
+
properties: {
|
|
120
|
+
"@type": { type: "string", enum: [ "oa:Choice" ] },
|
|
121
|
+
default: { $ref: makeSchemaUri("oaOrIiifSelector") },
|
|
122
|
+
item: { $ref: makeSchemaUri("oaOrIiifSelector") }
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// selector is either a string, string[], iiifImageApiSelector, iiifImageApiSelector[]. oaSelector, oaSelector[]
|
|
127
|
+
fastify.addSchema({
|
|
128
|
+
$id: makeSchemaUri("selector"),
|
|
129
|
+
anyOf: [
|
|
130
|
+
{ type: "string" },
|
|
131
|
+
{ type: "array", items: { type: "string" } },
|
|
132
|
+
{ $ref: makeSchemaUri("oaOrIiifSelector") },
|
|
133
|
+
{ $ref: makeSchemaUri("oaChoiceSelector") },
|
|
134
|
+
{
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { $ref: makeSchemaUri("oaOrIiifSelector") }
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
fastify.addSchema({
|
|
142
|
+
$id: makeSchemaUri("specificResource"),
|
|
143
|
+
type: "object",
|
|
144
|
+
required: [ "@type", "full", "selector" ],
|
|
145
|
+
properties: {
|
|
146
|
+
"@id": { type: "string" },
|
|
147
|
+
"@type": {
|
|
148
|
+
type: "string",
|
|
149
|
+
enum: [ "oa:SpecificResource" ]
|
|
150
|
+
},
|
|
151
|
+
// NOTE: OA defines a `source` field for SpecificResources, but in IIIF `full` seems to have the same role
|
|
152
|
+
// https://github.com/Aikon-platform/aiiinotate/blob/main/docs/specifications/0_w3c_web_annotations.md#specific-resources-data-model
|
|
153
|
+
full: {
|
|
154
|
+
anyOf: [
|
|
155
|
+
// URI
|
|
156
|
+
{ type: "string" },
|
|
157
|
+
// object describing an image
|
|
158
|
+
{
|
|
159
|
+
type: "object",
|
|
160
|
+
required: [ "@id", "@type" ],
|
|
161
|
+
properties: {
|
|
162
|
+
"@id": { type: "string" },
|
|
163
|
+
"@type": { type: "string" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
manifestShortId: { type: "string" },
|
|
169
|
+
canvasIdx: {
|
|
170
|
+
anyOf: [
|
|
171
|
+
{ type: "number" },
|
|
172
|
+
{ type: "null" }
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
manifestUri: {
|
|
176
|
+
anyOf: [
|
|
177
|
+
{ type: "string" },
|
|
178
|
+
{ type: "null" }
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
xywh: {
|
|
182
|
+
oneOf: [
|
|
183
|
+
{
|
|
184
|
+
type: "array",
|
|
185
|
+
items: { type: "number" }
|
|
186
|
+
},
|
|
187
|
+
{ type: "null" }
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
selector: { $ref: makeSchemaUri("selector") },
|
|
191
|
+
purpose: { type: "string" }
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
/////////////////////////////////////////////
|
|
196
|
+
// ANNOTATIONS
|
|
197
|
+
// NOTE : annotations can define both painting and non-painting annotations.
|
|
198
|
+
|
|
199
|
+
fastify.addSchema({
|
|
200
|
+
$id: makeSchemaUri("annotationTarget"),
|
|
201
|
+
type: "array",
|
|
202
|
+
items: {
|
|
203
|
+
anyOf: [
|
|
204
|
+
// URI
|
|
205
|
+
{ type: "string" },
|
|
206
|
+
// SpecificResource
|
|
207
|
+
{ $ref: makeSchemaUri("specificResource") }
|
|
208
|
+
]
|
|
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": { type: "string" },
|
|
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": { type: "string" },
|
|
311
|
+
"@type": {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: [ "sc:AnnotationList" ]
|
|
314
|
+
},
|
|
315
|
+
"resources": {
|
|
316
|
+
type: "array",
|
|
317
|
+
items: { $ref: makeSchemaUri("annotation") }
|
|
318
|
+
},
|
|
319
|
+
"prev": { type: "string" },
|
|
320
|
+
"next": { type: "string" },
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// NOTE : not sure it's needed.
|
|
325
|
+
fastify.addSchema({
|
|
326
|
+
$id: makeSchemaUri("annotationArray"),
|
|
327
|
+
type: "array",
|
|
328
|
+
items: { $ref: makeSchemaUri("annotation") }
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
/////////////////////////////////////////////
|
|
332
|
+
// MANIFESTS
|
|
333
|
+
|
|
334
|
+
// internal data model for IIIF manifests, containing just what we need.
|
|
335
|
+
// manifests are just stored as an @id, a short ID, an array of canvas Ids. we don't need more info.
|
|
336
|
+
fastify.addSchema({
|
|
337
|
+
$id: makeSchemaUri("manifestMongo"),
|
|
338
|
+
type: "object",
|
|
339
|
+
required: [ "@id", "@type", "manifestShortId", "canvasIds" ],
|
|
340
|
+
properties: {
|
|
341
|
+
"@id": { type: "string" },
|
|
342
|
+
"@type": { type: "string", enum: [ "sc:Manifest" ] },
|
|
343
|
+
manifestShortId: { type: "string" },
|
|
344
|
+
canvasIds: { type: "array", items: { type: "string" } }
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// minimal structure we need to work with a IIIF 2.x manifest.
|
|
349
|
+
fastify.addSchema({
|
|
350
|
+
$id: makeSchemaUri("manifestPublic"),
|
|
351
|
+
type: "object",
|
|
352
|
+
required: [ "@id", "sequences" ],
|
|
353
|
+
properties: {
|
|
354
|
+
"@id": { type: "string" },
|
|
355
|
+
sequences: {
|
|
356
|
+
type: "array",
|
|
357
|
+
items: {
|
|
358
|
+
type: "object",
|
|
359
|
+
required: [ "@id", "canvases" ],
|
|
360
|
+
properties: {
|
|
361
|
+
"@id": { type: "string" },
|
|
362
|
+
canvases: {
|
|
363
|
+
type: "array",
|
|
364
|
+
items: {
|
|
365
|
+
type: "object",
|
|
366
|
+
required: [ "@id" ],
|
|
367
|
+
properties: {
|
|
368
|
+
"@id": { type: "string" }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
/////////////////////////////////////////////
|
|
379
|
+
// COLLETION
|
|
380
|
+
|
|
381
|
+
fastify.addSchema({
|
|
382
|
+
$id: makeSchemaUri("collection"),
|
|
383
|
+
type: "object",
|
|
384
|
+
required: [ "@id", "@type", "@context", "members" ],
|
|
385
|
+
properties: {
|
|
386
|
+
"@context": { type: "string" },
|
|
387
|
+
"@type": { type: "string", enum: [ "sc:Collection" ] },
|
|
388
|
+
"@id": { type: "string" },
|
|
389
|
+
label: { type: "string" },
|
|
390
|
+
members: {
|
|
391
|
+
type: "array",
|
|
392
|
+
items: {
|
|
393
|
+
type: "object",
|
|
394
|
+
required: [ "@id" ],
|
|
395
|
+
properties: {
|
|
396
|
+
"@id": { type: "string" },
|
|
397
|
+
"@type": { type: "string", enum: [ "sc:Manifest" ] },
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return fastify;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
export default addSchemas;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
function addSchemas(fastify, makeSchemaUri) {
|
|
8
|
+
// minimal schema for IIIF manifests3, containing just what we need to process a manifest
|
|
9
|
+
fastify.addSchema({
|
|
10
|
+
$id: makeSchemaUri("manifestPublic"),
|
|
11
|
+
type: "object",
|
|
12
|
+
required: [ "@context", "id", "items" ],
|
|
13
|
+
properties: {
|
|
14
|
+
"@context": { type: "string" },
|
|
15
|
+
id: { type: "string" },
|
|
16
|
+
items: {
|
|
17
|
+
type: "array",
|
|
18
|
+
items: {
|
|
19
|
+
type: "object",
|
|
20
|
+
required: [ "id" ],
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: "string" }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return fastify
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default addSchemas;
|
|
32
|
+
|
|
33
|
+
|
|
@@ -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
|
+
|