diff --git a/.prettierignore b/.prettierignore
index b408caa8..28597980 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -16,6 +16,8 @@ tools/render/fixtures
# Written by tools/content-node/snapshot-jsoup/JsoupTables.java from Jsoup's jar.
tools/content-node/jsoup-tables.json
tools/content-node/openapi-snapshot.json
+# Written by tools/contract/generate.ts; generate.spec.ts holds it to that.
+projects/contract/openapi.json
projects/website-angular/content-dist
projects/website-angular/tina/__generated__
projects/website-angular/tina/tina-lock.json
diff --git a/package-lock.json b/package-lock.json
index 2e03d2d6..a8a9a421 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -84,6 +84,7 @@
"ng-packagr": "^21.2.7",
"npm-run-all": "^4.1.1",
"prettier": "^3.9.10",
+ "ts-json-schema-generator": "2.9.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.72.0",
"vite-tsconfig-paths": "^6.1.1",
@@ -6660,55 +6661,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/@npmcli/package-json/node_modules/balanced-match": {
- "version": "4.0.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@npmcli/package-json/node_modules/brace-expansion": {
- "version": "5.0.9",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@npmcli/package-json/node_modules/glob": {
- "version": "13.0.6",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.2",
- "minipass": "^7.1.3",
- "path-scurry": "^2.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@npmcli/package-json/node_modules/minimatch": {
- "version": "10.2.6",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@npmcli/package-json/node_modules/spdx-expression-parse": {
"version": "4.0.0",
"dev": true,
@@ -13966,41 +13918,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/cacache/node_modules/balanced-match": {
- "version": "4.0.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/cacache/node_modules/brace-expansion": {
- "version": "5.0.9",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/cacache/node_modules/glob": {
- "version": "13.0.6",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.2",
- "minipass": "^7.1.3",
- "path-scurry": "^2.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/cacache/node_modules/lru-cache": {
"version": "11.5.2",
"dev": true,
@@ -14009,20 +13926,6 @@
"node": "20 || >=22"
}
},
- "node_modules/cacache/node_modules/minimatch": {
- "version": "10.2.6",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/cacheable": {
"version": "2.5.0",
"dev": true,
@@ -17726,6 +17629,24 @@
"version": "0.0.0",
"license": "MIT"
},
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"license": "ISC",
@@ -17758,6 +17679,45 @@
"dev": true,
"license": "BSD-2-Clause"
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globalthis": {
"version": "1.0.4",
"dev": true,
@@ -25764,6 +25724,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"license": "MIT"
@@ -27695,6 +27665,39 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/ts-json-schema-generator": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.9.0.tgz",
+ "integrity": "sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15",
+ "commander": "^14.0.3",
+ "glob": "^13.0.6",
+ "json5": "^2.2.3",
+ "normalize-path": "^3.0.0",
+ "safe-stable-stringify": "^2.5.0",
+ "tslib": "^2.8.1",
+ "typescript": "^5.9.3"
+ },
+ "bin": {
+ "ts-json-schema-generator": "bin/ts-json-schema-generator.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/ts-json-schema-generator/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/tsconfck": {
"version": "3.1.6",
"dev": true,
diff --git a/package.json b/package.json
index c3cff9ed..cb8464e4 100644
--- a/package.json
+++ b/package.json
@@ -136,6 +136,7 @@
"ng-packagr": "^21.2.7",
"npm-run-all": "^4.1.1",
"prettier": "^3.9.10",
+ "ts-json-schema-generator": "2.9.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.72.0",
"vite-tsconfig-paths": "^6.1.1",
diff --git a/projects/contract/openapi.json b/projects/contract/openapi.json
new file mode 100644
index 00000000..68202b0e
--- /dev/null
+++ b/projects/contract/openapi.json
@@ -0,0 +1,1812 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "Reactome Content Service",
+ "version": "contract",
+ "description": "Generated from the shared contract (projects/contract). A route marked x-contract \"not yet\" is answered by the service but not yet described here."
+ },
+ "paths": {
+ "/ContentService/citation/export": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/citation/pathway/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/citation/static/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/complex/{id}/subunits": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/complexes/{resource}/{identifier}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/content/contributors": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/content/doi": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/content/toc": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/database/name": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/database/version": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/detail/{id}/locationsInPWB": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/discover/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/diseases": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/diseases/doid": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/entity/{id}/componentOf": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/entity/{id}/in-depth": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/entity/{id}/otherForms": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/event/{id}/ancestors": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/event/{id}/in-depth": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/instance/{id}/referrers": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/mapping/{resource}/{identifier}/pathways": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/mapping/{resource}/{identifier}/reactions": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/orthology/{id}/species/{speciesId}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/participants/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/participants/{id}/participatingPhysicalEntities": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/participants/{id}/referenceEntities": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathway/{id}/containedEvents": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathway/{id}/containedEvents/{attribute}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathways/low/diagram/entity/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathways/low/diagram/entity/{id}/allForms": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathways/low/entity/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathways/low/entity/{id}/allForms": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/pathways/top/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/authoredPathways": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/authoredReactions": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/publications": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/reviewedPathways": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/reviewedReactions": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/person/{id}/{attribute}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/enhanced/v2/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/enhanced/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/ids/map": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ },
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/{id}/normalPathway": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/{id}/stId": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/{id}/{attribute}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/model": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}/attributes": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}/count": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}/min": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}/reference": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/schema/{id}/referrals": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/species/all": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/species/main": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/exporter/diagram/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/exporter/event/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/exporter/fireworks/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/exporter/sbml/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/exporter/voronoi/{id}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/psicquic/resources": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecule/enhanced/{id}/details": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecule/{id}/details": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecule/{id}/pathways": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecule/{id}/summary": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecule/{id}/withEntities": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/references/mapping/{identifier}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/references/mapping/{identifier}/xrefs": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/search/diagram/summary": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/search/diagram/{diagram}": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DiagramResult_SearchEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/diagram/{diagram}/occurrences/{instance}": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/search/diagram/{pathwayId}/flag": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/search/facet": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FacetResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/facet_query": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FacetResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/fireworks": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DiagramResult_SearchEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/fireworks/flag": {
+ "get": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/search/icon/facet": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/IconFacetResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/icon/query": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/IconResult"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/icon/{identifier}": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/IconEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/pathways/of/{dbId}": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PathwayOfEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/query": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GroupedResult_SearchEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/query/paginated": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GroupedResult_SearchEntry"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/spellcheck": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/search/suggest": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "The answer.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ContentService/data/orthologies/ids/species/{speciesId}": {
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/data/query/ids": {
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecules/details": {
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/interactors/static/molecules/summary": {
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ },
+ "/ContentService/references/mapping/xrefs": {
+ "post": {
+ "x-contract": "not yet",
+ "responses": {
+ "200": {
+ "description": "Not yet described by the contract."
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "DiagramResult_SearchEntry": {
+ "type": "object",
+ "properties": {
+ "entries": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SearchEntry"
+ },
+ "description": "Left out when there are none."
+ },
+ "facets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FacetCount"
+ },
+ "description": "Left out when there are none."
+ },
+ "found": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "found"
+ ],
+ "additionalProperties": false,
+ "description": "`/search/diagram/{id}` and `/search/fireworks`: the entries, their type facets, Solr's count."
+ },
+ "EpochMillis": {
+ "type": "number",
+ "description": "A date written as milliseconds since the epoch."
+ },
+ "Facet": {
+ "type": "object",
+ "properties": {
+ "selected": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FacetCount"
+ }
+ },
+ "available": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FacetCount"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "FacetCount": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "count": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "count"
+ ],
+ "additionalProperties": false
+ },
+ "FacetResponse": {
+ "type": "object",
+ "properties": {
+ "totalNumFount": {
+ "type": "number"
+ },
+ "speciesFacet": {
+ "$ref": "#/components/schemas/Facet"
+ },
+ "typeFacet": {
+ "$ref": "#/components/schemas/Facet"
+ },
+ "keywordFacet": {
+ "$ref": "#/components/schemas/Facet"
+ },
+ "compartmentFacet": {
+ "$ref": "#/components/schemas/Facet"
+ }
+ },
+ "required": [
+ "totalNumFount"
+ ],
+ "additionalProperties": false
+ },
+ "GroupedResult_SearchEntry": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ResultGroup_SearchEntry"
+ }
+ },
+ "rowCount": {
+ "type": "number"
+ },
+ "numberOfGroups": {
+ "type": "number"
+ },
+ "numberOfMatches": {
+ "type": "number"
+ },
+ "targetResults": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/TargetResult"
+ },
+ "description": "For a search that found nothing: the terms, and whether each is a target."
+ }
+ },
+ "required": [
+ "results",
+ "rowCount",
+ "numberOfGroups",
+ "numberOfMatches"
+ ],
+ "additionalProperties": false,
+ "description": "`/search/query`: the groups, the rows written, the groups and matches Solr counted."
+ },
+ "Highlighted": {
+ "type": "string",
+ "description": "Text Solr may have highlighted: a match is wrapped in `…`, so a reader showing it as plain text escapes it."
+ },
+ "IconCategory": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "count": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "count"
+ ],
+ "additionalProperties": false
+ },
+ "IconEntry": {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string",
+ "const": "Icon"
+ },
+ "type": {
+ "type": "string",
+ "const": "Icon"
+ },
+ "icon": {
+ "type": "boolean",
+ "const": true,
+ "description": "Java's getter: an icon has a name."
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "species": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "summation": {
+ "$ref": "#/components/schemas/Highlighted",
+ "description": "The icon's description. Icons have no inferred one to fall back to."
+ },
+ "iconName": {
+ "type": "string"
+ },
+ "iconCategories": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "iconCuratorName": {
+ "type": "string"
+ },
+ "iconCuratorOrcidId": {
+ "type": "string"
+ },
+ "iconCuratorUrl": {
+ "type": "string"
+ },
+ "iconDesignerName": {
+ "type": "string"
+ },
+ "iconDesignerOrcidId": {
+ "type": "string"
+ },
+ "iconDesignerUrl": {
+ "type": "string"
+ },
+ "iconReferences": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "iconEhlds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "iconPhysicalEntities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "stId": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "compartments": {
+ "type": "string"
+ },
+ "displayName": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "description": "One physical entity an icon draws; an empty part is left out."
+ }
+ },
+ "hasReferenceEntity": {
+ "type": "boolean",
+ "description": "Always false."
+ },
+ "hasEHLD": {
+ "type": "boolean",
+ "description": "Always false."
+ }
+ },
+ "required": [
+ "dbId",
+ "exactType",
+ "hasEHLD",
+ "hasReferenceEntity",
+ "icon",
+ "id",
+ "stId",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "An icon of the icon library."
+ },
+ "IconFacet": {
+ "type": "object",
+ "properties": {
+ "selected": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/IconCategory"
+ }
+ },
+ "available": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/IconCategory"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "IconFacetResponse": {
+ "type": "object",
+ "properties": {
+ "totalNumFount": {
+ "type": "number"
+ },
+ "iconCategoriesFacet": {
+ "$ref": "#/components/schemas/IconFacet"
+ }
+ },
+ "required": [
+ "totalNumFount"
+ ],
+ "additionalProperties": false
+ },
+ "IconResult": {
+ "$ref": "#/components/schemas/ResultGroup_IconEntry",
+ "description": "`/search/icon/query`: the icon library's page, one ungrouped group of icons."
+ },
+ "Long": {
+ "type": "integer",
+ "description": "A Java long, written exactly (`JSON.rawJSON`): it may exceed 2^53, beyond which a JavaScript number reads it rounded.",
+ "format": "int64"
+ },
+ "PathwayOfEntry": {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "hasEHLD": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "dbId",
+ "stId",
+ "id"
+ ],
+ "additionalProperties": false,
+ "description": "What `/search/pathways/of/{dbId}` answers: pathways, written with only the fields the pathway browser asks for (`fields=DB_ID,ST_ID,NAME,HAS_EHLD`), so no type."
+ },
+ "ResultGroup_IconEntry": {
+ "type": "object",
+ "properties": {
+ "entries": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/IconEntry"
+ },
+ "description": "Left out when the group has none (an ungrouped search that found none)."
+ },
+ "typeName": {
+ "type": "string",
+ "description": "Left out for a group Solr names with nothing."
+ },
+ "entriesCount": {
+ "type": "number",
+ "description": "How many Solr found."
+ },
+ "rowCount": {
+ "type": "number",
+ "description": "How many are written here."
+ }
+ },
+ "required": [
+ "entriesCount",
+ "rowCount"
+ ],
+ "additionalProperties": false,
+ "description": "One group of results: a type's, or \"Results\" for a search not grouped."
+ },
+ "ResultGroup_SearchEntry": {
+ "type": "object",
+ "properties": {
+ "entries": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SearchEntry"
+ },
+ "description": "Left out when the group has none (an ungrouped search that found none)."
+ },
+ "typeName": {
+ "type": "string",
+ "description": "Left out for a group Solr names with nothing."
+ },
+ "entriesCount": {
+ "type": "number",
+ "description": "How many Solr found."
+ },
+ "rowCount": {
+ "type": "number",
+ "description": "How many are written here."
+ }
+ },
+ "required": [
+ "entriesCount",
+ "rowCount"
+ ],
+ "additionalProperties": false,
+ "description": "One group of results: a type's, or \"Results\" for a search not grouped."
+ },
+ "SearchEntry": {
+ "description": "One search result.",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Pathway",
+ "Reaction"
+ ]
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "species": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "summation": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "isDisease": {
+ "type": "boolean"
+ },
+ "disease": {
+ "type": "boolean",
+ "description": "`isDisease` again: Java's getter."
+ },
+ "hasReferenceEntity": {
+ "type": "boolean",
+ "description": "Always false: an event has no reference entity."
+ },
+ "hasEHLD": {
+ "type": "boolean",
+ "description": "A pathway's."
+ },
+ "compartmentNames": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "compartmentAccession": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "fireworksSpecies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Only on the species-overview searches."
+ }
+ },
+ "required": [
+ "dbId",
+ "disease",
+ "exactType",
+ "hasReferenceEntity",
+ "id",
+ "isDisease",
+ "species",
+ "stId",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "A pathway or a reaction. A reaction's `exactType` is its category (binding, transition, ...)."
+ },
+ {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string"
+ },
+ "type": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "Protein"
+ },
+ {
+ "type": "string",
+ "const": "DNA Sequence"
+ },
+ {
+ "type": "string",
+ "const": "RNA Sequence"
+ },
+ {
+ "type": "string",
+ "const": "Chemical Compound"
+ },
+ {
+ "type": "string",
+ "const": "Drug"
+ },
+ {
+ "type": "string",
+ "const": "Set"
+ },
+ {
+ "type": "string",
+ "const": "Genes and Transcripts"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "Cell",
+ "ChemicalDrug",
+ "Complex",
+ "OtherEntity",
+ "Polymer",
+ "ProteinDrug",
+ "RNADrug",
+ "SimpleEntity"
+ ],
+ "description": "The physical-entity classes the indexer names a type by: those neither a set nor genome-encoded. Every reference-entity class has a type of its own. entity-types.spec.ts derives the list from the domain snapshot."
+ }
+ ],
+ "description": "The indexer's types for a physical entity or a reference entity (`getType`, `getReferenceTypes`); any other class goes by its own name. The list of those names is checked against the domain snapshot."
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "species": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "\"Entries without species\" when it has none."
+ },
+ "summation": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "isDisease": {
+ "type": "boolean"
+ },
+ "disease": {
+ "type": "boolean"
+ },
+ "hasReferenceEntity": {
+ "type": "boolean"
+ },
+ "compartmentNames": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "compartmentAccession": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "referenceName": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "referenceIdentifier": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "databaseName": {
+ "type": "string"
+ },
+ "referenceURL": {
+ "type": "string"
+ },
+ "fireworksSpecies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "dbId",
+ "exactType",
+ "hasReferenceEntity",
+ "id",
+ "species",
+ "stId",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "A physical entity or a reference entity."
+ },
+ {
+ "$ref": "#/components/schemas/IconEntry"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string",
+ "const": "Person"
+ },
+ "type": {
+ "type": "string",
+ "const": "Person"
+ },
+ "species": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "\"Entries without species\"."
+ },
+ "orcidId": {
+ "type": "string"
+ },
+ "authoredPathways": {
+ "type": "string"
+ },
+ "authoredReactions": {
+ "type": "string"
+ },
+ "reviewedPathways": {
+ "type": "string"
+ },
+ "reviewedReactions": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dbId",
+ "exactType",
+ "id",
+ "species",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "A person, by name. No stId: `id` is the dbId. The counts are text, as Solr stores them."
+ },
+ {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string",
+ "const": "Interactor"
+ },
+ "type": {
+ "type": "string",
+ "const": "Interactor"
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "species": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "referenceName": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "referenceIdentifier": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "databaseName": {
+ "type": "string"
+ },
+ "referenceURL": {
+ "type": "string"
+ },
+ "fireworksSpecies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "dbId",
+ "exactType",
+ "id",
+ "species",
+ "stId",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "A molecule known only from interaction data; `dbId` and `stId` are its accession."
+ },
+ {
+ "type": "object",
+ "properties": {
+ "dbId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "description": "The stId, or the dbId when there is none; never highlighted."
+ },
+ "name": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "exactType": {
+ "type": "string"
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true
+ },
+ "type": {
+ "type": "string"
+ },
+ "stId": {
+ "$ref": "#/components/schemas/Highlighted"
+ },
+ "explanation": {
+ "type": "string"
+ },
+ "date": {
+ "$ref": "#/components/schemas/EpochMillis"
+ },
+ "replacementDbIds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Long"
+ }
+ },
+ "replacementStIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "dbId",
+ "deleted",
+ "exactType",
+ "id",
+ "stId",
+ "type"
+ ],
+ "additionalProperties": false,
+ "description": "An object of an earlier release, deleted since; its type and exactType are those it had."
+ }
+ ]
+ },
+ "TargetResult": {
+ "type": "object",
+ "properties": {
+ "term": {
+ "type": "string"
+ },
+ "resource": {
+ "type": [
+ "string",
+ "number",
+ "boolean"
+ ]
+ },
+ "target": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "additionalProperties": false,
+ "description": "A term the search found nothing for, and whether it names a resource; an empty term or resource is left out."
+ }
+ }
+ }
+}
diff --git a/projects/contract/routes.ts b/projects/contract/routes.ts
new file mode 100644
index 00000000..c0d0583b
--- /dev/null
+++ b/projects/contract/routes.ts
@@ -0,0 +1,30 @@
+// The content service's routes whose answers the contract describes: each
+// "METHOD /path", as the registry has it, to the answer it writes with a 200
+// (spec 016, contracts/module.md). The description is generated from this
+// (tools/contract/generate.ts); a route the registry has and this does not
+// is listed there as not yet in the contract.
+import type {
+ DiagramResult,
+ FacetResponse,
+ GroupedResult,
+ IconEntry,
+ IconFacetResponse,
+ IconResult,
+ PathwayOfEntry,
+ SearchEntry,
+} from './search.ts';
+
+export interface Routes {
+ 'GET /ContentService/search/query': GroupedResult;
+ 'GET /ContentService/search/query/paginated': GroupedResult;
+ 'GET /ContentService/search/facet': FacetResponse;
+ 'GET /ContentService/search/facet_query': FacetResponse;
+ 'GET /ContentService/search/suggest': string[];
+ 'GET /ContentService/search/spellcheck': string[];
+ 'GET /ContentService/search/pathways/of/{dbId}': PathwayOfEntry[];
+ 'GET /ContentService/search/icon/facet': IconFacetResponse;
+ 'GET /ContentService/search/icon/query': IconResult;
+ 'GET /ContentService/search/icon/{identifier}': IconEntry;
+ 'GET /ContentService/search/diagram/{diagram}': DiagramResult;
+ 'GET /ContentService/search/fireworks': DiagramResult;
+}
diff --git a/projects/contract/search.ts b/projects/contract/search.ts
index e2f1cff1..95eca0d1 100644
--- a/projects/contract/search.ts
+++ b/projects/contract/search.ts
@@ -212,7 +212,7 @@ interface DeletedEntry extends Omit {
}
/** One search result. */
-type SearchEntry =
+export type SearchEntry =
EventEntry | EntityEntry | IconEntry | PersonEntry | InteractorEntry | DeletedEntry;
/** Every field any kind of entry has. */
diff --git a/specs/016-api-contract/tasks.md b/specs/016-api-contract/tasks.md
index 7170053b..8a32b1f5 100644
--- a/specs/016-api-contract/tasks.md
+++ b/specs/016-api-contract/tasks.md
@@ -93,12 +93,15 @@ description: 'Tasks for spec 016: one API contract for the content service and t
**Independent test**: [quickstart.md](quickstart.md) §5.
-- [ ] T018 [US4] Add ts-json-schema-generator 2.9.0 as a dev dependency (check the lock's diff by name and version at every depth) and write `tools/contract/generate.ts`.
+- [x] T018 [US4] Add ts-json-schema-generator 2.9.0 as a dev dependency (check the lock's diff by name and version at every depth) and write `tools/contract/generate.ts`.
- It generates the route table's answer types (`discriminatorType: 'open-api'`, `skipTypeCheck: true`) and rewrites the refs to `#/components/schemas/`.
- It writes `projects/contract/openapi.json`.
- `tools/load.spec.ts` gains `'contract'` in `SERVICES`, and lists `generate` under `RUN_ON_LOAD` if it writes when loaded.
-- [ ] T019 [US4] Write `projects/contract/routes.ts`, the route table, as types. `tools/contract/generate.spec.ts` checks it against `tools/content-node/registry.ts` both ways, and marks registry routes not yet in the contract `x-contract: "not yet"`. Show it failing on a row for a route the registry lacks.
-- [ ] T020 [US4] Add the up-to-date check in `tools/contract/generate.spec.ts`: regenerate in memory and compare with the committed file. Show it failing on a contract change made without regenerating.
+ - Done in #707, with T019 and T020 (the generator reads the route table). The lock gains the generator, `commander`, `safe-stable-stringify` and a hoisted `glob`, whose copy of `brace-expansion` resolves to 5.0.12 where the two nested copies had 5.0.9 (in its range). It writes only when run as a script (`import.meta.main`), so loading it does nothing and it is not in `RUN_ON_LOAD`. A generic instance's name (`GroupedResult`) becomes an OpenAPI component name (`GroupedResult_SearchEntry`); the search entry's union is written as `oneOf` of its six kinds (research R4); `Long` is `integer`, `int64`. In 1.7 s, the same bytes on every run. Prettier leaves the file alone (`.prettierignore`); the up-to-date check holds it.
+- [x] T019 [US4] Write `projects/contract/routes.ts`, the route table, as types. `tools/contract/generate.spec.ts` checks it against `tools/content-node/registry.ts` both ways, and marks registry routes not yet in the contract `x-contract: "not yet"`. Show it failing on a row for a route the registry lacks.
+ - Done in #707. `projects/contract/routes.ts` is the interface `Routes`, each `"METHOD /path"` as the registry has it to its 200 answer: twelve search routes. The description lists all 90 the service serves, 78 marked `x-contract: "not yet"`. A row for a route the registry lacks failed the check.
+- [x] T020 [US4] Add the up-to-date check in `tools/contract/generate.spec.ts`: regenerate in memory and compare with the committed file. Show it failing on a contract change made without regenerating.
+ - Done in #707. A contract field renamed without regenerating failed it.
- [ ] T021 [US4] Add ajv 8.20.0 as a dev dependency and `tools/api-types/schema.spec.ts`, which validates the recorded answers of contract routes against `openapi.json`.
- Use ajv 2020-12, with `discriminator: true` where a type has one, and register the `int64` format.
- Read exact longs with a JSON.parse reviver.
diff --git a/tools/contract/generate.spec.ts b/tools/contract/generate.spec.ts
new file mode 100644
index 00000000..438f36b5
--- /dev/null
+++ b/tools/contract/generate.spec.ts
@@ -0,0 +1,55 @@
+// @vitest-environment node
+//
+// The API description (spec 016, US4): generated from the contract, its route
+// table held to the service's registry, and the committed file current.
+import { readFileSync } from 'node:fs';
+import { describe, expect, it } from 'vitest';
+import { DESCRIPTION, componentName, generate, servedRoutes, written } from './generate.ts';
+
+/** The contract's route table's keys, as the routes module declares them. */
+function contractRoutes(source: string): string[] {
+ return [...source.matchAll(/^ {2}'((?:GET|POST) [^']+)':/gm)].map((match) => match[1] ?? '');
+}
+
+const ROUTES = readFileSync(new URL('../../projects/contract/routes.ts', import.meta.url), 'utf8');
+
+describe('the API description', () => {
+ it('describes only routes the service serves', () => {
+ const served = new Set(servedRoutes());
+ expect(contractRoutes(ROUTES).filter((route) => !served.has(route))).toEqual([]);
+ });
+
+ it('lists every served route, the ones the contract lacks marked not yet', () => {
+ const document = generate();
+ const described = new Set(contractRoutes(ROUTES));
+ for (const route of servedRoutes()) {
+ const [method = '', where = ''] = route.split(' ');
+ const operation = document.paths[where]?.[method.toLowerCase()];
+ expect(operation, route).toBeDefined();
+ expect(operation?.['x-contract'], route).toBe(described.has(route) ? undefined : 'not yet');
+ }
+ });
+
+ it('is the committed file: regenerate it with `node tools/contract/generate.ts`', () => {
+ expect(readFileSync(DESCRIPTION, 'utf8')).toBe(written());
+ });
+
+ it('writes the search entry as exactly one of its kinds', () => {
+ const entry = generate().components.schemas['SearchEntry'] as {
+ oneOf?: unknown[];
+ anyOf?: unknown;
+ };
+ expect(entry.anyOf).toBeUndefined();
+ expect(entry.oneOf).toHaveLength(6);
+ });
+
+ it('names a generic instance as OpenAPI allows', () => {
+ expect(componentName('GroupedResult')).toBe('GroupedResult_SearchEntry');
+ expect(componentName('Map>')).toBe(
+ 'Map_string_ResultGroup_IconEntry'
+ );
+ expect(Object.keys(generate().components.schemas).every((name) => /^[\w.-]+$/.test(name))).toBe(
+ true
+ );
+ });
+});
diff --git a/tools/contract/generate.ts b/tools/contract/generate.ts
new file mode 100644
index 00000000..56f0beaa
--- /dev/null
+++ b/tools/contract/generate.ts
@@ -0,0 +1,117 @@
+/**
+ * The API description, generated from the contract (spec 016, US4): an
+ * OpenAPI 3.1 document of every route the content service answers, with the
+ * answer the contract has for it, and the rest listed as not yet in the
+ * contract. Run as a script, it writes projects/contract/openapi.json;
+ * generate.spec.ts checks the committed file is what this makes.
+ *
+ * node tools/contract/generate.ts
+ */
+import { writeFileSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createGenerator } from 'ts-json-schema-generator';
+import { endpoints } from '../content-node/registry.ts';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+export const DESCRIPTION = path.join(ROOT, 'projects/contract/openapi.json');
+
+type Schema = { [key: string]: unknown };
+
+/**
+ * A component name OpenAPI accepts (`^[a-zA-Z0-9._-]+$`): the generator names
+ * an instance of a generic type `GroupedResult`.
+ */
+export const componentName = (name: string) => name.replace(/[<>, ]+/g, '_').replace(/_+$/, '');
+
+/** Every `$ref` in a schema, rewritten to a component, by name. */
+function rewriteRefs(value: unknown): unknown {
+ if (Array.isArray(value)) return value.map(rewriteRefs);
+ if (value === null || typeof value !== 'object') return value;
+ const out: Schema = {};
+ for (const [key, inner] of Object.entries(value as Schema))
+ out[key] =
+ key === '$ref' && typeof inner === 'string'
+ ? `#/components/schemas/${componentName(decodeURIComponent(inner.replace('#/definitions/', '')))}`
+ : rewriteRefs(inner);
+ return out;
+}
+
+/** The registry's routes, as "METHOD /path". */
+export function servedRoutes(): string[] {
+ return endpoints.map((each) => `${(each.method ?? 'get').toUpperCase()} ${each.path}`);
+}
+
+/** The contract's route table: "METHOD /path" to its answer's schema, and the shared definitions. */
+function contractSchemas() {
+ const schema = createGenerator({
+ path: path.join(ROOT, 'projects/contract/routes.ts'),
+ tsconfig: path.join(ROOT, 'tools/tsconfig.json'),
+ type: 'Routes',
+ // The type checks have run already (check:types); the generator's own
+ // would repeat them, slower.
+ skipTypeCheck: true,
+ discriminatorType: 'open-api',
+ expose: 'export',
+ topRef: true,
+ jsDoc: 'extended',
+ additionalProperties: false,
+ }).createSchema('Routes');
+ const definitions = { ...(schema.definitions ?? {}) } as Record;
+ const table = definitions['Routes'] as { properties: Record };
+ delete definitions['Routes'];
+ // The search entry's kinds share no single tag (a deleted entry keeps the
+ // type it had), so it has no discriminator, and the generator writes its
+ // union as anyOf, which accepts an entry matching two kinds. Exactly one.
+ const entry = definitions['SearchEntry'];
+ if (entry?.['anyOf']) {
+ entry['oneOf'] = entry['anyOf'];
+ delete entry['anyOf'];
+ }
+ return { table: table.properties, definitions };
+}
+
+/** The OpenAPI document, as an object. */
+export function generate() {
+ const { table, definitions } = contractSchemas();
+ const paths: Record> = {};
+ const served = servedRoutes();
+ for (const route of [...new Set([...served, ...Object.keys(table)])].sort()) {
+ const [method = '', where = ''] = route.split(' ');
+ const answer = table[route];
+ (paths[where] ??= {})[method.toLowerCase()] = answer
+ ? {
+ responses: {
+ '200': {
+ description: 'The answer.',
+ content: { 'application/json': { schema: rewriteRefs(answer) } },
+ },
+ },
+ }
+ : {
+ 'x-contract': 'not yet',
+ responses: { '200': { description: 'Not yet described by the contract.' } },
+ };
+ }
+ const schemas = Object.fromEntries(
+ Object.entries(definitions)
+ .map(([name, schema]) => [componentName(name), rewriteRefs(schema)] as const)
+ .sort(([a], [b]) => a.localeCompare(b))
+ );
+ return {
+ openapi: '3.1.1',
+ info: {
+ title: 'Reactome Content Service',
+ version: 'contract',
+ description:
+ 'Generated from the shared contract (projects/contract). A route marked x-contract "not yet" is answered by the service but not yet described here.',
+ },
+ paths,
+ components: { schemas },
+ };
+}
+
+/** The document as written: two-space JSON and a final newline. */
+export const written = () => `${JSON.stringify(generate(), null, 2)}\n`;
+
+if (import.meta.main) writeFileSync(DESCRIPTION, written());
diff --git a/tools/load.spec.ts b/tools/load.spec.ts
index f2d4f1b1..733d5637 100644
--- a/tools/load.spec.ts
+++ b/tools/load.spec.ts
@@ -24,6 +24,7 @@ const SERVICES = [
'analysis-types',
'java-parity',
'library-update',
+ 'contract',
];
/**