@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
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# aiiinotate
|
|
2
|
+
|
|
3
|
+
aiiinotate is a **fast and lightweight annotation server for IIIF**.
|
|
4
|
+
|
|
5
|
+
- **aiiinotate relies on** `nodejs/fastify` and `mongodb`
|
|
6
|
+
- **provides a REST API** to read/write/update/delete IIIF annotations and index manifests.
|
|
7
|
+
- **is distributed as an NPM package**, can be used through NPM or Docker
|
|
8
|
+
- **is built for scalability and speed**: [in benchmarks](https://github.com/paulhectork/aiiinotate-benchmark) aiiinotate stores millions of annotations and its response times are always between $$\frac{1}{10}$$ and $$\frac{1}{100}$$ seconds
|
|
9
|
+
|
|
10
|
+
NOTE: currently, only annotations following the IIIF presentation API 2.0 and 2.1 are supported.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## API
|
|
15
|
+
|
|
16
|
+
See the [docs on the aiiinotate API](https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/api.md).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## PROD USAGE
|
|
21
|
+
|
|
22
|
+
### Install
|
|
23
|
+
|
|
24
|
+
1. **install mongodb** (see [dev installation script for help](./scripts/setup_mongodb.sh) and checkout the [official installation guide](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/#std-label-install-mdb-community-ubuntu))
|
|
25
|
+
|
|
26
|
+
2. **install aiiinotate**
|
|
27
|
+
```bash
|
|
28
|
+
npm install aiiinotate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Env definition
|
|
32
|
+
|
|
33
|
+
#### Basic definition
|
|
34
|
+
|
|
35
|
+
Copy [`config/.env.template`](./config/.env.template) to `.env` and edit it.
|
|
36
|
+
|
|
37
|
+
#### Runtime env sourcing
|
|
38
|
+
|
|
39
|
+
`aiiinotate` runs in a subproicess and won't inherit variables from a plain bash `source` call. Use either of these instead:
|
|
40
|
+
|
|
41
|
+
1. **`dotenvx` (recommended)**:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx dotenvx run -f /path/to/.env -- aiiinotate <command>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. **manual export**:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
set -a && source /path/to/.env && set +a
|
|
51
|
+
aiiinotate <command>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For clarity, we omit env sourcing from the below commands.
|
|
55
|
+
|
|
56
|
+
### Setup the app
|
|
57
|
+
|
|
58
|
+
1. **start `mongod`**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sudo systemctl start mongod
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
2. **create and configure the database**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
aiiinotate migrate apply
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Usage
|
|
71
|
+
|
|
72
|
+
All commands are accessible through a CLI (`./src/cli`).
|
|
73
|
+
|
|
74
|
+
#### Run the app
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
aiiinotate serve prod
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Run the CLI
|
|
81
|
+
|
|
82
|
+
The base command is:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
aiiinotate -- <command>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
It will give full access to the CLI interface of Aiiinotate. Run `aiiinotate --help` for more info.
|
|
89
|
+
|
|
90
|
+
Use the CLI to:
|
|
91
|
+
- import data
|
|
92
|
+
- export data
|
|
93
|
+
- apply and manage migrations
|
|
94
|
+
|
|
95
|
+
For more information, see [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/main/docs/cli.md).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## DOCKER USAGE
|
|
100
|
+
|
|
101
|
+
See the docs [here](https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/docker.md)
|
|
102
|
+
|
|
103
|
+
For a Mirador integration, see [the reference implementation](github.com/paulhectork/mirador-aiiinotate/tree/main) (aiiinotate + MongoDB + Mirador 4 + MAE bundled in a single `docker-compose`)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## DEV USAGE
|
|
108
|
+
|
|
109
|
+
### Install
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# clone the repo
|
|
113
|
+
git clone git@github.com:Aikon-platform/aiiinotate.git
|
|
114
|
+
|
|
115
|
+
# move inside it
|
|
116
|
+
cd aiiinotate
|
|
117
|
+
|
|
118
|
+
# install mongodb
|
|
119
|
+
bash ./scripts/setup_mongodb.sh
|
|
120
|
+
|
|
121
|
+
# install node
|
|
122
|
+
bash ./scripts/setup_node.sh
|
|
123
|
+
|
|
124
|
+
# install dependencies
|
|
125
|
+
npm i
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Setup
|
|
129
|
+
|
|
130
|
+
After installing, some setup must be done
|
|
131
|
+
|
|
132
|
+
1. **setup your `.env`** file after [`config/.env.template`](./config/.env.template) and place it at `./config/.env`.
|
|
133
|
+
|
|
134
|
+
2. **start `mongod`**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
sudo systemctl start mongod
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
3. **configure the database**
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm run migrate apply
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Usage
|
|
147
|
+
|
|
148
|
+
Remember to have your `mongodb` service running: `sudo systemctl start mongod` !
|
|
149
|
+
|
|
150
|
+
#### Start the app
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# reload enabled
|
|
154
|
+
npm run dev
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Test the app
|
|
158
|
+
|
|
159
|
+
Note that the tests will probably fail if you set the env variable `AIIINOTATE_STRICT_MODE` to `true`.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run test
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Run the CLI
|
|
166
|
+
|
|
167
|
+
See [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/cli.md) for more info:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run cli
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use the CLI to:
|
|
174
|
+
- import data
|
|
175
|
+
- export data
|
|
176
|
+
- apply and manage database migrations
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Test coverage
|
|
181
|
+
|
|
182
|
+
aiiinotate is well tested: **~90% test coverage** on all files !
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
186
|
+
ℹ file | line % | branch % | funcs % | uncovered lines
|
|
187
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
188
|
+
ℹ all files | 90.02 | 79.43 | 78.73 |
|
|
189
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Scalability
|
|
195
|
+
|
|
196
|
+
In [benchmarks](https://github.com/paulhectork/aiiinotate-benchmark), aiiinotate response times are between 1/100th and 1/10th of a second with up to 10,000,000 annotations.
|
|
197
|
+
|
|
198
|
+

|
|
199
|
+
|
|
200
|
+
See [scalability.md](./docs/scalability.md) for more information.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
GNU GPL 3.0.
|
package/cli/export.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { Command, Option, Argument } from "commander";
|
|
4
|
+
|
|
5
|
+
import loadMongoClient from "#cli/utils/mongoClient.js";
|
|
6
|
+
import { dirOk, getCwd, writeCursorToJson } from "#cli/utils/io.js";
|
|
7
|
+
|
|
8
|
+
/** @typedef {import("#types").MongoDbType} MongoDbType */
|
|
9
|
+
/** @typedef {import("#types").MongoFindCursorType} MongoFindCursorType */
|
|
10
|
+
/** @typedef {import("fs").PathLike} PathLike */
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const exportableCollections = [ "all", "annotations2", "annotations3", "manifests2", "manifests3" ];
|
|
14
|
+
const actualCollections = exportableCollections.filter(c => c!=="all");
|
|
15
|
+
|
|
16
|
+
/** @type {(outDir: string) => string} */
|
|
17
|
+
const getOutDir = (outDir) => {
|
|
18
|
+
let success;
|
|
19
|
+
if (outDir) {
|
|
20
|
+
[ outDir, success ] = dirOk(outDir);
|
|
21
|
+
if (!success) {
|
|
22
|
+
console.error(`Error opening directory "${outDir}". Are you sure it exists ?`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
outDir = getCwd();
|
|
27
|
+
}
|
|
28
|
+
return outDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @type {() => (collectionName: string) => string|PathLike} */
|
|
32
|
+
const outFileNameFunc = () => {
|
|
33
|
+
// we currify this function so that, if a user exports all collections,
|
|
34
|
+
// output file nmaes share the same timestamp.
|
|
35
|
+
const timestamp = (new Date).toISOString().slice(0,-2);
|
|
36
|
+
return (outDir, collectionName) =>
|
|
37
|
+
path.join(outDir, `${timestamp}-aiiinotate-${collectionName}.json`);
|
|
38
|
+
}
|
|
39
|
+
const outFileName = outFileNameFunc();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* if `count`, returns a function to count all documents in a collection.
|
|
43
|
+
* else, returns a function to find all documents in a collection.
|
|
44
|
+
* @type {(db:MongoClient) => (count: boolean) => (collectionName: string) => MongoFindCursorType|Promise<number>}
|
|
45
|
+
*/
|
|
46
|
+
const exportCmdFunc = (db) =>
|
|
47
|
+
(count) =>
|
|
48
|
+
(collectionName) => {
|
|
49
|
+
const collection = db.collection(collectionName);
|
|
50
|
+
return count
|
|
51
|
+
? collection.countDocuments()
|
|
52
|
+
: collection.find().project({ _id: 0 })
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* export a single collection to a file
|
|
58
|
+
* @param {MongoClient} db
|
|
59
|
+
* @param {string} outDir
|
|
60
|
+
* @returns {(collctionName: string) => Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
const exportCollectionFunc = (db, outDir) => {
|
|
63
|
+
const exportCmd = exportCmdFunc(db)(false);
|
|
64
|
+
const countFunc = exportCmdFunc(db)(true);
|
|
65
|
+
|
|
66
|
+
return async (collectionName) => {
|
|
67
|
+
const out = outFileName(outDir, collectionName);
|
|
68
|
+
const collectionCursor = exportCmd(collectionName);
|
|
69
|
+
const totalCount = await countFunc(collectionName);
|
|
70
|
+
try {
|
|
71
|
+
await writeCursorToJson(out, collectionCursor, totalCount);
|
|
72
|
+
console.log(`Wrote export of collection "${collectionName}" (${totalCount} documents) to "${out}"\n`)
|
|
73
|
+
} catch(e) {
|
|
74
|
+
console.error(`Error writing export of collection "${collectionName}" to "${out}": ${e}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} collection
|
|
82
|
+
* @param {object} options
|
|
83
|
+
*/
|
|
84
|
+
async function action(collection, options) {
|
|
85
|
+
const { client, db } = loadMongoClient();
|
|
86
|
+
const outDir = getOutDir(options.output);
|
|
87
|
+
const collectionNameArray =collection === "all" ? actualCollections : [ collection ];
|
|
88
|
+
const exportCollection = exportCollectionFunc(db, outDir);
|
|
89
|
+
|
|
90
|
+
console.log(`\nExporting collection(s): ${collectionNameArray}\n`)
|
|
91
|
+
|
|
92
|
+
// fetch and write to file
|
|
93
|
+
for (const collectionName of collectionNameArray) {
|
|
94
|
+
await exportCollection(collectionName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\nFinished exporting collection(s): ${collectionNameArray} to "${outDir}".`)
|
|
98
|
+
await client.close();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** define the cli */
|
|
103
|
+
function makeExportCommand() {
|
|
104
|
+
|
|
105
|
+
const collectionArg =
|
|
106
|
+
new Argument("collection", `collection to export import: one of ${exportableCollections}`)
|
|
107
|
+
.choices(exportableCollections);
|
|
108
|
+
|
|
109
|
+
const outOpt = new Option("-o, --output <directory>", "Output directory (defaults to current working directory");
|
|
110
|
+
|
|
111
|
+
return new Command("export")
|
|
112
|
+
.description("export aiiinotate data")
|
|
113
|
+
.addArgument(collectionArg)
|
|
114
|
+
.addOption(outOpt)
|
|
115
|
+
.action(async (collection, options, command) => await action(collection, options))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default makeExportCommand;
|
package/cli/import.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Command, Option, Argument } from "commander";
|
|
2
|
+
|
|
3
|
+
import { fileRead, parseImportInputFile } from "#cli/utils/io.js";
|
|
4
|
+
import FastifyClient from "#cli/utils/fastifyClient.js";
|
|
5
|
+
import ProgressBar from "#cli/utils/progressbar.js";
|
|
6
|
+
import logger from "#utils/logger.js";
|
|
7
|
+
import { inspectObj } from "#utils/utils.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
|
|
10
|
+
|
|
11
|
+
////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
const notImplementedExit = (method) => {
|
|
14
|
+
logger.error(`\n\nERROR: import is not implemented '${method}'`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parseNumber = (x) => Number(x);
|
|
19
|
+
|
|
20
|
+
////////////////////////////////////////
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {FastifyClient} fastifyClient
|
|
24
|
+
* @param {string[]} fileArr - array of full paths to annotationLists to insert.
|
|
25
|
+
*/
|
|
26
|
+
async function importData(importFunc, datatype, fileArr) {
|
|
27
|
+
|
|
28
|
+
let totalImports = 0
|
|
29
|
+
const
|
|
30
|
+
pb = new ProgressBar({ desc: `importing ${datatype}s`, total: fileArr.length }),
|
|
31
|
+
importErrors = [];
|
|
32
|
+
|
|
33
|
+
for (let [ i, file ] of fileArr.entries()) {
|
|
34
|
+
i += 1
|
|
35
|
+
const
|
|
36
|
+
data = JSON.parse(fileRead(file)),
|
|
37
|
+
[ statusCode, resultPromise ] = await importFunc(data),
|
|
38
|
+
result = await resultPromise;
|
|
39
|
+
if (statusCode <= 299) {
|
|
40
|
+
totalImports += result.insertedCount;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(result);
|
|
43
|
+
importErrors.push(file);
|
|
44
|
+
}
|
|
45
|
+
pb.update(i)
|
|
46
|
+
}
|
|
47
|
+
return [ totalImports, importErrors ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
////////////////////////////////////////
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* run the cli
|
|
54
|
+
* @param {import('commander').Command} command
|
|
55
|
+
* @param {"manifest"|"annotation"} datatype
|
|
56
|
+
* @param {object} options
|
|
57
|
+
*/
|
|
58
|
+
async function action(command, datatype, options) {
|
|
59
|
+
/** @type {2 | 3} */
|
|
60
|
+
const iiifVersion = options.iiifVersion;
|
|
61
|
+
/** @type {string[]} */
|
|
62
|
+
const inputFile = options.file;
|
|
63
|
+
|
|
64
|
+
if (iiifVersion===3) {
|
|
65
|
+
notImplementedExit("CLI imports for IIIF presentation V3 is not implemented");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fastifyClient = new FastifyClient();
|
|
69
|
+
await fastifyClient.build();
|
|
70
|
+
const importFunc = fastifyClient.importData(iiifVersion, datatype);
|
|
71
|
+
|
|
72
|
+
// run
|
|
73
|
+
const fileArr = await parseImportInputFile(inputFile);
|
|
74
|
+
const [ totalImports, importErrors ] = await importData(importFunc, datatype, fileArr);
|
|
75
|
+
|
|
76
|
+
if (importErrors.length) {
|
|
77
|
+
logger.info(`There were problems importing ${datatype}s from the following ${importErrors.length} files: ${inspectObj(importErrors, -1)}`)
|
|
78
|
+
}
|
|
79
|
+
logger.info(`Imported ${totalImports} ${datatype}s into aiiinotate !`);
|
|
80
|
+
await fastifyClient.stop(); // TODO app keeps idling after that ....
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/////////////////////////////////////////
|
|
85
|
+
|
|
86
|
+
/** define the cli */
|
|
87
|
+
function makeImportCommand() {
|
|
88
|
+
|
|
89
|
+
const datatypeArg =
|
|
90
|
+
new Argument("datatype", "type of data to import: manifests or annotations")
|
|
91
|
+
.choices([ "manifest", "annotation" ]);
|
|
92
|
+
|
|
93
|
+
const versionOpt =
|
|
94
|
+
new Option("-i, --iiif-version <version>", "IIIF version")
|
|
95
|
+
.choices([ "2", "3" ])
|
|
96
|
+
.argParser(parseNumber)
|
|
97
|
+
.makeOptionMandatory();
|
|
98
|
+
|
|
99
|
+
const filesOpt =
|
|
100
|
+
new Option("-f, --file <file>", "file containing paths to AnnotationLists, AnnotationPages or Manifests to process (1 line per path)")
|
|
101
|
+
.makeOptionMandatory();
|
|
102
|
+
|
|
103
|
+
return new Command("import")
|
|
104
|
+
.description("import data into aiiinotate")
|
|
105
|
+
.addArgument(datatypeArg)
|
|
106
|
+
.addOption(filesOpt)
|
|
107
|
+
.addOption(versionOpt)
|
|
108
|
+
.action((datatype, options, command) => action(command, datatype, options))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default makeImportCommand;
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* command line interface. run through the package.json.
|
|
5
|
+
* usage: npm run cli import -- [args] [opts]
|
|
6
|
+
*
|
|
7
|
+
* NOTE: node recommends only creating 1 mongoclient per app
|
|
8
|
+
* when possible for performance reasons => we create a global mongoClient
|
|
9
|
+
* here, and then pass it down to all the other scripts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command, Option } from "commander";
|
|
13
|
+
|
|
14
|
+
// import dotenvx from "dotenvx";
|
|
15
|
+
|
|
16
|
+
import makeImportCommand from "#cli/import.js";
|
|
17
|
+
import makeExportCommand from "#cli/export.js";
|
|
18
|
+
import makeMigrateCommand from "#cli/migrate.js";
|
|
19
|
+
import makeServeCommand from "#cli/serve.js";
|
|
20
|
+
import makeXywhToIntCommand from "#cli/xywhToInt.js";
|
|
21
|
+
|
|
22
|
+
function makeCli() {
|
|
23
|
+
|
|
24
|
+
const desc =
|
|
25
|
+
"Command line interface for aiiinotate.\n\n"
|
|
26
|
+
+ `All commands are accessible through this CLI: starting the app,
|
|
27
|
+
managing and running migrations, importing and exporting data.
|
|
28
|
+
Run individual commands to see command-specific help.
|
|
29
|
+
`.replace(/^\s+/gm, "");
|
|
30
|
+
|
|
31
|
+
// NOTE: before running, it is necessary to load env variables.
|
|
32
|
+
const cli = new Command();
|
|
33
|
+
cli
|
|
34
|
+
.name("aiiinotate")
|
|
35
|
+
.description(desc)
|
|
36
|
+
.addCommand(makeServeCommand())
|
|
37
|
+
.addCommand(makeImportCommand())
|
|
38
|
+
.addCommand(makeExportCommand())
|
|
39
|
+
.addCommand(makeMigrateCommand())
|
|
40
|
+
.addCommand(makeXywhToIntCommand());
|
|
41
|
+
|
|
42
|
+
cli.parse(process.argv);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
makeCli();
|
package/cli/migrate.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/** @typedef {"make"|"apply"|"revert"|"revert-all"} MigrateOpType */
|
|
19
|
+
const allowedMigrateOp = [ "make", "apply", "revert", "revert-all" ];
|
|
20
|
+
|
|
21
|
+
const
|
|
22
|
+
// path to current dirctory
|
|
23
|
+
dirCli = path.dirname(fileURLToPath(import.meta.url)),
|
|
24
|
+
dirRoot = path.resolve(dirCli, ".."),
|
|
25
|
+
dirMigrations = path.resolve(dirRoot, "migrations"),
|
|
26
|
+
dirMigrationsScripts = path.resolve(dirMigrations, "migrationScripts"),
|
|
27
|
+
migrationsConfigMain = path.resolve(dirMigrations, "migrate-mongo-config-main.js"),
|
|
28
|
+
migrationsConfigTest = path.resolve(dirMigrations, "migrate-mongo-config-test.js"),
|
|
29
|
+
migrationConfigs = [ migrationsConfigMain, migrationsConfigTest ];
|
|
30
|
+
|
|
31
|
+
/** return a date in YYYYMMDDhhmmss format */
|
|
32
|
+
function formatDate(date) {
|
|
33
|
+
function pad2(n) { // always returns a string
|
|
34
|
+
return (n < 10 ? "0" : "") + n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return date.getFullYear() +
|
|
38
|
+
pad2(date.getMonth() + 1) +
|
|
39
|
+
pad2(date.getDate()) +
|
|
40
|
+
pad2(date.getHours()) +
|
|
41
|
+
pad2(date.getMinutes()) +
|
|
42
|
+
pad2(date.getSeconds());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* create a single migration file
|
|
47
|
+
* @param {string} migrationName
|
|
48
|
+
*/
|
|
49
|
+
function migrateMake(migrationName) {
|
|
50
|
+
if (migrationName == null) {
|
|
51
|
+
throw new Error(`migration name must be a string. got ${migrationName}`);
|
|
52
|
+
}
|
|
53
|
+
fs.copyFileSync(
|
|
54
|
+
path.resolve(dirMigrations, "migrationTemplate.js"),
|
|
55
|
+
path.resolve(dirMigrationsScripts, `${formatDate(new Date())}-${migrationName}.js`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** apply all pending migrations */
|
|
60
|
+
function migrateApply() {
|
|
61
|
+
// NOTE: if we don't use a specific version of migrate-mongo, we get docker errors
|
|
62
|
+
migrationConfigs.map((mc) => execSync(`npx -- migrate-mongo@^12.1.3 up -f ${mc}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** revert the last migration */
|
|
66
|
+
function migrateRevert() {
|
|
67
|
+
migrationConfigs.map((mc) => execSync(`npx migrate-mongo@^12.1.3 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@^12.1.3 down -f ${mc}`)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* run the cli
|
|
84
|
+
* @param {import('commander').Command} command
|
|
85
|
+
* @param {MigrateOpType} mongoClient
|
|
86
|
+
* @param {object} options
|
|
87
|
+
*/
|
|
88
|
+
async function action(command, migrationOp, options) {
|
|
89
|
+
const { migrationName } = options;
|
|
90
|
+
|
|
91
|
+
switch (migrationOp) {
|
|
92
|
+
case ("make"):
|
|
93
|
+
migrateMake(migrationName);
|
|
94
|
+
break;
|
|
95
|
+
case ("apply"):
|
|
96
|
+
migrateApply();
|
|
97
|
+
break;
|
|
98
|
+
case ("revert"):
|
|
99
|
+
migrateRevert();
|
|
100
|
+
break;
|
|
101
|
+
case ("revert-all"):
|
|
102
|
+
migrateRevertAll();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function makeMigrateCommand() {
|
|
108
|
+
const migrationOpArg =
|
|
109
|
+
new Argument("<migration-op>", "name of migration operation")
|
|
110
|
+
.choices(allowedMigrateOp);
|
|
111
|
+
|
|
112
|
+
const migrationNameOpt =
|
|
113
|
+
new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)");
|
|
114
|
+
|
|
115
|
+
const command = new Command("migrate");
|
|
116
|
+
return command
|
|
117
|
+
.description("run database migrations")
|
|
118
|
+
.addArgument(migrationOpArg)
|
|
119
|
+
.addOption(migrationNameOpt)
|
|
120
|
+
.action((migrationOp, options, command) => {
|
|
121
|
+
if (
|
|
122
|
+
migrationOp == "make"
|
|
123
|
+
&& (options.migrationName === undefined || options.migrationName === "")
|
|
124
|
+
) {
|
|
125
|
+
command.error("migration operation \"apply\" requires option \"-n, --migration-name <name>\"", { exitCode: 1 });
|
|
126
|
+
}
|
|
127
|
+
action(command, migrationOp, options)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default makeMigrateCommand;
|
package/cli/serve.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import serve from "#src/server.js";
|
|
2
|
+
|
|
3
|
+
import { Command, Option, Argument } from "commander";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("#types").ServeModeType} ServeModeType */
|
|
6
|
+
|
|
7
|
+
const serveModeValues = [ "dev", "prod" ];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('commander').Command} command
|
|
11
|
+
* @param {ServeModeType} serveMode
|
|
12
|
+
*/
|
|
13
|
+
async function action(command, serveMode) {
|
|
14
|
+
await serve(serveMode);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeServeCommand() {
|
|
18
|
+
|
|
19
|
+
const serveModeArg =
|
|
20
|
+
new Argument("<run-mode>", "mode with which to run the app")
|
|
21
|
+
.choices(serveModeValues);
|
|
22
|
+
|
|
23
|
+
return new Command("serve")
|
|
24
|
+
.description("run Aiiinotate. <run-mode>")
|
|
25
|
+
.addArgument(serveModeArg)
|
|
26
|
+
.action((serveMode, command) => action(command, serveMode));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default makeServeCommand;
|