diff --git a/apps/agora/api-docs/.env.example b/apps/agora/api-docs/.env.example new file mode 100644 index 0000000000..b5c290eddd --- /dev/null +++ b/apps/agora/api-docs/.env.example @@ -0,0 +1 @@ +PORT=8010 \ No newline at end of file diff --git a/apps/agora/api-docs/Dockerfile b/apps/agora/api-docs/Dockerfile new file mode 100644 index 0000000000..45838db32c --- /dev/null +++ b/apps/agora/api-docs/Dockerfile @@ -0,0 +1,9 @@ +FROM redocly/redoc:v2.0.0 + +HEALTHCHECK --interval=2s --timeout=3s --retries=5 --start-period=2s \ + CMD curl --fail --silent "localhost:8010" || exit 1 + +COPY build/redoc-static.html /usr/share/nginx/html/index.html +COPY favicon.ico /usr/share/nginx/html/ + +EXPOSE 8010 \ No newline at end of file diff --git a/apps/agora/api-docs/favicon.ico b/apps/agora/api-docs/favicon.ico new file mode 100644 index 0000000000..579161c11f Binary files /dev/null and b/apps/agora/api-docs/favicon.ico differ diff --git a/apps/agora/api-docs/index.hbs b/apps/agora/api-docs/index.hbs new file mode 100644 index 0000000000..2adf572d49 --- /dev/null +++ b/apps/agora/api-docs/index.hbs @@ -0,0 +1,20 @@ + + + + {{title}} + + {{!-- --}} + + + {{{redocHead}}} + {{#unless disableGoogleFont}}{{/unless}} + + + {{{redocHTML}}} + + \ No newline at end of file diff --git a/apps/agora/api-docs/project.json b/apps/agora/api-docs/project.json new file mode 100644 index 0000000000..d647e8ac1e --- /dev/null +++ b/apps/agora/api-docs/project.json @@ -0,0 +1,87 @@ +{ + "name": "agora-api-docs", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "targets": { + "create-config": { + "executor": "nx:run-commands", + "options": { + "command": "cp -n .env.example .env", + "cwd": "{projectRoot}" + } + }, + "build": { + "executor": "nx:run-commands", + "options": { + "command": "redocly build-docs --config redocly.yaml --template index.hbs --output build/redoc-static.html", + "cwd": "{projectRoot}" + } + }, + "serve": { + "executor": "nx:run-commands", + "options": { + "command": "redocly preview-docs --config redocly.yaml --port 8010", + "cwd": "{projectRoot}" + } + }, + "serve-detach": { + "executor": "nx:run-commands", + "options": { + "command": "docker/agora/serve-detach.sh agora-api-docs" + } + }, + "build-image": { + "executor": "@nx-tools/nx-container:build", + "options": { + "context": "apps/agora/api-docs", + "metadata": { + "images": [ + "ghcr.io/sage-bionetworks/agora-api-docs" + ], + "tags": [ + "type=edge,branch=main", + "type=raw,value=local", + "type=sha" + ] + }, + "push": false + }, + "dependsOn": [ + "build" + ] + }, + "publish-image": { + "executor": "@nx-tools/nx-container:build", + "options": { + "context": "apps/agora/api-docs", + "metadata": { + "images": [ + "ghcr.io/sage-bionetworks/agora-api-docs" + ], + "tags": [ + "type=edge,branch=main", + "type=sha" + ] + }, + "push": true + }, + "dependsOn": [ + "build-image" + ] + }, + "scan-image": { + "executor": "nx:run-commands", + "options": { + "command": "trivy image ghcr.io/sage-bionetworks/agora-api-docs:local --quiet", + "color": true + } + } + }, + "tags": [ + "type:docs", + "scope:backend" + ], + "implicitDependencies": [ + "agora-api-description" + ] +} \ No newline at end of file diff --git a/apps/agora/api-docs/redocly.yaml b/apps/agora/api-docs/redocly.yaml new file mode 100644 index 0000000000..d8651d7af6 --- /dev/null +++ b/apps/agora/api-docs/redocly.yaml @@ -0,0 +1,11 @@ +# See https://docs.redoc.ly/cli/configuration/ for more information. +apis: + main: + root: '../../../libs/agora/api-description/build/openapi.yaml' +# theme: +# openapi: +# theme: +# rightPanel: +# backgroundColor: '#314fa7' +# sidebar: +# backgroundColor: '#ffffff' diff --git a/docker/agora/serve-detach.sh b/docker/agora/serve-detach.sh index 6afce4478b..978e2256f4 100755 --- a/docker/agora/serve-detach.sh +++ b/docker/agora/serve-detach.sh @@ -2,6 +2,7 @@ args=( # List of services in alphanumeric order + --file docker/agora/services/api-docs.yml --file docker/agora/services/app.yml --file docker/agora/services/data.yml --file docker/agora/services/mongo.yml diff --git a/docker/agora/services/api-docs.yml b/docker/agora/services/api-docs.yml new file mode 100644 index 0000000000..1882f7ab9a --- /dev/null +++ b/docker/agora/services/api-docs.yml @@ -0,0 +1,15 @@ +services: + agora-api-docs: + image: ghcr.io/sage-bionetworks/agora-api-docs:${AGORA_VERSION:-local} + container_name: agora-api-docs + restart: always + env_file: + - ../../../apps/agora/api-docs/.env + networks: + - agora + ports: + - '8010:8010' + deploy: + resources: + limits: + memory: 200M diff --git a/libs/agora/api-description/.gitignore b/libs/agora/api-description/.gitignore new file mode 100644 index 0000000000..684ae5d80c --- /dev/null +++ b/libs/agora/api-description/.gitignore @@ -0,0 +1 @@ +!build \ No newline at end of file diff --git a/libs/agora/api-description/README.md b/libs/agora/api-description/README.md new file mode 100644 index 0000000000..08efae00a6 --- /dev/null +++ b/libs/agora/api-description/README.md @@ -0,0 +1,7 @@ +# api-spec + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test api-spec` to execute the unit tests. diff --git a/libs/agora/api-description/build/openapi.yaml b/libs/agora/api-description/build/openapi.yaml new file mode 100644 index 0000000000..58a1b01753 --- /dev/null +++ b/libs/agora/api-description/build/openapi.yaml @@ -0,0 +1,87 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Agora REST API + license: + name: Apache 2.0 + url: https://github.com/Sage-Bionetworks/sage-monorepo/blob/main/LICENSE.txt + contact: + name: Support + url: https://github.com/Sage-Bionetworks/sage-monorepo + x-logo: + url: https://dev.openchallenges.io/img/unsafe/logo/OpenChallenges-logo.png +servers: + - url: http://localhost/v1 +tags: + - name: Dataversion + description: Operations about dataversion. +paths: + /dataversion: + get: + tags: + - Dataversion + summary: Get dataversion + description: Get dataversion + operationId: getDataversion + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Dataversion' + description: Success + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' +components: + schemas: + Dataversion: + type: object + description: A dataversion + properties: + data_file: + type: string + data_version: + type: string + team_images_id: + type: string + required: + - data_file + - data_version + - team_images_id + BasicError: + type: object + description: Problem details (tools.ietf.org/html/rfc7807) + properties: + title: + type: string + description: A human readable documentation for the problem type + status: + type: integer + description: The HTTP status code + detail: + type: string + description: A human readable explanation specific to this occurrence of the problem + type: + type: string + description: An absolute URI that identifies the problem type + required: + - title + - status + x-java-class-annotations: + - '@lombok.AllArgsConstructor' + - '@lombok.Builder' + responses: + BadRequest: + description: Invalid request + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BasicError' + InternalServerError: + description: The request cannot be fulfilled due to an unexpected server error + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BasicError' diff --git a/libs/agora/api-description/openapi-lint-config.yaml b/libs/agora/api-description/openapi-lint-config.yaml new file mode 100644 index 0000000000..f57d5db0f5 --- /dev/null +++ b/libs/agora/api-description/openapi-lint-config.yaml @@ -0,0 +1,4 @@ +errorsOnly: true +summaryOnly: false +limits: + warnings: 25 diff --git a/libs/agora/api-description/project.json b/libs/agora/api-description/project.json new file mode 100644 index 0000000000..4a28748f28 --- /dev/null +++ b/libs/agora/api-description/project.json @@ -0,0 +1,37 @@ +{ + "name": "agora-api-description", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/agora/api-description/src", + "projectType": "library", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "command": "redocly bundle --output build/openapi.yaml src/openapi.yaml", + "cwd": "{projectRoot}" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "lint-openapi --config openapi-lint-config.yaml --ruleset spectral.yaml build/openapi.yaml", + "cwd": "{projectRoot}" + }, + "dependsOn": ["build"] + }, + "format": { + "executor": "nx:run-commands", + "options": { + "command": "prettier --write {projectRoot}" + } + }, + "format-check": { + "executor": "nx:run-commands", + "options": { + "command": "prettier --check {projectRoot}" + } + } + }, + "tags": [], + "implicitDependencies": [] +} diff --git a/libs/agora/api-description/spectral.yaml b/libs/agora/api-description/spectral.yaml new file mode 100644 index 0000000000..1ce3811675 --- /dev/null +++ b/libs/agora/api-description/spectral.yaml @@ -0,0 +1,6 @@ +extends: '@ibm-cloud/openapi-ruleset' +rules: + ibm-enum-casing-convention: off + ibm-parameter-casing-convention: off + ibm-path-segment-casing-convention: off + ibm-property-casing-convention: off diff --git a/libs/agora/api-description/src/components/README.md b/libs/agora/api-description/src/components/README.md new file mode 100644 index 0000000000..7be6c7d692 --- /dev/null +++ b/libs/agora/api-description/src/components/README.md @@ -0,0 +1,13 @@ +# Reusable components + +- You can create the following folders here: + - `schemas` - reusable [Schema Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) + - `responses` - reusable [Response Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) + - `parameters` - reusable [Parameter Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) + - `examples` - reusable [Example Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject) + - `headers` - reusable [Header Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) + - `requestBodies` - reusable [Request Body Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject) + - `links` - reusable [Link Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject) + - `callbacks` - reusable [Callback Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject) + - `securitySchemes` - reusable [Security Scheme Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject) +- Filename of files inside the folders represent component name, e.g. `Customer.yaml` diff --git a/libs/agora/api-description/src/components/headers/ExpiresAfter.yaml b/libs/agora/api-description/src/components/headers/ExpiresAfter.yaml new file mode 100644 index 0000000000..0cbe9a5101 --- /dev/null +++ b/libs/agora/api-description/src/components/headers/ExpiresAfter.yaml @@ -0,0 +1,4 @@ +description: date in UTC when token expires +schema: + type: string + format: date-time diff --git a/libs/agora/api-description/src/components/responses/BadRequest.yaml b/libs/agora/api-description/src/components/responses/BadRequest.yaml new file mode 100644 index 0000000000..f1a3d5dd25 --- /dev/null +++ b/libs/agora/api-description/src/components/responses/BadRequest.yaml @@ -0,0 +1,5 @@ +description: Invalid request +content: + application/problem+json: + schema: + $ref: ../schemas/BasicError.yaml diff --git a/libs/agora/api-description/src/components/responses/Conflict.yaml b/libs/agora/api-description/src/components/responses/Conflict.yaml new file mode 100644 index 0000000000..d725a306cb --- /dev/null +++ b/libs/agora/api-description/src/components/responses/Conflict.yaml @@ -0,0 +1,5 @@ +description: The request conflicts with current state of the target resource +content: + application/problem+json: + schema: + $ref: ../schemas/BasicError.yaml diff --git a/libs/agora/api-description/src/components/responses/InternalServerError.yaml b/libs/agora/api-description/src/components/responses/InternalServerError.yaml new file mode 100644 index 0000000000..b660ed8ebe --- /dev/null +++ b/libs/agora/api-description/src/components/responses/InternalServerError.yaml @@ -0,0 +1,5 @@ +description: The request cannot be fulfilled due to an unexpected server error +content: + application/problem+json: + schema: + $ref: ../schemas/BasicError.yaml diff --git a/libs/agora/api-description/src/components/responses/NotFound.yaml b/libs/agora/api-description/src/components/responses/NotFound.yaml new file mode 100644 index 0000000000..695a10631a --- /dev/null +++ b/libs/agora/api-description/src/components/responses/NotFound.yaml @@ -0,0 +1,5 @@ +description: The specified resource was not found +content: + application/problem+json: + schema: + $ref: ../schemas/BasicError.yaml diff --git a/libs/agora/api-description/src/components/responses/Unauthorized.yaml b/libs/agora/api-description/src/components/responses/Unauthorized.yaml new file mode 100644 index 0000000000..b7955fdcd4 --- /dev/null +++ b/libs/agora/api-description/src/components/responses/Unauthorized.yaml @@ -0,0 +1,5 @@ +description: Unauthorized +content: + application/problem+json: + schema: + $ref: ../schemas/BasicError.yaml diff --git a/libs/agora/api-description/src/components/schemas/BasicError.yaml b/libs/agora/api-description/src/components/schemas/BasicError.yaml new file mode 100644 index 0000000000..08409396e6 --- /dev/null +++ b/libs/agora/api-description/src/components/schemas/BasicError.yaml @@ -0,0 +1,22 @@ +type: object +description: Problem details (tools.ietf.org/html/rfc7807) +properties: + title: + type: string + description: A human readable documentation for the problem type + status: + type: integer + description: The HTTP status code + detail: + type: string + description: A human readable explanation specific to this occurrence of + the problem + type: + type: string + description: An absolute URI that identifies the problem type +required: + - title + - status +x-java-class-annotations: + - '@lombok.AllArgsConstructor' + - '@lombok.Builder' diff --git a/libs/agora/api-description/src/components/schemas/Dataversion.yaml b/libs/agora/api-description/src/components/schemas/Dataversion.yaml new file mode 100644 index 0000000000..314e8b3716 --- /dev/null +++ b/libs/agora/api-description/src/components/schemas/Dataversion.yaml @@ -0,0 +1,13 @@ +type: object +description: A dataversion +properties: + data_file: + type: string + data_version: + type: string + team_images_id: + type: string +required: + - data_file + - data_version + - team_images_id diff --git a/libs/agora/api-description/src/components/schemas/EmptyObject.yaml b/libs/agora/api-description/src/components/schemas/EmptyObject.yaml new file mode 100644 index 0000000000..abf454afa2 --- /dev/null +++ b/libs/agora/api-description/src/components/schemas/EmptyObject.yaml @@ -0,0 +1,2 @@ +type: object +description: Empty JSON object diff --git a/libs/agora/api-description/src/openapi.yaml b/libs/agora/api-description/src/openapi.yaml new file mode 100644 index 0000000000..23005a7ab0 --- /dev/null +++ b/libs/agora/api-description/src/openapi.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Agora REST API + license: + name: Apache 2.0 + url: https://github.com/Sage-Bionetworks/sage-monorepo/blob/main/LICENSE.txt + contact: + name: Support + url: https://github.com/Sage-Bionetworks/sage-monorepo + x-logo: + url: https://dev.openchallenges.io/img/unsafe/logo/OpenChallenges-logo.png +servers: + - url: http://localhost/v1 +tags: + - name: Dataversion + description: Operations about dataversion. +paths: + /dataversion: + $ref: paths/dataversion.yaml diff --git a/libs/agora/api-description/src/paths/README.md b/libs/agora/api-description/src/paths/README.md new file mode 100644 index 0000000000..9ec9386823 --- /dev/null +++ b/libs/agora/api-description/src/paths/README.md @@ -0,0 +1,107 @@ +# Paths + +Organize your path definitions within this folder. You will reference your paths from your main `openapi.yaml` entrypoint file. + +It may help you to adopt some conventions: + +- path separator token (e.g. `@`) or subfolders +- path parameter (e.g. `{example}`) +- file-per-path or file-per-operation + +There are different benefits and drawbacks to each decision. + +You can adopt any organization you wish. We have some tips for organizing paths based on common practices. + +## Each path in a separate file + +Use a predefined "path separator" and keep all of your path files in the top level of the `paths` folder. + +``` +# todo: insert tree view of paths folder +``` + +Redocly recommends using the `@` character for this case. + +In addition, Redocly recommends placing path parameters within `{}` curly braces if you adopt this style. + +#### Motivations + +- Quickly see a list of all paths. Many people think in terms of the "number" of "endpoints" (paths), and not the "number" of "operations" (paths \* http methods). + +- Only the "file-per-path" option is semantically correct with the OpenAPI Specification 3.0.2. However, Redocly's openapi-cli will build valid bundles for any of the other options too. + +#### Drawbacks + +- This may require multiple definitions per http method within a single file. +- It requires settling on a path separator (that is allowed to be used in filenames) and sticking to that convention. + +## Each operation in a separate file + +You may also place each operation in a separate file. + +In this case, if you want all paths at the top-level, you can concatenate the http method to the path name. Similar to the above option, you can + +### Files at top-level of `paths` + +You may name your files with some concatenation for the http method. For example, following a convention such as: `-.yaml`. + +#### Motivations + +- Quickly see all operations without needing to navigate subfolders. + +#### Drawbacks + +- Adopting an unusual path separator convention, instead of using subfolders. + +### Use subfolders to mirror API path structure + +Example: + +``` +GET /customers + +/paths/customers/get.yaml +``` + +In this case, the path id defined within subfolders which mirror the API URL structure. + +Example with path parameter: + +``` +GET /customers/{id} + +/paths/customers/{id}/get.yaml +``` + +#### Motivations + +It matches the URL structure. + +It is pretty easy to reference: + +```yaml +paths: + '/customers/{id}': + get: + $ref: ./paths/customers/{id}/get.yaml + put: + $ref: ./paths/customers/{id}/put.yaml +``` + +#### Drawbacks + +If you have a lot of nested folders, it may be confusing to reference your schemas. + +Example + +``` +file: /paths/customers/{id}/timeline/{messageId}/get.yaml + +# excerpt of file + headers: + Rate-Limit-Remaining: + $ref: ../../../../../components/headers/Rate-Limit-Remaining.yaml + +``` + +Notice the `../../../../../` in the ref which requires some attention to formulate correctly. While openapi-cli has a linter which suggests possible refs when there is a mistake, this is still a net drawback for APIs with deep paths. diff --git a/libs/agora/api-description/src/paths/dataversion.yaml b/libs/agora/api-description/src/paths/dataversion.yaml new file mode 100644 index 0000000000..d41813ac80 --- /dev/null +++ b/libs/agora/api-description/src/paths/dataversion.yaml @@ -0,0 +1,19 @@ +get: + tags: + - Dataversion + summary: Get dataversion + description: Get dataversion + operationId: getDataversion + # parameters: + # - $ref: ../components/parameters/query/challengeSearchQuery.yaml + responses: + '200': + content: + application/json: + schema: + $ref: ../components/schemas/Dataversion.yaml + description: Success + '400': + $ref: ../components/responses/BadRequest.yaml + '500': + $ref: ../components/responses/InternalServerError.yaml