@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
|
@@ -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/fixtures/ - 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.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# DATABASE AND MIGRATIONS
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Collections
|
|
6
|
+
|
|
7
|
+
- `annotations2`: a collection of annotations following the IIIF Presentation API 2.x (OpenAnnotation specification)
|
|
8
|
+
- `annotations3`: a collection of annotations following the IIIF Presentation API 3.x (WebAnnotation specification)
|
|
9
|
+
- `manifests2`: an index of manifests (manifest ID and array of canvas IDs of the manifest) following the IIIF Presentation API 2.x (OpenAnnotation specification)
|
|
10
|
+
- `manifests3`: an index of manifests (manifest ID and array of canvas IDs of the manifest) following the IIIF Presentation API 3.x (WebAnnotation specification)
|
|
11
|
+
|
|
12
|
+
Conversion between IIIF Presentation APIs 2 and 3 is not implemented.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Validation rules
|
|
17
|
+
|
|
18
|
+
Two levels of validation rules are implemented:
|
|
19
|
+
- route-level validation
|
|
20
|
+
- database-level validation
|
|
21
|
+
|
|
22
|
+
At route-level, we ensure that the data has the absolutely necessary data to work with annotations.
|
|
23
|
+
|
|
24
|
+
At database-level (see [`src/schema`](https://github.com/Aikon-platform/aiiinotate/blob/main/src/schemas/) module, we define a complete model (in JSONSchema) to describe annotations.
|
|
25
|
+
|
|
26
|
+
**TLDR**: when inserting annotations (i.e., from an AnnotationList)
|
|
27
|
+
1. at [route-level](https://github.com/Aikon-platform/aiiinotate/blob/main/src/data/annotations/routes.js), we ensure that the AnnotationList is generally valid (contains a `resource` with annotations, each annotations contains a `on`...)
|
|
28
|
+
2. in the [`Annotations2`](https://github.com/Aikon-platform/aiiinotate/blob/main/src/data/annotations/annotations2.js) module, we do some extra checks and extra minimal reformatting to ensure that our annotations all have the same structure (i.e., convert `annotation.on` values to an array of `SpecificResources`). After finishing the cleanup, we are certain that an annotation can be inserted
|
|
29
|
+
3. when inserting an annotation in the MongDB `annotations2`, MongoDB uses the `annotation` schema defined in [`src/schemas/schemasPresentation2`](https://github.com/Aikon-platform/aiiinotate/blob/main/src/schemas/schemasPresentation2.js) to validate annotations before inserting.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Migrations
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
*Note that here, migrations only concern changes to the structure, not changes to the data.*
|
|
38
|
+
|
|
39
|
+
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**:
|
|
40
|
+
- a `main` database (default database that will be put in production)
|
|
41
|
+
- a `test` database (empty database to run tests, add dummy data to and so on).
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Our solution is to automate the management of both databases in parrallel:
|
|
47
|
+
- there are **2 migration config files**, one for each database
|
|
48
|
+
- both migration config files point to **the same migration scripts folder**, so that both database can apply the same migrations
|
|
49
|
+
- execute all migrations by **wrapping `migrate-mongo` in a homemade script**: [`scripts/migrations.sh`](../scripts/migrations.sh).
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
root/
|
|
53
|
+
|__migrations/
|
|
54
|
+
|__migrationScripts // folder containing all migration scripts. consumed by both migration config files.
|
|
55
|
+
|__baseConfig.js // base config file that both config files are derived from
|
|
56
|
+
|__migrate-mongo-config-main.js // config file for the main db
|
|
57
|
+
|__migrate-mongo-config-test.js // config file for the test db
|
|
58
|
+
```
|
|
@@ -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.
|
|
@@ -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/docker.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Docker usage
|
|
2
|
+
|
|
3
|
+
The `docker/` folder contains:
|
|
4
|
+
- a `Dockerfile` for aiiinotate
|
|
5
|
+
- a `docker-comose` to integrate aiiinotate with MongoDB, with volume persistance
|
|
6
|
+
- a script to import data into a dockerized aiiinotate instance
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Docker setup
|
|
11
|
+
|
|
12
|
+
1. [Install docker](https://docs.docker.com/engine/install/)
|
|
13
|
+
2. On linux, add your user to the `docker` group (see [full docs](https://docs.docker.com/engine/install/linux-postinstall/))
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
sudo groupadd docker
|
|
17
|
+
sudo usermod -aG docker $USER
|
|
18
|
+
newgrp docker
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Note that all `docker` commands must be run from the `docker/` directory!
|
|
26
|
+
|
|
27
|
+
1. **Clone the repo** and `cd` in it
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone git@github.com:Aikon-platform/aiiinotate.git
|
|
31
|
+
cd aiiinotate/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. **Create a `.env` file** at `docker/.env` (from the root of the project):
|
|
35
|
+
```bash
|
|
36
|
+
cp ./config/.env.template ./docker/.env
|
|
37
|
+
vim ./docker/.env # do your edits
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. **Edit the `.env` file**. Pay attention to the following values:
|
|
41
|
+
- `MONGODB_HOST=mongo` (container name)
|
|
42
|
+
- `AIIINOTATE_HOST=aiiinotate` (container name)
|
|
43
|
+
- `AIIINOTATE_SCHEME=http`
|
|
44
|
+
- `AIIINOTATE_LOG_TARGET=stdout` (Docker aldready collects stdout logs, so setting log target to `file` is redundant)
|
|
45
|
+
- set `AIIINOTATE_PUBLIC_URL` to the URL at which your aiiinotate instance will be accessible (e.g., `aiiinotate.enpc.fr`).
|
|
46
|
+
|
|
47
|
+
4. **Build the containers**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd docker
|
|
51
|
+
bash docker.sh build
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
5. **Start the containers**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bash docker.sh start
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
6. (Optional) **import data** into the running aiiinotate instance. `path/to/dir` is a path to a folder containing AnnotationLists or manifests to import.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bash docker_aiiinotate_import.sh <manifests|annotations> path/to/dir
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Troubleshooting and useful commands
|
|
69
|
+
|
|
70
|
+
View logs:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
docker logs docker-aiiinotate-1 -f
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Access `mongosh` within the Mongo container:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
docker exec -it docker-mongo-1 mongosh
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Check running ports in the Web container:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
docker exec -it docker-web-1 ss -tnl
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
View globally installed NPM packages in the Web container:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
docker exec -it docker-web-1 npm list -g --depth=0
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
CURL the Web container
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
docker exec -it docker-web-1 curl http://0.0.0.0:4444 # change 4444 by your $AIIINOTATE_PORT
|
|
98
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Scalability
|
|
2
|
+
|
|
3
|
+
## Results
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Discussion
|
|
10
|
+
|
|
11
|
+
### Write times
|
|
12
|
+
|
|
13
|
+
Write times (`Write anno. list`, `Write anno.`) presented above are in all probability lower than in real world applications.
|
|
14
|
+
|
|
15
|
+
Indeed, a big bottleneck when inserting annotations is to fetch an annotation's target manifest through HTTP, in ordrer to fill the `canvasIdx` field.
|
|
16
|
+
|
|
17
|
+
When running the benchmark, all annotation URIs are on `https://localhost`, an URI on localhost that is inaccessible. The request thus fails instantly.
|
|
18
|
+
|
|
19
|
+
The reason this was done is that HTTP requests are non-deterministic: if we make requests to an external server storing IIIF manifests, then we end up also benchmarking this server. By fetching manifests on an inaccessible `https://localhost`, this non-deterministic process becomes deterministic and the benchmark makes more sense.
|
|
20
|
+
|
|
21
|
+
### >10M annotations
|
|
22
|
+
|
|
23
|
+
After benchmarking for 10M annotations, we delete the database and attempt to insert 100M annotations to benchmark response times.
|
|
24
|
+
|
|
25
|
+
With 100M annotations stored, the database size becomes an issue. These scalability limits are mostly caused by MongoDB, and not by the aiiinotate app itself: the larger the database, the more hard drive memory it uses, and the more RAM it uses (since indexes are stored in the RAM). To store >100M annotations, we should scale hardware:
|
|
26
|
+
|
|
27
|
+
- **vertical scaling**: more RAM and disk space
|
|
28
|
+
- **horizontal scaling**: use a Mongo [sharded cluster](https://www.mongodb.com/docs/manual/sharding/).
|
|
29
|
+
|
|
30
|
+
It should be noted that the benchmark stress tests your machine, aiiinotate and Mongo at the same time:
|
|
31
|
+
- results presented above are obtained by running the benchmark, aiiinotate and Mongo on a single machine. All 3 are thus competing for RAM access and CPU.
|
|
32
|
+
- it uses mutliple threads, costly JSON-stringification, fast I/O and has a high throughput: it runs a lot of queries to aiiinotate, and to the MongoDB.
|
|
33
|
+
|
|
34
|
+
In real-world examples, with less throughput and when not running the benchmark itself, there should be less stress on your MongoDB server, thus less CPU usage, thus possibly more scalability.
|