mirror of
https://github.com/standardnotes/server
synced 2026-04-21 14:02:27 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2073c735a5 | |||
| 34085ac6fb | |||
| 3d6559921b | |||
| 15a7f0e71a | |||
| 3e56243d6f | |||
| 032fcb938d | |||
| e98393452b | |||
| 302b624504 | |||
| e00d9d2ca0 | |||
| 9ab4601c8d | |||
| 19e43bdb1a | |||
| 49832e7944 | |||
| 916e98936a | |||
| 31d1eef7f7 |
@@ -24,6 +24,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
@@ -50,12 +51,22 @@ jobs:
|
||||
DB_TYPE: mysql
|
||||
CACHE_TYPE: redis
|
||||
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
|
||||
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
|
||||
|
||||
- name: Wait for server to start
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
- name: Define if vault tests are enabled
|
||||
id: vaults
|
||||
run: |
|
||||
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
|
||||
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
@@ -73,6 +84,7 @@ jobs:
|
||||
db_type: [mysql, sqlite]
|
||||
cache_type: [redis, memory]
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -141,6 +153,7 @@ jobs:
|
||||
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
|
||||
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
|
||||
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
|
||||
echo "MONGO_HOST=localhost" >> packages/home-server/.env
|
||||
echo "MONGO_PORT=27017" >> packages/home-server/.env
|
||||
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
@@ -157,8 +170,17 @@ jobs:
|
||||
- name: Wait for server to start
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Define if vault tests are enabled
|
||||
id: vaults
|
||||
run: |
|
||||
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
|
||||
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
|
||||
|
||||
- name: Show logs on failure
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
DB_TYPE: "${DB_TYPE}"
|
||||
CACHE_TYPE: "${CACHE_TYPE}"
|
||||
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
|
||||
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
|
||||
container_name: server-ci
|
||||
ports:
|
||||
- 3123:3000
|
||||
|
||||
@@ -66,6 +66,9 @@ fi
|
||||
if [ -z "$SECONDARY_DB_ENABLED" ]; then
|
||||
export SECONDARY_DB_ENABLED=false
|
||||
fi
|
||||
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
|
||||
export TRANSITION_MODE_ENABLED=false
|
||||
fi
|
||||
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
|
||||
|
||||
#########
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.13",
|
||||
"version": "2.25.15",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.70.4",
|
||||
"version": "1.71.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
const cacheKey = `${authHeaderValue}${
|
||||
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
|
||||
}`
|
||||
|
||||
try {
|
||||
let crossServiceTokenFetchedFromCache = true
|
||||
let crossServiceToken = null
|
||||
if (this.crossServiceTokenCacheTTL) {
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
|
||||
const authResponse = await this.serviceProxy.validateSession({
|
||||
authorization: authHeaderValue,
|
||||
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
|
||||
})
|
||||
|
||||
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
|
||||
return
|
||||
@@ -52,7 +59,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
authorizationHeaderValue: authHeaderValue,
|
||||
key: cacheKey,
|
||||
encodedCrossServiceToken: crossServiceToken,
|
||||
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
|
||||
userUuid: decodedToken.user.uuid,
|
||||
@@ -62,6 +69,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.roles = decodedToken.roles
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
|
||||
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
let userAuthHeaders = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeys = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeys = JSON.parse(userKeysJSON)
|
||||
}
|
||||
userAuthHeaders.push(dto.authorizationHeaderValue)
|
||||
userKeys.push(dto.key)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
|
||||
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
async get(key: string): Promise<string | null> {
|
||||
this.invalidateExpiredTokens()
|
||||
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
|
||||
if (!cachedToken) {
|
||||
return null
|
||||
}
|
||||
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
let userAuthorizationHeaderValues = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeyValues = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeyValues = JSON.parse(userKeysJSON)
|
||||
}
|
||||
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
|
||||
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
|
||||
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
async get(key: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
pipeline.del(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface CrossServiceTokenCacheInterface {
|
||||
set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void>
|
||||
get(authorizationHeaderValue: string): Promise<string | null>
|
||||
get(key: string): Promise<string | null>
|
||||
invalidate(userUuid: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authorizationHeaderValue,
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(authorizationHeaderValue: string): Promise<{
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
const serviceResponse = (await authService.handleRequest(
|
||||
{
|
||||
headers: {
|
||||
authorization: authorizationHeaderValue,
|
||||
authorization: headers.authorization,
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
|
||||
|
||||
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348191367 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348280258 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.131.5",
|
||||
"version": "1.133.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -560,6 +560,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.Auth_READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
container
|
||||
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
|
||||
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
|
||||
@@ -101,6 +101,7 @@ const TYPES = {
|
||||
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
|
||||
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
|
||||
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
|
||||
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
|
||||
// use cases
|
||||
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
|
||||
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
|
||||
|
||||
+79
-19
@@ -8,6 +8,8 @@ import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
@@ -15,6 +17,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getSettingUseCase: GetSetting
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
@@ -22,7 +25,15 @@ describe('CreateCrossServiceToken', () => {
|
||||
let role: Role
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
userRepository,
|
||||
jwtTTL,
|
||||
getSettingUseCase,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
@@ -50,6 +61,9 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
|
||||
getSettingUseCase = {} as jest.Mocked<GetSetting>
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
@@ -125,28 +139,74 @@ describe('CreateCrossServiceToken', () => {
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if user uuid is invalid', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('shared vault context', () => {
|
||||
it('should add shared vault context if shared vault owner uuid is provided', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
shared_vault_owner_context: {
|
||||
upload_bytes_limit: 100,
|
||||
},
|
||||
user: {
|
||||
email: 'test@test.te',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if shared vault owner context is sensitive', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
|
||||
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+30
-10
@@ -1,5 +1,6 @@
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
@@ -7,14 +8,13 @@ import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { GetSetting } from '../GetSetting/GetSetting'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
@injectable()
|
||||
export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
export class CreateCrossServiceToken implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@@ -22,14 +22,16 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.Auth_GetSetting)
|
||||
private getSettingUseCase: GetSetting,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
|
||||
let user: User | undefined | null = dto.user
|
||||
if (user === undefined && dto.userUuid !== undefined) {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(userUuidOrError.getError())
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
@@ -37,7 +39,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
|
||||
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const roles = await user.roles
|
||||
@@ -45,15 +47,33 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
shared_vault_owner_context: undefined,
|
||||
}
|
||||
|
||||
if (dto.sharedVaultOwnerContext !== undefined) {
|
||||
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
userUuid: dto.sharedVaultOwnerContext,
|
||||
})
|
||||
if (uploadBytesLimitSettingOrError.isFailed()) {
|
||||
return Result.fail(uploadBytesLimitSettingOrError.getError())
|
||||
}
|
||||
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
|
||||
if (uploadBytesLimitSetting.sensitive) {
|
||||
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
|
||||
}
|
||||
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
|
||||
|
||||
authTokenData.shared_vault_owner_context = {
|
||||
upload_bytes_limit: uploadBytesLimit,
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
{
|
||||
userUuid: string
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
|
||||
|
||||
describe('no subscription', () => {
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not find a setting if the setting name is invalid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid setting name: invalid',
|
||||
},
|
||||
})
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not get a setting for user if it does not exist', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive setting for user', async () => {
|
||||
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not retrieve a subscription setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
|
||||
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
|
||||
it('should not get a suscription setting for user if it does not exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive subscription setting for user', async () => {
|
||||
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a regular subscription only setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingProjector } from '../../../Projection/SettingProjector'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
|
||||
@injectable()
|
||||
export class GetSetting implements UseCaseInterface {
|
||||
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
|
||||
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
|
||||
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
|
||||
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
|
||||
const settingNameOrError = SettingName.create(dto.settingName)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: settingNameOrError.getError(),
|
||||
},
|
||||
}
|
||||
return Result.fail(settingNameOrError.getError())
|
||||
}
|
||||
const settingName = settingNameOrError.getValue()
|
||||
|
||||
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
}
|
||||
return Result.fail('No subscription found.')
|
||||
}
|
||||
|
||||
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (subscriptionSetting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSubscriptionSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setting = await this.settingService.findSettingWithDecryptedValue({
|
||||
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSetting = await this.settingProjector.projectSimple(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { SimpleSetting } from '../../Setting/SimpleSetting'
|
||||
|
||||
export type GetSettingResponse =
|
||||
| {
|
||||
success: true
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
}
|
||||
| {
|
||||
success: true
|
||||
sensitive: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
export type GetSettingResponse = Either<
|
||||
{
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
},
|
||||
{
|
||||
sensitive: true
|
||||
}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Register } from './Register'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
import { Session } from '../Session/Session'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('Register', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -20,9 +21,19 @@ describe('Register', () => {
|
||||
let user: User
|
||||
let crypter: CrypterInterface
|
||||
let timer: TimerInterface
|
||||
let transitionModeEnabled = false
|
||||
|
||||
const createUseCase = () =>
|
||||
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
|
||||
new Register(
|
||||
userRepository,
|
||||
roleRepository,
|
||||
authResponseFactory,
|
||||
crypter,
|
||||
false,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
@@ -75,6 +86,7 @@ describe('Register', () => {
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: expect.any(String),
|
||||
version: '004',
|
||||
roles: Promise.resolve([]),
|
||||
createdAt: new Date(1),
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
@@ -118,6 +130,48 @@ describe('Register', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a new user with default role and transition role', async () => {
|
||||
transitionModeEnabled = true
|
||||
|
||||
const role = new Role()
|
||||
role.name = RoleName.NAMES.CoreUser
|
||||
|
||||
const transitionRole = new Role()
|
||||
transitionRole.name = RoleName.NAMES.TransitionUser
|
||||
|
||||
roleRepository.findOneByName = jest.fn().mockReturnValueOnce(role).mockReturnValueOnce(transitionRole)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
apiVersion: '20200115',
|
||||
ephemeralSession: false,
|
||||
version: '004',
|
||||
pwCost: 11,
|
||||
pwSalt: 'qweqwe',
|
||||
pwNonce: undefined,
|
||||
}),
|
||||
).toEqual({ success: true, authResponse: { foo: 'bar' } })
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
email: 'test@test.te',
|
||||
encryptedPassword: expect.any(String),
|
||||
encryptedServerKey: 'test',
|
||||
serverEncryptionVersion: 1,
|
||||
pwCost: 11,
|
||||
pwNonce: undefined,
|
||||
pwSalt: 'qweqwe',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: expect.any(String),
|
||||
version: '004',
|
||||
createdAt: new Date(1),
|
||||
updatedAt: new Date(1),
|
||||
roles: Promise.resolve([role, transitionRole]),
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail to register if username is invalid', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
@@ -195,6 +249,7 @@ describe('Register', () => {
|
||||
true,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
).execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { RoleName, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
|
||||
import { UseCaseInterface } from './UseCaseInterface'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
|
||||
@@ -27,6 +27,7 @@ export class Register implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
|
||||
) {}
|
||||
|
||||
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
|
||||
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
|
||||
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
|
||||
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
|
||||
|
||||
const roles = []
|
||||
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
|
||||
if (defaultRole) {
|
||||
user.roles = Promise.resolve([defaultRole])
|
||||
roles.push(defaultRole)
|
||||
}
|
||||
if (this.transitionModeEnabled) {
|
||||
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
|
||||
if (transitionRole) {
|
||||
roles.push(transitionRole)
|
||||
}
|
||||
}
|
||||
user.roles = Promise.resolve(roles)
|
||||
|
||||
Object.assign(user, registrationFields)
|
||||
|
||||
|
||||
+6
-10
@@ -7,7 +7,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@@ -34,23 +33,20 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
bytesUsed: number,
|
||||
): Promise<void> {
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
|
||||
let bytesAlreadyUsed = '0'
|
||||
const subscriptionUser = await subscription.user
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
userUuid: subscriptionUser.uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
})
|
||||
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
user: subscriptionUser,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedInternalController', () => {
|
||||
let getUserFeatures: GetUserFeatures
|
||||
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
|
||||
|
||||
@httpGet('/users/:userUuid/settings/:settingName')
|
||||
async getSetting(request: Request): Promise<results.JsonResult> {
|
||||
const result = await this.doGetSetting.execute({
|
||||
const resultOrError = await this.doGetSetting.execute({
|
||||
userUuid: request.params.userUuid,
|
||||
settingName: request.params.settingName,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
|
||||
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { Session } from '../../Domain/Session/Session'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSessionsController', () => {
|
||||
let getActiveSessionsForUser: GetActiveSessionsForUser
|
||||
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
|
||||
|
||||
request = {
|
||||
params: {},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
|
||||
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSettingsController', () => {
|
||||
let deleteSetting: DeleteSetting
|
||||
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
+3
-2
@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
|
||||
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
let getSetting: GetSetting
|
||||
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
|
||||
|
||||
const user = authenticateRequestResponse.user as User
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
|
||||
const resultOrError = await this.createCrossServiceToken.execute({
|
||||
user,
|
||||
session: authenticateRequestResponse.session,
|
||||
sharedVaultOwnerContext,
|
||||
})
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
return this.json({ authToken: resultOrError.getValue() })
|
||||
}
|
||||
|
||||
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
|
||||
@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const { userUuid, settingName } = request.params
|
||||
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
|
||||
+14
-4
@@ -14,15 +14,25 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.doGetSetting.execute({
|
||||
const resultOrError = await this.doGetSetting.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
settingName: request.params.subscriptionSettingName.toUpperCase(),
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,20 @@ export class BaseWebSocketsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
const resultOrError = await this.createCrossServiceToken.execute({
|
||||
userUuid: token.userUuid,
|
||||
})
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
return this.json({ authToken: resultOrError.getValue() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.2...@standardnotes/domain-core@1.26.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
|
||||
|
||||
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.1...@standardnotes/domain-core@1.25.2) (2023-08-09)
|
||||
|
||||
### Reverts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.25.2",
|
||||
"version": "1.26.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -21,25 +21,36 @@ describe('RoleName', () => {
|
||||
const plusUserRole = RoleName.create(RoleName.NAMES.PlusUser).getValue()
|
||||
const coreUser = RoleName.create(RoleName.NAMES.CoreUser).getValue()
|
||||
const internalTeamUser = RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()
|
||||
const transitionUser = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
|
||||
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export class RoleName extends ValueObject<RoleNameProps> {
|
||||
PlusUser: 'PLUS_USER',
|
||||
ProUser: 'PRO_USER',
|
||||
InternalTeamUser: 'INTERNAL_TEAM_USER',
|
||||
TransitionUser: 'TRANSITION_USER',
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
@@ -19,11 +20,19 @@ export class RoleName extends ValueObject<RoleNameProps> {
|
||||
case RoleName.NAMES.InternalTeamUser:
|
||||
return true
|
||||
case RoleName.NAMES.ProUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser].includes(roleName.value)
|
||||
return [
|
||||
RoleName.NAMES.CoreUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.TransitionUser,
|
||||
].includes(roleName.value)
|
||||
case RoleName.NAMES.PlusUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser].includes(roleName.value)
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.TransitionUser].includes(
|
||||
roleName.value,
|
||||
)
|
||||
case RoleName.NAMES.CoreUser:
|
||||
return [RoleName.NAMES.CoreUser].includes(roleName.value)
|
||||
case RoleName.NAMES.TransitionUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.TransitionUser].includes(roleName.value)
|
||||
/*istanbul ignore next*/
|
||||
default:
|
||||
throw new Error(`Invalid role name: ${this.value}`)
|
||||
|
||||
@@ -3,32 +3,24 @@ import { RoleNameCollection } from './RoleNameCollection'
|
||||
|
||||
describe('RoleNameCollection', () => {
|
||||
it('should create a value object', () => {
|
||||
const role1 = RoleName.create(RoleName.NAMES.ProUser).getValue()
|
||||
|
||||
const valueOrError = RoleNameCollection.create([role1])
|
||||
const valueOrError = RoleNameCollection.create([RoleName.NAMES.ProUser])
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual([role1])
|
||||
expect(valueOrError.getValue().value[0].value).toEqual('PRO_USER')
|
||||
})
|
||||
|
||||
it('should tell if collections are not equal', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
let roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
]).getValue()
|
||||
let roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser]).getValue()
|
||||
|
||||
let valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
|
||||
|
||||
roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
RoleName.NAMES.CoreUser,
|
||||
]).getValue()
|
||||
|
||||
valueOrError = RoleNameCollection.create(roles1)
|
||||
@@ -36,42 +28,30 @@ describe('RoleNameCollection', () => {
|
||||
})
|
||||
|
||||
it('should tell if collections are equal', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]).getValue()
|
||||
const roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]).getValue()
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().equals(roles2)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell if collection includes element', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell if collection does not includes element', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.CoreUser).getValue())).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell if collection has a role with more or equal power to', () => {
|
||||
let roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue()]
|
||||
let roles = [RoleName.NAMES.CoreUser]
|
||||
let valueOrError = RoleNameCollection.create(roles)
|
||||
let roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -83,7 +63,7 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
|
||||
roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
|
||||
roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]
|
||||
valueOrError = RoleNameCollection.create(roles)
|
||||
roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -95,7 +75,7 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
|
||||
roles = [RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
|
||||
roles = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
valueOrError = RoleNameCollection.create(roles)
|
||||
roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -109,4 +89,11 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail to create a collection if a role name is invalid', () => {
|
||||
const valueOrError = RoleNameCollection.create(['invalid-role-name'])
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
expect(valueOrError.getError()).toEqual('Invalid role name: invalid-role-name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,16 @@ export class RoleNameCollection extends ValueObject<RoleNameCollectionProps> {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(roleName: RoleName[]): Result<RoleNameCollection> {
|
||||
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleName }))
|
||||
static create(roleNameStrings: string[]): Result<RoleNameCollection> {
|
||||
const roleNames: RoleName[] = []
|
||||
for (const roleNameString of roleNameStrings) {
|
||||
const roleNameOrError = RoleName.create(roleNameString)
|
||||
if (roleNameOrError.isFailed()) {
|
||||
return Result.fail<RoleNameCollection>(roleNameOrError.getError())
|
||||
}
|
||||
roleNames.push(roleNameOrError.getValue())
|
||||
}
|
||||
|
||||
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleNames }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.11](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.10...@standardnotes/domain-events-infra@1.12.11) (2023-08-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.11",
|
||||
"version": "1.12.12",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [2.115.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.114.0...@standardnotes/domain-events@2.115.0) (2023-08-08)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.115.0",
|
||||
"version": "2.115.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.19...@standardnotes/event-store@1.11.20) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.19](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.18...@standardnotes/event-store@1.11.19) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.19",
|
||||
"version": "1.11.21",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/files/issues/704)) ([34085ac](https://github.com/standardnotes/files/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
## [1.20.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.3...@standardnotes/files-server@1.20.4) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.20.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.2...@standardnotes/files-server@1.20.3) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.20.3",
|
||||
"version": "1.21.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -121,6 +121,10 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
if (locals.uploadBytesLimit === undefined) {
|
||||
return this.badRequest('Missing upload bytes limit')
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
userUuid: locals.vaultOwnerUuid,
|
||||
sharedVaultUuid: locals.sharedVaultUuid,
|
||||
|
||||
+40
-1
@@ -1,4 +1,4 @@
|
||||
import { SharedVaultValetTokenData, TokenDecoderInterface } from '@standardnotes/security'
|
||||
import { SharedVaultValetTokenData, TokenDecoderInterface, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
@@ -61,6 +61,17 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
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 ask the vault owner to upgrade subscription',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const whitelistedData: SharedVaultValetTokenData = {
|
||||
sharedVaultUuid: valetTokenData.sharedVaultUuid,
|
||||
vaultOwnerUuid: valetTokenData.vaultOwnerUuid,
|
||||
@@ -79,4 +90,32 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
return next(error)
|
||||
}
|
||||
}
|
||||
|
||||
private userHasNoSpaceToUpload(valetTokenData: SharedVaultValetTokenData) {
|
||||
if (![ValetTokenOperation.Write, ValetTokenOperation.Move].includes(valetTokenData.permittedOperation)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (valetTokenData.uploadBytesLimit === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isMovingToNonSharedVault =
|
||||
valetTokenData.permittedOperation === ValetTokenOperation.Move &&
|
||||
valetTokenData.moveOperation?.type === 'shared-vault-to-user'
|
||||
|
||||
if (isMovingToNonSharedVault) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (valetTokenData.uploadBytesLimit === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
const remainingUploadSpace = valetTokenData.uploadBytesLimit - valetTokenData.uploadBytesUsed
|
||||
|
||||
const consideredUploadSize = valetTokenData.unencryptedFileSize as number
|
||||
|
||||
return remainingUploadSpace - consideredUploadSize <= 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,3 +9,12 @@ PSEUDO_KEY_PARAMS_KEY=
|
||||
VALET_TOKEN_SECRET=
|
||||
|
||||
FILES_SERVER_URL=
|
||||
|
||||
SECONDARY_DB_ENABLED=false
|
||||
MONGO_HOST=localhost
|
||||
MONGO_PORT=27017
|
||||
MONGO_USERNAME=standardnotes
|
||||
MONGO_PASSWORD=standardnotes
|
||||
MONGO_DATABASE=standardnotes
|
||||
|
||||
TRANSITION_MODE_ENABLED=false
|
||||
|
||||
@@ -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.14.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.1...@standardnotes/home-server@1.14.2) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.0...@standardnotes/home-server@1.14.1) (2023-08-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.51...@standardnotes/home-server@1.14.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
|
||||
|
||||
## [1.13.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.50...@standardnotes/home-server@1.13.51) (2023-08-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** add default env values for secondary database ([916e989](https://github.com/standardnotes/server/commit/916e98936a276a3960d949c5b70803214c945686))
|
||||
|
||||
## [1.13.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.49...@standardnotes/home-server@1.13.50) (2023-08-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.13.50",
|
||||
"version": "1.14.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.8...@standardnotes/revisions-server@1.26.9) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.26.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.7...@standardnotes/revisions-server@1.26.8) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.26.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.6...@standardnotes/revisions-server@1.26.7) (2023-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.26.7",
|
||||
"version": "1.26.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.23...@standardnotes/scheduler-server@1.20.24) (2023-08-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scheduler:** remove exit interview form link ([#702](https://github.com/standardnotes/server/issues/702)) ([3e56243](https://github.com/standardnotes/server/commit/3e56243d6f72f5ac86d4fb2191349ec3a589bc83))
|
||||
|
||||
## [1.20.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.22...@standardnotes/scheduler-server@1.20.23) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.22](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.21...@standardnotes/scheduler-server@1.20.22) (2023-08-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.22",
|
||||
"version": "1.20.25",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -15,8 +15,6 @@ export const html = `<div>
|
||||
are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
|
||||
Notes.
|
||||
</p>
|
||||
<p>If you have a minute, please fill out this brief exit interview: </p>
|
||||
<a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
|
||||
<p>
|
||||
Our team reads every single response, and your feedback will be shared with the relevant department within our
|
||||
team.
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.9.0...@standardnotes/security@1.10.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.8.1...@standardnotes/security@1.9.0) (2023-08-08)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@ export type CrossServiceTokenData = {
|
||||
uuid: string
|
||||
email: string
|
||||
}
|
||||
shared_vault_owner_context?: {
|
||||
upload_bytes_limit: number
|
||||
}
|
||||
roles: Array<Role>
|
||||
session?: {
|
||||
uuid: string
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface SharedVaultValetTokenData {
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
uploadBytesLimit?: number
|
||||
moveOperation?: {
|
||||
type: SharedVaultMoveType
|
||||
fromUuid: string
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.25](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.24...@standardnotes/settings@1.21.25) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.24](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.23...@standardnotes/settings@1.21.24) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.24",
|
||||
"version": "1.21.25",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.82.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.81.0...@standardnotes/syncing-server@1.82.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/syncing-server-js/issues/704)) ([34085ac](https://github.com/standardnotes/syncing-server-js/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
|
||||
|
||||
# [1.81.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.80.0...@standardnotes/syncing-server@1.81.0) (2023-08-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** DocumentDB retry writes support ([#703](https://github.com/standardnotes/syncing-server-js/issues/703)) ([15a7f0e](https://github.com/standardnotes/syncing-server-js/commit/15a7f0e71ac2f6c355fb73208559a8fd822773aa))
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** add use case for migrating items from one database to another ([#701](https://github.com/standardnotes/syncing-server-js/issues/701)) ([032fcb9](https://github.com/standardnotes/syncing-server-js/commit/032fcb938d9f81381dd9879af4bda9254ee8c499))
|
||||
|
||||
# [1.80.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.79.1...@standardnotes/syncing-server@1.80.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/syncing-server-js/issues/700)) ([302b624](https://github.com/standardnotes/syncing-server-js/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
|
||||
|
||||
## [1.79.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.79.0...@standardnotes/syncing-server@1.79.1) (2023-08-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** refactor shared vault and key system associations ([#698](https://github.com/standardnotes/syncing-server-js/issues/698)) ([31d1eef](https://github.com/standardnotes/syncing-server-js/commit/31d1eef7f74310b176085311fc04c2efc4a7059f))
|
||||
|
||||
# [1.79.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.11...@standardnotes/syncing-server@1.79.0) (2023-08-16)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class RemoveAssociations1692264556858 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'DROP INDEX `key_system_identifier_on_key_system_associations` ON `key_system_associations`',
|
||||
)
|
||||
await queryRunner.query('DROP INDEX `item_uuid_on_key_system_associations` ON `key_system_associations`')
|
||||
await queryRunner.query('DROP TABLE `key_system_associations`')
|
||||
|
||||
await queryRunner.query('DROP INDEX `item_uuid_on_shared_vault_associations` ON `shared_vault_associations`')
|
||||
await queryRunner.query(
|
||||
'DROP INDEX `shared_vault_uuid_on_shared_vault_associations` ON `shared_vault_associations`',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE `shared_vault_associations`')
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class RemoveSharedVaultLimit1692619430384 implements MigrationInterface {
|
||||
name = 'RemoveSharedVaultLimit1692619430384'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vaults` DROP COLUMN `file_upload_bytes_limit`')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vaults` ADD `file_upload_bytes_limit` int NOT NULL')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class RemoveAssociations1692264735730 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "key_system_identifier_on_key_system_associations"')
|
||||
await queryRunner.query('DROP INDEX "item_uuid_on_key_system_associations"')
|
||||
await queryRunner.query('DROP TABLE "key_system_associations"')
|
||||
|
||||
await queryRunner.query('DROP INDEX "item_uuid_on_shared_vault_associations"')
|
||||
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_associations"')
|
||||
await queryRunner.query('DROP TABLE "shared_vault_associations"')
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class RemoveSharedVaultLimit1692619677621 implements MigrationInterface {
|
||||
name = 'RemoveSharedVaultLimit1692619677621'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "shared_vaults" RENAME TO "temporary_shared_vaults"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "shared_vaults" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "file_upload_bytes_used" integer NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "shared_vaults"("uuid", "user_uuid", "file_upload_bytes_used", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "user_uuid", "file_upload_bytes_used", "created_at_timestamp", "updated_at_timestamp" FROM "temporary_shared_vaults"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "temporary_shared_vaults"')
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.79.0",
|
||||
"version": "1.82.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ import { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { InversifyExpressAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware'
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
|
||||
import { ContentDecoder } from '@standardnotes/common'
|
||||
import { ContentDecoder, ContentDecoderInterface } from '@standardnotes/common'
|
||||
import {
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventHandlerInterface,
|
||||
@@ -78,16 +78,6 @@ import { SaveItems } from '../Domain/UseCase/Syncing/SaveItems/SaveItems'
|
||||
import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
|
||||
import { ItemHash } from '../Domain/Item/ItemHash'
|
||||
import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
|
||||
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
|
||||
import { SharedVaultAssociation } from '../Domain/SharedVault/SharedVaultAssociation'
|
||||
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
|
||||
import { SharedVaultAssociationPersistenceMapper } from '../Mapping/Persistence/SharedVaultAssociationPersistenceMapper'
|
||||
import { TypeORMKeySystemAssociationRepository } from '../Infra/TypeORM/TypeORMKeySystemAssociationRepository'
|
||||
import { SharedVaultAssociationRepositoryInterface } from '../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
|
||||
import { TypeORMSharedVaultAssociationRepository } from '../Infra/TypeORM/TypeORMSharedVaultAssociationRepository'
|
||||
import { KeySystemAssociation } from '../Domain/KeySystem/KeySystemAssociation'
|
||||
import { KeySystemAssociationRepositoryInterface } from '../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
|
||||
import { KeySystemAssociationPersistenceMapper } from '../Mapping/Persistence/KeySystemAssociationPersistenceMapper'
|
||||
import { BaseSharedVaultInvitesController } from '../Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController'
|
||||
import { InviteUserToSharedVault } from '../Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault'
|
||||
import { TypeORMSharedVaultRepository } from '../Infra/TypeORM/TypeORMSharedVaultRepository'
|
||||
@@ -161,6 +151,10 @@ import { AddNotificationsForUsers } from '../Domain/UseCase/Messaging/AddNotific
|
||||
import { MongoDBItem } from '../Infra/TypeORM/MongoDBItem'
|
||||
import { MongoDBItemRepository } from '../Infra/TypeORM/MongoDBItemRepository'
|
||||
import { MongoDBItemPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBItemPersistenceMapper'
|
||||
import { Logger } from 'winston'
|
||||
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
|
||||
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
|
||||
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -292,6 +286,18 @@ export class ContainerConfigLoader {
|
||||
})
|
||||
}
|
||||
|
||||
container
|
||||
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
|
||||
.toConstantValue(
|
||||
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
|
||||
)
|
||||
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container
|
||||
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(
|
||||
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
|
||||
)
|
||||
|
||||
// Mapping
|
||||
container
|
||||
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
|
||||
@@ -316,16 +322,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
|
||||
.toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>>(
|
||||
TYPES.Sync_KeySystemAssociationPersistenceMapper,
|
||||
)
|
||||
.toConstantValue(new KeySystemAssociationPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>>(
|
||||
TYPES.Sync_SharedVaultAssociationPersistenceMapper,
|
||||
)
|
||||
.toConstantValue(new SharedVaultAssociationPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<SharedVault, TypeORMSharedVault>>(TYPES.Sync_SharedVaultPersistenceMapper)
|
||||
.toConstantValue(new SharedVaultPersistenceMapper())
|
||||
@@ -363,12 +359,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
|
||||
.toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
|
||||
container
|
||||
.bind<Repository<TypeORMSharedVaultAssociation>>(TYPES.Sync_ORMSharedVaultAssociationRepository)
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMSharedVaultAssociation))
|
||||
container
|
||||
.bind<Repository<TypeORMKeySystemAssociation>>(TYPES.Sync_ORMKeySystemAssociationRepository)
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMKeySystemAssociation))
|
||||
container
|
||||
.bind<Repository<TypeORMSharedVault>>(TYPES.Sync_ORMSharedVaultRepository)
|
||||
.toConstantValue(appDataSource.getRepository(TypeORMSharedVault))
|
||||
@@ -392,44 +382,38 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new MongoDBItemPersistenceMapper())
|
||||
|
||||
container
|
||||
.bind<MongoRepository<MongoDBItem>>(TYPES.Sync_MongoItemRepository)
|
||||
.bind<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository)
|
||||
.toConstantValue(appDataSource.getMongoRepository(MongoDBItem))
|
||||
|
||||
container
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
|
||||
.toConstantValue(
|
||||
new MongoDBItemRepository(
|
||||
container.get<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository),
|
||||
container.get<MapperInterface<Item, MongoDBItem>>(TYPES.Sync_MongoDBItemPersistenceMapper),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Repositories
|
||||
container
|
||||
.bind<KeySystemAssociationRepositoryInterface>(TYPES.Sync_KeySystemAssociationRepository)
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMKeySystemAssociationRepository(
|
||||
container.get(TYPES.Sync_ORMKeySystemAssociationRepository),
|
||||
container.get(TYPES.Sync_KeySystemAssociationPersistenceMapper),
|
||||
new TypeORMItemRepository(
|
||||
container.get<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository),
|
||||
container.get<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultAssociationRepositoryInterface>(TYPES.Sync_SharedVaultAssociationRepository)
|
||||
.bind<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver)
|
||||
.toConstantValue(
|
||||
new TypeORMSharedVaultAssociationRepository(
|
||||
container.get(TYPES.Sync_ORMSharedVaultAssociationRepository),
|
||||
container.get(TYPES.Sync_SharedVaultAssociationPersistenceMapper),
|
||||
new TypeORMItemRepositoryResolver(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
|
||||
.toConstantValue(
|
||||
isSecondaryDatabaseEnabled
|
||||
? new MongoDBItemRepository(
|
||||
container.get(TYPES.Sync_MongoItemRepository),
|
||||
container.get(TYPES.Sync_MongoDBItemPersistenceMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
: new TypeORMItemRepository(
|
||||
container.get(TYPES.Sync_ORMItemRepository),
|
||||
container.get(TYPES.Sync_ItemPersistenceMapper),
|
||||
container.get(TYPES.Sync_KeySystemAssociationRepository),
|
||||
container.get(TYPES.Sync_SharedVaultAssociationRepository),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository)
|
||||
.toConstantValue(
|
||||
@@ -480,10 +464,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemTransferCalculator(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
return new ItemTransferCalculator(context.container.get<Logger>(TYPES.Sync_Logger))
|
||||
})
|
||||
|
||||
// Middleware
|
||||
@@ -561,7 +542,7 @@ export class ContainerConfigLoader {
|
||||
.bind<GetItems>(TYPES.Sync_GetItems)
|
||||
.toConstantValue(
|
||||
new GetItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
@@ -573,7 +554,7 @@ export class ContainerConfigLoader {
|
||||
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
|
||||
.toConstantValue(
|
||||
new SaveNewItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
@@ -599,7 +580,7 @@ export class ContainerConfigLoader {
|
||||
.bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
|
||||
.toConstantValue(
|
||||
new UpdateExistingItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
@@ -614,7 +595,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new SaveItems(
|
||||
container.get(TYPES.Sync_ItemSaveValidator),
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_SaveNewItem),
|
||||
container.get(TYPES.Sync_UpdateExistingItem),
|
||||
@@ -643,7 +624,7 @@ export class ContainerConfigLoader {
|
||||
.bind<SyncItems>(TYPES.Sync_SyncItems)
|
||||
.toConstantValue(
|
||||
new SyncItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_GetItems),
|
||||
container.get(TYPES.Sync_SaveItems),
|
||||
container.get(TYPES.Sync_GetSharedVaults),
|
||||
@@ -653,10 +634,10 @@ export class ContainerConfigLoader {
|
||||
),
|
||||
)
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepositoryResolver))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepositoryResolver))
|
||||
})
|
||||
container
|
||||
.bind<InviteUserToSharedVault>(TYPES.Sync_InviteUserToSharedVault)
|
||||
@@ -787,6 +768,15 @@ export class ContainerConfigLoader {
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind(TransitionItemsFromPrimaryToSecondaryDatabaseForUser)
|
||||
.toConstantValue(
|
||||
new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container
|
||||
@@ -814,48 +804,56 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
})
|
||||
|
||||
// env vars
|
||||
container
|
||||
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toConstantValue(
|
||||
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
|
||||
)
|
||||
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container
|
||||
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(
|
||||
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
|
||||
env.get('S3_AWS_REGION', true)
|
||||
? new S3ItemBackupService(
|
||||
container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
container.get(TYPES.Sync_ItemBackupMapper),
|
||||
container.get(TYPES.Sync_ItemHttpMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
container.get(TYPES.Sync_S3),
|
||||
)
|
||||
: new FSItemBackupService(
|
||||
container.get(TYPES.Sync_FILE_UPLOAD_PATH),
|
||||
container.get(TYPES.Sync_ItemBackupMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<DuplicateItemSyncedEventHandler>(TYPES.Sync_DuplicateItemSyncedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new DuplicateItemSyncedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new DuplicateItemSyncedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Sync_AccountDeletionRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new AccountDeletionRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemRevisionCreationRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_ItemBackupService),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Sync_SharedVaultFileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -880,38 +878,17 @@ export class ContainerConfigLoader {
|
||||
container.bind<AxiosInstance>(TYPES.Sync_HTTPClient).toDynamicValue(() => axios.create())
|
||||
container
|
||||
.bind<ExtensionsHttpServiceInterface>(TYPES.Sync_ExtensionsHttpService)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ExtensionsHttpService(
|
||||
context.container.get(TYPES.Sync_HTTPClient),
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_ContentDecoder),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
|
||||
container
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Sync_Env)
|
||||
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
return new S3ItemBackupService(
|
||||
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
context.container.get(TYPES.Sync_ItemBackupMapper),
|
||||
context.container.get(TYPES.Sync_ItemHttpMapper),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
context.container.get(TYPES.Sync_S3),
|
||||
)
|
||||
} else {
|
||||
return new FSItemBackupService(
|
||||
context.container.get(TYPES.Sync_FILE_UPLOAD_PATH),
|
||||
context.container.get(TYPES.Sync_ItemBackupMapper),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
}
|
||||
})
|
||||
.toConstantValue(
|
||||
new ExtensionsHttpService(
|
||||
container.get<AxiosInstance>(TYPES.Sync_HTTPClient),
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<ContentDecoderInterface>(TYPES.Sync_ContentDecoder),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['DUPLICATE_ITEM_SYNCED', container.get(TYPES.Sync_DuplicateItemSyncedEventHandler)],
|
||||
@@ -940,19 +917,22 @@ export class ContainerConfigLoader {
|
||||
|
||||
container
|
||||
.bind<EmailBackupRequestedEventHandler>(TYPES.Sync_EmailBackupRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new EmailBackupRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_AuthHttpService),
|
||||
context.container.get(TYPES.Sync_ItemBackupService),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
|
||||
context.container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new EmailBackupRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled
|
||||
? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
|
||||
: null,
|
||||
container.get<AuthHttpServiceInterface>(TYPES.Sync_AuthHttpService),
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<number>(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
|
||||
container.get<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get<string>(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
eventHandlers.set('EMAIL_BACKUP_REQUESTED', container.get(TYPES.Sync_EmailBackupRequestedEventHandler))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Env } from './Env'
|
||||
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
|
||||
import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
|
||||
import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
|
||||
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
|
||||
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
|
||||
import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
|
||||
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
|
||||
import { TypeORMSharedVaultInvite } from '../Infra/TypeORM/TypeORMSharedVaultInvite'
|
||||
@@ -58,6 +56,7 @@ export class AppDataSource {
|
||||
password: this.env.get('MONGO_PASSWORD', true),
|
||||
database: this.env.get('MONGO_DATABASE'),
|
||||
entities: [MongoDBItem],
|
||||
retryWrites: false,
|
||||
synchronize: true,
|
||||
})
|
||||
|
||||
@@ -78,8 +77,6 @@ export class AppDataSource {
|
||||
entities: [
|
||||
TypeORMItem,
|
||||
TypeORMNotification,
|
||||
TypeORMSharedVaultAssociation,
|
||||
TypeORMKeySystemAssociation,
|
||||
TypeORMSharedVault,
|
||||
TypeORMSharedVaultUser,
|
||||
TypeORMSharedVaultInvite,
|
||||
|
||||
@@ -7,9 +7,9 @@ const TYPES = {
|
||||
Sync_S3: Symbol.for('Sync_S3'),
|
||||
Sync_Env: Symbol.for('Sync_Env'),
|
||||
// Repositories
|
||||
Sync_ItemRepository: Symbol.for('Sync_ItemRepository'),
|
||||
Sync_KeySystemAssociationRepository: Symbol.for('Sync_KeySystemAssociationRepository'),
|
||||
Sync_SharedVaultAssociationRepository: Symbol.for('Sync_SharedVaultAssociationRepository'),
|
||||
Sync_ItemRepositoryResolver: Symbol.for('Sync_ItemRepositoryResolver'),
|
||||
Sync_MySQLItemRepository: Symbol.for('Sync_MySQLItemRepository'),
|
||||
Sync_MongoDBItemRepository: Symbol.for('Sync_MongoDBItemRepository'),
|
||||
Sync_SharedVaultRepository: Symbol.for('Sync_SharedVaultRepository'),
|
||||
Sync_SharedVaultInviteRepository: Symbol.for('Sync_SharedVaultInviteRepository'),
|
||||
Sync_SharedVaultUserRepository: Symbol.for('Sync_SharedVaultUserRepository'),
|
||||
@@ -17,15 +17,13 @@ const TYPES = {
|
||||
Sync_MessageRepository: Symbol.for('Sync_MessageRepository'),
|
||||
// ORM
|
||||
Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
|
||||
Sync_ORMSharedVaultAssociationRepository: Symbol.for('Sync_ORMSharedVaultAssociationRepository'),
|
||||
Sync_ORMKeySystemAssociationRepository: Symbol.for('Sync_ORMKeySystemAssociationRepository'),
|
||||
Sync_ORMSharedVaultRepository: Symbol.for('Sync_ORMSharedVaultRepository'),
|
||||
Sync_ORMSharedVaultInviteRepository: Symbol.for('Sync_ORMSharedVaultInviteRepository'),
|
||||
Sync_ORMSharedVaultUserRepository: Symbol.for('Sync_ORMSharedVaultUserRepository'),
|
||||
Sync_ORMNotificationRepository: Symbol.for('Sync_ORMNotificationRepository'),
|
||||
Sync_ORMMessageRepository: Symbol.for('Sync_ORMMessageRepository'),
|
||||
// Mongo
|
||||
Sync_MongoItemRepository: Symbol.for('Sync_MongoItemRepository'),
|
||||
Sync_ORMMongoItemRepository: Symbol.for('Sync_ORMMongoItemRepository'),
|
||||
// Middleware
|
||||
Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
|
||||
// env vars
|
||||
@@ -82,6 +80,9 @@ const TYPES = {
|
||||
Sync_DetermineSharedVaultOperationOnItem: Symbol.for('Sync_DetermineSharedVaultOperationOnItem'),
|
||||
Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'),
|
||||
Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'),
|
||||
Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
|
||||
'Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
@@ -132,8 +133,6 @@ const TYPES = {
|
||||
Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
|
||||
Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
|
||||
Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
|
||||
Sync_KeySystemAssociationPersistenceMapper: Symbol.for('Sync_KeySystemAssociationPersistenceMapper'),
|
||||
Sync_SharedVaultAssociationPersistenceMapper: Symbol.for('Sync_SharedVaultAssociationPersistenceMapper'),
|
||||
Sync_SharedVaultPersistenceMapper: Symbol.for('Sync_SharedVaultPersistenceMapper'),
|
||||
Sync_SharedVaultUserPersistenceMapper: Symbol.for('Sync_SharedVaultUserPersistenceMapper'),
|
||||
Sync_SharedVaultInvitePersistenceMapper: Symbol.for('Sync_SharedVaultInvitePersistenceMapper'),
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardn
|
||||
|
||||
describe('ExtensionsHttpService', () => {
|
||||
let httpClient: AxiosInstance
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let contentDecoder: ContentDecoderInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
@@ -24,7 +25,8 @@ describe('ExtensionsHttpService', () => {
|
||||
const createService = () =>
|
||||
new ExtensionsHttpService(
|
||||
httpClient,
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
contentDecoder,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
@@ -54,8 +56,8 @@ describe('ExtensionsHttpService', () => {
|
||||
|
||||
authParams = {} as jest.Mocked<KeyParamsData>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
@@ -191,6 +193,31 @@ describe('ExtensionsHttpService', () => {
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should publish a failed backup event if the extension is in the secondary repository', async () => {
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
})
|
||||
|
||||
await createService().sendItemsToExtensionsServer({
|
||||
userUuid: '1-2-3',
|
||||
extensionId: '2-3-4',
|
||||
extensionsServerUrl: '',
|
||||
forceMute: false,
|
||||
items: [item],
|
||||
backupFilename: 'backup-file',
|
||||
authParams,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
|
||||
|
||||
@@ -273,7 +300,7 @@ describe('ExtensionsHttpService', () => {
|
||||
})
|
||||
|
||||
it('should throw an error if the extension to post to is not found', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
@@ -299,7 +326,7 @@ describe('ExtensionsHttpService', () => {
|
||||
|
||||
it('should throw an error if the extension to post to has no content', async () => {
|
||||
item = {} as jest.Mocked<Item>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
|
||||
@@ -17,7 +17,8 @@ import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email
|
||||
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
|
||||
constructor(
|
||||
private httpClient: AxiosInstance,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private contentDecoder: ContentDecoderInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -139,9 +140,14 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
|
||||
userUuid: string,
|
||||
email: string,
|
||||
): Promise<DomainEventInterface> {
|
||||
const extension = await this.itemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
let extension = await this.primaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
if (extension === null || !extension.props.content) {
|
||||
throw Error(`Could not find extensions with id ${extensionId}`)
|
||||
if (this.secondaryItemRepository) {
|
||||
extension = await this.secondaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
}
|
||||
if (extension === null || !extension.props.content) {
|
||||
throw Error(`Could not find extensions with id ${extensionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const content = this.contentDecoder.decode(extension.props.content)
|
||||
|
||||
+19
-6
@@ -8,12 +8,14 @@ import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequested
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let logger: Logger
|
||||
let event: AccountDeletionRequestedEvent
|
||||
let item: Item
|
||||
|
||||
const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger)
|
||||
const createHandler = () =>
|
||||
new AccountDeletionRequestedEventHandler(primaryItemRepository, secondaryItemRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
item = Item.create(
|
||||
@@ -33,9 +35,9 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
itemRepository.deleteByUserUuid = jest.fn()
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository.deleteByUserUuid = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
@@ -52,6 +54,17 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
it('should remove all items for a user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
})
|
||||
|
||||
it('should remove all items for a user from secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.deleteByUserUuid = jest.fn()
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(secondaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,17 @@ import { Logger } from 'winston'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
|
||||
constructor(
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
await this.itemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
await this.primaryItemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.secondaryItemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
}
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let logger: Logger
|
||||
let duplicateItem: Item
|
||||
let originalItem: Item
|
||||
@@ -22,7 +23,13 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger)
|
||||
new DuplicateItemSyncedEventHandler(
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
originalItem = Item.create(
|
||||
@@ -59,8 +66,8 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(duplicateItem)
|
||||
.mockReturnValueOnce(originalItem)
|
||||
@@ -90,8 +97,22 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy revisions from original item to the duplicate item in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuidAndUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(duplicateItem)
|
||||
.mockReturnValueOnce(originalItem)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should not copy revisions if original item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
@@ -99,7 +120,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
})
|
||||
|
||||
it('should not copy revisions if duplicate item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
|
||||
@@ -9,14 +9,26 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: DuplicateItemSyncedEvent): Promise<void> {
|
||||
const item = await this.itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
|
||||
await this.requestRevisionsCopy(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.requestRevisionsCopy(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestRevisionsCopy(
|
||||
event: DuplicateItemSyncedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
const item = await itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
|
||||
|
||||
if (item === null) {
|
||||
this.logger.warn(`Could not find item with uuid ${event.payload.itemUuid}`)
|
||||
@@ -30,7 +42,7 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
|
||||
return
|
||||
}
|
||||
|
||||
const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid(
|
||||
const existingOriginalItem = await itemRepository.findByUuidAndUserUuid(
|
||||
item.props.duplicateOf.value,
|
||||
event.payload.userUuid,
|
||||
)
|
||||
|
||||
+25
-4
@@ -13,9 +13,11 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { EmailBackupRequestedEventHandler } from './EmailBackupRequestedEventHandler'
|
||||
import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
|
||||
import { ItemContentSizeDescriptor } from '../Item/ItemContentSizeDescriptor'
|
||||
|
||||
describe('EmailBackupRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let authHttpService: AuthHttpServiceInterface
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
@@ -28,7 +30,8 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
|
||||
const createHandler = () =>
|
||||
new EmailBackupRequestedEventHandler(
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
authHttpService,
|
||||
itemBackupService,
|
||||
domainEventPublisher,
|
||||
@@ -42,8 +45,11 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
beforeEach(() => {
|
||||
item = {} as jest.Mocked<Item>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
authHttpService = {} as jest.Mocked<AuthHttpServiceInterface>
|
||||
authHttpService.getUserKeyParams = jest.fn().mockReturnValue({ identifier: 'test@test.com' })
|
||||
@@ -81,6 +87,21 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should inform that backup attachment for email was created in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
secondaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should inform that multipart backup attachment for email was created', async () => {
|
||||
itemBackupService.backup = jest
|
||||
.fn()
|
||||
|
||||
@@ -16,7 +16,8 @@ import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
|
||||
|
||||
export class EmailBackupRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private authHttpService: AuthHttpServiceInterface,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@@ -28,6 +29,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
) {}
|
||||
|
||||
async handle(event: EmailBackupRequestedEvent): Promise<void> {
|
||||
await this.requestEmailWithBackupFile(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.requestEmailWithBackupFile(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestEmailWithBackupFile(
|
||||
event: EmailBackupRequestedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
let authParams: KeyParamsData
|
||||
try {
|
||||
authParams = await this.authHttpService.getUserKeyParams({
|
||||
@@ -46,14 +58,15 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
sortOrder: 'ASC',
|
||||
deleted: false,
|
||||
}
|
||||
const itemContentSizeDescriptors = await itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
|
||||
itemQuery,
|
||||
itemContentSizeDescriptors,
|
||||
this.emailAttachmentMaxByteSize,
|
||||
)
|
||||
|
||||
const backupFileNames: string[] = []
|
||||
for (const itemUuidBundle of itemUuidBundles) {
|
||||
const items = await this.itemRepository.findAll({
|
||||
const items = await itemRepository.findAll({
|
||||
uuids: itemUuidBundle,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
|
||||
+19
-5
@@ -14,7 +14,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let event: ItemRevisionCreationRequestedEvent
|
||||
let item: Item
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
@@ -23,7 +24,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
|
||||
const createHandler = () =>
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
itemBackupService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
@@ -47,8 +49,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
@@ -80,8 +82,20 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a revision for an item in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should not create a revision for an item that does not exist', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
|
||||
+14
-2
@@ -11,20 +11,32 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
|
||||
await this.createItemDump(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.createItemDump(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async createItemDump(
|
||||
event: ItemRevisionCreationRequestedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
const itemUuidOrError = Uuid.create(event.payload.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const item = await this.itemRepository.findByUuid(itemUuid)
|
||||
const item = await itemRepository.findByUuid(itemUuid)
|
||||
if (item === null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,10 +61,8 @@ describe('Item', () => {
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
@@ -112,11 +110,7 @@ describe('Item', () => {
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
keySystemAssociation: KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
keySystemAssociation: KeySystemAssociation.create('key-system-identifier').getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
@@ -142,111 +136,117 @@ describe('Item', () => {
|
||||
expect(entityOrError.getValue().isAssociatedWithKeySystem('key-system-identifier')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should set shared vault association', () => {
|
||||
const sharedVaultAssociation = SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.setSharedVaultAssociation(sharedVaultAssociation)
|
||||
|
||||
expect(entity.props.sharedVaultAssociation).toEqual(sharedVaultAssociation)
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should unset a shared vault association', () => {
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
it('should tell if an item is identical to another item', () => {
|
||||
const entity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
entity.unsetSharedVaultAssociation()
|
||||
|
||||
expect(entity.props.sharedVaultAssociation).toBeUndefined()
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should set key system association', () => {
|
||||
const keySystemAssociation = KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.setKeySystemAssociation(keySystemAssociation)
|
||||
|
||||
expect(entity.props.keySystemAssociation).toEqual(keySystemAssociation)
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should unset a key system association', () => {
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
keySystemAssociation: KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
const otherEntity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
entity.unsetKeySystemAssociation()
|
||||
expect(entity.isIdenticalTo(otherEntity)).toBeTruthy()
|
||||
})
|
||||
|
||||
expect(entity.props.keySystemAssociation).toBeUndefined()
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
it('should tell that an item is not identical to another item', () => {
|
||||
const entity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
const otherEntity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(124)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell that an item is not identical to another item if their ids are different', () => {
|
||||
const entity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
const otherEntity = Item.create(
|
||||
{
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(124)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Aggregate, Change, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { Aggregate, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemProps } from './ItemProps'
|
||||
import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
|
||||
|
||||
export class Item extends Aggregate<ItemProps> {
|
||||
private constructor(props: ItemProps, id?: UniqueEntityId) {
|
||||
@@ -55,57 +53,17 @@ export class Item extends Aggregate<ItemProps> {
|
||||
return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier
|
||||
}
|
||||
|
||||
setSharedVaultAssociation(sharedVaultAssociation: SharedVaultAssociation): void {
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: this.props.sharedVaultAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
|
||||
changeData: sharedVaultAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
this.props.sharedVaultAssociation = sharedVaultAssociation
|
||||
}
|
||||
|
||||
unsetSharedVaultAssociation(): void {
|
||||
if (!this.props.sharedVaultAssociation) {
|
||||
return
|
||||
isIdenticalTo(item: Item): boolean {
|
||||
if (this._id.toString() !== item._id.toString()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: Change.TYPES.Remove,
|
||||
changeData: this.props.sharedVaultAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
this.props.sharedVaultAssociation = undefined
|
||||
}
|
||||
const stringifiedThis = JSON.stringify(this.props)
|
||||
const stringifiedItem = JSON.stringify(item.props)
|
||||
|
||||
setKeySystemAssociation(keySystemAssociation: KeySystemAssociation): void {
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: this.props.keySystemAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
|
||||
changeData: keySystemAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
const base64This = Buffer.from(stringifiedThis).toString('base64')
|
||||
const base64Item = Buffer.from(stringifiedItem).toString('base64')
|
||||
|
||||
this.props.keySystemAssociation = keySystemAssociation
|
||||
}
|
||||
|
||||
unsetKeySystemAssociation(): void {
|
||||
if (!this.props.keySystemAssociation) {
|
||||
return
|
||||
}
|
||||
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: Change.TYPES.Remove,
|
||||
changeData: this.props.keySystemAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
this.props.keySystemAssociation = undefined
|
||||
return base64This === base64Item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
describe('ItemContentSizeDescriptor', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return error if shared vault uuid is not valid', () => {
|
||||
const valueOrError = ItemContentSizeDescriptor.create('invalid', 20)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemContentSizeDescriptorProps } from './ItemContentSizeDescriptorProps'
|
||||
|
||||
export class ItemContentSizeDescriptor extends ValueObject<ItemContentSizeDescriptorProps> {
|
||||
private constructor(props: ItemContentSizeDescriptorProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(itemUuidString: string, contentSize: number | null): Result<ItemContentSizeDescriptor> {
|
||||
const uuidOrError = Uuid.create(itemUuidString)
|
||||
if (uuidOrError.isFailed()) {
|
||||
return Result.fail<ItemContentSizeDescriptor>(uuidOrError.getError())
|
||||
}
|
||||
const uuid = uuidOrError.getValue()
|
||||
|
||||
return Result.ok<ItemContentSizeDescriptor>(
|
||||
new ItemContentSizeDescriptor({
|
||||
uuid,
|
||||
contentSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface ItemContentSizeDescriptorProps {
|
||||
uuid: Uuid
|
||||
contentSize: number | null
|
||||
}
|
||||
@@ -3,14 +3,13 @@ import { Uuid } from '@standardnotes/domain-core'
|
||||
import { Item } from './Item'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemRepositoryInterface {
|
||||
deleteByUserUuid(userUuid: string): Promise<void>
|
||||
findAll(query: ItemQuery): Promise<Item[]>
|
||||
countAll(query: ItemQuery): Promise<number>
|
||||
findContentSizeForComputingTransferLimit(
|
||||
query: ItemQuery,
|
||||
): Promise<Array<{ uuid: string; contentSize: number | null }>>
|
||||
findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<Array<ItemContentSizeDescriptor>>
|
||||
findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>>
|
||||
findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
|
||||
findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RoleNameCollection } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
|
||||
export interface ItemRepositoryResolverInterface {
|
||||
resolve(roleNames: RoleNameCollection): ItemRepositoryInterface
|
||||
}
|
||||
@@ -1,201 +1,143 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
|
||||
import { ItemTransferCalculator } from './ItemTransferCalculator'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
describe('ItemTransferCalculator', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createCalculator = () => new ItemTransferCalculator(itemRepository, logger)
|
||||
const createCalculator = () => new ItemTransferCalculator(logger)
|
||||
|
||||
beforeEach(() => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([])
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
describe('fetching uuids', () => {
|
||||
it('should compute uuids to fetch based on transfer limit - one item overlaping limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - exact limit fit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
])
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4'])
|
||||
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - content size not defined on an item', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - first item over the limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 50,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
])
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4'])
|
||||
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetching bundles', () => {
|
||||
it('should compute uuid bundles to fetch based on transfer limit - one item overlaping limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - exact limit fit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
['00000000-0000-0000-0000-000000000002'],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - content size not defined on an item', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - first item over the limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 50,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
['00000000-0000-0000-0000-000000000002'],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
async computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>> {
|
||||
async computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<string>> {
|
||||
const itemUuidsToFetch = []
|
||||
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
let totalContentSizeInBytes = 0
|
||||
for (const itemContentSize of itemContentSizes) {
|
||||
const contentSize = itemContentSize.contentSize ?? 0
|
||||
for (const itemContentSize of itemContentSizeDescriptors) {
|
||||
const contentSize = itemContentSize.props.contentSize ?? 0
|
||||
|
||||
itemUuidsToFetch.push(itemContentSize.uuid)
|
||||
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
|
||||
totalContentSizeInBytes += contentSize
|
||||
|
||||
const transferLimitBreached = this.isTransferLimitBreached({
|
||||
totalContentSizeInBytes,
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizes,
|
||||
itemContentSizeDescriptors,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -32,22 +33,24 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
return itemUuidsToFetch
|
||||
}
|
||||
|
||||
async computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>> {
|
||||
async computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<Array<string>>> {
|
||||
let itemUuidsToFetch = []
|
||||
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
let totalContentSizeInBytes = 0
|
||||
const bundles = []
|
||||
for (const itemContentSize of itemContentSizes) {
|
||||
const contentSize = itemContentSize.contentSize ?? 0
|
||||
for (const itemContentSize of itemContentSizeDescriptors) {
|
||||
const contentSize = itemContentSize.props.contentSize ?? 0
|
||||
|
||||
itemUuidsToFetch.push(itemContentSize.uuid)
|
||||
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
|
||||
totalContentSizeInBytes += contentSize
|
||||
|
||||
const transferLimitBreached = this.isTransferLimitBreached({
|
||||
totalContentSizeInBytes,
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizes,
|
||||
itemContentSizeDescriptors,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -68,11 +71,11 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
totalContentSizeInBytes: number
|
||||
bytesTransferLimit: number
|
||||
itemUuidsToFetch: Array<string>
|
||||
itemContentSizes: Array<{ uuid: string; contentSize: number | null }>
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
|
||||
}): boolean {
|
||||
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
|
||||
const transferLimitBreachedAtFirstItem =
|
||||
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizes.length > 1
|
||||
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
|
||||
|
||||
if (transferLimitBreachedAtFirstItem) {
|
||||
this.logger.warn(
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemTransferCalculatorInterface {
|
||||
computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>>
|
||||
computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>>
|
||||
computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<string>>
|
||||
computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<Array<string>>>
|
||||
}
|
||||
|
||||
@@ -40,10 +40,8 @@ describe('SharedVaultFilter', () => {
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
@@ -254,10 +252,8 @@ describe('SharedVaultFilter', () => {
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
@@ -427,10 +423,8 @@ describe('SharedVaultFilter', () => {
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
@@ -589,10 +583,8 @@ describe('SharedVaultFilter', () => {
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
|
||||
@@ -26,10 +26,8 @@ describe('SharedVaultSnjsFilter', () => {
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user