mirror of
https://github.com/standardnotes/server
synced 2026-04-26 21:01:31 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5be40fa99c | |||
| bc909dd3aa | |||
| 3110c20596 | |||
| 69a9c5555b | |||
| cca6c7f65e | |||
| 7a8a5fcfdf | |||
| 845d310af9 | |||
| d61e6f338e | |||
| f2ddbc82d0 | |||
| 8708b10dfc | |||
| 165ee50dc8 | |||
| 6cc4ef90db |
@@ -0,0 +1,170 @@
|
||||
name: Files Server Dev
|
||||
|
||||
concurrency:
|
||||
group: files_dev_environment
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '@standardnotes/files-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
|
||||
- '@standardnotes/files-server@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn lint:files
|
||||
- run: yarn test:files
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
- name: Build, tag, and push image to Amazon ECR
|
||||
id: build-image
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: files
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/files -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
|
||||
publish-docker-hub:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: standardnotes/files
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
tags: "dev,${{ github.sha }}"
|
||||
|
||||
deploy-web:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: files-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: files-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-worker-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: files-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: files-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Web
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_FILES_WEB_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_FILES_WORKER_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
|
||||
notify_discord:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Discord Webhook
|
||||
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
@@ -0,0 +1,170 @@
|
||||
name: Syncing Server Dev
|
||||
|
||||
concurrency:
|
||||
group: syncing_server_dev_environment
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '@standardnotes/syncing-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
|
||||
- '@standardnotes/syncing-server@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn lint:syncing-server
|
||||
- run: yarn test:syncing-server
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
- name: Build, tag, and push image to Amazon ECR
|
||||
id: build-image
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: syncing-server-js
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/syncing-server -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
|
||||
publish-docker-hub:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: standardnotes/syncing-server-js
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
tags: "dev,${{ github.sha }}"
|
||||
|
||||
deploy-web:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: syncing-server-js-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-worker-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: syncing-server-js-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Web
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_SYNCING_SERVER_WEB_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_SYNCING_SERVER_WORKER_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
|
||||
notify_discord:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Discord Webhook
|
||||
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
@@ -15,3 +15,6 @@ newrelic_agent.log
|
||||
!.yarn/unplugged
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
packages/files/uploads/*
|
||||
!packages/files/uploads/.gitkeep
|
||||
|
||||
@@ -24,17 +24,27 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"name": "@standardnotes/auth-server",\
|
||||
"reference": "workspace:packages/auth"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/files-server",\
|
||||
"reference": "workspace:packages/files"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/scheduler-server",\
|
||||
"reference": "workspace:packages/scheduler"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/syncing-server",\
|
||||
"reference": "workspace:packages/syncing-server"\
|
||||
}\
|
||||
],\
|
||||
"enableTopLevelFallback": true,\
|
||||
"ignorePatternData": "(^(?:\\\\.yarn\\\\/sdks(?:\\\\/(?!\\\\.{1,2}(?:\\\\/|$))(?:(?:(?!(?:^|\\\\/)\\\\.{1,2}(?:\\\\/|$)).)*?)|$))$)",\
|
||||
"fallbackExclusionList": [\
|
||||
["@standardnotes/auth-server", ["workspace:packages/auth"]],\
|
||||
["@standardnotes/files-server", ["workspace:packages/files"]],\
|
||||
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
|
||||
["@standardnotes/server-monorepo", ["workspace:."]]\
|
||||
["@standardnotes/server-monorepo", ["workspace:."]],\
|
||||
["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]]\
|
||||
],\
|
||||
"fallbackPool": [\
|
||||
],\
|
||||
@@ -1714,6 +1724,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@newrelic/aws-sdk", [\
|
||||
["npm:3.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-aws-sdk-npm-3.1.0-7c3485a153-5601d90c78.zip/node_modules/@newrelic/aws-sdk/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/aws-sdk", "npm:3.1.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:4.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-aws-sdk-npm-4.1.2-9930120a02-610f6353a7.zip/node_modules/@newrelic/aws-sdk/",\
|
||||
"packageDependencies": [\
|
||||
@@ -1748,9 +1765,29 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:3.1.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@newrelic-aws-sdk-virtual-9720173dde/0/cache/@newrelic-aws-sdk-npm-3.1.0-7c3485a153-5601d90c78.zip/node_modules/@newrelic/aws-sdk/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/aws-sdk", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:3.1.0"],\
|
||||
["@types/newrelic", null],\
|
||||
["newrelic", "npm:7.5.2"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/newrelic",\
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@newrelic/koa", [\
|
||||
["npm:5.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-koa-npm-5.0.0-c9c6a0e1dc-e98d921b96.zip/node_modules/@newrelic/koa/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/koa", "npm:5.0.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:6.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-koa-npm-6.1.2-df0f7c71b5-e269d37b13.zip/node_modules/@newrelic/koa/",\
|
||||
"packageDependencies": [\
|
||||
@@ -1783,9 +1820,33 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:5.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@newrelic-koa-virtual-2873d18af2/0/cache/@newrelic-koa-npm-5.0.0-c9c6a0e1dc-e98d921b96.zip/node_modules/@newrelic/koa/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/koa", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:5.0.0"],\
|
||||
["@types/newrelic", null],\
|
||||
["methods", "npm:1.1.2"],\
|
||||
["newrelic", "npm:7.5.2"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/newrelic",\
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@newrelic/native-metrics", [\
|
||||
["npm:6.0.2", {\
|
||||
"packageLocation": "./.yarn/unplugged/@newrelic-native-metrics-npm-6.0.2-805c5534f5/node_modules/@newrelic/native-metrics/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/native-metrics", "npm:6.0.2"],\
|
||||
["nan", "npm:2.16.0"],\
|
||||
["node-gyp", "npm:9.0.0"],\
|
||||
["semver", "npm:5.7.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:7.0.2", {\
|
||||
"packageLocation": "./.yarn/unplugged/@newrelic-native-metrics-npm-7.0.2-b4dcec08eb/node_modules/@newrelic/native-metrics/",\
|
||||
"packageDependencies": [\
|
||||
@@ -1820,6 +1881,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@newrelic/superagent", [\
|
||||
["npm:4.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-superagent-npm-4.0.0-6cc7e8ec57-5fb257ac05.zip/node_modules/@newrelic/superagent/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/superagent", "npm:4.0.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:5.1.1", {\
|
||||
"packageLocation": "./.yarn/cache/@newrelic-superagent-npm-5.1.1-0d3c1fccf8-b43f7b9bb6.zip/node_modules/@newrelic/superagent/",\
|
||||
"packageDependencies": [\
|
||||
@@ -1852,6 +1920,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:4.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@newrelic-superagent-virtual-05297db2ec/0/cache/@newrelic-superagent-npm-4.0.0-6cc7e8ec57-5fb257ac05.zip/node_modules/@newrelic/superagent/",\
|
||||
"packageDependencies": [\
|
||||
["@newrelic/superagent", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:4.0.0"],\
|
||||
["@types/newrelic", null],\
|
||||
["methods", "npm:1.1.2"],\
|
||||
["newrelic", "npm:7.5.2"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/newrelic",\
|
||||
"newrelic"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@newrelic/winston-enricher", [\
|
||||
@@ -2583,6 +2665,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/config", [\
|
||||
["npm:2.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-config-npm-2.0.1-5f34962133-5284e034f2.zip/node_modules/@standardnotes/config/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/config", "npm:2.0.1"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0"],\
|
||||
["@typescript-eslint/parser", "virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/domain-events", [\
|
||||
["npm:2.32.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-domain-events-npm-2.32.2-73adf7a999-54da5fc885.zip/node_modules/@standardnotes/domain-events/",\
|
||||
@@ -2634,6 +2727,58 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/files-server", [\
|
||||
["workspace:packages/files", {\
|
||||
"packageLocation": "./packages/files/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/files-server", "workspace:packages/files"],\
|
||||
["@newrelic/native-metrics", "npm:7.0.2"],\
|
||||
["@sentry/node", "npm:6.19.7"],\
|
||||
["@standardnotes/auth", "npm:3.19.3"],\
|
||||
["@standardnotes/common", "npm:1.23.0"],\
|
||||
["@standardnotes/config", "npm:2.0.1"],\
|
||||
["@standardnotes/domain-events", "npm:2.32.2"],\
|
||||
["@standardnotes/domain-events-infra", "npm:1.5.2"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.9.0"],\
|
||||
["@standardnotes/sncrypto-node", "npm:1.8.3"],\
|
||||
["@standardnotes/time", "npm:1.7.0"],\
|
||||
["@types/connect-busboy", "npm:1.0.0"],\
|
||||
["@types/cors", "npm:2.8.12"],\
|
||||
["@types/express", "npm:4.17.13"],\
|
||||
["@types/ioredis", "npm:4.28.10"],\
|
||||
["@types/jest", "npm:28.1.3"],\
|
||||
["@types/jsonwebtoken", "npm:8.5.8"],\
|
||||
["@types/newrelic", "npm:7.0.3"],\
|
||||
["@types/prettyjson", "npm:0.0.29"],\
|
||||
["@types/uuid", "npm:8.3.4"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
|
||||
["aws-sdk", "npm:2.1158.0"],\
|
||||
["connect-busboy", "npm:1.0.0"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dayjs", "npm:1.11.3"],\
|
||||
["dotenv", "npm:8.6.0"],\
|
||||
["eslint", "npm:8.18.0"],\
|
||||
["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
|
||||
["express", "npm:4.18.1"],\
|
||||
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
||||
["helmet", "npm:4.6.0"],\
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.0.6"],\
|
||||
["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
|
||||
["jsonwebtoken", "npm:8.5.1"],\
|
||||
["newrelic", "npm:7.5.2"],\
|
||||
["nodemon", "npm:2.0.16"],\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
|
||||
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.1"],\
|
||||
["uuid", "npm:8.3.2"],\
|
||||
["winston", "npm:3.7.2"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/models", [\
|
||||
["npm:1.11.10", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.11.10-e4b5e4717d-d69fd3940e.zip/node_modules/@standardnotes/models/",\
|
||||
@@ -2646,6 +2791,18 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/payloads", [\
|
||||
["npm:1.5.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-payloads-npm-1.5.1-45dffe2f5c-65c28421e7.zip/node_modules/@standardnotes/payloads/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/payloads", "npm:1.5.1"],\
|
||||
["@standardnotes/common", "npm:1.23.0"],\
|
||||
["@standardnotes/features", "npm:1.45.5"],\
|
||||
["@standardnotes/utils", "npm:1.6.11"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/responses", [\
|
||||
["npm:1.6.36", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.6.36-d245f42de1-bb78a2cefa.zip/node_modules/@standardnotes/responses/",\
|
||||
@@ -2765,6 +2922,62 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/syncing-server", [\
|
||||
["workspace:packages/syncing-server", {\
|
||||
"packageLocation": "./packages/syncing-server/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||
["@newrelic/native-metrics", "npm:7.0.2"],\
|
||||
["@newrelic/winston-enricher", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2"],\
|
||||
["@sentry/node", "npm:6.19.7"],\
|
||||
["@standardnotes/analytics", "npm:1.6.0"],\
|
||||
["@standardnotes/auth", "npm:3.19.3"],\
|
||||
["@standardnotes/common", "npm:1.23.0"],\
|
||||
["@standardnotes/domain-events", "npm:2.32.2"],\
|
||||
["@standardnotes/domain-events-infra", "npm:1.5.2"],\
|
||||
["@standardnotes/payloads", "npm:1.5.1"],\
|
||||
["@standardnotes/responses", "npm:1.6.36"],\
|
||||
["@standardnotes/settings", "npm:1.14.3"],\
|
||||
["@standardnotes/time", "npm:1.7.0"],\
|
||||
["@types/cors", "npm:2.8.12"],\
|
||||
["@types/dotenv", "npm:8.2.0"],\
|
||||
["@types/express", "npm:4.17.13"],\
|
||||
["@types/inversify-express-utils", "npm:2.0.0"],\
|
||||
["@types/ioredis", "npm:4.28.10"],\
|
||||
["@types/jest", "npm:28.1.3"],\
|
||||
["@types/jsonwebtoken", "npm:8.5.8"],\
|
||||
["@types/newrelic", "npm:7.0.3"],\
|
||||
["@types/prettyjson", "npm:0.0.29"],\
|
||||
["@types/ua-parser-js", "npm:0.7.36"],\
|
||||
["@types/uuid", "npm:8.3.4"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
|
||||
["aws-sdk", "npm:2.1159.0"],\
|
||||
["axios", "npm:0.24.0"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dotenv", "npm:8.2.0"],\
|
||||
["eslint", "npm:8.18.0"],\
|
||||
["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
|
||||
["express", "npm:4.17.1"],\
|
||||
["helmet", "npm:4.3.1"],\
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.0.6"],\
|
||||
["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
|
||||
["jsonwebtoken", "npm:8.5.1"],\
|
||||
["mysql2", "npm:2.3.3"],\
|
||||
["newrelic", "npm:8.6.0"],\
|
||||
["nodemon", "npm:2.0.7"],\
|
||||
["prettyjson", "npm:1.2.1"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.6"],\
|
||||
["ua-parser-js", "npm:1.0.2"],\
|
||||
["uuid", "npm:8.3.2"],\
|
||||
["winston", "npm:3.3.3"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/time", [\
|
||||
["npm:1.7.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-time-npm-1.7.0-fa2b65b191-51b168d8a5.zip/node_modules/@standardnotes/time/",\
|
||||
@@ -2916,6 +3129,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/busboy", [\
|
||||
["npm:1.5.0", {\
|
||||
"packageLocation": "./.yarn/cache/@types-busboy-npm-1.5.0-0e24e7f08d-ffa7bf25c0.zip/node_modules/@types/busboy/",\
|
||||
"packageDependencies": [\
|
||||
["@types/busboy", "npm:1.5.0"],\
|
||||
["@types/node", "npm:18.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/connect", [\
|
||||
["npm:3.4.35", {\
|
||||
"packageLocation": "./.yarn/cache/@types-connect-npm-3.4.35-7337eee0a3-fe81351470.zip/node_modules/@types/connect/",\
|
||||
@@ -2926,6 +3149,18 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/connect-busboy", [\
|
||||
["npm:1.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/@types-connect-busboy-npm-1.0.0-fca702448d-ccbf7bc42d.zip/node_modules/@types/connect-busboy/",\
|
||||
"packageDependencies": [\
|
||||
["@types/connect-busboy", "npm:1.0.0"],\
|
||||
["@types/busboy", "npm:1.5.0"],\
|
||||
["@types/express", "npm:4.17.13"],\
|
||||
["@types/node", "npm:18.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/cors", [\
|
||||
["npm:2.8.12", {\
|
||||
"packageLocation": "./.yarn/cache/@types-cors-npm-2.8.12-ff52e8e514-8c45f112c7.zip/node_modules/@types/cors/",\
|
||||
@@ -2935,6 +3170,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/dotenv", [\
|
||||
["npm:8.2.0", {\
|
||||
"packageLocation": "./.yarn/cache/@types-dotenv-npm-8.2.0-f4d0e3d65b-a1f524da7d.zip/node_modules/@types/dotenv/",\
|
||||
"packageDependencies": [\
|
||||
["@types/dotenv", "npm:8.2.0"],\
|
||||
["dotenv", "npm:16.0.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/express", [\
|
||||
["npm:4.17.13", {\
|
||||
"packageLocation": "./.yarn/cache/@types-express-npm-4.17.13-0e12fe9c24-12a2a0e6c4.zip/node_modules/@types/express/",\
|
||||
@@ -2970,6 +3215,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/inversify-express-utils", [\
|
||||
["npm:2.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/@types-inversify-express-utils-npm-2.0.0-e78182955d-848aa75f0f.zip/node_modules/@types/inversify-express-utils/",\
|
||||
"packageDependencies": [\
|
||||
["@types/inversify-express-utils", "npm:2.0.0"],\
|
||||
["inversify-express-utils", "npm:6.4.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/ioredis", [\
|
||||
["npm:4.28.10", {\
|
||||
"packageLocation": "./.yarn/cache/@types-ioredis-npm-4.28.10-4bdbe26a79-0f2788cf25.zip/node_modules/@types/ioredis/",\
|
||||
@@ -3038,6 +3293,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/jsonwebtoken", [\
|
||||
["npm:8.5.8", {\
|
||||
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-8.5.8-798e14708c-56738a918c.zip/node_modules/@types/jsonwebtoken/",\
|
||||
"packageDependencies": [\
|
||||
["@types/jsonwebtoken", "npm:8.5.8"],\
|
||||
["@types/node", "npm:18.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/keyv", [\
|
||||
["npm:3.1.4", {\
|
||||
"packageLocation": "./.yarn/cache/@types-keyv-npm-3.1.4-a8082ea56b-e009a2bfb5.zip/node_modules/@types/keyv/",\
|
||||
@@ -3234,6 +3499,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/eslint-plugin", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-eslint-plugin-npm-4.33.0-b5d1be4879-d74855d0a5.zip/node_modules/@typescript-eslint/eslint-plugin/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/eslint-plugin", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-eslint-plugin-npm-5.29.0-d7e482bb3e-b1022a640f.zip/node_modules/@typescript-eslint/eslint-plugin/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3300,9 +3572,73 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-56ea46a0fe/0/cache/@typescript-eslint-eslint-plugin-npm-4.33.0-b5d1be4879-d74855d0a5.zip/node_modules/@typescript-eslint/eslint-plugin/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0"],\
|
||||
["@types/eslint", null],\
|
||||
["@types/typescript", null],\
|
||||
["@types/typescript-eslint__parser", null],\
|
||||
["@typescript-eslint/experimental-utils", "virtual:56ea46a0fe17b3df61f8f63a15a082b4da2a385d6e774395a132d9a90b9ce8a1ea4c8896dbc69243dfdd2325db5c22821bfa276cdbaddb1ec4f5f1efddea3e20#npm:4.33.0"],\
|
||||
["@typescript-eslint/parser", "virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0"],\
|
||||
["@typescript-eslint/scope-manager", "npm:4.33.0"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||
["eslint", null],\
|
||||
["functional-red-black-tree", "npm:1.0.1"],\
|
||||
["ignore", "npm:5.2.0"],\
|
||||
["regexpp", "npm:3.2.0"],\
|
||||
["semver", "npm:7.3.7"],\
|
||||
["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
|
||||
["typescript", null]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/eslint",\
|
||||
"@types/typescript-eslint__parser",\
|
||||
"@types/typescript",\
|
||||
"@typescript-eslint/parser",\
|
||||
"eslint",\
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/experimental-utils", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-experimental-utils-npm-4.33.0-f10f287886-f859800ada.zip/node_modules/@typescript-eslint/experimental-utils/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/experimental-utils", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:56ea46a0fe17b3df61f8f63a15a082b4da2a385d6e774395a132d9a90b9ce8a1ea4c8896dbc69243dfdd2325db5c22821bfa276cdbaddb1ec4f5f1efddea3e20#npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-experimental-utils-virtual-3b1d487b65/0/cache/@typescript-eslint-experimental-utils-npm-4.33.0-f10f287886-f859800ada.zip/node_modules/@typescript-eslint/experimental-utils/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/experimental-utils", "virtual:56ea46a0fe17b3df61f8f63a15a082b4da2a385d6e774395a132d9a90b9ce8a1ea4c8896dbc69243dfdd2325db5c22821bfa276cdbaddb1ec4f5f1efddea3e20#npm:4.33.0"],\
|
||||
["@types/eslint", null],\
|
||||
["@types/json-schema", "npm:7.0.11"],\
|
||||
["@typescript-eslint/scope-manager", "npm:4.33.0"],\
|
||||
["@typescript-eslint/types", "npm:4.33.0"],\
|
||||
["@typescript-eslint/typescript-estree", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:4.33.0"],\
|
||||
["eslint", null],\
|
||||
["eslint-scope", "npm:5.1.1"],\
|
||||
["eslint-utils", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:3.0.0"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/eslint",\
|
||||
"eslint"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/parser", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip/node_modules/@typescript-eslint/parser/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/parser", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-parser-npm-5.29.0-491a7f9690-7805796638.zip/node_modules/@typescript-eslint/parser/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3310,6 +3646,27 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-parser-virtual-636bc1eaeb/0/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip/node_modules/@typescript-eslint/parser/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/parser", "virtual:5f34962133311be1f14bb34c0982459488dcec82f934766f3a869be491e21f1b6d2b4254097b1e3671b0c43a4da48e7dafc6c0e314719fdafff3dd267b51be35#npm:4.33.0"],\
|
||||
["@types/eslint", null],\
|
||||
["@types/typescript", null],\
|
||||
["@typescript-eslint/scope-manager", "npm:4.33.0"],\
|
||||
["@typescript-eslint/types", "npm:4.33.0"],\
|
||||
["@typescript-eslint/typescript-estree", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:4.33.0"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||
["eslint", null],\
|
||||
["typescript", null]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/eslint",\
|
||||
"@types/typescript",\
|
||||
"eslint",\
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:8859b278716fedf3e7458b5628625f7e35678c418626878559a0b816445001b7e24c55546f4677ba4c20b521aa0cf52cc33ac07deff171e383ada6eeab69933f#npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-parser-virtual-451c3112c8/0/cache/@typescript-eslint-parser-npm-5.29.0-491a7f9690-7805796638.zip/node_modules/@typescript-eslint/parser/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3333,6 +3690,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/scope-manager", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-scope-manager-npm-4.33.0-28014c179d-9a25fb7ba7.zip/node_modules/@typescript-eslint/scope-manager/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/scope-manager", "npm:4.33.0"],\
|
||||
["@typescript-eslint/types", "npm:4.33.0"],\
|
||||
["@typescript-eslint/visitor-keys", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-scope-manager-npm-5.29.0-03a6d28ed2-540642bef9.zip/node_modules/@typescript-eslint/scope-manager/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3393,6 +3759,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/types", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-types-npm-4.33.0-9e9b956afa-3baae1ca35.zip/node_modules/@typescript-eslint/types/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/types", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-types-npm-5.29.0-2bea7f0c9b-982ecdd691.zip/node_modules/@typescript-eslint/types/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3402,6 +3775,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/typescript-estree", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-typescript-estree-npm-4.33.0-b6b79c10d0-2566984390.zip/node_modules/@typescript-eslint/typescript-estree/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/typescript-estree", "npm:4.33.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-typescript-estree-npm-5.29.0-f23de2ab5c-b91107a9fc.zip/node_modules/@typescript-eslint/typescript-estree/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3409,6 +3789,26 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-ec62c0bda6/0/cache/@typescript-eslint-typescript-estree-npm-4.33.0-b6b79c10d0-2566984390.zip/node_modules/@typescript-eslint/typescript-estree/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/typescript-estree", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:4.33.0"],\
|
||||
["@types/typescript", null],\
|
||||
["@typescript-eslint/types", "npm:4.33.0"],\
|
||||
["@typescript-eslint/visitor-keys", "npm:4.33.0"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||
["globby", "npm:11.1.0"],\
|
||||
["is-glob", "npm:4.0.3"],\
|
||||
["semver", "npm:7.3.7"],\
|
||||
["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
|
||||
["typescript", null]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/typescript",\
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:451c3112c8ebc24954be5135e65b7a370326adef1f6bb7aaca3ef2abc346ee165ef171b721a7207548ab6a19505983a443fc07f41cc553d4c9c5cddd04862b50#npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-7e6283c452/0/cache/@typescript-eslint-typescript-estree-npm-5.29.0-f23de2ab5c-b91107a9fc.zip/node_modules/@typescript-eslint/typescript-estree/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3488,7 +3888,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@typescript-eslint/typescript-estree", "virtual:4ec458b53cfcb38d153394fe4d0300908a12ce721ae6026f1e2d7bbe8409ed98079b29d9688a9eb93463ace5dbaac7d454b12c4582b1cd0b1d8210588cf0cb1c#npm:5.29.0"],\
|
||||
["eslint", null],\
|
||||
["eslint-scope", "npm:5.1.1"],\
|
||||
["eslint-utils", "virtual:9b3cc2e468ebc82b101c5313a9afa58bf6c93ab196f710844b44e247fc606cd503de5b07cdee6c592a841949dbe5daecc3f46a7ae43ee5bbf7fe046d76ec335e#npm:3.0.0"]\
|
||||
["eslint-utils", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:3.0.0"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/eslint",\
|
||||
@@ -3498,6 +3898,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@typescript-eslint/visitor-keys", [\
|
||||
["npm:4.33.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-visitor-keys-npm-4.33.0-8b7e72a3c9-59953e474a.zip/node_modules/@typescript-eslint/visitor-keys/",\
|
||||
"packageDependencies": [\
|
||||
["@typescript-eslint/visitor-keys", "npm:4.33.0"],\
|
||||
["@typescript-eslint/types", "npm:4.33.0"],\
|
||||
["eslint-visitor-keys", "npm:2.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.29.0", {\
|
||||
"packageLocation": "./.yarn/cache/@typescript-eslint-visitor-keys-npm-5.29.0-fe23f55f18-15f228ad9f.zip/node_modules/@typescript-eslint/visitor-keys/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3598,6 +4007,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["agent-base", [\
|
||||
["npm:5.1.1", {\
|
||||
"packageLocation": "./.yarn/cache/agent-base-npm-5.1.1-d451a4ad62-61ae789f30.zip/node_modules/agent-base/",\
|
||||
"packageDependencies": [\
|
||||
["agent-base", "npm:5.1.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:6.0.2", {\
|
||||
"packageLocation": "./.yarn/cache/agent-base-npm-6.0.2-428f325a93-f52b6872cc.zip/node_modules/agent-base/",\
|
||||
"packageDependencies": [\
|
||||
@@ -4126,6 +4542,21 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["boxen", [\
|
||||
["npm:4.2.0", {\
|
||||
"packageLocation": "./.yarn/cache/boxen-npm-4.2.0-471e88ddba-ce2b565a2e.zip/node_modules/boxen/",\
|
||||
"packageDependencies": [\
|
||||
["boxen", "npm:4.2.0"],\
|
||||
["ansi-align", "npm:3.0.1"],\
|
||||
["camelcase", "npm:5.3.1"],\
|
||||
["chalk", "npm:3.0.0"],\
|
||||
["cli-boxes", "npm:2.2.1"],\
|
||||
["string-width", "npm:4.2.3"],\
|
||||
["term-size", "npm:2.2.1"],\
|
||||
["type-fest", "npm:0.8.1"],\
|
||||
["widest-line", "npm:3.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/boxen-npm-5.1.2-364ee34f2f-82d03e42a7.zip/node_modules/boxen/",\
|
||||
"packageDependencies": [\
|
||||
@@ -4263,6 +4694,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["busboy", [\
|
||||
["npm:1.6.0", {\
|
||||
"packageLocation": "./.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip/node_modules/busboy/",\
|
||||
"packageDependencies": [\
|
||||
["busboy", "npm:1.6.0"],\
|
||||
["streamsearch", "npm:1.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["byte-size", [\
|
||||
["npm:7.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/byte-size-npm-7.0.1-cda9f76d28-6791663a6d.zip/node_modules/byte-size/",\
|
||||
@@ -4424,6 +4865,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/chalk-npm-3.0.0-e813208025-8e3ddf3981.zip/node_modules/chalk/",\
|
||||
"packageDependencies": [\
|
||||
["chalk", "npm:3.0.0"],\
|
||||
["ansi-styles", "npm:4.3.0"],\
|
||||
["supports-color", "npm:7.2.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:4.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/chalk-npm-4.1.2-ba8b67ab80-fe75c9d5c7.zip/node_modules/chalk/",\
|
||||
"packageDependencies": [\
|
||||
@@ -4789,6 +5239,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["connect-busboy", [\
|
||||
["npm:1.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip/node_modules/connect-busboy/",\
|
||||
"packageDependencies": [\
|
||||
["connect-busboy", "npm:1.0.0"],\
|
||||
["busboy", "npm:1.6.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["console-control-strings", [\
|
||||
["npm:1.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/console-control-strings-npm-1.1.0-e3160e5275-8755d76787.zip/node_modules/console-control-strings/",\
|
||||
@@ -5441,6 +5901,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["dotenv", "npm:8.2.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:8.6.0", {\
|
||||
"packageLocation": "./.yarn/cache/dotenv-npm-8.6.0-2ce3e9f7bb-38e902c80b.zip/node_modules/dotenv/",\
|
||||
"packageDependencies": [\
|
||||
["dotenv", "npm:8.6.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["duplexer", [\
|
||||
@@ -5779,12 +6246,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:3b3bfb190f25ed01591b1d51c8e6a15e818ab97d9cabea5c63912afc819a8f6e3ad395aaf338cd170314411b04e35eec5c8cff33dfa644476d292dcf2c5354d1#npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/eslint-utils-virtual-c2e00a0f83/0/cache/eslint-utils-npm-3.0.0-630b3a4013-0668fe02f5.zip/node_modules/eslint-utils/",\
|
||||
["virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/eslint-utils-virtual-19087eaf4f/0/cache/eslint-utils-npm-3.0.0-630b3a4013-0668fe02f5.zip/node_modules/eslint-utils/",\
|
||||
"packageDependencies": [\
|
||||
["eslint-utils", "virtual:3b3bfb190f25ed01591b1d51c8e6a15e818ab97d9cabea5c63912afc819a8f6e3ad395aaf338cd170314411b04e35eec5c8cff33dfa644476d292dcf2c5354d1#npm:3.0.0"],\
|
||||
["eslint-utils", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:3.0.0"],\
|
||||
["@types/eslint", null],\
|
||||
["eslint", "npm:8.18.0"],\
|
||||
["eslint", null],\
|
||||
["eslint-visitor-keys", "npm:2.1.0"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
@@ -5793,12 +6260,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:9b3cc2e468ebc82b101c5313a9afa58bf6c93ab196f710844b44e247fc606cd503de5b07cdee6c592a841949dbe5daecc3f46a7ae43ee5bbf7fe046d76ec335e#npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/eslint-utils-virtual-d9a84e87ea/0/cache/eslint-utils-npm-3.0.0-630b3a4013-0668fe02f5.zip/node_modules/eslint-utils/",\
|
||||
["virtual:3b3bfb190f25ed01591b1d51c8e6a15e818ab97d9cabea5c63912afc819a8f6e3ad395aaf338cd170314411b04e35eec5c8cff33dfa644476d292dcf2c5354d1#npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/eslint-utils-virtual-c2e00a0f83/0/cache/eslint-utils-npm-3.0.0-630b3a4013-0668fe02f5.zip/node_modules/eslint-utils/",\
|
||||
"packageDependencies": [\
|
||||
["eslint-utils", "virtual:9b3cc2e468ebc82b101c5313a9afa58bf6c93ab196f710844b44e247fc606cd503de5b07cdee6c592a841949dbe5daecc3f46a7ae43ee5bbf7fe046d76ec335e#npm:3.0.0"],\
|
||||
["eslint-utils", "virtual:3b3bfb190f25ed01591b1d51c8e6a15e818ab97d9cabea5c63912afc819a8f6e3ad395aaf338cd170314411b04e35eec5c8cff33dfa644476d292dcf2c5354d1#npm:3.0.0"],\
|
||||
["@types/eslint", null],\
|
||||
["eslint", null],\
|
||||
["eslint", "npm:8.18.0"],\
|
||||
["eslint-visitor-keys", "npm:2.1.0"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
@@ -6035,6 +6502,30 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["express-winston", [\
|
||||
["npm:4.2.0", {\
|
||||
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip/node_modules/express-winston/",\
|
||||
"packageDependencies": [\
|
||||
["express-winston", "npm:4.2.0"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0", {\
|
||||
"packageLocation": "./.yarn/__virtual__/express-winston-virtual-7edb98a399/0/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip/node_modules/express-winston/",\
|
||||
"packageDependencies": [\
|
||||
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
||||
["@types/winston", null],\
|
||||
["chalk", "npm:2.4.2"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["winston", "npm:3.7.2"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/winston",\
|
||||
"winston"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["external-editor", [\
|
||||
["npm:3.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/external-editor-npm-3.1.0-878e7807af-1c2a616a73.zip/node_modules/external-editor/",\
|
||||
@@ -6576,6 +7067,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:2.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/global-dirs-npm-2.1.0-790e02e61c-f80b74032c.zip/node_modules/global-dirs/",\
|
||||
"packageDependencies": [\
|
||||
["global-dirs", "npm:2.1.0"],\
|
||||
["ini", "npm:1.3.7"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:3.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/global-dirs-npm-3.0.0-45faebeb68-953c17cf14.zip/node_modules/global-dirs/",\
|
||||
"packageDependencies": [\
|
||||
@@ -6724,6 +7223,22 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["helmet", [\
|
||||
["npm:4.3.1", {\
|
||||
"packageLocation": "./.yarn/cache/helmet-npm-4.3.1-22cd4b53d2-47f59d8b99.zip/node_modules/helmet/",\
|
||||
"packageDependencies": [\
|
||||
["helmet", "npm:4.3.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:4.6.0", {\
|
||||
"packageLocation": "./.yarn/cache/helmet-npm-4.6.0-f244fd965c-139ad678d1.zip/node_modules/helmet/",\
|
||||
"packageDependencies": [\
|
||||
["helmet", "npm:4.6.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["highlight.js", [\
|
||||
["npm:10.7.3", {\
|
||||
"packageLocation": "./.yarn/cache/highlight.js-npm-10.7.3-247e67d5c0-defeafcd54.zip/node_modules/highlight.js/",\
|
||||
@@ -6846,6 +7361,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["https-proxy-agent", [\
|
||||
["npm:4.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-4.0.0-9021ec873f-19471d5aae.zip/node_modules/https-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
["https-proxy-agent", "npm:4.0.0"],\
|
||||
["agent-base", "npm:5.1.1"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.0.1", {\
|
||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-571fccdf38.zip/node_modules/https-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
@@ -7023,6 +7547,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["ini", [\
|
||||
["npm:1.3.7", {\
|
||||
"packageLocation": "./.yarn/cache/ini-npm-1.3.7-188ee858c0-f8f3801e8e.zip/node_modules/ini/",\
|
||||
"packageDependencies": [\
|
||||
["ini", "npm:1.3.7"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.3.8", {\
|
||||
"packageLocation": "./.yarn/cache/ini-npm-1.3.8-fb5040b4c0-dfd98b0ca3.zip/node_modules/ini/",\
|
||||
"packageDependencies": [\
|
||||
@@ -7236,6 +7767,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["is-installed-globally", [\
|
||||
["npm:0.3.2", {\
|
||||
"packageLocation": "./.yarn/cache/is-installed-globally-npm-0.3.2-a593acf078-7f7489ae30.zip/node_modules/is-installed-globally/",\
|
||||
"packageDependencies": [\
|
||||
["is-installed-globally", "npm:0.3.2"],\
|
||||
["global-dirs", "npm:2.1.0"],\
|
||||
["is-path-inside", "npm:3.0.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:0.4.0", {\
|
||||
"packageLocation": "./.yarn/cache/is-installed-globally-npm-0.4.0-a30dd056c7-3359840d59.zip/node_modules/is-installed-globally/",\
|
||||
"packageDependencies": [\
|
||||
@@ -7265,6 +7805,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["is-npm", [\
|
||||
["npm:4.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/is-npm-npm-4.0.0-86d312340b-c0d1550266.zip/node_modules/is-npm/",\
|
||||
"packageDependencies": [\
|
||||
["is-npm", "npm:4.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/is-npm-npm-5.0.0-2758bcd54b-9baff02b0c.zip/node_modules/is-npm/",\
|
||||
"packageDependencies": [\
|
||||
@@ -9085,6 +9632,26 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["newrelic", [\
|
||||
["npm:7.5.2", {\
|
||||
"packageLocation": "./.yarn/cache/newrelic-npm-7.5.2-b949bcba7c-f6c67dbb7d.zip/node_modules/newrelic/",\
|
||||
"packageDependencies": [\
|
||||
["newrelic", "npm:7.5.2"],\
|
||||
["@grpc/grpc-js", "npm:1.6.7"],\
|
||||
["@grpc/proto-loader", "npm:0.5.6"],\
|
||||
["@newrelic/aws-sdk", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:3.1.0"],\
|
||||
["@newrelic/koa", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:5.0.0"],\
|
||||
["@newrelic/native-metrics", "npm:6.0.2"],\
|
||||
["@newrelic/superagent", "virtual:b949bcba7c7d71e38bb586c0239fa765beb131ea5f64fec66d133f6e38e6e87926b1ba9f91c8df6bcbc8243f33e33f909381ca14cfb301b09e6526205ac71883#npm:4.0.0"],\
|
||||
["@tyriar/fibonacci-heap", "npm:2.0.9"],\
|
||||
["async", "npm:3.2.4"],\
|
||||
["concat-stream", "npm:2.0.0"],\
|
||||
["https-proxy-agent", "npm:4.0.0"],\
|
||||
["json-stringify-safe", "npm:5.0.1"],\
|
||||
["readable-stream", "npm:3.6.0"],\
|
||||
["semver", "npm:5.7.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:8.14.1", {\
|
||||
"packageLocation": "./.yarn/cache/newrelic-npm-8.14.1-b659d4d19c-cd12bb2ac9.zip/node_modules/newrelic/",\
|
||||
"packageDependencies": [\
|
||||
@@ -9240,6 +9807,23 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["update-notifier", "npm:5.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:2.0.7", {\
|
||||
"packageLocation": "./.yarn/unplugged/nodemon-npm-2.0.7-7b95e46511/node_modules/nodemon/",\
|
||||
"packageDependencies": [\
|
||||
["nodemon", "npm:2.0.7"],\
|
||||
["chokidar", "npm:3.5.3"],\
|
||||
["debug", "virtual:f564cd587f82296d3fd6026dfab3e339413babae6e81b9c38de9addd7cd419ff4ad05c2c7d821d4792f5d97254f1f8a10edadcbab7fc3eef777350e5087c47c4#npm:3.2.7"],\
|
||||
["ignore-by-default", "npm:1.0.1"],\
|
||||
["minimatch", "npm:3.1.2"],\
|
||||
["pstree.remy", "npm:1.1.8"],\
|
||||
["semver", "npm:5.7.1"],\
|
||||
["supports-color", "npm:5.5.0"],\
|
||||
["touch", "npm:3.1.0"],\
|
||||
["undefsafe", "npm:2.0.5"],\
|
||||
["update-notifier", "npm:4.1.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["nopt", [\
|
||||
@@ -10039,6 +10623,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["minimist", "npm:1.2.6"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.2.5", {\
|
||||
"packageLocation": "./.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip/node_modules/prettyjson/",\
|
||||
"packageDependencies": [\
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["colors", "npm:1.4.0"],\
|
||||
["minimist", "npm:1.2.6"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["proc-log", [\
|
||||
@@ -11197,6 +11790,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["streamsearch", [\
|
||||
["npm:1.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip/node_modules/streamsearch/",\
|
||||
"packageDependencies": [\
|
||||
["streamsearch", "npm:1.1.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["strict-uri-encode", [\
|
||||
["npm:2.0.0", {\
|
||||
"packageLocation": "./.yarn/cache/strict-uri-encode-npm-2.0.0-1ec3189376-eaac4cf978.zip/node_modules/strict-uri-encode/",\
|
||||
@@ -11390,6 +11992,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["term-size", [\
|
||||
["npm:2.2.1", {\
|
||||
"packageLocation": "./.yarn/unplugged/term-size-npm-2.2.1-77ce7141d0/node_modules/term-size/",\
|
||||
"packageDependencies": [\
|
||||
["term-size", "npm:2.2.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["terminal-link", [\
|
||||
["npm:2.1.1", {\
|
||||
"packageLocation": "./.yarn/cache/terminal-link-npm-2.1.1-de80341758-ce3d2cd3a4.zip/node_modules/terminal-link/",\
|
||||
@@ -11776,6 +12387,42 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.1", {\
|
||||
"packageLocation": "./.yarn/__virtual__/ts-node-virtual-28037b75c9/0/cache/ts-node-npm-10.8.1-24280b0982-7d1aa7aa3a.zip/node_modules/ts-node/",\
|
||||
"packageDependencies": [\
|
||||
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.1"],\
|
||||
["@cspotcode/source-map-support", "npm:0.8.1"],\
|
||||
["@swc/core", null],\
|
||||
["@swc/wasm", null],\
|
||||
["@tsconfig/node10", "npm:1.0.9"],\
|
||||
["@tsconfig/node12", "npm:1.0.11"],\
|
||||
["@tsconfig/node14", "npm:1.0.3"],\
|
||||
["@tsconfig/node16", "npm:1.0.3"],\
|
||||
["@types/node", null],\
|
||||
["@types/swc__core", null],\
|
||||
["@types/swc__wasm", null],\
|
||||
["@types/typescript", null],\
|
||||
["acorn", "npm:8.7.1"],\
|
||||
["acorn-walk", "npm:8.2.0"],\
|
||||
["arg", "npm:4.1.3"],\
|
||||
["create-require", "npm:1.1.1"],\
|
||||
["diff", "npm:4.0.2"],\
|
||||
["make-error", "npm:1.3.6"],\
|
||||
["typescript", null],\
|
||||
["v8-compile-cache-lib", "npm:3.0.1"],\
|
||||
["yn", "npm:3.1.1"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@swc/core",\
|
||||
"@swc/wasm",\
|
||||
"@types/node",\
|
||||
"@types/swc__core",\
|
||||
"@types/swc__wasm",\
|
||||
"@types/typescript",\
|
||||
"typescript"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["tslib", [\
|
||||
@@ -12130,6 +12777,26 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["update-notifier", [\
|
||||
["npm:4.1.3", {\
|
||||
"packageLocation": "./.yarn/cache/update-notifier-npm-4.1.3-837e724aca-67652056e6.zip/node_modules/update-notifier/",\
|
||||
"packageDependencies": [\
|
||||
["update-notifier", "npm:4.1.3"],\
|
||||
["boxen", "npm:4.2.0"],\
|
||||
["chalk", "npm:3.0.0"],\
|
||||
["configstore", "npm:5.0.1"],\
|
||||
["has-yarn", "npm:2.1.0"],\
|
||||
["import-lazy", "npm:2.1.0"],\
|
||||
["is-ci", "npm:2.0.0"],\
|
||||
["is-installed-globally", "npm:0.3.2"],\
|
||||
["is-npm", "npm:4.0.0"],\
|
||||
["is-yarn-global", "npm:0.3.0"],\
|
||||
["latest-version", "npm:5.1.0"],\
|
||||
["pupa", "npm:2.1.1"],\
|
||||
["semver-diff", "npm:3.1.1"],\
|
||||
["xdg-basedir", "npm:4.0.0"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:5.1.0", {\
|
||||
"packageLocation": "./.yarn/cache/update-notifier-npm-5.1.0-6bf595ecee-461e5e5b00.zip/node_modules/update-notifier/",\
|
||||
"packageDependencies": [\
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,17 +14,27 @@
|
||||
"lint": "yarn workspaces foreach -p -j 10 --verbose run lint",
|
||||
"lint:auth": "yarn workspace @standardnotes/auth-server lint",
|
||||
"lint:scheduler": "yarn workspace @standardnotes/scheduler-server lint",
|
||||
"lint:syncing-server": "yarn workspace @standardnotes/syncing-server lint",
|
||||
"lint:files": "yarn workspace @standardnotes/files-server lint",
|
||||
"test": "yarn workspaces foreach -p -j 10 --verbose run test",
|
||||
"test:auth": "yarn workspace @standardnotes/auth-server test",
|
||||
"test:scheduler": "yarn workspace @standardnotes/scheduler-server test",
|
||||
"test:syncing-server": "yarn workspace @standardnotes/syncing-server test",
|
||||
"test:files": "yarn workspace @standardnotes/files-server test",
|
||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||
"setup:env": "yarn workspaces foreach -p --verbose run setup:env",
|
||||
"build": "yarn workspaces foreach -pt -j 10 --verbose run build",
|
||||
"build:auth": "yarn workspace @standardnotes/auth-server build",
|
||||
"build:scheduler": "yarn workspace @standardnotes/scheduler-server build",
|
||||
"build:syncing-server": "yarn workspace @standardnotes/syncing-server build",
|
||||
"build:files": "yarn workspace @standardnotes/files-server build",
|
||||
"start:auth": "yarn workspace @standardnotes/auth-server start",
|
||||
"start:auth-worker": "yarn workspace @standardnotes/auth-server worker",
|
||||
"start:scheduler": "yarn workspace @standardnotes/scheduler-server worker",
|
||||
"start:syncing-server": "yarn workspace @standardnotes/syncing-server start",
|
||||
"start:syncing-server-worker": "yarn workspace @standardnotes/syncing-server worker",
|
||||
"start:files": "yarn workspace @standardnotes/files-server start",
|
||||
"start:files-worker": "yarn workspace @standardnotes/files-server worker",
|
||||
"release:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\""
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.1.0-alpha.5](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.4...@standardnotes/auth-server@1.1.0-alpha.5) (2022-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make DISABLE_USER_REGISTRATION env var optional ([3110c20](https://github.com/standardnotes/auth/commit/3110c20596b52da8a43551432cfef94e68046385))
|
||||
|
||||
# [1.1.0-alpha.4](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.3...@standardnotes/auth-server@1.1.0-alpha.4) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.3](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.2...@standardnotes/auth-server@1.1.0-alpha.3) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.2](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.1...@standardnotes/auth-server@1.1.0-alpha.2) (2022-06-22)
|
||||
|
||||
### Features
|
||||
|
||||
* add syncing-server package ([6cc4ef9](https://github.com/standardnotes/auth/commit/6cc4ef90db9e79d53f9646311d234432ef4cf1e3))
|
||||
|
||||
# [1.1.0-alpha.1](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.0...@standardnotes/auth-server@1.1.0-alpha.1) (2022-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.1.0-alpha.1",
|
||||
"version": "1.1.0-alpha.5",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -357,7 +357,9 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.PSEUDO_KEY_PARAMS_KEY).toConstantValue(env.get('PSEUDO_KEY_PARAMS_KEY'))
|
||||
container.bind(TYPES.EPHEMERAL_SESSION_AGE).toConstantValue(env.get('EPHEMERAL_SESSION_AGE'))
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.DISABLE_USER_REGISTRATION).toConstantValue(env.get('DISABLE_USER_REGISTRATION') === 'true')
|
||||
container
|
||||
.bind(TYPES.DISABLE_USER_REGISTRATION)
|
||||
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
|
||||
container.bind(TYPES.ANALYTICS_ENABLED).toConstantValue(env.get('ANALYTICS_ENABLED', true) === 'true')
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
|
||||
@@ -58,7 +58,7 @@ export const AppDataSource = new DataSource({
|
||||
SubscriptionSetting,
|
||||
AnalyticsEntity,
|
||||
],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH')],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
PORT=3000
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
VALET_TOKEN_SECRET=change-me-!
|
||||
|
||||
MAX_CHUNK_BYTES=1000000
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=Syncing Server JS
|
||||
NEW_RELIC_LICENSE_KEY=
|
||||
NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
|
||||
# (Optional) AWS Setup
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_AWS_REGION=
|
||||
S3_ENDPOINT=
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
# (Optional) File upload path (relative to root directory)
|
||||
FILE_UPLOAD_PATH=
|
||||
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
test-setup.ts
|
||||
data
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.1.0-alpha.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.0...@standardnotes/files-server@1.1.0-alpha.1) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# 1.1.0-alpha.0 (2022-06-22)
|
||||
|
||||
### Features
|
||||
|
||||
* add files server package ([7a8a5fc](https://github.com/standardnotes/files/commit/7a8a5fcfdfe0f9cad51114b43cdae748e297b543))
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client python3 alpine-sdk
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# docker-build plugin copies everything needed for `yarn install` to `manifests` folder.
|
||||
COPY manifests ./
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM node:16.15.1-alpine
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Copy the installed dependencies from the previous stage.
|
||||
COPY --from=builder /workspace ./
|
||||
|
||||
# docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
|
||||
COPY packs ./
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/auth/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-web" ]
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import * as Sentry from '@sentry/node'
|
||||
import * as busboy from 'connect-busboy'
|
||||
|
||||
import '../src/Controller/HealthCheckController'
|
||||
import '../src/Controller/FilesController'
|
||||
|
||||
import * as helmet from 'helmet'
|
||||
import * as cors from 'cors'
|
||||
import { urlencoded, json, raw, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||
import * as winston from 'winston'
|
||||
|
||||
import { InversifyExpressServer } from 'inversify-express-utils'
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
app.use((_request: Request, response: Response, next: NextFunction) => {
|
||||
response.setHeader('X-Files-Version', container.get(TYPES.VERSION))
|
||||
next()
|
||||
})
|
||||
app.use(
|
||||
busboy({
|
||||
highWaterMark: 2 * 1024 * 1024,
|
||||
}),
|
||||
)
|
||||
/* eslint-disable */
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["https: 'self'"],
|
||||
baseUri: ["'self'"],
|
||||
childSrc: ["*", "blob:"],
|
||||
connectSrc: ["*"],
|
||||
fontSrc: ["*", "'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
|
||||
frameSrc: ["*", "blob:"],
|
||||
imgSrc: ["'self'", "*", "data:"],
|
||||
manifestSrc: ["'self'"],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'"]
|
||||
}
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
|
||||
app.use(urlencoded({ extended: true, limit: '50mb' }))
|
||||
app.use(
|
||||
cors({
|
||||
exposedHeaders: ['Content-Range', 'Accept-Ranges'],
|
||||
}),
|
||||
)
|
||||
|
||||
if (env.get('SENTRY_DSN', true)) {
|
||||
Sentry.init({
|
||||
dsn: env.get('SENTRY_DSN'),
|
||||
integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })],
|
||||
tracesSampleRate: 0,
|
||||
})
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler() as RequestHandler)
|
||||
}
|
||||
})
|
||||
|
||||
const logger: winston.Logger = container.get(TYPES.Logger)
|
||||
|
||||
server.setErrorConfig((app) => {
|
||||
if (env.get('SENTRY_DSN', true)) {
|
||||
app.use(Sentry.Handlers.errorHandler() as ErrorRequestHandler)
|
||||
}
|
||||
|
||||
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
|
||||
logger.error(error.stack)
|
||||
|
||||
response.status(500).send({
|
||||
error: {
|
||||
message:
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const serverInstance = server.build()
|
||||
|
||||
serverInstance.listen(env.get('PORT'))
|
||||
|
||||
logger.info(`Server started on port ${process.env.PORT}`)
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
dayjs.extend(utc)
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-local')
|
||||
echo "Starting Web in Local Mode..."
|
||||
yarn workspace @standardnotes/files-server start:local
|
||||
;;
|
||||
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
yarn workspace @standardnotes/files-server start
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
yarn workspace @standardnotes/files-server worker
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,19 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../jest.config');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: [
|
||||
'/Bootstrap/',
|
||||
'HealthCheckController',
|
||||
"/Infra/FS"
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
'./test-setup.ts'
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "test-setup.ts"]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.1.0-alpha.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Standard Notes Files Server",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"repository": "git@github.com:standardnotes/files.git",
|
||||
"authors": [
|
||||
"Karol Sójko <karol@standardnotes.com>"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc --rootDir ./",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/native-metrics": "7.0.2",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@standardnotes/auth": "^3.18.9",
|
||||
"@standardnotes/common": "^1.19.4",
|
||||
"@standardnotes/domain-events": "^2.27.6",
|
||||
"@standardnotes/domain-events-infra": "^1.4.93",
|
||||
"@standardnotes/sncrypto-common": "^1.3.0",
|
||||
"@standardnotes/sncrypto-node": "^1.3.0",
|
||||
"@standardnotes/time": "^1.4.5",
|
||||
"aws-sdk": "^2.1158.0",
|
||||
"connect-busboy": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-winston": "^4.0.5",
|
||||
"helmet": "^4.3.1",
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-express-utils": "^6.4.3",
|
||||
"ioredis": "^5.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"newrelic": "^7.3.1",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettyjson": "^1.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.4.0",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@standardnotes/config": "2.0.1",
|
||||
"@types/connect-busboy": "^1.0.0",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/newrelic": "^7.0.1",
|
||||
"@types/prettyjson": "^0.0.29",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^28.1.1",
|
||||
"nodemon": "^2.0.16",
|
||||
"ts-jest": "^28.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import * as winston from 'winston'
|
||||
import Redis from 'ioredis'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import { Container } from 'inversify'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
import { ValetTokenAuthMiddleware } from '../Controller/ValetTokenAuthMiddleware'
|
||||
import { TokenDecoder, TokenDecoderInterface, ValetTokenData } from '@standardnotes/auth'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import {
|
||||
RedisDomainEventPublisher,
|
||||
RedisDomainEventSubscriberFactory,
|
||||
RedisEventMessageHandler,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { FileDownloaderInterface } from '../Domain/Services/FileDownloaderInterface'
|
||||
import { S3FileDownloader } from '../Infra/S3/S3FileDownloader'
|
||||
import { FileUploaderInterface } from '../Domain/Services/FileUploaderInterface'
|
||||
import { S3FileUploader } from '../Infra/S3/S3FileUploader'
|
||||
import { FSFileDownloader } from '../Infra/FS/FSFileDownloader'
|
||||
import { FSFileUploader } from '../Infra/FS/FSFileUploader'
|
||||
import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { UploadRepositoryInterface } from '../Domain/Upload/UploadRepositoryInterface'
|
||||
import { RedisUploadRepository } from '../Infra/Redis/RedisUploadRepository'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { FileRemoverInterface } from '../Domain/Services/FileRemoverInterface'
|
||||
import { S3FileRemover } from '../Infra/S3/S3FileRemover'
|
||||
import { FSFileRemover } from '../Infra/FS/FSFileRemover'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const logger = this.createLogger({ env })
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
|
||||
container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||
container.bind(TYPES.MAX_CHUNK_BYTES).toConstantValue(+env.get('MAX_CHUNK_BYTES'))
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
container
|
||||
.bind(TYPES.FILE_UPLOAD_PATH)
|
||||
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
|
||||
|
||||
const redisUrl = container.get(TYPES.REDIS_URL) as string
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Redis).toConstantValue(redis)
|
||||
|
||||
if (env.get('AWS_ACCESS_KEY_ID', true)) {
|
||||
AWS.config.credentials = new AWS.EnvironmentCredentials('AWS')
|
||||
}
|
||||
|
||||
if (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true)) {
|
||||
const s3Opts: AWS.S3.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
}
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
s3Opts.region = env.get('S3_AWS_REGION', true)
|
||||
}
|
||||
if (env.get('S3_ENDPOINT', true)) {
|
||||
s3Opts.endpoint = new AWS.Endpoint(env.get('S3_ENDPOINT', true))
|
||||
}
|
||||
const s3Client = new AWS.S3(s3Opts)
|
||||
container.bind<AWS.S3>(TYPES.S3).toConstantValue(s3Client)
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
|
||||
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
|
||||
} else {
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
|
||||
container
|
||||
.bind<FileUploaderInterface>(TYPES.FileUploader)
|
||||
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
|
||||
}
|
||||
|
||||
if (env.get('SNS_AWS_REGION', true)) {
|
||||
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(
|
||||
new AWS.SNS({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
const sqsConfig: AWS.SQS.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SQS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig))
|
||||
}
|
||||
|
||||
// use cases
|
||||
container.bind<UploadFileChunk>(TYPES.UploadFileChunk).to(UploadFileChunk)
|
||||
container.bind<StreamDownloadFile>(TYPES.StreamDownloadFile).to(StreamDownloadFile)
|
||||
container.bind<CreateUploadSession>(TYPES.CreateUploadSession).to(CreateUploadSession)
|
||||
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
|
||||
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
|
||||
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
|
||||
// middleware
|
||||
container.bind<ValetTokenAuthMiddleware>(TYPES.ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
|
||||
|
||||
// services
|
||||
container
|
||||
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.ValetTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
// repositories
|
||||
container.bind<UploadRepositoryInterface>(TYPES.UploadRepository).to(RedisUploadRepository)
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||
} else {
|
||||
container
|
||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||
)
|
||||
}
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
container
|
||||
.bind<SharedSubscriptionInvitationCanceledEventHandler>(TYPES.SharedSubscriptionInvitationCanceledEventHandler)
|
||||
.to(SharedSubscriptionInvitationCanceledEventHandler)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||
[
|
||||
'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
|
||||
container.get(TYPES.SharedSubscriptionInvitationCanceledEventHandler),
|
||||
],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(
|
||||
env.get('NEW_RELIC_ENABLED', true) === 'true'
|
||||
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
|
||||
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new SQSDomainEventSubscriberFactory(
|
||||
container.get(TYPES.SQS),
|
||||
container.get(TYPES.SQS_QUEUE_URL),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(new RedisEventMessageHandler(eventHandlers, container.get(TYPES.Logger)))
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventSubscriberFactory(
|
||||
container.get(TYPES.Redis),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
container.get(TYPES.REDIS_EVENTS_CHANNEL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
createLogger({ env }: { env: Env }): winston.Logger {
|
||||
return winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(winston.format.splat(), winston.format.json()),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
HTTPClient: Symbol.for('HTTPClient'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
S3: Symbol.for('S3'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
|
||||
// use cases
|
||||
UploadFileChunk: Symbol.for('UploadFileChunk'),
|
||||
StreamDownloadFile: Symbol.for('StreamDownloadFile'),
|
||||
CreateUploadSession: Symbol.for('CreateUploadSession'),
|
||||
FinishUploadSession: Symbol.for('FinishUploadSession'),
|
||||
GetFileMetadata: Symbol.for('GetFileMetadata'),
|
||||
RemoveFile: Symbol.for('RemoveFile'),
|
||||
MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
|
||||
|
||||
// services
|
||||
ValetTokenDecoder: Symbol.for('ValetTokenDecoder'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
DomainEventFactory: Symbol.for('DomainEventFactory'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
FileUploader: Symbol.for('FileUploader'),
|
||||
FileDownloader: Symbol.for('FileDownloader'),
|
||||
FileRemover: Symbol.for('FileRemover'),
|
||||
|
||||
// repositories
|
||||
UploadRepository: Symbol.for('UploadRepository'),
|
||||
|
||||
// middleware
|
||||
ValetTokenAuthMiddleware: Symbol.for('ValetTokenAuthMiddleware'),
|
||||
|
||||
// env vars
|
||||
AWS_ACCESS_KEY_ID: Symbol.for('AWS_ACCESS_KEY_ID'),
|
||||
AWS_SECRET_ACCESS_KEY: Symbol.for('AWS_SECRET_ACCESS_KEY'),
|
||||
S3_ENDPOINT: Symbol.for('S3_ENDPOINT'),
|
||||
S3_BUCKET_NAME: Symbol.for('S3_BUCKET_NAME'),
|
||||
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||
VALET_TOKEN_SECRET: Symbol.for('VALET_TOKEN_SECRET'),
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
MAX_CHUNK_BYTES: Symbol.for('MAX_CHUNK_BYTES'),
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
FILE_UPLOAD_PATH: Symbol.for('FILE_UPLOAD_PATH'),
|
||||
|
||||
// Handlers
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
SharedSubscriptionInvitationCanceledEventHandler: Symbol.for('SharedSubscriptionInvitationCanceledEventHandler'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
@@ -0,0 +1,259 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
|
||||
import { Request, Response } from 'express'
|
||||
import { Writable, Readable } from 'stream'
|
||||
import { FilesController } from './FilesController'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
|
||||
describe('FilesController', () => {
|
||||
let uploadFileChunk: UploadFileChunk
|
||||
let createUploadSession: CreateUploadSession
|
||||
let finishUploadSession: FinishUploadSession
|
||||
let streamDownloadFile: StreamDownloadFile
|
||||
let getFileMetadata: GetFileMetadata
|
||||
let removeFile: RemoveFile
|
||||
let request: Request
|
||||
let response: Response
|
||||
let readStream: Readable
|
||||
const maxChunkBytes = 100_000
|
||||
|
||||
const createController = () =>
|
||||
new FilesController(
|
||||
uploadFileChunk,
|
||||
createUploadSession,
|
||||
finishUploadSession,
|
||||
streamDownloadFile,
|
||||
getFileMetadata,
|
||||
removeFile,
|
||||
maxChunkBytes,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
readStream = {} as jest.Mocked<Readable>
|
||||
readStream.pipe = jest.fn().mockReturnValue(new Writable())
|
||||
|
||||
streamDownloadFile = {} as jest.Mocked<StreamDownloadFile>
|
||||
streamDownloadFile.execute = jest.fn().mockReturnValue({ success: true, readStream })
|
||||
|
||||
uploadFileChunk = {} as jest.Mocked<UploadFileChunk>
|
||||
uploadFileChunk.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
createUploadSession = {} as jest.Mocked<CreateUploadSession>
|
||||
createUploadSession.execute = jest.fn().mockReturnValue({ success: true, uploadId: '123' })
|
||||
|
||||
finishUploadSession = {} as jest.Mocked<FinishUploadSession>
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
getFileMetadata = {} as jest.Mocked<GetFileMetadata>
|
||||
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
|
||||
|
||||
removeFile = {} as jest.Mocked<RemoveFile>
|
||||
removeFile.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
request = {
|
||||
body: {},
|
||||
headers: {},
|
||||
} as jest.Mocked<Request>
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<Response>
|
||||
response.locals.userUuid = '1-2-3'
|
||||
response.locals.permittedResources = [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
unencryptedFileSize: 123,
|
||||
},
|
||||
]
|
||||
response.writeHead = jest.fn()
|
||||
})
|
||||
|
||||
it('should return a writable stream upon file download', async () => {
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
const result = (await createController().download(request, response)) as () => Writable
|
||||
|
||||
expect(response.writeHead).toHaveBeenCalledWith(206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': 100000,
|
||||
'Content-Range': 'bytes 0-99999/555555',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
})
|
||||
|
||||
expect(result()).toBeInstanceOf(Writable)
|
||||
})
|
||||
|
||||
it('should return proper byte range on consecutive calls', async () => {
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
;(await createController().download(request, response)) as () => Writable
|
||||
|
||||
request.headers['range'] = 'bytes=100000-'
|
||||
;(await createController().download(request, response)) as () => Writable
|
||||
|
||||
expect(response.writeHead).toHaveBeenNthCalledWith(1, 206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': 100000,
|
||||
'Content-Range': 'bytes 0-99999/555555',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
})
|
||||
|
||||
expect(response.writeHead).toHaveBeenNthCalledWith(2, 206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': 100000,
|
||||
'Content-Range': 'bytes 100000-199999/555555',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a writable stream with custom chunk size', async () => {
|
||||
request.headers['x-chunk-size'] = '50000'
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
const result = (await createController().download(request, response)) as () => Writable
|
||||
|
||||
expect(response.writeHead).toHaveBeenCalledWith(206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': 50000,
|
||||
'Content-Range': 'bytes 0-49999/555555',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
})
|
||||
|
||||
expect(result()).toBeInstanceOf(Writable)
|
||||
})
|
||||
|
||||
it('should default to maximum chunk size if custom chunk size is too large', async () => {
|
||||
request.headers['x-chunk-size'] = '200000'
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
const result = (await createController().download(request, response)) as () => Writable
|
||||
|
||||
expect(response.writeHead).toHaveBeenCalledWith(206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': 100000,
|
||||
'Content-Range': 'bytes 0-99999/555555',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
})
|
||||
|
||||
expect(result()).toBeInstanceOf(Writable)
|
||||
})
|
||||
|
||||
it('should not return a writable stream if bytes range is not provided', async () => {
|
||||
const httpResponse = await createController().download(request, response)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should not return a writable stream if getting file metadata fails', async () => {
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
|
||||
|
||||
const httpResponse = await createController().download(request, response)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should not return a writable stream if creating download stream fails', async () => {
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
streamDownloadFile.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
|
||||
|
||||
const httpResponse = await createController().download(request, response)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should create an upload session', async () => {
|
||||
await createController().startUpload(request, response)
|
||||
|
||||
expect(createUploadSession.execute).toHaveBeenCalledWith({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return bad request if upload session could not be created', async () => {
|
||||
createUploadSession.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().startUpload(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should finish an upload session', async () => {
|
||||
await createController().finishUpload(request, response)
|
||||
|
||||
expect(finishUploadSession.execute).toHaveBeenCalledWith({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return bad request if upload session could not be finished', async () => {
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().finishUpload(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should remove a file', async () => {
|
||||
await createController().remove(request, response)
|
||||
|
||||
expect(removeFile.execute).toHaveBeenCalledWith({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return bad request if file removal could not be completed', async () => {
|
||||
removeFile.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().remove(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should upload a chunk to an upload session', async () => {
|
||||
request.headers['x-chunk-id'] = '2'
|
||||
request.body = Buffer.from([123])
|
||||
|
||||
await createController().uploadChunk(request, response)
|
||||
|
||||
expect(uploadFileChunk.execute).toHaveBeenCalledWith({
|
||||
chunkId: 2,
|
||||
data: Buffer.from([123]),
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return bad request if chunk could not be uploaded', async () => {
|
||||
request.headers['x-chunk-id'] = '2'
|
||||
request.body = Buffer.from([123])
|
||||
uploadFileChunk.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().uploadChunk(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should return bad request if chunk id is missing', async () => {
|
||||
request.body = Buffer.from([123])
|
||||
|
||||
const httpResponse = await createController().uploadChunk(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { Writable } from 'stream'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
|
||||
@controller('/v1/files', TYPES.ValetTokenAuthMiddleware)
|
||||
export class FilesController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.UploadFileChunk) private uploadFileChunk: UploadFileChunk,
|
||||
@inject(TYPES.CreateUploadSession) private createUploadSession: CreateUploadSession,
|
||||
@inject(TYPES.FinishUploadSession) private finishUploadSession: FinishUploadSession,
|
||||
@inject(TYPES.StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
|
||||
@inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata,
|
||||
@inject(TYPES.RemoveFile) private removeFile: RemoveFile,
|
||||
@inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/upload/create-session')
|
||||
async startUpload(
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
const result = await this.createUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return this.json({ success: true, uploadId: result.uploadId })
|
||||
}
|
||||
|
||||
@httpPost('/upload/chunk')
|
||||
async uploadChunk(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
const chunkId = +(request.headers['x-chunk-id'] as string)
|
||||
if (!chunkId) {
|
||||
return this.badRequest('Missing x-chunk-id header in request.')
|
||||
}
|
||||
|
||||
const result = await this.uploadFileChunk.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
chunkId,
|
||||
data: request.body,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'Chunk uploaded successfully' })
|
||||
}
|
||||
|
||||
@httpPost('/upload/close-session')
|
||||
public async finishUpload(
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
const result = await this.finishUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
uploadBytesLimit: response.locals.uploadBytesLimit,
|
||||
uploadBytesUsed: response.locals.uploadBytesUsed,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'File uploaded successfully' })
|
||||
}
|
||||
|
||||
@httpDelete('/')
|
||||
async remove(
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
const result = await this.removeFile.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
regularSubscriptionUuid: response.locals.regularSubscriptionUuid,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'File removed successfully' })
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async download(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
|
||||
const range = request.headers['range']
|
||||
if (!range) {
|
||||
return this.badRequest('File download requires range header to be set.')
|
||||
}
|
||||
|
||||
let chunkSize = +(request.headers['x-chunk-size'] as string)
|
||||
if (!chunkSize || chunkSize > this.maxChunkBytes) {
|
||||
chunkSize = this.maxChunkBytes
|
||||
}
|
||||
|
||||
const fileMetadata = await this.getFileMetadata.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
})
|
||||
|
||||
if (!fileMetadata.success) {
|
||||
return this.badRequest(fileMetadata.message)
|
||||
}
|
||||
|
||||
const startRange = Number(range.replace(/\D/g, ''))
|
||||
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
|
||||
|
||||
const headers = {
|
||||
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': endRange - startRange + 1,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
}
|
||||
|
||||
response.writeHead(206, headers)
|
||||
|
||||
const result = await this.streamDownloadFile.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
startRange,
|
||||
endRange,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return () => result.readStream.pipe(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { HealthCheckController } from './HealthCheckController'
|
||||
|
||||
describe('HealthCheckController', () => {
|
||||
const createController = () => new HealthCheckController()
|
||||
|
||||
it('should return OK', async () => {
|
||||
const response = (await createController().get()) as string
|
||||
expect(response).toEqual('OK')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { controller, httpGet } from 'inversify-express-utils'
|
||||
|
||||
@controller('/healthcheck')
|
||||
export class HealthCheckController {
|
||||
@httpGet('/')
|
||||
public async get(): Promise<string> {
|
||||
return 'OK'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ValetTokenAuthMiddleware } from './ValetTokenAuthMiddleware'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { Logger } from 'winston'
|
||||
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/auth'
|
||||
|
||||
describe('ValetTokenAuthMiddleware', () => {
|
||||
let tokenDecoder: TokenDecoderInterface<ValetTokenData>
|
||||
let request: Request
|
||||
let response: Response
|
||||
let next: NextFunction
|
||||
|
||||
const logger = {
|
||||
debug: jest.fn(),
|
||||
} as unknown as jest.Mocked<Logger>
|
||||
|
||||
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<ValetTokenData>>
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 30,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'write',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
query: {},
|
||||
body: {},
|
||||
} as jest.Mocked<Request>
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<Response>
|
||||
response.status = jest.fn().mockReturnThis()
|
||||
response.send = jest.fn()
|
||||
next = jest.fn()
|
||||
})
|
||||
|
||||
it('should authorize user with a valet token', async () => {
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 30,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'write',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.locals).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
permittedOperation: 'write',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 30,
|
||||
},
|
||||
],
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should authorize user with unlimited upload with a valet token', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 10,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'write',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.locals).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
permittedOperation: 'write',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 10,
|
||||
},
|
||||
],
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not authorize user with no space left for upload', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 21,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'write',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(403)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should authorize user with no space left for upload for download operations', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 21,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'read',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.locals).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
permittedOperation: 'read',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '1-2-3/2-3-4',
|
||||
unencryptedFileSize: 21,
|
||||
},
|
||||
],
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 80,
|
||||
})
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not authorize if request is missing valet token in headers', async () => {
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(401)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not authorize if auth valet token is malformed', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(401)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass the error to next middleware if one occurres', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
|
||||
const error = new Error('Ooops')
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockImplementation(() => {
|
||||
throw error
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).not.toHaveBeenCalled()
|
||||
|
||||
expect(next).toHaveBeenCalledWith(error)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/auth'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class ValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<ValetTokenData>,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const valetToken = request.headers['x-valet-token'] || request.body.valetToken || request.query.valetToken
|
||||
if (!valetToken) {
|
||||
this.logger.debug('ValetTokenAuthMiddleware missing valet token.')
|
||||
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid valet token.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const valetTokenData = this.tokenDecoder.decodeToken(valetToken)
|
||||
|
||||
if (valetTokenData === undefined) {
|
||||
this.logger.debug('ValetTokenAuthMiddleware authentication failure.')
|
||||
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid valet token.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.userHasNoSpaceToUpload(valetTokenData)) {
|
||||
response.status(403).send({
|
||||
error: {
|
||||
tag: 'no-space',
|
||||
message: 'The file you are trying to upload is too big. Please upgrade your subscription',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.userUuid = valetTokenData.userUuid
|
||||
response.locals.permittedResources = valetTokenData.permittedResources
|
||||
response.locals.permittedOperation = valetTokenData.permittedOperation
|
||||
response.locals.uploadBytesUsed = valetTokenData.uploadBytesUsed
|
||||
response.locals.uploadBytesLimit = valetTokenData.uploadBytesLimit
|
||||
response.locals.regularSubscriptionUuid = valetTokenData.regularSubscriptionUuid
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
return next(error)
|
||||
}
|
||||
}
|
||||
|
||||
private userHasNoSpaceToUpload(valetTokenData: ValetTokenData) {
|
||||
if (valetTokenData.permittedOperation !== 'write') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (valetTokenData.uploadBytesLimit === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const remainingUploadSpace = valetTokenData.uploadBytesLimit - valetTokenData.uploadBytesUsed
|
||||
|
||||
let consideredUploadSize = 0
|
||||
for (const resource of valetTokenData.permittedResources) {
|
||||
consideredUploadSize += resource.unencryptedFileSize as number
|
||||
}
|
||||
|
||||
return remainingUploadSpace - consideredUploadSize <= 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { DomainEventFactory } from './DomainEventFactory'
|
||||
|
||||
describe('DomainEventFactory', () => {
|
||||
let timer: TimerInterface
|
||||
|
||||
const createFactory = () => new DomainEventFactory(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a FILE_UPLOADED event', () => {
|
||||
expect(
|
||||
createFactory().createFileUploadedEvent({
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
},
|
||||
type: 'FILE_UPLOADED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a FILE_REMOVED event', () => {
|
||||
expect(
|
||||
createFactory().createFileRemovedEvent({
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
},
|
||||
type: 'FILE_REMOVED',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { FileUploadedEvent, FileRemovedEvent, DomainEventService } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: Uuid
|
||||
}): FileRemovedEvent {
|
||||
return {
|
||||
type: 'FILE_REMOVED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createFileUploadedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): FileUploadedEvent {
|
||||
return {
|
||||
type: 'FILE_UPLOADED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { FileUploadedEvent, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileUploadedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): FileUploadedEvent
|
||||
createFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: Uuid
|
||||
}): FileRemovedEvent
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type RemovedFileDescription = {
|
||||
userUuid: Uuid
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
AccountDeletionRequestedEvent,
|
||||
AccountDeletionRequestedEventPayload,
|
||||
DomainEventPublisherInterface,
|
||||
FileRemovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { RemovedFileDescription } from '../File/RemovedFileDescription'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let markFilesToBeRemoved: MarkFilesToBeRemoved
|
||||
let event: AccountDeletionRequestedEvent
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new AccountDeletionRequestedEventHandler(markFilesToBeRemoved, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
markFilesToBeRemoved = {} as jest.Mocked<MarkFilesToBeRemoved>
|
||||
markFilesToBeRemoved.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
filesRemoved: [{} as jest.Mocked<RemovedFileDescription>],
|
||||
})
|
||||
|
||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
} as jest.Mocked<AccountDeletionRequestedEventPayload>
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileRemovedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileRemovedEvent>)
|
||||
})
|
||||
|
||||
it('should mark files to be remove for user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not mark files to be remove for user if user has no regular subscription', async () => {
|
||||
event.payload.regularSubscriptionUuid = undefined
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).not.toHaveBeenCalled()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not publish events if failed to mark files to be removed', async () => {
|
||||
markFilesToBeRemoved.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
AccountDeletionRequestedEvent,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
@injectable()
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
if (event.payload.regularSubscriptionUuid === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const fileRemoved of response.filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
SharedSubscriptionInvitationCanceledEvent,
|
||||
SharedSubscriptionInvitationCanceledEventPayload,
|
||||
DomainEventPublisherInterface,
|
||||
FileRemovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from './SharedSubscriptionInvitationCanceledEventHandler'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { RemovedFileDescription } from '../File/RemovedFileDescription'
|
||||
|
||||
describe('SharedSubscriptionInvitationCanceledEventHandler', () => {
|
||||
let markFilesToBeRemoved: MarkFilesToBeRemoved
|
||||
let event: SharedSubscriptionInvitationCanceledEvent
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SharedSubscriptionInvitationCanceledEventHandler(markFilesToBeRemoved, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
markFilesToBeRemoved = {} as jest.Mocked<MarkFilesToBeRemoved>
|
||||
markFilesToBeRemoved.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
filesRemoved: [{} as jest.Mocked<RemovedFileDescription>],
|
||||
})
|
||||
|
||||
event = {} as jest.Mocked<SharedSubscriptionInvitationCanceledEvent>
|
||||
event.payload = {
|
||||
inviteeIdentifier: '1-2-3',
|
||||
inviteeIdentifierType: 'uuid',
|
||||
} as jest.Mocked<SharedSubscriptionInvitationCanceledEventPayload>
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileRemovedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileRemovedEvent>)
|
||||
})
|
||||
|
||||
it('should mark files to be remove for user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not mark files to be remove for user if identifier is not of uuid type', async () => {
|
||||
event.payload.inviteeIdentifierType = 'email'
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).not.toHaveBeenCalled()
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not publish events if failed to mark files to be removed', async () => {
|
||||
markFilesToBeRemoved.execute = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(markFilesToBeRemoved.execute).toHaveBeenCalledWith({ userUuid: '1-2-3' })
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
SharedSubscriptionInvitationCanceledEvent,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
@injectable()
|
||||
export class SharedSubscriptionInvitationCanceledEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SharedSubscriptionInvitationCanceledEvent): Promise<void> {
|
||||
if (event.payload.inviteeIdentifierType !== 'uuid') {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.inviteeIdentifier,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const fileRemoved of response.filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Readable } from 'stream'
|
||||
|
||||
export interface FileDownloaderInterface {
|
||||
createDownloadStream(filePath: string, startRange: number, endRange: number): Readable
|
||||
getFileSize(filePath: string): Promise<number>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { RemovedFileDescription } from '../File/RemovedFileDescription'
|
||||
|
||||
export interface FileRemoverInterface {
|
||||
remove(filePath: string): Promise<number>
|
||||
markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChunkId } from '../Upload/ChunkId'
|
||||
import { UploadChunkResult } from '../Upload/UploadChunkResult'
|
||||
import { UploadId } from '../Upload/UploadId'
|
||||
|
||||
export interface FileUploaderInterface {
|
||||
createUploadSession(filePath: string): Promise<UploadId>
|
||||
uploadFileChunk(dto: { uploadId: string; data: Uint8Array; filePath: string; chunkId: ChunkId }): Promise<string>
|
||||
finishUploadSession(uploadId: string, filePath: string, uploadChunkResults: Array<UploadChunkResult>): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type ChunkId = number
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ChunkId } from './ChunkId'
|
||||
|
||||
export type UploadChunkResult = {
|
||||
chunkId: ChunkId
|
||||
tag: string
|
||||
chunkSize: number
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type UploadId = string
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UploadChunkResult } from './UploadChunkResult'
|
||||
import { UploadId } from './UploadId'
|
||||
|
||||
export interface UploadRepositoryInterface {
|
||||
storeUploadSession(filePath: string, uploadId: UploadId): Promise<void>
|
||||
retrieveUploadSessionId(filePath: string): Promise<UploadId | undefined>
|
||||
storeUploadChunkResult(uploadId: UploadId, uploadChunkResult: UploadChunkResult): Promise<void>
|
||||
retrieveUploadChunkResults(uploadId: UploadId): Promise<Array<UploadChunkResult>>
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
|
||||
import { CreateUploadSession } from './CreateUploadSession'
|
||||
|
||||
describe('CreateUploadSession', () => {
|
||||
let fileUploader: FileUploaderInterface
|
||||
let uploadRepository: UploadRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new CreateUploadSession(fileUploader, uploadRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileUploader = {} as jest.Mocked<FileUploaderInterface>
|
||||
fileUploader.createUploadSession = jest.fn().mockReturnValue('123')
|
||||
|
||||
uploadRepository = {} as jest.Mocked<UploadRepositoryInterface>
|
||||
uploadRepository.storeUploadSession = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should indicate of an error in creating the upload session', async () => {
|
||||
uploadRepository.storeUploadSession = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops')
|
||||
})
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not create upload session',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an upload session', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(fileUploader.createUploadSession).toHaveBeenCalledWith('1-2-3/2-3-4')
|
||||
expect(uploadRepository.storeUploadSession).toHaveBeenCalledWith('1-2-3/2-3-4', '123')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateUploadSessionDTO } from './CreateUploadSessionDTO'
|
||||
import { CreateUploadSessionResponse } from './CreateUploadSessionResponse'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class CreateUploadSession implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileUploader) private fileUploader: FileUploaderInterface,
|
||||
@inject(TYPES.UploadRepository) private uploadRepository: UploadRepositoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateUploadSessionDTO): Promise<CreateUploadSessionResponse> {
|
||||
try {
|
||||
this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.fileUploader.createUploadSession(filePath)
|
||||
|
||||
this.logger.debug(`Created upload session with id: ${uploadId}`)
|
||||
|
||||
await this.uploadRepository.storeUploadSession(filePath, uploadId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
uploadId,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not create upload session for resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not create upload session',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type CreateUploadSessionDTO = {
|
||||
userUuid: Uuid
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { UploadId } from '../../Upload/UploadId'
|
||||
|
||||
export type CreateUploadSessionResponse =
|
||||
| {
|
||||
success: true
|
||||
uploadId: UploadId
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventPublisherInterface, FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
|
||||
import { FinishUploadSession } from './FinishUploadSession'
|
||||
|
||||
describe('FinishUploadSession', () => {
|
||||
let fileUploader: FileUploaderInterface
|
||||
let uploadRepository: UploadRepositoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new FinishUploadSession(fileUploader, uploadRepository, domainEventPublisher, domainEventFactory, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileUploader = {} as jest.Mocked<FileUploaderInterface>
|
||||
fileUploader.finishUploadSession = jest.fn().mockReturnValue('ETag123')
|
||||
|
||||
uploadRepository = {} as jest.Mocked<UploadRepositoryInterface>
|
||||
uploadRepository.retrieveUploadSessionId = jest.fn().mockReturnValue('123')
|
||||
uploadRepository.retrieveUploadChunkResults = jest.fn().mockReturnValue([{ tag: '123', chunkId: 1, chunkSize: 1 }])
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileUploadedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileUploadedEvent>)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should not finish an upload session if non existing', async () => {
|
||||
uploadRepository.retrieveUploadSessionId = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should indicate of an error in finishing session fails', async () => {
|
||||
uploadRepository.retrieveUploadSessionId = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops')
|
||||
})
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should finish an upload session', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
|
||||
{ tag: '123', chunkId: 1, chunkSize: 1 },
|
||||
])
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not finish an upload session if the file size exceeds storage quota', async () => {
|
||||
uploadRepository.retrieveUploadChunkResults = jest.fn().mockReturnValue([
|
||||
{ tag: '123', chunkId: 1, chunkSize: 60 },
|
||||
{ tag: '234', chunkId: 2, chunkSize: 10 },
|
||||
{ tag: '345', chunkId: 3, chunkSize: 20 },
|
||||
])
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 20,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not finish upload session. You are out of space.',
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { FinishUploadSessionDTO } from './FinishUploadSessionDTO'
|
||||
import { FinishUploadSessionResponse } from './FinishUploadSessionResponse'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class FinishUploadSession implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileUploader) private fileUploader: FileUploaderInterface,
|
||||
@inject(TYPES.UploadRepository) private uploadRepository: UploadRepositoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: FinishUploadSessionDTO): Promise<FinishUploadSessionResponse> {
|
||||
try {
|
||||
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||
if (uploadId === undefined) {
|
||||
this.logger.warn(`Could not find upload session for file path: ${filePath}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
}
|
||||
}
|
||||
|
||||
const uploadChunkResults = await this.uploadRepository.retrieveUploadChunkResults(uploadId)
|
||||
|
||||
let totalFileSize = 0
|
||||
for (const uploadChunkResult of uploadChunkResults) {
|
||||
totalFileSize += uploadChunkResult.chunkSize
|
||||
}
|
||||
|
||||
const remainingSpaceLeft = dto.uploadBytesLimit - dto.uploadBytesUsed
|
||||
if (remainingSpaceLeft < totalFileSize) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session. You are out of space.',
|
||||
}
|
||||
}
|
||||
|
||||
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileUploadedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not finish upload session for resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type FinishUploadSessionDTO = {
|
||||
userUuid: Uuid
|
||||
resourceRemoteIdentifier: string
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type FinishUploadSessionResponse =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
|
||||
import { GetFileMetadata } from './GetFileMetadata'
|
||||
|
||||
describe('GetFileMetadata', () => {
|
||||
let fileDownloader: FileDownloaderInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new GetFileMetadata(fileDownloader, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileDownloader = {} as jest.Mocked<FileDownloaderInterface>
|
||||
fileDownloader.getFileSize = jest.fn().mockReturnValue(123)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should return the file metadata', async () => {
|
||||
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', userUuid: '2-3-4' })).toEqual({
|
||||
success: true,
|
||||
size: 123,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not return the file metadata if it fails', async () => {
|
||||
fileDownloader.getFileSize = jest.fn().mockImplementation(() => {
|
||||
throw new Error('ooops')
|
||||
})
|
||||
|
||||
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', userUuid: '2-3-4' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not get file metadata.',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
|
||||
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
|
||||
|
||||
@injectable()
|
||||
export class GetFileMetadata implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileDownloader) private fileDownloader: FileDownloaderInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
|
||||
try {
|
||||
const size = await this.fileDownloader.getFileSize(`${dto.userUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
size,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not get file metadata for resource: ${dto.userUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not get file metadata.',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type GetFileMetadataDTO = {
|
||||
userUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type GetFileMetadataResponse =
|
||||
| {
|
||||
success: true
|
||||
size: number
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||
|
||||
import { MarkFilesToBeRemoved } from './MarkFilesToBeRemoved'
|
||||
|
||||
describe('MarkFilesToBeRemoved', () => {
|
||||
let fileRemover: FileRemoverInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new MarkFilesToBeRemoved(fileRemover, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileRemover = {} as jest.Mocked<FileRemoverInterface>
|
||||
fileRemover.markFilesToBeRemoved = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should mark files for being removed', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ success: true })
|
||||
|
||||
expect(fileRemover.markFilesToBeRemoved).toHaveBeenCalledWith('1-2-3')
|
||||
})
|
||||
|
||||
it('should indicate if marking files for being removed goes wrong', async () => {
|
||||
fileRemover.markFilesToBeRemoved = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not mark resources for removal',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MarkFilesToBeRemovedDTO } from './MarkFilesToBeRemovedDTO'
|
||||
import { MarkFilesToBeRemovedResponse } from './MarkFilesToBeRemovedResponse'
|
||||
|
||||
@injectable()
|
||||
export class MarkFilesToBeRemoved implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileRemover) private fileRemover: FileRemoverInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {
|
||||
try {
|
||||
this.logger.debug(`Marking files for later removal for user: ${dto.userUuid}`)
|
||||
|
||||
const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.userUuid)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filesRemoved,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not mark resources for removal: ${dto.userUuid} - ${(error as Error).message}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not mark resources for removal',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type MarkFilesToBeRemovedDTO = {
|
||||
userUuid: Uuid
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { RemovedFileDescription } from '../../File/RemovedFileDescription'
|
||||
|
||||
export type MarkFilesToBeRemovedResponse =
|
||||
| {
|
||||
success: true
|
||||
filesRemoved: Array<RemovedFileDescription>
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventPublisherInterface, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
import { RemoveFile } from './RemoveFile'
|
||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||
|
||||
describe('RemoveFile', () => {
|
||||
let fileRemover: FileRemoverInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new RemoveFile(fileRemover, domainEventPublisher, domainEventFactory, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileRemover = {} as jest.Mocked<FileRemoverInterface>
|
||||
fileRemover.remove = jest.fn().mockReturnValue(413)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileRemovedEvent = jest.fn().mockReturnValue({} as jest.Mocked<FileRemovedEvent>)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should indicate of an error in removing fails', async () => {
|
||||
fileRemover.remove = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops')
|
||||
})
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '3-4-5',
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not remove resource',
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove a file', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '3-4-5',
|
||||
})
|
||||
|
||||
expect(fileRemover.remove).toHaveBeenCalledWith('1-2-3/2-3-4')
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { RemoveFileDTO } from './RemoveFileDTO'
|
||||
import { RemoveFileResponse } from './RemoveFileResponse'
|
||||
|
||||
@injectable()
|
||||
export class RemoveFile implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileRemover) private fileRemover: FileRemoverInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RemoveFileDTO): Promise<RemoveFileResponse> {
|
||||
try {
|
||||
this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const removedFileSize = await this.fileRemover.remove(filePath)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: removedFileSize,
|
||||
regularSubscriptionUuid: dto.regularSubscriptionUuid,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not remove resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not remove resource',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user