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.
- package/LICENSE +661 -0
- package/README.md +61 -0
- package/cli/import.js +142 -0
- package/cli/index.js +26 -0
- package/cli/io.js +105 -0
- package/cli/migrate.js +123 -0
- package/cli/mongoClient.js +11 -0
- package/docs/architecture.md +88 -0
- package/docs/db.md +38 -0
- package/docs/dev_iiif_compatibility.md +43 -0
- package/docs/endpoints.md +48 -0
- package/docs/progress.md +159 -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 +396 -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 +27 -0
- package/migrations/baseConfig.js +56 -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 +41 -0
- package/migrations/migrationScripts/20250826194832-annotations2-canvas-index.js +31 -0
- package/migrations/migrationScripts/20250904080710-annotations2-schema.js +42 -0
- package/migrations/migrationScripts/20251002141951-manifest2-schema.js +43 -0
- package/migrations/migrationScripts/20251006212110-manifest-unique-index.js +29 -0
- package/migrations/migrationScripts/20251028115614-annotations2-id-index.js +27 -0
- package/migrations/migrationTemplate.js +25 -0
- package/package.json +78 -0
- package/run.sh +70 -0
- package/scripts/_migrations.sh +79 -0
- package/scripts/_setup.js +31 -0
- package/scripts/setup_mongodb.sh +61 -0
- package/scripts/setup_mongodb_migrate.sh +17 -0
- package/scripts/setup_node.sh +15 -0
- package/scripts/utils.sh +192 -0
- package/setup.sh +20 -0
- package/src/app.js +113 -0
- package/src/config/.env.template +22 -0
- package/src/data/annotations/annotations2.js +419 -0
- package/src/data/annotations/annotations3.js +32 -0
- package/src/data/annotations/routes.js +271 -0
- package/src/data/annotations/routes.test.js +180 -0
- package/src/data/collectionAbstract.js +270 -0
- package/src/data/index.js +29 -0
- package/src/data/manifests/manifests2.js +305 -0
- package/src/data/manifests/manifests2.test.js +53 -0
- package/src/data/manifests/manifests3.js +23 -0
- package/src/data/manifests/routes.js +95 -0
- package/src/data/manifests/routes.test.js +69 -0
- package/src/data/routes.js +141 -0
- package/src/data/routes.test.js +117 -0
- package/src/data/utils/iiif2Utils.js +196 -0
- package/src/data/utils/iiif2Utils.test.js +98 -0
- package/src/data/utils/iiif3Utils.js +0 -0
- package/src/data/utils/iiifUtils.js +18 -0
- package/src/data/utils/routeUtils.js +109 -0
- package/src/data/utils/testUtils.js +253 -0
- package/src/data/utils/utils.js +231 -0
- package/src/db/index.js +48 -0
- package/src/fileServer/annotations.js +39 -0
- package/src/fileServer/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
- package/src/fileServer/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
- package/src/fileServer/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
- package/src/fileServer/data/annotations2Invalid.jsonld +39 -0
- package/src/fileServer/data/annotations2Valid.jsonld +39 -0
- package/src/fileServer/data/bnf_invalid_manifest.json +2806 -0
- package/src/fileServer/data/bnf_valid_manifest.json +2817 -0
- package/src/fileServer/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
- package/src/fileServer/index.js +64 -0
- package/src/fileServer/manifests.js +14 -0
- package/src/fileServer/utils.js +35 -0
- package/src/schemas/index.js +20 -0
- package/src/schemas/schemasBase.js +47 -0
- package/src/schemas/schemasPresentation2.js +417 -0
- package/src/schemas/schemasPresentation3.js +57 -0
- package/src/schemas/schemasResolver.js +71 -0
- package/src/schemas/schemasRoutes.js +277 -0
- package/src/server.js +22 -0
- 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.
|