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
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # aiiinotate
2
+
3
+ aiiinotate is a fast and lightweight annotations server for IIIF. It relies on `nodejs/fastify` and `mongodb` and provides an API to read/write/update/delete IIIF annotations and index manifests.
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bash setup.sh
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Usage
16
+
17
+ Start the app in dev:
18
+
19
+ ```bash
20
+ npm start
21
+ ```
22
+
23
+ Start the app in prod:
24
+
25
+ ```bash
26
+ npm prod
27
+ ```
28
+
29
+ Test the app:
30
+
31
+ ```bash
32
+ npm test
33
+ ```
34
+
35
+ Run the CLI:
36
+
37
+ ```bash
38
+ npm cli
39
+ ```
40
+
41
+ Process migrations:
42
+
43
+ ```bash
44
+ # create a new migration. NOTE: the `--` is necessary !
45
+ npm run migrate-make -- --migrate-name <your migration name>
46
+
47
+ # apply all pending migrations
48
+ npm run migrate-apply
49
+
50
+ # revert the last migration
51
+ npm run migrate-revert
52
+
53
+ # revert all migrations
54
+ npm run migrate-revert-all)
55
+ ```
56
+
57
+ ---
58
+
59
+ ## License
60
+
61
+ MIT License
package/cli/import.js ADDED
@@ -0,0 +1,142 @@
1
+ import { Command, Option, Argument } from "commander";
2
+
3
+ import Annotations2 from "#annotations/annotations2.js";
4
+ import Annotations3 from "#annotations/annotations3.js";
5
+ import { getFilesToProcess, fileRead } from "#cli/io.js";
6
+
7
+ ////////////////////////////////////////
8
+
9
+ // allowed imports
10
+ const importTypes = [
11
+ "annotation", // import a single annotation
12
+ "annotation-list", // import a IIIF 2.x annotationList
13
+ "annotation-page", // import a IIIF 3.x annotationPage
14
+ "manifest", // import a single manifest
15
+ // "annotation-array", // import a JSON array of IIIF annotations
16
+ // "manifest-array" // import a json array of manifests
17
+ ]
18
+
19
+ // allowed import types per IIIF version
20
+ const allowedImportTypes = {
21
+ 2: ["annotation", "annotation-list", "manifest"],
22
+ 3: ["annotation", "annotation-page", "manifest"]
23
+ }
24
+
25
+ const checkAllowedImportType = (iiifVersion, dataType) => {
26
+ if (
27
+ ! allowedImportTypes[iiifVersion].includes(dataType)
28
+ ) {
29
+ console.error(`${checkAllowedImportType.name}: forbidden import type '${dataType}' for IIIF version '${iiifVersion}'. allowed import types are: ${allowedImportTypes[iiifVersion]}`);
30
+ process.exit(1);
31
+ };
32
+
33
+ }
34
+
35
+ const notImplementedExit = (method) => {
36
+ console.log(`\n\nERROR: import is not implemented '${method}'`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const parseNumber = (x) => Number(x);
41
+
42
+ ////////////////////////////////////////
43
+
44
+ async function importAnnotationPage(annotations3, fileArr, iiifVersion) {
45
+ notImplementedExit(`${importAnnotationPage.name} is not implemented`)
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {Annotations2} annotations2
51
+ * @param {string[]} fileArr
52
+ * @param {2|3} iiifVersion
53
+ */
54
+ async function importAnnotationList(annotations2, fileArr, iiifVersion) {
55
+ // RUN THE SCRIPT:
56
+ // > npm run migrate-revert && npm run migrate-apply && npm run cli import -- annotation-list -i 2 -f ./data/aikon_wit9_man11_anno165_annotation_list.jsonld
57
+ let totalImports = 0
58
+
59
+ for (const file of fileArr) {
60
+ const annotationList = JSON.parse(await fileRead(file));
61
+ const result = await annotations2.insertAnnotationList(annotationList);
62
+ totalImports += Object.keys(result).length;
63
+ }
64
+
65
+ console.log(`\n\nDONE: imported ${totalImports} annotations into Aiiinotate !`);
66
+ return
67
+ }
68
+
69
+ ////////////////////////////////////////
70
+
71
+ /**
72
+ * run the cli
73
+ * @param {string} dataType: one of importTypes
74
+ * @param {object} options
75
+ * @param {import('commander').Command} command
76
+ * @param {import('mongodb').MongoClient} mongoClient
77
+ */
78
+ async function action(mongoClient, command, dataType, options) {
79
+
80
+ /** @type {2 | 3} */
81
+ const iiifVersion = options.iiifVersion;
82
+ /** @type {string[]} */
83
+ const files = options.files;
84
+ /** @type {boolean} */
85
+ const listFiles = options.listFiles;
86
+
87
+ checkAllowedImportType(iiifVersion, dataType);
88
+
89
+ const filesToProcess = await getFilesToProcess(files, listFiles);
90
+
91
+ const annotations2 = new Annotations2(
92
+ mongoClient,
93
+ mongoClient.db()
94
+ );
95
+
96
+ // run
97
+ switch (dataType) {
98
+ case ("annotation-list"):
99
+ await importAnnotationList(annotations2, filesToProcess, iiifVersion);
100
+ break;
101
+ default:
102
+ notImplementedExit(dataType);
103
+ }
104
+ mongoClient.close();
105
+
106
+ }
107
+
108
+ /////////////////////////////////////////
109
+
110
+ /** define the cli */
111
+ function makeImportCommand(mongoClient) {
112
+
113
+ // argument and option name syntax:
114
+ // --opt-name <requiredVal> => you mst provide a value after --opt-name
115
+ // --opt-name [optionalVal] => if `optionalVal` is not provided, --opt-name will be treated as boolean
116
+ const dataTypeArg =
117
+ new Argument("<data-type>", "type of data to import")
118
+ .choices(importTypes);
119
+
120
+ const versionOpt =
121
+ new Option("-i, --iiif-version <version>", "IIIF version")
122
+ .choices(["2", "3"])
123
+ .argParser(parseNumber)
124
+ .makeOptionMandatory();
125
+
126
+ const filesOpt =
127
+ new Option("-f, --files <files...>", "files to process, either as: space-separated filepath(s) to the JSON(s) OR path to a file containing a list of paths to JSON files to process (1 line per path)")
128
+ .makeOptionMandatory();
129
+
130
+ const listFilesOpt =
131
+ new Option("-l, --list-files", "flag indicating that --files points to a file containing a list of JSON files to process (1 line per path)")
132
+
133
+ return new Command("import")
134
+ .description("import data into aiiinotate")
135
+ .addArgument(dataTypeArg)
136
+ .addOption(filesOpt)
137
+ .addOption(versionOpt)
138
+ .addOption(listFilesOpt)
139
+ .action((dataType, options, command) => action(mongoClient, command, dataType, options))
140
+ }
141
+
142
+ export default makeImportCommand;
package/cli/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * command line interface. run through the package.json.
3
+ * usage: npm run cli import -- [args] [opts]
4
+ *
5
+ * NOTE: node recommends only creating 1 mongoclient per app
6
+ * when possible for performance reasons => we create a global mongoClient
7
+ * here, and then pass it down to all the other scripts.
8
+ */
9
+
10
+ import { Command } from "commander";
11
+
12
+ import makeMongoClient from "#cli/mongoClient.js";
13
+ import makeImportCommand from "#cli/import.js";
14
+ import makeMigrateCommand from "#cli/migrate.js";
15
+
16
+ const cli = new Command();
17
+
18
+ const mongoClient = await makeMongoClient();
19
+
20
+ cli
21
+ .name("aiiinotate-cli")
22
+ .description("utility command line interfaces for aiiinotate")
23
+ .addCommand(makeImportCommand(mongoClient))
24
+ .addCommand(makeMigrateCommand(mongoClient));
25
+
26
+ cli.parse(process.argv);
package/cli/io.js ADDED
@@ -0,0 +1,105 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+
4
+ const cwd = process.cwd(); // directory the script is run from
5
+
6
+ /** @returns {Promise<boolean>} true if file `f` exists, false otherwise */
7
+ const fileOk = (f) =>
8
+ fs.promises.access(f, fs.constants.R_OK)
9
+ .then(() => true)
10
+ .catch(() => {
11
+ console.log("file does not exist or could not be read: ", f);
12
+ return false
13
+ });
14
+
15
+ /**
16
+ * @param {string} f
17
+ * @return {Promise<string>}
18
+ */
19
+ const fileRead = (f) =>
20
+ fs.promises.readFile(f, { encoding: "utf8" })
21
+ .then(data => data)
22
+ .catch(() => {
23
+ console.log("error reading file: ", f);
24
+ });
25
+
26
+ /**
27
+ * take an input array of filepaths. convert the paths to absolute, and check that the files exist
28
+ * the cli exits if any of the files don't exist
29
+ * @param {string[]} fileArr
30
+ * @returns { Promise<string[]> } array of absolute filepaths
31
+ */
32
+ async function fileArrayValidate (fileArr) {
33
+ // convert to absolute filepaths
34
+ fileArr = fileArr.map(f =>
35
+ path.isAbsolute(f) ? f : path.join(cwd, f));
36
+
37
+ // validate paths
38
+ // the `fileOk` map is wrapped in a `Promise.all` because `fileOk` returns a promise,
39
+ // so we `await` for all file checks to be performed before ensuring that all files have been found.
40
+ const success = await Promise.all(
41
+ fileArr.map(async (f) => await fileOk(f))
42
+ ).then(filesExistArr =>
43
+ filesExistArr.every(x => x===true)
44
+ );
45
+
46
+ if (!success) {
47
+ console.log("\n\nERROR: some files could not be accessed. exiting...");
48
+ process.exit(1);
49
+ }
50
+ return fileArr
51
+ }
52
+
53
+ /**
54
+ * `file` is a path to a file containing paths to other files (1 file per line).
55
+ * validate all paths and return them as absolute paths
56
+ * @param {str} file
57
+ * @returns {Promise<string[]>}
58
+ */
59
+ async function getFilesInListFile(file) {
60
+ return fileRead(file)
61
+ .then(content =>
62
+ content.split("\n").filter(l => !l.match(/^\s*$/g)) )
63
+ .then(fileArrayValidate);
64
+ }
65
+
66
+ /**
67
+ * get the files to import and return them as absolute paths
68
+ *
69
+ * `fileArr` is a list of paths to either:
70
+ * - (fileArr=false) JSON files to import
71
+ * - (fileArr=true) text files containing paths to the JSONS to import
72
+ * => take `fileArr`, validate that all files exist, extract all filepaths
73
+ * from `fileArr`, and return the array of actual JSON paths to process.
74
+ *
75
+ * NOTE: file order is NOT PRESERVED since files are opened using async pools
76
+ *
77
+ * @param {string[]} fileArr
78
+ * @param {boolean} listFiles
79
+ * @returns {string[]}
80
+ */
81
+ async function getFilesToProcess(fileArr, listFiles=false) {
82
+ let filesToProcess = await fileArrayValidate(fileArr);
83
+
84
+ // if `listFile`, open the files containing paths of files to proces, and redo the same validation process for each file in a list file.
85
+ if ( listFiles ) {
86
+ let filesInListFiles = []
87
+
88
+ await Promise.all(filesToProcess.map(async (theListFile) => {
89
+ const files = await getFilesInListFile(theListFile);
90
+ filesInListFiles = filesInListFiles.concat(files);
91
+ }))
92
+
93
+ filesToProcess = [...new Set(filesInListFiles)]; // deduplicate
94
+ }
95
+
96
+ return filesToProcess
97
+ }
98
+
99
+
100
+ export {
101
+ fileRead,
102
+ fileOk,
103
+ fileArrayValidate,
104
+ getFilesToProcess
105
+ }
package/cli/migrate.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * run and apply migrations.
3
+ *
4
+ * the commands here are wrappers for migrate-mongo.
5
+ * the big particularity is that we handle 2 databases in parrallel:
6
+ * a dev/prod database and a test database (that will be populated
7
+ * by running tests, emptied after running the tests)
8
+ * in turn, we need to apply migrations in parrallel to both databases.
9
+ *
10
+ */
11
+ import path from "node:path";
12
+ import fs from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import { execSync } from "node:child_process"
15
+
16
+ import { Command, Option, Argument } from "commander";
17
+
18
+
19
+ /** @typedef {"make"|"apply"|"revert"|"revert-all"} MigrateOpType */
20
+ const allowedMigrateOp = ["make", "apply", "revert", "revert-all"];
21
+
22
+ const
23
+ // path to current dirctory
24
+ dirCli = path.dirname(fileURLToPath(import.meta.url)),
25
+ dirRoot = path.resolve(dirCli, ".."),
26
+ dirMigrations = path.resolve(dirRoot, "migrations"),
27
+ dirMigrationsScripts = path.resolve(dirMigrations, "migrationScripts"),
28
+ migrationsConfigMain = path.resolve(dirMigrations, "migrate-mongo-config-main.js"),
29
+ migrationsConfigTest = path.resolve(dirMigrations, "migrate-mongo-config-test.js"),
30
+ migrationConfigs = [migrationsConfigMain, migrationsConfigTest];
31
+
32
+ /** return a date in YYYYMMDDhhmmss format */
33
+ function formatDate(date) {
34
+ function pad2(n) { // always returns a string
35
+ return (n < 10 ? "0" : "") + n;
36
+ }
37
+
38
+ return date.getFullYear() +
39
+ pad2(date.getMonth() + 1) +
40
+ pad2(date.getDate()) +
41
+ pad2(date.getHours()) +
42
+ pad2(date.getMinutes()) +
43
+ pad2(date.getSeconds());
44
+ }
45
+
46
+ /**
47
+ * create a single migration file
48
+ * @param {string} migrationName
49
+ */
50
+ function migrateMake(migrationName) {
51
+ if ( migrationName == null ) {
52
+ throw new Error(`migration name must be a string. got ${migrationName}`);
53
+ }
54
+ fs.copyFileSync(
55
+ path.resolve(dirMigrations, "migrationTemplate.js"),
56
+ path.resolve(dirMigrationsScripts, `${formatDate(new Date())}-${migrationName}.js`)
57
+ );
58
+ }
59
+
60
+ /** apply all pending migrations */
61
+ function migrateApply() {
62
+ migrationConfigs.map((mc) => execSync(`npx migrate-mongo up -f ${mc}`));
63
+ }
64
+
65
+ /** revert the last migration */
66
+ function migrateRevert() {
67
+ migrationConfigs.map((mc) => execSync(`npx migrate-mongo down -f ${mc}`));
68
+ }
69
+
70
+ /** revert all migrations */
71
+ function migrateRevertAll() {
72
+ // there are as many migrations as there are files in `dirMigrationsScripts`
73
+ // => revert one migration per migration file
74
+ // do this for each migration file (prod and test database).
75
+ migrationConfigs.map((mc) =>
76
+ fs.readdirSync(dirMigrationsScripts).map((_) =>
77
+ execSync(`npx migrate-mongo down -f ${mc}`)
78
+ )
79
+ )
80
+ }
81
+
82
+ /**
83
+ * run the cli
84
+ * @param {import('mongodb').MongoClient} mongoClient
85
+ * @param {import('commander').Command} command
86
+ * @param {MigrateOpType} mongoClient
87
+ * @param {object} options
88
+ */
89
+ function action(mongoClient, command, migrationOp, options) {
90
+ const { migrationName } = options;
91
+ console.log(">>>", migrationName, options)
92
+
93
+ switch (migrationOp) {
94
+ case ("make"):
95
+ migrateMake(migrationName);
96
+ break;
97
+ case ("apply"):
98
+ migrateApply();
99
+ break;
100
+ case ("revert"):
101
+ migrateRevert();
102
+ break;
103
+ case ("revert-all"):
104
+ migrateRevertAll();
105
+ break;
106
+ }
107
+ }
108
+
109
+ function makeMigrateCommand(mongoClient) {
110
+ const migrationOpArg =
111
+ new Argument("<migration-op>", "name of migration operation").choices(allowedMigrateOp);
112
+
113
+ const migrationNameOpt =
114
+ new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)");
115
+
116
+ return new Command("migrate")
117
+ .description("run database migrations")
118
+ .addArgument(migrationOpArg)
119
+ .addOption(migrationNameOpt)
120
+ .action((migrationOp, options, command) => action(mongoClient, command, migrationOp, options))
121
+ }
122
+
123
+ export default makeMigrateCommand;
@@ -0,0 +1,11 @@
1
+ import { MongoClient } from "mongodb";
2
+
3
+ export default async function() {
4
+ const client = new MongoClient(process.env.MONGODB_CONNSTRING);
5
+ try {
6
+ client.db(process.env.MONGODB_DB); // client.db(config.mongodbName);
7
+ return client;
8
+ } catch (err) {
9
+ console.log("cli/mongoClient: error connecting", err);
10
+ }
11
+ }
@@ -0,0 +1,88 @@
1
+ # Architecture
2
+
3
+ This is a high-level overview of the Aiiinotate and a good place to start if you want to work on the app
4
+
5
+ ---
6
+
7
+ ## Top level plugins
8
+
9
+ Fastify uses a [**plugin system**](https://fastify.dev/docs/latest/Guides/Plugins-Guide/). In short, it means that,
10
+ - to access the global `fastify` instance, your code needs to be a plugin
11
+ - all plugins must export a single function that registers the plugin on the global fastify instance
12
+
13
+ This can be quite convoluted and conflict with a "normal" module architecture, so we do a mix of the two:
14
+ - **all modules at the root** of `src/` are plugins (except `config`)
15
+ - **nested plugins** within each of the above are defined when they need to access the fastify instance.
16
+
17
+ ```
18
+ ./src/fileServer/ - module storing and serving data files for test fixtures
19
+ ./src/schemas/ - JsonSchemas definition and validation
20
+ ./src/db/ - connects the app to the mongojs database
21
+ ./src/data/ - routes and modules to read/write data
22
+
23
+ ```
24
+
25
+ ---
26
+
27
+ ## The `data` plugin
28
+
29
+ ### In general
30
+
31
+ As you can see, the `data` plugin stores the vast majority of the app's logic. It defines:
32
+ - **`route.js` files** HTTP routes for user interactions
33
+ - **`collection classes`** (`abstractCollection.js`, `manifests(2|3).js`, `annotations(2|3).js`) for each MongoJS collection that handles all internal functionnalities for all collections (read/write/update/delete data)
34
+
35
+ Each `route` and `class` is a fastify plugin. Since the `data` plugin is registered last, it can access functionalities from all other root plugins defined above.
36
+
37
+ ### Structure and inheritence
38
+
39
+ ```
40
+ ├── index.js // `data` plugin root
41
+ ├── routes.js // generic routes
42
+ ├── annotations
43
+ │   ├── annotations2.js // plugin for IIIF presentation 2 annotations
44
+ │   ├── annotations3.js // plugin for IIIF presentation 3 annotations
45
+ │   ├── routes.js // routes for both plugins
46
+ ├── collectionAbstract.js // abstract class with functionnalities for all plugins
47
+ ├── manifests
48
+    ├── manifests2.js // plugin for IIIF presentation 2 manifests
49
+    ├── manifests3.js // plugin for IIIF presentation 3 manifests
50
+    ├── routes.js // routes for both plugins
51
+ ```
52
+
53
+ At a high level, `route` files receive data and relegate internal mongoJS interaction to the `collection classes`. Once these processes are finished, the `collection classes` return data to the `routes`, which send the response to the users. So:
54
+
55
+ - the work of `routes` is to define input/output JsonSchemas and delegate to the proper `collection classes`
56
+ - the work of `classes` is to do the database interaction, data formatting etc.
57
+
58
+ Our 5 `collection classes` (`collectionAbstract`, `annotations2`, `annotations3`, `manifests2`, `manifests3`) use class inheritence: all classes inherit from `collectionAbstract`
59
+ - `collectionAbstract` defines collection-agnostic processes that can be used by all other classes.
60
+ - other classes implement functionnalities for a specific data type and collection.
61
+
62
+ ---
63
+
64
+ ## Details: why plugins are complicated ?
65
+
66
+ If using a plugin-only architecture, **you should not do "local" imports** (imports from one part of your app to another): everything that you want to communicate between apps must be registered as a plugin on the global fastify instance, and then other files need to access the fastify instance:
67
+
68
+ ```js
69
+ // here, we define a root plugin with 2 subplugins (1st is a decorator, 2nd a normal plugin).
70
+ fastify.register((instance, opts, done) => {
71
+
72
+ // decorator
73
+ instance.decorate('util', (a, b) => a + b)
74
+ console.log(instance.util('that is ', 'awesome'))
75
+
76
+ // plugin
77
+ fastify.register((instance, opts, done) => {
78
+ console.log(instance.util('that is ', 'awesome'))
79
+ done()
80
+ })
81
+
82
+ done()
83
+ })
84
+ ```
85
+
86
+ In the above example, functionnalities defined in each plugin (except decorators) are encapsulated: a plugin is a scope and everything that's defined in a plugin must be accessed through the fastify instance. There is also the question of plugins vs. decorators, plugin definition order...
87
+
88
+ In short, if using a plugin-only architecture, every single one of your files would have to be a plugin, registered in the proper order... It would be hell.
package/docs/db.md ADDED
@@ -0,0 +1,38 @@
1
+ # DATABASE AND MIGRATIONS
2
+
3
+ ---
4
+
5
+ ## Collections
6
+
7
+ ---
8
+
9
+ ## Validation rules
10
+
11
+ ---
12
+
13
+ ## Migrations
14
+
15
+ Migrations are used to specify changes to the database's structure (creation and deletion of collections and indexes, changes to collection options such as validation rules etc.). Migration is done using [`migrate-mongo`](https://github.com/seppevs/migrate-mongo), a command-line interface for npm.
16
+
17
+ *Note that here, migrations only concern changes to the structure, not changes to the data.*
18
+
19
+ Where things get a bit complicated is that, while in use, our app uses a single database, in **dev mode, our app uses 2 databses**:
20
+ - a `main` database (default database that will be put in production)
21
+ - a `test` database (empty database to run tests, add dummy data to and so on).
22
+
23
+ For consistency, `test` must mirror the structure of `main`: same collections, indexes, validation rules etc. So, we must manage our migrations so that **all changes to the structure of `main` are reflected in the structure of `test`**. This is not possible natively through `migrate-mongo`. Managing both databases in parrallel manually would risk a lot of inconsistencies.
24
+
25
+
26
+ Our solution is to automate the management of both databases in parrallel:
27
+ - there are **2 migration config files**, one for each database
28
+ - both migration config files point to **the same migration scripts folder**, so that both database can apply the same migrations
29
+ - execute all migrations by **wrapping `migrate-mongo` in a homemade script**: [`scripts/migrations.sh`](../scripts/migrations.sh).
30
+
31
+ ```
32
+ root/
33
+ |__migrations/
34
+ |__migrationScripts // folder containing all migration scripts. consumed by both migration config files.
35
+ |__baseConfig.js // base config file that both config files are derived from
36
+ |__migrate-mongo-config-main.js // config file for the main db
37
+ |__migrate-mongo-config-test.js // config file for the test db
38
+ ```
@@ -0,0 +1,43 @@
1
+ # Development notes: IIIF compatibility and variations between IIIF prsentation APIxs 3.x and IIIF 2.x
2
+
3
+ The annotation server uses an internal data model that can convert to and from annotation models defined in IIIF 2.x and 3.x presentation API. Respectively:
4
+ - IIIF 3.x follows the [W3C Web Annotations standard (WA)](https://www.w3.org/TR/annotation-model/)
5
+ - IIIF 2.x follows the [Open Annotations standard (OA)] (http://www.commonsemantics.com/oa/Open%20Annotation%20Data%20Model%20Primer.html#examples)
6
+
7
+ In general, there are [breaking changes](https://iiif.io/api/presentation/3.0/change-log/#1-breaking-changes) between IIIF 2.x and 3.x. For annotations, the OA model allows things that are not possible in WA, and vice versa. Our internal data model tries to store a maximum amount of data from WA and OA annotations, and then do the necessary conversion to produce valid OA/WA annotations.
8
+
9
+ To make things funnier, the original Open Annotations standard website is no longer available.
10
+
11
+ ## Annotations and the `motivation` attribute
12
+
13
+ ### The problem
14
+
15
+ Both OA and WA have a `motivation` attribute that describes the function of the annotation in relation to the Manifest. Mainly, the use of `painting` (WA) / `sc:painting` (OA) indicates that the annotation is the primary content of the canvas and should be rendered.
16
+ - WA allows 2 values: `painting`, `supplementing`
17
+ - OA allows a more values and defines an ontology. [IIIF 2.1 specifies](https://iiif.io/api/presentation/2.1/#comment-annotations) that the value given to a non-painting annotation should be `oa:commenting`
18
+
19
+ In practice, in SAS/Aikon, non-painting annotations have the `motivation`: `[ "oa:tagging", "oa:supplementing" ]`.
20
+
21
+ ### What we do
22
+
23
+ In an annotation, the `motivation` field is a `string[]` array that accepts all values it receives.
24
+ - **when writing data from IIIF to DB**, `motivation` values are connected to string arrays but are not verified.
25
+ - **when converting data from DB to IIIF**, `motivation` values stored in the database are processed:
26
+ - converting to IIIF 2.x:
27
+ - if WA values are stored in the DB, they are converted to 2.x: `painting => sc:painting`, `supplementing => oa:commenting`.
28
+ - if OA values are stored in the DB, they are kept as is
29
+ - converting to IIIF 3.x:
30
+ - if WA values are in the DB, they are kept as is
31
+ - if OA values are in the DB, they are converted to `painting` (if `sc:painting` is in the array of motivations stored) or `commenting` otherwise.
32
+
33
+ ## DCTypes and the `body.type` attribute
34
+
35
+ ### Problem
36
+
37
+ - WA allows : `Dataset`, `Image`, `Video`, `Sound`, `Text`
38
+ - OA allows : `text`, `image`, `sound`, `dataset`, `software`, `interactive`, `event`, `physical`, `object`
39
+ - In IIIF 2.x, values are prefixed by `dctypes:`: `dctypes:text`.
40
+
41
+ ### Solution
42
+
43
+ Since there is a close mapping between the two, we convert to WA allowed types before saving to database.