@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.
Files changed (104) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +206 -0
  3. package/cli/export.js +118 -0
  4. package/cli/import.js +111 -0
  5. package/cli/index.js +45 -0
  6. package/cli/migrate.js +131 -0
  7. package/cli/serve.js +29 -0
  8. package/cli/utils/fastifyClient.js +86 -0
  9. package/cli/utils/io.js +225 -0
  10. package/cli/utils/mongoClient.js +21 -0
  11. package/cli/utils/progressbar.js +86 -0
  12. package/cli/xywhToInt.js +99 -0
  13. package/config/.env.template +48 -0
  14. package/docker/Dockerfile +39 -0
  15. package/docker/README.md +5 -0
  16. package/docker/docker-compose.yaml +49 -0
  17. package/docker/docker.sh +43 -0
  18. package/docker/docker_aiiinotate_import.sh +62 -0
  19. package/docs/api.md +381 -0
  20. package/docs/cli.md +132 -0
  21. package/docs/dev_documentation/dev_architecture.md +88 -0
  22. package/docs/dev_documentation/dev_db.md +58 -0
  23. package/docs/dev_documentation/dev_iiif_compatibility.md +43 -0
  24. package/docs/dev_documentation/dev_notes_quirks_and_troubleshooting.md +143 -0
  25. package/docs/docker.md +98 -0
  26. package/docs/includes/report_benchmark_aiiinotate_2026-05-28-02:50:48_7steps.png +0 -0
  27. package/docs/scalability.md +34 -0
  28. package/docs/specifications/0_w3c_open_annotations.md +332 -0
  29. package/docs/specifications/1_w3c_web_annotations.md +577 -0
  30. package/docs/specifications/2_iiif_apis.md +428 -0
  31. package/docs/specifications/3_iiif_annotations.md +103 -0
  32. package/docs/specifications/4_search_api.md +135 -0
  33. package/docs/specifications/5_sas.md +119 -0
  34. package/docs/specifications/6_mirador.md +119 -0
  35. package/docs/specifications/7_aikon.md +137 -0
  36. package/docs/specifications/include/presentation_2.0.webp +0 -0
  37. package/docs/specifications/include/presentation_2.0_white.png +0 -0
  38. package/docs/specifications/include/presentation_3.0.png +0 -0
  39. package/docs/specifications/include/presentation_3.0_resize.png +0 -0
  40. package/eslint.config.js +30 -0
  41. package/migrations/baseConfig.js +57 -0
  42. package/migrations/manageIndex.js +55 -0
  43. package/migrations/migrate-mongo-config-main.js +8 -0
  44. package/migrations/migrate-mongo-config-test.js +8 -0
  45. package/migrations/migrationScripts/20250825185706-collections.js +48 -0
  46. package/migrations/migrationScripts/20250826194832-annotations2-schema.js +42 -0
  47. package/migrations/migrationScripts/20250904080710-annotations2-indexes.js +69 -0
  48. package/migrations/migrationScripts/20251002141951-manifests2-schema.js +43 -0
  49. package/migrations/migrationScripts/20251006212110-manifests2-indexes.js +35 -0
  50. package/migrations/migrationTemplate.js +25 -0
  51. package/package.json +82 -0
  52. package/scripts/get_version.py +12 -0
  53. package/scripts/run.sh +36 -0
  54. package/scripts/setup_mongodb.sh +70 -0
  55. package/scripts/setup_node.sh +15 -0
  56. package/scripts/update_version.py +30 -0
  57. package/scripts/utils.sh +65 -0
  58. package/src/app.js +116 -0
  59. package/src/constants.js +73 -0
  60. package/src/data/annotations/annotations2.js +681 -0
  61. package/src/data/annotations/annotations3.js +28 -0
  62. package/src/data/annotations/routes.js +335 -0
  63. package/src/data/annotations/routes.test.js +271 -0
  64. package/src/data/collectionAbstract.js +283 -0
  65. package/src/data/index.js +29 -0
  66. package/src/data/manifests/manifests2.js +378 -0
  67. package/src/data/manifests/manifests2.test.js +53 -0
  68. package/src/data/manifests/manifests3.js +23 -0
  69. package/src/data/manifests/routes.js +122 -0
  70. package/src/data/manifests/routes.test.js +70 -0
  71. package/src/data/routes.js +181 -0
  72. package/src/data/routes.test.js +166 -0
  73. package/src/db/index.js +50 -0
  74. package/src/fixtures/annotations.js +41 -0
  75. package/src/fixtures/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
  76. package/src/fixtures/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
  77. package/src/fixtures/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
  78. package/src/fixtures/data/annotations2Invalid.jsonld +39 -0
  79. package/src/fixtures/data/annotations2SvgValid.jsonld +81 -0
  80. package/src/fixtures/data/annotations2Valid.jsonld +39 -0
  81. package/src/fixtures/data/bnf_invalid_manifest.json +2806 -0
  82. package/src/fixtures/data/bnf_valid_manifest.json +2817 -0
  83. package/src/fixtures/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
  84. package/src/fixtures/generate.js +181 -0
  85. package/src/fixtures/index.js +69 -0
  86. package/src/fixtures/manifests.js +14 -0
  87. package/src/fixtures/utils.js +37 -0
  88. package/src/schemas/index.js +100 -0
  89. package/src/schemas/schemasBase.js +19 -0
  90. package/src/schemas/schemasPresentation2.js +410 -0
  91. package/src/schemas/schemasPresentation3.js +33 -0
  92. package/src/schemas/schemasResolver.js +71 -0
  93. package/src/schemas/schemasRoutes.js +318 -0
  94. package/src/server.js +25 -0
  95. package/src/types.js +97 -0
  96. package/src/utils/iiif2Utils.js +332 -0
  97. package/src/utils/iiif2Utils.test.js +146 -0
  98. package/src/utils/iiif3Utils.js +0 -0
  99. package/src/utils/iiifUtils.js +18 -0
  100. package/src/utils/logger.js +119 -0
  101. package/src/utils/routeUtils.js +137 -0
  102. package/src/utils/svg.js +417 -0
  103. package/src/utils/testUtils.js +289 -0
  104. 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
+ ![benchmark results](./docs/includes/report_benchmark_aiiinotate_2026-05-28-02:50:48_7steps.png)
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;