aiiinotate 0.2.9 → 0.2.11

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/cli/migrate.js CHANGED
@@ -61,6 +61,7 @@ function migrateMake(migrationName) {
61
61
 
62
62
  /** apply all pending migrations */
63
63
  function migrateApply() {
64
+ // NOTE: if we don't use a specific version of migrate-mongo, we get docker errors
64
65
  migrationConfigs.map((mc) => execSync(`npx -- migrate-mongo@^12.1.3 up -f ${mc}`));
65
66
  }
66
67
 
@@ -6,9 +6,7 @@ import { MongoClient } from "mongodb";
6
6
  */
7
7
  function loadMongoClient() {
8
8
  try {
9
- const client = new MongoClient(
10
- process.env.MONGODB_CONNSTRING,
11
- );
9
+ const client = new MongoClient(process.env.MONGODB_CONNSTRING);
12
10
  client.db(process.env.MONGODB_DB);
13
11
  return client;
14
12
  } catch (err) {
package/docker/Dockerfile CHANGED
@@ -1,8 +1,8 @@
1
1
  # syntax=docker/dockerfile:1
2
- FROM node:23.11 AS aiiinotate
2
+ FROM node:23.11
3
3
 
4
4
  # aiiinotate port
5
- ARG PORT=4444
5
+ ARG PORT
6
6
  ENV PORT=${PORT}
7
7
 
8
8
  # set up environment
@@ -11,15 +11,17 @@ SHELL ["/bin/bash", "-c"]
11
11
 
12
12
  # root of the app in the docker container
13
13
  WORKDIR /aiiinotate
14
- # copy the .env in the docker container
15
- COPY ./config/.env /aiiinotate/.env
16
- # install the app as an NPM library
14
+ # copy the docker .env in the docker container
15
+ COPY ./docker/.env /aiiinotate/.env
16
+ # for debug: install `iproute2` which provides CLI `ss` to monitor active ports
17
+ RUN apt update && apt install iproute2 -y
18
+ # install the app as an NPM library. we do a global install as it saves us from issues with `$PATH`.
17
19
  RUN npm i -g aiiinotate
18
20
 
19
- RUN npm i -g migrate-mongo@^12.1.3
20
- # migrate the database
21
- RUN aiiinotate --env=/aiiinotate/.env -- migrate apply
22
21
  # expose the used port
23
22
  EXPOSE $PORT
24
- # start the app
25
- CMD aiiinotate --env=/aiiinotate/.env -- serve prod
23
+
24
+ # run the migrations and start the app.
25
+ # NOTE migrations must be done in CMD: they need the mongo service to be running.
26
+ CMD aiiinotate --env=/aiiinotate/.env -- migrate apply && \
27
+ aiiinotate --env=/aiiinotate/.env -- serve prod;
@@ -0,0 +1,62 @@
1
+ # Docker usage
2
+
3
+ ---
4
+
5
+ ## Usage
6
+
7
+ Note that all `docker` commands must be run from the `docker/` directory !
8
+
9
+ 1. Clone the repo and `cd` in it
10
+
11
+ ```bash
12
+ git clone git@github.com:Aikon-platform/aiiinotate.git
13
+ cd aiiinotate
14
+ ```
15
+
16
+ 2. Create a `.env` file at `config/.env` (from the root of the project)
17
+
18
+ ```bash
19
+ cp ./config/.env.template ./config/.env
20
+ vim ./config/.env # do your edits
21
+ ```
22
+
23
+ 3. Build the containers
24
+
25
+ ```bash
26
+ cd docker
27
+ bash docker.sh build
28
+ ```
29
+
30
+ 4. Start the containers
31
+
32
+ ```bash
33
+ bash docker.sh start
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Troubleshooting and useful commands
39
+
40
+ Access `mongosh` within the Mongo container:
41
+
42
+ ```bash
43
+ sudo docker exec -it docker-mongo-1 mongosh
44
+ ```
45
+
46
+ Check running ports in the Web container:
47
+
48
+ ```bash
49
+ sudo docker exec -it docker-web-1 ss -tnl
50
+ ```
51
+
52
+ View globally installed NPM packages in the Web container:
53
+
54
+ ```bash
55
+ sudo docker exec -it docker-web-1 npm list -g --depth=0
56
+ ```
57
+
58
+ CURL the Web container
59
+
60
+ ```bash
61
+ sudo docker exec -it docker-web-1 curl http://0.0.0.0:4444 # change 4444 by your $APP_PORT
62
+ ```
@@ -1,8 +1,16 @@
1
+ x-proxy-settings: &proxy-settings
2
+ HTTP_PROXY: ${HTTP_PROXY:-}
3
+ HTTPS_PROXY: ${HTTPS_PROXY:-}
4
+ NO_PROXY: "localhost,127.0.0.1,mongo,web,.aiiinotate_network"
5
+
1
6
  services:
2
7
  mongo:
3
8
  image: mongo:8
4
9
  restart: always
5
- # environment:
10
+ networks:
11
+ - aiiinotate_network
12
+ environment:
13
+ <<: *proxy-settings
6
14
  # MONGO_INITDB_ROOT_USERNAME: root
7
15
  # MONGO_INITDB_ROOT_PASSWORD: example
8
16
 
@@ -15,7 +23,15 @@ services:
15
23
  env_file:
16
24
  - ../config/.env
17
25
  ports:
18
- - ${APP_PORT}:${APP_PORT}
26
+ - "${APP_PORT}:${APP_PORT}"
19
27
  depends_on:
20
28
  - mongo
29
+ networks:
30
+ - aiiinotate_network
31
+ environment:
32
+ <<: *proxy-settings
21
33
  restart: always
34
+
35
+ networks:
36
+ aiiinotate_network:
37
+ driver: bridge
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e;
4
+
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
6
+
7
+ # basic / dev config
8
+ ENV_CONFIG="$SCRIPT_DIR/../config/.env"
9
+ # docker specific config, created by `env_to_docker`
10
+ ENV_DOCKER="$SCRIPT_DIR/.env"
11
+
12
+ get_os() {
13
+ unameOut="$(uname -s)"
14
+ case "${unameOut}" in
15
+ Linux*) os=Linux;;
16
+ Darwin*) os=Mac;;
17
+ CYGWIN*) os=Cygwin;;
18
+ MINGW*) os=MinGw;;
19
+ MSYS_NT*) os=Git;;
20
+ *) os="UNKNOWN:${unameOut}"
21
+ esac
22
+ echo "${os}"
23
+ }
24
+ OS=$(get_os)
25
+
26
+ # mac+linux compatible file replacements
27
+ sed_repl () {
28
+ sed_expr="$1"
29
+ file="$2"
30
+ if [ "$OS" = "Linux" ];
31
+ then sed -i -e "$sed_expr" "$file";
32
+ else sed -i "" -e "$sed_expr" "$file";
33
+ fi
34
+ }
35
+
36
+ # copy config/.env file to docker/.env and adapt the file to work with docker.
37
+ env_to_docker() {
38
+ # check the env file is found
39
+ if [ ! -f "$ENV_CONFIG" ];
40
+ then
41
+ echo ".env file not found at at '$ENV_CONFIG'. exiting...";
42
+ return 1; # will exit when used with `set -e`
43
+ fi;
44
+
45
+ # NOTE that the MongoDB host in Docker MUST BE the name of the Mongo docker service (defined in docker-compose)
46
+ cp "$ENV_CONFIG" "$ENV_DOCKER";
47
+ sed_repl s~^MONGODB_HOST=.*$~MONGODB_HOST=mongo~ "$ENV_DOCKER";
48
+ sed_repl s~^APP_HOST=.*~APP_HOST=0.0.0.0~ "$ENV_DOCKER";
49
+ }
50
+ env_to_docker;
51
+
52
+ build_containers () {
53
+ sudo docker compose --env-file "$ENV_DOCKER" build --no-cache;
54
+ }
55
+
56
+ start_containers() {
57
+ sudo docker compose --env-file "$ENV_DOCKER" up --force-recreate;
58
+ }
59
+
60
+ stop_containers() {
61
+ sudo docker compose --env-file "$ENV_DOCKER" down;
62
+ }
63
+
64
+ case "$1" in
65
+ start)
66
+ start_containers
67
+ ;;
68
+ stop)
69
+ stop_containers
70
+ ;;
71
+ build)
72
+ stop_containers
73
+ build_containers
74
+ start_containers
75
+ ;;
76
+ *)
77
+ echo "Usage: $0 {build|start|stop}"
78
+ exit 1
79
+ ;;
80
+ esac;
81
+
@@ -0,0 +1,143 @@
1
+ # Notes, quirks and troubleshooting
2
+
3
+ ---
4
+
5
+ ## Notes
6
+
7
+ ### Uniqueness
8
+
9
+ As of writing (09.10.25), there are no uniqueness constraints on annotations. There is only a uniqueness constraint on collection `manifest2` on field `manifest2.@id` (the ID of a manifest).
10
+
11
+ Ideally, we would want to avoid having duplicate annotations in the database. This is more complicated in practice: at least for `annotions2`, an annotation's `@id` field is re-generated and a random part (an UUID) is generated at insert time. This means that, when trying to store the same annotation (with the same `@id`), the `@id` is changed, and so a different value is inserted.
12
+
13
+ This means that we can't have a uniqueness constraint on `@id` or `id` fields of annotations. Another option would be to have a uniqueness constraint on annotation targets (no 2 annotations can annotate the same region), but this behaviour seems brittle in practice, so it's not yet implemented.
14
+
15
+ ### Concurrency
16
+
17
+ For clients, concurrency/parrallelization (i.e., with JS `Promise.all()`) on insert/update should be avoided because it can cause a data race: several processes can attempt to write the same thing in parrallel.
18
+
19
+ For example, when inserting annotations, the manifests related to each annotation are inserted in parrallel. Since this is a side effect, 2 processes may unknowingly try to insert the same manifest in the database, which causes a uniqueness constraint to fail. This error can be hard to debug, so it's best to avoid concurrency at write time.
20
+
21
+ ---
22
+
23
+ ## Dev quirks
24
+
25
+ Sometimes, node/fastify can be weird. When scratching your head at dev errors, look here:)
26
+
27
+ ### Error swallowing at app build
28
+
29
+ Errors that happen when a plugin is being registered will cause the app's startup to fail, without necessarily throwing an error.
30
+
31
+ This is especially true for test cases that build the fastify app:
32
+
33
+ ```
34
+ test
35
+ => build fastify app
36
+ => build step fails silently
37
+ => test fails
38
+ ```
39
+
40
+ #### Troubleshooting
41
+
42
+ - normal behaviour: when a runtime error happens, the failing test will log the error on the console
43
+ - what this error looks like:
44
+ - tests fail **very quickly**,
45
+ - without throwing an error, seemingly without even running the test suite
46
+ - the proces doesn't exit, although all tests have failed
47
+ - when `npm run test` fails like this, run `npm run start`. See if normal startup throws an error
48
+ - NOTE: normal startup not throwing does NOT mean that the build step necessarily worked
49
+
50
+ #### Possible help/solutions
51
+
52
+ - find a way to stisfyingly use `try...catch` at plugin registration.
53
+ - look into these issues: [2694](https://github.com/fastify/fastify/issues/2694)
54
+ - in particular, it may be an issue specific to async plugins ?
55
+
56
+ ### Route response schema definition
57
+
58
+ For some reason, route schema definition is much less flexible for responses than for queries.
59
+
60
+ #### The problem
61
+
62
+ **Query schemas**: In queries (`schema.params` or `schema.querystring`), fastify is very permissive. You can use unresolved schemas (with `$ref`), save entire schemas as JsonSchemas ...
63
+
64
+ **Response schemas**: Response schemas have more constraints.
65
+ - **response schemas are defined at route level** and cannot be stored as full JsonSchemas:
66
+ ```js
67
+ // this will be an invalid response schema: it is trying to store a complete response schema as a JsonSchema
68
+ fastify.addSchema({
69
+ $id: makeSchemaUri("routeResponsePost"),
70
+ properties: {
71
+ 200: { ... },
72
+ 500: { ... },
73
+ }
74
+ })
75
+
76
+ // this is a fixed version: an object containing schemas, not a full JsonSchema, to be used inside a Route definition
77
+ schema: {
78
+ response: {
79
+ 200: { ... },
80
+ 500: { ... }
81
+ }
82
+ }
83
+ // if you want to reuse a schema, store it as a JS object and import it.
84
+ ```
85
+ - **unresolved response schemas (`$ref`) are forbidden**. You cannot use `$ref`. In the app, use `fastify.schemasResolver` to resolve the schema to a plain `JsonSchema` without `$ref`.
86
+
87
+ #### The fix
88
+
89
+ In short, here's **how to use shared schemas in responses**:
90
+ 1. Define payload schemas for different response cases:
91
+ ```js
92
+ // in case of a POST success
93
+ fastify.addSchema({
94
+ $id: makeSchemaUri("routeResponseInsert"),
95
+ type: "object",
96
+ // ...properties
97
+ });
98
+
99
+ // in case of a POST error
100
+ fastify.addSchema({
101
+ $id: makeSchemaUri("routeResponseError"),
102
+ type: "object",
103
+ // ...properties
104
+ });
105
+ ```
106
+ 2. Resolve schemas and use in responses
107
+ ```js
108
+ const routeResponseInsert = fastify.getSchema("...");
109
+ const routeResponseError = fastify.getSchema("...");
110
+ fastify.post(
111
+ "/annotations/:iiifPresentationVersion/create",
112
+ {
113
+ schema: {
114
+ body: routeAnnotations2Or3Schema,
115
+ response: {
116
+ 200: routeResponseInsert,
117
+ 500: routeResponseError
118
+ }
119
+ }
120
+ },
121
+ async (req, rep) => {}
122
+ )
123
+ ```
124
+
125
+ ### `migrate revert-all && migrate apply`
126
+
127
+ #### The problem
128
+
129
+ Sometimes, you want to remove all migrations and then rerun them. This will cause an error like:
130
+
131
+ ```bash
132
+ {
133
+ ...,
134
+ errmsg: 'namespace aiiinotate.manifests2 already exists, but with different options: { uuid: UUID("65ca87c4-ddce-4c2d-a6f9-08ff6b70c0a7"), validationLevel: "strict", validationAction: "error" }',
135
+ code: 48,
136
+ codeName: 'NamespaceExists',
137
+ ...
138
+ }
139
+ ```
140
+
141
+ #### The fix
142
+
143
+ I figure it's an error due to cache or something like that. Either wait or manually delete the collection or the entire database using `mongosh`.
package/docs/progress.md CHANGED
@@ -36,124 +36,3 @@ Routes are only implemented with IIIF Presentation API 2.x, not with the 3.0 ver
36
36
  - fetching and inserting manifests related to an annotation when using inserting annotations.
37
37
 
38
38
  ---
39
-
40
- ## Notes
41
-
42
- ### Uniqueness
43
-
44
- As of writing (09.10.25), there are no uniqueness constraints on annotations. There is only a uniqueness constraint on collection `manifest2` on field `manifest2.@id` (the ID of a manifest).
45
-
46
- Ideally, we would want to avoid having duplicate annotations in the database. This is more complicated in practice: at least for `annotions2`, an annotation's `@id` field is re-generated and a random part (an UUID) is generated at insert time. This means that, when trying to store the same annotation (with the same `@id`), the `@id` is changed, and so a different value is inserted.
47
-
48
- This means that we can't have a uniqueness constraint on `@id` or `id` fields of annotations. Another option would be to have a uniqueness constraint on annotation targets (no 2 annotations can annotate the same region), but this behaviour seems brittle in practice, so it's not yet implemented.
49
-
50
- ### Concurrency
51
-
52
- For clients, concurrency/parrallelization (i.e., with JS `Promise.all()`) on insert/update should be avoided because it can cause a data race: several processes can attempt to write the same thing in parrallel.
53
-
54
- For example, when inserting annotations, the manifests related to each annotation are inserted in parrallel. Since this is a side effect, 2 processes may unknowingly try to insert the same manifest in the database, which causes a uniqueness constraint to fail. This error can be hard to debug, so it's best to avoid concurrency at write time.
55
-
56
- ---
57
-
58
- ## Dev quirks
59
-
60
- Sometimes, node/fastify can be weird. When scratching your head at dev errors, look here:)
61
-
62
- ### Error swallowing at app build
63
-
64
- Errors that happen when a plugin is being registered will cause the app's startup to fail, without necessarily throwing an error.
65
-
66
- This is especially true for test cases that build the fastify app:
67
-
68
- ```
69
- test
70
- => build fastify app
71
- => build step fails silently
72
- => test fails
73
- ```
74
-
75
- #### Troubleshooting
76
-
77
- - normal behaviour: when a runtime error happens, the failing test will log the error on the console
78
- - what this error looks like:
79
- - tests fail **very quickly**,
80
- - without throwing an error, seemingly without even running the test suite
81
- - the proces doesn't exit, although all tests have failed
82
- - when `npm run test` fails like this, run `npm run start`. See if normal startup throws an error
83
- - NOTE: normal startup not throwing does NOT mean that the build step necessarily worked
84
-
85
- #### Possible help/solutions
86
-
87
- - find a way to stisfyingly use `try...catch` at plugin registration.
88
- - look into these issues: [2694](https://github.com/fastify/fastify/issues/2694)
89
- - in particular, it may be an issue specific to async plugins ?
90
-
91
- ### Route response schema definition
92
-
93
- For some reason, route schema definition is much less flexible for responses than for queries.
94
-
95
- #### The problem
96
-
97
- **Query schemas**: In queries (`schema.params` or `schema.querystring`), fastify is very permissive. You can use unresolved schemas (with `$ref`), save entire schemas as JsonSchemas ...
98
-
99
- **Response schemas**: Response schemas have more constraints.
100
- - **response schemas are defined at route level** and cannot be stored as full JsonSchemas:
101
- ```js
102
- // this will be an invalid response schema: it is trying to store a complete response schema as a JsonSchema
103
- fastify.addSchema({
104
- $id: makeSchemaUri("routeResponsePost"),
105
- properties: {
106
- 200: { ... },
107
- 500: { ... },
108
- }
109
- })
110
-
111
- // this is a fixed version: an object containing schemas, not a full JsonSchema, to be used inside a Route definition
112
- schema: {
113
- response: {
114
- 200: { ... },
115
- 500: { ... }
116
- }
117
- }
118
- // if you want to reuse a schema, store it as a JS object and import it.
119
- ```
120
- - **unresolved response schemas (`$ref`) are forbidden**. You cannot use `$ref`. In the app, use `fastify.schemasResolver` to resolve the schema to a plain `JsonSchema` without `$ref`.
121
-
122
- #### The fix
123
-
124
- In short, here's **how to use shared schemas in responses**:
125
- 1. Define payload schemas for different response cases:
126
- ```js
127
- // in case of a POST success
128
- fastify.addSchema({
129
- $id: makeSchemaUri("routeResponseInsert"),
130
- type: "object",
131
- // ...properties
132
- });
133
-
134
- // in case of a POST error
135
- fastify.addSchema({
136
- $id: makeSchemaUri("routeResponseError"),
137
- type: "object",
138
- // ...properties
139
- });
140
- ```
141
- 2.
142
- 2. Resolve schemas and use in responses
143
- ```js
144
- const routeResponseInsert = fastify.getSchema("...");
145
- const routeResponseError = fastify.getSchema("...");
146
- fastify.post(
147
- "/annotations/:iiifPresentationVersion/create",
148
- {
149
- schema: {
150
- body: routeAnnotations2Or3Schema,
151
- response: {
152
- 200: routeResponseInsert,
153
- 500: routeResponseError
154
- }
155
- }
156
- },
157
- async (req, rep) => {}
158
- )
159
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiiinotate",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "a fast IIIF-compliant annotation server",
5
5
  "main": "./cli/index.js",
6
6
  "type": "module",
@@ -12,9 +12,9 @@
12
12
  },
13
13
  "scripts": {
14
14
  "cli": "node ./cli/index.js --env=./config/.env",
15
- "setup": "npm run cli migrate apply",
16
- "start": "npm run cli serve dev",
17
- "test": "dotenvx run -f ./config/.env -- node --test --test-isolation=none",
15
+ "setup": "sudo systemctl start mongod && npm run cli migrate apply",
16
+ "start": "sudo systemctl start mongod && npm run cli serve dev",
17
+ "test": "sudo systemctl start mongod && dotenvx run -f ./config/.env -- node --test --test-isolation=none",
18
18
  "lint": "npx eslint --fix",
19
19
  "migrate": "npm run cli -- migrate"
20
20
  },
@@ -41,7 +41,7 @@ test("test annotation Routes", async (t) => {
41
41
 
42
42
  // NOTE: it is necessary to run the app because internally there are fetches to external data.
43
43
  try {
44
- await fastify.listen({ port: process.env.APP_PORT });
44
+ await fastify.listen({ port: process.env.APP_PORT, host: process.env.APP_HOST });
45
45
  } catch (err) {
46
46
  console.log("FASTIFY ERROR", err);
47
47
  throw err;
@@ -27,7 +27,7 @@ test("test Manifests2 module", async (t) => {
27
27
 
28
28
  // NOTE: it is necessary to run the app because internally there are fetches to external data.
29
29
  try {
30
- await fastify.listen({ port: process.env.APP_PORT });
30
+ await fastify.listen({ port: process.env.APP_PORT, host: process.env.APP_HOST });
31
31
  } catch (err) {
32
32
  console.log("FASTIFY ERROR", err);
33
33
  throw err;
@@ -30,7 +30,7 @@ test("test manifests Routes", async (t) => {
30
30
 
31
31
  // NOTE: it is necessary to run the app because internally there are fetches to external data.
32
32
  try {
33
- await fastify.listen({ port: process.env.APP_PORT });
33
+ await fastify.listen({ port: process.env.APP_PORT, host: process.env.APP_HOST });
34
34
  } catch (err) {
35
35
  console.log("FASTIFY ERROR", err);
36
36
  throw err;
@@ -26,7 +26,7 @@ test("test common routes", async (t) => {
26
26
 
27
27
  // NOTE: it is necessary to run the app because internally there are fetches to external data.
28
28
  try {
29
- await fastify.listen({ port: process.env.APP_PORT });
29
+ await fastify.listen({ port: process.env.APP_PORT, host: process.env.APP_HOST });
30
30
  } catch (err) {
31
31
  console.log("FASTIFY ERROR", err);
32
32
  throw err;
package/src/server.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import build from "#src/app.js";
6
+ import { visibleLog } from "#utils/utils.js";
6
7
 
7
8
  /**
8
9
  * @param {import("#types").RerveModeType} serveMode
@@ -14,8 +15,10 @@ async function server (serveMode) {
14
15
 
15
16
  const fastify = await build(serveMode);
16
17
 
18
+ visibleLog([process.env.APP_HOST, process.env.APP_PORT]);
19
+
17
20
  try {
18
- fastify.listen({ port: process.env.APP_PORT });
21
+ fastify.listen({ port: process.env.APP_PORT, host: process.env.APP_HOST });
19
22
  } catch(err) {
20
23
  fastify.log.error(err);
21
24
  process.exit(1);