aiiinotate 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +61 -0
  3. package/cli/import.js +142 -0
  4. package/cli/index.js +26 -0
  5. package/cli/io.js +105 -0
  6. package/cli/migrate.js +123 -0
  7. package/cli/mongoClient.js +11 -0
  8. package/docs/architecture.md +88 -0
  9. package/docs/db.md +38 -0
  10. package/docs/dev_iiif_compatibility.md +43 -0
  11. package/docs/endpoints.md +48 -0
  12. package/docs/progress.md +159 -0
  13. package/docs/specifications/0_w3c_open_annotations.md +332 -0
  14. package/docs/specifications/1_w3c_web_annotations.md +577 -0
  15. package/docs/specifications/2_iiif_apis.md +396 -0
  16. package/docs/specifications/3_iiif_annotations.md +103 -0
  17. package/docs/specifications/4_search_api.md +135 -0
  18. package/docs/specifications/5_sas.md +119 -0
  19. package/docs/specifications/6_mirador.md +119 -0
  20. package/docs/specifications/7_aikon.md +137 -0
  21. package/docs/specifications/include/presentation_2.0.webp +0 -0
  22. package/docs/specifications/include/presentation_2.0_white.png +0 -0
  23. package/docs/specifications/include/presentation_3.0.png +0 -0
  24. package/docs/specifications/include/presentation_3.0_resize.png +0 -0
  25. package/eslint.config.js +27 -0
  26. package/migrations/baseConfig.js +56 -0
  27. package/migrations/manageIndex.js +55 -0
  28. package/migrations/migrate-mongo-config-main.js +8 -0
  29. package/migrations/migrate-mongo-config-test.js +8 -0
  30. package/migrations/migrationScripts/20250825185706-collections.js +41 -0
  31. package/migrations/migrationScripts/20250826194832-annotations2-canvas-index.js +31 -0
  32. package/migrations/migrationScripts/20250904080710-annotations2-schema.js +42 -0
  33. package/migrations/migrationScripts/20251002141951-manifest2-schema.js +43 -0
  34. package/migrations/migrationScripts/20251006212110-manifest-unique-index.js +29 -0
  35. package/migrations/migrationScripts/20251028115614-annotations2-id-index.js +27 -0
  36. package/migrations/migrationTemplate.js +25 -0
  37. package/package.json +78 -0
  38. package/run.sh +70 -0
  39. package/scripts/_migrations.sh +79 -0
  40. package/scripts/_setup.js +31 -0
  41. package/scripts/setup_mongodb.sh +61 -0
  42. package/scripts/setup_mongodb_migrate.sh +17 -0
  43. package/scripts/setup_node.sh +15 -0
  44. package/scripts/utils.sh +192 -0
  45. package/setup.sh +20 -0
  46. package/src/app.js +113 -0
  47. package/src/config/.env.template +22 -0
  48. package/src/data/annotations/annotations2.js +419 -0
  49. package/src/data/annotations/annotations3.js +32 -0
  50. package/src/data/annotations/routes.js +271 -0
  51. package/src/data/annotations/routes.test.js +180 -0
  52. package/src/data/collectionAbstract.js +270 -0
  53. package/src/data/index.js +29 -0
  54. package/src/data/manifests/manifests2.js +305 -0
  55. package/src/data/manifests/manifests2.test.js +53 -0
  56. package/src/data/manifests/manifests3.js +23 -0
  57. package/src/data/manifests/routes.js +95 -0
  58. package/src/data/manifests/routes.test.js +69 -0
  59. package/src/data/routes.js +141 -0
  60. package/src/data/routes.test.js +117 -0
  61. package/src/data/utils/iiif2Utils.js +196 -0
  62. package/src/data/utils/iiif2Utils.test.js +98 -0
  63. package/src/data/utils/iiif3Utils.js +0 -0
  64. package/src/data/utils/iiifUtils.js +18 -0
  65. package/src/data/utils/routeUtils.js +109 -0
  66. package/src/data/utils/testUtils.js +253 -0
  67. package/src/data/utils/utils.js +231 -0
  68. package/src/db/index.js +48 -0
  69. package/src/fileServer/annotations.js +39 -0
  70. package/src/fileServer/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
  71. package/src/fileServer/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
  72. package/src/fileServer/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
  73. package/src/fileServer/data/annotations2Invalid.jsonld +39 -0
  74. package/src/fileServer/data/annotations2Valid.jsonld +39 -0
  75. package/src/fileServer/data/bnf_invalid_manifest.json +2806 -0
  76. package/src/fileServer/data/bnf_valid_manifest.json +2817 -0
  77. package/src/fileServer/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
  78. package/src/fileServer/index.js +64 -0
  79. package/src/fileServer/manifests.js +14 -0
  80. package/src/fileServer/utils.js +35 -0
  81. package/src/schemas/index.js +20 -0
  82. package/src/schemas/schemasBase.js +47 -0
  83. package/src/schemas/schemasPresentation2.js +417 -0
  84. package/src/schemas/schemasPresentation3.js +57 -0
  85. package/src/schemas/schemasResolver.js +71 -0
  86. package/src/schemas/schemasRoutes.js +277 -0
  87. package/src/server.js +22 -0
  88. package/src/types.js +93 -0
