From eff58d3862b5846c50c1f0dda40859cc7c1ece53 Mon Sep 17 00:00:00 2001 From: Luca Marchesini Date: Mon, 25 Oct 2021 11:31:13 +0200 Subject: [PATCH 01/13] [ci] Cross-repo doc deploy (#2123) --- .github/actions/deploy-doc/action.yml | 57 --------------- .github/workflows/pull_request.workflow.yml | 33 +++++++-- .github/workflows/push_dev.workflow.yml | 64 ++++++++++++----- .github/workflows/push_master.workflow.yml | 70 +++++++++++++------ .../security/get-user-strategies/index.md | 2 +- doc/2/framework/events/realtime/index.md | 4 +- .../main-concepts/authentication/index.md | 2 +- 7 files changed, 126 insertions(+), 106 deletions(-) delete mode 100644 .github/actions/deploy-doc/action.yml diff --git a/.github/actions/deploy-doc/action.yml b/.github/actions/deploy-doc/action.yml deleted file mode 100644 index 9671a4b8d5..0000000000 --- a/.github/actions/deploy-doc/action.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Deploy Documentation -description: Build doc, upload it to S3 and invalidate Cloudfront cache - -inputs: - AWS_ACCESS_KEY_ID: - description: AWS Access key ID - required: true - AWS_SECRET_ACCESS_KEY: - description: AWS secret key - required: true - S3_BUCKET: - description: S3 bucket name - required: true - CLOUDFRONT_ID: - description: Cloudfront distribution ID - required: true - REGION: - description: AWS default region - required: true - default: 'us-west-2' - FRAMEWORK_BRANCH: - description: Documentation framework branch to use - required: true - default: 'master' - -runs: - using: "composite" - steps: - - name: Install AWS CLI - run: | - sudo apt-get update - sudo apt-get install python python-pip - pip install awscli --upgrade --user - shell: bash - - name: Build documentation - run: | - rm -fr doc/framework - npm ci --production=false - npm i --save-dev kuzdoc - npm run doc-prepare - npm run doc-build - env: - NODE_ENV: production - NODE_OPTIONS: --max-old-space-size=4096 - FRAMEWORK_BRANCH: ${{ inputs.FRAMEWORK_BRANCH }} - shell: bash - - name: Deploy documentation - run: | - npm run doc-upload - npm run doc-cloudfront - env: - AWS_DEFAULT_REGION: ${{ inputs.REGION }} - AWS_ACCESS_KEY_ID: ${{ inputs.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.AWS_SECRET_ACCESS_KEY }} - S3_BUCKET: ${{ inputs.S3_BUCKET }} - CLOUDFRONT_DISTRIBUTION_ID: ${{ inputs.CLOUDFRONT_ID }} - shell: bash diff --git a/.github/workflows/pull_request.workflow.yml b/.github/workflows/pull_request.workflow.yml index 40aaad5cec..0757cf826d 100644 --- a/.github/workflows/pull_request.workflow.yml +++ b/.github/workflows/pull_request.workflow.yml @@ -44,7 +44,7 @@ jobs: needs: [lint] strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -62,7 +62,7 @@ jobs: strategy: matrix: test_set: [http, websocket, mqtt] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -83,7 +83,7 @@ jobs: strategy: matrix: test_set: [http, websocket] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -113,14 +113,14 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] steps: - uses: actions/checkout@v2 - name: Cloning Monkey Tester uses: actions/checkout@v2 with: repository: kuzzleio/kuzzle-monkey-tests - path: 'kuzzle-monkey-tests' + path: "kuzzle-monkey-tests" - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} @@ -130,3 +130,26 @@ jobs: - uses: ./.github/actions/monkey-tests with: node-version: ${{ matrix.node-version }} + + doc-dead-links: + name: Check dead-links + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Extract references from context + shell: bash + id: extract-refs + run: | + echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" + echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + echo "::set-output name=fw-branch::$(if [ $BASE_BRANCH == master ]; then echo master; else echo develop; fi)" + - uses: convictional/trigger-workflow-and-wait@v1.3.0 + with: + owner: kuzzleio + repo: documentation + github_token: ${{ secrets.ACCESS_TOKEN_CI }} + workflow_file_name: dead_links.workflow.yml + ref: ${{ steps.extract-refs.outputs.fw-branch }} + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml index 142a7f172e..c147b2d602 100644 --- a/.github/workflows/push_dev.workflow.yml +++ b/.github/workflows/push_dev.workflow.yml @@ -44,7 +44,7 @@ jobs: needs: [lint] strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -61,8 +61,8 @@ jobs: needs: [unit-tests] strategy: matrix: - test_set: ['http', 'websocket', 'mqtt'] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + test_set: ["http", "websocket", "mqtt"] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -83,7 +83,7 @@ jobs: strategy: matrix: test_set: [http, websocket] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -104,14 +104,14 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] steps: - uses: actions/checkout@v2 - name: Cloning Monkey Tester uses: actions/checkout@v2 with: repository: kuzzleio/kuzzle-monkey-tests - path: 'kuzzle-monkey-tests' + path: "kuzzle-monkey-tests" - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} @@ -131,25 +131,51 @@ jobs: - name: Test error codes run: ./.ci/scripts/check-error-codes-documentation.sh - documentation-staging: - name: Deploy Documentation to staging - needs: [error-codes-check] + doc-dead-links: + name: Check dead-links runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node-version }} + fetch-depth: 0 + - name: Extract references from context + shell: bash + id: extract-refs + run: | + echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" + echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + echo "::set-output name=fw-branch::$(if [ $BASE_BRANCH == master ]; then echo master; else echo develop; fi)" + - uses: convictional/trigger-workflow-and-wait@v1.3.0 + with: + owner: kuzzleio + repo: documentation + github_token: ${{ secrets.ACCESS_TOKEN_CI }} + workflow_file_name: dead_links.workflow.yml + ref: ${{ steps.extract-refs.outputs.fw-branch }} + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + + doc-deploy: + name: Deployment Doc to Next + runs-on: ubuntu-18.04 + needs: [cluster-monkey-tests, doc-dead-links] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Extract references from context + shell: bash + id: extract-refs + run: | + echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" + echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" - uses: ./.github/actions/cache-node-modules with: NODE_VERSION: ${{ matrix.node-version }} - uses: ./.github/actions/deploy-doc with: - REGION: us-west-2 - S3_BUCKET: docs-next.kuzzle.io - CLOUDFRONT_ID: E2ZCCEK9GRB49U - FRAMEWORK_BRANCH: develop - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - env: - NODE_OPTIONS: --max-old-space-size=8096 + owner: kuzzleio + repo: documentation + github_token: ${{ secrets.ACCESS_TOKEN_CI }} + workflow_file_name: child_repo.workflow.yml + ref: master + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml index 310f5752d8..af436d751f 100644 --- a/.github/workflows/push_master.workflow.yml +++ b/.github/workflows/push_master.workflow.yml @@ -44,7 +44,7 @@ jobs: needs: [lint] strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -62,7 +62,7 @@ jobs: strategy: matrix: test_set: [http, websocket, mqtt] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -83,7 +83,7 @@ jobs: strategy: matrix: test_set: [http, websocket] - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -104,14 +104,14 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: ['14.17.0', '12.16.3', '12.20.0'] + node-version: ["14.17.0", "12.16.3", "12.20.0"] steps: - uses: actions/checkout@v2 - name: Cloning Monkey Tester uses: actions/checkout@v2 with: repository: kuzzleio/kuzzle-monkey-tests - path: 'kuzzle-monkey-tests' + path: "kuzzle-monkey-tests" - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} @@ -131,23 +131,51 @@ jobs: - name: Test error codes run: ./.ci/scripts/check-error-codes-documentation.sh - documentation-production: - name: Deploy Documentation to production - needs: [functional-tests-legacy, functional-tests] + doc-dead-links: + name: Check dead-links runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node-version }} - - uses: ./.github/actions/deploy-doc - with: - REGION: us-west-2 - S3_BUCKET: docs.kuzzle.io - CLOUDFRONT_ID: E3D6RP0POLCJMM - FRAMEWORK_BRANCH: master - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + fetch-depth: 0 + - name: Extract references from context + shell: bash + id: extract-refs + run: | + echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" + echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + echo "::set-output name=fw-branch::$(if [ $BASE_BRANCH == master ]; then echo master; else echo develop; fi)" + - uses: convictional/trigger-workflow-and-wait@v1.3.0 + with: + owner: kuzzleio + repo: documentation + github_token: ${{ secrets.ACCESS_TOKEN_CI }} + workflow_file_name: dead_links.workflow.yml + ref: ${{ steps.extract-refs.outputs.fw-branch }} + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + + doc-deploy: + name: Deployment Doc to Prod + runs-on: ubuntu-18.04 + needs: [cluster-monkey-tests, doc-dead-links] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Extract references from context + shell: bash + id: extract-refs + run: | + echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" + echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + - uses: convictional/trigger-workflow-and-wait@v1.3.0 + with: + owner: kuzzleio + repo: documentation + github_token: ${{ secrets.ACCESS_TOKEN_CI }} + workflow_file_name: child_repo.workflow.yml + ref: master + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' dockerhub-deploy: name: Build and deploy images to Dockerhub @@ -177,9 +205,9 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '12.x' - registry-url: 'https://registry.npmjs.org' - - run: npm install --production + node-version: "14.x" + registry-url: "https://registry.npmjs.org" + - run: npm install - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/doc/2/api/controllers/security/get-user-strategies/index.md b/doc/2/api/controllers/security/get-user-strategies/index.md index d03ea896ad..8175a14ec0 100644 --- a/doc/2/api/controllers/security/get-user-strategies/index.md +++ b/doc/2/api/controllers/security/get-user-strategies/index.md @@ -35,7 +35,7 @@ Method: GET ## Arguments -- `_id`: user [kuid](/core/2/guides/kuzzle-depth/authentication#the-kuzzle-user-identifier) +- `_id`: user [kuid](/core/2/guides/main-concepts/authentication#kuzzle-user-identifier-kuid) --- diff --git a/doc/2/framework/events/realtime/index.md b/doc/2/framework/events/realtime/index.md index 7efb7ec79d..89dcb8f63c 100644 --- a/doc/2/framework/events/realtime/index.md +++ b/doc/2/framework/events/realtime/index.md @@ -36,7 +36,7 @@ The provided `subscription` object has the following properties: | `connectionId` |
integer
| [ClientConnection](/core/2/guides/write-protocols/context/clientconnection) unique identifier | | `index` |
string
| Index | | `collection` |
string
| Collection | -| `filters` |
object
| Filters in [Koncorde's normalized format](https://www.npmjs.com/package/koncorde#filter-unique-identifier) | +| `filters` |
object
| Filters in [Koncorde's normalized format](https://github.com/kuzzleio/koncorde/wiki/Filter-Unique-Identifiers) | @@ -140,7 +140,7 @@ The provided `subscription` object has the following properties: | `index` |
string
| Index | | `collection` |
string
| Collection | | `filters` |
object
| Filters in [Koncorde's normalized format](https://www.npmjs.com/package/koncorde#filter-unique-identifier) | -| `kuid` |
string
| ID of the user | +| `kuid` |
string
| ID of the user | --- diff --git a/doc/2/guides/main-concepts/authentication/index.md b/doc/2/guides/main-concepts/authentication/index.md index 9d00372d0d..394d8b80ba 100644 --- a/doc/2/guides/main-concepts/authentication/index.md +++ b/doc/2/guides/main-concepts/authentication/index.md @@ -177,7 +177,7 @@ kourou auth:login -a strategy=local --body '{ When you're sending HTTP requests from a browser you can instruct Kuzzle to `load` and `store` authentication tokens within an [HTTP Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). -This is possible thanks to the option [cookieAuth](/core/2/api/protocols/http#cookieAuth) in [auth:login](/core/2/api/controllers/auth/login), [auth:logout](/core/2/api/controllers/auth/logout), [auth:checkToken](/core/2/api/controllers/auth/checkToken), [auth:refreshToken](/core/2/api/controllers/auth/refresh-token) +This is possible thanks to the option [cookieAuth](/core/2/api/protocols/http#cookieAuth) in [auth:login](/core/2/api/controllers/auth/login), [auth:logout](/core/2/api/controllers/auth/logout), [auth:checkToken](/core/2/api/controllers/auth/check-token), [auth:refreshToken](/core/2/api/controllers/auth/refresh-token) You can disable the cookie authentication by setting `http.cookieAuthentication` to `false` in [Kuzzle Configuration](/core/2/guides/advanced/configuration). From 24f55c4c83635d785e5d0d8bfd13601527fcc488 Mon Sep 17 00:00:00 2001 From: Luca Marchesini Date: Mon, 25 Oct 2021 14:54:16 +0200 Subject: [PATCH 02/13] [ci] fix doc deploy --- .github/workflows/push_dev.workflow.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml index c147b2d602..3ac51da04f 100644 --- a/.github/workflows/push_dev.workflow.yml +++ b/.github/workflows/push_dev.workflow.yml @@ -168,10 +168,7 @@ jobs: run: | echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" - - uses: ./.github/actions/cache-node-modules - with: - NODE_VERSION: ${{ matrix.node-version }} - - uses: ./.github/actions/deploy-doc + - uses: convictional/trigger-workflow-and-wait@v1.3.0 with: owner: kuzzleio repo: documentation From 0ff14ec126e3615b5dd7fc42748411a7c2f058cc Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 26 Oct 2021 10:36:10 +0200 Subject: [PATCH 03/13] [ci.doc-deploy] head_ref vs ref --- .github/workflows/push_dev.workflow.yml | 8 ++++---- .github/workflows/push_master.workflow.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml index 3ac51da04f..d320b5e374 100644 --- a/.github/workflows/push_dev.workflow.yml +++ b/.github/workflows/push_dev.workflow.yml @@ -25,8 +25,8 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: ['14.17.0'] - kuzzle-image: ['kuzzle'] + node-version: ["14.17.0"] + kuzzle-image: ["kuzzle"] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -174,5 +174,5 @@ jobs: repo: documentation github_token: ${{ secrets.ACCESS_TOKEN_CI }} workflow_file_name: child_repo.workflow.yml - ref: master - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + ref: develop + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml index af436d751f..f783dc5f98 100644 --- a/.github/workflows/push_master.workflow.yml +++ b/.github/workflows/push_master.workflow.yml @@ -25,8 +25,8 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: ['14.17.0'] - kuzzle-image: ['kuzzle'] + node-version: ["14.17.0"] + kuzzle-image: ["kuzzle"] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -175,7 +175,7 @@ jobs: github_token: ${{ secrets.ACCESS_TOKEN_CI }} workflow_file_name: child_repo.workflow.yml ref: master - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' dockerhub-deploy: name: Build and deploy images to Dockerhub From 24f6569ddbddd1ce0237646ccc17a164f31cd688 Mon Sep 17 00:00:00 2001 From: Luca Date: Tue, 26 Oct 2021 10:59:27 +0200 Subject: [PATCH 04/13] [ci.doc-deploy] cut ref --- .github/workflows/push_dev.workflow.yml | 3 ++- .github/workflows/push_master.workflow.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml index d320b5e374..405ae4c1b5 100644 --- a/.github/workflows/push_dev.workflow.yml +++ b/.github/workflows/push_dev.workflow.yml @@ -168,6 +168,7 @@ jobs: run: | echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + echo "::set-output name=branch::$(echo $GITHUB_REF | cut -d/ -f 3)" - uses: convictional/trigger-workflow-and-wait@v1.3.0 with: owner: kuzzleio @@ -175,4 +176,4 @@ jobs: github_token: ${{ secrets.ACCESS_TOKEN_CI }} workflow_file_name: child_repo.workflow.yml ref: develop - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ steps.extract-refs.outputs.branch }}", "version": "${{ steps.extract-refs.outputs.version }}"}' diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml index f783dc5f98..2fd64e139d 100644 --- a/.github/workflows/push_master.workflow.yml +++ b/.github/workflows/push_master.workflow.yml @@ -168,6 +168,7 @@ jobs: run: | echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" + echo "::set-output name=branch::$(echo $GITHUB_REF | cut -d/ -f 3)" - uses: convictional/trigger-workflow-and-wait@v1.3.0 with: owner: kuzzleio @@ -175,7 +176,7 @@ jobs: github_token: ${{ secrets.ACCESS_TOKEN_CI }} workflow_file_name: child_repo.workflow.yml ref: master - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' + inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ steps.extract-refs.outputs.branch }}", "version": "${{ steps.extract-refs.outputs.version }}"}' dockerhub-deploy: name: Build and deploy images to Dockerhub From e8715e382d240621a92ae1a1acf6f4fe6b20f3f5 Mon Sep 17 00:00:00 2001 From: Luca Marchesini Date: Tue, 26 Oct 2021 11:34:04 +0200 Subject: [PATCH 05/13] [ci] lighter workflows (no dead-link check) --- .github/workflows/push_dev.workflow.yml | 25 +--------------------- .github/workflows/push_master.workflow.yml | 25 +--------------------- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml index 405ae4c1b5..5fe87bf237 100644 --- a/.github/workflows/push_dev.workflow.yml +++ b/.github/workflows/push_dev.workflow.yml @@ -131,33 +131,10 @@ jobs: - name: Test error codes run: ./.ci/scripts/check-error-codes-documentation.sh - doc-dead-links: - name: Check dead-links - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Extract references from context - shell: bash - id: extract-refs - run: | - echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" - echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" - echo "::set-output name=fw-branch::$(if [ $BASE_BRANCH == master ]; then echo master; else echo develop; fi)" - - uses: convictional/trigger-workflow-and-wait@v1.3.0 - with: - owner: kuzzleio - repo: documentation - github_token: ${{ secrets.ACCESS_TOKEN_CI }} - workflow_file_name: dead_links.workflow.yml - ref: ${{ steps.extract-refs.outputs.fw-branch }} - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' - doc-deploy: name: Deployment Doc to Next runs-on: ubuntu-18.04 - needs: [cluster-monkey-tests, doc-dead-links] + needs: [cluster-monkey-tests] steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml index 2fd64e139d..4a5a6318ac 100644 --- a/.github/workflows/push_master.workflow.yml +++ b/.github/workflows/push_master.workflow.yml @@ -131,33 +131,10 @@ jobs: - name: Test error codes run: ./.ci/scripts/check-error-codes-documentation.sh - doc-dead-links: - name: Check dead-links - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Extract references from context - shell: bash - id: extract-refs - run: | - echo "::set-output name=version::$(git describe --abbrev=0 --tags | cut -d. -f 1)" - echo "::set-output name=repo::$(echo $GITHUB_REPOSITORY | cut -d/ -f 2)" - echo "::set-output name=fw-branch::$(if [ $BASE_BRANCH == master ]; then echo master; else echo develop; fi)" - - uses: convictional/trigger-workflow-and-wait@v1.3.0 - with: - owner: kuzzleio - repo: documentation - github_token: ${{ secrets.ACCESS_TOKEN_CI }} - workflow_file_name: dead_links.workflow.yml - ref: ${{ steps.extract-refs.outputs.fw-branch }} - inputs: '{"repo_name": "${{ steps.extract-refs.outputs.repo }}", "branch": "${{ github.head_ref }}", "version": "${{ steps.extract-refs.outputs.version }}"}' - doc-deploy: name: Deployment Doc to Prod runs-on: ubuntu-18.04 - needs: [cluster-monkey-tests, doc-dead-links] + needs: [cluster-monkey-tests] steps: - uses: actions/checkout@v2 with: From 6cda61429fe4f0dd8fd42e1f1f07f5925df3cab7 Mon Sep 17 00:00:00 2001 From: Aschen Date: Wed, 27 Oct 2021 22:55:27 +0200 Subject: [PATCH 06/13] chore(nginx): bump max ws connections --- .gitignore | 2 +- docker/nginx/kuzzle.conf | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f3250d661b..b0550868f4 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,4 @@ docker/scripts/functional-tests-controller.js docker/scripts/start-kuzzle-dev.js lib/model/security/token.js lib/core/auth/tokenManager.js -lib/core/cluster/state.js +lib/cluster/state.js diff --git a/docker/nginx/kuzzle.conf b/docker/nginx/kuzzle.conf index aa38c40217..7926ba71d8 100644 --- a/docker/nginx/kuzzle.conf +++ b/docker/nginx/kuzzle.conf @@ -7,6 +7,10 @@ upstream kuzzle { server kuzzle:7512; } +events { + worker_connections 20000; +} + server { listen 7443 ssl; From 8c026f64d616b83c2a48bda3f1e3950102da6117 Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Mon, 1 Nov 2021 14:21:02 +0300 Subject: [PATCH 07/13] Convert HotelClerk to Typescript (#2179) --- .eslintignore | 10 +- .gitignore | 8 + docker/scripts/start-kuzzle-dev.ts | 2 + lib/cluster/state.ts | 15 +- lib/core/auth/tokenManager.ts | 6 +- lib/core/network/router.js | 2 +- lib/core/realtime/channel.ts | 106 +++++ lib/core/realtime/connectionRooms.ts | 73 +++ .../realtime/{hotelClerk.js => hotelClerk.ts} | 427 +++++++----------- lib/core/realtime/index.js | 2 +- lib/core/realtime/notifier.js | 12 +- lib/core/realtime/room.ts | 125 +++++ lib/core/realtime/subscription.ts | 64 +++ lib/types/KuzzleDocument.ts | 8 + lib/types/index.ts | 4 + lib/types/realtime/RealtimeScope.ts | 4 + lib/types/realtime/RealtimeUsers.ts | 4 + lib/types/realtime/RoomList.ts | 20 + test/core/auth/tokenManager.test.js | 12 +- test/core/network/router/router.test.js | 4 +- .../realtime/hotelClerk/disconnect.test.js | 60 +-- test/core/realtime/hotelClerk/join.test.js | 4 +- test/core/realtime/hotelClerk/list.test.js | 2 +- .../hotelClerk/listCollections.test.js | 2 +- .../realtime/hotelClerk/subscribe.test.js | 25 +- .../realtime/hotelClerk/unsubscribe.test.js | 67 +-- .../realtime/notifier/notifyMethods.test.js | 81 ++-- 27 files changed, 753 insertions(+), 396 deletions(-) create mode 100644 lib/core/realtime/channel.ts create mode 100644 lib/core/realtime/connectionRooms.ts rename lib/core/realtime/{hotelClerk.js => hotelClerk.ts} (54%) create mode 100644 lib/core/realtime/room.ts create mode 100644 lib/core/realtime/subscription.ts create mode 100644 lib/types/KuzzleDocument.ts create mode 100644 lib/types/realtime/RealtimeScope.ts create mode 100644 lib/types/realtime/RealtimeUsers.ts create mode 100644 lib/types/realtime/RoomList.ts diff --git a/.eslintignore b/.eslintignore index 375c27a657..68b3d39449 100644 --- a/.eslintignore +++ b/.eslintignore @@ -65,4 +65,12 @@ lib/util/koncordeCompat.js features/support/application/functional-tests-app.js lib/model/security/token.js lib/core/auth/tokenManager.js -lib/core/cluster/state.js +lib/cluster/state.js +lib/core/realtime/hotelClerk.js +lib/types/realtime/RealtimeScope.js +lib/types/realtime/RealtimeUsers.js +lib/core/realtime/channel.js +lib/core/realtime/connectionRooms.js +lib/core/realtime/room.js +lib/core/realtime/subscription.js +lib/types/realtime/RoomList.js diff --git a/.gitignore b/.gitignore index b0550868f4..b2a2447854 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,11 @@ docker/scripts/start-kuzzle-dev.js lib/model/security/token.js lib/core/auth/tokenManager.js lib/cluster/state.js +lib/core/realtime/hotelClerk.js +lib/types/realtime/RealtimeScope.js +lib/types/realtime/RealtimeUsers.js +lib/core/realtime/channel.js +lib/core/realtime/connectionRooms.js +lib/core/realtime/room.js +lib/core/realtime/subscription.js +lib/types/realtime/RoomList.js diff --git a/docker/scripts/start-kuzzle-dev.ts b/docker/scripts/start-kuzzle-dev.ts index 784514337a..729782864a 100644 --- a/docker/scripts/start-kuzzle-dev.ts +++ b/docker/scripts/start-kuzzle-dev.ts @@ -105,6 +105,8 @@ app.hook.register('custom:event', async (name) => { }); }); +app.hook.register('core:realtime:user:unsubscribe:after', () => console.log('UNSUBSCRIBE')) + let syncedHello = 'World'; let dynamicPipeId; diff --git a/lib/cluster/state.ts b/lib/cluster/state.ts index a8d48efede..7735667f26 100644 --- a/lib/cluster/state.ts +++ b/lib/cluster/state.ts @@ -21,6 +21,8 @@ import { NormalizedFilter } from 'koncorde'; import { JSONObject } from 'kuzzle-sdk'; + +import { RoomList } from '../types'; import Long from 'long'; import kerror from '../kerror'; @@ -321,7 +323,7 @@ export default class State { } listRealtimeRooms () { - const list: RealtimeRoomsList = {}; + const list: RoomList = {}; for (const room of this.realtime.values()) { if (!list[room.index]) { @@ -425,17 +427,6 @@ export default class State { } } -export type RealtimeRoomsList = { - [index: string]: { - [collection: string]: { - /** - * Number of subscriptions per room - */ - [roomId: string]: number - } - } -}; - export type SerializedState = { authStrategies: JSONObject[]; diff --git a/lib/core/auth/tokenManager.ts b/lib/core/auth/tokenManager.ts index 511015eebb..bae73ff59e 100644 --- a/lib/core/auth/tokenManager.ts +++ b/lib/core/auth/tokenManager.ts @@ -37,7 +37,7 @@ interface ISortedArray { */ class ManagedToken extends Token { /** - * Unique string to identify the token and sort it by expiration date + * Unique string to identify the token and sort it by expiration date */ idx: string; @@ -72,7 +72,7 @@ const TIMEOUT_MAX = Math.pow(2, 31) - 1; /** * Maintains a list of valid tokens used by connected protocols. - * + * * When a token expires, this module cleans up the corresponding connection's * subscriptions if any, and notify the user */ @@ -212,7 +212,7 @@ export class TokenManager { for (const connectionId of managedToken.connectionIds) { this.tokensByConnection.delete(connectionId); - await global.kuzzle.ask('core:realtime:user:remove', connectionId); + await global.kuzzle.ask('core:realtime:connection:remove', connectionId); } this.deleteByIndex(searchResult); diff --git a/lib/core/network/router.js b/lib/core/network/router.js index aceca0521e..4dddf37e43 100644 --- a/lib/core/network/router.js +++ b/lib/core/network/router.js @@ -83,7 +83,7 @@ class Router { this.connections.delete(connId); global.kuzzle - .ask('core:realtime:user:remove', requestContext.connection.id) + .ask('core:realtime:connection:remove', requestContext.connection.id) .catch(err => global.kuzzle.log.info(err)); global.kuzzle.statistics.dropConnection(requestContext); diff --git a/lib/core/realtime/channel.ts b/lib/core/realtime/channel.ts new file mode 100644 index 0000000000..73a7592354 --- /dev/null +++ b/lib/core/realtime/channel.ts @@ -0,0 +1,106 @@ +/* + * Kuzzle, a backend software, self-hostable and ready to use + * to power modern apps + * + * Copyright 2015-2020 Kuzzle + * mailto: support AT kuzzle.io + * website: http://kuzzle.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RealtimeScope, RealtimeUsers } from '../../types'; +import kerror from '../../kerror'; + +const realtimeError = kerror.wrap('core', 'realtime'); + +/** + * A channel define how notifications should be send for a particular realtime + * room. + * + * Channel names are sent back to users who subscribe to realtime notification, + * then each notification sent to them contains the associated channel. + * + * It allows to makes two subscriptions on index + collection + filters but one + * for documents entering the scope and the other for documents exiting the scope + * for example. + * + * Channels define with more granularity if a room notification should be sent: + * - is the document entering or leaving the scope + * - should I notify when users join or leave the room + * - should I propagate the notification to other cluster nodes + * + * @property name + * @property scope + * @property users + * @property cluster + */ +export class Channel { + static USERS_ALLOWED_VALUES = ['all', 'in', 'out', 'none']; + + static SCOPE_ALLOWED_VALUES = Channel.USERS_ALLOWED_VALUES; + + /** + * Identifier of the channel. + * + * Result of roomId + hash(channel) + */ + public name: string; + + /** + * Define if notification should be send when documents are entering or leaving + * the subscription scope. + * + * @default "all" + */ + public scope: RealtimeScope; + + /** + * Define if notification should be send when users join or leave the channel. + * + * @default "none" + */ + public users: RealtimeUsers; + + /** + * Define if the notification should be propagated on other cluster nodes or + * only to connection subscribing on this node. + * + * This is used by the EmbeddedSDK realtime subscription to propagate or not + * callback execution among other nodes. + */ + public cluster: boolean; + + constructor ( + roomId: string, + { + scope = 'all', + users = 'none', + propagate = true, + }: { scope?: RealtimeScope, users?: RealtimeUsers, propagate?: boolean } = {} + ) { + this.scope = scope; + this.users = users; + this.cluster = propagate; + + if (! Channel.SCOPE_ALLOWED_VALUES.includes(this.scope)) { + throw realtimeError.get('invalid_scope'); + } + + if (! Channel.USERS_ALLOWED_VALUES.includes(this.users)) { + throw realtimeError.get('invalid_users'); + } + + this.name = `${roomId}-${global.kuzzle.hash(this)}`; + } +} diff --git a/lib/core/realtime/connectionRooms.ts b/lib/core/realtime/connectionRooms.ts new file mode 100644 index 0000000000..0a29a127ec --- /dev/null +++ b/lib/core/realtime/connectionRooms.ts @@ -0,0 +1,73 @@ +/* + * Kuzzle, a backend software, self-hostable and ready to use + * to power modern apps + * + * Copyright 2015-2020 Kuzzle + * mailto: support AT kuzzle.io + * website: http://kuzzle.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSONObject } from 'kuzzle-sdk'; + +/** + * Each connection can subscribe to many rooms with different sets of volatile + * data. + * + * This object is responsible to keep the association between each rooms and the + * associated volatile data. + * + * @property rooms + */ +export class ConnectionRooms { + /** + * List of subscribed rooms and their associated volatile data + * + * Map + */ + private rooms = new Map(); + + constructor (rooms?: Map) { + if (rooms) { + this.rooms = rooms; + } + } + + get roomIds (): string[] { + return Array.from(this.rooms.keys()); + } + + get count (): number { + return this.rooms.size; + } + + addRoom (roomId: string, volatile: JSONObject) { + this.rooms.set(roomId, volatile); + } + + removeRoom (roomId: string) { + this.rooms.delete(roomId); + } + + hasRoom (roomId: string) { + return this.rooms.has(roomId); + } + + /** + * Returns the volatile data for this room subscription + */ + getVolatile (roomId: string) { + return this.rooms.get(roomId); + } +} \ No newline at end of file diff --git a/lib/core/realtime/hotelClerk.js b/lib/core/realtime/hotelClerk.ts similarity index 54% rename from lib/core/realtime/hotelClerk.js rename to lib/core/realtime/hotelClerk.ts index 5dc0d55c32..d95bdd131a 100644 --- a/lib/core/realtime/hotelClerk.js +++ b/lib/core/realtime/hotelClerk.ts @@ -19,113 +19,89 @@ * limitations under the License. */ -'use strict'; - -const Bluebird = require('bluebird'); - -const { Request, RequestContext } = require('../../api/request'); -const kerror = require('../../kerror'); -const debug = require('../../util/debug')('kuzzle:realtime:hotelClerk'); -const { +import Bluebird from 'bluebird'; +import { JSONObject } from 'kuzzle-sdk'; +import { Koncorde, NormalizedFilter } from 'koncorde'; + +import { KuzzleRequest, Request, RequestContext } from '../../api/request'; +import kerror from '../../kerror'; +import createDebug from '../../util/debug'; +import { + fromKoncordeIndex, getCollections, toKoncordeIndex, -} = require('../../util/koncordeCompat'); +} from '../../util/koncordeCompat'; +import { User, RoomList } from '../../types'; +import { Channel } from './channel'; +import { ConnectionRooms } from './connectionRooms'; +import { Room } from './room'; +import { Subscription } from './subscription'; const realtimeError = kerror.wrap('core', 'realtime'); -const CHANNEL_ALLOWED_VALUES = ['all', 'in', 'out', 'none']; +const debug = createDebug('kuzzle:realtime:hotelClerk'); -class Channel { - constructor (roomId, { scope='all', users='none', propagate=true } = {}) { - this.scope = scope; - this.users = users; - this.cluster = propagate; +/** + * The HotelClerk is responsible of keeping the list of rooms and subscriptions + * made to those rooms. + * + * When a subscription is made to a room, the HotelClerk link the connection + * to a channel of this room. Each channel represents a specific configuration + * about which kind of notification the subscriber should receive (e.g. scope in/out) + * + * When an user is subscribing, we send him back the channel he is subscribing to. + * + * Here stop the role of the HotelClerk, then the notifier will select the channels + * according to the notification and notify them. + */ +export class HotelClerk { + private module: any; - if (! CHANNEL_ALLOWED_VALUES.includes(this.scope)) { - throw realtimeError.get('invalid_scope'); - } + /** + * Number of created rooms. + * + * Used with the "subscriptionRooms" configuration limit. + */ + private roomsCount = 0; - if (! CHANNEL_ALLOWED_VALUES.includes(this.users)) { - throw realtimeError.get('invalid_users'); - } + /** + * Current realtime rooms. + * + * This object is used by the notifier to list wich channel has to be notified + * when a subscription scope is matching. + * It's also used to notify channels when an user join/exit a room. + * + * Map + */ + private rooms = new Map(); - this.name = `${roomId}-${global.kuzzle.hash(this)}`; - } -} + /** + * Current subscribing connections handled by the HotelClerk. + * + * Each connection can subscribe to many rooms with different volatile data. + * + * This object is used to keep track of all subscriptions made by a connection + * to be able to unsubscribe when a connection is removed. + * + * Map + */ + private subscriptions = new Map(); -class Subscription { - constructor (index, collection, filters, roomId, connectionId, user) { - this.index = index; - this.collection = collection; - this.filters = filters; - this.roomId = roomId; - this.connectionId = connectionId; - this.kuid = user && user._id || null; - } -} + /** + * Shortcut to the Koncorde instance on the global object. + */ + private koncorde: Koncorde; -class HotelClerk { - constructor (realtimeModule) { + constructor (realtimeModule: any) { this.module = realtimeModule; - /** - * Number of created rooms. Used with the "subscriptionRooms" - * configuration limit - */ - this.roomsCount = 0; - - /** - * A simple list of rooms, containing their associated filter and how many - * users have subscribed to it - * - * Example: subscribing to a chat room where the subject is Kuzzle - * rooms = Map - * - * Where: - * - the room ID is the filter ID (e.g. 'f45de4d8ef4f3ze4ffzer85d4fgkzm41') - * - room is an object with the following properties: - * { - * // list of users subscribing to this room - * customers: Set([ 'connectionId' ]), - * - * // room channels - * channels: { - * - * // channel ID - * 'roomId-': { - * - * // request scope filter, default: 'all' - * scope: 'all|in|out|none', - * - * // filter users notifications, default: 'none' - * users: 'all|in|out|none', - * - * // should propagate notification to the cluster - * // (used for plugin subscriptions) - * cluster: true|false - * } - * }, - * index: 'index', - * collection: 'collection', - * // the room unique identifier - * id: 'id', - * } - * } - */ - this.rooms = new Map(); - - /** - * In addition to this.rooms, this.customers allows managing users and their rooms - * Example for a customer who subscribes to the room 'chat-room-kuzzle' - * customers = Map. - * - * Where a customer room is an object with the following properties: - * Map.> - */ - this.customers = new Map(); + this.koncorde = global.kuzzle.koncorde; } - async init () { + /** + * Registers the ask events. + */ + async init (): Promise { /** * Create a new, empty room. * @param {string} index @@ -171,8 +147,8 @@ class HotelClerk { * @param {string} connectionId */ global.kuzzle.onAsk( - 'core:realtime:user:remove', - connectionId => this.removeUser(connectionId)); + 'core:realtime:connection:remove', + connectionId => this.removeConnection(connectionId)); /** * Adds a new user subscription @@ -198,18 +174,18 @@ class HotelClerk { } /** - * Link a user connection to a room. - * Create a new room if one doesn't already exist. + * Subscribe a connection to a realtime room. + * + * The room will be created if it does not already exists. + * * Notify other subscribers on this room about this new subscription * - * @param {Request} request - * @return {Promise.} * @throws Throws if the user has already subscribed to this room name * (just for rooms with same name, there is no error if the room * has a different name with same filter) or if there is an error * during room creation */ - async subscribe (request) { + async subscribe (request: KuzzleRequest): Promise<{ channel: string, roomId: string }> { const { index, collection } = request.input.resource; if (! index) { @@ -235,10 +211,10 @@ class HotelClerk { return null; } - let normalized; + let normalized: NormalizedFilter; try { - normalized = global.kuzzle.koncorde.normalize( + normalized = this.koncorde.normalize( request.input.body, toKoncordeIndex(index, collection)); } @@ -246,9 +222,9 @@ class HotelClerk { throw kerror.get('api', 'assert', 'koncorde_dsl_error', e.message); } - this._createRoom(normalized); + this.createRoom(normalized); - const { channel, subscribed } = await this._subscribeToRoom( + const { channel, subscribed } = await this.subscribeToRoom( normalized.id, request); @@ -256,7 +232,7 @@ class HotelClerk { global.kuzzle.emit('core:realtime:subscribe:after', normalized.id); // @deprecated -- to be removed in next major version - // we have to recreate the old "diff" object -_- + // we have to recreate the old "diff" object await global.kuzzle.pipe('core:hotelClerk:addSubscription', { changed: subscribed, collection, @@ -284,152 +260,126 @@ class HotelClerk { } /** - * Given an index, returns an array of collections on which some filters are - * registered - * @param {string} index - * @returns {Array.} + * Returns the list of collections of an index with realtime rooms. */ - listCollections (index) { - return getCollections(global.kuzzle.koncorde, index); + listCollections (index: string): string[] { + return getCollections(this.koncorde, index); } /** - * Joins an existing room. + * Joins an existing realtime room. * - * @param {Request} request - * @returns {Promise.} + * The room may exists on another cluster node, if it's the case, the normalized + * filters will be fetched from the cluster. */ - async join (request) { + async join (request: KuzzleRequest): Promise<{ roomId, channel }> { const roomId = request.input.body.roomId; if (! this.rooms.has(roomId)) { - const normalized = await global.kuzzle.ask( + const normalized: NormalizedFilter = await global.kuzzle.ask( 'cluster:realtime:filters:get', roomId); - if (!normalized) { + if (! normalized) { throw realtimeError.get('room_not_found', roomId); } - this._createRoom(normalized); + this.createRoom(normalized); } - const response = await this._subscribeToRoom(roomId, request); + const { channel, cluster, subscribed } = await this.subscribeToRoom(roomId, request); - if (response.cluster && response.subscribed) { + if (cluster && subscribed) { global.kuzzle.emit('core:realtime:subscribe:after', roomId); } return { - channel: response.channel, + channel, roomId, }; } /** - * Return the list of index, collection, rooms (+ their number of subscribers) + * Return the list of index, collection, rooms and subscribing connections * on all index/collection pairs that the requesting user is allowed to - * subscribe - * - * Returned object looks like this: - * { - * : { - * : { - * : - * } - * } - * } - * - * @param {User} user - * @returns {Promise.} resolve an object listing all rooms subscribed - * by the connected user + * subscribe. */ - async list (user) { + async list (user: User): Promise { // We need the room list from the cluster's full state, NOT the one stored // in Koncorde: the latter also contains subscriptions created by the // framework (or by plugins), and we don't want those to appear in the API - const list = await global.kuzzle.ask('cluster:realtime:room:list'); + const fullStateRooms: RoomList = await global.kuzzle.ask('cluster:realtime:room:list'); - const isAllowedRequest = new Request({ + const isAllowedRequest = new KuzzleRequest({ action: 'subscribe', controller: 'realtime', - }); + }, {}); - for (const index of Object.keys(list)) { + for (const [index, collections] of Object.entries(fullStateRooms)) { isAllowedRequest.input.resource.index = index; const toRemove = await Bluebird.filter( - Object.keys(list[index]), + Object.keys(collections), collection => { isAllowedRequest.input.resource.collection = collection; - return !user.isActionAllowed(isAllowedRequest); + return ! user.isActionAllowed(isAllowedRequest); }); for (const collection of toRemove) { - delete list[index][collection]; + delete fullStateRooms[index][collection]; } } - return list; + return fullStateRooms; } /** - * This function will delete a user from this.customers, and - * decrement the subscribers count in all rooms where he has subscribed to - * Usually called on a user disconnection event + * Removes a connections and unsubscribe it from every subscribed rooms. * - * @param {string} connectionId + * Usually called when an user has been disconnected from Kuzzle. */ - async removeUser (connectionId) { - const customer = this.customers.get(connectionId); + async removeConnection (connectionId: string): Promise { + const connectionRooms = this.subscriptions.get(connectionId); - if (!customer) { - // No need to raise an error if the connection has already been cleaned up + if (! connectionRooms) { + // No need to raise an error if the connection does not have room subscriptions return; } - await Bluebird.map(customer.keys(), roomId => { - return this.unsubscribe(connectionId, roomId) - .catch(err => global.kuzzle.log.error(err)); - }); + await Bluebird.map(connectionRooms.roomIds, (roomId: string) => ( + this.unsubscribe(connectionId, roomId).catch(global.kuzzle.log.error) + )); } /** - * Associate the room to the connection id in this.clients - * Allow to manage later disconnection and delete socket/rooms/... - * - * @param {string} connectionId - * @param {string} roomId - * @param {object} volatile + * Register a new subscription + * - save the subscription on the provided room with volatile data + * - add the connection to the list of active connections of the room */ - _addRoomForCustomer (connectionId, roomId, volatile) { - debug('Add room %s for customer %s', roomId, connectionId); + private registerSubscription (connectionId: string, roomId: string, volatile: JSONObject): void { + debug('Add room %s for connection %s', roomId, connectionId); - let customer = this.customers.get(connectionId); + let connectionRooms = this.subscriptions.get(connectionId); - if (! customer) { - customer = new Map(); - this.customers.set(connectionId, customer); + if (! connectionRooms) { + connectionRooms = new ConnectionRooms(); + this.subscriptions.set(connectionId, connectionRooms); } - this.rooms.get(roomId).customers.add(connectionId); - customer.set(roomId, volatile); + connectionRooms.addRoom(roomId, volatile); + + this.rooms.get(roomId).addConnection(connectionId); } /** * Create new room if needed * - * @this HotelClerk - * - * @param {NormalizedFilter} normalized - Obtained with Koncorde.normalize - * @param {Object} [options] - * * @returns {void} */ - _createRoom (normalized) { + private createRoom (normalized: NormalizedFilter): void { const { index: koncordeIndex, id: roomId } = normalized; - const [index, collection] = koncordeIndex.split('/'); + const { index, collection } = fromKoncordeIndex(koncordeIndex); if (this.rooms.has(normalized.id)) { return; @@ -437,11 +387,11 @@ class HotelClerk { const roomsLimit = global.kuzzle.config.limits.subscriptionRooms; - if ( roomsLimit > 0 && this.roomsCount >= roomsLimit ) { + if (roomsLimit > 0 && this.roomsCount >= roomsLimit) { throw realtimeError.get('too_many_rooms'); } - global.kuzzle.koncorde.store(normalized); + this.koncorde.store(normalized); global.kuzzle.emit('core:realtime:room:create:after', normalized); @@ -462,42 +412,38 @@ class HotelClerk { } /** - * Remove the room from subscribed room from the user - * Return the roomId in user mapping + * Remove a connection from a room. + * + * Also delete the rooms if it was the last connection subscribing to it. * - * @this HotelClerk - * @param {string} connectionId - * @param {string} roomId - * @param {Boolean} [notify] - * @returns {Promise} */ - async unsubscribe (connectionId, roomId, notify = true) { - const customer = this.customers.get(connectionId); + async unsubscribe (connectionId: string, roomId: string, notify = true) { + const connectionRooms = this.subscriptions.get(connectionId); const requestContext = new RequestContext({ connection: { id: connectionId } }); - if (! customer) { + if (! connectionRooms) { throw realtimeError.get('not_subscribed', connectionId, roomId); } - const volatile = customer.get(roomId); + const volatile = connectionRooms.getVolatile(roomId); if (volatile === undefined) { throw realtimeError.get('not_subscribed', connectionId, roomId); } - if (customer.size > 1) { - customer.delete(roomId); + if (connectionRooms.count > 1) { + connectionRooms.removeRoom(roomId); } else { - this.customers.delete(connectionId); + this.subscriptions.delete(connectionId); } const room = this.rooms.get(roomId); if (! room) { - global.kuzzle.log.error(`[hotelClerk] Cannot remove room "${roomId}": room not found`); + global.kuzzle.log.error(`Cannot remove room "${roomId}": room not found`); throw realtimeError.get('room_not_found', roomId); } @@ -505,16 +451,10 @@ class HotelClerk { global.kuzzle.entryPoint.leaveChannel(channel, connectionId); } - if (room.customers.size === 1) { - this.roomsCount--; - this.rooms.delete(roomId); - - await this._removeRoomFromRealtimeEngine(roomId); + room.removeConnection(connectionId); - room.customers = new Set(); - } - else { - room.customers.delete(connectionId); + if (room.size === 0) { + await this.removeRoom(roomId); } // even if the room is deleted for this node, another one may need the @@ -529,14 +469,13 @@ class HotelClerk { }, requestContext); - await this.module.notifier.notifyUser(roomId, request, 'out', { - count: room.customers.size - }); + await this.module.notifier.notifyUser(roomId, request, 'out', { count: room.size }); // Do not send an unsubscription notification if the room has been destroyed + // @aschen Why ? if ( notify && this.rooms.has(roomId) - && Object.keys(room.channels).length > 0 + && room.channels.size > 0 ) { await global.kuzzle.pipe('core:realtime:unsubscribe:after', roomId); @@ -577,10 +516,11 @@ class HotelClerk { /** * Deletes a room if no user has subscribed to it, and removes it also from the * real-time engine - * - * @param {string} roomId */ - async _removeRoomFromRealtimeEngine (roomId) { + private async removeRoom (roomId: string): Promise { + this.roomsCount--; + this.rooms.delete(roomId); + // @deprecated -- to be removed in the next major version try { await global.kuzzle.pipe('room:remove', roomId); @@ -597,29 +537,35 @@ class HotelClerk { } /** - * Subscribes a user to an existing room. + * Subscribes a connection to an existing room. + * + * The subscription is made on a configuration channel who will be created + * on the room if it does not already exists. * - * @param {string} roomId - * @param {Request} request - * @returns {Promise.} */ - async _subscribeToRoom (roomId, request) { + private async subscribeToRoom ( + roomId: string, + request: KuzzleRequest + ): Promise<{ channel: string, cluster: boolean, subscribed: boolean }> { let subscribed = false; let notifyPromise; - const channel = new Channel(roomId, request.input.args); + + const { scope, users, propagate } = request.input.args; const connectionId = request.context.connection.id; - const customer = this.customers.get(connectionId); + + const channel = new Channel(roomId, { propagate, scope, users }); + const connectionRooms = this.subscriptions.get(connectionId); const room = this.rooms.get(roomId); - if ( !customer || !customer.has(roomId)) { + if (! connectionRooms || ! connectionRooms.hasRoom(roomId)) { subscribed = true; - this._addRoomForCustomer(connectionId, roomId, request.input.volatile); + this.registerSubscription(connectionId, roomId, request.input.volatile); notifyPromise = this.module.notifier.notifyUser( roomId, request, 'in', - { count: room.customers.size }); + { count: room.size }); } else { notifyPromise = Bluebird.resolve(); @@ -627,9 +573,7 @@ class HotelClerk { global.kuzzle.entryPoint.joinChannel(channel.name, connectionId); - if (! room.channels[channel.name]) { - room.channels[channel.name] = channel; - } + room.createChannel(channel); await notifyPromise; @@ -641,36 +585,13 @@ class HotelClerk { } /** - * Return the rooms a user has subscribed to. - * @param {connectionId} connectionId - * @returns {Array.} - */ - getUserRooms (connectionId) { - const rooms = this.customers.get(connectionId); - - if (rooms) { - return Array.from(rooms.keys()); - } - - return []; - } - - /** - * Create an empty room in the RAM cache - * @param {string} index - * @param {string } collection - * @param {string} roomId - * @returns {boolean} + * Create an empty room in the RAM cache if it doesn't exists + * + * @returns True if a new room has been created */ - newRoom (index, collection, roomId) { - if (!this.rooms.has(roomId)) { - this.rooms.set(roomId, { - channels: {}, - collection, - customers: new Set(), - id: roomId, - index, - }); + private newRoom (index: string, collection: string, roomId: string): boolean { + if (! this.rooms.has(roomId)) { + this.rooms.set(roomId, new Room(roomId, index, collection)); return true; } @@ -678,5 +599,3 @@ class HotelClerk { return false; } } - -module.exports = HotelClerk; diff --git a/lib/core/realtime/index.js b/lib/core/realtime/index.js index 0e4ae5b4f2..02c24d10ce 100644 --- a/lib/core/realtime/index.js +++ b/lib/core/realtime/index.js @@ -22,7 +22,7 @@ 'use strict'; const Notifier = require('./notifier'); -const HotelClerk = require('./hotelClerk'); +const { HotelClerk } = require('./hotelClerk'); class RealtimeModule { constructor () { diff --git a/lib/core/realtime/notifier.js b/lib/core/realtime/notifier.js index bf59e48a8f..4ddfb45159 100644 --- a/lib/core/realtime/notifier.js +++ b/lib/core/realtime/notifier.js @@ -185,14 +185,14 @@ class NotifierController { * @returns {Promise} */ async notifyTokenExpired (connectionId) { - + await this._dispatch( 'notify:server', [KUZZLE_NOTIFICATION_CHANNEL], // Sending notification on Kuzzle notification channel new ServerNotification('TokenExpired', 'Authentication Token Expired'), connectionId); - - await this.module.hotelClerk.removeUser(connectionId); + + await this.module.hotelClerk.removeConnection(connectionId); } /** @@ -426,10 +426,10 @@ class NotifierController { continue; } - for (const [channelId, channel] of Object.entries(hotelClerkRoom.channels)) { + for (const [channelId, channel] of hotelClerkRoom.channels.entries()) { + const executeOnNode = fromCluster ? channel.cluster : true; const matchScope = channel.scope === 'all' || channel.scope === notification.scope; - const executeOnNode = fromCluster ? channel.cluster : true; if (matchScope && executeOnNode) { channels.push(channelId); @@ -459,7 +459,7 @@ class NotifierController { const hotelClerkRoom = this.module.hotelClerk.rooms.get(room); if (hotelClerkRoom !== undefined) { - for (const [id, channel] of Object.entries(hotelClerkRoom.channels)) { + for (const [id, channel] of hotelClerkRoom.channels.entries()) { const match = channel.users === 'all' || channel.users === notification.user; const executeOnNode = fromCluster ? channel.cluster : true; diff --git a/lib/core/realtime/room.ts b/lib/core/realtime/room.ts new file mode 100644 index 0000000000..58cbaf1d48 --- /dev/null +++ b/lib/core/realtime/room.ts @@ -0,0 +1,125 @@ +/* + * Kuzzle, a backend software, self-hostable and ready to use + * to power modern apps + * + * Copyright 2015-2020 Kuzzle + * mailto: support AT kuzzle.io + * website: http://kuzzle.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Channel } from './channel'; + +/** + * A room represents a subscription scope made on a combination of: + * - index + * - collection + * - filters + * + * A room may contain differents channels that describe which notifications should + * be sent. (e.g. only document leaving the scope, users joining the room) + * + * The rooms also contains the list of connections who subscribed to it. + * + * @property id + * @property index + * @property collection + * @property connections + * @property channels + */ +export class Room { + /** + * Room unique identifier. + * + * Koncorde hash for the desired scope (index + collection + filters) + */ + public id: string; + public index: string; + public collection: string; + + /** + * List of connections subscribing to this room. + */ + private connections = new Set(); + + /** + * Map of channels configuration for this room. + * + * Map + * + * @example + * + * channels: { + * '': { + * + * // request scope filter, default: 'all' + * scope: 'all|in|out|none', + * + * // filter users notifications, default: 'none' + * users: 'all|in|out|none', + * + * // should propagate notification to the cluster + * // (used for plugin subscriptions) + * cluster: true|false + * } + * }, + */ + public channels = new Map(); + + constructor ( + id: string, + index: string, + collection: string, + channels?: Map, + connections?: Set + ) { + this.id = id; + this.index = index; + this.collection = collection; + + if (channels) { + this.channels = channels; + } + + if (connections) { + this.connections = connections; + } + } + + /** + * Number of connections subscribing to the room + */ + get size (): number { + return this.connections.size; + } + + /** + * Creates a new configuration channel on the room if it doesn't already exists + */ + createChannel (channel: Channel): void { + if (this.channels.has(channel.name)) { + return; + } + + this.channels.set(channel.name, channel); + } + + addConnection (connectionId: string): void { + this.connections.add(connectionId); + } + + removeConnection (connectionId: string): void { + this.connections.delete(connectionId); + } +} diff --git a/lib/core/realtime/subscription.ts b/lib/core/realtime/subscription.ts new file mode 100644 index 0000000000..832f1f36d7 --- /dev/null +++ b/lib/core/realtime/subscription.ts @@ -0,0 +1,64 @@ +/* + * Kuzzle, a backend software, self-hostable and ready to use + * to power modern apps + * + * Copyright 2015-2020 Kuzzle + * mailto: support AT kuzzle.io + * website: http://kuzzle.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSONObject } from 'kuzzle-sdk'; + +/** + * Represents a realtime subscription of a connection to a room. + * + * This object is used in the Internal Event System to give informations about + * added/removed subscriptions. + * + * @property connectionId + * @property roomId + * @property index + * @property collection + * @property filters + * @property kuid + */ +export class Subscription { + public connectionId: string; + public roomId: string; + + public index: string; + public collection: string; + public filters: JSONObject; + + public kuid: string; + + constructor ( + index: string, + collection: string, + filters: JSONObject, + roomId: string, + connectionId: string, + user: { _id: string }, + ) { + this.connectionId = connectionId; + this.roomId = roomId; + + this.index = index; + this.collection = collection; + this.filters = filters; + + this.kuid = user && user._id || null; + } +} diff --git a/lib/types/KuzzleDocument.ts b/lib/types/KuzzleDocument.ts new file mode 100644 index 0000000000..2838034d97 --- /dev/null +++ b/lib/types/KuzzleDocument.ts @@ -0,0 +1,8 @@ +import { JSONObject } from 'kuzzle-sdk'; + +// Should be in the SDK instead +export interface KuzzleDocument { + _id: string; + + _source: JSONObject; +} diff --git a/lib/types/index.ts b/lib/types/index.ts index c63609d9f2..d8f0b2b019 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -29,3 +29,7 @@ export * from './EventHandler'; export * from './User'; export * from './Token'; export * from './Global'; +export * from './realtime/RealtimeScope'; +export * from './realtime/RealtimeUsers'; +export * from './realtime/RoomList'; +export * from './KuzzleDocument'; diff --git a/lib/types/realtime/RealtimeScope.ts b/lib/types/realtime/RealtimeScope.ts new file mode 100644 index 0000000000..917fa33bba --- /dev/null +++ b/lib/types/realtime/RealtimeScope.ts @@ -0,0 +1,4 @@ +/** + * Subscribe to notification of document entering, leaving the scope or both. + */ +export type RealtimeScope = 'in' | 'out' | 'all'; diff --git a/lib/types/realtime/RealtimeUsers.ts b/lib/types/realtime/RealtimeUsers.ts new file mode 100644 index 0000000000..af1c1d622f --- /dev/null +++ b/lib/types/realtime/RealtimeUsers.ts @@ -0,0 +1,4 @@ +/** + * Subscribe to users entering or leaving the room + */ +export type RealtimeUsers= 'in' | 'out' | 'all' | 'none'; diff --git a/lib/types/realtime/RoomList.ts b/lib/types/realtime/RoomList.ts new file mode 100644 index 0000000000..4419136c44 --- /dev/null +++ b/lib/types/realtime/RoomList.ts @@ -0,0 +1,20 @@ +/** + * List of realtime rooms and the number of connections subscribing + * + * @example + * { + * : { + * : { + * : + * } + * } + * } + * + */ +export type RoomList = { + [index: string]: { + [collection: string]: { + [roomId: string]: number + } + } +}; diff --git a/test/core/auth/tokenManager.test.js b/test/core/auth/tokenManager.test.js index e07097dbe7..2931049238 100644 --- a/test/core/auth/tokenManager.test.js +++ b/test/core/auth/tokenManager.test.js @@ -76,8 +76,8 @@ describe('Test: token manager core component', () => { }); it('should add the connection ID to the list if an entry already exists for this token', () => { - - + + tokenManager.link(token, 'foo'); tokenManager.link(token, 'bar'); @@ -139,7 +139,7 @@ describe('Test: token manager core component', () => { userId: tokenAfter.userId, }]) .and.have.length(2); - + should(tokenManager.tokensByConnection.get('foo2')).match({ idx: `${tokenAfter.expiresAt};${tokenAfter._id}`, connectionIds: new Set(['foo2']), @@ -202,9 +202,9 @@ describe('Test: token manager core component', () => { await tokenManager.expire(token); should(tokenManager.tokens.array).be.an.Array().and.be.empty(); should(kuzzle.ask) - .calledWith('core:realtime:user:remove', 'foo') - .and.calledWith('core:realtime:user:remove', 'bar'); - + .calledWith('core:realtime:connection:remove', 'foo') + .and.calledWith('core:realtime:connection:remove', 'bar'); + should(tokenManager.tokensByConnection.size).equal(0); }); diff --git a/test/core/network/router/router.test.js b/test/core/network/router/router.test.js index c9c7d94cd7..76561d4ca9 100644 --- a/test/core/network/router/router.test.js +++ b/test/core/network/router/router.test.js @@ -67,7 +67,7 @@ describe('Test: router', () => { beforeEach(() => { realtimeDisconnectStub = kuzzle.ask - .withArgs('core:realtime:user:remove') + .withArgs('core:realtime:connection:remove') .resolves(); }); @@ -75,7 +75,7 @@ describe('Test: router', () => { router.connections.set(connectionId, requestContext); router.removeConnection(requestContext); - should(kuzzle.ask).calledWith('core:realtime:user:remove', connectionId); + should(kuzzle.ask).calledWith('core:realtime:connection:remove', connectionId); should(kuzzle.statistics.dropConnection) .calledOnce() .calledWith(requestContext); diff --git a/test/core/realtime/hotelClerk/disconnect.test.js b/test/core/realtime/hotelClerk/disconnect.test.js index d20f512b42..dfff77f4c4 100644 --- a/test/core/realtime/hotelClerk/disconnect.test.js +++ b/test/core/realtime/hotelClerk/disconnect.test.js @@ -5,7 +5,10 @@ const sinon = require('sinon'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); +const { ConnectionRooms } = require('../../../../lib/core/realtime/connectionRooms'); +const { Room } = require('../../../../lib/core/realtime/room'); +const { Channel } = require('../../../../lib/core/realtime/channel'); describe('Test: hotelClerk.removeUser', () => { const connectionId = 'connectionid'; @@ -26,62 +29,61 @@ describe('Test: hotelClerk.removeUser', () => { hotelClerk = new HotelClerk(realtimeModule); - hotelClerk.customers.set(connectionId, new Map([ + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms(new Map([ [ 'foo', { volatile: 'room foo' } ], [ 'bar', { volatile: 'room bar' } ] - ])); - hotelClerk.customers.set('a', new Map([['foo', null]])); - hotelClerk.customers.set('b', new Map([['foo', null]])); + ]))); - hotelClerk.rooms.set('foo', { - customers: new Set([connectionId, 'a', 'b']), + hotelClerk.subscriptions.set('a', new ConnectionRooms(new Map([['foo', null]]))); + hotelClerk.subscriptions.set('b', new ConnectionRooms(new Map([['foo', null]]))); + + hotelClerk.rooms.set('foo', new Room( + 'foo', index, collection, - channels: ['foobar'] - }); - hotelClerk.rooms.set('bar', { - customers: new Set([connectionId]), + new Map([['foobar', new Channel('foo')]]), + new Set([connectionId, 'a', 'b']), + )); + hotelClerk.rooms.set('bar', new Room( + 'bar', index, collection, - channels: ['barfoo'] - }); + new Map([['barfoo', new Channel('bar')]]), + new Set([connectionId]), + )); hotelClerk.roomsCount = 2; return hotelClerk.init(); }); - it('should register a "user:remove" event', async () => { - sinon.stub(hotelClerk, 'removeUser'); + it('should register a "connection:remove" event', async () => { + sinon.stub(hotelClerk, 'removeConnection'); kuzzle.ask.restore(); - await kuzzle.ask('core:realtime:user:remove', 'connectionId'); + await kuzzle.ask('core:realtime:connection:remove', 'connectionId'); - should(hotelClerk.removeUser).calledWith('connectionId'); + should(hotelClerk.removeConnection).calledWith('connectionId'); }); it('should do nothing when a bad connectionId is given', async () => { sinon.stub(hotelClerk, 'unsubscribe'); - await hotelClerk.removeUser('nope'); + await hotelClerk.removeConnection('nope'); should(hotelClerk.unsubscribe).not.be.called(); should(hotelClerk.roomsCount).be.eql(2); }); - it('should clean up customers, rooms object', async () => { - await hotelClerk.removeUser(connectionId); + it('should clean up subscriptions, rooms object', async () => { + await hotelClerk.removeConnection(connectionId); - should(hotelClerk.rooms).have.value('foo', { - customers: new Set(['a', 'b']), - index, - collection, - channels: ['foobar'] - }); + should(hotelClerk.rooms).have.key('foo'); should(hotelClerk.rooms).not.have.key('bar'); - should(hotelClerk.customers.get('a')).have.value('foo', null); - should(hotelClerk.customers.get('b')).have.value('foo', null); + should(hotelClerk.subscriptions).have.key('a'); + should(hotelClerk.subscriptions).have.key('b'); + should(hotelClerk.subscriptions).not.have.key(connectionId); should(hotelClerk.roomsCount).be.eql(1); should(realtimeModule.notifier.notifyUser).calledWithMatch( @@ -115,7 +117,7 @@ describe('Test: hotelClerk.removeUser', () => { const error = new Error('Mocked error'); realtimeModule.notifier.notifyUser.throws(error); - await hotelClerk.removeUser(connectionId); + await hotelClerk.removeConnection(connectionId); should(kuzzle.log.error).be.calledWith(error); }); diff --git a/test/core/realtime/hotelClerk/join.test.js b/test/core/realtime/hotelClerk/join.test.js index 8e2df2c3b0..3f5015f530 100644 --- a/test/core/realtime/hotelClerk/join.test.js +++ b/test/core/realtime/hotelClerk/join.test.js @@ -6,7 +6,7 @@ const sinon = require('sinon'); const { NotFoundError, Request } = require('../../../../index'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); describe('Test: hotelClerk.join', () => { const connectionId = 'connectionid'; @@ -121,7 +121,7 @@ describe('Test: hotelClerk.join', () => { }, context); const response = { cluster: false, channel: 'foobar', subscribed: true }; hotelClerk.rooms.set('i-exist', {}); - sinon.stub(hotelClerk, '_subscribeToRoom').resolves(response); + sinon.stub(hotelClerk, 'subscribeToRoom').resolves(response); await hotelClerk.join(joinRequest); diff --git a/test/core/realtime/hotelClerk/list.test.js b/test/core/realtime/hotelClerk/list.test.js index 8e159082c1..cec8d743ae 100644 --- a/test/core/realtime/hotelClerk/list.test.js +++ b/test/core/realtime/hotelClerk/list.test.js @@ -5,7 +5,7 @@ const sinon = require('sinon'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); describe('Test: hotelClerk.list', () => { let kuzzle; diff --git a/test/core/realtime/hotelClerk/listCollections.test.js b/test/core/realtime/hotelClerk/listCollections.test.js index 73be59d512..6c3e0af900 100644 --- a/test/core/realtime/hotelClerk/listCollections.test.js +++ b/test/core/realtime/hotelClerk/listCollections.test.js @@ -5,7 +5,7 @@ const sinon = require('sinon'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); describe('Test: hotelClerk.listCollections', () => { let kuzzle; diff --git a/test/core/realtime/hotelClerk/subscribe.test.js b/test/core/realtime/hotelClerk/subscribe.test.js index bee3266511..177e9d5d65 100644 --- a/test/core/realtime/hotelClerk/subscribe.test.js +++ b/test/core/realtime/hotelClerk/subscribe.test.js @@ -10,7 +10,7 @@ const { } = require('../../../../index'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); describe('Test: hotelClerk.subscribe', () => { const connectionId = 'connectionid'; @@ -63,8 +63,8 @@ describe('Test: hotelClerk.subscribe', () => { it('should initialize base structures', () => { should(hotelClerk.rooms).be.empty(); - should(hotelClerk.customers).be.empty(); - should(hotelClerk.roomsCount).be.a.Number().and.be.eql(0); + should(hotelClerk.subscriptions).be.empty(); + should(hotelClerk.roomsCount).be.eql(0); }); it('should register a new room and customer', async () => { @@ -98,17 +98,18 @@ describe('Test: hotelClerk.subscribe', () => { const roomId = hotelClerk.rooms.get(response.roomId).id; - const customer = hotelClerk.customers.get(connectionId); + const connectionRooms = hotelClerk.subscriptions.get(connectionId); - should(customer).have.value(roomId, request.input.volatile); + should(connectionRooms.getVolatile(roomId)).be.eql(request.input.volatile); const room = hotelClerk.rooms.get(roomId); - should(room.channels).be.an.Object().and.not.be.undefined(); - should(Object.keys(room.channels).length).be.exactly(1); + should(room.channels).not.be.undefined(); + should(Array.from(room.channels.keys()).length).be.exactly(1); - const channel = Object.keys(room.channels)[0]; - should(room.channels[channel].scope).be.exactly('all'); - should(room.channels[channel].users).be.exactly('none'); + const channelName = Array.from(room.channels.keys())[0]; + const channel = room.channels.get(channelName); + should(channel.scope).be.eql('all'); + should(channel.users).be.eql('none'); response = await hotelClerk.subscribe(request); @@ -221,10 +222,10 @@ describe('Test: hotelClerk.subscribe', () => { it('should discard the request if the associated connection is no longer active', async () => { kuzzle.router.isConnectionAlive.returns(false); - sinon.stub(hotelClerk, '_createRoom'); + sinon.stub(hotelClerk, 'createRoom'); await hotelClerk.subscribe(request); - should(hotelClerk._createRoom).not.called(); + should(hotelClerk.createRoom).not.be.called(); }); }); diff --git a/test/core/realtime/hotelClerk/unsubscribe.test.js b/test/core/realtime/hotelClerk/unsubscribe.test.js index e333b56e68..b2d060283b 100644 --- a/test/core/realtime/hotelClerk/unsubscribe.test.js +++ b/test/core/realtime/hotelClerk/unsubscribe.test.js @@ -10,7 +10,10 @@ const { } = require('../../../../index'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); +const { Room } = require('../../../../lib/core/realtime/room'); +const { Channel } = require('../../../../lib/core/realtime/channel'); +const { ConnectionRooms } = require('../../../../lib/core/realtime/connectionRooms'); describe('Test: hotelClerk.unsubscribe', () => { const connectionId = 'connectionId'; @@ -30,20 +33,21 @@ describe('Test: hotelClerk.unsubscribe', () => { hotelClerk = new HotelClerk(realtimeModule); - hotelClerk.customers.clear(); - hotelClerk.rooms.set(roomId, { - channels: { - ch1: { cluster: true }, - ch2: { cluster: true } - }, - customers: new Set([connectionId]), - index: 'index', - collection: 'collection', - }); + hotelClerk.subscriptions.clear(); + hotelClerk.rooms.set(roomId, new Room( + roomId, + 'index', + 'collection', + new Map([ + ['ch1', new Channel(roomId, { cluster: true })], + ['ch2', new Channel(roomId, { cluster: true })] + ]), + new Set([connectionId]), + )); hotelClerk.roomsCount = 1; - hotelClerk._removeRoomFromRealtimeEngine = sinon.spy(); + sinon.spy(hotelClerk, 'removeRoom'); kuzzle.tokenManager.getKuidFromConnection.returns(null); @@ -67,7 +71,7 @@ describe('Test: hotelClerk.unsubscribe', () => { }); it('should reject if the customer did not subscribe to the room', async () => { - hotelClerk.customers.set(connectionId, new Map([])); + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms()); await should(hotelClerk.unsubscribe(connectionId, roomId)) .rejectedWith(PreconditionError, { id: 'core.realtime.not_subscribed' }); @@ -78,9 +82,9 @@ describe('Test: hotelClerk.unsubscribe', () => { }); it('should reject if the room does not exist', () => { - hotelClerk.customers.set(connectionId, new Map([ + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms(new Map([ [ 'nowhere', null ] - ])); + ]))); return hotelClerk.unsubscribe(connectionId, 'nowhere') .should.be.rejectedWith(NotFoundError, { @@ -90,15 +94,15 @@ describe('Test: hotelClerk.unsubscribe', () => { it('should remove the room from the customer list and remove the connection entry if empty', async () => { kuzzle.tokenManager.getKuidFromConnection.returns('Umraniye'); - hotelClerk.customers.set(connectionId, new Map([ + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms(new Map([ [ roomId, null ] - ])); + ]))); await hotelClerk.unsubscribe(connectionId, roomId); - should(hotelClerk.customers).be.empty(); + should(hotelClerk.subscriptions).be.empty(); - should(hotelClerk._removeRoomFromRealtimeEngine) + should(hotelClerk.removeRoom) .be.calledOnce() .be.calledWith(roomId); @@ -141,19 +145,18 @@ describe('Test: hotelClerk.unsubscribe', () => { }); it('should remove the room from the customer list and keep other existing rooms', async () => { - hotelClerk.customers.set(connectionId, new Map([ + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms(new Map([ [ roomId, null ], [ 'anotherRoom', null ] - ])); + ]))); hotelClerk.rooms.set('anotherRoom', {}); hotelClerk.roomsCount = 2; await hotelClerk.unsubscribe(connectionId, roomId); - should(hotelClerk.customers.get('connectionId')).have.value( - 'anotherRoom', - null); + const connectionRooms = hotelClerk.subscriptions.get('connectionId'); + should(connectionRooms.getVolatile('anotherRoom')).be.eql(null); should(hotelClerk.rooms).not.have.key('roomId'); should(hotelClerk.rooms).have.key('anotherRoom'); @@ -175,20 +178,20 @@ describe('Test: hotelClerk.unsubscribe', () => { }); it('should remove a customer and notify other users in the room', async () => { - hotelClerk.customers.set(connectionId, new Map([ + hotelClerk.subscriptions.set(connectionId, new ConnectionRooms(new Map([ [ roomId, null ] - ])); - hotelClerk.customers.set('foobar', new Map([ + ]))); + hotelClerk.subscriptions.set('foobar', new ConnectionRooms(new Map([ [ roomId, null ] - ])); - hotelClerk.rooms.get(roomId).customers.add('foobar'); + ]))); + hotelClerk.rooms.get(roomId).connections.add('foobar'); await hotelClerk.unsubscribe(connectionId, roomId); should(hotelClerk.rooms).have.key(roomId); - should(hotelClerk.rooms.get(roomId).customers).have.size(1); - should(hotelClerk.rooms.get(roomId).customers).have.key('foobar'); - should(hotelClerk.rooms.get(roomId).customers).not.have.key(connectionId); + should(hotelClerk.rooms.get(roomId).connections).have.size(1); + should(hotelClerk.rooms.get(roomId).connections).have.key('foobar'); + should(hotelClerk.rooms.get(roomId).connections).not.have.key(connectionId); should(hotelClerk.roomsCount).be.eql(1); should(realtimeModule.notifier.notifyUser).calledWithMatch( diff --git a/test/core/realtime/notifier/notifyMethods.test.js b/test/core/realtime/notifier/notifyMethods.test.js index 9b33e4a8cd..1ed3de1bb3 100644 --- a/test/core/realtime/notifier/notifyMethods.test.js +++ b/test/core/realtime/notifier/notifyMethods.test.js @@ -6,12 +6,15 @@ const sinon = require('sinon'); const { Request } = require('../../../../index'); const KuzzleMock = require('../../../mocks/kuzzle.mock'); -const HotelClerk = require('../../../../lib/core/realtime/hotelClerk'); +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); +const { Channel } = require('../../../../lib/core/realtime/channel'); +const { Room } = require('../../../../lib/core/realtime/room'); const Notifier = require('../../../../lib/core/realtime/notifier'); const { DocumentNotification, UserNotification, } = require('../../../../lib/core/realtime/notification'); +const { ConnectionRooms } = require('../../../../lib/core/realtime/connectionRooms'); describe('notify methods', () => { let kuzzle; @@ -23,7 +26,7 @@ describe('notify methods', () => { kuzzle = new KuzzleMock(); hotelClerk = new HotelClerk(); - sinon.stub(hotelClerk, 'removeUser'); + sinon.stub(hotelClerk, 'removeConnection'); notifier = new Notifier({ hotelClerk }); @@ -35,35 +38,47 @@ describe('notify methods', () => { action: 'action' }, {protocol: 'protocol'}); - hotelClerk.rooms.set('matchingSome', { - channels: { - matching_all: {state: 'all', scope: 'all', users: 'all', cluster: true }, - matching_in: {state: 'all', scope: 'in', users: 'none', cluster: true }, - matching_out: {state: 'all', scope: 'out', users: 'none', cluster: true }, - matching_none: {state: 'none', scope: 'none', users: 'none', cluster: true }, - matching_userIn: {state: 'none', scope: 'none', users: 'in', cluster: true }, - matching_userOut: {state: 'none', scope: 'none', users: 'out', cluster: true } - } - }); - - hotelClerk.rooms.set('nonMatching', { - channels: { - foobar: { cluster: true } - } - }); - - hotelClerk.rooms.set('cluster', { - channels: { - clusterOn: {state: 'all', scope: 'all', users: 'all', cluster: true }, - clusterOff: {state: 'all', scope: 'all', users: 'all', cluster: false }, - } - }); - - hotelClerk.rooms.set('alwaysMatching', { - channels: { - always: {state: 'all', scope: 'all', cluster: true } - } - }); + hotelClerk.rooms.set('matchingSome', new Room( + 'matchingSome', + 'index', + 'collection', + new Map([ + ['matching_all', new Channel('matchingSome', { scope: 'all', users: 'all', propagate: true })], + ['matching_in', new Channel('matchingSome', { scope: 'in', users: 'none', propagate: true })], + ['matching_out', new Channel('matchingSome', { scope: 'out', users: 'none', propagate: true })], + ['matching_none', new Channel('matchingSome', { scope: 'none', users: 'none', propagate: true })], + ['matching_userIn', new Channel('matchingSome', { scope: 'none', users: 'in', propagate: true })], + ['matching_userOut', new Channel('matchingSome', { scope: 'none', users: 'out', propagate: true })], + ]), + )); + + hotelClerk.rooms.set('nonMatching', new Room( + 'nonMatching', + 'index', + 'collection', + new Map([ + ['foobar', new Channel('nonMatching', { scope: 'none', propagate: true })], + ]), + )); + + hotelClerk.rooms.set('cluster', new Room( + 'cluster', + 'index', + 'collection', + new Map([ + ['clusterOn', new Channel('cluster', { scope: 'all', users: 'all', propagate: true }) ], + ['clusterOff', new Channel('cluster', { scope: 'all', users: 'all', propagate: false }) ], + ]), + )); + + hotelClerk.rooms.set('alwaysMatching', new Room( + 'alwaysMatching', + 'index', + 'collection', + new Map([ + ['always', new Channel('alwaysMatching', { scope: 'all', propagate: true })], + ]), + )); return notifier.init(); }); @@ -300,10 +315,10 @@ describe('notify methods', () => { }); it('should notify on channel kuzzle:notification:server', async () => { - hotelClerk.customers.set('foobar', new Map([ + hotelClerk.subscriptions.set('foobar', new ConnectionRooms(new Map([ ['nonMatching', null], ['alwaysMatching', null], - ])); + ]))); await notifier.notifyTokenExpired('foobar'); From c954382c65f8e931fa67e62609ed5b593af8836d Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Mon, 1 Nov 2021 14:36:38 +0300 Subject: [PATCH 08/13] Fix realtime connection cleaning after unexpected deconnection (#2173) This PR fix two memory leaks when realtime connection were dropped unexpectedly: - TokenManager: the tokens were not correctly cleaned - HotelClerk: the list of connections was not correctly cleaned This also fix triggering of `core:realtime:user:unsubscribe:after` after an unexpected deconnection. ### Other changes - save channel configuration in a simple string rather than hashing the object with murmur --- docker/scripts/run-dev.sh | 4 +- lib/api/funnel.js | 5 ++- lib/core/auth/tokenManager.ts | 37 +++++++++++++---- lib/core/network/router.js | 4 -- lib/core/realtime/channel.ts | 53 +++++++++++++++++++++++-- lib/core/realtime/hotelClerk.ts | 10 ++++- lib/types/realtime/RealtimeUsers.ts | 2 +- test/api/funnel/checkRights.test.js | 12 ++++++ test/core/auth/tokenManager.test.js | 28 ++++++++++++- test/core/network/router/router.test.js | 1 - test/mocks/kuzzle.mock.js | 1 + 11 files changed, 134 insertions(+), 23 deletions(-) diff --git a/docker/scripts/run-dev.sh b/docker/scripts/run-dev.sh index 11db71a8b7..2eb1047bf2 100755 --- a/docker/scripts/run-dev.sh +++ b/docker/scripts/run-dev.sh @@ -8,8 +8,8 @@ fi if [ -z "$NODE_VERSION" ]; then - echo "Missing NODE_VERSION, use default NODE_12_VERSION" - n $NODE_12_VERSION + echo "Missing NODE_VERSION, use default $NODE_14_VERSION" + n $NODE_14_VERSION fi if [ -n "$TRAVIS" ] || [ -n "$REBUILD" ]; then diff --git a/lib/api/funnel.js b/lib/api/funnel.js index d95ce32cee..985ab83105 100644 --- a/lib/api/funnel.js +++ b/lib/api/funnel.js @@ -403,7 +403,7 @@ class Funnel { * @returns {Promise} */ async checkRights (request) { - if ( !global.kuzzle.config.http.cookieAuthentication + if ( ! global.kuzzle.config.http.cookieAuthentication && request.getBoolean('cookieAuth') ) { throw kerror.get( @@ -470,8 +470,9 @@ class Funnel { 'core:security:user:get', userId); + // If we have a token, link the connection with the token, + // this way the connection can be notified when the token has expired. if (global.kuzzle.config.internal.notifiableProtocols.includes(request.context.connection.protocol)) { - // Link the connection with the token, this way the connection can be notified when the token has expired. global.kuzzle.tokenManager.link( request.context.token, request.context.connection.id); diff --git a/lib/core/auth/tokenManager.ts b/lib/core/auth/tokenManager.ts index bae73ff59e..9dd8ccf006 100644 --- a/lib/core/auth/tokenManager.ts +++ b/lib/core/auth/tokenManager.ts @@ -79,6 +79,9 @@ const TIMEOUT_MAX = Math.pow(2, 31) - 1; export class TokenManager { private tokens: ISortedArray; private anonymousUserId: string = null; + /** + * Map + */ private tokensByConnection = new Map(); private timer: NodeJS.Timeout = null; @@ -116,6 +119,11 @@ export class TokenManager { async init () { const anonymous = await global.kuzzle.ask('core:security:user:anonymous:get'); this.anonymousUserId = anonymous._id; + + global.kuzzle.on('connection:remove', connection => { + this.removeConnection(connection.id) + .catch(err => global.kuzzle.log.info(err)); + }); } runTimer () { @@ -137,8 +145,7 @@ export class TokenManager { * @param connectionId */ link (token: Token, connectionId: string) { - // Embedded SDK does not use tokens - if (! token || token._id === this.anonymousUserId) { + if (! token || token.userId === this.anonymousUserId) { return; } @@ -171,8 +178,7 @@ export class TokenManager { * @param connectionId */ unlink (token: Token, connectionId: string) { - // Embedded SDK does not use tokens - if (! token || token._id === this.anonymousUserId) { + if (! token || token.userId === this.anonymousUserId) { return; } @@ -191,6 +197,20 @@ export class TokenManager { } } + /** + * Remove token associated with a connection. + */ + async removeConnection (connectionId: string) { + const managedToken = this.tokensByConnection.get(connectionId); + + // Anonymous connection does not have associated token + if (! managedToken) { + return; + } + + await this.expire(managedToken); + } + /** * Called when a token expires before its time (e.g. following a * auth:logout action) @@ -200,12 +220,12 @@ export class TokenManager { * @param token */ async expire (token: Token) { - if (token._id === this.anonymousUserId) { + if (token.userId === this.anonymousUserId) { return; } const idx = ManagedToken.indexFor(token); - const searchResult = this.tokens.search({idx}); + const searchResult = this.tokens.search({ idx }); if (searchResult > -1) { const managedToken = this.tokens.array[searchResult]; @@ -251,12 +271,13 @@ export class TokenManager { // API key can never expire (-1) if (arr.length > 0 && (arr[0].expiresAt > 0 && arr[0].expiresAt < Date.now())) { - const connectionIds = arr[0].connectionIds; + const managedToken = arr[0]; arr.shift(); - for (const connectionId of connectionIds) { + for (const connectionId of managedToken.connectionIds) { await global.kuzzle.ask('core:realtime:tokenExpired:notify', connectionId); + this.tokensByConnection.delete(connectionId); } setImmediate(() => this.checkTokensValidity()); diff --git a/lib/core/network/router.js b/lib/core/network/router.js index 4dddf37e43..fb76bdec50 100644 --- a/lib/core/network/router.js +++ b/lib/core/network/router.js @@ -82,10 +82,6 @@ class Router { this.connections.delete(connId); - global.kuzzle - .ask('core:realtime:connection:remove', requestContext.connection.id) - .catch(err => global.kuzzle.log.info(err)); - global.kuzzle.statistics.dropConnection(requestContext); } diff --git a/lib/core/realtime/channel.ts b/lib/core/realtime/channel.ts index 73a7592354..26ea4fda15 100644 --- a/lib/core/realtime/channel.ts +++ b/lib/core/realtime/channel.ts @@ -46,6 +46,53 @@ const realtimeError = kerror.wrap('core', 'realtime'); * @property cluster */ export class Channel { + /** + * Dummy hash function since we only need to keep the channel configuration. + * + * This is 10x faster than murmur. + */ + static hash (channel: Channel) { + let str = ''; + + switch (channel.users) { + case 'all': + str += '1'; + break; + case 'in': + str += '2'; + break; + case 'out': + str += '3'; + break; + case 'none': + str += '3'; + break; + } + + switch (channel.cluster) { + case true: + str += '1'; + break; + case false: + str += '2'; + break; + } + + switch (channel.scope) { + case 'all': + str += '1'; + break; + case 'in': + str += '2'; + break; + case 'out': + str += '3'; + break; + } + + return str; + } + static USERS_ALLOWED_VALUES = ['all', 'in', 'out', 'none']; static SCOPE_ALLOWED_VALUES = Channel.USERS_ALLOWED_VALUES; @@ -93,14 +140,14 @@ export class Channel { this.users = users; this.cluster = propagate; - if (! Channel.SCOPE_ALLOWED_VALUES.includes(this.scope)) { + if (!Channel.SCOPE_ALLOWED_VALUES.includes(this.scope)) { throw realtimeError.get('invalid_scope'); } - if (! Channel.USERS_ALLOWED_VALUES.includes(this.users)) { + if (!Channel.USERS_ALLOWED_VALUES.includes(this.users)) { throw realtimeError.get('invalid_users'); } - this.name = `${roomId}-${global.kuzzle.hash(this)}`; + this.name = `${roomId}-${Channel.hash(this)}`; } } diff --git a/lib/core/realtime/hotelClerk.ts b/lib/core/realtime/hotelClerk.ts index d95bdd131a..77b6636cbc 100644 --- a/lib/core/realtime/hotelClerk.ts +++ b/lib/core/realtime/hotelClerk.ts @@ -171,6 +171,14 @@ export class HotelClerk { (connectionId, roomId, notify) => { return this.unsubscribe(connectionId, roomId, notify); }); + + /** + * Clear subscriptions when a connection is dropped + */ + global.kuzzle.on('connection:remove', connection => { + this.removeConnection(connection.id) + .catch(err => global.kuzzle.log.info(err)); + }); } /** @@ -272,7 +280,7 @@ export class HotelClerk { * The room may exists on another cluster node, if it's the case, the normalized * filters will be fetched from the cluster. */ - async join (request: KuzzleRequest): Promise<{ roomId, channel }> { + async join (request: KuzzleRequest): Promise<{ channel, roomId }> { const roomId = request.input.body.roomId; if (! this.rooms.has(roomId)) { diff --git a/lib/types/realtime/RealtimeUsers.ts b/lib/types/realtime/RealtimeUsers.ts index af1c1d622f..d202c9ebd0 100644 --- a/lib/types/realtime/RealtimeUsers.ts +++ b/lib/types/realtime/RealtimeUsers.ts @@ -1,4 +1,4 @@ /** * Subscribe to users entering or leaving the room */ -export type RealtimeUsers= 'in' | 'out' | 'all' | 'none'; +export type RealtimeUsers = 'in' | 'out' | 'all' | 'none'; diff --git a/test/api/funnel/checkRights.test.js b/test/api/funnel/checkRights.test.js index d7b82d897e..13c9d81396 100644 --- a/test/api/funnel/checkRights.test.js +++ b/test/api/funnel/checkRights.test.js @@ -62,6 +62,17 @@ describe('funnel.checkRights', () => { global.kuzzle.config.plugins.common.failsafeMode = false; }); + it('should link the token to the connection for realtime protocols', async () => { + sinon.stub(loadedUser, 'isActionAllowed').resolves(true); + request.context.connection.id = 'connection-id'; + request.context.connection.protocol = 'websocket'; + + await funnel.checkRights(request); + + should(global.kuzzle.tokenManager.link) + .be.calledWith(request.context.token, 'connection-id'); + }); + it('should reject with an UnauthorizedError if an anonymous user is not allowed to execute the action', async () => { verifiedToken.userId = '-1'; @@ -134,6 +145,7 @@ describe('funnel.checkRights', () => { should(kuzzle.pipe).calledWith('request:onAuthorized', request); should(kuzzle.pipe).not.calledWith('request:onUnauthorized', request); + should(global.kuzzle.tokenManager.link).not.be.called(); }); it('should reject if non admin user use the API during failsafe mode', async () => { diff --git a/test/core/auth/tokenManager.test.js b/test/core/auth/tokenManager.test.js index 2931049238..f5f96da292 100644 --- a/test/core/auth/tokenManager.test.js +++ b/test/core/auth/tokenManager.test.js @@ -9,7 +9,7 @@ const { Token } = require('../../../lib/model/security/token'); const { TokenManager } = require('../../../lib/core/auth/tokenManager'); describe('Test: token manager core component', () => { - const anonymousToken = new Token({ _id: '-1' }); + const anonymousToken = new Token({ _id: null, userId: '-1' }); let kuzzle; let token; let tokenManager; @@ -38,6 +38,28 @@ describe('Test: token manager core component', () => { } }); + describe('events', () => { + it('should register a listener on "connection:remove"', done => { + tokenManager.removeConnection = async connectionId => { + should(connectionId).be.eql('connection-id'); + done(); + }; + + kuzzle.emit('connection:remove', { id: 'connection-id' }); + }); + }); + + describe('#removeConnection', () => { + it('should expire the token if it exists', async () => { + sinon.stub(tokenManager, 'expire').resolves(); + tokenManager.link(token, 'connectionId'); + + await tokenManager.removeConnection('connectionId'); + + should(tokenManager.expire.getCall(0).args[0]._id).be.eql(token._id); + }); + }); + describe('#link', () => { it('should do nothing if the token is not set', () => { tokenManager.link(null, 'foo'); @@ -47,6 +69,7 @@ describe('Test: token manager core component', () => { it('should not add a link to an anonymous token', () => { tokenManager.link(anonymousToken, 'foo'); + should(tokenManager.tokens.array).be.an.Array().and.be.empty(); should(tokenManager.tokensByConnection.size).equal(0); }); @@ -55,6 +78,7 @@ describe('Test: token manager core component', () => { const runTimerStub = sinon.stub(tokenManager, 'runTimer'); tokenManager.link(token, 'foo'); + should(tokenManager.tokens.array) .be.an.Array() .and.match([{ @@ -303,6 +327,8 @@ describe('Test: token manager core component', () => { should(tokenManager.tokens.array.length).be.eql(1); should(tokenManager.tokens.array[0]._id).be.eql('bar'); should(runTimerStub).be.calledOnce(); + should(tokenManager.tokensByConnection).have.key('connectionId1'); + should(tokenManager.tokensByConnection).not.have.key('connectionId2'); }); it('should not expire API key', async () => { diff --git a/test/core/network/router/router.test.js b/test/core/network/router/router.test.js index 76561d4ca9..da61461c07 100644 --- a/test/core/network/router/router.test.js +++ b/test/core/network/router/router.test.js @@ -75,7 +75,6 @@ describe('Test: router', () => { router.connections.set(connectionId, requestContext); router.removeConnection(requestContext); - should(kuzzle.ask).calledWith('core:realtime:connection:remove', connectionId); should(kuzzle.statistics.dropConnection) .calledOnce() .calledWith(requestContext); diff --git a/test/mocks/kuzzle.mock.js b/test/mocks/kuzzle.mock.js index 0ae4323539..c4e8add380 100644 --- a/test/mocks/kuzzle.mock.js +++ b/test/mocks/kuzzle.mock.js @@ -173,6 +173,7 @@ class KuzzleMock extends KuzzleEventEmitter { refresh: sinon.stub(), unlink: sinon.stub(), getKuidFromConnection: sinon.stub(), + removeConnection: sinon.stub().resolves(), }; this.validation = { From c7e35fdcd36d5ea65c3866cbcdc229f4e1fa9fbf Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 1 Nov 2021 12:37:04 +0100 Subject: [PATCH 09/13] Add logs to cluster interfaces / ip adress selection in debug (#2169) --- .github/workflows/push_master.workflow.yml | 5 ----- lib/cluster/node.js | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml index 9c2738bcfb..4a5a6318ac 100644 --- a/.github/workflows/push_master.workflow.yml +++ b/.github/workflows/push_master.workflow.yml @@ -183,13 +183,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: -<<<<<<< HEAD node-version: "14.x" registry-url: "https://registry.npmjs.org" -======= - node-version: '12.x' - registry-url: 'https://registry.npmjs.org' ->>>>>>> master - run: npm install - run: npm publish env: diff --git a/lib/cluster/node.js b/lib/cluster/node.js index 06c1ce672a..346231764e 100644 --- a/lib/cluster/node.js +++ b/lib/cluster/node.js @@ -107,6 +107,8 @@ function getIP ({ family = 'IPv4', interface: netInterface, ip } = {}) { } } + debug('Found interfaces %o', interfaces); + interfaces = interfaces.filter(n => { return !n.internal && !isInternalIP(n.address) @@ -114,6 +116,8 @@ function getIP ({ family = 'IPv4', interface: netInterface, ip } = {}) { && (!ip || mustBePrivate === isPrivateIP(n.address)); }); + debug('Filtered interfaces %o', interfaces); + if (interfaces.length === 0) { return null; } @@ -156,6 +160,7 @@ class ClusterNode { ip: this.config.ip, }); + debug('Found IP address: %s with config %o', this.ip, this.config); assert(this.ip !== null, `[CLUSTER] No suitable IP address found with the provided configuration (family: ${family}, interface: ${this.config.interface}, ip: ${this.config.ip})`); this.nodeId = null; From 0d9e2bcd0f9dab353f795c89a00c7a7726774d92 Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Mon, 1 Nov 2021 16:24:58 +0300 Subject: [PATCH 10/13] Allows to disable the SDK version safety check (#2180) By default, Kuzzle respond with an error when a request is made by using an SDK with an incompatible version. (e.g. SDK JS 5 with Kuzzle 2) This PR introduce a new configuration to disable this safety check `server.strictSdkVersion` (default `true`) --- .kuzzlerc.sample | 3 +++ doc/2/api/errors/error-codes/api/index.md | 2 +- lib/api/funnel.js | 9 ++++++--- lib/config/default.config.js | 3 ++- lib/kerror/codes/2-api.json | 2 +- test/api/funnel/processRequest.test.js | 11 +++++++++++ 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.kuzzlerc.sample b/.kuzzlerc.sample index f5d3292886..0dd9baeae1 100644 --- a/.kuzzlerc.sample +++ b/.kuzzlerc.sample @@ -356,8 +356,11 @@ // ("gb") or terabytes ("tb") // * port: // The listening port for HTTP and WebSocket + // * strictSdkVersion: + // Raise an error when an incompatible SDK is used. "maxRequestSize": "1mb", "port": 7512, + "strictSdkVersion": true, // [logs] // Configuration section for Kuzzle access logs // * transports: diff --git a/doc/2/api/errors/error-codes/api/index.md b/doc/2/api/errors/error-codes/api/index.md index 8442b1234b..71fbbc686c 100644 --- a/doc/2/api/errors/error-codes/api/index.md +++ b/doc/2/api/errors/error-codes/api/index.md @@ -43,7 +43,7 @@ description: Error codes definitions | api.process.connection_dropped
0x02020003
| [BadRequestError](/core/2/api/errors/error-codes#badrequesterror)
(400)
| Client connection dropped | The request has been discarded because its linked client connection has dropped | | api.process.controller_not_found
0x02020004
| [NotFoundError](/core/2/api/errors/error-codes#notfounderror)
(404)
| API controller "%s" not found. | API controller not found | | api.process.action_not_found
0x02020005
| [NotFoundError](/core/2/api/errors/error-codes#notfounderror)
(404)
| API action "%s":"%s" not found | API controller action not found | -| api.process.incompatible_sdk_version
0x02020006
| [BadRequestError](/core/2/api/errors/error-codes#badrequesterror)
(400)
| Incompatible SDK client. Your SDK version (%s) does not match Kuzzle requirement (%s). | SDK is incompatible with the current Kuzzle version | +| api.process.incompatible_sdk_version
0x02020006
| [BadRequestError](/core/2/api/errors/error-codes#badrequesterror)
(400)
| Incompatible SDK client. Your SDK version (%s) does not match Kuzzle requirement (%s). | SDK is incompatible with the current Kuzzle version. You can set "config.server.strictSdkVersion" to false to disable this safety check at your own risk. | | api.process.shutting_down
0x02020007
| [ServiceUnavailableError](/core/2/api/errors/error-codes#serviceunavailableerror)
(503)
| Rejected: this node is shutting down. | This Kuzzle node is shutting down and refuses new requests | | api.process.too_many_requests
0x02020008
| [TooManyRequestsError](/core/2/api/errors/error-codes#toomanyrequestserror)
(429)
| Rejected: requests rate limit exceeded for this user. | The request has been refused because a rate limit has been exceeded for this user | | api.process.admin_exists
0x02020009
| [PreconditionError](/core/2/api/errors/error-codes#preconditionerror)
(412)
| Admin user is already set. | Attempted to create the first administrator, when one already exists | diff --git a/lib/api/funnel.js b/lib/api/funnel.js index 985ab83105..d6ba72d4d4 100644 --- a/lib/api/funnel.js +++ b/lib/api/funnel.js @@ -725,9 +725,12 @@ class Funnel { * @throws */ _checkSdkVersion (request) { - const - sdkVersion = request.input.volatile && request.input.volatile.sdkVersion, - sdkName = request.input.volatile && request.input.volatile.sdkName; + if (! global.kuzzle.config.server.strictSdkVersion) { + return; + } + + const sdkVersion = request.input.volatile && request.input.volatile.sdkVersion; + const sdkName = request.input.volatile && request.input.volatile.sdkName; // sdkVersion property is only used by Kuzzle v1 SDKs if (sdkVersion) { diff --git a/lib/config/default.config.js b/lib/config/default.config.js index d03eafa997..e21fe13322 100644 --- a/lib/config/default.config.js +++ b/lib/config/default.config.js @@ -234,7 +234,8 @@ module.exports = { rateLimit: 0, realtimeNotifications: true, } - } + }, + strictSdkVersion: true, }, services: { diff --git a/lib/kerror/codes/2-api.json b/lib/kerror/codes/2-api.json index bbc07bb187..815e3b03e9 100644 --- a/lib/kerror/codes/2-api.json +++ b/lib/kerror/codes/2-api.json @@ -124,7 +124,7 @@ "class": "NotFoundError" }, "incompatible_sdk_version": { - "description": "SDK is incompatible with the current Kuzzle version", + "description": "SDK is incompatible with the current Kuzzle version. You can set \"config.server.strictSdkVersion\" to false to disable this safety check at your own risk.", "code": 6, "message": "Incompatible SDK client. Your SDK version (%s) does not match Kuzzle requirement (%s).", "class": "BadRequestError" diff --git a/test/api/funnel/processRequest.test.js b/test/api/funnel/processRequest.test.js index 42f9552f28..1192096b90 100644 --- a/test/api/funnel/processRequest.test.js +++ b/test/api/funnel/processRequest.test.js @@ -291,6 +291,17 @@ describe('funnel.processRequest', () => { volatile: {} } }; + kuzzle.config.server.strictSdkVersion = true; + }); + + it('should not check SDK version if "config.server.strictSdkVersion" is false', () => { + kuzzle.config.server.strictSdkVersion = false; + funnel.sdkCompatibility = { js: { min: 8 } }; + + request.input.volatile.sdkName = 'js@7.4.2'; + + should(() => funnel._checkSdkVersion(request)) + .not.throw(BadRequestError, { id: 'api.process.incompatible_sdk_version' }); }); it('should not throw if sdkName is in incorrect format', () => { From 49d6bc335d75037103a0bad9537e28b073261f78 Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Mon, 1 Nov 2021 16:25:14 +0300 Subject: [PATCH 11/13] Properly disconnect connections on shutdown (#2181) Properly disconnect every connection when the node is shutting down: - emit the `core:realtime:user:unsubscribe:after` event - send notification about user leaving a room --- docker-compose.yml | 2 +- lib/core/realtime/hotelClerk.ts | 16 ++++++ lib/kuzzle/kuzzle.ts | 2 + .../realtime/hotelClerk/disconnect.test.js | 2 +- .../realtime/hotelClerk/hotelClerk.test.js | 50 +++++++++++++++++++ test/kuzzle/kuzzle.test.js | 4 +- 6 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 test/core/realtime/hotelClerk/hotelClerk.test.js diff --git a/docker-compose.yml b/docker-compose.yml index 12a0a5c4a6..579e7b07c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,7 +78,7 @@ services: - "9231:9229" # Debug port redis: - image: redis:6.2.4 + image: redis:6 container_name: kuzzle_redis ports: - "6379:6379" diff --git a/lib/core/realtime/hotelClerk.ts b/lib/core/realtime/hotelClerk.ts index 77b6636cbc..ba85230e63 100644 --- a/lib/core/realtime/hotelClerk.ts +++ b/lib/core/realtime/hotelClerk.ts @@ -172,6 +172,11 @@ export class HotelClerk { return this.unsubscribe(connectionId, roomId, notify); }); + /** + * Clear the hotel clerk and properly disconnect connections. + */ + global.kuzzle.onAsk('core:realtime:shutdown', () => this.clearConnections()); + /** * Clear subscriptions when a connection is dropped */ @@ -360,6 +365,17 @@ export class HotelClerk { )); } + /** + * Clear all connections made to this node: + * - trigger appropriate core events + * - send user exit room notifications + */ + async clearConnections (): Promise { + await Bluebird.map(this.subscriptions.keys(), (connectionId: string) => ( + this.removeConnection(connectionId) + )); + } + /** * Register a new subscription * - save the subscription on the provided room with volatile data diff --git a/lib/kuzzle/kuzzle.ts b/lib/kuzzle/kuzzle.ts index 673fb41022..bb7716b9c1 100644 --- a/lib/kuzzle/kuzzle.ts +++ b/lib/kuzzle/kuzzle.ts @@ -307,6 +307,8 @@ class Kuzzle extends KuzzleEventEmitter { // Ask the network layer to stop accepting new request this.entryPoint.dispatch('shutdown'); + await this.ask('core:realtime:shutdown'); + while (this.funnel.remainingRequests !== 0) { this.log.info(`[shutdown] Waiting: ${this.funnel.remainingRequests} remaining requests`); await Bluebird.delay(1000); diff --git a/test/core/realtime/hotelClerk/disconnect.test.js b/test/core/realtime/hotelClerk/disconnect.test.js index dfff77f4c4..3898a12e1b 100644 --- a/test/core/realtime/hotelClerk/disconnect.test.js +++ b/test/core/realtime/hotelClerk/disconnect.test.js @@ -10,7 +10,7 @@ const { ConnectionRooms } = require('../../../../lib/core/realtime/connectionRoo const { Room } = require('../../../../lib/core/realtime/room'); const { Channel } = require('../../../../lib/core/realtime/channel'); -describe('Test: hotelClerk.removeUser', () => { +describe('Test: hotelClerk.removeConnection', () => { const connectionId = 'connectionid'; const collection = 'user'; const index = '%test'; diff --git a/test/core/realtime/hotelClerk/hotelClerk.test.js b/test/core/realtime/hotelClerk/hotelClerk.test.js new file mode 100644 index 0000000000..894b8b293d --- /dev/null +++ b/test/core/realtime/hotelClerk/hotelClerk.test.js @@ -0,0 +1,50 @@ +'use strict'; + +const should = require('should'); +const sinon = require('sinon'); + +const KuzzleMock = require('../../../mocks/kuzzle.mock'); + +const { HotelClerk } = require('../../../../lib/core/realtime/hotelClerk'); +const { ConnectionRooms } = require('../../../../lib/core/realtime/connectionRooms'); + +describe('HotelClerk', () => { + let kuzzle; + let hotelClerk; + let realtimeModule; + + beforeEach(() => { + kuzzle = new KuzzleMock(); + + hotelClerk = new HotelClerk(realtimeModule); + + realtimeModule = {}; + + hotelClerk.subscriptions.set('a', new ConnectionRooms(new Map([['foo', null]]))); + hotelClerk.subscriptions.set('b', new ConnectionRooms(new Map([['foo', null]]))); + + return hotelClerk.init(); + }); + + describe('#clearConnections', () => { + it('should have been registered with the "core:realtime:shutdown" event', async () => { + sinon.stub(hotelClerk, 'clearConnections').resolves(); + + kuzzle.ask.restore(); + await kuzzle.ask('core:realtime:shutdown'); + + should(hotelClerk.clearConnections).be.calledOnce(); + }); + + it('should properly remove each connection', async () => { + sinon.stub(hotelClerk, 'removeConnection').resolves(); + + await hotelClerk.clearConnections(); + + should(hotelClerk.removeConnection) + .be.calledTwice() + .be.calledWith('a') + .be.calledWith('b'); + }); + }); +}); diff --git a/test/kuzzle/kuzzle.test.js b/test/kuzzle/kuzzle.test.js index d644a233ac..811ec39afb 100644 --- a/test/kuzzle/kuzzle.test.js +++ b/test/kuzzle/kuzzle.test.js @@ -219,7 +219,7 @@ describe('/lib/kuzzle/kuzzle.js', () => { }); }); - describe('#kuzzle/shutdown', () => { + describe('#shutdown', () => { it('should exit only when there is no request left in the funnel', async () => { sinon.stub(process, 'exit'); @@ -246,6 +246,8 @@ describe('/lib/kuzzle/kuzzle.js', () => { should(kuzzle.pipe).calledWith('kuzzle:shutdown'); should(Bluebird.delay.callCount).approximately(5, 1); + should(kuzzle.ask).be.calledWith('core:realtime:shutdown'); + // @deprecated should(kuzzle.emit).calledWith('core:shutdown'); From 63b562d695566a094c3d4dec219ee47cf30e0b1f Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Mon, 1 Nov 2021 16:29:01 +0300 Subject: [PATCH 12/13] Allows to deactivate statistics module (#2174) Kuzzle statistics module collect information about requests and connections handled by a node and store them into Redis with a limited TTL. This PR allows to completely deactivate this module/ --- .kuzzlerc.sample | 3 + .../api/errors/error-codes/services/index.md | 9 +++ lib/config/default.config.js | 1 + lib/core/statistics/statistics.js | 48 +++++++++++++-- lib/kerror/codes/1-services.json | 11 ++++ test/core/statistics/statistics.test.js | 58 +++++++++++++++++++ 6 files changed, 125 insertions(+), 5 deletions(-) diff --git a/.kuzzlerc.sample b/.kuzzlerc.sample index 0dd9baeae1..3d32e42696 100644 --- a/.kuzzlerc.sample +++ b/.kuzzlerc.sample @@ -823,11 +823,14 @@ }, // Configuration of the Kuzzle's internal statistics module + // * enabled: + // Enable or disable the stats module // * ttl: // Time to live (in seconds) of a statistics frame // * statsInterval: // Time (in seconds) between statistics snapshots "stats": { + "enabled": true, "ttl": 3600, "statsInterval": 10 }, diff --git a/doc/2/api/errors/error-codes/services/index.md b/doc/2/api/errors/error-codes/services/index.md index dfc66ab124..deb35c7644 100644 --- a/doc/2/api/errors/error-codes/services/index.md +++ b/doc/2/api/errors/error-codes/services/index.md @@ -75,3 +75,12 @@ description: Error codes definitions | services.cache.write_failed
0x01030004
| [InternalError](/core/2/api/errors/error-codes#internalerror)
(500)
| Cache write fail: %s | An attempt to write to the cache failed | --- + + +### Subdomain: 0x0104: statistics + +| id / code | class / status | message | description | +| --------- | -------------- | --------| ----------- | +| services.statistics.not_available
0x01040001
| [InternalError](/core/2/api/errors/error-codes#internalerror)
(500)
| Statistics module is not available. | The statistics module is not enabled. See "config.stats.enabled". | + +--- diff --git a/lib/config/default.config.js b/lib/config/default.config.js index e21fe13322..ba2ae30163 100644 --- a/lib/config/default.config.js +++ b/lib/config/default.config.js @@ -372,6 +372,7 @@ module.exports = { }, stats: { + enabled: true, ttl: 3600, statsInterval: 10 }, diff --git a/lib/core/statistics/statistics.js b/lib/core/statistics/statistics.js index 5c36b0ba80..7d56addd05 100644 --- a/lib/core/statistics/statistics.js +++ b/lib/core/statistics/statistics.js @@ -21,19 +21,21 @@ 'use strict'; -const kerror = require('../../kerror').wrap('api', 'assert'); +const errorApiAssert = require('../../kerror').wrap('api', 'assert'); +const errorStats = require('../../kerror').wrap('services', 'stats'); /** * @class Statistics * @param {Kuzzle} kuzzle */ class Statistics { - constructor() { + constructor () { // uses '{' and '}' to force all statistics frames to be stored on 1 redis // node // (see https://redis.io/topics/cluster-spec#keys-distribution-model) this.cacheKeyPrefix = '{stats/}'; + this.enabled = global.kuzzle.config.stats.enabled; this.ttl = global.kuzzle.config.stats.ttl * 1000; this.interval = global.kuzzle.config.stats.statsInterval * 1000; this.lastFrame = null; @@ -53,6 +55,10 @@ class Statistics { * @param {Request} request */ startRequest (request) { + if (! this.enabled) { + return; + } + const protocol = request && request.context.connection.protocol; if (!protocol) { @@ -75,6 +81,10 @@ class Statistics { * @param {Request} request */ completedRequest (request) { + if (! this.enabled) { + return; + } + const protocol = request && request.context.connection.protocol; if (!protocol) { @@ -101,6 +111,10 @@ class Statistics { * @param {Request} request */ failedRequest (request) { + if (! this.enabled) { + return; + } + const protocol = request && request.context.connection.protocol; if (!protocol) { @@ -127,6 +141,10 @@ class Statistics { * @param {RequestContext} requestContext */ newConnection (requestContext) { + if (! this.enabled) { + return; + } + if (!requestContext.connection.protocol) { return; } @@ -147,6 +165,10 @@ class Statistics { * @param {RequestContext} requestContext */ dropConnection (requestContext) { + if (! this.enabled) { + return; + } + if (!requestContext.connection.protocol) { return; } @@ -166,7 +188,11 @@ class Statistics { * * @returns {Promise} */ - async getLastStats() { + async getLastStats () { + if (! this.enabled) { + throw errorStats.get('not_available'); + } + const frame = Object.assign( {timestamp: (new Date()).getTime()}, this.currentStats); @@ -189,6 +215,10 @@ class Statistics { * @returns {Promise} */ async getStats (request) { + if (! this.enabled) { + throw errorStats.get('not_available'); + } + const response = { hits: [], total: null @@ -211,11 +241,11 @@ class Statistics { } if (startTime !== undefined && isNaN(startTime)) { - throw kerror.get('invalid_argument', 'startTime', 'number'); + throw errorApiAssert.get('invalid_argument', 'startTime', 'number'); } if (stopTime !== undefined && isNaN(stopTime)) { - throw kerror.get('invalid_argument', 'stopTime', 'number'); + throw errorApiAssert.get('invalid_argument', 'stopTime', 'number'); } if (startTime !== undefined && startTime >= currentDate) { @@ -286,6 +316,10 @@ class Statistics { * Init statistics component */ init () { + if (! this.enabled) { + return; + } + this.timer = setInterval(async () => { try { await this.writeStats(); @@ -301,6 +335,10 @@ class Statistics { } async writeStats () { + if (! this.enabled) { + return; + } + const stats = JSON.stringify(this.currentStats); this.lastFrame = Date.now(); diff --git a/lib/kerror/codes/1-services.json b/lib/kerror/codes/1-services.json index 2477803410..0798685bb8 100644 --- a/lib/kerror/codes/1-services.json +++ b/lib/kerror/codes/1-services.json @@ -306,6 +306,17 @@ "class": "InternalError" } } + }, + "statistics": { + "code": 4, + "errors": { + "not_available": { + "description": "The statistics module is not enabled. See \"config.stats.enabled\".", + "code": 1, + "message": "Statistics module is not available.", + "class": "InternalError" + } + } } } } diff --git a/test/core/statistics/statistics.test.js b/test/core/statistics/statistics.test.js index c40c8ad0c0..460eeb65f0 100644 --- a/test/core/statistics/statistics.test.js +++ b/test/core/statistics/statistics.test.js @@ -41,6 +41,7 @@ describe('Test: statistics core component', () => { kuzzle = new Kuzzle(); stats = new Statistics(); + stats.enabled = true; }); afterEach(() => { @@ -76,6 +77,15 @@ describe('Test: statistics core component', () => { should(stats.currentStats.ongoingRequests).be.empty(); }); + it('should do nothing for startRequest if module is disabled', () => { + request.context.protocol = 'foobar'; + stats.enabled = false; + + stats.startRequest(); + + should(stats.currentStats.ongoingRequests).be.empty(); + }); + it('should handle completed requests', () => { stats.currentStats.ongoingRequests.set('foobar', 2); request.context.protocol = 'foobar'; @@ -95,6 +105,16 @@ describe('Test: statistics core component', () => { should(stats.currentStats.completedRequests).be.empty(); }); + it('should do nothing for completedRequest if module is disabled', () => { + stats.currentStats.ongoingRequests.set('foobar', 2); + request.context.protocol = 'foobar'; + stats.enabled = false; + + stats.completedRequest(); + + should(stats.currentStats.completedRequests).be.empty(); + }); + it('should handle failed requests', () => { stats.currentStats.ongoingRequests.set('foobar', 2); request.context.protocol = 'foobar'; @@ -115,6 +135,16 @@ describe('Test: statistics core component', () => { should(stats.currentStats.failedRequests).be.empty(); }); + it('should do nothing for failedRequest if module is disabled', () => { + stats.currentStats.ongoingRequests.set('foobar', 2); + request.context.protocol = 'foobar'; + stats.enabled = false; + + stats.failedRequest(request); + + should(stats.currentStats.failedRequests).be.empty(); + }); + it('should handle new connections', () => { const context = new RequestContext({connection: {protocol: 'foobar'}}); stats.newConnection(context); @@ -123,6 +153,15 @@ describe('Test: statistics core component', () => { should(stats.currentStats.connections.get('foobar')).not.be.undefined().and.be.exactly(2); }); + it('should not handle new connections if module is disabled', () => { + const context = new RequestContext({connection: {protocol: 'foobar'}}); + stats.enabled = false; + + stats.newConnection(context); + + should(stats.currentStats.connections.get('foobar')).be.undefined(); + }); + it('should be able to unregister a connection', () => { const context = new RequestContext({connection: {protocol: 'foobar'}}); @@ -133,6 +172,16 @@ describe('Test: statistics core component', () => { should(stats.currentStats.connections.get('foobar')).be.undefined(); }); + it('should not handle unregister a connection if module is disabled', () => { + const context = new RequestContext({connection: {protocol: 'foobar'}}); + stats.currentStats.connections.set('foobar', 2); + stats.enabled = false; + + stats.dropConnection(context); + + should(stats.currentStats.connections.get('foobar')).be.exactly(2); + }); + it('should return the current frame when there is still no statistics in cache', () => { stats.currentStats = fakeStats; request.input.args.startTime = lastFrame - 10000000; @@ -329,6 +378,15 @@ describe('Test: statistics core component', () => { { ttl: stats.ttl }); }); + it('should not write statistics frames in cache if module is disabled', async () => { + stats.currentStats = Object.assign({}, fakeStats); + stats.enabled = false; + + await stats.writeStats(); + + should(kuzzle.ask).not.be.called(); + }); + it('should reject the promise if the cache returns an error', () => { stats.lastFrame = Date.now(); From a2429a3434de3d2313fe557b25a98d2f68f74c7e Mon Sep 17 00:00:00 2001 From: Aschen Date: Mon, 1 Nov 2021 16:34:38 +0100 Subject: [PATCH 13/13] Release 2.14.11 --- lib/config/index.js | 7 +- lib/types/Plugin.ts | 12 +- package-lock.json | 514 +++++++++++++++++++++++++++++++------------- package.json | 30 +-- 4 files changed, 389 insertions(+), 174 deletions(-) diff --git a/lib/config/index.js b/lib/config/index.js index 31de49febd..03539109f0 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -214,8 +214,13 @@ function checkClusterOptions (config) { } assert(typeof cfg.ipv6 === 'boolean', '[CONFIG] kuzzlerc.cluster.ipv6: boolean expected'); - + // If config is passed with env variable, ip cannot be the value null + // but only blank string or the string "null" + if (`${cfg.ip}`.length === 0 || cfg.ip === 'null') { + cfg.ip = null; + } assert(!cfg.ip || ['private', 'public'].includes(cfg.ip), '[CONFIG] kuzzlerc.cluster.ip: invalid value (accepted values: public, private)'); + assert(!cfg.interface || typeof cfg.interface === 'string', '[CONFIG] kuzzlerc.cluster.interface: value must be either null, or a string'); } diff --git a/lib/types/Plugin.ts b/lib/types/Plugin.ts index b1950db214..a7044aea64 100644 --- a/lib/types/Plugin.ts +++ b/lib/types/Plugin.ts @@ -90,7 +90,7 @@ export abstract class Plugin { * } * } */ - public api?: PluginApiDefinition + public api?: PluginApiDefinition; /** * Define hooks on Kuzzle events. @@ -103,7 +103,7 @@ export abstract class Plugin { * 'security:afterCreateUser': async (request: Request) => ... * } */ - public hooks?: PluginHookDefinition + public hooks?: PluginHookDefinition; /** * Define pipes on Kuzzle events. @@ -116,7 +116,7 @@ export abstract class Plugin { * 'document:afterCreate': async (request: Request) => ... * } */ - public pipes?: PluginPipeDefinition + public pipes?: PluginPipeDefinition; /** * Define authenticator classes used by strategies. @@ -127,15 +127,15 @@ export abstract class Plugin { /** * The key is the authenticator name and the value is the class. */ - [name: string]: any - } + [name: string]: any; + }; /** * Define authentications strategies. * * @see https://docs.kuzzle.io/core/2/plugins/guides/strategies/overview */ - public strategies?: StrategyDefinition + public strategies?: StrategyDefinition; /** * Plugin initialization method. diff --git a/package-lock.json b/package-lock.json index 6b7cdadda4..395e73eddf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kuzzle", - "version": "2.14.10", + "version": "2.14.11", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -363,9 +363,9 @@ "dev": true }, "@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" @@ -400,9 +400,9 @@ } }, "@elastic/elasticsearch": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.13.0.tgz", - "integrity": "sha512-WgwLWo2p9P2tdqzBGX9fHeG8p5IOTXprXNTECQG2mJ7z9n93N5AFBJpEw4d35tWWeCWi9jI13A2wzQZH7XZ/xw==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.15.0.tgz", + "integrity": "sha512-FUKvjV2IKtIiWsvBy7D+wLbSEONsmNR15RRN7P/Sb30g4ObZRHH2qGOP5PPnzxdntEkzZ8HzY7nKKXFS+3Du1g==", "requires": { "debug": "^4.3.1", "hpagent": "^0.1.1", @@ -720,9 +720,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.175", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", - "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", + "version": "4.14.176", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", + "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==", "dev": true }, "@types/long": { @@ -742,17 +742,17 @@ "optional": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.2.0.tgz", + "integrity": "sha512-qQwg7sqYkBF4CIQSyRQyqsYvP+g/J0To9ZPVNJpfxfekl5RmdvQnFFTVVwpRtaUDFNvjfe/34TgY/dpc3MgNTw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", + "@typescript-eslint/experimental-utils": "5.2.0", + "@typescript-eslint/scope-manager": "5.2.0", + "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", - "regexpp": "^3.1.0", + "regexpp": "^3.2.0", "semver": "^7.3.5", "tsutils": "^3.21.0" }, @@ -766,70 +766,81 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.2.0.tgz", + "integrity": "sha512-fWyT3Agf7n7HuZZRpvUYdFYbPk3iDCq6fgu3ulia4c7yxmPnwVBovdSOX7RL+k8u6hLbrXcdAehlWUVpGh6IEw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.2.0", + "@typescript-eslint/types": "5.2.0", + "@typescript-eslint/typescript-estree": "5.2.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.2.0.tgz", + "integrity": "sha512-Uyy4TjJBlh3NuA8/4yIQptyJb95Qz5PX//6p8n7zG0QnN4o3NF9Je3JHbVU7fxf5ncSXTmnvMtd/LDQWDk0YqA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "5.2.0", + "@typescript-eslint/types": "5.2.0", + "@typescript-eslint/typescript-estree": "5.2.0", + "debug": "^4.3.2" } }, "@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.2.0.tgz", + "integrity": "sha512-RW+wowZqPzQw8MUFltfKYZfKXqA2qgyi6oi/31J1zfXJRpOn6tCaZtd9b5u9ubnDG2n/EMvQLeZrsLNPpaUiFQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "5.2.0", + "@typescript-eslint/visitor-keys": "5.2.0" } }, "@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.2.0.tgz", + "integrity": "sha512-cTk6x08qqosps6sPyP2j7NxyFPlCNsJwSDasqPNjEQ8JMD5xxj2NHxcLin5AJQ8pAVwpQ8BMI3bTxR0zxmK9qQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.2.0.tgz", + "integrity": "sha512-RsdXq2XmVgKbm9nLsE3mjNUM7BTr/K4DYR9WfFVMUuozHWtH5gMpiNZmtrMG8GR385EOSQ3kC9HiEMJWimxd/g==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", + "@typescript-eslint/types": "5.2.0", + "@typescript-eslint/visitor-keys": "5.2.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", "semver": "^7.3.5", "tsutils": "^3.21.0" + }, + "dependencies": { + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + } } }, "@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.2.0.tgz", + "integrity": "sha512-Nk7HizaXWWCUBfLA/rPNKMzXzWS8Wg9qHMuGtT+v2/YpPij4nVXrVJc24N/r5WrrmqK31jCrZxeHqIgqRzs0Xg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "5.2.0", + "eslint-visitor-keys": "^3.0.0" } }, "@ungap/promise-all-settled": { @@ -1132,9 +1143,9 @@ "dev": true }, "async": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", - "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==" }, "async-cache": { "version": "1.1.0", @@ -1510,15 +1521,14 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, "cli-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz", - "integrity": "sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.1.tgz", + "integrity": "sha512-eBbxZF6fqPUNnf7CLAFOersUnyYzv83tHFLSlts+OAHsNendaqv2tHCq+/MO+b3Y+9JeoUlIvobyxG/Z8GNeOg==", "requires": { - "ansi-regex": "^2.1.1", "d": "^1.0.1", - "es5-ext": "^0.10.51", + "es5-ext": "^0.10.53", "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", + "memoizee": "^0.4.15", "timers-ext": "^0.1.7" } }, @@ -2219,6 +2229,104 @@ "cli-color": "^2.0.0", "eslint": "^7.25.0", "yargs": "^17.0.1" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } } }, "err-code": { @@ -2352,37 +2460,36 @@ "dev": true }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.1.0.tgz", + "integrity": "sha512-JZvNneArGSUsluHWJ8g8MMs3CfIEzwaLx9KyH4tZ2i+R2/rPWzL8c0zg3rHdwYVpN/1sB9gqnjHwz9HoeJpGHw==", "dev": true, "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", + "@eslint/eslintrc": "^1.0.3", + "@humanwhocodes/config-array": "^0.6.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", + "eslint-scope": "^6.0.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", + "glob-parent": "^6.0.1", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2390,15 +2497,60 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^3.1.0", + "regexpp": "^3.2.0", "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { + "@eslint/eslintrc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.3.tgz", + "integrity": "sha512-DHI1wDPoKCBPoLZA3qDR91+3te/wDSc1YhKg3jR8NxKKRJq2hwHwcWv31cSwSYvIBrmbENoYMWcenW8uproQqg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.0.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", + "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "acorn": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2414,31 +2566,51 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "eslint-scope": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", + "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" } }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "espree": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", + "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", "dev": true, "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "acorn": "^8.5.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.0.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + }, + "dependencies": { + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + } } } } @@ -2460,12 +2632,20 @@ "dev": true, "requires": { "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } } }, "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true }, "espree": { @@ -2831,9 +3011,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", + "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" }, "foreground-child": { "version": "2.0.0", @@ -3068,9 +3248,9 @@ } }, "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -3430,9 +3610,9 @@ } }, "ioredis": { - "version": "4.27.10", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.10.tgz", - "integrity": "sha512-BtV2mEoZlhnW0EyxuK49V5iutLeZeJAYi/+Fuc4Q6DpDjq0cGMLODdS/+Kb5CHpT7v3YT6SK0vgJF6y0Ls4+Bg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz", + "integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==", "requires": { "cluster-key-slot": "^1.1.0", "debug": "^4.3.1", @@ -4210,9 +4390,9 @@ } }, "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.1.0.tgz", + "integrity": "sha512-eNc10JP6ezXp/qxXZlKS4OHAKNae3je9LUkjmXPDEa+Iidlz0n4nFi/9LT+GOgcayMWhykLoISN+v0THeOiWQQ==" }, "lower-case": { "version": "1.1.4", @@ -4454,9 +4634,9 @@ } }, "mocha": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.2.tgz", - "integrity": "sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", @@ -4688,9 +4868,9 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, "nanoid": { - "version": "3.1.29", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.29.tgz", - "integrity": "sha512-dW2pUSGZ8ZnCFIlBIA31SV8huOGCHb6OwzVCc7A69rb/a+SgPBwfmLvK5TKQ3INPbRkcI8a/Owo0XbiTNH19wg==" + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" }, "natural-compare": { "version": "1.4.0", @@ -5232,9 +5412,9 @@ } }, "openapi-enforcer": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/openapi-enforcer/-/openapi-enforcer-1.15.4.tgz", - "integrity": "sha512-CEBdnAXANDvqSX5EDFoKOicb4irZ0Jy9B+Tys5PLyPdEdp1A4+Wobv3WJjhA0DBadBZ2liriIubf0zt9V0iO8A==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/openapi-enforcer/-/openapi-enforcer-1.15.5.tgz", + "integrity": "sha512-lHU5Qhj6i/4LinbVqOLpDwukVQkn+MUlS5exa71fQcJ36ona17lgI4csA8+9WSiVr0FeALUCWi87Glsseb1Jzw==", "requires": { "axios": "^0.21.1", "json-schema-ref-parser": "^6.1.0", @@ -5584,6 +5764,13 @@ "@types/long": "^4.0.1", "@types/node": ">=13.7.0", "long": "^4.0.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } } }, "pseudomap": { @@ -6718,6 +6905,31 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, "smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", @@ -7049,23 +7261,23 @@ } }, "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", + "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", "dev": true, "requires": { "ajv": "^8.0.1", "lodash.clonedeep": "^4.5.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -7074,10 +7286,10 @@ "uri-js": "^4.2.2" } }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -7092,26 +7304,24 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "ansi-regex": "^5.0.1" } } } @@ -7312,12 +7522,12 @@ "integrity": "sha512-Ck+9CKS+lZ277gvUSbrV99ZKLlgZCEANUIZNmOsf8749LQnJ6vYk92dWwaVdCOj4jdEc1TebC8Fu3i2aK0H6cA==" }, "ts-node": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", - "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "requires": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -7418,9 +7628,9 @@ } }, "typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, "uWebSockets.js": { diff --git a/package.json b/package.json index ba987da86f..d4e441cb6e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kuzzle", "author": "The Kuzzle Team ", - "version": "2.14.10", + "version": "2.14.11", "description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.", "bin": { "kuzzle": "bin/start-kuzzle-server" @@ -39,10 +39,10 @@ "lib": "lib" }, "dependencies": { - "@elastic/elasticsearch": "7.13.0", + "@elastic/elasticsearch": "7.15.0", "aedes": "^0.46.1", "bluebird": "^3.7.2", - "cli-color": "^2.0.0", + "cli-color": "^2.0.1", "cookie": "^0.4.1", "debug": "^4.3.2", "denque": "^2.0.1", @@ -50,7 +50,7 @@ "dumpme": "^1.0.3", "eventemitter3": "^4.0.7", "inquirer": "^8.2.0", - "ioredis": "^4.27.10", + "ioredis": "^4.28.0", "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "json2yaml": "^1.1.0", @@ -61,12 +61,12 @@ "kuzzle-sdk": "7.7.6", "kuzzle-vault": "^2.0.4", "lodash": "4.17.21", - "long": "^4.0.0", + "long": "^5.1.0", "moment": "^2.29.1", "ms": "^2.1.3", "murmurhash-native": "^3.5.0", - "nanoid": "^3.1.29", - "openapi-enforcer": "^1.15.4", + "nanoid": "^3.1.30", + "openapi-enforcer": "^1.15.5", "passport": "^0.5.0", "protobufjs": "~6.11.2", "rc": "1.2.8", @@ -88,16 +88,16 @@ "url": "git://github.com/kuzzleio/kuzzle.git" }, "devDependencies": { - "@types/lodash": "^4.14.175", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "async": "^3.2.1", + "@types/lodash": "^4.14.176", + "@typescript-eslint/eslint-plugin": "^5.2.0", + "@typescript-eslint/parser": "^5.2.0", + "async": "^3.2.2", "chokidar": "^3.5.2", "codecov": "^3.8.3", "cucumber": "^6.0.5", "ergol": "^1.0.1", - "eslint": "^7.32.0", - "mocha": "^9.1.2", + "eslint": "^8.1.0", + "mocha": "^9.1.3", "mock-require": "^3.0.3", "mqtt": "^4.2.8", "nyc": "^15.1.0", @@ -108,8 +108,8 @@ "should-sinon": "0.0.6", "sinon": "^11.1.2", "strip-json-comments": "3.1.1", - "ts-node": "^10.2.1", - "typescript": "^4.4.3", + "ts-node": "^10.4.0", + "typescript": "^4.4.4", "yaml": "^1.10.2" }, "engines": {