From 64f23ab82061138e3dd9cceab5b46497a1efccab Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 13 Mar 2026 13:39:02 -0500 Subject: [PATCH 01/37] Add generate-openapi script to create api docs --- api/src/controllers/person.js | 72 + package-lock.json | 1474 ++++++++++++++++++- package.json | 6 + scripts/generate-openapi.js | 120 ++ shared-libs/cht-datasource/src/input.ts | 1 - shared-libs/cht-datasource/src/libs/core.ts | 12 +- 6 files changed, 1657 insertions(+), 28 deletions(-) create mode 100644 scripts/generate-openapi.js diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index b40d250cbee..458813113ff 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -9,8 +9,51 @@ const getPage = ctx.bind(Person.v1.getPage); const createPerson = ctx.bind(Person.v1.create); const updatePerson = ctx.bind(Person.v1.update); +/** + * @openapi + * tags: + * - name: Person + * description: Operations for person contacts + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/person/{uuid}: + * get: + * summary: Get a person by UUID + * operationId: getPersonByUuid + * description: Returns a person contact record. Optionally includes the full parent place lineage. + * tags: + * - Person + * parameters: + * - in: path + * name: uuid + * required: true + * schema: + * type: string + * description: The UUID of the person to retrieve + * - in: query + * name: with_lineage + * schema: + * type: string + * enum: + * - 'true' + * description: When set to 'true', includes the full parent place lineage + * responses: + * '200': + * description: The person record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Person' + * '404': + * description: Person not found + * '401': + * description: Not authenticated + * '403': + * description: Insufficient permissions (requires can_view_contacts) + */ get: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); const { params: { uuid }, query: { with_lineage } } = req; @@ -29,6 +72,35 @@ module.exports = { return res.json(docs); }), + /** + * @openapi + * /api/v1/person: + * post: + * summary: Create a new person + * operationId: createPerson + * description: Creates a new person contact record. + * tags: + * - Person + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.PersonInput' + * responses: + * '200': + * description: The created person record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Person' + * '400': + * description: Invalid input (missing required fields, invalid types, etc.) + * '401': + * description: Not authenticated + * '403': + * description: Insufficient permissions (requires can_create_people or can_edit) + */ create: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_create_people', 'can_edit'] }); const personDoc = await createPerson(req.body); diff --git a/package-lock.json b/package-lock.json index 5103346c2b0..5c05a9db0d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,9 @@ "@medic/translation-checker": "^1.1.0", "@octokit/core": "^5.2.0", "@octokit/plugin-paginate-graphql": "^4.0.1", + "@stoplight/spectral-core": "^1.21.0", + "@stoplight/spectral-parsers": "^1.0.5", + "@stoplight/spectral-rulesets": "^1.22.0", "@stylistic/eslint-plugin": "^5.1.0", "@tsconfig/node22": "^22.0.2", "@types/chai": "^4.3.6", @@ -168,7 +171,9 @@ "shellcheck": "^4.1.0", "sinon": "^21.0.1", "sinon-chai": "^3.7.0", + "swagger-jsdoc": "^6.2.8", "tail": "^2.2.6", + "ts-json-schema-generator": "^2.9.0", "ts-node": "^10.9.2", "typedoc": "^0.28.7", "typescript": "5.7.3", @@ -454,6 +459,74 @@ "ajv": ">=8" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@appium/base-driver": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@appium/base-driver/-/base-driver-10.2.0.tgz", @@ -1303,6 +1376,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asyncapi/specs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.11.1.tgz", + "integrity": "sha512-A3WBLqAKGoJ2+6FWFtpjBlCQ1oFCcs4GxF7zsIGvNqp/klGUHjlA3aAcZ9XMMpLGE8zPeYDz2x9FmO6DSuKraQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -5552,6 +5635,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jsdoc/salty": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", @@ -5564,6 +5654,45 @@ "node": ">=v12.0.0" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@mdn/browser-compat-data": { "version": "5.7.6", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz", @@ -6494,6 +6623,358 @@ "dev": true, "license": "MIT" }, + "node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json/node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/json/node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.21.0.tgz", + "integrity": "sha512-oj4e/FrDLUhBRocIW+lRMKlJ/q/rDZw61HkLbTFsdMd+f/FTkli2xHNB1YC6n1mrMKjjvy7XlUuFkC7XxtgbWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.17.1", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "jsonpath-plus": "^10.3.0", + "lodash": "~4.17.23", + "lodash.topath": "^4.5.2", + "minimatch": "3.1.2", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "simple-eval": "1.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", + "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.2", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", + "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.17.1", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", + "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^6.8.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-functions": "^1.9.1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@types/json-schema": "^7.0.7", + "ajv": "^8.17.1", + "ajv-formats": "~2.1.1", + "json-schema-traverse": "^1.0.0", + "leven": "3.1.0", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", + "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "abort-controller": "^3.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.1.0.tgz", @@ -6730,6 +7211,16 @@ "@types/ms": "*" } }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -7164,6 +7655,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/urijs": { + "version": "1.19.26", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", + "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", @@ -11384,6 +11882,31 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -11691,6 +12214,49 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/appium-uiautomator2-driver/node_modules/@appium/docutils": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@appium/docutils/-/docutils-2.2.1.tgz", + "integrity": "sha512-mhyQuvQUGepH+MEOGt3ixTM5q1NNVsxr+jvz/6t7KFJm8ElRlfyGGWcA3fqvnXm72hm3rzhh2t13sLct7qWUBg==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@appium/support": "^7.0.5", + "consola": "3.4.2", + "diff": "8.0.3", + "lilconfig": "3.1.3", + "lodash": "4.17.23", + "package-directory": "8.1.0", + "read-pkg": "10.0.0", + "teen_process": "4.0.8", + "type-fest": "5.4.1", + "yaml": "2.8.2", + "yargs": "18.0.0", + "yargs-parser": "22.0.0" + }, + "bin": { + "appium-docs": "bin/appium-docs.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/@appium/docutils/node_modules/teen_process": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-4.0.8.tgz", + "integrity": "sha512-0DTX2KfgVOr6+8TVmheEdiJHZ/bPOPeJuX0yvv5VOX3x+OFteNkmWkI+hX6zTkzxjddrktsrXkacfS2Gom1YyA==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "shell-quote": "^1.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/@appium/logger": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-2.0.4.tgz", @@ -11865,6 +12431,16 @@ "node": ">=0.1.90" } }, + "node_modules/appium-uiautomator2-driver/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -12304,6 +12880,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/appium-uiautomator2-driver/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "extraneous": true, + "license": "Python-2.0" + }, "node_modules/appium-uiautomator2-driver/node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -12545,6 +13128,92 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/appium-uiautomator2-driver/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "extraneous": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/appium-uiautomator2-driver/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -12602,6 +13271,16 @@ "node": ">= 14" } }, + "node_modules/appium-uiautomator2-driver/node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -12799,6 +13478,16 @@ "license": "MIT", "optional": true }, + "node_modules/appium-uiautomator2-driver/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "extraneous": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -12901,6 +13590,16 @@ "node": ">= 0.4" } }, + "node_modules/appium-uiautomator2-driver/node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -13204,6 +13903,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/appium-uiautomator2-driver/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "extraneous": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -13358,6 +14080,16 @@ "license": "MIT", "optional": true }, + "node_modules/appium-uiautomator2-driver/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -13765,6 +14497,19 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/appium-uiautomator2-driver/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/lockfile": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", @@ -13919,6 +14664,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/appium-uiautomator2-driver/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -14113,6 +14868,22 @@ "wrappy": "1" } }, + "node_modules/appium-uiautomator2-driver/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/p-limit": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", @@ -15153,6 +15924,13 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/appium-uiautomator2-driver/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "extraneous": true, + "license": "0BSD" + }, "node_modules/appium-uiautomator2-driver/node_modules/type-fest": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.1.tgz", @@ -15293,6 +16071,24 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -15312,6 +16108,60 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -15361,6 +16211,101 @@ "node": ">=0.6.0" } }, + "node_modules/appium-uiautomator2-driver/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "extraneous": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "extraneous": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/appium-uiautomator2-driver/node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/appium-uiautomator2-driver/node_modules/yargs/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/appium-uiautomator2-driver/node_modules/yauzl": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", @@ -16044,6 +16989,16 @@ "node": ">=4" } }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -17394,6 +18349,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -20666,6 +21628,16 @@ "node": ">= 0.8" } }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -21492,27 +22464,27 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -21524,21 +22496,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -21547,7 +22522,30 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -23808,6 +24806,13 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -24180,11 +25185,18 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { @@ -25793,6 +26805,17 @@ "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", "dev": true }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", @@ -26440,6 +27463,18 @@ "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", "dev": true }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", @@ -26682,12 +27717,12 @@ } }, "node_modules/is-weakref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", - "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -27786,6 +28821,16 @@ "node": ">= 12" } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -27997,6 +29042,25 @@ "node >= 0.2.0" ] }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -29477,6 +30541,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -30996,6 +32067,26 @@ "node": ">=0.10.0" } }, + "node_modules/nimma": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -31722,6 +32813,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -32745,6 +33844,16 @@ "node": ">=4" } }, + "node_modules/pony-cause": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -36460,6 +37569,19 @@ } ] }, + "node_modules/simple-eval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsep": "^1.3.6" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/simple-fmt": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", @@ -36999,6 +38121,19 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -37526,6 +38661,106 @@ "node": ">=16" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -38224,6 +39459,137 @@ "typescript": ">=4.0.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/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/ts-json-schema-generator/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/ts-json-schema-generator/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/ts-json-schema-generator/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/ts-json-schema-generator/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-json-schema-generator/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-json-schema-generator/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -39051,6 +40417,13 @@ "node": ">=6" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/url": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", @@ -39151,6 +40524,16 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -40094,15 +41477,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -41114,6 +42498,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/z-schema/node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 5b88335aadd..9514fe32f89 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dev-sentinel": "npm run --prefix shared-libs/cht-datasource build-watch & npm run --prefix sentinel run-watch", "local-images": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && node scripts/build/cli localDockerComposeFiles", "update-service-worker": "node scripts/build/cli updateServiceWorker", + "generate-openapi": "node ./scripts/generate-openapi.js", "-- DEV TEST SCRIPTS": "-----------------------------------------------------------------------------------------------", "integration-all-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-all", "integration-replication": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-replication", @@ -93,6 +94,9 @@ "@medic/translation-checker": "^1.1.0", "@octokit/core": "^5.2.0", "@octokit/plugin-paginate-graphql": "^4.0.1", + "@stoplight/spectral-core": "^1.21.0", + "@stoplight/spectral-parsers": "^1.0.5", + "@stoplight/spectral-rulesets": "^1.22.0", "@stylistic/eslint-plugin": "^5.1.0", "@tsconfig/node22": "^22.0.2", "@types/chai": "^4.3.6", @@ -182,7 +186,9 @@ "shellcheck": "^4.1.0", "sinon": "^21.0.1", "sinon-chai": "^3.7.0", + "swagger-jsdoc": "^6.2.8", "tail": "^2.2.6", + "ts-json-schema-generator": "^2.9.0", "ts-node": "^10.9.2", "typedoc": "^0.28.7", "typescript": "5.7.3", diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js new file mode 100644 index 00000000000..846a3f67fec --- /dev/null +++ b/scripts/generate-openapi.js @@ -0,0 +1,120 @@ +const path = require('node:path'); +const fs = require('node:fs'); +const swaggerJsdoc = require('swagger-jsdoc'); +const tsj = require('ts-json-schema-generator'); +const { Spectral, Document } = require('@stoplight/spectral-core'); +const Parsers = require('@stoplight/spectral-parsers'); +const { oas } = require('@stoplight/spectral-rulesets'); +const { version } = require('../package.json'); + +const DATASOURCE_DIR = path.resolve(__dirname, '../shared-libs/cht-datasource/src'); +const TSCONFIG = path.resolve(__dirname, '../shared-libs/cht-datasource/tsconfig.build.json'); + +const TYPE_SOURCES = [ + 'person.ts', + 'input.ts', +].map(file => path.join(DATASOURCE_DIR, file)); + +const TSJ_OPTIONS = { + tsconfig: TSCONFIG, + type: '*', + skipTypeCheck: true, + discriminatorType: 'open-api', + functions: 'hide', +}; + +const SWAGGER_OPTIONS = { + failOnErrors: true, + definition: { + openapi: '3.1.0', + info: { + title: 'CHT API', + version: version, + description: 'API for interacting with the Community Health Toolkit', + contact: { + name: 'Medic', + email: 'hello@medic.org', + url: 'https://forum.communityhealthtoolkit.org/' + }, + license: { + name: 'AGPL-3.0', + url: 'https://www.gnu.org/licenses/agpl-3.0.html' + } + }, + servers: [{ url: '/' }], + components: { schemas: {} }, + }, + apis: [path.resolve(__dirname, '../api/src/controllers/**/*.js')], +}; + +const SPECTRAL_OPTIONS = { extends: [[oas, 'all']], rules: {} }; + +const transformSchema = (obj) => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(transformSchema); + } + + const result = {}; + for (const [key, value] of Object.entries(obj)) { + if (key === '$ref') { + result[key] = value.replace('#/definitions/', '#/components/schemas/'); + continue; + } + result[key] = transformSchema(value); + } + + // Simplify complex additionalProperties to just `true` since these are open-ended document types + if (result.additionalProperties) { + result.additionalProperties = true; + } + + return result; +}; + +const generateTsSchemas = () => { + const schemas = {}; + TYPE_SOURCES + .map(path => ({ ...TSJ_OPTIONS, path })) + .map(tsj.createGenerator) + .map(generator => generator.createSchema()) + .map(({ definitions }) => ({ ...definitions, '*': undefined })) + .forEach((definitions) => Object.assign(schemas, definitions)); + return transformSchema(schemas); +}; + +const DIAGNOSTIC_SEVERITY = ['error', 'warn', 'info', 'hint']; + +const lintSpec = async (spec) => { + const spectral = new Spectral(); + spectral.setRuleset(SPECTRAL_OPTIONS); + const doc = new Document(JSON.stringify(spec), Parsers.Json, '/openapi.json'); + const results = await spectral.run(doc); + + results.forEach(({ + severity, code, message, path + }) => console.log(` [${DIAGNOSTIC_SEVERITY[severity]}] ${code}: ${message} (at ${path.join('.')})`)); + + const errors = results.filter(r => r.severity === 0); + if (errors.length > 0) { + throw new Error(`OpenAPI spec has ${errors.length} validation error(s)`); + } + // TODO Consider failiing for warnings +}; + +const main = async () => { + const swaggerSpec = swaggerJsdoc(SWAGGER_OPTIONS); + const tsSchemas = generateTsSchemas(); + Object.assign(swaggerSpec.components.schemas, tsSchemas); + await lintSpec(swaggerSpec); + const outputPath = path.resolve(__dirname, '../build/openapi.json'); + fs.writeFileSync(outputPath, JSON.stringify(swaggerSpec, null, 2) + '\n'); +}; + +main() + .catch((err) => { + console.error(err.message); + process.exit(1); + }); diff --git a/shared-libs/cht-datasource/src/input.ts b/shared-libs/cht-datasource/src/input.ts index a14af3311c4..6f40998dda1 100644 --- a/shared-libs/cht-datasource/src/input.ts +++ b/shared-libs/cht-datasource/src/input.ts @@ -11,7 +11,6 @@ export namespace v1 { */ export interface ContactInput extends DataObject { readonly type: string - readonly contact_type?: string readonly name: string readonly reported_date?: DateTimeString | number readonly _id?: never diff --git a/shared-libs/cht-datasource/src/libs/core.ts b/shared-libs/cht-datasource/src/libs/core.ts index 34ab3b62e66..59e0f4efa5c 100644 --- a/shared-libs/cht-datasource/src/libs/core.ts +++ b/shared-libs/cht-datasource/src/libs/core.ts @@ -52,7 +52,9 @@ const isDataArray = (value: unknown): value is DataArray => { return Array.isArray(value) && value.every(v => isDataPrimitive(v) || isDataArray(v) || isDataObject(v)); }; -/** @internal */ +/** + * A data object. + */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface DataObject extends Readonly> { } @@ -175,7 +177,9 @@ export function assertHasRequiredField , N ext } } -/** @internal */ +/** + * An identifiable entity. + */ export interface Identifiable extends DataObject { readonly _id: string } @@ -222,7 +226,9 @@ export const getPagedGenerator = async function* ( return null; }; -/** @internal */ +/** + * Parent lineage data for an entity. + */ export interface NormalizedParent extends DataObject, Identifiable { readonly parent?: NormalizedParent; } From 15ad52fc73f8690db9c320f307336a9ad7c8bfe0 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 13 Mar 2026 14:10:56 -0500 Subject: [PATCH 02/37] Move generated docs to cht-datasource/docs so they get pushed to GitHub pages --- .github/workflows/build.yml | 4 +++- scripts/generate-openapi.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e37607658f3..03993bbd044 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -416,8 +416,10 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} - run: npm ci - - name: Generate TypeDoc + - name: Generate cht-datasource TypeDoc run: npm run --prefix shared-libs/cht-datasource gen-docs + - name: Generate openapi.json + run: npm run generate-openapi - name: Main Branch Only - Deploy to GH pages uses: peaceiris/actions-gh-pages@v4 if: github.ref == 'refs/heads/master' diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 846a3f67fec..7f2879b8e4f 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -101,7 +101,7 @@ const lintSpec = async (spec) => { if (errors.length > 0) { throw new Error(`OpenAPI spec has ${errors.length} validation error(s)`); } - // TODO Consider failiing for warnings + // TODO Consider failing for warnings }; const main = async () => { @@ -109,7 +109,8 @@ const main = async () => { const tsSchemas = generateTsSchemas(); Object.assign(swaggerSpec.components.schemas, tsSchemas); await lintSpec(swaggerSpec); - const outputPath = path.resolve(__dirname, '../build/openapi.json'); + // TODO Currently publishing with cht-datasource docs site. + const outputPath = path.resolve(__dirname, '../shared-libs/cht-datasource/docs/openapi.json'); fs.writeFileSync(outputPath, JSON.stringify(swaggerSpec, null, 2) + '\n'); }; From dc305602e16746cfa781d9b10e8abb9fc1b6058c Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Mon, 16 Mar 2026 15:24:16 -0500 Subject: [PATCH 03/37] More updates to current docs --- api/src/controllers/person.js | 82 +++++++++++++++++++++++++++-------- scripts/generate-openapi.js | 10 ++++- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 458813113ff..9a99e5904ca 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -19,26 +19,28 @@ module.exports = { v1: { /** * @openapi - * /api/v1/person/{uuid}: + * /api/v1/person/{id}: * get: - * summary: Get a person by UUID - * operationId: getPersonByUuid + * summary: Get a person by id + * operationId: v1PersonIdGet * description: Returns a person contact record. Optionally includes the full parent place lineage. * tags: * - Person + * x-since: 4.9.0 + * x-permissions: + * hasAll: [can_view_contacts] * parameters: * - in: path - * name: uuid + * name: id * required: true * schema: * type: string - * description: The UUID of the person to retrieve + * description: The id of the person to retrieve * - in: query * name: with_lineage * schema: * type: string - * enum: - * - 'true' + * enum: ['true'] * description: When set to 'true', includes the full parent place lineage * responses: * '200': @@ -46,13 +48,15 @@ module.exports = { * content: * application/json: * schema: - * $ref: '#/components/schemas/v1.Person' - * '404': - * description: Person not found + * oneOf: + * - $ref: '#/components/schemas/v1.Person' + * - $ref: '#/components/schemas/v1.PersonWithLineage' * '401': - * description: Not authenticated + * $ref: '#/components/responses/Unauthorized' * '403': - * description: Insufficient permissions (requires can_view_contacts) + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' */ get: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); @@ -77,10 +81,13 @@ module.exports = { * /api/v1/person: * post: * summary: Create a new person - * operationId: createPerson - * description: Creates a new person contact record. + * operationId: v1PersonPost + * description: Creates a new person record. * tags: * - Person + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_create_people, can_edit] * requestBody: * required: true * content: @@ -95,11 +102,11 @@ module.exports = { * schema: * $ref: '#/components/schemas/v1.Person' * '400': - * description: Invalid input (missing required fields, invalid types, etc.) + * $ref: '#/components/responses/BadRequest' * '401': - * description: Not authenticated + * $ref: '#/components/responses/Unauthorized' * '403': - * description: Insufficient permissions (requires can_create_people or can_edit) + * $ref: '#/components/responses/Forbidden' */ create: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_create_people', 'can_edit'] }); @@ -107,6 +114,47 @@ module.exports = { return res.json(personDoc); }), + /** + * @openapi + * /api/v1/person/{id}: + * put: + * summary: Update a person + * operationId: v1PersonIdPut + * description: Updates an existing person contact record. + * tags: + * - Person + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_update_people, can_edit] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the person to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.UpdatePersonInput' + * responses: + * '200': + * description: The updated person record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Person' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ update: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_update_people', 'can_edit'] }); const { params: { uuid }, body } = req; diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 7f2879b8e4f..084ae7e7e8c 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -42,7 +42,15 @@ const SWAGGER_OPTIONS = { } }, servers: [{ url: '/' }], - components: { schemas: {} }, + components: { + schemas: {}, + responses: { + NotFound: { description: 'Entity not found' }, + BadRequest: { description: 'Invalid input (missing required fields, invalid types, etc.)' }, + Unauthorized: { description: 'Not authenticated' }, + Forbidden: { description: 'Insufficient permissions' } + } + }, }, apis: [path.resolve(__dirname, '../api/src/controllers/**/*.js')], }; From 368500bd39f27038bd15809144e85681b0720505 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Mon, 16 Mar 2026 16:03:49 -0500 Subject: [PATCH 04/37] Add remaining doc for person endpoints --- api/src/controllers/person.js | 48 +++++++++++++++++++++++++++++++++++ scripts/generate-openapi.js | 23 ++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 9a99e5904ca..b83cfe180e5 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -69,6 +69,54 @@ module.exports = { return res.json(person); }), + + /** + * @openapi + * /api/v1/person: + * get: + * summary: Get persons + * operationId: v1PersonGetAll + * description: > + * Returns a paginated array of persons for the given contact type. Use the `cursor` returned in each response + * to retrieve subsequent pages. See also [Get Person by id](#/Person/v1PersonIdGet) for retrieving a single + * person. + * tags: + * - Person + * x-since: 4.11.0 + * x-permissions: + * hasAll: [can_view_contacts] + * parameters: + * - in: query + * name: type + * required: true + * schema: + * type: string + * description: The contact_type id for the type of persons to fetch + * - $ref: '#/components/parameters/cursor' + * - $ref: '#/components/parameters/entityLimit' + * responses: + * '200': + * description: A page of person records + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * description: The results for this page + * items: + * $ref: '#/components/schemas/v1.Person' + * cursor: + * $ref: '#/components/schemas/PageCursor' + * required: + * - data + * - cursor + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ getAll: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); const personType = Qualifier.byContactType(req.query.type); diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 084ae7e7e8c..8dc81496fac 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -43,7 +43,28 @@ const SWAGGER_OPTIONS = { }, servers: [{ url: '/' }], components: { - schemas: {}, + schemas: { + PageCursor: { + type: ['string', 'null'], + description: 'Token for retrieving the next page. A `null` value indicates there are no more pages.', + }, + }, + parameters: { + cursor: { + in: 'query', + name: 'cursor', + schema: { type: 'string' }, + description: + 'Token identifying which page to retrieve. Omit for the first page. ' + + 'Subsequent pages can be retrieved by providing the cursor returned with the previous page.', + }, + entityLimit: { + in: 'query', + name: 'limit', + schema: { type: 'number', default: 100, minimum: 1 }, + description: 'The maximum number of entities to return. Defaults to 100.', + }, + }, responses: { NotFound: { description: 'Entity not found' }, BadRequest: { description: 'Invalid input (missing required fields, invalid types, etc.)' }, From 8a3f00aa8eb2597f01492414c7c30934fb396f2f Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Mon, 16 Mar 2026 16:26:38 -0500 Subject: [PATCH 05/37] Add doc for contact endpoints --- api/src/controllers/contact.js | 101 +++++++++++++++++++++++++++++++++ api/src/controllers/person.js | 9 +-- scripts/generate-openapi.js | 15 ++++- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/api/src/controllers/contact.js b/api/src/controllers/contact.js index e2dad837a81..acd9ca3a2f4 100644 --- a/api/src/controllers/contact.js +++ b/api/src/controllers/contact.js @@ -7,8 +7,51 @@ const getContact = ctx.bind(Contact.v1.get); const getContactWithLineage = ctx.bind(Contact.v1.getWithLineage); const getContactIds = ctx.bind(Contact.v1.getUuidsPage); +/** + * @openapi + * tags: + * - name: Contact + * description: Operations for contacts (persons and places) + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/contact/{id}: + * get: + * summary: Get a contact by id + * operationId: v1ContactIdGet + * description: > + * Returns a contact record (person or place). Optionally includes the full parent place lineage. + * tags: + * - Contact + * x-since: 4.18.0 + * x-permissions: + * hasAll: [can_view_contacts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the contact to retrieve + * - $ref: '#/components/parameters/withLineage' + * responses: + * '200': + * description: The contact record + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Contact' + * - $ref: '#/components/schemas/v1.ContactWithLineage' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); const { params: { uuid }, query: { with_lineage } } = req; @@ -20,6 +63,64 @@ module.exports = { return res.json(contact); }), + + /** + * @openapi + * /api/v1/contact/uuid: + * get: + * summary: Get contact UUIDs + * operationId: v1ContactUuidGet + * description: > + * Returns a paginated array of contact identifier strings matching the given filter criteria. + * At least one of `type` or `freetext` must be provided. + * tags: + * - Contact + * x-since: 4.18.0 + * x-permissions: + * hasAll: [can_view_contacts] + * parameters: + * - in: query + * name: type + * schema: + * type: string + * description: > + * The contact_type id for the type of contacts to fetch. Required if `freetext` is not provided + * and may be combined with `freetext`. + * - in: query + * name: freetext + * schema: + * type: string + * minLength: 3 + * description: > + * A search term for filtering contacts. Must be at least 3 characters and not contain whitespace. + * Required if `type` is not provided and may be combined with `type`. + * - $ref: '#/components/parameters/cursor' + * - $ref: '#/components/parameters/limitId' + * responses: + * '200': + * description: A page of contact UUIDs + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * description: The results for this page + * items: + * type: string + * cursor: + * $ref: '#/components/schemas/PageCursor' + * required: + * - data + * - cursor + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ getUuids: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); if (!req.query.freetext && !req.query.type) { diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index b83cfe180e5..62696eb395d 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -36,12 +36,7 @@ module.exports = { * schema: * type: string * description: The id of the person to retrieve - * - in: query - * name: with_lineage - * schema: - * type: string - * enum: ['true'] - * description: When set to 'true', includes the full parent place lineage + * - $ref: '#/components/parameters/withLineage' * responses: * '200': * description: The person record @@ -93,7 +88,7 @@ module.exports = { * type: string * description: The contact_type id for the type of persons to fetch * - $ref: '#/components/parameters/cursor' - * - $ref: '#/components/parameters/entityLimit' + * - $ref: '#/components/parameters/limitEntity' * responses: * '200': * description: A page of person records diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 8dc81496fac..4ccfc54c00e 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -11,6 +11,7 @@ const DATASOURCE_DIR = path.resolve(__dirname, '../shared-libs/cht-datasource/sr const TSCONFIG = path.resolve(__dirname, '../shared-libs/cht-datasource/tsconfig.build.json'); const TYPE_SOURCES = [ + 'contact.ts', 'person.ts', 'input.ts', ].map(file => path.join(DATASOURCE_DIR, file)); @@ -58,12 +59,24 @@ const SWAGGER_OPTIONS = { 'Token identifying which page to retrieve. Omit for the first page. ' + 'Subsequent pages can be retrieved by providing the cursor returned with the previous page.', }, - entityLimit: { + limitEntity: { in: 'query', name: 'limit', schema: { type: 'number', default: 100, minimum: 1 }, description: 'The maximum number of entities to return. Defaults to 100.', }, + limitId: { + in: 'query', + name: 'limit', + schema: { type: 'number', default: 10000, minimum: 1 }, + description: 'The maximum number of identifiers to return. Defaults to 10000.', + }, + withLineage: { + in: 'query', + name: 'with_lineage', + schema: { type: 'string', 'enum': ['true'] }, // TODO Do we need string? + description: 'Include the full parent lineage.' + } }, responses: { NotFound: { description: 'Entity not found' }, From 5383b2d327d567251e2d9fbef7430b093cdaf9bb Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Mon, 16 Mar 2026 17:05:43 -0500 Subject: [PATCH 06/37] Add doc for report endpoints --- api/src/controllers/person.js | 8 +- api/src/controllers/report.js | 175 ++++++++++++++++++++++++++++++++++ scripts/generate-openapi.js | 3 +- 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 62696eb395d..8a1cfa4466c 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -181,14 +181,18 @@ module.exports = { * content: * application/json: * schema: - * $ref: '#/components/schemas/v1.UpdatePersonInput' + * oneOf: + * - $ref: '#/components/schemas/v1.Person' + * - $ref: '#/components/schemas/v1.PersonWithLineage' * responses: * '200': * description: The updated person record * content: * application/json: * schema: - * $ref: '#/components/schemas/v1.Person' + * oneOf: + * - $ref: '#/components/schemas/v1.Person' + * - $ref: '#/components/schemas/v1.PersonWithLineage' * '400': * $ref: '#/components/responses/BadRequest' * '401': diff --git a/api/src/controllers/report.js b/api/src/controllers/report.js index 5193f730ed0..2e4cc2b10f7 100644 --- a/api/src/controllers/report.js +++ b/api/src/controllers/report.js @@ -9,8 +9,51 @@ const getReportIds = ctx.bind(Report.v1.getUuidsPage); const create = ctx.bind(Report.v1.create); const update = ctx.bind(Report.v1.update); +/** + * @openapi + * tags: + * - name: Report + * description: Operations for reports + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/report/{id}: + * get: + * summary: Get a report by id + * operationId: v1ReportIdGet + * description: > + * Returns a report record. Optionally includes the full contact, patient, and/or place lineage. + * tags: + * - Report + * x-since: 4.18.0 + * x-permissions: + * hasAll: [can_view_reports] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the report to retrieve + * - $ref: '#/components/parameters/withLineage' + * responses: + * '200': + * description: The report record + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Report' + * - $ref: '#/components/schemas/v1.ReportWithLineage' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_reports'] }); const { params: { uuid }, query: { with_lineage } } = req; @@ -23,6 +66,55 @@ module.exports = { return res.json(report); }), + /** + * @openapi + * /api/v1/report/uuid: + * get: + * summary: Get report UUIDs + * operationId: v1ReportUuidGet + * description: > + * Returns a paginated array of report identifiers matching the given freetext search term. + * tags: + * - Report + * x-since: 4.18.0 + * x-permissions: + * hasAll: [can_view_reports] + * parameters: + * - in: query + * name: freetext + * required: true + * schema: + * type: string + * minLength: 3 + * description: > + * A search term for filtering reports. Must be at least 3 characters and not contain whitespace. + * - $ref: '#/components/parameters/cursor' + * - $ref: '#/components/parameters/limitId' + * responses: + * '200': + * description: A page of report UUIDs + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * description: The results for this page + * items: + * type: string + * cursor: + * $ref: '#/components/schemas/PageCursor' + * required: + * - data + * - cursor + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ getUuids: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_reports'] }); const qualifier = Qualifier.byFreetext(req.query.freetext); @@ -30,12 +122,95 @@ module.exports = { return res.json(docs); }), + /** + * @openapi + * /api/v1/report: + * post: + * summary: Create a new report + * operationId: v1ReportPost + * description: Creates a new report. + * tags: + * - Report + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_create_records, can_edit] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.ReportInput' + * responses: + * '200': + * description: The created report record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Report' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ create: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_create_records', 'can_edit'] }); const reportDoc = await create(req.body); return res.json(reportDoc); }), + /** + * @openapi + * /api/v1/report/{id}: + * put: + * summary: Update a report + * operationId: v1ReportIdPut + * description: Updates an existing report. + * tags: + * - Report + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_update_reports, can_edit] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the report to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Report' + * - $ref: '#/components/schemas/v1.ReportWithLineage' + * properties: + * contact: + * oneOf: + * - type: string + * description: UUID of the contact + * - $ref: '#/components/schemas/NormalizedParent' + * responses: + * '200': + * description: The updated report record + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Report' + * - $ref: '#/components/schemas/v1.ReportWithLineage' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ update: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_update_reports', 'can_edit'] }); const { params: { uuid }, body } = req; diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 4ccfc54c00e..45f0c0c4c81 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -13,6 +13,7 @@ const TSCONFIG = path.resolve(__dirname, '../shared-libs/cht-datasource/tsconfig const TYPE_SOURCES = [ 'contact.ts', 'person.ts', + 'report.ts', 'input.ts', ].map(file => path.join(DATASOURCE_DIR, file)); @@ -74,7 +75,7 @@ const SWAGGER_OPTIONS = { withLineage: { in: 'query', name: 'with_lineage', - schema: { type: 'string', 'enum': ['true'] }, // TODO Do we need string? + schema: { 'enum': ['true'] }, description: 'Include the full parent lineage.' } }, From aaeff97da8b6aaddba6c4b73b1817633818e5eb5 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Mon, 16 Mar 2026 18:12:31 -0500 Subject: [PATCH 07/37] Add doc for place endpoints --- api/src/controllers/person.js | 4 +- api/src/controllers/place.js | 174 ++++++++++++++++++++++++++++++++++ scripts/generate-openapi.js | 1 + 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 8a1cfa4466c..753663b8dfa 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -70,7 +70,7 @@ module.exports = { * /api/v1/person: * get: * summary: Get persons - * operationId: v1PersonGetAll + * operationId: v1PersonGet * description: > * Returns a paginated array of persons for the given contact type. Use the `cursor` returned in each response * to retrieve subsequent pages. See also [Get Person by id](#/Person/v1PersonIdGet) for retrieving a single @@ -107,6 +107,8 @@ module.exports = { * required: * - data * - cursor + * '400': + * $ref: '#/components/responses/BadRequest' * '401': * $ref: '#/components/responses/Unauthorized' * '403': diff --git a/api/src/controllers/place.js b/api/src/controllers/place.js index 2890d81ffb0..1cb2b77c714 100644 --- a/api/src/controllers/place.js +++ b/api/src/controllers/place.js @@ -9,8 +9,50 @@ const getPageByType = ctx.bind(Place.v1.getPage); const create = ctx.bind(Place.v1.create); const update = ctx.bind(Place.v1.update); +/** + * @openapi + * tags: + * - name: Place + * description: Operations for place contacts + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/place/{id}: + * get: + * summary: Get a place by id + * operationId: v1PlaceIdGet + * description: Returns a place contact record. Optionally includes the full parent place lineage. + * tags: + * - Place + * x-since: 4.10.0 + * x-permissions: + * hasAll: [can_view_contacts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the place to retrieve + * - $ref: '#/components/parameters/withLineage' + * responses: + * '200': + * description: The place record + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Place' + * - $ref: '#/components/schemas/v1.PlaceWithLineage' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); const { params: { uuid }, query: { with_lineage } } = req; @@ -22,6 +64,55 @@ module.exports = { return res.json(place); }), + /** + * @openapi + * /api/v1/place: + * get: + * summary: Get places + * operationId: v1PlaceGet + * description: > + * Returns a paginated array of places for the given contact type. Use the `cursor` returned in each response + * to retrieve subsequent pages. See also [Get Place by id](#/Place/v1PlaceIdGet) for retrieving a single + * place. + * tags: + * - Place + * x-since: 4.12.0 + * x-permissions: + * hasAll: [can_view_contacts] + * parameters: + * - in: query + * name: type + * required: true + * schema: + * type: string + * description: The contact_type id for the type of places to fetch + * - $ref: '#/components/parameters/cursor' + * - $ref: '#/components/parameters/limitEntity' + * responses: + * '200': + * description: A page of place records + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * description: The results for this page + * items: + * $ref: '#/components/schemas/v1.Place' + * cursor: + * $ref: '#/components/schemas/PageCursor' + * required: + * - data + * - cursor + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ getAll: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] }); const placeType = Qualifier.byContactType(req.query.type); @@ -29,12 +120,95 @@ module.exports = { return res.json(docs); }), + /** + * @openapi + * /api/v1/place: + * post: + * summary: Create a new place + * operationId: v1PlacePost + * description: Creates a new place record. + * tags: + * - Place + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_create_places, can_edit] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.PlaceInput' + * responses: + * '200': + * description: The created place record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Place' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ create: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_create_places', 'can_edit'] }); const placeDoc = await create(req.body); return res.json(placeDoc); }), + /** + * @openapi + * /api/v1/place/{id}: + * put: + * summary: Update a place + * operationId: v1PlaceIdPut + * description: Updates an existing place contact record. + * tags: + * - Place + * x-since: 5.2.0 + * x-permissions: + * hasAny: [can_update_places, can_edit] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the place to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Place' + * - $ref: '#/components/schemas/v1.PlaceWithLineage' + * properties: + * contact: + * oneOf: + * - type: string + * description: UUID of the contact + * - $ref: '#/components/schemas/NormalizedParent' + * responses: + * '200': + * description: The updated place record + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/v1.Place' + * - $ref: '#/components/schemas/v1.PlaceWithLineage' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ update: serverUtils.doOrError(async (req, res) => { await auth.assertPermissions(req, { isOnline: true, hasAny: ['can_update_places', 'can_edit'] }); const { params: { uuid }, body } = req; diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 45f0c0c4c81..82c4afab5e3 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -13,6 +13,7 @@ const TSCONFIG = path.resolve(__dirname, '../shared-libs/cht-datasource/tsconfig const TYPE_SOURCES = [ 'contact.ts', 'person.ts', + 'place.ts', 'report.ts', 'input.ts', ].map(file => path.join(DATASOURCE_DIR, file)); From 5d2bcd513146905f207bc4a94e227d7cfc5f0980 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 09:28:01 -0500 Subject: [PATCH 08/37] Add doc for target endpoints --- api/src/controllers/target.js | 100 +++++++++++++++++++++++++++++++++- scripts/generate-openapi.js | 1 + 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/target.js b/api/src/controllers/target.js index 12c73273373..143f5a2ec1e 100644 --- a/api/src/controllers/target.js +++ b/api/src/controllers/target.js @@ -24,8 +24,45 @@ const getContactIdQualifier = ({ contact_ids, contact_id }) => { return Qualifier.byContactIds(contactIds); }; +/** + * @openapi + * tags: + * - name: Target + * description: Operations for targets + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/target/{id}: + * get: + * summary: Get a target by id + * operationId: v1TargetIdGet + * description: Returns a target record. + * tags: + * - Target + * x-since: 5.1.0 + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the target to retrieve + * responses: + * '200': + * description: The target record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v1.Target' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: serverUtils.doOrError(async (req, res) => { await checkUserPermissions(req); const { id } = req.params; @@ -37,6 +74,67 @@ module.exports = { return res.json(target); }), + /** + * @openapi + * /api/v1/target: + * get: + * summary: Get targets + * operationId: v1TargetGet + * description: > + * Returns a paginated array of targets for the given contact and reporting period. Use the `cursor` returned + * in each response to retrieve subsequent pages. See also [Get Target by id](#/Target/v1TargetIdGet) for + * retrieving a single target. + * tags: + * - Target + * x-since: 5.1.0 + * parameters: + * - in: query + * name: contact_id + * schema: + * type: string + * description: > + * A single contact id to filter targets by. Either `contact_id` or `contact_ids` must be + * provided. + * - in: query + * name: contact_ids + * schema: + * type: string + * description: > + * Comma-separated contact ids to filter targets by. Either `contact_id` or `contact_ids` must + * be provided. + * - in: query + * name: reporting_period + * required: true + * schema: + * type: string + * description: "The reporting period to filter targets by (e.g. '2025-09')" + * - $ref: '#/components/parameters/cursor' + * - $ref: '#/components/parameters/limitEntity' + * responses: + * '200': + * description: A page of target records + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * description: The results for this page + * items: + * $ref: '#/components/schemas/v1.Target' + * cursor: + * $ref: '#/components/schemas/PageCursor' + * required: + * - data + * - cursor + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ getAll: serverUtils.doOrError(async (req, res) => { await checkUserPermissions(req); @@ -48,7 +146,7 @@ module.exports = { req.query.cursor, req.query.limit ); - + return res.json(docs); }), }, diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 82c4afab5e3..6935f810a54 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -15,6 +15,7 @@ const TYPE_SOURCES = [ 'person.ts', 'place.ts', 'report.ts', + 'target.ts', 'input.ts', ].map(file => path.join(DATASOURCE_DIR, file)); From 95d04f254d593e4608a372c3792e7b517ce145b7 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 12:07:32 -0500 Subject: [PATCH 09/37] Add doc for info endpoints --- api/src/routing.js | 53 +++++++++++++++++++++++++++++++++++++ scripts/generate-openapi.js | 6 ++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/api/src/routing.js b/api/src/routing.js index 073d4201c3c..475382dfa2a 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -124,6 +124,13 @@ const handleJsonOrCsvRequest = (method, path, callback) => { }); }; +/** + * @openapi + * tags: + * - name: Info + * description: Operations for getting server info + */ + app.deleteJson = (path, callback) => handleJsonRequest('delete', path, callback); app.postJsonOrCsv = (path, callback) => handleJsonOrCsvRequest('post', path, callback); app.postJson = (path, callback) => handleJsonRequest('post', path, callback); @@ -394,11 +401,57 @@ app.all('/setup/finish', function(req, res) { res.status(200).send('Setup services are not currently available'); }); +/** + * @openapi + * /api/info: + * get: + * summary: Get the version of the CHT server + * operationId: apiInfoGet + * description: Returns the version of the CHT server. + * tags: + * - Info + * responses: + * '200': + * description: The API info data + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * type: string + * required: [version] + */ app.get('/api/info', function(req, res) { const p = require('../package.json'); res.json({ version: p.version }); }); +/** + * @openapi + * /api/deploy-info: + * get: + * summary: Get deploy information + * operationId: apiDeployInfoGet + * description: Returns build and deploy information for the running CHT instance. + * tags: + * - Info + * responses: + * '200': + * description: The deploy info data + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * type: string + * description: The version of the deployed CHT instance + * required: [version] + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + */ app.get('/api/deploy-info', async (req, res) => { if (!req.userCtx) { return serverUtils.notLoggedIn(req, res); diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 6935f810a54..869175f3763 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -89,7 +89,10 @@ const SWAGGER_OPTIONS = { } }, }, - apis: [path.resolve(__dirname, '../api/src/controllers/**/*.js')], + apis: [ + path.resolve(__dirname, '../api/src/routing.js'), + path.resolve(__dirname, '../api/src/controllers/**/*.js'), + ], }; const SPECTRAL_OPTIONS = { extends: [[oas, 'all']], rules: {} }; @@ -153,6 +156,7 @@ const main = async () => { const swaggerSpec = swaggerJsdoc(SWAGGER_OPTIONS); const tsSchemas = generateTsSchemas(); Object.assign(swaggerSpec.components.schemas, tsSchemas); + swaggerSpec.tags.sort((a, b) => a.name.localeCompare(b.name)); await lintSpec(swaggerSpec); // TODO Currently publishing with cht-datasource docs site. const outputPath = path.resolve(__dirname, '../shared-libs/cht-datasource/docs/openapi.json'); From b57bfef4dd0f91b615b95309f6df3f40d48d016b Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 12:40:13 -0500 Subject: [PATCH 10/37] Update generate script to fail on warn --- api/src/controllers/person.js | 4 +--- scripts/generate-openapi.js | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 753663b8dfa..bd7dc42032c 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -183,9 +183,7 @@ module.exports = { * content: * application/json: * schema: - * oneOf: - * - $ref: '#/components/schemas/v1.Person' - * - $ref: '#/components/schemas/v1.PersonWithLineage' + * $ref: '#/components/schemas/v1.UpdatePersonInput' * responses: * '200': * description: The updated person record diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 869175f3763..e65757b31b2 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -145,11 +145,10 @@ const lintSpec = async (spec) => { severity, code, message, path }) => console.log(` [${DIAGNOSTIC_SEVERITY[severity]}] ${code}: ${message} (at ${path.join('.')})`)); - const errors = results.filter(r => r.severity === 0); + const errors = results.filter(r => r.severity <= 1); if (errors.length > 0) { throw new Error(`OpenAPI spec has ${errors.length} validation error(s)`); } - // TODO Consider failing for warnings }; const main = async () => { From aacf368e0530fafa067e719dde2dec3718c63e90 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 13:36:48 -0500 Subject: [PATCH 11/37] Add docs for monitoring endpoints --- api/src/controllers/monitoring.js | 361 +++++++++++++++++++++++- shared-libs/cht-datasource/src/input.ts | 1 + 2 files changed, 361 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/monitoring.js b/api/src/controllers/monitoring.js index 5e034f8dd68..ec5120b0bde 100644 --- a/api/src/controllers/monitoring.js +++ b/api/src/controllers/monitoring.js @@ -3,7 +3,215 @@ const serverUtils = require('../server-utils'); const DEFAULT_CONNECTED_USER_INTERVAL = 7; +/** + * @openapi + * tags: + * - name: Monitoring + * description: Operations for monitoring the CHT instance + * components: + * schemas: + * MonitoringVersion: + * type: object + * properties: + * app: + * type: string + * description: The version of the webapp. + * node: + * type: string + * description: The version of NodeJS. + * couchdb: + * type: string + * description: The version of CouchDB. + * MonitoringCouchDb: + * type: object + * description: > + * CouchDB metrics keyed by database name (e.g. "medic", "medic-sentinel", + * "medic-users-meta", "_users"). + * additionalProperties: + * type: object + * properties: + * name: + * type: string + * description: The name of the db. + * update_sequence: + * type: number + * description: The number of changes in the db. + * doc_count: + * type: number + * description: The number of docs in the db. + * doc_del_count: + * type: number + * description: The number of deleted docs in the db. + * fragmentation: + * type: number + * description: > + * The fragmentation of the entire db (including view indexes) as stored on disk. + * A lower value is better. `1` is no fragmentation. + * sizes: + * type: object + * description: Database size information. Requires CHT Core `4.11.0` or later. + * properties: + * active: + * type: number + * description: > + * The size in bytes of live data inside the database. Includes documents, + * metadata, and attachments, but not view indexes. + * file: + * type: number + * description: > + * The size in bytes of the database file on disk. Includes documents, + * metadata, and attachments, but not view indexes. + * view_indexes: + * type: array + * description: View index information. Requires CHT Core `4.11.0` or later. + * items: + * type: object + * properties: + * name: + * type: string + * description: The name of the view index (the design). + * sizes: + * type: object + * properties: + * active: + * type: number + * description: The size in bytes of live data inside the view. + * file: + * type: number + * description: The size in bytes of the view as stored on disk. + * MonitoringDate: + * type: object + * properties: + * current: + * type: number + * description: > + * The current server date in millis since the epoch, useful for ensuring the + * server time is correct. + * uptime: + * type: number + * description: How long API has been running in seconds. + * MonitoringSentinel: + * type: object + * properties: + * backlog: + * type: number + * description: Number of changes yet to be processed by Sentinel. + * MonitoringOutboundPush: + * type: object + * properties: + * backlog: + * type: number + * description: Number of changes yet to be processed by Outbound Push. + * MonitoringFeedback: + * type: object + * properties: + * count: + * type: number + * description: > + * Number of feedback docs created, usually indicative of client side errors. + * MonitoringConflict: + * type: object + * properties: + * count: + * type: number + * description: Number of doc conflicts which need to be resolved manually. + * MonitoringReplicationLimit: + * type: object + * properties: + * count: + * type: number + * description: Number of users that exceeded the replication limit of documents. + * MonitoringConnectedUsers: + * type: object + * properties: + * count: + * type: number + * description: > + * Number of users that have connected to the api in a given number of days. + * The period defaults to 7 days but can be changed via the + * `connected_user_interval` query parameter. + */ module.exports = { + /** + * @openapi + * /api/v1/monitoring: + * get: + * summary: Get monitoring metrics + * operationId: v1MonitoringGet + * deprecated: true + * description: > + * Use [GET /api/v2/monitoring](#/Monitoring/v2MonitoringGet) instead. + * Returns a range of metrics about the instance for automated monitoring, allowing tracking of trends over + * time and alerting about potential issues. No authentication is required. + * + * Errors: + * + * - A metric of `""` (for string values) or `-1` (for numeric values) indicates an error occurred while + * querying the metric - check the API logs for details. + * - If no response or an error response is received the instance is unreachable. Thus, this API can be used + * as an uptime monitoring endpoint. + * tags: + * - Monitoring + * parameters: + * - in: query + * name: connected_user_interval + * schema: + * type: number + * default: 7 + * description: The number of days to use when counting connected users + * responses: + * '200': + * description: Monitoring metrics + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * $ref: '#/components/schemas/MonitoringVersion' + * couchdb: + * $ref: '#/components/schemas/MonitoringCouchDb' + * date: + * $ref: '#/components/schemas/MonitoringDate' + * sentinel: + * $ref: '#/components/schemas/MonitoringSentinel' + * messaging: + * type: object + * properties: + * outgoing: + * type: object + * properties: + * state: + * type: object + * properties: + * due: + * type: number + * description: The number of messages due to be sent. + * scheduled: + * type: number + * description: The number of messages scheduled to be sent in the future. + * muted: + * type: number + * description: > + * The number of messages that are muted and therefore will not be sent. + * delivered: + * type: number + * description: > + * The number of messages that have been delivered or sent. As of 3.12.x. + * failed: + * type: number + * description: The number of messages that have failed to be delivered. As of 3.12.x. + * outbound_push: + * $ref: '#/components/schemas/MonitoringOutboundPush' + * feedback: + * $ref: '#/components/schemas/MonitoringFeedback' + * conflict: + * $ref: '#/components/schemas/MonitoringConflict' + * replication_limit: + * $ref: '#/components/schemas/MonitoringReplicationLimit' + * connected_users: + * $ref: '#/components/schemas/MonitoringConnectedUsers' + */ getV1: (req, res) => { const connectedUserInterval = req.query.connected_user_interval || DEFAULT_CONNECTED_USER_INTERVAL; @@ -11,9 +219,160 @@ module.exports = { .then(body => res.json(body)) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v2/monitoring: + * get: + * summary: Get monitoring metrics + * operationId: v2MonitoringGet + * description: > + * Returns a range of metrics about the instance for automated monitoring, allowing tracking of trends over + * time and alerting about potential issues. No authentication is required. + * tags: + * - Monitoring + * x-since: 3.12.0 + * parameters: + * - in: query + * name: connected_user_interval + * schema: + * type: number + * default: 7 + * description: The number of days to use when counting connected users + * responses: + * '200': + * description: Monitoring metrics + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * $ref: '#/components/schemas/MonitoringVersion' + * couchdb: + * $ref: '#/components/schemas/MonitoringCouchDb' + * date: + * $ref: '#/components/schemas/MonitoringDate' + * sentinel: + * $ref: '#/components/schemas/MonitoringSentinel' + * messaging: + * type: object + * properties: + * outgoing: + * type: object + * properties: + * total: + * type: object + * properties: + * due: + * type: number + * description: The number of messages due to be sent. + * scheduled: + * type: number + * description: The number of messages scheduled to be sent in the future. + * muted: + * type: number + * description: > + * The number of messages that are muted and therefore will not be sent. + * delivered: + * type: number + * description: The number of messages that have been delivered or sent. + * failed: + * type: number + * description: The number of messages that have failed to be delivered. + * seven_days: + * type: object + * properties: + * due: + * type: number + * description: The number of messages due to be sent in the last seven days. + * scheduled: + * type: number + * description: > + * The number of messages that were scheduled to be sent in the last seven days. + * muted: + * type: number + * description: > + * The number of messages that were due in the last seven days and are muted. + * delivered: + * type: number + * description: > + * The number of messages that were due in the last seven days and have been + * delivered or sent. + * failed: + * type: number + * description: > + * The number of messages that were due in the last seven days and have failed + * to be delivered. + * last_hundred: + * type: object + * properties: + * pending: + * type: object + * description: > + * Counts within last 100 messages that have received status updates, and are + * one of the "pending" group statuses. + * properties: + * pending: + * type: number + * description: Number of messages that are pending. + * forwarded-to-gateway: + * type: number + * description: Number of messages that are forwarded-to-gateway. + * received-by-gateway: + * type: number + * description: Number of messages that are received-by-gateway. + * forwarded-by-gateway: + * type: number + * description: Number of messages that are forwarded-by-gateway. + * final: + * type: object + * description: > + * Counts within last 100 messages that have received status updates, and are + * in one of the "final" group statuses. + * properties: + * sent: + * type: number + * description: Number of messages that are sent. + * delivered: + * type: number + * description: Number of messages that are delivered. + * failed: + * type: number + * description: Number of messages that are failed. + * denied: + * type: number + * description: Number of messages that are denied. + * cleared: + * type: number + * description: Number of messages that are cleared. + * muted: + * type: number + * description: Number of messages that are muted. + * duplicate: + * type: number + * description: Number of messages that are duplicate. + * muted: + * type: object + * description: > + * Counts within last 100 messages that have received status updates, and are + * in one of the "muted" group statuses. + * additionalProperties: + * type: number + * outbound_push: + * $ref: '#/components/schemas/MonitoringOutboundPush' + * feedback: + * $ref: '#/components/schemas/MonitoringFeedback' + * conflict: + * $ref: '#/components/schemas/MonitoringConflict' + * replication_limit: + * $ref: '#/components/schemas/MonitoringReplicationLimit' + * connected_users: + * $ref: '#/components/schemas/MonitoringConnectedUsers' + */ getV2: (req, res) => { const connectedUserInterval = req.query.connected_user_interval || DEFAULT_CONNECTED_USER_INTERVAL; - + return service .jsonV2(connectedUserInterval) .then(body => res.json(body)) diff --git a/shared-libs/cht-datasource/src/input.ts b/shared-libs/cht-datasource/src/input.ts index 6f40998dda1..d5271a83227 100644 --- a/shared-libs/cht-datasource/src/input.ts +++ b/shared-libs/cht-datasource/src/input.ts @@ -8,6 +8,7 @@ import { Doc } from './libs/doc'; export namespace v1 { /** * Input data for a contact. + * @internal */ export interface ContactInput extends DataObject { readonly type: string From def39ff41198d667dec32b9400a017528fd85458 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 13:40:28 -0500 Subject: [PATCH 12/37] Add docs for impact endpoints --- api/src/controllers/impact.js | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/api/src/controllers/impact.js b/api/src/controllers/impact.js index fc731926149..a5a98cd25a2 100644 --- a/api/src/controllers/impact.js +++ b/api/src/controllers/impact.js @@ -9,8 +9,80 @@ const checkUserPermissions = async (req) => { throw new PermissionError('Insufficient privileges'); } }; + +/** + * @openapi + * tags: + * - name: Impact + * description: Operations for impact metrics + */ module.exports = { v1: { + /** + * @openapi + * /api/v1/impact: + * get: + * summary: Get impact metrics + * operationId: v1ImpactGet + * description: Returns aggregated impact metrics including user, contact, and report counts. + * tags: + * - Impact + * x-since: 5.0.0 + * responses: + * '200': + * description: The impact metrics + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: object + * properties: + * count: + * type: number + * description: Total number of users. + * contacts: + * type: object + * properties: + * count: + * type: number + * description: Total number of contacts. + * by_type: + * type: array + * description: Contact counts broken down by contact type. + * items: + * type: object + * properties: + * type: + * type: string + * description: Name of the contact type. + * count: + * type: number + * description: Total number of contacts with the type. + * reports: + * type: object + * properties: + * count: + * type: number + * description: Total number of reports. + * by_form: + * type: array + * description: Report counts broken down by form. + * items: + * type: object + * properties: + * form: + * type: string + * description: Name of the form. + * count: + * type: number + * description: Total number of reports with the form. + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ get: serverUtils.doOrError(async (req, res) => { await checkUserPermissions(req); const impact = await service.jsonV1(); From bef704d9c4a0dff96abd1367e723fedf895a5642 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Tue, 17 Mar 2026 15:05:55 -0500 Subject: [PATCH 13/37] Add docs for upgrade endpoints --- api/src/controllers/upgrade.js | 385 +++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) diff --git a/api/src/controllers/upgrade.js b/api/src/controllers/upgrade.js index 1d4e14ba4a1..6ad02283e9f 100644 --- a/api/src/controllers/upgrade.js +++ b/api/src/controllers/upgrade.js @@ -8,6 +8,34 @@ const configWatcher = require('../services/config-watcher'); const REQUIRED_PERMISSIONS = ['can_upgrade']; const checkAuth = (req) => auth.check(req, REQUIRED_PERMISSIONS); +/** + * @openapi + * tags: + * - name: Upgrade + * description: Operations for upgrading the CHT instance + * components: + * schemas: + * UpgradeRequestBody: + * type: object + * required: [build] + * properties: + * build: + * type: object + * required: [namespace, application, version] + * properties: + * namespace: + * type: string + * description: Must be "medic". + * application: + * type: string + * description: Must be "medic". + * version: + * type: string + * description: > + * The version to upgrade to. Should correspond to a release, pre-release, or branch + * that has been pushed to the builds server. + */ + const upgrade = (req, res, stageOnly) => { return checkAuth(req) .then(userCtx => { @@ -86,17 +114,374 @@ const compareUpgrade = async (req, res) => { }; module.exports = { + /** + * @openapi + * /api/v2/upgrade/can-upgrade: + * get: + * summary: Check if an upgrade can be performed + * operationId: v2UpgradeCanUpgradeGet + * description: Returns whether the instance is in a state where an upgrade can be performed. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: Whether an upgrade can be performed + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * description: Whether an upgrade can be performed. + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ canUpgrade: canUpgrade, + + /** + * @openapi + * /api/v1/upgrade: + * post: + * summary: Upgrade to a version + * operationId: v1UpgradePost + * deprecated: true + * description: > + * Use [POST /api/v2/upgrade](#/Upgrade/v2UpgradePost) instead. + * Performs a complete upgrade to the provided version. This is equivalent to calling + * `/api/v1/upgrade/stage` and then `/api/v1/upgrade/complete` once staging has finished. + * This is asynchronous. Progress can be followed by watching the `horti-upgrade` document. + * Calling this endpoint will eventually cause api and sentinel to restart. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpgradeRequestBody' + * responses: + * '200': + * description: Upgrade initiated + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/upgrade: + * post: + * summary: Upgrade to a version + * operationId: v2UpgradePost + * description: > + * Performs a complete upgrade to the provided version. This is equivalent to calling + * `/api/v2/upgrade/stage` and then `/api/v2/upgrade/complete` once staging has finished. + * This is asynchronous. Progress can be followed via [GET /api/v2/upgrade](#/Upgrade/v2UpgradeGet). + * Calling this endpoint will eventually cause api and sentinel to restart. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpgradeRequestBody' + * responses: + * '200': + * description: Upgrade initiated + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ upgrade: (req, res) => upgrade(req, res, false), + + /** + * @openapi + * /api/v1/upgrade/stage: + * post: + * summary: Stage an upgrade + * operationId: v1UpgradeStagePost + * deprecated: true + * description: > + * Use [POST /api/v2/upgrade/stage](#/Upgrade/v2UpgradeStagePost) instead. + * Stages an upgrade to the provided version. Does as much of the upgrade as possible without actually + * swapping versions over and restarting. An upgrade has been staged when the `horti-upgrade` document + * has `"action": "stage"` and `"staging_complete": true`. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpgradeRequestBody' + * responses: + * '200': + * description: Staging initiated + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/upgrade/stage: + * post: + * summary: Stage an upgrade + * operationId: v2UpgradeStagePost + * description: > + * Stages an upgrade to the provided version. Does as much of the upgrade as possible without actually + * swapping versions over and restarting. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpgradeRequestBody' + * responses: + * '200': + * description: Staging initiated + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ stage: (req, res) => upgrade(req, res, true), + + /** + * @openapi + * /api/v1/upgrade/complete: + * post: + * summary: Complete a staged upgrade + * operationId: v1UpgradeCompletePost + * deprecated: true + * description: > + * Use [POST /api/v2/upgrade/complete](#/Upgrade/v2UpgradeCompletePost) instead. + * Completes a staged upgrade. Returns a 404 if there is no upgrade in the staged position. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: Upgrade completed + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + * /api/v2/upgrade/complete: + * post: + * summary: Complete a staged upgrade + * operationId: v2UpgradeCompletePost + * description: > + * Completes a staged upgrade. Returns a 404 if there is no upgrade in the staged position. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: Upgrade completed + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ complete: completeUpgrade, + + /** + * @openapi + * /api/v2/upgrade: + * get: + * summary: Get upgrade status + * operationId: v2UpgradeGet + * description: Returns the current upgrade status, indexer progress, and builds server URL. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: The current upgrade status + * content: + * application/json: + * schema: + * type: object + * properties: + * upgradeDoc: + * description: The current upgrade document, or null if no upgrade is in progress. + * indexers: + * description: Current indexer progress information. + * buildsUrl: + * type: string + * description: The URL of the builds server. + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ upgradeInProgress: upgradeInProgress, + + /** + * @openapi + * /api/v2/upgrade: + * delete: + * summary: Abort an upgrade + * operationId: v2UpgradeDelete + * description: Aborts an in-progress upgrade. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: Upgrade aborted + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ abort: abortUpgrade, + + /** + * @openapi + * /api/v2/upgrade/service-worker: + * post: + * summary: Update the service worker + * operationId: v2UpgradeServiceWorkerPost + * description: Triggers an update of the service worker cache. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * responses: + * '200': + * description: Service worker updated + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ serviceWorker: (req, res) => { return checkAuth(req) .then(() => configWatcher.updateServiceWorker()) .then(() => res.json({ ok: true })) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v2/upgrade/compare: + * post: + * summary: Compare build versions + * operationId: v2UpgradeComparePost + * description: > + * Compares the provided build version against the currently deployed version, returning + * differences in design documents. + * tags: + * - Upgrade + * x-permissions: + * hasAll: [can_upgrade] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpgradeRequestBody' + * responses: + * '200': + * description: Comparison results + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * additionalProperties: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ compare: compareUpgrade, }; From d4cc9fbeaaa59e871205a60908f8f831d3ccada9 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 09:04:33 -0500 Subject: [PATCH 14/37] Add docs for africas-talking endpoints --- api/src/controllers/africas-talking.js | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/api/src/controllers/africas-talking.js b/api/src/controllers/africas-talking.js index 2420a14fff3..099677cf47c 100644 --- a/api/src/controllers/africas-talking.js +++ b/api/src/controllers/africas-talking.js @@ -46,7 +46,62 @@ const validateKey = req => { }); }; +/** + * @openapi + * tags: + * - name: SMS + * description: Operations for SMS messaging integrations + */ module.exports = { + /** + * @openapi + * /api/v1/sms/africastalking/incoming-messages: + * post: + * summary: Receive incoming SMS from Africa's Talking + * operationId: v1SmsAfricasTalkingIncomingMessagesPost + * description: > + * Webhook endpoint for receiving incoming SMS messages from the Africa's Talking gateway. + * Requires a valid incoming key passed as a query parameter. See the + * [documentation](/building/messaging/gateways/africas-talking/) for more details. + * tags: + * - SMS + * parameters: + * - in: query + * name: key + * required: true + * schema: + * type: string + * description: The configured incoming key for authentication. + * requestBody: + * required: true + * content: + * application/x-www-form-urlencoded: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The Africa's Talking message id. + * from: + * type: string + * description: The sender's phone number. + * text: + * type: string + * description: The message content. + * required: [id, from, text] + * responses: + * '200': + * description: Message processing results + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '403': + * $ref: '#/components/responses/Forbidden' + */ incomingMessages: (req, res) => { return validateKey(req) .then(() => { @@ -63,6 +118,56 @@ module.exports = { .then(results => res.json(results)) .catch(err => serverUtils.error(err, req, res)); }, + /** + * @openapi + * /api/v1/sms/africastalking/delivery-reports: + * post: + * summary: Receive delivery reports from Africa's Talking + * operationId: v1SmsAfricasTalkingDeliveryReportsPost + * description: > + * Webhook endpoint for receiving SMS delivery status reports from the Africa's Talking gateway. + * Requires a valid incoming key passed as a query parameter. See the + * [documentation](/building/messaging/gateways/africas-talking/) for more details. + * tags: + * - SMS + * parameters: + * - in: query + * name: key + * required: true + * schema: + * type: string + * description: The configured incoming key for authentication. + * requestBody: + * required: true + * content: + * application/x-www-form-urlencoded: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The gateway message reference. + * status: + * type: string + * enum: [Sent, Submitted, Buffered, Rejected, Success, Failed] + * description: The delivery status from Africa's Talking. + * failureReason: + * type: string + * description: The reason for failure, if applicable. + * required: [id, status] + * responses: + * '200': + * description: Delivery report processing results + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '403': + * $ref: '#/components/responses/Forbidden' + */ deliveryReports: (req, res) => { return validateKey(req) .then(() => { From 59e2e62951b86448661840d17d56c4a10006c72d Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 10:10:09 -0500 Subject: [PATCH 15/37] Add docs for rapidpro endpoints --- api/src/controllers/rapidpro.js | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/api/src/controllers/rapidpro.js b/api/src/controllers/rapidpro.js index fdd2b99fc96..78db46b3dc8 100644 --- a/api/src/controllers/rapidpro.js +++ b/api/src/controllers/rapidpro.js @@ -49,6 +49,95 @@ const validateRequest = req => { }; module.exports = { + /** + * @openapi + * /api/v1/sms/radpidpro/incoming-messages: + * post: + * summary: Receive incoming SMS from RapidPro + * operationId: v1SmsRadpidproProIncomingMessagesPost + * deprecated: true + * description: > + * Use [POST /api/v2/sms/rapidpro/incoming-messages](#/SMS/v2SmsRapidProIncomingMessagesPost) instead. + * Webhook endpoint for receiving incoming SMS messages from the RapidPro gateway. + * Authenticated via an `Authorization: Token ` header. See the + * [documentation](/building/messaging/gateways/rapidpro/) for more details. + * tags: + * - SMS + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The RapidPro message id. + * from: + * type: string + * description: The sender's phone number. + * content: + * type: string + * description: The message content. + * required: [id, from, content] + * responses: + * '200': + * description: Message processing results + * content: + * application/json: + * schema: + * type: object + * properties: + * saved: + * type: number + * description: The number of messages saved. + * '400': + * $ref: '#/components/responses/BadRequest' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/sms/rapidpro/incoming-messages: + * post: + * summary: Receive incoming SMS from RapidPro + * operationId: v2SmsRapidProIncomingMessagesPost + * description: > + * Webhook endpoint for receiving incoming SMS messages from the RapidPro gateway. + * Authenticated via an `Authorization: Token ` header. See the + * [documentation](/building/messaging/gateways/rapidpro/) for more details. + * tags: + * - SMS + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The RapidPro message id. + * from: + * type: string + * description: The sender's phone number. + * content: + * type: string + * description: The message content. + * required: [id, from, content] + * responses: + * '200': + * description: Message processing results + * content: + * application/json: + * schema: + * type: object + * properties: + * saved: + * type: number + * description: The number of messages saved. + * '400': + * $ref: '#/components/responses/BadRequest' + * '403': + * $ref: '#/components/responses/Forbidden' + */ incomingMessages: (req, res) => { return validateRequest(req) .then(() => { From 782dfc4a49e348957bf7ac0c796ddce13a98827c Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 13:17:59 -0500 Subject: [PATCH 16/37] Add docs for sms-gateway endpoints --- api/src/controllers/sms-gateway.js | 105 +++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/api/src/controllers/sms-gateway.js b/api/src/controllers/sms-gateway.js index 79b371b591f..c0eec69888d 100644 --- a/api/src/controllers/sms-gateway.js +++ b/api/src/controllers/sms-gateway.js @@ -68,9 +68,32 @@ const checkAuth = req => auth.check(req, 'can_access_gateway_api'); module.exports = { /** - * Check that the endpoint exists - * @param {Object} req The request - * @param {Object} res The response + * @openapi + * /api/sms: + * get: + * summary: Check SMS gateway connectivity + * operationId: smsGet + * description: > + * Returns a simple response to verify that the cht-gateway SMS endpoint is available. + * See the [cht-gateway documentation](https://github.com/medic/cht-gateway) for more details. + * tags: + * - SMS + * x-permissions: + * hasAll: [can_access_gateway_api] + * responses: + * '200': + * description: Gateway endpoint is available + * content: + * application/json: + * schema: + * type: object + * properties: + * medic-gateway: + * type: boolean + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' */ get: (req, res) => { return checkAuth(req) @@ -78,10 +101,78 @@ module.exports = { .catch(err => serverUtils.error(err, req, res)); }, /** - * Stores new incoming messages, outgoing message status updates, - * and returns outgoing messages that are ready to be sent. - * @param {Object} req The request - * @param {Object} res The response + * @openapi + * /api/sms: + * post: + * summary: Exchange SMS messages with cht-gateway + * operationId: smsPost + * description: > + * Processes incoming messages and delivery status updates from cht-gateway, and returns + * outgoing messages that are ready to be sent. + * See the [cht-gateway documentation](https://github.com/medic/cht-gateway) for more details. + * tags: + * - SMS + * x-permissions: + * hasAll: [can_access_gateway_api] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * messages: + * type: array + * description: Incoming messages to process. + * items: + * type: object + * properties: + * id: + * type: string + * description: The message id. + * from: + * type: string + * description: The sender's phone number. + * content: + * type: string + * description: The message content. + * required: [id, from, content] + * updates: + * type: array + * description: Delivery status updates for previously sent messages. + * items: + * type: object + * properties: + * id: + * type: string + * description: The message id. + * status: + * type: string + * enum: [UNSENT, PENDING, SENT, DELIVERED, FAILED] + * description: The delivery status from the gateway. + * reason: + * type: string + * description: The reason for failure, if applicable. + * required: [id, status] + * required: [messages] + * responses: + * '200': + * description: Outgoing messages ready to be sent + * content: + * application/json: + * schema: + * type: object + * properties: + * messages: + * type: array + * description: Outgoing messages for the gateway to send. + * items: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' */ post: (req, res) => { return checkAuth(req) From 3c1f0b5291a8717a6ca39015e72b633eaf242607 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 14:30:42 -0500 Subject: [PATCH 17/37] Add docs for export endpoints --- api/src/controllers/export-data.js | 561 ++++++++++++++++++++++++++++- 1 file changed, 557 insertions(+), 4 deletions(-) diff --git a/api/src/controllers/export-data.js b/api/src/controllers/export-data.js index 6ef04ca4287..9351c396433 100644 --- a/api/src/controllers/export-data.js +++ b/api/src/controllers/export-data.js @@ -8,10 +8,6 @@ const service = require('../services/export-data'); const writeExportHeaders = (res, type, format) => { const formats = { - xml: { - extension: 'xml', - contentType: 'application/vnd.ms-excel' - }, csv: { extension: 'csv', contentType: 'text/csv' @@ -41,7 +37,564 @@ const getVerifiedValue = (value) => { return null; }; +/** + * @openapi + * tags: + * - name: Export + * description: Export data in various formats + * components: + * schemas: + * ExportOptions: + * type: object + * properties: + * humanReadable: + * type: boolean + * description: Set to true to format dates as ISO 8601 instead of epoch timestamps. + * additionalProperties: true + * UserDevice: + * type: object + * properties: + * user: + * type: string + * description: The user's name. + * deviceId: + * type: string + * description: The unique key for the user's device. + * date: + * type: string + * description: The date the telemetry entry was taken (YYYY-MM-DD). + * browser: + * type: object + * properties: + * name: + * type: string + * description: The name of the browser used. + * version: + * type: string + * description: The version of the browser used. + * apk: + * type: string + * description: The version code of the Android app. + * android: + * type: string + * description: The version of Android OS. + * cht: + * type: string + * description: The version of CHT at time of telemetry. + * settings: + * type: string + * description: The revision of the App Settings document. + * storageFree: + * type: number + * description: Free storage space on the device in bytes. Added in CHT 5.0.0. + * storageTotal: + * type: number + * description: Total storage capacity of the device in bytes. Added in CHT 5.0.0. + * parameters: + * exportOptionsQuery: + * in: query + * name: options + * description: Export options. + * style: deepObject + * explode: true + * schema: + * type: object + * properties: + * humanReadable: + * enum: ['true'] + * description: Set to "true" to format dates as ISO 8601 instead of epoch timestamps. + * responses: + * CsvExport: + * description: The exported data as a CSV file download + * content: + * text/csv: + * schema: + * type: string + * format: binary + * UserDeviceExport: + * description: User device information + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/UserDevice' + */ module.exports = { + /** + * @openapi + * /api/v2/export/dhis: + * get: + * summary: Export DHIS2 target data + * operationId: v2ExportDhisGet + * description: > + * Exports target data formatted as a DHIS2 dataValueSet. The data can be filtered to a specific section + * of the contact hierarchy or for a given time interval. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_dhis] + * parameters: + * - in: query + * name: filters + * schema: + * type: object + * properties: + * dataSet: + * type: string + * description: > + * A DHIS2 dataSet ID. Targets associated with this dataSet will have their data aggregated. + * date: + * type: object + * required: [from] + * properties: + * from: + * type: number + * description: Filter the target data to be within the month of this timestamp. + * orgUnit: + * type: string + * description: > + * Filter the target data to only that associated with contacts with attribute + * `{ dhis: { orgUnit } }`. + * required: [dataSet, date] + * style: deepObject + * explode: true + * description: Filters for the DHIS2 export. + * - $ref: '#/components/parameters/exportOptionsQuery' + * responses: + * '200': + * description: DHIS2 dataValueSet + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export DHIS2 target data + * operationId: v2ExportDhisPost + * description: > + * Exports target data formatted as a DHIS2 dataValueSet. Accepts filters in the request body. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_dhis] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * filters: + * type: object + * properties: + * dataSet: + * type: string + * description: A DHIS2 dataSet ID. + * date: + * type: object + * required: [from] + * properties: + * from: + * type: number + * description: Filter target data to be within the month of this timestamp. + * orgUnit: + * type: string + * description: Filter by contacts with this DHIS2 orgUnit attribute. + * required: [dataSet, date] + * options: + * $ref: '#/components/schemas/ExportOptions' + * responses: + * '200': + * description: DHIS2 dataValueSet + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/export/reports: + * get: + * summary: Export reports + * operationId: v2ExportReportsGet + * description: > + * Exports reports as CSV. Uses the + * [search library](https://github.com/medic/cht-core/tree/master/shared-libs/search) to ensure identical + * results to the front-end. Filters can be passed as query parameters using form-style encoding + * (e.g. `filters[forms][selected][0][code]=immunization_visit`). + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_messages] + * parameters: + * - in: query + * name: filters + * schema: + * type: object + * properties: + * search: + * type: string + * description: A freetext search term. + * forms: + * type: object + * description: Filter by form codes. + * properties: + * selected: + * type: array + * items: + * type: object + * properties: + * code: + * type: string + * date: + * type: object + * properties: + * from: + * type: number + * description: Start of date range (epoch timestamp). + * to: + * type: number + * description: End of date range (epoch timestamp). + * verified: + * type: string + * description: Filter by verification status ("true", "false"). + * valid: + * type: string + * description: Filter by validity ("true"). + * additionalProperties: true + * style: deepObject + * explode: true + * description: Filters for the reports export. + * - $ref: '#/components/parameters/exportOptionsQuery' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export reports + * operationId: v2ExportReportsPost + * description: Exports reports as CSV. Accepts filters in the request body. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_messages] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * filters: + * type: object + * properties: + * search: + * type: string + * description: A freetext search term. + * forms: + * type: object + * description: Filter by form codes. + * properties: + * selected: + * type: array + * items: + * type: object + * properties: + * code: + * type: string + * date: + * type: object + * properties: + * from: + * type: number + * description: Start of date range (epoch timestamp). + * to: + * type: number + * description: End of date range (epoch timestamp). + * verified: + * description: Filter by verification status. + * oneOf: + * - type: boolean + * - type: array + * items: + * type: boolean + * valid: + * type: boolean + * description: Filter by validity. + * additionalProperties: true + * options: + * $ref: '#/components/schemas/ExportOptions' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/export/messages: + * get: + * summary: Export messages + * operationId: v2ExportMessagesGet + * description: | + * Exports messages as CSV. + * + * ### Columns + * + * | Column | Description | + * | ------------------ | ------------------------------------------------------------------------------------ | + * | Record UUID | The unique ID for the message in the database. | + * | Patient ID | The generated short patient ID for use in SMS. | + * | Reported Date | The date the message was received or generated. | + * | From | This phone number the message is or will be sent from. | + * | Contact Name | The name of the user this message is assigned to. | + * | Message Type | The type of the message | + * | Message State | The state of the message at the time this export was generated | + * | Received Timestamp | The datetime the message was received. Only applies to incoming messages. | + * | Other Timestamps | The datetime the message transitioned to each state. | + * | Sent By | The phone number the message was sent from. Only applies to incoming messages. | + * | To Phone | The phone number the message is or will be sent to. Only applies to outgoing. | + * | Message Body | The content of the message. | + * + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_messages] + * parameters: + * - in: query + * name: filters + * schema: + * type: object + * properties: + * date: + * type: object + * properties: + * from: + * type: number + * description: Start of date range (epoch timestamp). + * to: + * type: number + * description: End of date range (epoch timestamp). + * additionalProperties: true + * style: deepObject + * explode: true + * description: Filters for the messages export. + * - $ref: '#/components/parameters/exportOptionsQuery' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export messages + * operationId: v2ExportMessagesPost + * description: > + * Exports messages as CSV. Accepts filters in the request body. See + * [GET](#/Export/v2ExportMessagesGet) for output column details. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_messages] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * filters: + * type: object + * properties: + * date: + * type: object + * properties: + * from: + * type: number + * description: Start of date range (epoch timestamp). + * to: + * type: number + * description: End of date range (epoch timestamp). + * additionalProperties: true + * options: + * $ref: '#/components/schemas/ExportOptions' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/export/contacts: + * get: + * summary: Export contacts + * operationId: v2ExportContactsGet + * description: | + * Exports contacts as a CSV. Filters use the same query format as the + * [search library](https://github.com/medic/cht-core/tree/master/shared-libs/search). + * + * ### Columns + * + * | Column | Description | + * | -------------| -------------------------------------------------------------------------------| + * | id | The unique ID for the contact in the database. | + * | rev | The current CouchDb revision of contact in the database. | + * | name | The name of the user this message is assigned to. | + * | patient_id | The generated short patient ID for use in SMS. | + * | type | The contact type. For configurable hierarchies, this will always be `contact`. | + * | contact_type | The configurable contact type. Will be empty if using the default hierarchy. | + * | place_id | The generated short place ID for use in SMS. | + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_contacts] + * parameters: + * - in: query + * name: filters + * schema: + * type: object + * properties: + * search: + * type: string + * description: A freetext search term. + * additionalProperties: true + * style: deepObject + * explode: true + * description: Filters for the contacts export. + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export contacts + * operationId: v2ExportContactsPost + * description: > + * Exports contacts as a CSV. Accepts filters in the request body. See [GET](#/Export/v2ExportContactsGet) for + * output column details. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_contacts] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * filters: + * type: object + * properties: + * search: + * type: string + * description: A freetext search term. + * additionalProperties: true + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/export/feedback: + * get: + * summary: Export feedback + * operationId: v2ExportFeedbackGet + * description: Exports user feedback data as CSV. + * tags: + * - Export + * x-permissions: + * hasAny: [can_export_all, can_export_feedback] + * parameters: + * - $ref: '#/components/parameters/exportOptionsQuery' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export feedback + * operationId: v2ExportFeedbackPost + * description: Exports user feedback data as CSV. Accepts options in the request body. + * tags: + * - Export + * x-permissions: + * isOnline: true + * hasAny: [can_export_all, can_export_feedback] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * options: + * $ref: '#/components/schemas/ExportOptions' + * responses: + * '200': + * $ref: '#/components/responses/CsvExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * /api/v2/export/user-devices: + * get: + * summary: Export user device information + * operationId: v2ExportUserDevicesGet + * description: | + * Returns a JSON array of CHT-related software versions and device information for each user device. + * This information is derived from the latest telemetry entry for each user device. + * + * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using + * this endpoint or use it only when end users will not be impacted. An improved endpoint is + * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. + * tags: + * - Export + * deprecated: true + * x-since: 4.7.0 + * x-permissions: + * hasAny: [can_export_all, can_export_devices_details] + * responses: + * '200': + * $ref: '#/components/responses/UserDeviceExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Export user device information + * operationId: v2ExportUserDevicesPost + * description: | + * Returns a JSON array of CHT-related software versions and device information for each user device. + * This endpoint may negatively impact server performance. + * + * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using + * this endpoint or use it only when end users will not be impacted. An improved endpoint is + * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. + * tags: + * - Export + * deprecated: true + * x-since: 4.7.0 + * x-permissions: + * hasAny: [can_export_all, can_export_devices_details] + * responses: + * '200': + * $ref: '#/components/responses/UserDeviceExport' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ get: (req, res) => { /** * Integer values get parsed in by express as strings. This will not do! From 519f848358f48552caa2a7c58f80173ed1bebc89 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 15:05:32 -0500 Subject: [PATCH 18/37] Add docs for record endpoints --- api/src/controllers/records.js | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/api/src/controllers/records.js b/api/src/controllers/records.js index c009cacf39d..0561ebe7bef 100644 --- a/api/src/controllers/records.js +++ b/api/src/controllers/records.js @@ -32,12 +32,135 @@ const process = (req, res, options) => { .catch(err => serverUtils.error(err, req, res)); }; +/** + * @openapi + * tags: + * - name: Records + * description: Operations for creating records for JSON forms + * components: + * schemas: + * RecordSuccess: + * type: object + * properties: + * success: + * type: boolean + * id: + * type: string + * description: The id of the created record. + * required: [success, id] + * requestBodies: + * RecordInput: + * required: true + * content: + * application/x-www-form-urlencoded: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Message string in a supported format (e.g. Muvuku or Textforms). + * from: + * type: string + * description: Reporting phone number. + * sent_timestamp: + * type: number + * description: > + * Timestamp in ms since Unix Epoch of when the message was received on the gateway. + * Defaults to now. + * required: [message, from] + * application/json: + * schema: + * type: object + * description: > + * Form field values as properties. Special values reside in `_meta`. Only strings and + * numbers are supported as field values. All property names will be lowercased. + * properties: + * _meta: + * type: object + * properties: + * form: + * type: string + * description: The form code. + * from: + * type: string + * description: Reporting phone number. Optional. + * reported_date: + * type: number + * description: > + * Timestamp in ms since Unix Epoch of when the message was received on the + * gateway. Defaults to now. + * locale: + * type: string + * description: "Optional locale string (e.g. 'fr')." + * required: [form] + * additionalProperties: true + */ module.exports = { + /** + * @openapi + * /api/v1/records: + * post: + * summary: Create a record + * operationId: v1RecordsPost + * deprecated: true + * description: | + * Use [POST /api/v2/records](#/Records/v2RecordsPost) instead. + * + * Creates a new record based on a configured form. Accepts form-encoded or JSON data. + * tags: + * - Records + * x-permissions: + * hasAll: [can_create_records] + * parameters: + * - in: query + * name: locale + * schema: + * type: string + * description: Optional locale string (e.g. `fr`). + * requestBody: + * $ref: '#/components/requestBodies/RecordInput' + * responses: + * '200': + * description: Record created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecordSuccess' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ v1: (req, res) => { return process(req, res, { locale: req.query && req.query.locale }); }, + /** + * @openapi + * /api/v2/records: + * post: + * summary: Create a record + * operationId: v2RecordsPost + * description: Creates a new record based on a configured form. Accepts either form-encoded or JSON data. + * tags: + * - Records + * x-permissions: + * hasAll: [can_create_records] + * requestBody: + * $ref: '#/components/requestBodies/RecordInput' + * responses: + * '200': + * description: Record created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecordSuccess' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ // dropped support for locale because it makes no sense v2: (req, res) => { return process(req, res); From 9cde9b21301452d4fbc56a3e0e03bdfe58c9a4e1 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 18 Mar 2026 15:16:03 -0500 Subject: [PATCH 19/37] Add docs for forms endpoints --- api/src/controllers/forms.js | 144 +++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/api/src/controllers/forms.js b/api/src/controllers/forms.js index 6d6044894fc..756200ad4bb 100644 --- a/api/src/controllers/forms.js +++ b/api/src/controllers/forms.js @@ -54,7 +54,71 @@ const listFormsJSON = forms => { return JSON.stringify(forms.map(form => form.internalId + '.xml')); }; +/** + * @openapi + * tags: + * - name: Forms + * description: Operations for XForms + */ module.exports = { + /** + * @openapi + * /api/v1/forms: + * get: + * summary: List installed forms + * operationId: v1FormsGet + * description: > + * Returns a list of currently installed forms. By default returns a JSON array of form filenames. If the + * `X-OpenRosa-Version` header is set to `1.0`, returns an OpenRosa xformsList compatible XML response instead. + * tags: + * - Forms + * parameters: + * - in: header + * name: X-OpenRosa-Version + * schema: + * enum: ['1.0'] + * description: If set to "1.0", returns XML formatted forms list compatible with the OpenRosa FormListAPI. + * responses: + * '200': + * description: List of installed forms + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: ["anc_visit.xml", "anc_registration.xml"] + * text/xml: + * schema: + * type: string + * description: OpenRosa xformsList compatible XML. + * example: | + * ```.xml + * + * + * + * Visit + * ANCVisit + * md5:1f0f096602ed794a264ab67224608cf4 + * http://medic.local/api/v1/forms/anc_visit.xml + * + * + * Registration with LMP + * PregnancyRegistration + * md5:1f0f096602ed794a264ab67224608cf4 + * http://medic.local/api/v1/forms/anc_registration.xml + * + * + * Stop + * Stop + * md5:1f0f096602ed794a264ab67224608cf4 + * http://medic.local/api/v1/forms/off.xml + * + * + * ``` + * '401': + * $ref: '#/components/responses/Unauthorized' + */ list: (req, res) => { if (!req.userCtx) { return serverUtils.notLoggedIn(req, res); @@ -72,6 +136,40 @@ module.exports = { }) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v1/forms/{form}: + * get: + * summary: Get a form definition + * operationId: v1FormsFormGet + * description: > + * Returns the form definition for a given form ID and format. The form parameter should + * include the format extension (e.g. `pregnancyregistration.xml`). Currently only `xml` + * format is supported. + * tags: + * - Forms + * parameters: + * - in: path + * name: form + * required: true + * schema: + * type: string + * description: "Form identifier with format extension (e.g. `pregnancyregistration.xml`)." + * responses: + * '200': + * description: The form definition + * content: + * text/xml: + * schema: + * type: string + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: (req, res) => { if (!req.userCtx) { return serverUtils.notLoggedIn(req, res); @@ -110,6 +208,52 @@ module.exports = { }) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v1/forms/validate: + * post: + * summary: Validate an XForm + * operationId: v1FormsValidatePost + * description: > + * Validates the XForm XML passed in the request body. + * tags: + * - Forms + * x-since: 3.12.0 + * x-permissions: + * hasAll: [can_configure] + * requestBody: + * required: true + * content: + * application/xml: + * schema: + * type: string + * description: The XForm XML to validate. + * responses: + * '200': + * description: Form validation passed + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * '400': + * description: Form validation failed + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Description of the validation error. + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ validate: (req, res) => { return auth .check(req, 'can_configure') From 167fcf6351473adfddcec4743ba863ce27876f6d Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 09:21:03 -0500 Subject: [PATCH 20/37] Add docs for users endpoints --- api/src/controllers/users.js | 615 +++++++++++++++++++++++++++++++++++ 1 file changed, 615 insertions(+) diff --git a/api/src/controllers/users.js b/api/src/controllers/users.js index 25a0cb4fc03..b1bf53a550b 100644 --- a/api/src/controllers/users.js +++ b/api/src/controllers/users.js @@ -172,13 +172,207 @@ const verifyUpdateRequest = async (req) => { return { fullPermission }; }; +/** + * @openapi + * tags: + * - name: Users + * description: Operations for user management + * components: + * schemas: + * UserInput: + * type: object + * required: [username, roles] + * properties: + * username: + * type: string + * description: Identifier used for authentication. + * roles: + * type: array + * items: + * type: string + * place: + * description: > + * Place identifier or object representing the place the user resides in. Required if roles contain an + * offline role. + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * contact: + * description: > + * Person identifier or object representing the person contact for the user. Required if roles + * contain an offline role. + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * password: + * type: string + * description: > + * Password for authentication. Must be at least 8 characters long and difficult to guess. Required if + * `token_login` or `oidc_username` is not set. + * phone: + * type: string + * description: Valid phone number. Required if `token_login` is set. + * token_login: + * type: boolean + * description: > + * Sets [login by SMS](/building/reference/app-settings/token_login) for this user. Added in `3.10.0`. + * fullname: + * type: string + * email: + * type: string + * known: + * type: boolean + * description: Indicates if the user has logged in before. + * password_change_required: + * type: boolean + * description: > + * Set `false` to skip [password reset prompt](/building/login/#password-reset-on-first-login) on next + * login. Added in `4.17.0`. + * oidc_username: + * type: string + * description: > + * Unique username for [authenticating via OIDC](/building/reference/api/#login-by-oidc). This + * value must match the `email` claim returned for the user by the OIDC provider. Added in `4.20.0`. + * UserCreateResult: + * type: object + * properties: + * contact: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * user-settings: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * user: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * InvalidUserInputError: + * type: object + * additionalProperties: true + * properties: + * error: + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * examples: + * - error: + * message: 'Username "mary" already taken.' + * translationKey: username.taken + * translationParams: + * username: mary + * - error: 'Missing required fields: username, password, type or roles' + * details: + * failingIndexes: + * - fields: [username, password, 'type or roles'] + * index: 0 + * - fields: [password, 'type or roles'] + * index: 1 + */ module.exports = { + /** + * @openapi + * /api/v1/users: + * get: + * summary: List users + * operationId: v1UsersGet + * deprecated: true + * description: > + * Use [GET /api/v2/users](#/Users/v2UsersGet) instead. + * Returns a list of users and their profile data. + * tags: + * - Users + * x-permissions: + * hasAll: [can_view_users] + * responses: + * '200': + * description: A list of users + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ list: (req, res) => { return getUserList(req) .then(list => convertUserListToV1(list)) .then(body => res.json(body)) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v1/users: + * post: + * summary: Create users + * operationId: v1UsersPost + * deprecated: true + * description: | + * Use [POST /api/v3/users](#/Users/v3UsersPost) to create a single user or + * [POST /api/v2/users](#/Users/v2UsersPost) to create multiple users. + * Create new users with a place and a contact. Accepts a single user object or an array of user objects + * (since CHT `3.15.0`). Users are created in parallel and the creation is not aborted even if one of the + * users fails to be created. + * + * Passing a single user in the request’s body will return a single object whereas passing an array of users + * will return an array of objects. + * tags: + * - Users + * x-permissions: + * hasAll: [can_edit, can_create_users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/UserInput' + * - type: array + * items: + * $ref: '#/components/schemas/UserInput' + * responses: + * '200': + * description: User creation results + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/UserCreateResult' + * - type: array + * items: + * oneOf: + * - $ref: '#/components/schemas/UserCreateResult' + * - $ref: '#/components/schemas/InvalidUserInputError' + * '400': + * description: Invalid user data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidUserInputError' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ create: (req, res) => { return auth .check(req, ['can_edit', 'can_create_users']) @@ -186,6 +380,73 @@ module.exports = { .then(body => res.json(body)) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v1/users/{username}: + * post: + * summary: Update a user + * operationId: v1UsersUsernamePost + * deprecated: true + * description: > + * Use [POST /api/v3/users/{username}](#/Users/v3UsersUsernamePost) instead. + * Update property values on a user account. Users with `can_edit` and `can_update_users` + * permissions can update any user. Users can update themselves without these permissions, + * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require + * Basic Auth (either the header or in the URL). This is to ensure the password is known at + * time of request, and no one is hijacking a cookie. + * tags: + * - Users + * x-permissions: + * hasAll: [can_edit, can_update_users] + * parameters: + * - in: path + * name: username + * required: true + * schema: + * type: string + * description: The username of the user to update. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * description: User properties to update. + * responses: + * '200': + * description: User updated + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * user-settings: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * '400': + * description: Invalid user data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidUserInputError' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ update: async (req, res) => { if (_.isEmpty(req.body)) { return serverUtils.emptyJSONBodyError(req, res); @@ -209,6 +470,38 @@ module.exports = { serverUtils.error(err, req, res); } }, + + /** + * @openapi + * /api/v1/users/{username}: + * delete: + * summary: Delete a user + * operationId: v1UsersUsernameDelete + * description: Delete a user. Does not affect the person or place associated with the user. + * tags: + * - Users + * x-permissions: + * hasAll: [can_edit, can_delete_users] + * parameters: + * - in: path + * name: username + * required: true + * schema: + * type: string + * description: The username of the user to delete. + * responses: + * '200': + * description: User deleted + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ delete: (req, res) => { auth .check(req, ['can_edit', 'can_delete_users']) @@ -216,6 +509,68 @@ module.exports = { .then(result => res.json(result)) .catch(err => serverUtils.error(err, req, res)); }, + + /** + * @openapi + * /api/v1/users-info: + * get: + * summary: Get user replication info + * operationId: v1UsersInfoGet + * description: | + * Returns the total number of documents an offline user would replicate (`total_docs`), the number of + * docs excluding tasks the user would replicate (`warn_docs`), and a warning flag (`warn`) if the count + * exceeds the recommended limit (`10,000`). + * + * Offline users can get their own doc count. Online users with `can_update_users` permission + * can query for any user by providing `role` and `facility_id` query parameters. (When requested as an + * online user, the number of tasks are never counted and never returned, so `warn_docs` is always equal to + * `total_docs`.) + * tags: + * - Users + * parameters: + * - in: query + * name: facility_id + * schema: + * type: string + * description: UUID of the user's facility. Required for online users. + * - in: query + * name: role + * schema: + * type: string + * description: > + * User role (must be an offline role). Accepts a string or JSON array. Required for online users. + * - in: query + * name: contact_id + * schema: + * type: string + * description: UUID of the user's associated contact. Optional for online users. + * responses: + * '200': + * description: User replication info + * content: + * application/json: + * schema: + * type: object + * properties: + * total_docs: + * type: number + * description: Total number of documents the user would replicate. + * warn_docs: + * type: number + * description: Number of docs excluding tasks the user would replicate. + * warn: + * type: boolean + * description: Whether the doc count exceeds the recommended limit. + * limit: + * type: number + * description: The recommended document limit. + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ info: (req, res) => { let userCtx; try { @@ -229,6 +584,63 @@ module.exports = { }, v2: { + /** + * @openapi + * /api/v2/users/{username}: + * get: + * summary: Get a user by username. + * operationId: v2UsersUsernameGet + * description: > + * Returns a user's profile data. Users with `can_view_users` permission can view any + * user. Users can also view their own profile. + * tags: + * - Users + * x-since: 4.7.0 + * x-permissions: + * hasAll: [can_view_users] + * parameters: + * - in: path + * name: username + * required: true + * schema: + * type: string + * description: The username to retrieve. + * responses: + * '200': + * description: The user profile + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * example: + * id: org.couchdb.user:demo + * rev: 14-8758c8493edcc6dac50366173fc3e24a + * type: district-manager + * fullname: Example User + * username: demo + * oidc_username: demo@email.com + * place: + * _id: eeb17d6d-5dde-c2c0-62c4a1a0ca17d38b + * type: district_hospital + * name: Sample District + * contact: + * _id: eeb17d6d-5dde-c2c0-62c4a1a0ca17fd17 + * type: person + * name: Paul + * phone: '+2868917046' + * contact: + * _id: eeb17d6d-5dde-c2c0-62c4a1a0ca17fd17 + * type: person + * name: Paul + * phone: '+2868917046' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ get: async (req, res) => { try { const userCtx = await auth.getUserCtx(req); @@ -243,6 +655,45 @@ module.exports = { serverUtils.error(err, req, res); } }, + + /** + * @openapi + * /api/v2/users: + * get: + * summary: List users + * operationId: v2UsersGet + * description: Returns a list of users and their profile data including roles. + * tags: + * - Users + * x-since: 4.1.0 + * x-permissions: + * hasAll: [can_view_users] + * parameters: + * - in: query + * name: facility_id + * schema: + * type: string + * description: Filter by facility UUID. Added in `4.7.0`. + * - in: query + * name: contact_id + * schema: + * type: string + * description: Filter by associated contact UUID. Added in `4.7.0`. + * responses: + * '200': + * description: A list of users + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ list: async (req, res) => { try { const body = await getUserList(req); @@ -251,6 +702,70 @@ module.exports = { serverUtils.error(err, req, res); } }, + + /** + * @openapi + * /api/v2/users: + * post: + * summary: Create users - bulk import (JSON or CSV) + * operationId: v2UsersPost + * description: | + * Create new users with a place and a contact. Accepts either a JSON array of user objects or a CSV file + * where each row represents a user. A log entry is created for each bulk import in the `medic-logs` database. + * + * ### Example + * + * A spreadsheet compatible with the default configuration of the CHT is available. + * [Click here](https://docs.google.com/spreadsheets/d/1yUenFP-5deQ0I9c-OYDTpbKYrkl3juv9djXoLLPoQ7Y/copy) to + * make a copy of the spreadsheet in Google Sheets. [A guide](/building/training/users-bulk-load) on how to + * import users with this spreadsheet from within the Admin Console (without manually calling this endpoint) + * is available. + * + * ### Logging + * + * A log entry is created with each bulk import that contains the import status for each user and the import + * progress status that gets updated throughout the import and finalized upon completion. These entries are + * saved in the `medic-logs` database and you can access them by querying documents with a key that starts + * with `bulk-user-upload-`. + * tags: + * - Users + * x-since: 3.16.0 + * x-permissions: + * hasAll: [can_edit, can_create_users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/UserInput' + * - type: array + * items: + * $ref: '#/components/schemas/UserInput' + * text/csv: + * schema: + * type: string + * description: > + * CSV with headers matching user properties. Columns with `:excluded` suffix + * are ignored. + * responses: + * '200': + * description: User creation results + * content: + * application/json: + * schema: + * type: array + * items: + * oneOf: + * - $ref: '#/components/schemas/UserCreateResult' + * - $ref: '#/components/schemas/InvalidUserInputError' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ create: async (req, res) => { try { await auth.check(req, ['can_edit', 'can_create_users']); @@ -279,6 +794,42 @@ module.exports = { }, }, v3: { + + /** + * @openapi + * /api/v3/users: + * post: + * summary: Create a user + * operationId: v3UsersPost + * description: Creates a user that can be associated with multiple facilities. + * tags: + * - Users + * x-permissions: + * hasAll: [can_edit, can_create_users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserInput' + * responses: + * '200': + * description: User creation result + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserCreateResult' + * '400': + * description: Invalid user data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidUserInputError' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ create: async (req, res) => { try { await auth.check(req, ['can_edit', 'can_create_users']); @@ -289,6 +840,70 @@ module.exports = { serverUtils.error(error, req, res); } }, + /** + * @openapi + * /api/v3/users/{username}: + * post: + * summary: Update a user + * operationId: v3UsersUsernamePost + * description: > + * Update property values on a user account. Users with `can_edit` and `can_update_users` + * permissions can update any user. Users can update themselves without these permissions, + * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require + * Basic Auth (either the header or in the URL). This is to ensure the password is known at + * time of request, and no one is hijacking a cookie. + * tags: + * - Users + * x-permissions: + * hasAll: [can_edit, can_update_users] + * parameters: + * - in: path + * name: username + * required: true + * schema: + * type: string + * description: The username of the user to update. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * description: User properties to update. + * responses: + * '200': + * description: User updated + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * user-settings: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * '400': + * description: Invalid user data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidUserInputError' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ update: (req, res) => { return module.exports.update(req, res); }, From f759f7ec268fc4595bb3a6031afeb1d033496876 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 09:28:49 -0500 Subject: [PATCH 21/37] Add docs for places endpoints --- api/src/routing.js | 109 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/api/src/routing.js b/api/src/routing.js index 475382dfa2a..133377c3256 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -514,6 +514,67 @@ app.postJson('/api/v3/users/:username', users.v3.update); app.delete('/api/v1/users/:username', users.delete); app.get('/api/v1/users-info', authorization.handleAuthErrors, authorization.getUserSettings, users.info); +/** + * @openapi + * /api/v1/places: + * post: + * summary: Create a place + * operationId: v1PlacesPost + * deprecated: true + * description: > + * Use [POST /api/v1/place](#/Place/v1PlacePost) instead. + * Create a new place and optionally a contact. The parent can be referenced by UUID or + * created inline. A contact can also be created inline or referenced by UUID. + * tags: + * - Place + * x-permissions: + * hasAll: [can_edit, can_create_places] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: The name of the place. + * type: + * type: string + * description: "The place type (e.g. `clinic`, `health_center`, `district_hospital`)." + * parent: + * description: Parent place UUID or inline object. Required unless creating a top-level place. + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * contact: + * description: Contact person UUID or inline object. + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * required: [name, type] + * additionalProperties: true + * responses: + * '200': + * description: Place created + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ app.postJson('/api/v1/places', function(req, res) { auth .check(req, ['can_edit', 'can_create_places']) @@ -526,6 +587,54 @@ app.postJson('/api/v1/places', function(req, res) { .catch(err => serverUtils.error(err, req, res)); }); +/** + * @openapi + * /api/v1/places/{id}: + * post: + * summary: Update a place + * operationId: v1PlacesIdPost + * deprecated: true + * description: > + * Use [PUT /api/v1/place/{id}](#/Place/v1PlaceIdPut) instead. + * Update a place and optionally its contact. + * tags: + * - Place + * x-permissions: + * hasAll: [can_edit, can_update_places] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The id of the place to update. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * description: Place properties to update. + * responses: + * '200': + * description: Place updated + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ app.postJson('/api/v1/places/:id', function(req, res) { auth .check(req, ['can_edit', 'can_update_places']) From ae2795ceaa2aa7657f9ff5c4690da4fedcff8c97 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 09:44:07 -0500 Subject: [PATCH 22/37] Add docs for people endpoints --- api/src/routing.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/api/src/routing.js b/api/src/routing.js index 133377c3256..09e165a59d7 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -654,6 +654,64 @@ app.get('/api/v1/place/:uuid', place.v1.get); app.postJson('/api/v1/place', place.v1.create); app.putJson('/api/v1/place/:uuid', place.v1.update); +/** + * @openapi + * /api/v1/people: + * post: + * summary: Create a person + * operationId: v1PeoplePost + * deprecated: true + * description: > + * Use [POST /api/v1/person](#/Person/v1PersonPost) instead. + * Create a new person contact. A place can be created inline or referenced by UUID. + * tags: + * - Person + * x-permissions: + * hasAll: [can_edit, can_create_people] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: The person's name. + * type: + * type: string + * description: > + * ID of the contact_type for the new person. Defaults to `person` for backwards compatibility. + * place: + * description: Place UUID or inline object. + * oneOf: + * - type: string + * - type: object + * additionalProperties: true + * reported_date: + * type: number + * description: Timestamp of when the record was reported or created. Defaults to now. + * required: [name] + * additionalProperties: true + * responses: + * '200': + * description: Person created + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * rev: + * type: string + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ app.postJson('/api/v1/people', function(req, res) { auth .check(req, ['can_edit', 'can_create_people']) From b14506a12f318529895f7e9763f7ee7ec57ca646 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:07:44 -0500 Subject: [PATCH 23/37] Add docs for bulk-delete endpoints --- api/src/controllers/bulk-docs.js | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/api/src/controllers/bulk-docs.js b/api/src/controllers/bulk-docs.js index 4fc52d3fa36..2a31d39c767 100644 --- a/api/src/controllers/bulk-docs.js +++ b/api/src/controllers/bulk-docs.js @@ -29,7 +29,76 @@ const interceptResponse = (requestDocs, req, res, response) => { return bulkDocs.formatResults(requestDocs, req.body.docs, response); }; +/** + * @openapi + * tags: + * - name: Bulk + * description: Bulk document operations + */ module.exports = { + /** + * @openapi + * /api/v1/bulk-delete: + * post: + * summary: Bulk delete documents + * operationId: v1BulkDeletePost + * description: | + * Bulk delete endpoint for deleting large numbers of documents. Docs are batched into groups + * of 100 and sent sequentially to CouchDB. The response is chunked JSON (one batch at a + * time), so to get an indication of progress incomplete JSON must be parsed with a library such as + * [`partial-json-parser`](https://github.com/indgov/partial-json-parser). + * + * ### Errors + * + * If an error is encountered part-way through the response (eg on the third batch), it’s impossible to send + * new headers to indicate a 5xx error, so the connection will simply be terminated (as recommended here + * https://github.com/expressjs/express/issues/2700). + * tags: + * - Bulk + * x-permissions: + * hasAll: [can_edit] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [docs] + * properties: + * docs: + * type: array + * description: Array of objects each with an `_id` property. + * items: + * type: object + * additionalProperties: true + * required: [_id] + * properties: + * _id: + * type: string + * responses: + * '200': + * description: > + * Chunked JSON response. Each chunk is an array of results for a batch of deletions. + * content: + * application/json: + * schema: + * type: array + * items: + * type: array + * items: + * type: object + * properties: + * ok: + * type: boolean + * id: + * type: string + * rev: + * type: string + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ bulkDelete: (req, res, next) => { return auth .check(req, ['can_edit']) From d56c0c7909271d2f13daa27daf4d6f41c2c3fda8 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:18:37 -0500 Subject: [PATCH 24/37] Add docs for hydration endpoints --- api/src/controllers/hydration.js | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/api/src/controllers/hydration.js b/api/src/controllers/hydration.js index 57fe92e847a..d0c2cdc6d38 100644 --- a/api/src/controllers/hydration.js +++ b/api/src/controllers/hydration.js @@ -47,7 +47,104 @@ const formatResponse = (docIds, hydratedDocs) => { }); }; +/** + * @openapi + * components: + * schemas: + * HydrationResult: + * type: object + * required: [id] + * properties: + * id: + * type: string + * description: The document uuid. + * doc: + * type: object + * additionalProperties: true + * description: The fully hydrated document. Present when the document is found. + * error: + * type: string + * description: '"not_found" when the document is not found.' + */ module.exports = { + /** + * @openapi + * /api/v1/hydrate: + * get: + * summary: Hydrate documents by id (GET) + * operationId: v1HydrateGet + * description: > + * Accepts a JSON array of document uuids and returns fully hydrated documents, in the same + * order in which they were requested. When documents are not found, an entry with the + * missing uuid and an error is added instead. + * tags: + * - Bulk + * parameters: + * - in: query + * name: doc_ids + * required: true + * description: A JSON-encoded array of document uuids. + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * responses: + * '200': + * description: Hydrated documents + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/HydrationResult' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * post: + * summary: Hydrate documents by id (POST) + * operationId: v1HydratePost + * description: > + * Accepts a JSON array of document uuids and returns fully hydrated documents, in the same + * order in which they were requested. When documents are not found, an entry with the + * missing uuid and an error is added instead. + * tags: + * - Bulk + * x-permissions: + * isOnline: true + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [doc_ids] + * properties: + * doc_ids: + * type: array + * description: A JSON array of document uuids. + * items: + * type: string + * responses: + * '200': + * description: Hydrated documents + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/HydrationResult' + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ hydrate: (req, res) => { const docIds = getDocIds(req); if (!docIds) { From 0d8836d034b05f4c9a9f79b112d1766380544111 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:25:21 -0500 Subject: [PATCH 25/37] Add docs for contacts-by-phone endpoints --- api/src/controllers/contacts-by-phone.js | 88 ++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/api/src/controllers/contacts-by-phone.js b/api/src/controllers/contacts-by-phone.js index 536840dc0d6..8fa2a8b9709 100644 --- a/api/src/controllers/contacts-by-phone.js +++ b/api/src/controllers/contacts-by-phone.js @@ -14,6 +14,94 @@ const getPhoneNumber = (req) => { }; module.exports = { + /** + * @openapi + * /api/v1/contacts-by-phone: + * get: + * summary: Find contacts by phone number + * operationId: v1ContactsByPhoneGet + * description: > + * Accepts a phone number and returns fully hydrated contacts that match. If multiple + * contacts are found, all are returned. Returns 404 when no matches are found. + * tags: + * - Contact + * x-since: 3.10.0 + * parameters: + * - in: query + * name: phone + * required: true + * schema: + * type: string + * description: A URL-encoded string representing a phone number. + * responses: + * '200': + * description: Matching contacts found + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * docs: + * type: array + * description: Fully hydrated matching contacts. + * items: + * type: object + * additionalProperties: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + * post: + * summary: Find contacts by phone number (POST) + * operationId: v1ContactsByPhonePost + * description: > + * Accepts a phone number and returns fully hydrated contacts that match. If multiple + * contacts are found, all are returned. Returns 404 when no matches are found. + * tags: + * - Contact + * x-since: 3.10.0 + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [phone] + * properties: + * phone: + * type: string + * description: A string representing a phone number. + * responses: + * '200': + * description: Matching contacts found + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * docs: + * type: array + * description: Fully hydrated matching contacts. + * items: + * type: object + * additionalProperties: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + * '404': + * $ref: '#/components/responses/NotFound' + */ request: (req, res) => { const phone = getPhoneNumber(req); if (!phone) { From 5f81cc6d60ffc62b18612bdaee163d8356a6bcdc Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:37:43 -0500 Subject: [PATCH 26/37] Add docs for settings endpoints --- api/src/controllers/forms.js | 12 +--- api/src/controllers/impact.js | 8 +-- api/src/controllers/records.js | 7 +-- api/src/controllers/settings.js | 104 ++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 21 deletions(-) diff --git a/api/src/controllers/forms.js b/api/src/controllers/forms.js index 756200ad4bb..f005bb5a439 100644 --- a/api/src/controllers/forms.js +++ b/api/src/controllers/forms.js @@ -54,12 +54,6 @@ const listFormsJSON = forms => { return JSON.stringify(forms.map(form => form.internalId + '.xml')); }; -/** - * @openapi - * tags: - * - name: Forms - * description: Operations for XForms - */ module.exports = { /** * @openapi @@ -71,7 +65,7 @@ module.exports = { * Returns a list of currently installed forms. By default returns a JSON array of form filenames. If the * `X-OpenRosa-Version` header is set to `1.0`, returns an OpenRosa xformsList compatible XML response instead. * tags: - * - Forms + * - Config * parameters: * - in: header * name: X-OpenRosa-Version @@ -148,7 +142,7 @@ module.exports = { * include the format extension (e.g. `pregnancyregistration.xml`). Currently only `xml` * format is supported. * tags: - * - Forms + * - Config * parameters: * - in: path * name: form @@ -218,7 +212,7 @@ module.exports = { * description: > * Validates the XForm XML passed in the request body. * tags: - * - Forms + * - Config * x-since: 3.12.0 * x-permissions: * hasAll: [can_configure] diff --git a/api/src/controllers/impact.js b/api/src/controllers/impact.js index a5a98cd25a2..928735b6b32 100644 --- a/api/src/controllers/impact.js +++ b/api/src/controllers/impact.js @@ -10,12 +10,6 @@ const checkUserPermissions = async (req) => { } }; -/** - * @openapi - * tags: - * - name: Impact - * description: Operations for impact metrics - */ module.exports = { v1: { /** @@ -26,7 +20,7 @@ module.exports = { * operationId: v1ImpactGet * description: Returns aggregated impact metrics including user, contact, and report counts. * tags: - * - Impact + * - Monitoring * x-since: 5.0.0 * responses: * '200': diff --git a/api/src/controllers/records.js b/api/src/controllers/records.js index 0561ebe7bef..687db7a73cc 100644 --- a/api/src/controllers/records.js +++ b/api/src/controllers/records.js @@ -34,9 +34,6 @@ const process = (req, res, options) => { /** * @openapi - * tags: - * - name: Records - * description: Operations for creating records for JSON forms * components: * schemas: * RecordSuccess: @@ -109,7 +106,7 @@ module.exports = { * * Creates a new record based on a configured form. Accepts form-encoded or JSON data. * tags: - * - Records + * - SMS * x-permissions: * hasAll: [can_create_records] * parameters: @@ -144,7 +141,7 @@ module.exports = { * operationId: v2RecordsPost * description: Creates a new record based on a configured form. Accepts either form-encoded or JSON data. * tags: - * - Records + * - SMS * x-permissions: * hasAll: [can_create_records] * requestBody: diff --git a/api/src/controllers/settings.js b/api/src/controllers/settings.js index 6c1d7d83825..09e993d4959 100644 --- a/api/src/controllers/settings.js +++ b/api/src/controllers/settings.js @@ -5,7 +5,33 @@ const objectPath = require('object-path'); const doGet = req => auth.getUserCtx(req).then(() => settingsService.get()); +/** + * @openapi + * tags: + * - name: Config + * description: Operations for app configuration + */ module.exports = { + /** + * @openapi + * /api/v1/settings: + * get: + * summary: Get app settings + * operationId: v1SettingsGet + * description: Returns the app settings in JSON format. + * tags: + * - Config + * responses: + * '200': + * description: The app settings + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + */ get: (req, res) => { return doGet(req, res) .then(settings => res.json(settings)) @@ -13,6 +39,27 @@ module.exports = { serverUtils.error(err, req, res, true); }); }, + + /** + * @openapi + * /api/v1/settings/deprecated-transitions: + * get: + * summary: Get deprecated transitions + * operationId: v1SettingsDeprecatedTransitionsGet + * description: Returns a list of deprecated transitions configured in the app settings. + * tags: + * - Config + * responses: + * '200': + * description: Deprecated transitions + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * '401': + * $ref: '#/components/responses/Unauthorized' + */ getDeprecatedTransitions: (req, res) => { return auth .getUserCtx(req) @@ -39,6 +86,63 @@ module.exports = { serverUtils.error(err, req, res, true); }); }, + + /** + * @openapi + * /api/v1/settings: + * put: + * summary: Update app settings + * operationId: v1SettingsPut + * description: > + * Update the app settings. By default, the provided properties are merged with existing + * settings. Use query parameters to control replacement behavior. + * tags: + * - Config + * x-permissions: + * hasAll: [can_edit, can_configure] + * parameters: + * - in: query + * name: replace + * schema: + * enum: ['true'] + * description: > + * Whether to replace existing settings for the given properties or to merge. + * Defaults to merging. + * - in: query + * name: overwrite + * schema: + * type: string + * enum: ['true'] + * description: > + * Whether to replace the entire settings document with the input document. + * If both `replace` and `overwrite` are set, `overwrite` takes precedence. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * description: The settings properties to update. + * responses: + * '200': + * description: Settings update result + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * const: true + * updated: + * type: boolean + * description: Whether the settings document was updated. + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ put: (req, res) => { return auth .check(req, ['can_edit', 'can_configure']) From 41360b09cac5c9c3931ffa03dbf3903086644b53 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:47:23 -0500 Subject: [PATCH 27/37] Add docs for credentials endpoints --- api/src/controllers/credentials.js | 45 ++++++++++++++++++++++++++++++ api/src/routing.js | 11 ++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/api/src/controllers/credentials.js b/api/src/controllers/credentials.js index 1fe2cfdb583..d9b96ddd1c9 100644 --- a/api/src/controllers/credentials.js +++ b/api/src/controllers/credentials.js @@ -15,6 +15,51 @@ const checkAuth = req => { }; module.exports = { + /** + * @openapi + * /api/v1/credentials/{key}: + * put: + * summary: Set a credential + * operationId: v1CredentialsKeyPut + * description: > + * Securely store a credential for authentication with third-party systems such as SMS + * aggregators and HMIS. The credential key is provided as a path parameter and the + * password as plain text in the request body. Only database admins can access this + * endpoint. + * tags: + * - Config + * x-since: 4.0.0 + * parameters: + * - in: path + * name: key + * required: true + * schema: + * type: string + * description: The credential key identifier. + * requestBody: + * required: true + * content: + * text/plain: + * schema: + * type: string + * description: The credential password/secret value. + * responses: + * '200': + * description: Credential stored successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * const: true + * '400': + * $ref: '#/components/responses/BadRequest' + * '401': + * $ref: '#/components/responses/Unauthorized' + * '403': + * $ref: '#/components/responses/Forbidden' + */ put: (req, res) => { const key = req.params.key; const password = req.body; diff --git a/api/src/routing.js b/api/src/routing.js index 09e165a59d7..2bedcb1bc67 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -124,13 +124,6 @@ const handleJsonOrCsvRequest = (method, path, callback) => { }); }; -/** - * @openapi - * tags: - * - name: Info - * description: Operations for getting server info - */ - app.deleteJson = (path, callback) => handleJsonRequest('delete', path, callback); app.postJsonOrCsv = (path, callback) => handleJsonOrCsvRequest('post', path, callback); app.postJson = (path, callback) => handleJsonRequest('post', path, callback); @@ -409,7 +402,7 @@ app.all('/setup/finish', function(req, res) { * operationId: apiInfoGet * description: Returns the version of the CHT server. * tags: - * - Info + * - Monitoring * responses: * '200': * description: The API info data @@ -435,7 +428,7 @@ app.get('/api/info', function(req, res) { * operationId: apiDeployInfoGet * description: Returns build and deploy information for the running CHT instance. * tags: - * - Info + * - Monitoring * responses: * '200': * description: The deploy info data From c1391179321665077743141c7744308f6fcbae61 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 10:54:15 -0500 Subject: [PATCH 28/37] Add shared schema for OkResponse --- api/src/controllers/credentials.js | 5 +--- api/src/controllers/forms.js | 5 +--- api/src/controllers/upgrade.js | 40 ++++++------------------------ scripts/generate-openapi.js | 6 +++++ 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/api/src/controllers/credentials.js b/api/src/controllers/credentials.js index d9b96ddd1c9..56f19e22173 100644 --- a/api/src/controllers/credentials.js +++ b/api/src/controllers/credentials.js @@ -49,10 +49,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * const: true + * $ref: '#/components/schemas/OkResponse' * '400': * $ref: '#/components/responses/BadRequest' * '401': diff --git a/api/src/controllers/forms.js b/api/src/controllers/forms.js index f005bb5a439..95fd13e3be9 100644 --- a/api/src/controllers/forms.js +++ b/api/src/controllers/forms.js @@ -229,10 +229,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '400': * description: Form validation failed * content: diff --git a/api/src/controllers/upgrade.js b/api/src/controllers/upgrade.js index 6ad02283e9f..0572d32a2f9 100644 --- a/api/src/controllers/upgrade.js +++ b/api/src/controllers/upgrade.js @@ -172,10 +172,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -207,10 +204,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -248,10 +242,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -281,10 +272,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -314,10 +302,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '401': * $ref: '#/components/responses/Unauthorized' * '403': @@ -340,10 +325,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '401': * $ref: '#/components/responses/Unauthorized' * '403': @@ -403,10 +385,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '401': * $ref: '#/components/responses/Unauthorized' * '403': @@ -431,10 +410,7 @@ module.exports = { * content: * application/json: * schema: - * type: object - * properties: - * ok: - * type: boolean + * $ref: '#/components/schemas/OkResponse' * '401': * $ref: '#/components/responses/Unauthorized' * '403': diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index e65757b31b2..df185812d61 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -52,6 +52,12 @@ const SWAGGER_OPTIONS = { type: ['string', 'null'], description: 'Token for retrieving the next page. A `null` value indicates there are no more pages.', }, + OkResponse: { + type: 'object', + properties: { + ok: { const: true }, + }, + }, }, parameters: { cursor: { From 0d21e912595dd54432552321d796fb6e93e85aff Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 11:03:30 -0500 Subject: [PATCH 29/37] Add doc for replication-limit-log endpoint --- api/src/controllers/replication-limit-log.js | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/api/src/controllers/replication-limit-log.js b/api/src/controllers/replication-limit-log.js index 3aafc017123..78373776aa7 100644 --- a/api/src/controllers/replication-limit-log.js +++ b/api/src/controllers/replication-limit-log.js @@ -3,6 +3,57 @@ const serverUtils = require('../server-utils'); const replicationLimitLog = require('../services/replication-limit-log'); module.exports = { + /** + * @openapi + * /api/v1/users-doc-count: + * get: + * summary: Get user document replication counts + * operationId: v1UsersDocCountGet + * description: > + * Returns the quantity of documents replicated by each user. Optionally filter by username. Only allowed for + * database admins. + * tags: + * - Users + * x-since: 3.11.0 + * parameters: + * - in: query + * name: user + * schema: + * type: string + * description: Filter by username. If not provided, returns all users. + * responses: + * '200': + * description: User replication document counts + * content: + * application/json: + * schema: + * type: object + * properties: + * limit: + * type: number + * description: The configured replication limit. + * users: + * description: > + * A single user replication log object (when filtered) or an object + * keyed by username. + * type: object + * properties: + * _id: + * type: string + * _rev: + * type: string + * user: + * type: string + * description: The username. + * date: + * type: number + * description: Timestamp of the replication count entry. + * count: + * type: number + * description: Number of documents replicated by the user. + * '401': + * $ref: '#/components/responses/Unauthorized' + */ get: (req, res) => { return auth .getUserCtx(req) From 73be575cfa178e6fa6235fdbed2a4cc324bf6cef Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 11:15:03 -0500 Subject: [PATCH 30/37] Clean up openapi arrays --- api/src/controllers/africas-talking.js | 6 ++-- api/src/controllers/bulk-docs.js | 3 +- api/src/controllers/contact.js | 10 ++---- api/src/controllers/contacts-by-phone.js | 6 ++-- api/src/controllers/credentials.js | 3 +- api/src/controllers/export-data.js | 36 +++++++------------- api/src/controllers/forms.js | 9 ++--- api/src/controllers/hydration.js | 6 ++-- api/src/controllers/impact.js | 3 +- api/src/controllers/monitoring.js | 6 ++-- api/src/controllers/person.js | 16 +++------ api/src/controllers/place.js | 16 +++------ api/src/controllers/rapidpro.js | 6 ++-- api/src/controllers/records.js | 6 ++-- api/src/controllers/replication-limit-log.js | 3 +- api/src/controllers/report.js | 16 +++------ api/src/controllers/settings.js | 9 ++--- api/src/controllers/sms-gateway.js | 6 ++-- api/src/controllers/target.js | 10 ++---- api/src/controllers/upgrade.js | 33 ++++++------------ api/src/controllers/users.js | 30 ++++++---------- api/src/routing.js | 15 +++----- 22 files changed, 83 insertions(+), 171 deletions(-) diff --git a/api/src/controllers/africas-talking.js b/api/src/controllers/africas-talking.js index 099677cf47c..84ec56c973a 100644 --- a/api/src/controllers/africas-talking.js +++ b/api/src/controllers/africas-talking.js @@ -63,8 +63,7 @@ module.exports = { * Webhook endpoint for receiving incoming SMS messages from the Africa's Talking gateway. * Requires a valid incoming key passed as a query parameter. See the * [documentation](/building/messaging/gateways/africas-talking/) for more details. - * tags: - * - SMS + * tags: [SMS] * parameters: * - in: query * name: key @@ -128,8 +127,7 @@ module.exports = { * Webhook endpoint for receiving SMS delivery status reports from the Africa's Talking gateway. * Requires a valid incoming key passed as a query parameter. See the * [documentation](/building/messaging/gateways/africas-talking/) for more details. - * tags: - * - SMS + * tags: [SMS] * parameters: * - in: query * name: key diff --git a/api/src/controllers/bulk-docs.js b/api/src/controllers/bulk-docs.js index 2a31d39c767..8eafaca1351 100644 --- a/api/src/controllers/bulk-docs.js +++ b/api/src/controllers/bulk-docs.js @@ -53,8 +53,7 @@ module.exports = { * If an error is encountered part-way through the response (eg on the third batch), it’s impossible to send * new headers to indicate a 5xx error, so the connection will simply be terminated (as recommended here * https://github.com/expressjs/express/issues/2700). - * tags: - * - Bulk + * tags: [Bulk] * x-permissions: * hasAll: [can_edit] * requestBody: diff --git a/api/src/controllers/contact.js b/api/src/controllers/contact.js index acd9ca3a2f4..2a1d6d07dba 100644 --- a/api/src/controllers/contact.js +++ b/api/src/controllers/contact.js @@ -23,8 +23,7 @@ module.exports = { * operationId: v1ContactIdGet * description: > * Returns a contact record (person or place). Optionally includes the full parent place lineage. - * tags: - * - Contact + * tags: [Contact] * x-since: 4.18.0 * x-permissions: * hasAll: [can_view_contacts] @@ -73,8 +72,7 @@ module.exports = { * description: > * Returns a paginated array of contact identifier strings matching the given filter criteria. * At least one of `type` or `freetext` must be provided. - * tags: - * - Contact + * tags: [Contact] * x-since: 4.18.0 * x-permissions: * hasAll: [can_view_contacts] @@ -111,9 +109,7 @@ module.exports = { * type: string * cursor: * $ref: '#/components/schemas/PageCursor' - * required: - * - data - * - cursor + * required: [data, cursor] * '400': * $ref: '#/components/responses/BadRequest' * '401': diff --git a/api/src/controllers/contacts-by-phone.js b/api/src/controllers/contacts-by-phone.js index 8fa2a8b9709..08841b4e58d 100644 --- a/api/src/controllers/contacts-by-phone.js +++ b/api/src/controllers/contacts-by-phone.js @@ -23,8 +23,7 @@ module.exports = { * description: > * Accepts a phone number and returns fully hydrated contacts that match. If multiple * contacts are found, all are returned. Returns 404 when no matches are found. - * tags: - * - Contact + * tags: [Contact] * x-since: 3.10.0 * parameters: * - in: query @@ -63,8 +62,7 @@ module.exports = { * description: > * Accepts a phone number and returns fully hydrated contacts that match. If multiple * contacts are found, all are returned. Returns 404 when no matches are found. - * tags: - * - Contact + * tags: [Contact] * x-since: 3.10.0 * requestBody: * required: true diff --git a/api/src/controllers/credentials.js b/api/src/controllers/credentials.js index 56f19e22173..acd5a719669 100644 --- a/api/src/controllers/credentials.js +++ b/api/src/controllers/credentials.js @@ -26,8 +26,7 @@ module.exports = { * aggregators and HMIS. The credential key is provided as a path parameter and the * password as plain text in the request body. Only database admins can access this * endpoint. - * tags: - * - Config + * tags: [Config] * x-since: 4.0.0 * parameters: * - in: path diff --git a/api/src/controllers/export-data.js b/api/src/controllers/export-data.js index 9351c396433..73a6330a250 100644 --- a/api/src/controllers/export-data.js +++ b/api/src/controllers/export-data.js @@ -130,8 +130,7 @@ module.exports = { * description: > * Exports target data formatted as a DHIS2 dataValueSet. The data can be filtered to a specific section * of the contact hierarchy or for a given time interval. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_dhis] * parameters: @@ -178,8 +177,7 @@ module.exports = { * operationId: v2ExportDhisPost * description: > * Exports target data formatted as a DHIS2 dataValueSet. Accepts filters in the request body. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_dhis] * requestBody: @@ -228,8 +226,7 @@ module.exports = { * [search library](https://github.com/medic/cht-core/tree/master/shared-libs/search) to ensure identical * results to the front-end. Filters can be passed as query parameters using form-style encoding * (e.g. `filters[forms][selected][0][code]=immunization_visit`). - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_messages] * parameters: @@ -283,8 +280,7 @@ module.exports = { * summary: Export reports * operationId: v2ExportReportsPost * description: Exports reports as CSV. Accepts filters in the request body. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_messages] * requestBody: @@ -363,8 +359,7 @@ module.exports = { * | To Phone | The phone number the message is or will be sent to. Only applies to outgoing. | * | Message Body | The content of the message. | * - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_messages] * parameters: @@ -400,8 +395,7 @@ module.exports = { * description: > * Exports messages as CSV. Accepts filters in the request body. See * [GET](#/Export/v2ExportMessagesGet) for output column details. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_messages] * requestBody: @@ -451,8 +445,7 @@ module.exports = { * | type | The contact type. For configurable hierarchies, this will always be `contact`. | * | contact_type | The configurable contact type. Will be empty if using the default hierarchy. | * | place_id | The generated short place ID for use in SMS. | - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_contacts] * parameters: @@ -481,8 +474,7 @@ module.exports = { * description: > * Exports contacts as a CSV. Accepts filters in the request body. See [GET](#/Export/v2ExportContactsGet) for * output column details. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_contacts] * requestBody: @@ -510,8 +502,7 @@ module.exports = { * summary: Export feedback * operationId: v2ExportFeedbackGet * description: Exports user feedback data as CSV. - * tags: - * - Export + * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_feedback] * parameters: @@ -527,8 +518,7 @@ module.exports = { * summary: Export feedback * operationId: v2ExportFeedbackPost * description: Exports user feedback data as CSV. Accepts options in the request body. - * tags: - * - Export + * tags: [Export] * x-permissions: * isOnline: true * hasAny: [can_export_all, can_export_feedback] @@ -558,8 +548,7 @@ module.exports = { * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using * this endpoint or use it only when end users will not be impacted. An improved endpoint is * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. - * tags: - * - Export + * tags: [Export] * deprecated: true * x-since: 4.7.0 * x-permissions: @@ -581,8 +570,7 @@ module.exports = { * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using * this endpoint or use it only when end users will not be impacted. An improved endpoint is * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. - * tags: - * - Export + * tags: [Export] * deprecated: true * x-since: 4.7.0 * x-permissions: diff --git a/api/src/controllers/forms.js b/api/src/controllers/forms.js index 95fd13e3be9..36f77f561a8 100644 --- a/api/src/controllers/forms.js +++ b/api/src/controllers/forms.js @@ -64,8 +64,7 @@ module.exports = { * description: > * Returns a list of currently installed forms. By default returns a JSON array of form filenames. If the * `X-OpenRosa-Version` header is set to `1.0`, returns an OpenRosa xformsList compatible XML response instead. - * tags: - * - Config + * tags: [Config] * parameters: * - in: header * name: X-OpenRosa-Version @@ -141,8 +140,7 @@ module.exports = { * Returns the form definition for a given form ID and format. The form parameter should * include the format extension (e.g. `pregnancyregistration.xml`). Currently only `xml` * format is supported. - * tags: - * - Config + * tags: [Config] * parameters: * - in: path * name: form @@ -211,8 +209,7 @@ module.exports = { * operationId: v1FormsValidatePost * description: > * Validates the XForm XML passed in the request body. - * tags: - * - Config + * tags: [Config] * x-since: 3.12.0 * x-permissions: * hasAll: [can_configure] diff --git a/api/src/controllers/hydration.js b/api/src/controllers/hydration.js index d0c2cdc6d38..c0b8a658573 100644 --- a/api/src/controllers/hydration.js +++ b/api/src/controllers/hydration.js @@ -77,8 +77,7 @@ module.exports = { * Accepts a JSON array of document uuids and returns fully hydrated documents, in the same * order in which they were requested. When documents are not found, an entry with the * missing uuid and an error is added instead. - * tags: - * - Bulk + * tags: [Bulk] * parameters: * - in: query * name: doc_ids @@ -112,8 +111,7 @@ module.exports = { * Accepts a JSON array of document uuids and returns fully hydrated documents, in the same * order in which they were requested. When documents are not found, an entry with the * missing uuid and an error is added instead. - * tags: - * - Bulk + * tags: [Bulk] * x-permissions: * isOnline: true * requestBody: diff --git a/api/src/controllers/impact.js b/api/src/controllers/impact.js index 928735b6b32..a7b1fd00130 100644 --- a/api/src/controllers/impact.js +++ b/api/src/controllers/impact.js @@ -19,8 +19,7 @@ module.exports = { * summary: Get impact metrics * operationId: v1ImpactGet * description: Returns aggregated impact metrics including user, contact, and report counts. - * tags: - * - Monitoring + * tags: [Monitoring] * x-since: 5.0.0 * responses: * '200': diff --git a/api/src/controllers/monitoring.js b/api/src/controllers/monitoring.js index ec5120b0bde..8fec70a10ed 100644 --- a/api/src/controllers/monitoring.js +++ b/api/src/controllers/monitoring.js @@ -150,8 +150,7 @@ module.exports = { * querying the metric - check the API logs for details. * - If no response or an error response is received the instance is unreachable. Thus, this API can be used * as an uptime monitoring endpoint. - * tags: - * - Monitoring + * tags: [Monitoring] * parameters: * - in: query * name: connected_user_interval @@ -229,8 +228,7 @@ module.exports = { * description: > * Returns a range of metrics about the instance for automated monitoring, allowing tracking of trends over * time and alerting about potential issues. No authentication is required. - * tags: - * - Monitoring + * tags: [Monitoring] * x-since: 3.12.0 * parameters: * - in: query diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index bd7dc42032c..506bf64cdc2 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -24,8 +24,7 @@ module.exports = { * summary: Get a person by id * operationId: v1PersonIdGet * description: Returns a person contact record. Optionally includes the full parent place lineage. - * tags: - * - Person + * tags: [Person] * x-since: 4.9.0 * x-permissions: * hasAll: [can_view_contacts] @@ -75,8 +74,7 @@ module.exports = { * Returns a paginated array of persons for the given contact type. Use the `cursor` returned in each response * to retrieve subsequent pages. See also [Get Person by id](#/Person/v1PersonIdGet) for retrieving a single * person. - * tags: - * - Person + * tags: [Person] * x-since: 4.11.0 * x-permissions: * hasAll: [can_view_contacts] @@ -104,9 +102,7 @@ module.exports = { * $ref: '#/components/schemas/v1.Person' * cursor: * $ref: '#/components/schemas/PageCursor' - * required: - * - data - * - cursor + * required: [data, cursor] * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -128,8 +124,7 @@ module.exports = { * summary: Create a new person * operationId: v1PersonPost * description: Creates a new person record. - * tags: - * - Person + * tags: [Person] * x-since: 5.2.0 * x-permissions: * hasAny: [can_create_people, can_edit] @@ -166,8 +161,7 @@ module.exports = { * summary: Update a person * operationId: v1PersonIdPut * description: Updates an existing person contact record. - * tags: - * - Person + * tags: [Person] * x-since: 5.2.0 * x-permissions: * hasAny: [can_update_people, can_edit] diff --git a/api/src/controllers/place.js b/api/src/controllers/place.js index 1cb2b77c714..776f241682e 100644 --- a/api/src/controllers/place.js +++ b/api/src/controllers/place.js @@ -24,8 +24,7 @@ module.exports = { * summary: Get a place by id * operationId: v1PlaceIdGet * description: Returns a place contact record. Optionally includes the full parent place lineage. - * tags: - * - Place + * tags: [Place] * x-since: 4.10.0 * x-permissions: * hasAll: [can_view_contacts] @@ -74,8 +73,7 @@ module.exports = { * Returns a paginated array of places for the given contact type. Use the `cursor` returned in each response * to retrieve subsequent pages. See also [Get Place by id](#/Place/v1PlaceIdGet) for retrieving a single * place. - * tags: - * - Place + * tags: [Place] * x-since: 4.12.0 * x-permissions: * hasAll: [can_view_contacts] @@ -103,9 +101,7 @@ module.exports = { * $ref: '#/components/schemas/v1.Place' * cursor: * $ref: '#/components/schemas/PageCursor' - * required: - * - data - * - cursor + * required: [data, cursor] * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -127,8 +123,7 @@ module.exports = { * summary: Create a new place * operationId: v1PlacePost * description: Creates a new place record. - * tags: - * - Place + * tags: [Place] * x-since: 5.2.0 * x-permissions: * hasAny: [can_create_places, can_edit] @@ -165,8 +160,7 @@ module.exports = { * summary: Update a place * operationId: v1PlaceIdPut * description: Updates an existing place contact record. - * tags: - * - Place + * tags: [Place] * x-since: 5.2.0 * x-permissions: * hasAny: [can_update_places, can_edit] diff --git a/api/src/controllers/rapidpro.js b/api/src/controllers/rapidpro.js index 78db46b3dc8..9ce2d8b867a 100644 --- a/api/src/controllers/rapidpro.js +++ b/api/src/controllers/rapidpro.js @@ -61,8 +61,7 @@ module.exports = { * Webhook endpoint for receiving incoming SMS messages from the RapidPro gateway. * Authenticated via an `Authorization: Token ` header. See the * [documentation](/building/messaging/gateways/rapidpro/) for more details. - * tags: - * - SMS + * tags: [SMS] * requestBody: * required: true * content: @@ -103,8 +102,7 @@ module.exports = { * Webhook endpoint for receiving incoming SMS messages from the RapidPro gateway. * Authenticated via an `Authorization: Token ` header. See the * [documentation](/building/messaging/gateways/rapidpro/) for more details. - * tags: - * - SMS + * tags: [SMS] * requestBody: * required: true * content: diff --git a/api/src/controllers/records.js b/api/src/controllers/records.js index 687db7a73cc..c2a4701e382 100644 --- a/api/src/controllers/records.js +++ b/api/src/controllers/records.js @@ -105,8 +105,7 @@ module.exports = { * Use [POST /api/v2/records](#/Records/v2RecordsPost) instead. * * Creates a new record based on a configured form. Accepts form-encoded or JSON data. - * tags: - * - SMS + * tags: [SMS] * x-permissions: * hasAll: [can_create_records] * parameters: @@ -140,8 +139,7 @@ module.exports = { * summary: Create a record * operationId: v2RecordsPost * description: Creates a new record based on a configured form. Accepts either form-encoded or JSON data. - * tags: - * - SMS + * tags: [SMS] * x-permissions: * hasAll: [can_create_records] * requestBody: diff --git a/api/src/controllers/replication-limit-log.js b/api/src/controllers/replication-limit-log.js index 78373776aa7..70b7fdc12f9 100644 --- a/api/src/controllers/replication-limit-log.js +++ b/api/src/controllers/replication-limit-log.js @@ -12,8 +12,7 @@ module.exports = { * description: > * Returns the quantity of documents replicated by each user. Optionally filter by username. Only allowed for * database admins. - * tags: - * - Users + * tags: [Users] * x-since: 3.11.0 * parameters: * - in: query diff --git a/api/src/controllers/report.js b/api/src/controllers/report.js index 2e4cc2b10f7..8109a36da06 100644 --- a/api/src/controllers/report.js +++ b/api/src/controllers/report.js @@ -25,8 +25,7 @@ module.exports = { * operationId: v1ReportIdGet * description: > * Returns a report record. Optionally includes the full contact, patient, and/or place lineage. - * tags: - * - Report + * tags: [Report] * x-since: 4.18.0 * x-permissions: * hasAll: [can_view_reports] @@ -74,8 +73,7 @@ module.exports = { * operationId: v1ReportUuidGet * description: > * Returns a paginated array of report identifiers matching the given freetext search term. - * tags: - * - Report + * tags: [Report] * x-since: 4.18.0 * x-permissions: * hasAll: [can_view_reports] @@ -105,9 +103,7 @@ module.exports = { * type: string * cursor: * $ref: '#/components/schemas/PageCursor' - * required: - * - data - * - cursor + * required: [data, cursor] * '400': * $ref: '#/components/responses/BadRequest' * '401': @@ -129,8 +125,7 @@ module.exports = { * summary: Create a new report * operationId: v1ReportPost * description: Creates a new report. - * tags: - * - Report + * tags: [Report] * x-since: 5.2.0 * x-permissions: * hasAny: [can_create_records, can_edit] @@ -167,8 +162,7 @@ module.exports = { * summary: Update a report * operationId: v1ReportIdPut * description: Updates an existing report. - * tags: - * - Report + * tags: [Report] * x-since: 5.2.0 * x-permissions: * hasAny: [can_update_reports, can_edit] diff --git a/api/src/controllers/settings.js b/api/src/controllers/settings.js index 09e993d4959..a384c1a1e57 100644 --- a/api/src/controllers/settings.js +++ b/api/src/controllers/settings.js @@ -19,8 +19,7 @@ module.exports = { * summary: Get app settings * operationId: v1SettingsGet * description: Returns the app settings in JSON format. - * tags: - * - Config + * tags: [Config] * responses: * '200': * description: The app settings @@ -47,8 +46,7 @@ module.exports = { * summary: Get deprecated transitions * operationId: v1SettingsDeprecatedTransitionsGet * description: Returns a list of deprecated transitions configured in the app settings. - * tags: - * - Config + * tags: [Config] * responses: * '200': * description: Deprecated transitions @@ -96,8 +94,7 @@ module.exports = { * description: > * Update the app settings. By default, the provided properties are merged with existing * settings. Use query parameters to control replacement behavior. - * tags: - * - Config + * tags: [Config] * x-permissions: * hasAll: [can_edit, can_configure] * parameters: diff --git a/api/src/controllers/sms-gateway.js b/api/src/controllers/sms-gateway.js index c0eec69888d..fa54aeeb776 100644 --- a/api/src/controllers/sms-gateway.js +++ b/api/src/controllers/sms-gateway.js @@ -76,8 +76,7 @@ module.exports = { * description: > * Returns a simple response to verify that the cht-gateway SMS endpoint is available. * See the [cht-gateway documentation](https://github.com/medic/cht-gateway) for more details. - * tags: - * - SMS + * tags: [SMS] * x-permissions: * hasAll: [can_access_gateway_api] * responses: @@ -110,8 +109,7 @@ module.exports = { * Processes incoming messages and delivery status updates from cht-gateway, and returns * outgoing messages that are ready to be sent. * See the [cht-gateway documentation](https://github.com/medic/cht-gateway) for more details. - * tags: - * - SMS + * tags: [SMS] * x-permissions: * hasAll: [can_access_gateway_api] * requestBody: diff --git a/api/src/controllers/target.js b/api/src/controllers/target.js index 143f5a2ec1e..18d18ec8be5 100644 --- a/api/src/controllers/target.js +++ b/api/src/controllers/target.js @@ -39,8 +39,7 @@ module.exports = { * summary: Get a target by id * operationId: v1TargetIdGet * description: Returns a target record. - * tags: - * - Target + * tags: [Target] * x-since: 5.1.0 * parameters: * - in: path @@ -84,8 +83,7 @@ module.exports = { * Returns a paginated array of targets for the given contact and reporting period. Use the `cursor` returned * in each response to retrieve subsequent pages. See also [Get Target by id](#/Target/v1TargetIdGet) for * retrieving a single target. - * tags: - * - Target + * tags: [Target] * x-since: 5.1.0 * parameters: * - in: query @@ -125,9 +123,7 @@ module.exports = { * $ref: '#/components/schemas/v1.Target' * cursor: * $ref: '#/components/schemas/PageCursor' - * required: - * - data - * - cursor + * required: [data, cursor] * '400': * $ref: '#/components/responses/BadRequest' * '401': diff --git a/api/src/controllers/upgrade.js b/api/src/controllers/upgrade.js index 0572d32a2f9..1d1790e4207 100644 --- a/api/src/controllers/upgrade.js +++ b/api/src/controllers/upgrade.js @@ -121,8 +121,7 @@ module.exports = { * summary: Check if an upgrade can be performed * operationId: v2UpgradeCanUpgradeGet * description: Returns whether the instance is in a state where an upgrade can be performed. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -156,8 +155,7 @@ module.exports = { * `/api/v1/upgrade/stage` and then `/api/v1/upgrade/complete` once staging has finished. * This is asynchronous. Progress can be followed by watching the `horti-upgrade` document. * Calling this endpoint will eventually cause api and sentinel to restart. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * requestBody: @@ -188,8 +186,7 @@ module.exports = { * `/api/v2/upgrade/stage` and then `/api/v2/upgrade/complete` once staging has finished. * This is asynchronous. Progress can be followed via [GET /api/v2/upgrade](#/Upgrade/v2UpgradeGet). * Calling this endpoint will eventually cause api and sentinel to restart. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * requestBody: @@ -226,8 +223,7 @@ module.exports = { * Stages an upgrade to the provided version. Does as much of the upgrade as possible without actually * swapping versions over and restarting. An upgrade has been staged when the `horti-upgrade` document * has `"action": "stage"` and `"staging_complete": true`. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * requestBody: @@ -256,8 +252,7 @@ module.exports = { * description: > * Stages an upgrade to the provided version. Does as much of the upgrade as possible without actually * swapping versions over and restarting. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * requestBody: @@ -292,8 +287,7 @@ module.exports = { * description: > * Use [POST /api/v2/upgrade/complete](#/Upgrade/v2UpgradeCompletePost) instead. * Completes a staged upgrade. Returns a 404 if there is no upgrade in the staged position. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -315,8 +309,7 @@ module.exports = { * operationId: v2UpgradeCompletePost * description: > * Completes a staged upgrade. Returns a 404 if there is no upgrade in the staged position. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -342,8 +335,7 @@ module.exports = { * summary: Get upgrade status * operationId: v2UpgradeGet * description: Returns the current upgrade status, indexer progress, and builds server URL. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -375,8 +367,7 @@ module.exports = { * summary: Abort an upgrade * operationId: v2UpgradeDelete * description: Aborts an in-progress upgrade. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -400,8 +391,7 @@ module.exports = { * summary: Update the service worker * operationId: v2UpgradeServiceWorkerPost * description: Triggers an update of the service worker cache. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * responses: @@ -432,8 +422,7 @@ module.exports = { * description: > * Compares the provided build version against the currently deployed version, returning * differences in design documents. - * tags: - * - Upgrade + * tags: [Upgrade] * x-permissions: * hasAll: [can_upgrade] * requestBody: diff --git a/api/src/controllers/users.js b/api/src/controllers/users.js index b1bf53a550b..976f7599b5a 100644 --- a/api/src/controllers/users.js +++ b/api/src/controllers/users.js @@ -293,8 +293,7 @@ module.exports = { * description: > * Use [GET /api/v2/users](#/Users/v2UsersGet) instead. * Returns a list of users and their profile data. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_view_users] * responses: @@ -335,8 +334,7 @@ module.exports = { * * Passing a single user in the request’s body will return a single object whereas passing an array of users * will return an array of objects. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_edit, can_create_users] * requestBody: @@ -395,8 +393,7 @@ module.exports = { * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require * Basic Auth (either the header or in the URL). This is to ensure the password is known at * time of request, and no one is hijacking a cookie. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_edit, can_update_users] * parameters: @@ -478,8 +475,7 @@ module.exports = { * summary: Delete a user * operationId: v1UsersUsernameDelete * description: Delete a user. Does not affect the person or place associated with the user. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_edit, can_delete_users] * parameters: @@ -525,8 +521,7 @@ module.exports = { * can query for any user by providing `role` and `facility_id` query parameters. (When requested as an * online user, the number of tasks are never counted and never returned, so `warn_docs` is always equal to * `total_docs`.) - * tags: - * - Users + * tags: [Users] * parameters: * - in: query * name: facility_id @@ -593,8 +588,7 @@ module.exports = { * description: > * Returns a user's profile data. Users with `can_view_users` permission can view any * user. Users can also view their own profile. - * tags: - * - Users + * tags: [Users] * x-since: 4.7.0 * x-permissions: * hasAll: [can_view_users] @@ -663,8 +657,7 @@ module.exports = { * summary: List users * operationId: v2UsersGet * description: Returns a list of users and their profile data including roles. - * tags: - * - Users + * tags: [Users] * x-since: 4.1.0 * x-permissions: * hasAll: [can_view_users] @@ -727,8 +720,7 @@ module.exports = { * progress status that gets updated throughout the import and finalized upon completion. These entries are * saved in the `medic-logs` database and you can access them by querying documents with a key that starts * with `bulk-user-upload-`. - * tags: - * - Users + * tags: [Users] * x-since: 3.16.0 * x-permissions: * hasAll: [can_edit, can_create_users] @@ -802,8 +794,7 @@ module.exports = { * summary: Create a user * operationId: v3UsersPost * description: Creates a user that can be associated with multiple facilities. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_edit, can_create_users] * requestBody: @@ -852,8 +843,7 @@ module.exports = { * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require * Basic Auth (either the header or in the URL). This is to ensure the password is known at * time of request, and no one is hijacking a cookie. - * tags: - * - Users + * tags: [Users] * x-permissions: * hasAll: [can_edit, can_update_users] * parameters: diff --git a/api/src/routing.js b/api/src/routing.js index 2bedcb1bc67..8502934130d 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -401,8 +401,7 @@ app.all('/setup/finish', function(req, res) { * summary: Get the version of the CHT server * operationId: apiInfoGet * description: Returns the version of the CHT server. - * tags: - * - Monitoring + * tags: [Monitoring] * responses: * '200': * description: The API info data @@ -427,8 +426,7 @@ app.get('/api/info', function(req, res) { * summary: Get deploy information * operationId: apiDeployInfoGet * description: Returns build and deploy information for the running CHT instance. - * tags: - * - Monitoring + * tags: [Monitoring] * responses: * '200': * description: The deploy info data @@ -518,8 +516,7 @@ app.get('/api/v1/users-info', authorization.handleAuthErrors, authorization.getU * Use [POST /api/v1/place](#/Place/v1PlacePost) instead. * Create a new place and optionally a contact. The parent can be referenced by UUID or * created inline. A contact can also be created inline or referenced by UUID. - * tags: - * - Place + * tags: [Place] * x-permissions: * hasAll: [can_edit, can_create_places] * requestBody: @@ -590,8 +587,7 @@ app.postJson('/api/v1/places', function(req, res) { * description: > * Use [PUT /api/v1/place/{id}](#/Place/v1PlaceIdPut) instead. * Update a place and optionally its contact. - * tags: - * - Place + * tags: [Place] * x-permissions: * hasAll: [can_edit, can_update_places] * parameters: @@ -657,8 +653,7 @@ app.putJson('/api/v1/place/:uuid', place.v1.update); * description: > * Use [POST /api/v1/person](#/Person/v1PersonPost) instead. * Create a new person contact. A place can be created inline or referenced by UUID. - * tags: - * - Person + * tags: [Person] * x-permissions: * hasAll: [can_edit, can_create_people] * requestBody: From b04dcfa8f080bdb05b0e2fbf8c8a118ae474e20b Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 11:17:56 -0500 Subject: [PATCH 31/37] Switch Users tab to just User --- api/src/controllers/replication-limit-log.js | 2 +- api/src/controllers/users.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/src/controllers/replication-limit-log.js b/api/src/controllers/replication-limit-log.js index 70b7fdc12f9..3329462fa30 100644 --- a/api/src/controllers/replication-limit-log.js +++ b/api/src/controllers/replication-limit-log.js @@ -12,7 +12,7 @@ module.exports = { * description: > * Returns the quantity of documents replicated by each user. Optionally filter by username. Only allowed for * database admins. - * tags: [Users] + * tags: [User] * x-since: 3.11.0 * parameters: * - in: query diff --git a/api/src/controllers/users.js b/api/src/controllers/users.js index 976f7599b5a..c7f26089e12 100644 --- a/api/src/controllers/users.js +++ b/api/src/controllers/users.js @@ -293,7 +293,7 @@ module.exports = { * description: > * Use [GET /api/v2/users](#/Users/v2UsersGet) instead. * Returns a list of users and their profile data. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_view_users] * responses: @@ -334,7 +334,7 @@ module.exports = { * * Passing a single user in the request’s body will return a single object whereas passing an array of users * will return an array of objects. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_edit, can_create_users] * requestBody: @@ -393,7 +393,7 @@ module.exports = { * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require * Basic Auth (either the header or in the URL). This is to ensure the password is known at * time of request, and no one is hijacking a cookie. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_edit, can_update_users] * parameters: @@ -475,7 +475,7 @@ module.exports = { * summary: Delete a user * operationId: v1UsersUsernameDelete * description: Delete a user. Does not affect the person or place associated with the user. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_edit, can_delete_users] * parameters: @@ -521,7 +521,7 @@ module.exports = { * can query for any user by providing `role` and `facility_id` query parameters. (When requested as an * online user, the number of tasks are never counted and never returned, so `warn_docs` is always equal to * `total_docs`.) - * tags: [Users] + * tags: [User] * parameters: * - in: query * name: facility_id @@ -588,7 +588,7 @@ module.exports = { * description: > * Returns a user's profile data. Users with `can_view_users` permission can view any * user. Users can also view their own profile. - * tags: [Users] + * tags: [User] * x-since: 4.7.0 * x-permissions: * hasAll: [can_view_users] @@ -657,7 +657,7 @@ module.exports = { * summary: List users * operationId: v2UsersGet * description: Returns a list of users and their profile data including roles. - * tags: [Users] + * tags: [User] * x-since: 4.1.0 * x-permissions: * hasAll: [can_view_users] @@ -720,7 +720,7 @@ module.exports = { * progress status that gets updated throughout the import and finalized upon completion. These entries are * saved in the `medic-logs` database and you can access them by querying documents with a key that starts * with `bulk-user-upload-`. - * tags: [Users] + * tags: [User] * x-since: 3.16.0 * x-permissions: * hasAll: [can_edit, can_create_users] @@ -794,7 +794,7 @@ module.exports = { * summary: Create a user * operationId: v3UsersPost * description: Creates a user that can be associated with multiple facilities. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_edit, can_create_users] * requestBody: @@ -843,7 +843,7 @@ module.exports = { * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require * Basic Auth (either the header or in the URL). This is to ensure the password is known at * time of request, and no one is hijacking a cookie. - * tags: [Users] + * tags: [User] * x-permissions: * hasAll: [can_edit, can_update_users] * parameters: From 1f65f6e9b187526db5ddd8836b74476769e1ad69 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 19 Mar 2026 11:19:50 -0500 Subject: [PATCH 32/37] Switch Users tab to just User --- api/src/controllers/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/users.js b/api/src/controllers/users.js index c7f26089e12..31f342e5100 100644 --- a/api/src/controllers/users.js +++ b/api/src/controllers/users.js @@ -175,7 +175,7 @@ const verifyUpdateRequest = async (req) => { /** * @openapi * tags: - * - name: Users + * - name: User * description: Operations for user management * components: * schemas: From 0c4697c0d6fbcc2fa6d58215c8a5fef1177a0354 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 25 Mar 2026 15:29:56 -0500 Subject: [PATCH 33/37] Fix build-documentation script --- api/src/services/authorization.js | 49 ++++++++++++++-------------- scripts/build/build-documentation.sh | 2 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/api/src/services/authorization.js b/api/src/services/authorization.js index ae20b775f22..2e6c5f59e38 100644 --- a/api/src/services/authorization.js +++ b/api/src/services/authorization.js @@ -19,14 +19,13 @@ const DEFAULT_DDOCS = [ ]; /** - * @typedef {{ - * name:string, - * roles:string[] - * contact_id:string, - * facility_id: string[], - * contact:Object, - * facility:Object[] - * }} userCtx + * @typedef {Object} userCtx + * @property {string} name + * @property {Array.} roles + * @property {string} contact_id + * @property {Array.} facility_id + * @property {Object} contact + * @property {Array.} facility */ /** @@ -36,19 +35,18 @@ const DEFAULT_DDOCS = [ * @property {string[]} subjectIds, * @property {number} contactDepth. * @property {number} reportDepth. - * @property {{[docId:string]:number}} subjectsDepth, + * @property {Object.} subjectsDepth, * @property {boolean} replicatePrimaryContacts, */ /** - * @typedef {{ - * key: string|string[], - * type?: string, - * submitter?: string, - * subject?: string, - * private?: string, - * needed_signoff?: string, - * }} DocByReplicationKey + * @typedef {Object} DocByReplicationKey + * @property {string|Array.} key + * @property {string} [type] + * @property {string} [submitter] + * @property {string} [subject] + * @property {string} [private] + * @property {string} [needed_signoff] */ // fake view map, to store whether doc is a medic.user-settings doc @@ -210,7 +208,7 @@ const allowedDoc = (docId, authorizationContext, { docsByReplicationKey, contact /** * Returns whether an authenticated user has access to a document * @param {String} docId document id - * @param {Array<{ key: [string, string?], value: { _id:string, shortcode:string} }>} docContactsByDepth + * @param {Array.<{ key: Array., value: {_id: string, shortcode: string} }>} docContactsByDepth * @param {AuthorizationContext} authorizationContext * @param {Boolean} authorizationContext.replicatePrimaryContacts - whether to allow replication of primary contacts * @@ -384,7 +382,8 @@ const getAuthorizationContext = async (userCtx) => { * Retrieves unknown primary contacts from the database. * Iterates over all primary contacts and includes them in the subjects lists and assigns correct depth. * @param {AuthorizationContext} authCtx - * @param {{[docId:string]: { primaryContact:string, subjects:string[] }}} contacts - map of contacts and their subject + * @param {Object.}>} contacts - map of contacts and their + * subject * ids and primary contact * @returns {Promise} */ @@ -462,7 +461,7 @@ const findContactsByReplicationKeys = (replicationKeys) => { /** * Returns a list of places for which the passed contacts are assigned as primary contacts. - * @param {{ _id:string }[]} docs + * @param {Array.<{_id: string}>} docs * @returns {Promise} */ const getPrimaryPlaces = async (docs) => { @@ -508,7 +507,7 @@ const populateAllowedSubjectIds = (authorizationCtx, contacts) => { * relevant allowed subject ids. * * @param {userCtx} userCtx - * @param {{ doc:{}, viewResults:{} }[]} scopeDocsCtx + * @param {Array.<{doc: Object, viewResults: Object}>} scopeDocsCtx * @returns { Promise } */ const getScopedAuthorizationContext = async (userCtx, scopeDocsCtx = []) => { @@ -618,7 +617,7 @@ const sortedIncludes = (sortedArray, element) => _.sortedIndexOf(sortedArray, St /** * Returns a list of document ids that the user is allowed to see and edit * @param authorizationContext - * @returns {Promise<{ id: string, fields: DocByReplicationKey }[]>} + * @returns {Promise.>} */ const getDocsByReplicationKeyNouveau = async (authorizationContext) => { const allKeys = [...authorizationContext.subjectIds]; @@ -653,7 +652,7 @@ const getDocsByReplicationKeyNouveau = async (authorizationContext) => { /** * Returns a list of document ids that the user is allowed to see and edit * @param {AuthorizationContext} authorizationContext - * @returns {Promise<{ id: string, fields: DocByReplicationKey }[]>} + * @returns {Promise.>} */ const getDocsByReplicationKey = async (authorizationContext) => { return getDocsByReplicationKeyNouveau(authorizationContext).then(hits => { @@ -684,7 +683,7 @@ const getDocsByReplicationKey = async (authorizationContext) => { /** * Returns a list of document ids that the user is allowed to see and edit * @param {AuthorizationContext} authCtx - * @param {{ id: string, fields: DocByReplicationKey }[]} docsByReplicationKey + * @param {Array.<{id: string, fields: DocByReplicationKey}>} docsByReplicationKey * @param {boolean} includeTasks - whether task documents should be included * @returns {string[]} */ @@ -708,7 +707,7 @@ const filterAllowedDocIds = (authCtx, docsByReplicationKey, { includeTasks = tru * Evaluates medic/contacts_by_depth and medic/docs_by_replication_key view map functions over the document and * returns results, and whether the document is a user-settings document or not * @param {Object} doc - CouchDb document - * @returns {{contactsByDepth: [], docsByReplicationKey: [], couchDbUser: boolean}} + * @returns {{contactsByDepth: Array, docsByReplicationKey: Array, couchDbUser: boolean}} */ const getViewResults = (doc) => { const docsByReplicationKey = viewMapUtils.getNouveauViewMapFn('medic', 'docs_by_replication_key')(doc) || {}; diff --git a/scripts/build/build-documentation.sh b/scripts/build/build-documentation.sh index d96163881e6..9aa2ef66e74 100755 --- a/scripts/build/build-documentation.sh +++ b/scripts/build/build-documentation.sh @@ -3,7 +3,7 @@ set -e echo "build-documentation: building jsdocs" -jsdoc -d jsdocs/admin -c admin/node_modules/angular-jsdoc/common/conf.json -t admin/node_modules/angular-jsdoc/angular-template admin/src/js/**/*.js +(cd admin && jsdoc -d ../jsdocs/admin -c node_modules/angular-jsdoc/common/conf.json -t node_modules/angular-jsdoc/angular-template src/js/**/*.js) jsdoc -d jsdocs/sentinel sentinel/src/**/*.js jsdoc -d jsdocs/api -R api/README.md api/src/**/*.js jsdoc -d jsdocs/shared-libs shared-libs/**/src/**/*.js From b4fe7d7762f48496fb32bac3b00f8aa46c58fac4 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 25 Mar 2026 16:32:12 -0500 Subject: [PATCH 34/37] Clean up and simplify doc building into single script. Update CI to cache/restore contents on master --- .github/workflows/build.yml | 67 ++++++++++++++----------- package.json | 1 - scripts/build/build-documentation.sh | 5 +- scripts/{ => build}/generate-openapi.js | 15 +++--- 4 files changed, 50 insertions(+), 38 deletions(-) rename scripts/{ => build}/generate-openapi.js (89%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03993bbd044..4c1dfbee4c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -228,6 +228,27 @@ jobs: - run: npm ci - run: npm run lint-translations + build-generated-docs: + needs: build + name: Build generated docs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm ci + - name: Build Documentation + run: npm run build-documentation + - name: Cache generated docs + if: github.ref == 'refs/heads/master' + uses: actions/cache/save@v4 + with: + key: cht-datasource-docs + path: ./shared-libs/cht-datasource/docs + tests: needs: build name: ${{ matrix.cmd }}-${{ matrix.suite || '' }}${{ matrix.chrome-version == '90' && '-minimum-browser' || '' }} @@ -353,7 +374,7 @@ jobs: if: ${{ failure() }} publish: - needs: [tests, config-tests, test-cht-form] + needs: [tests, config-tests, test-cht-form, build-generated-docs] name: Publish branch build runs-on: ubuntu-latest timeout-minutes: 60 @@ -403,33 +424,23 @@ jobs: node ./publish.js node ./tag-docker-images.js - publish-generated-docs: - needs: [publish] - name: Publish generated docs - runs-on: ubuntu-latest - timeout-minutes: 5 - if: ${{ github.event_name != 'pull_request' }} - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - run: npm ci - - name: Generate cht-datasource TypeDoc - run: npm run --prefix shared-libs/cht-datasource gen-docs - - name: Generate openapi.json - run: npm run generate-openapi - - name: Main Branch Only - Deploy to GH pages - uses: peaceiris/actions-gh-pages@v4 - if: github.ref == 'refs/heads/master' - with: - personal_token: ${{ secrets.DEPLOY_TO_GITHUB_PAGES }} - external_repository: medic/cht-datasource - publish_dir: ./shared-libs/cht-datasource/docs - user_name: medic-ci - user_email: medic-ci@github - publish_branch: main + - name: Restore generated docs + if: github.ref == 'refs/heads/master' + uses: actions/cache/restore@v4 + with: + key: cht-datasource-docs + path: ./shared-libs/cht-datasource/docs + fail-on-cache-miss: true + - name: Publish docs to GH pages + if: github.ref == 'refs/heads/master' + uses: peaceiris/actions-gh-pages@v4 + with: + personal_token: ${{ secrets.DEPLOY_TO_GITHUB_PAGES }} + external_repository: medic/cht-datasource + publish_dir: ./shared-libs/cht-datasource/docs + user_name: medic-ci + user_email: medic-ci@github + publish_branch: main upgrade: needs: [publish] diff --git a/package.json b/package.json index 9514fe32f89..0d8878c12db 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "dev-sentinel": "npm run --prefix shared-libs/cht-datasource build-watch & npm run --prefix sentinel run-watch", "local-images": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && node scripts/build/cli localDockerComposeFiles", "update-service-worker": "node scripts/build/cli updateServiceWorker", - "generate-openapi": "node ./scripts/generate-openapi.js", "-- DEV TEST SCRIPTS": "-----------------------------------------------------------------------------------------------", "integration-all-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-all", "integration-replication": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-replication", diff --git a/scripts/build/build-documentation.sh b/scripts/build/build-documentation.sh index 9aa2ef66e74..8b342d64362 100755 --- a/scripts/build/build-documentation.sh +++ b/scripts/build/build-documentation.sh @@ -1,10 +1,13 @@ #!/bin/bash set -e +shopt -s extglob echo "build-documentation: building jsdocs" (cd admin && jsdoc -d ../jsdocs/admin -c node_modules/angular-jsdoc/common/conf.json -t node_modules/angular-jsdoc/angular-template src/js/**/*.js) jsdoc -d jsdocs/sentinel sentinel/src/**/*.js jsdoc -d jsdocs/api -R api/README.md api/src/**/*.js -jsdoc -d jsdocs/shared-libs shared-libs/**/src/**/*.js +jsdoc -d jsdocs/shared-libs shared-libs/!(cht-datasource)/src/**/*.js +npm run --prefix shared-libs/cht-datasource gen-docs +node scripts/build/generate-openapi.js echo "build-documentation: done" diff --git a/scripts/generate-openapi.js b/scripts/build/generate-openapi.js similarity index 89% rename from scripts/generate-openapi.js rename to scripts/build/generate-openapi.js index df185812d61..c4cbf1fdb73 100644 --- a/scripts/generate-openapi.js +++ b/scripts/build/generate-openapi.js @@ -5,10 +5,10 @@ const tsj = require('ts-json-schema-generator'); const { Spectral, Document } = require('@stoplight/spectral-core'); const Parsers = require('@stoplight/spectral-parsers'); const { oas } = require('@stoplight/spectral-rulesets'); -const { version } = require('../package.json'); +const { version } = require('../../package.json'); -const DATASOURCE_DIR = path.resolve(__dirname, '../shared-libs/cht-datasource/src'); -const TSCONFIG = path.resolve(__dirname, '../shared-libs/cht-datasource/tsconfig.build.json'); +const DATASOURCE_DIR = path.resolve(__dirname, '../../shared-libs/cht-datasource/src'); +const TSCONFIG = path.resolve(__dirname, '../../shared-libs/cht-datasource/tsconfig.build.json'); const TYPE_SOURCES = [ 'contact.ts', @@ -96,8 +96,8 @@ const SWAGGER_OPTIONS = { }, }, apis: [ - path.resolve(__dirname, '../api/src/routing.js'), - path.resolve(__dirname, '../api/src/controllers/**/*.js'), + path.resolve(__dirname, '../../api/src/routing.js'), + path.resolve(__dirname, '../../api/src/controllers/**/*.js'), ], }; @@ -163,9 +163,8 @@ const main = async () => { Object.assign(swaggerSpec.components.schemas, tsSchemas); swaggerSpec.tags.sort((a, b) => a.name.localeCompare(b.name)); await lintSpec(swaggerSpec); - // TODO Currently publishing with cht-datasource docs site. - const outputPath = path.resolve(__dirname, '../shared-libs/cht-datasource/docs/openapi.json'); - fs.writeFileSync(outputPath, JSON.stringify(swaggerSpec, null, 2) + '\n'); + const outputPath = path.resolve(__dirname, '../../shared-libs/cht-datasource/docs/openapi.json'); + fs.writeFileSync(outputPath, JSON.stringify(swaggerSpec)); }; main() From 8ec6ec2bf047650278cc18385ad3131b96d7c7c0 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Wed, 25 Mar 2026 16:48:41 -0500 Subject: [PATCH 35/37] Fix sonar issues in generation script --- scripts/build/generate-openapi.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/build/generate-openapi.js b/scripts/build/generate-openapi.js index c4cbf1fdb73..da47ee198b5 100644 --- a/scripts/build/generate-openapi.js +++ b/scripts/build/generate-openapi.js @@ -103,14 +103,7 @@ const SWAGGER_OPTIONS = { const SPECTRAL_OPTIONS = { extends: [[oas, 'all']], rules: {} }; -const transformSchema = (obj) => { - if (obj === null || typeof obj !== 'object') { - return obj; - } - if (Array.isArray(obj)) { - return obj.map(transformSchema); - } - +const transformObject = (obj) => { const result = {}; for (const [key, value] of Object.entries(obj)) { if (key === '$ref') { @@ -124,15 +117,24 @@ const transformSchema = (obj) => { if (result.additionalProperties) { result.additionalProperties = true; } - return result; }; +const transformSchema = (schema) => { + if (schema === null || typeof schema !== 'object') { + return schema; + } + if (Array.isArray(schema)) { + return schema.map(transformSchema); + } + return transformObject(schema); +}; + const generateTsSchemas = () => { const schemas = {}; TYPE_SOURCES .map(path => ({ ...TSJ_OPTIONS, path })) - .map(tsj.createGenerator) + .map(opts => tsj.createGenerator(opts)) .map(generator => generator.createSchema()) .map(({ definitions }) => ({ ...definitions, '*': undefined })) .forEach((definitions) => Object.assign(schemas, definitions)); @@ -167,8 +169,7 @@ const main = async () => { fs.writeFileSync(outputPath, JSON.stringify(swaggerSpec)); }; -main() - .catch((err) => { - console.error(err.message); - process.exit(1); - }); +main().catch((err) => { + console.error(err.message); + process.exit(1); +}); From 4b4aa3e9202f02d9142c54cc1e9ccba3676d389e Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 26 Mar 2026 10:13:32 -0500 Subject: [PATCH 36/37] Fix minor issues --- api/src/controllers/africas-talking.js | 1 - api/src/controllers/rapidpro.js | 2 +- api/src/controllers/records.js | 2 +- api/src/controllers/settings.js | 1 - api/src/controllers/sms-gateway.js | 1 - api/src/controllers/users.js | 8 ++++---- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/api/src/controllers/africas-talking.js b/api/src/controllers/africas-talking.js index 84ec56c973a..044f4f0ea95 100644 --- a/api/src/controllers/africas-talking.js +++ b/api/src/controllers/africas-talking.js @@ -146,7 +146,6 @@ module.exports = { * type: string * description: The gateway message reference. * status: - * type: string * enum: [Sent, Submitted, Buffered, Rejected, Success, Failed] * description: The delivery status from Africa's Talking. * failureReason: diff --git a/api/src/controllers/rapidpro.js b/api/src/controllers/rapidpro.js index 9ce2d8b867a..2b46e7ab0c0 100644 --- a/api/src/controllers/rapidpro.js +++ b/api/src/controllers/rapidpro.js @@ -54,7 +54,7 @@ module.exports = { * /api/v1/sms/radpidpro/incoming-messages: * post: * summary: Receive incoming SMS from RapidPro - * operationId: v1SmsRadpidproProIncomingMessagesPost + * operationId: v1SmsRadpidproIncomingMessagesPost * deprecated: true * description: > * Use [POST /api/v2/sms/rapidpro/incoming-messages](#/SMS/v2SmsRapidProIncomingMessagesPost) instead. diff --git a/api/src/controllers/records.js b/api/src/controllers/records.js index c2a4701e382..03aa07558e8 100644 --- a/api/src/controllers/records.js +++ b/api/src/controllers/records.js @@ -102,7 +102,7 @@ module.exports = { * operationId: v1RecordsPost * deprecated: true * description: | - * Use [POST /api/v2/records](#/Records/v2RecordsPost) instead. + * Use [POST /api/v2/records](#/SMS/v2RecordsPost) instead. * * Creates a new record based on a configured form. Accepts form-encoded or JSON data. * tags: [SMS] diff --git a/api/src/controllers/settings.js b/api/src/controllers/settings.js index a384c1a1e57..de022d71005 100644 --- a/api/src/controllers/settings.js +++ b/api/src/controllers/settings.js @@ -108,7 +108,6 @@ module.exports = { * - in: query * name: overwrite * schema: - * type: string * enum: ['true'] * description: > * Whether to replace the entire settings document with the input document. diff --git a/api/src/controllers/sms-gateway.js b/api/src/controllers/sms-gateway.js index fa54aeeb776..7894ff2bb4a 100644 --- a/api/src/controllers/sms-gateway.js +++ b/api/src/controllers/sms-gateway.js @@ -145,7 +145,6 @@ module.exports = { * type: string * description: The message id. * status: - * type: string * enum: [UNSENT, PENDING, SENT, DELIVERED, FAILED] * description: The delivery status from the gateway. * reason: diff --git a/api/src/controllers/users.js b/api/src/controllers/users.js index 31f342e5100..f10659c2eba 100644 --- a/api/src/controllers/users.js +++ b/api/src/controllers/users.js @@ -291,7 +291,7 @@ module.exports = { * operationId: v1UsersGet * deprecated: true * description: > - * Use [GET /api/v2/users](#/Users/v2UsersGet) instead. + * Use [GET /api/v2/users](#/User/v2UsersGet) instead. * Returns a list of users and their profile data. * tags: [User] * x-permissions: @@ -326,8 +326,8 @@ module.exports = { * operationId: v1UsersPost * deprecated: true * description: | - * Use [POST /api/v3/users](#/Users/v3UsersPost) to create a single user or - * [POST /api/v2/users](#/Users/v2UsersPost) to create multiple users. + * Use [POST /api/v3/users](#/User/v3UsersPost) to create a single user or + * [POST /api/v2/users](#/User/v2UsersPost) to create multiple users. * Create new users with a place and a contact. Accepts a single user object or an array of user objects * (since CHT `3.15.0`). Users are created in parallel and the creation is not aborted even if one of the * users fails to be created. @@ -387,7 +387,7 @@ module.exports = { * operationId: v1UsersUsernamePost * deprecated: true * description: > - * Use [POST /api/v3/users/{username}](#/Users/v3UsersUsernamePost) instead. + * Use [POST /api/v3/users/{username}](#/User/v3UsersUsernamePost) instead. * Update property values on a user account. Users with `can_edit` and `can_update_users` * permissions can update any user. Users can update themselves without these permissions, * but cannot modify `type`, `roles`, `contact`, or `place`. Password changes require From 61b98ac853aa1f6c45347698b6fd0491d4d9dabd Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Thu, 26 Mar 2026 15:23:15 -0500 Subject: [PATCH 37/37] More cleanup --- api/src/controllers/export-data.js | 59 +++++++++++++++++++----------- api/src/controllers/forms.js | 58 +++++++++++++++-------------- api/src/controllers/person.js | 4 +- api/src/controllers/place.js | 4 +- api/src/controllers/records.js | 7 ++-- api/src/controllers/report.js | 4 +- api/src/controllers/settings.js | 10 ++--- api/src/routing.js | 1 + scripts/build/generate-openapi.js | 6 +-- 9 files changed, 90 insertions(+), 63 deletions(-) diff --git a/api/src/controllers/export-data.js b/api/src/controllers/export-data.js index 73a6330a250..e71bf4de5c2 100644 --- a/api/src/controllers/export-data.js +++ b/api/src/controllers/export-data.js @@ -62,7 +62,9 @@ const getVerifiedValue = (value) => { * description: The unique key for the user's device. * date: * type: string - * description: The date the telemetry entry was taken (YYYY-MM-DD). + * description: > + * The date the telemetry entry was taken (YYYY-MM-DD), see + * [relevant docs](/technical-overview/data/performance/telemetry/). * browser: * type: object * properties: @@ -74,7 +76,9 @@ const getVerifiedValue = (value) => { * description: The version of the browser used. * apk: * type: string - * description: The version code of the Android app. + * description: > + * The [version code](https://developer.android.com/reference/android/R.styleable#AndroidManifest_versionCode) + * of the Android app. * android: * type: string * description: The version of Android OS. @@ -101,7 +105,8 @@ const getVerifiedValue = (value) => { * type: object * properties: * humanReadable: - * enum: ['true'] + * enum: ['true', 'false'] + * default: 'false' * description: Set to "true" to format dates as ISO 8601 instead of epoch timestamps. * responses: * CsvExport: @@ -175,7 +180,9 @@ module.exports = { * post: * summary: Export DHIS2 target data * operationId: v2ExportDhisPost + * deprecated: true * description: > + * Use [GET /api/v2/export/dhis](#/Export/v2ExportDhisGet) instead. * Exports target data formatted as a DHIS2 dataValueSet. Accepts filters in the request body. * tags: [Export] * x-permissions: @@ -279,7 +286,10 @@ module.exports = { * post: * summary: Export reports * operationId: v2ExportReportsPost - * description: Exports reports as CSV. Accepts filters in the request body. + * deprecated: true + * description: > + * Use [GET /api/v2/export/reports](#/Export/v2ExportReportsGet) instead. + * Exports reports as CSV. Accepts filters in the request body. * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_messages] @@ -342,7 +352,7 @@ module.exports = { * description: | * Exports messages as CSV. * - * ### Columns + * ### Response Columns * * | Column | Description | * | ------------------ | ------------------------------------------------------------------------------------ | @@ -392,7 +402,9 @@ module.exports = { * post: * summary: Export messages * operationId: v2ExportMessagesPost + * deprecated: true * description: > + * Use [GET /api/v2/export/messages](#/Export/v2ExportMessagesGet) instead. * Exports messages as CSV. Accepts filters in the request body. See * [GET](#/Export/v2ExportMessagesGet) for output column details. * tags: [Export] @@ -434,7 +446,7 @@ module.exports = { * Exports contacts as a CSV. Filters use the same query format as the * [search library](https://github.com/medic/cht-core/tree/master/shared-libs/search). * - * ### Columns + * ### Response Columns * * | Column | Description | * | -------------| -------------------------------------------------------------------------------| @@ -456,7 +468,7 @@ module.exports = { * properties: * search: * type: string - * description: A freetext search term. + * description: A freetext search term. (e.g. `GET /api/v2/export/contacts?filters[search]=jim`) * additionalProperties: true * style: deepObject * explode: true @@ -471,7 +483,9 @@ module.exports = { * post: * summary: Export contacts * operationId: v2ExportContactsPost + * deprecated: true * description: > + * Use [GET /api/v2/export/contacts](#/Export/v2ExportContactsGet) instead. * Exports contacts as a CSV. Accepts filters in the request body. See [GET](#/Export/v2ExportContactsGet) for * output column details. * tags: [Export] @@ -505,8 +519,6 @@ module.exports = { * tags: [Export] * x-permissions: * hasAny: [can_export_all, can_export_feedback] - * parameters: - * - $ref: '#/components/parameters/exportOptionsQuery' * responses: * '200': * $ref: '#/components/responses/CsvExport' @@ -517,7 +529,10 @@ module.exports = { * post: * summary: Export feedback * operationId: v2ExportFeedbackPost - * description: Exports user feedback data as CSV. Accepts options in the request body. + * deprecated: true + * description: > + * Use [GET /api/v2/export/feedback](#/Export/v2ExportFeedbackGet) instead. + * Exports user feedback data as CSV. * tags: [Export] * x-permissions: * isOnline: true @@ -527,9 +542,7 @@ module.exports = { * application/json: * schema: * type: object - * properties: - * options: - * $ref: '#/components/schemas/ExportOptions' + * additionalProperties: true * responses: * '200': * $ref: '#/components/responses/CsvExport' @@ -542,12 +555,17 @@ module.exports = { * summary: Export user device information * operationId: v2ExportUserDevicesGet * description: | - * Returns a JSON array of CHT-related software versions and device information for each user device. - * This information is derived from the latest telemetry entry for each user device. - * * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using * this endpoint or use it only when end users will not be impacted. An improved endpoint is * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. + * + * Returns a JSON array of CHT-related software versions and device information for each user device. + * This information is derived from the latest telemetry entry for each user device. + * + * If a particular user has used multiple devices, an entry will be included for each device. Reference the + * date value to determine which devices have been recently used. If multiple users used the same physical + * device (e.g. they were logged into the same phone at different times), an entry will be included for each + * user. * tags: [Export] * deprecated: true * x-since: 4.7.0 @@ -563,15 +581,12 @@ module.exports = { * post: * summary: Export user device information * operationId: v2ExportUserDevicesPost - * description: | + * deprecated: true + * description: > + * Use [GET /api/v2/export/user-devices](#/Export/v2ExportUserDevicesGet) instead. * Returns a JSON array of CHT-related software versions and device information for each user device. * This endpoint may negatively impact server performance. - * - * This endpoint is deprecated as it can negatively impact server performance. Deployments should avoid using - * this endpoint or use it only when end users will not be impacted. An improved endpoint is - * [being planned](https://github.com/medic/cht-core/issues/10298) for a later date. * tags: [Export] - * deprecated: true * x-since: 4.7.0 * x-permissions: * hasAny: [can_export_all, can_export_devices_details] diff --git a/api/src/controllers/forms.js b/api/src/controllers/forms.js index 36f77f561a8..edff50c031b 100644 --- a/api/src/controllers/forms.js +++ b/api/src/controllers/forms.js @@ -63,14 +63,17 @@ module.exports = { * operationId: v1FormsGet * description: > * Returns a list of currently installed forms. By default returns a JSON array of form filenames. If the - * `X-OpenRosa-Version` header is set to `1.0`, returns an OpenRosa xformsList compatible XML response instead. + * `X-OpenRosa-Version` header is set to `1.0`, returns an OpenRosa `xformsList` compatible XML response + * instead. * tags: [Config] * parameters: * - in: header * name: X-OpenRosa-Version * schema: * enum: ['1.0'] - * description: If set to "1.0", returns XML formatted forms list compatible with the OpenRosa FormListAPI. + * description: > + * If set to "1.0", returns XML formatted forms list compatible with the + * [OpenRosa FormListAPI](https://bitbucket.org/javarosa/javarosa/wiki/FormListAPI). * responses: * '200': * description: List of installed forms @@ -83,32 +86,33 @@ module.exports = { * example: ["anc_visit.xml", "anc_registration.xml"] * text/xml: * schema: - * type: string + * type: object + * xml: + * name: xforms + * namespace: 'http://openrosa.org/xforms/xformsList' * description: OpenRosa xformsList compatible XML. - * example: | - * ```.xml - * - * - * - * Visit - * ANCVisit - * md5:1f0f096602ed794a264ab67224608cf4 - * http://medic.local/api/v1/forms/anc_visit.xml - * - * - * Registration with LMP - * PregnancyRegistration - * md5:1f0f096602ed794a264ab67224608cf4 - * http://medic.local/api/v1/forms/anc_registration.xml - * - * - * Stop - * Stop - * md5:1f0f096602ed794a264ab67224608cf4 - * http://medic.local/api/v1/forms/off.xml - * - * - * ``` + * properties: + * xform: + * type: array + * xml: + * wrapped: false + * items: + * type: object + * xml: + * name: xform + * properties: + * name: + * type: string + * example: Visit + * formID: + * type: string + * example: ANCVisit + * hash: + * type: string + * example: 'md5:1f0f096602ed794a264ab67224608cf4' + * downloadUrl: + * type: string + * example: 'http://medic.local/api/v1/forms/anc_visit.xml' * '401': * $ref: '#/components/responses/Unauthorized' */ diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 506bf64cdc2..f9468ab39e5 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -160,7 +160,9 @@ module.exports = { * put: * summary: Update a person * operationId: v1PersonIdPut - * description: Updates an existing person contact record. + * description: > + * Updates an existing person contact record. Fields omitted on the request will be removed from the record. + * Any included lineage data will be minified on the stored record. * tags: [Person] * x-since: 5.2.0 * x-permissions: diff --git a/api/src/controllers/place.js b/api/src/controllers/place.js index 776f241682e..02dfc657f9d 100644 --- a/api/src/controllers/place.js +++ b/api/src/controllers/place.js @@ -159,7 +159,9 @@ module.exports = { * put: * summary: Update a place * operationId: v1PlaceIdPut - * description: Updates an existing place contact record. + * description: > + * Updates an existing place contact record. Fields omitted on the request will be removed from the record. + * Any included lineage data will be minified on the stored record. * tags: [Place] * x-since: 5.2.0 * x-permissions: diff --git a/api/src/controllers/records.js b/api/src/controllers/records.js index 03aa07558e8..d5052bb6238 100644 --- a/api/src/controllers/records.js +++ b/api/src/controllers/records.js @@ -101,9 +101,8 @@ module.exports = { * summary: Create a record * operationId: v1RecordsPost * deprecated: true - * description: | + * description: > * Use [POST /api/v2/records](#/SMS/v2RecordsPost) instead. - * * Creates a new record based on a configured form. Accepts form-encoded or JSON data. * tags: [SMS] * x-permissions: @@ -138,7 +137,9 @@ module.exports = { * post: * summary: Create a record * operationId: v2RecordsPost - * description: Creates a new record based on a configured form. Accepts either form-encoded or JSON data. + * description: > + * Creates a new record based on a configured [JSON form](/building/reference/app-settings/forms/). + * Accepts either form-encoded or JSON data. * tags: [SMS] * x-permissions: * hasAll: [can_create_records] diff --git a/api/src/controllers/report.js b/api/src/controllers/report.js index 8109a36da06..e820648e90f 100644 --- a/api/src/controllers/report.js +++ b/api/src/controllers/report.js @@ -161,7 +161,9 @@ module.exports = { * put: * summary: Update a report * operationId: v1ReportIdPut - * description: Updates an existing report. + * description: > + * Updates an existing report. Fields omitted on the request will be removed from the record. + * Any included lineage data will be minified on the stored record. * tags: [Report] * x-since: 5.2.0 * x-permissions: diff --git a/api/src/controllers/settings.js b/api/src/controllers/settings.js index de022d71005..4f70a51f10e 100644 --- a/api/src/controllers/settings.js +++ b/api/src/controllers/settings.js @@ -101,14 +101,14 @@ module.exports = { * - in: query * name: replace * schema: - * enum: ['true'] - * description: > - * Whether to replace existing settings for the given properties or to merge. - * Defaults to merging. + * enum: ['true', 'false'] + * default: 'false' + * description: Whether to replace existing settings for the given properties or to merge. * - in: query * name: overwrite * schema: - * enum: ['true'] + * enum: ['true', 'false'] + * default: 'false' * description: > * Whether to replace the entire settings document with the input document. * If both `replace` and `overwrite` are set, `overwrite` takes precedence. diff --git a/api/src/routing.js b/api/src/routing.js index 8502934130d..5f288726735 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -668,6 +668,7 @@ app.putJson('/api/v1/place/:uuid', place.v1.update); * description: The person's name. * type: * type: string + * default: person * description: > * ID of the contact_type for the new person. Defaults to `person` for backwards compatibility. * place: diff --git a/scripts/build/generate-openapi.js b/scripts/build/generate-openapi.js index da47ee198b5..09024d0d8d1 100644 --- a/scripts/build/generate-openapi.js +++ b/scripts/build/generate-openapi.js @@ -72,18 +72,18 @@ const SWAGGER_OPTIONS = { in: 'query', name: 'limit', schema: { type: 'number', default: 100, minimum: 1 }, - description: 'The maximum number of entities to return. Defaults to 100.', + description: 'The maximum number of entities to return.', }, limitId: { in: 'query', name: 'limit', schema: { type: 'number', default: 10000, minimum: 1 }, - description: 'The maximum number of identifiers to return. Defaults to 10000.', + description: 'The maximum number of identifiers to return.', }, withLineage: { in: 'query', name: 'with_lineage', - schema: { 'enum': ['true'] }, + schema: { 'enum': ['true', 'false'], default: 'false' }, description: 'Include the full parent lineage.' } },