@@ -0,0 +1,119 @@
1
+ # SIMPLE ANNOTATION SERVER
2
+
3
+ ---
4
+
5
+ ## Sources
6
+
7
+ - [official docs](https://github.com/glenrobson/SimpleAnnotationServer/tree/master/doc)
8
+ - [endpoints](https://github.com/glenrobson/SimpleAnnotationServer/blob/master/doc/Endpoints.md)
9
+ - [IIIF annotations search](https://github.com/glenrobson/SimpleAnnotationServer/blob/master/doc/IIIFSearch.md)
10
+
11
+ ---
12
+
13
+ ## What does SAS do ?
14
+
15
+ From what I gather, SAS stores 2 types of data:
16
+ - IIIF annotations
17
+ - IIIF manifests
18
+ - annotations and manifests seem to follow the IIIF 2.x presentation API (instead of the most recent 3.0).
19
+
20
+ SAS has 2 main functionnalities:
21
+ - "backend": store and serve annotations in JSON format
22
+ - "frontend": provide a GUI to manage IIIF collections and, most importantly, collections of IIIF annotations.
23
+ - SAS uses Mirador as its IIIF graphical interface.
24
+ - SAS does have a Mirador plugin but a custom version of Mirador.
25
+
26
+ In turn, annotations are the primary ressources stored by SAS. SAS also needs to store manifests, so that it can enrich the manifests with the annitations, in order to send a complete manifest (a manifest in which annotations are either referenced or embdded).
27
+
28
+ ---
29
+
30
+ ## Endpoints
31
+
32
+ ### Fetch data
33
+
34
+ #### IIIF search API
35
+ ```
36
+ GET /search-api/$manifestShortId/search
37
+ ```
38
+ - **returns**: `AnnotationList` with an empty `@id`
39
+ - `$manifestShortId` is the manifest's ID
40
+ - search parameters are [as per the IIIF docs](https://iiif.io/api/search/2.0/#request-1):
41
+ - `q`: query string, searched in the annotation's textual body or its URI
42
+ - `motivation`: search through each annotation's `motivation` key
43
+ - `date`: date ranges
44
+ - `users`: the usrs who edited the annotation
45
+ - if no parameters are supplied, all annotations are returned
46
+ - *example: https://aikon.enpc.fr/sas/search-api/wit9_man11_anno165/search?q=*
47
+
48
+ #### Show all annotations for a canvas
49
+ ```
50
+ GET /annotation/search?uri=$canvasUri
51
+ ```
52
+ - **returns** `Annotation[]`
53
+ - `$canvasUri` is the URI for the canvas we want annotations for
54
+ - *example: https://aikon.enpc.fr/sas/annotation/search?uri=https://aikon.enpc.fr/aikon/iiif/v2/wit9_man11_anno165/canvas/c16.json#xywh=0,31,1865,1670 shows annotations for canvas https://aikon.enpc.fr/aikon/iiif/v2/wit9_man11_anno165/canvas/c16.json#xywh=0,31,1865,1670*
55
+
56
+ #### List all indexed IIIF manifests
57
+ ```
58
+ GET /manifests
59
+ ```
60
+
61
+ - **returns**
62
+ ```js
63
+ {
64
+ "@type": "sc:Collection",
65
+ "@id": "http://annotation//collection/managed.json",
66
+ "label": "string",
67
+ "@context": "http://iiif.io/api/presentation/2/context.json",
68
+ "members": [
69
+ {
70
+ "@type": "sc:Manifest",
71
+ "@id": "URI"
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ ### Create/Update data
78
+
79
+ #### Populate / Import an AnnotationList into SAS
80
+ ```
81
+ POST /annotation/populate
82
+ ```
83
+ - POST body: the AnnotationList to import can be passed dirctly or as an URI to the AnnotationList (see [SAS source code](https://github.com/glenrobson/SimpleAnnotationServer/blob/dc7c8c6de9f4693c678643db2a996a49eebfcbb0/src/main/java/uk/org/llgc/annotation/store/Populate.java#L46)):
84
+ - `{ "uri": $annotationListUri }`, where `$annotationListUri` is an URI pointing to a IIIF annotations list
85
+ - IIIF AnnotationList
86
+
87
+ #### Create an annotation
88
+ ```
89
+ POST /annotation/create
90
+ ```
91
+ - POST body: IIIF Annotation
92
+
93
+ #### Update an annotation
94
+ ```
95
+ POST /annotation/update
96
+ ```
97
+ - POST body: IIIF Annotation
98
+ - the annotation's `@id` should point to an annotation that exists in the store
99
+
100
+ #### Index a new manifest
101
+ ```
102
+ POST /manifests
103
+ ```
104
+ - POST body: IIIF Manifest
105
+
106
+ ### Delete data
107
+
108
+ #### Delete an annotation
109
+ ```
110
+ DELETE /annotation/destroy/?uri=$annotationUri
111
+ ```
112
+ - `$annotationUri` is the `@id` of the annotation to delete
113
+
114
+ #### Unindex a manifest
115
+ ```
116
+ DELETE /manifests/$manifestId
117
+ ```
118
+ - `$manifestId` is the short manifest identifier
119
+ - actually I'm not 100% sure it is implemented by SAS but we [would need it](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L769)
@@ -0,0 +1,119 @@
1
+ # WRITING A MIRADOR PLUGIN
2
+
3
+ To integrate an annotation server with Mirador, we need to write a Mirador plugin that that interfaces Mirador with the annotation server.
4
+
5
+ Writing a mirador plugin is actually really simple: see the [annotot endpoint plugin](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin)
6
+
7
+ SAS apparently uses a [custom version of Mirador 2](https://github.com/glenrobson/SimpleAnnotationServer/tree/master/src/main/webapp/mirador-2.6.1)
8
+
9
+ ---
10
+
11
+ ## Sources
12
+
13
+ - [Mirador docs: plugins](https://github.com/ProjectMirador/mirador/wiki/Mirador-3-plugins)
14
+ - [Mirador docs: writing a plugin](https://github.com/ProjectMirador/mirador/wiki/Creating-a-Mirador-4-plugin)
15
+ - [Mirador annoto endpoint](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin/tree/master)
16
+
17
+ ---
18
+
19
+ ## Install and use a Mirador plugin
20
+
21
+ 1. npm-install the plugin you want to use:
22
+ ```bash
23
+ npm install mirador-image-tools --save
24
+ ```
25
+ 2. when instanciating Mirador, add the plugin to the `plugins` array:
26
+ ```js
27
+ import miradorImageToolsPlugin from 'mirador-image-tools/es/plugins/miradorImageToolsPlugin.js';
28
+
29
+ const config = {
30
+ // your Mirador config
31
+ };
32
+
33
+ const plugins = [
34
+ ...miradorImageToolsPlugin
35
+ ];
36
+
37
+ Mirador.viewer(config, plugins);
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Writing a plugin
43
+
44
+ ### General architecture
45
+
46
+ ```
47
+ root/
48
+ |_src
49
+ |_index.js // exports the plugin
50
+ |_components/ // folder containing all the react components that comprise the plugin
51
+ |_pluginComponent.js // the react files.
52
+ ```
53
+
54
+ ### Plugin config
55
+
56
+ The plugin's configuration in `index.js` defines how to inject the plugin in Mirador:
57
+ ```js
58
+ const plugin = {
59
+ target: 'WorkspaceControlPanelButtons', // where to inject the plugin in the Mirador architecture
60
+ mode: 'wrap', // how to inject. "add"/"wrap"
61
+ component: PluginComponent, // the plugin as a React component
62
+ // `connectOptions`, `mapStateToProps` and `mapDispatchToProps` connect the plugin to Mirador
63
+ connectOptions: additionalOptionsToPassToReduxConnect, // plugin extras
64
+ mapStateToProps: mapStateToProps, // React redux function that connects store data with react props
65
+ mapDispatchToProps: mapDispatchToProps, // React redux function that dispatches actions to the store
66
+ // `reducers` and `saga` manipulate state
67
+ reducers: {
68
+ pluginState: pluginStateReducer,
69
+ },
70
+ saga: saga
71
+ };
72
+ ```
73
+
74
+ `PluginComponent` is a React component that contains the entire plugin. Nested React components are allowed in `PluginComponent`.
75
+
76
+ About the `mode`:
77
+ - `mode=add` injects the component at designated areas of the Mirador app.
78
+ - `mode=wrap` overrides mirador behaviour.
79
+ - wrapping plugins can:
80
+ - replace the content of a Mirador coponent
81
+ - wrap a Mirador component with additional context
82
+ - to intercept and modify the attributes of a component
83
+ - all Mirador components can be wrapped
84
+
85
+ ### Plugin components
86
+
87
+ A plugin component is a React component:
88
+
89
+ ```js
90
+ import React, { Component } from 'react';
91
+
92
+ export default function () {
93
+ return <h5>Custom metadata!</h5>;
94
+ }
95
+ ```
96
+
97
+ In the example above,
98
+ - if `mode=add`, the `PluginComponent` will will be added as a child of its `target`.
99
+ - if `mode=wrap`, the `PluginComponent` will replace the `target`.
100
+
101
+ When `mode = wrap`, the plugin component can modify its `target`. The plugin will receive the Mirador component that is being wrapped as an attribute => we can change the props of the component, surround it with extra context, change its behaviour...
102
+
103
+ ```js
104
+ import React, { Component } from 'react';
105
+
106
+ export default function ({ TargetComponent, targetProps }) {
107
+ return <TargetComponent {...targetProps} />;
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Annotot plugin
114
+
115
+ - [repo](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin/)
116
+ - [plugin component](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin/blob/master/src/plugins/miradorAnnototEndpointPlugin.js)
117
+ - [plugin config](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin/blob/42a04b93429b0a85dee50a3dbe4d6d5e3fb8046e/src/plugins/miradorAnnototEndpointPlugin.js#L65)
118
+ - [usage demo](https://github.com/ProjectMirador/mirador-annotot-endpoint-plugin/blob/master/demo/src/index.js)
119
+
@@ -0,0 +1,137 @@
1
+ # AIKON NEEDS
2
+
3
+ Hre we desribe the functionnalities our annotation server should implement, and list which functionnalities are aldready implemented by SAS.
4
+
5
+ ---
6
+
7
+ ## Reminders
8
+
9
+ - in Aikon, a Regions corresponds to a regions extraction on a document, and that Regions extrction corresponds to a IIIF manifest. 1 manifest => 1 regions extraction
10
+ - IIIF and SAS specific code in Aikon is stored in [front.app.webapp.utils.iiif](https://github.com/Aikon-platform/aikon/tree/main/front/app/webapp/utils/iiif)
11
+ - in AIKON, IIIF manifests aren't stored in the app. Instead, they are generated dynamically and served when needed.
12
+ - IIIF and SAS specific code in Aikon is stored in [front.app.webapp.utils.iiif](https://github.com/Aikon-platform/aikon/tree/main/front/app/webapp/utils/iiif)
13
+ - see [front.app.webapp.utils.iiif.manifests](https://github.com/Aikon-platform/aikon/blob/main/front/app/webapp/utils/iiif/manifest.py)
14
+ - *note: IIIF manifests are stored by SAS, because SAS needs the manifests. In the app however, we don't use the SAS stored manifests*
15
+
16
+ ---
17
+
18
+ ## Implemented by SAS
19
+
20
+ ### Fetch data
21
+
22
+ - get all manifests stored in SAS
23
+ - SAS route: `GET /manifests`
24
+ - get all annotations / implement a IIIF search API.
25
+ - SAS route: `GET /search-api/$manifestId/search`
26
+ - if we don't implement a full search API, we can use this route to return all annotations for a manifest
27
+ - get all annotations for a single canvas
28
+ - SAS route: `GET /annotation/search/?uri=$canvasUri`, where `$canvasUri` is the URI of the canvas for which we want the annotatione
29
+
30
+ ### Create / update data
31
+
32
+ - import a new IIIF manifest
33
+ - SAS route: `POST /manifests`, where the body is the new IIIF manifest
34
+ - import an AnnotationList
35
+ - SAS route: `POST /annotation/populate`, where the body is either an URI pointing to an AnnotationList, or the AnnotationList itself
36
+
37
+ ### Delete data
38
+
39
+ - delete a manifest (actually not 100% sure that this is implemented by SAS)
40
+ - sas route: `DELETE /manifests`
41
+ - delete an annotation
42
+ - sas route: `DELETE /annotation/destroy/?uri=$annotationUri` where `$annotationUri` is the ID of the annotation to delete
43
+
44
+ ### Provided by SAS but not currently used in AIKON:
45
+
46
+ - `POST /annotation/create`: crate a single annotation
47
+ - `POST /annotation/update`: update a single annotation
48
+
49
+ ---
50
+
51
+ ## Not implemented by SAS
52
+
53
+ ### Fetch data
54
+
55
+ - get all annotations for a single manifest ([code](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L32)).
56
+ - this behaviour is implemented by SAS but results are paginated which requires to run several queries
57
+ - get the total number of annotations in a single manifest ([code](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L648))
58
+ - get all annotations for a specific canvas
59
+ - get all annotations for a range of canvases
60
+ - have several "export format" for an annotation:
61
+ - IIIF annotation of course
62
+ ```
63
+ {
64
+ "@id" : "http://aikon.enpc.fr/sas/annotation/wit69_man69_anno134_c179_23f885ed66914139ab7d67d22f8f8f46",
65
+ "@type" : "oa:Annotation",
66
+ "dcterms:created" : "2025-03-03T14:40:57",
67
+ "dcterms:modified" : "2025-03-03T14:40:57",
68
+ "resource" : {
69
+ "@type" : "dctypes:Text",
70
+ "format" : "text/html",
71
+ "chars" : "<p></p>",
72
+ "https://aikon.enpc.fr/sas/full_text" : "",
73
+ "https://iscd.huma-num.fr/sas/full_text" : ""
74
+ },
75
+ "on" : "https://aikon.enpc.fr/aikon/iiif/v2/wit69_man69_anno134/canvas/c179.json#xywh=52,1221,891,54",
76
+ "motivation" : [ "oa:tagging", "oa:commenting" ],
77
+ "@context" : "http://iiif.io/* TLSv1.2 (IN), TLS header, Supplemental data (23):
78
+ api/presentation/2/context.json",
79
+ "label" : ""
80
+ }
81
+ ```
82
+ - standardized metadata (used in application for svelte) (e.g. `https://iscd.huma-num.fr/vhs/witness/2339/regions/canvas`)
83
+ ```
84
+ r_annos[canvas][aid] = {
85
+ "id": aid,
86
+ "ref": f"{img}_{xywh}",
87
+ "class": "Region",
88
+ "type": get_name("Regions"),
89
+ "title": region_title(canvas, xywh),
90
+ "url": gen_iiif_url(img, res=f"{xywh}/full/0"),
91
+ "canvas": canvas,
92
+ "xywh": xywh.split(","),
93
+ "img": img,
94
+ }
95
+ ```
96
+ - only ids
97
+ ```
98
+ manifest_annotations.extend(
99
+ annotation["@id"] for annotation in annotations["resources"]
100
+ )
101
+ ```
102
+ - API url list (e.g. [`https://iscd.huma-num.fr/vhs/witness/2339/regions/canvas`](https://iscd.huma-num.fr/vhs/wit249_man249_anno249/list/))
103
+ ```
104
+ "wit249_man249_0021_695,1020,123,214": "https://iscd.huma-num.fr/iiif/2/wit249_man249_0021.jpg/695,1020,123,214/full/0/default.jpg",
105
+ "wit249_man249_0021_880,1032,421,135": "https://iscd.huma-num.fr/iiif/2/wit249_man249_0021.jpg/880,1032,421,135/full/0/default.jpg",
106
+ "wit249_man249_0021_167,1282,505,770": "https://iscd.huma-num.fr/iiif/2/wit249_man249_0021.jpg/167,1282,505,770/full/0/default.jpg",
107
+ "wit249_man249_0021_308,1179,220,236": "https://iscd.huma-num.fr/iiif/2/wit249_man249_0021.jpg/308,1179,220,236/full/0/default.jpg",
108
+ "wit249_man249_0021_207,1013,468,149": "https://iscd.huma-num.fr/iiif/2/wit249_man249_0021.jpg/207,1013,468,149/full/0/default.jpg"
109
+ ```
110
+
111
+ ### Create / update data
112
+
113
+ - index all annotations for a manifest in 1 query ([code](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L197)).
114
+
115
+ ### Delete data
116
+
117
+ - unindex a manifest ([code](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L769))
118
+ - remove all annotations for a single manifest in 1 query ([code](https://github.com/Aikon-platform/aikon/blob/cc8430c52e205e6a1c04c4ae84f69126fb5a3bda/front/app/webapp/utils/iiif/annotation.py#L798)). currently, we need to
119
+ - loop over each canvas in a manifest
120
+ - loop over each annotation in the canvas
121
+ - delete that annotation in an HTTP request => tons of HTTP requests
122
+
123
+ ### Other
124
+
125
+ - annotations should be ordered by their position on the page (or have a method that returns annotations ordered)
126
+ - store rectangular annotations (bounding boxes) as well as polygonal annotations
127
+ - canvas number management:
128
+ - annotation should have their canvas number as standard metadata (for now we need to parse `canvas = anno["on"].split("/canvas/c")[1].split(".json")[0]` 😰)
129
+ - make annotations ordered not alphabetically (137 arriving before 14) but by canvas order
130
+ - technically, this means that, when saving an annotation, you need to:
131
+ - fetch the manifest of the `annotation.on`
132
+ - index it (minimally, as a manifest URL + ordered array of canvases)
133
+ - extract the proper canvas number for the current annotation
134
+ - save in the annotation the canvas number
135
+ - Create a pipeline to import SAS annotation inside new server easily
136
+
137
+
@@ -0,0 +1,27 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import json from "@eslint/json";
4
+ import css from "@eslint/css";
5
+ import { defineConfig, globalIgnores } from "eslint/config";
6
+ import stylistic from "@stylistic/eslint-plugin"
7
+
8
+ /** https://eslint.org/docs/latest/use/configure/rules */
9
+ export default defineConfig([
10
+ {
11
+ files: ["**/*.{js,mjs,cjs}"],
12
+ plugins: {
13
+ "js": js,
14
+ "@stylistic": stylistic
15
+ },
16
+ extends: ["js/recommended"],
17
+ languageOptions: { globals: {...globals.browser, ...globals.node} },
18
+ rules: {
19
+ "no-unused-vars": "off",
20
+ "quotes": ["error", "double"],
21
+ "@stylistic/indent": ["error", 2],
22
+ },
23
+ },
24
+ { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] },
25
+ { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
26
+ globalIgnores(["package-lock.json"]),
27
+ ]);
@@ -0,0 +1,56 @@
1
+ import path from "path";
2
+ import { dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /**
6
+ * @typedef Config
7
+ * @type {object}
8
+ * @property {string} connString
9
+ * @property {dbName} dbName
10
+ * @property {migrationsDir} migrationsDir
11
+ */
12
+
13
+ // path to dirctory of curent file
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ /**
17
+ * generate a migrate-mongo config file based on options defined in `mongoConfig`.
18
+ * the object returned here is a copy of what's generated by `migrate-mongo init`.
19
+ * @param {Config} config */
20
+ export default (config) => ({
21
+ mongodb: {
22
+ url: config.connString,
23
+
24
+ databaseName: config.dbName,
25
+
26
+ options: {
27
+ useNewUrlParser: true, // removes a deprecation warning when connecting
28
+ useUnifiedTopology: true, // removes a deprecating warning when connecting
29
+ // connectTimeoutMS: 3600000, // increase connection timeout to 1 hour
30
+ // socketTimeoutMS: 3600000, // increase socket timeout to 1 hour
31
+ }
32
+ },
33
+
34
+ // The migrations dir, can be an relative or absolute path. Only edit this when really necessary.
35
+ migrationsDir: path.join(path.resolve(__dirname), "migrationScripts"),
36
+
37
+ // The mongodb collection where the applied changes are stored. Only edit this when really necessary.
38
+ changelogCollectionName: "changelog",
39
+
40
+ // The mongodb collection where the lock will be created.
41
+ lockCollectionName: "changelog_lock",
42
+
43
+ // The value in seconds for the TTL index that will be used for the lock. Value of 0 will disable the feature.
44
+ lockTtl: 0,
45
+
46
+ // The file extension to create migrations and search for in migration dir
47
+ migrationFileExtension: ".js",
48
+
49
+ // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine
50
+ // if the file should be run. Requires that scripts are coded to be run multiple times.
51
+ useFileHash: false,
52
+
53
+ // Don't change this, unless you know what you're doing
54
+ //NOTE: default value is 'commonjs'
55
+ moduleSystem: "esm",
56
+ })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * utility functions to create/remove an index
3
+ */
4
+
5
+ /**
6
+ * @typedef IndexSpecType
7
+ * @type {object}
8
+ * @property {string} indexName - the name of the index we're creating
9
+ * @property {any} [prop] - any other key-value pairs are allowed
10
+ */
11
+
12
+ /** @typedef {import("mongodb").IndexSpecification} IndexSpecificationType */
13
+ /** @typedef {import("mongodb").Db} DbType */
14
+ /** @typedef {import("mongodb").CreateIndexesOptions} CreateIndexesOptionsType */
15
+
16
+
17
+ /**
18
+ * @param {CreateIndexesOptionsType} indexOptions - indexOptions.name MUST be defined !
19
+ */
20
+ const validateIndexOptions = (indexOptions) => {
21
+ if ( indexOptions.name == null ) {
22
+ throw new Error("indexOptions.name must be defined !")
23
+ }
24
+ }
25
+
26
+ /**
27
+ *
28
+ * @param {DbType} db
29
+ * @param {string} collectionName
30
+ * @param {IndexSpecificationType} indexSpec
31
+ * @param {CreateIndexesOptionsType} indexOptions - indexOptions.name MUST be defined !
32
+ */
33
+ async function createIndex(db, collectionName, indexSpec, indexOptions) {
34
+ validateIndexOptions(indexOptions);
35
+ const collection = db.collection(collectionName);
36
+ const result = await collection.createIndex(indexSpec, indexOptions);
37
+ console.log("created index:", result);
38
+ }
39
+
40
+ /**
41
+ * @param {DbType} db
42
+ * @param {string} collectionName
43
+ * @param {CreateIndexesOptionsType} indexOptions - indexOptions.name MUST be defined !
44
+ */
45
+ async function removeIndex(db, collectionName, indexOptions) {
46
+ validateIndexOptions(indexOptions);
47
+ const collection = db.collection(collectionName);
48
+ const result = await collection.dropIndex(indexOptions.name);
49
+ console.log("dropped index:", result);
50
+ }
51
+
52
+ export {
53
+ createIndex,
54
+ removeIndex
55
+ }
@@ -0,0 +1,8 @@
1
+ import baseConfig from "#migrations/baseConfig.js";
2
+
3
+ const config = {
4
+ connString: process.env.MONGODB_CONNSTRING,
5
+ dbName: process.env.MONGODB_DB
6
+ }
7
+
8
+ export default baseConfig(config);
@@ -0,0 +1,8 @@
1
+ import baseConfig from "#migrations/baseConfig.js";
2
+
3
+ const config = {
4
+ connString: process.env.MONGODB_CONNSTRING_TEST,
5
+ dbName: process.env.MONGODB_DB_TEST
6
+ }
7
+
8
+ export default baseConfig(config);
@@ -0,0 +1,41 @@
1
+ /**
2
+ * create the collections of the database (without schema)
3
+ * - annotations3 : annotations following the w3c Web Annotations standard (used by IIIF 3.0 presentation API)
4
+ * - annotations2 : annotations following the Open Annotations standard (used by IIIF 2.1 presentation API)
5
+ * - manifests3 : IIIF manifests following the IIIF 3.0 presentation API
6
+ * - manifests2 : IIIF manifests following the IIIF 2.1 (and 2.0) presentation API
7
+ */
8
+
9
+ const collectionNames = [
10
+ "annotations3",
11
+ "annotations2",
12
+ "manifests3",
13
+ "manifests2"
14
+ ];
15
+
16
+ /**
17
+ * @param {import('mongodb').Db} db
18
+ * @param {import('mongodb').MongoClient} client
19
+ * @returns {Promise<void>}
20
+ */
21
+ export const up = async (db, client) => {
22
+ // See https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script
23
+ collectionNames.forEach((colName) => {
24
+ db.createCollection(colName);
25
+ })
26
+ };
27
+
28
+ /**
29
+ * @param {import('mongodb').Db} db
30
+ * @param {import('mongodb').MongoClient} client
31
+ * @returns {Promise<void>}
32
+ */
33
+ export const down = async (db, client) => {
34
+ // Example:
35
+ // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}});
36
+ collectionNames.forEach(async (colName) => {
37
+ const collection = db.collection(colName);
38
+ await collection.drop();
39
+ })
40
+ }
41
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * create a text index in collection `annotations2` on annotation targets;
3
+ * indexes key is `on.full` that describes the target canvases for each annotation.
4
+ */
5
+
6
+ const
7
+ colName = "annotations2",
8
+ indexSpec = { "on.full": 1 },
9
+ indexOptions = { name: "canvasIdIndex" };
10
+
11
+ /**
12
+ * @param {import('mongodb').Db} db
13
+ * @param {import('mongodb').MongoClient} client
14
+ * @returns {Promise<void>}
15
+ */
16
+ export const up = async (db, client) => {
17
+ const collection = db.collection(colName);
18
+ const result = await collection.createIndex(indexSpec, indexOptions);
19
+ console.log("created index:", result);
20
+ };
21
+
22
+ /**
23
+ * @param {import('mongodb').Db} db
24
+ * @param {import('mongodb').MongoClient} client
25
+ * @returns {Promise<void>}
26
+ */
27
+ export const down = async (db, client) => {
28
+ const collection = db.collection(colName);
29
+ const result = await collection.dropIndex(indexOptions.name);
30
+ console.log("dropped index:", result);
31
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * define the validation schema on collection `annotations2`.
3
+ */
4
+ import build from "#src/app.js"
5
+
6
+ // const inspect = (theObj) => util.inspect(theObj, {showHidden: false, depth: null, colors: true});
7
+
8
+ /**
9
+ * @param db {import('mongodb').Db}
10
+ * @param client {import('mongodb').MongoClient}
11
+ * @returns {Promise<void>}
12
+ */
13
+ export const up = async (db, client) => {
14
+ const
15
+ fastify = await build(),
16
+ fastifySchema = fastify.schemasPresentation2.getSchema("annotation"),
17
+ schema = fastify.schemasResolver(fastifySchema),
18
+ commandDoc = {
19
+ collMod: "annotations2",
20
+ validator: { $jsonSchema: schema }
21
+ },
22
+ r = await db.command(commandDoc);
23
+
24
+ if ( r.ok !== 1 ) {
25
+ throw new Error(`command failed with error: ${r}`);
26
+ }
27
+ };
28
+
29
+ /**
30
+ * @param db {import('mongodb').Db}
31
+ * @param client {import('mongodb').MongoClient}
32
+ * @returns {Promise<void>}
33
+ */
34
+ export const down = async (db, client) => {
35
+ const r = await db.command({
36
+ collMod: "annotations2",
37
+ validator: {}
38
+ });
39
+ if ( r.ok !== 1 ) {
40
+ throw new Error(`command failed with error: ${r}`);
41
+ }
42
+ };