aiiinotate 0.8.4 → 0.9.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/README.md +14 -0
- package/cli/migrate.js +14 -8
- package/package.json +2 -3
- package/scripts/run.sh +1 -1
- package/src/data/annotations/annotations2.js +41 -14
- package/src/data/manifests/manifests2.js +26 -14
- package/src/data/routes.js +5 -6
- package/src/data/routes.test.js +31 -1
- package/src/schemas/schemasRoutes.js +35 -5
package/README.md
CHANGED
|
@@ -176,6 +176,20 @@ npm run migrate -- <command> <arguments?>
|
|
|
176
176
|
npm run cli -- import <arguments>
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Test coverage
|
|
182
|
+
|
|
183
|
+
aiiinotate is well tested: **over 90% test coverage** on all files !
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
187
|
+
ℹ file | line % | branch % | funcs % | uncovered lines
|
|
188
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
189
|
+
ℹ all files | 90.02 | 79.43 | 78.73 |
|
|
190
|
+
ℹ ----------------------------------------------------------------------------------------
|
|
191
|
+
```
|
|
192
|
+
|
|
179
193
|
---
|
|
180
194
|
|
|
181
195
|
## License
|
package/cli/migrate.js
CHANGED
|
@@ -15,9 +15,6 @@ import { execSync } from "node:child_process"
|
|
|
15
15
|
|
|
16
16
|
import { Command, Option, Argument } from "commander";
|
|
17
17
|
|
|
18
|
-
import loadMongoClient from "#cli/utils/mongoClient.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
18
|
/** @typedef {"make"|"apply"|"revert"|"revert-all"} MigrateOpType */
|
|
22
19
|
const allowedMigrateOp = ["make", "apply", "revert", "revert-all"];
|
|
23
20
|
|
|
@@ -109,17 +106,26 @@ async function action(command, migrationOp, options) {
|
|
|
109
106
|
|
|
110
107
|
function makeMigrateCommand() {
|
|
111
108
|
const migrationOpArg =
|
|
112
|
-
new Argument("<migration-op>", "name of migration operation")
|
|
109
|
+
new Argument("<migration-op>", "name of migration operation")
|
|
110
|
+
.choices(allowedMigrateOp);
|
|
113
111
|
|
|
114
112
|
const migrationNameOpt =
|
|
115
|
-
new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)")
|
|
116
|
-
.makeOptionMandatory();
|
|
113
|
+
new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)");
|
|
117
114
|
|
|
118
|
-
|
|
115
|
+
const command = new Command("migrate");
|
|
116
|
+
return command
|
|
119
117
|
.description("run database migrations")
|
|
120
118
|
.addArgument(migrationOpArg)
|
|
121
119
|
.addOption(migrationNameOpt)
|
|
122
|
-
.action((migrationOp, options, command) =>
|
|
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
|
+
})
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
export default makeMigrateCommand;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiiinotate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "a fast IIIF-compliant annotation server",
|
|
5
5
|
"main": "./cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"lint": "npx eslint --fix",
|
|
19
19
|
"migrate": "npm run cli -- migrate",
|
|
20
20
|
"update_version": "python3 scripts/update_version.py",
|
|
21
|
-
"get_version": "python3 scripts/get_version.py"
|
|
22
|
-
"ttt": "echo 'HELLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLO'"
|
|
21
|
+
"get_version": "python3 scripts/get_version.py"
|
|
23
22
|
},
|
|
24
23
|
"pre-commit": [
|
|
25
24
|
"lint"
|
package/scripts/run.sh
CHANGED
|
@@ -27,7 +27,7 @@ case "$SCRIPT" in
|
|
|
27
27
|
nodemon --watch ./src --exec "bash -c '$DOTENVX_BIN run -f $ENV_PATH -- node $ROOT_DIR/cli/index.js serve dev'";
|
|
28
28
|
;;
|
|
29
29
|
test)
|
|
30
|
-
"$DOTENVX_BIN" run -f "$ENV_PATH" -- node --test --test-isolation=none
|
|
30
|
+
"$DOTENVX_BIN" run -f "$ENV_PATH" -- node --test --test-isolation=none --experimental-test-coverage
|
|
31
31
|
;;
|
|
32
32
|
*)
|
|
33
33
|
echo "Unknown run mode: $SCRIPT. Exiting...";
|
|
@@ -7,7 +7,7 @@ import fastifyPlugin from "fastify-plugin";
|
|
|
7
7
|
import CollectionAbstract from "#data/collectionAbstract.js";
|
|
8
8
|
import { STRICT_MODE } from "#constants";
|
|
9
9
|
import { IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
|
|
10
|
-
import { ajvCompile, objectHasKey, isNullish, maybeToArray, visibleLog, memoize } from "#utils/utils.js";
|
|
10
|
+
import { ajvCompile, objectHasKey, isNullish, maybeToArray, visibleLog, memoize, getFirstNonEmptyPair } from "#utils/utils.js";
|
|
11
11
|
import { getManifestShortId, makeTarget, makeAnnotationId, toAnnotationList, canvasUriToManifestUri } from "#utils/iiif2Utils.js";
|
|
12
12
|
import { PAGE_SIZE } from "#constants";
|
|
13
13
|
|
|
@@ -62,7 +62,7 @@ class Annotations2 extends CollectionAbstract {
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* @type {() => Promise<number>}
|
|
65
|
+
* @type {(object) => Promise<number>}
|
|
66
66
|
* cache the number of documents corresponding to a paginated query in a JS cache
|
|
67
67
|
* a simple cache avoids rerunning a count to get the total number of documents for each page of a paginated query
|
|
68
68
|
* see: https://dev.to/codewithjohnson/the-power-of-a-simple-cache-system-with-javascript-map-3j01
|
|
@@ -76,15 +76,28 @@ class Annotations2 extends CollectionAbstract {
|
|
|
76
76
|
* @returns
|
|
77
77
|
*/
|
|
78
78
|
#expandRouteAnnotationFilter(filterKey, filterVal) {
|
|
79
|
-
const allowedFilterKeys = [ "uri", "manifestShortId", "canvasUri" ];
|
|
79
|
+
const allowedFilterKeys = [ "uri", "manifestShortId", "canvasUri", "tag" ];
|
|
80
80
|
if ( !allowedFilterKeys.includes(filterKey) ) {
|
|
81
81
|
throw new Error(`${this.funcname(this.#expandRouteAnnotationFilter)}: expected one of ${allowedFilterKeys} for param 'deleteKey', got '${filterKey}'`)
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const map = {
|
|
84
|
+
uri: { "@id": filterVal },
|
|
85
|
+
canvasUri: { "on.full": filterVal },
|
|
86
|
+
manifestShortId: { "on.manifestShortId": filterVal },
|
|
87
|
+
tag: {
|
|
88
|
+
$and: [
|
|
89
|
+
{
|
|
90
|
+
// schema accepts both oa:Tag and Tag
|
|
91
|
+
$or: [
|
|
92
|
+
{"resource.@type": "oa:Tag"},
|
|
93
|
+
{"resource.@type": "Tag"}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{ "resource.chars": filterVal }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return map[filterKey];
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
@@ -412,16 +425,30 @@ class Annotations2 extends CollectionAbstract {
|
|
|
412
425
|
// delete
|
|
413
426
|
|
|
414
427
|
/**
|
|
415
|
-
* @param {
|
|
416
|
-
* @param {string} deleteVal - deletion key
|
|
428
|
+
* @param {Object<string,string>} deleteFilter - filter for the annotations to delete
|
|
417
429
|
* @returns {Promise<DeleteResponseType>}
|
|
418
430
|
*/
|
|
419
|
-
async deleteAnnotations(
|
|
431
|
+
async deleteAnnotations(deleteFilter) {
|
|
432
|
+
const err = (message) => this.deleteError(`${this.funcName(this.deleteAnnotations)}: ${message}`);
|
|
420
433
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
434
|
+
let expandedDeleteFilter;
|
|
435
|
+
if ( Object.keys(deleteFilter).includes("tag") ) {
|
|
436
|
+
// should be validated by the route's JSONSchema, but just in case.
|
|
437
|
+
if ( ! Object.keys(deleteFilter).includes("manifestShortId") ) {
|
|
438
|
+
throw err("Cannot delete by \"tag\" without also filtering by \"manifestShortId\" !")
|
|
439
|
+
}
|
|
440
|
+
const expand = (k) => this.#expandRouteAnnotationFilter(k, deleteFilter[k]);
|
|
441
|
+
expandedDeleteFilter = {
|
|
442
|
+
...expand("tag"),
|
|
443
|
+
...expand("manifestShortId")
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
const [deleteKey, deleteVal] = getFirstNonEmptyPair(deleteFilter);
|
|
447
|
+
expandedDeleteFilter = this.#expandRouteAnnotationFilter(deleteKey, deleteVal);
|
|
448
|
+
}
|
|
449
|
+
return this.delete(expandedDeleteFilter);
|
|
423
450
|
} catch (err) {
|
|
424
|
-
throw
|
|
451
|
+
throw err(err.message);
|
|
425
452
|
}
|
|
426
453
|
}
|
|
427
454
|
|
|
@@ -3,7 +3,7 @@ import fastifyPlugin from "fastify-plugin";
|
|
|
3
3
|
import CollectionAbstract from "#data/collectionAbstract.js";
|
|
4
4
|
import { getManifestShortId } from "#utils/iiif2Utils.js";
|
|
5
5
|
import { formatInsertResponse } from "#utils/routeUtils.js";
|
|
6
|
-
import { inspectObj, visibleLog, ajvCompile } from "#utils/utils.js";
|
|
6
|
+
import { inspectObj, visibleLog, ajvCompile, memoize } from "#utils/utils.js";
|
|
7
7
|
import { IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
|
|
8
8
|
import { BASE_URL } from "#constants";
|
|
9
9
|
|
|
@@ -46,6 +46,23 @@ class Manifests2 extends CollectionAbstract {
|
|
|
46
46
|
/////////////////////////////////////////////
|
|
47
47
|
// utils
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* fetch the array of canvasIds for a single manifest.
|
|
51
|
+
* since, in AIKON after a RegionExtraction, an annotation insert
|
|
52
|
+
* is done once per canvas, and for each annotation insert, we
|
|
53
|
+
* fetch the canvas index, we memoize the canvas list for each manifest URI
|
|
54
|
+
* to avoid multiplying database calls.
|
|
55
|
+
* @type {(string) => Promise<string[]>}
|
|
56
|
+
*/
|
|
57
|
+
#memoizeGetManifestCanvasIds = memoize(async (manifestUri) => {
|
|
58
|
+
const doc = await this.collection
|
|
59
|
+
.findOne(
|
|
60
|
+
{ "@id": manifestUri },
|
|
61
|
+
{ projection: { canvasIds: 1, _id: 0 } } // findOne is enough, there's only one manifest per URI
|
|
62
|
+
);
|
|
63
|
+
return doc?.canvasIds ?? [];
|
|
64
|
+
}, 60_000);
|
|
65
|
+
|
|
49
66
|
/**
|
|
50
67
|
* NOTE: PERFORMANCE: using AJV validation is MUCH FASTER than doing manual verifications (-25% execution time for the test suite)
|
|
51
68
|
* @param {object} manifest
|
|
@@ -295,23 +312,18 @@ class Manifests2 extends CollectionAbstract {
|
|
|
295
312
|
* @returns {Promise<number?>}
|
|
296
313
|
*/
|
|
297
314
|
async getCanvasIdx(manifestUri, canvasUri) {
|
|
298
|
-
//
|
|
315
|
+
// old method without memoization.
|
|
299
316
|
// - with `aggregate`, ~2800ms for the whole test suite to run.
|
|
300
317
|
// - with a native `coll.findOne()` and then getting the canvas ID manually (`arr.indexOf`), ~4000ms for the whole test suite to run.
|
|
301
318
|
// https://www.mongodb.com/docs/manual/aggregation/
|
|
302
319
|
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfArray/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
{ $project: { index: { $indexOfArray: ["$canvasIds", canvasUri] } } }
|
|
311
|
-
]).next();
|
|
312
|
-
return r === null
|
|
313
|
-
? undefined
|
|
314
|
-
: r.index !== -1 ? r.index : undefined;
|
|
320
|
+
// const r = await this.collection.aggregate([
|
|
321
|
+
// { $match: { "@id": manifestUri } },
|
|
322
|
+
// { $project: { index: { $indexOfArray: ["$canvasIds", canvasUri] } } }
|
|
323
|
+
// ]).next();
|
|
324
|
+
const r = await this.#memoizeGetManifestCanvasIds(manifestUri);
|
|
325
|
+
const index = r.indexOf(canvasUri);
|
|
326
|
+
return index !== -1 ? index : undefined
|
|
315
327
|
}
|
|
316
328
|
|
|
317
329
|
/**
|
package/src/data/routes.js
CHANGED
|
@@ -29,7 +29,7 @@ function commonRoutes(fastify, options, done) {
|
|
|
29
29
|
routeDeleteSchema = fastify.schemasRoutes.getSchema("routeDelete"),
|
|
30
30
|
responsePostSchema = makeResponsePostSchema(fastify),
|
|
31
31
|
validatorRouteAnnotationDeleteSchema = ajvCompile(fastify.schemasResolver(
|
|
32
|
-
fastify.schemasRoutes.getSchema("
|
|
32
|
+
fastify.schemasRoutes.getSchema("routeAnnotationDeleteFilter")
|
|
33
33
|
)),
|
|
34
34
|
validatorRouteManifestDeleteSchema = ajvCompile(fastify.schemasResolver(
|
|
35
35
|
fastify.schemasRoutes.getSchema("routeManifestFilter")
|
|
@@ -147,7 +147,6 @@ function commonRoutes(fastify, options, done) {
|
|
|
147
147
|
? validatorRouteAnnotationDeleteSchema
|
|
148
148
|
: validatorRouteManifestDeleteSchema,
|
|
149
149
|
error = new Error(`Error validating DELETE route on collection '${collectionName}' with queryString '${inspectObj(query)}'`);
|
|
150
|
-
|
|
151
150
|
if ( !validator(query) ) {
|
|
152
151
|
returnError(request, reply, error, {}, 400);
|
|
153
152
|
}
|
|
@@ -155,16 +154,16 @@ function commonRoutes(fastify, options, done) {
|
|
|
155
154
|
}
|
|
156
155
|
},
|
|
157
156
|
async (request, reply) => {
|
|
158
|
-
const
|
|
159
|
-
{ collectionName, iiifPresentationVersion } = request.params,
|
|
160
|
-
[ deleteKey, deleteVal ] = getFirstNonEmptyPair(request.query);
|
|
157
|
+
const { collectionName, iiifPresentationVersion } = request.params;
|
|
161
158
|
|
|
162
159
|
try {
|
|
163
160
|
if ( collectionName==="annotations" ) {
|
|
161
|
+
const deleteFilter = request.query;
|
|
164
162
|
return iiifPresentationVersion === 2
|
|
165
|
-
? await annotations2.deleteAnnotations(
|
|
163
|
+
? await annotations2.deleteAnnotations(deleteFilter)
|
|
166
164
|
: annotations3.notImplementedError();
|
|
167
165
|
} else {
|
|
166
|
+
const [ deleteKey, deleteVal ] = getFirstNonEmptyPair(request.query);
|
|
168
167
|
return iiifPresentationVersion === 2
|
|
169
168
|
? await manifests2.deleteManifest(deleteKey, deleteVal)
|
|
170
169
|
: manifests3.notImplementedError();
|
package/src/data/routes.test.js
CHANGED
|
@@ -58,7 +58,10 @@ test("test common routes", async (t) => {
|
|
|
58
58
|
await t.test("test preValidation hook for queryString validation", async (t) => {
|
|
59
59
|
const data = [
|
|
60
60
|
["/manifests/2/delete?canvasUri=xxx", false], // canvasUri is only allowed if `collectionName==="annotations"` => will fail.
|
|
61
|
-
["/
|
|
61
|
+
["/annotations/2/delete?tag=xxx", false], // if using tag, manifestShortId must also be defined
|
|
62
|
+
["/manifests/2/delete?tag=xxx&manifestShortId=xxx", false], // tag is not allowed with `manifests`
|
|
63
|
+
["/annotations/2/delete?tag=xxx&manifestShortId=xxx", true],
|
|
64
|
+
["/manifests/2/delete?manifestShortId=xxx", true],
|
|
62
65
|
];
|
|
63
66
|
for ( let i=0; i<data.length; i++ ) {
|
|
64
67
|
const [url, expectSuccess] = data.at(i);
|
|
@@ -125,6 +128,33 @@ test("test common routes", async (t) => {
|
|
|
125
128
|
|
|
126
129
|
await deletePipeline(true);
|
|
127
130
|
await deletePipeline(false);
|
|
131
|
+
await fastify.emptyCollections();
|
|
132
|
+
|
|
133
|
+
await t.test("test route /annotations/:iiifPresentationVersion/delete with param 'tag'", async (t) => {
|
|
134
|
+
const
|
|
135
|
+
tags = ["tag1", "tag2", "tag3"],
|
|
136
|
+
// outputs one of the tags at random
|
|
137
|
+
selectTag = () => tags[Math.floor(Math.random() * 3)],
|
|
138
|
+
testTag = selectTag(),
|
|
139
|
+
// avoid changes to the global annotationList by cloning it.
|
|
140
|
+
annotationListCopy = structuredClone(annotationList),
|
|
141
|
+
// all annotations in the anno list are on the same manifest
|
|
142
|
+
manifestShortId = getManifestShortId(annotationList.resources[0].on);
|
|
143
|
+
|
|
144
|
+
annotationListCopy.resources = annotationList.resources.map((anno) => {
|
|
145
|
+
anno.resource = {
|
|
146
|
+
"@type": "oa:Tag",
|
|
147
|
+
"chars": selectTag()
|
|
148
|
+
}
|
|
149
|
+
return anno;
|
|
150
|
+
})
|
|
151
|
+
const expectedDeletedCount = annotationListCopy.resources.filter((anno) =>
|
|
152
|
+
anno.resource["@type"]==="oa:Tag"
|
|
153
|
+
&& anno.resource["chars"]===testTag
|
|
154
|
+
).length;
|
|
155
|
+
await injectTestAnnotations(fastify, t, annotationList);
|
|
156
|
+
await testDeleteRoute(t, `/annotations/2/delete?manifestShortId=${manifestShortId}&tag=${testTag}`, expectedDeletedCount);
|
|
157
|
+
})
|
|
128
158
|
});
|
|
129
159
|
|
|
130
160
|
})
|
|
@@ -165,6 +165,22 @@ function addSchemas(fastify, options, done) {
|
|
|
165
165
|
]
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
+
// to delete an annotation by tag, you must also provide a manifestShortId (to avoid deleting all annotations with this tag in the entire db)
|
|
169
|
+
fastify.addSchema({
|
|
170
|
+
$id: makeSchemaUri("routeAnnotationFilterTag"),
|
|
171
|
+
type: "object",
|
|
172
|
+
required: ["tag", "manifestShortId"],
|
|
173
|
+
properties: {
|
|
174
|
+
manifestShortId: {
|
|
175
|
+
type: "string", description: "delete all annotations for a single manifest"
|
|
176
|
+
},
|
|
177
|
+
tag: {
|
|
178
|
+
type: "string", description: "delete allannotations for a single tag"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
additionalProperties: false
|
|
182
|
+
})
|
|
183
|
+
|
|
168
184
|
// for annotations: key-value pairs to filter a manifests collection by
|
|
169
185
|
fastify.addSchema({
|
|
170
186
|
$id: makeSchemaUri("routeAnnotationFilter"),
|
|
@@ -172,21 +188,33 @@ function addSchemas(fastify, options, done) {
|
|
|
172
188
|
{
|
|
173
189
|
type: "object",
|
|
174
190
|
required: ["uri"],
|
|
175
|
-
properties: { uri: { type: "string", description: "delete the annotation with this '@id'" } }
|
|
191
|
+
properties: { uri: { type: "string", description: "delete the annotation with this '@id'" } },
|
|
192
|
+
additionalProperties: false
|
|
176
193
|
},
|
|
177
194
|
{
|
|
178
195
|
type: "object",
|
|
179
196
|
required: ["manifestShortId"],
|
|
180
|
-
properties: { manifestShortId: { type: "string", description: "delete all annotations for a single manifest" } }
|
|
197
|
+
properties: { manifestShortId: { type: "string", description: "delete all annotations for a single manifest" } },
|
|
198
|
+
additionalProperties: false
|
|
181
199
|
},
|
|
182
200
|
{
|
|
183
201
|
type: "object",
|
|
184
202
|
required: ["canvasUri"],
|
|
185
|
-
properties: { canvasUri: { type: "string", description: "delete all annotations for a single canvas" } }
|
|
203
|
+
properties: { canvasUri: { type: "string", description: "delete all annotations for a single canvas" } },
|
|
204
|
+
additionalProperties: false
|
|
186
205
|
}
|
|
187
206
|
]
|
|
188
207
|
})
|
|
189
208
|
|
|
209
|
+
// delete either by tag+manifestShortId, or by annotation URI, manifestShortId, canvasUri
|
|
210
|
+
fastify.addSchema({
|
|
211
|
+
$id: makeSchemaUri("routeAnnotationDeleteFilter"),
|
|
212
|
+
oneOf: [
|
|
213
|
+
{ $ref: makeSchemaUri("routeAnnotationFilterTag") },
|
|
214
|
+
{ $ref: makeSchemaUri("routeAnnotationFilter") },
|
|
215
|
+
]
|
|
216
|
+
})
|
|
217
|
+
|
|
190
218
|
////////////////////////////////////////////////////////
|
|
191
219
|
// MANIFESTS ROUTES
|
|
192
220
|
|
|
@@ -211,12 +239,14 @@ function addSchemas(fastify, options, done) {
|
|
|
211
239
|
{
|
|
212
240
|
type: "object",
|
|
213
241
|
required: ["uri"],
|
|
214
|
-
properties: { uri: { type: "string" } }
|
|
242
|
+
properties: { uri: { type: "string" } },
|
|
243
|
+
additionalProperties: false
|
|
215
244
|
},
|
|
216
245
|
{
|
|
217
246
|
type: "object",
|
|
218
247
|
required: ["manifestShortId"],
|
|
219
|
-
properties: { manifestShortId: { type: "string" } }
|
|
248
|
+
properties: { manifestShortId: { type: "string" } },
|
|
249
|
+
additionalProperties: false
|
|
220
250
|
}
|
|
221
251
|
]
|
|
222
252
|
});
|