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/knip.json b/knip.json index f59e0beb..d550a6d2 100644 --- a/knip.json +++ b/knip.json @@ -19,7 +19,8 @@ "tools/analysis-types/record.ts", "tools/svg-export-harness/{harness,harness.check,playwright.config}.ts", "tools/java-parity/parity.ts", - "tools/library-update/check-library-update.ts" + "tools/library-update/check-library-update.ts", + "projects/contract/routes.ts" ], "project": [ "src/**/*.ts", 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..57388cfe --- /dev/null +++ b/projects/contract/openapi.json @@ -0,0 +1,2496 @@ +{ + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/citation/static/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/complex/{id}/subunits": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/complexes/{resource}/{identifier}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "resource", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/discover/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/entity/{id}/in-depth": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/entity/{id}/otherForms": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/event/{id}/ancestors": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/event/{id}/in-depth": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/instance/{id}/referrers": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/mapping/{resource}/{identifier}/pathways": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "resource", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/mapping/{resource}/{identifier}/reactions": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "resource", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/orthology/{id}/species/{speciesId}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "speciesId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/participants/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/participants/{id}/participatingPhysicalEntities": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/participants/{id}/referenceEntities": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathway/{id}/containedEvents": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathway/{id}/containedEvents/{attribute}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attribute", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathways/low/diagram/entity/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathways/low/diagram/entity/{id}/allForms": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathways/low/entity/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathways/low/entity/{id}/allForms": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/pathways/top/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/authoredPathways": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/authoredReactions": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/publications": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/reviewedPathways": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/reviewedReactions": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/person/{id}/{attribute}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attribute", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/query/enhanced/v2/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/query/enhanced/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/query/{id}/normalPathway": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/query/{id}/stId": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/query/{id}/{attribute}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attribute", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/schema/{id}/attributes": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/schema/{id}/count": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/schema/{id}/min": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/schema/{id}/reference": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/data/schema/{id}/referrals": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/exporter/event/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/exporter/fireworks/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/exporter/sbml/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/exporter/voronoi/{id}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/interactors/static/molecule/{id}/details": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/interactors/static/molecule/{id}/pathways": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/interactors/static/molecule/{id}/summary": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/interactors/static/molecule/{id}/withEntities": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/references/mapping/{identifier}": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/references/mapping/{identifier}/xrefs": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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": { + "parameters": [ + { + "name": "diagram", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "diagram", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "instance", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Not yet described by the contract." + } + } + } + }, + "/ContentService/search/diagram/{pathwayId}/flag": { + "get": { + "x-contract": "not yet", + "parameters": [ + { + "name": "pathwayId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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": { + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The answer.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IconEntry" + } + } + } + } + } + } + }, + "/ContentService/search/pathways/of/{dbId}": { + "get": { + "parameters": [ + { + "name": "dbId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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", + "parameters": [ + { + "name": "speciesId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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/contracts/module.md b/specs/016-api-contract/contracts/module.md index be1fcb8c..d41b6f11 100644 --- a/specs/016-api-contract/contracts/module.md +++ b/specs/016-api-contract/contracts/module.md @@ -31,6 +31,6 @@ projects/contract/ - **The format.** It is OpenAPI 3.1.1, generated by a script under `tools/` (TypeScript, spec 013) with ts-json-schema-generator 2.9.0 (`discriminatorType: 'open-api'`, `skipTypeCheck: true`: the contract is type-checked by `tools/tsconfig.json`, whose `include` gains `../projects/contract/**/*.ts`, so a module no reader imports yet is still checked). The search entry's union is rewritten from `anyOf` to `oneOf` (research R4). - **`components.schemas`.** These are the generator's definitions for the answer types the route table names, with refs rewritten from `#/definitions/` to `#/components/schemas/`. -- **`paths`.** There is one per route in the route table: its method, and a `200` response with the answer type's `$ref`. A route the service serves that is not yet in the contract is listed with the extension `x-contract: "not yet"` and no response schema, so nothing presents an unchecked generic object as checked. +- **`paths`.** There is one per route the service serves: its method, its path variables, and for a route in the route table a `200` response with the answer's schema (a `$ref` where the answer is a named type). A route the service serves that is not yet in the contract is listed with the extension `x-contract: "not yet"` and no response schema, so nothing presents an unchecked generic object as checked. - **What it does not hold.** Parameters stay in Java's `tools/content-node/openapi-snapshot.json` (spec 011's cache labelling) until a later task merges them in. The site's API page keeps reading Java's live description until the service publishes this one. - **When it is regenerated.** It is regenerated in the same change as any contract change; the up-to-date check fails otherwise. diff --git a/specs/016-api-contract/tasks.md b/specs/016-api-contract/tasks.md index 7170053b..ab3e4f87 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 #708, 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 #708. `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 91 routes the service serves (90 paths; one is both GET and POST), 79 marked `x-contract: "not yet"`, each templated path's variables declared as OpenAPI requires (from #707's review; Redocly's spec rules then pass). Component names that would clash once made valid fail the generator; the order is by code unit, the same in every locale. 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 #708. 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..4e9fd2f5 --- /dev/null +++ b/tools/contract/generate.spec.ts @@ -0,0 +1,70 @@ +// @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, + components, + contractRoutes, + generate, + servedRoutes, + written, +} from './generate.ts'; + +describe('the API description', () => { + it('describes only routes the service serves', () => { + const served = new Set(servedRoutes()); + expect(contractRoutes().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()); + 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('declares every templated path variable, as OpenAPI requires', () => { + for (const [where, operations] of Object.entries(generate().paths)) + for (const operation of Object.values(operations)) { + const declared = ((operation['parameters'] ?? []) as { name: string }[]).map((p) => p.name); + const templated = [...where.matchAll(/\{([^}]+)\}/g)].map(([, name]) => name); + expect(declared, where).toEqual(templated); + } + }); + + it('refuses two types whose component names would clash', () => { + expect(() => components({ 'A': {}, 'A': {} })).toThrow(/clash: A_B_C/); + expect(Object.keys(components({ 'B': {}, A: {} }))).toEqual(['A', 'B_X']); + }); + + 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..ea5ce603 --- /dev/null +++ b/tools/contract/generate.ts @@ -0,0 +1,149 @@ +/** + * 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 route table's routes, as "METHOD /path": what the contract describes. */ +export const contractRoutes = () => Object.keys(contractSchemas().table); + +/** Code-unit order, the same in every locale (localeCompare is not). */ +const byCodeUnit = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0); + +/** + * A templated path's variables, as OpenAPI requires them declared: text, as + * the service reads them (an id may be a stable id or a dbId). + */ +const pathParameters = (where: string) => + [...where.matchAll(/\{([^}]+)\}/g)].map(([, name]) => ({ + name, + in: 'path', + required: true, + schema: { type: 'string' }, + })); + +/** + * The definitions as components, by their valid names, in code-unit order. + * Two types named alike once made valid (A and A) would leave one + * schema in place of two, and nothing would say so: that fails here. + */ +export function components(definitions: Record): Record { + const named = Object.entries(definitions).map( + ([name, schema]) => [componentName(name), rewriteRefs(schema)] as const + ); + const clashes = named.filter(([name], at) => named.findIndex(([other]) => other === name) !== at); + if (clashes.length) + throw new Error(`component names clash: ${clashes.map(([name]) => name).join(', ')}`); + return Object.fromEntries(named.sort(([a], [b]) => byCodeUnit(a, b))); +} + +/** 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(byCodeUnit)) { + const [method = '', where = ''] = route.split(' '); + const answer = table[route]; + const parameters = pathParameters(where); + (paths[where] ??= {})[method.toLowerCase()] = answer + ? { + ...(parameters.length ? { parameters } : {}), + responses: { + '200': { + description: 'The answer.', + content: { 'application/json': { schema: rewriteRefs(answer) } }, + }, + }, + } + : { + 'x-contract': 'not yet', + ...(parameters.length ? { parameters } : {}), + responses: { '200': { description: 'Not yet described by the contract.' } }, + }; + } + const schemas = components(definitions); + 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', ]; /**