mirror of
https://github.com/standardnotes/server
synced 2026-01-18 08:04:28 -05:00
Compare commits
10 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0188f290f9 | ||
|
|
676cf36f8d | ||
|
|
f8aef6c8ef | ||
|
|
5bf8cf49c1 | ||
|
|
51cd0a4dad | ||
|
|
1d06ffe9d5 | ||
|
|
dbf532f55e | ||
|
|
ca6dbc0053 | ||
|
|
1bb5980b45 | ||
|
|
a02a28774b |
29
.github/workflows/common-e2e.yml
vendored
29
.github/workflows/common-e2e.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
suite: ['base', 'vaults']
|
||||
secondary_db_enabled: [true, false]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -55,21 +56,24 @@ jobs:
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html?suite=${{ matrix.suite }}
|
||||
|
||||
- name: Show logs on failure
|
||||
- name: Archive failed run logs
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "# Errors:"
|
||||
tail -n 100 logs/*.err
|
||||
echo "# Logs:"
|
||||
tail -n 100 logs/*.log
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: self-hosted-failure-logs-${{ matrix.suite }}-${{ matrix.secondary_db_enabled }}
|
||||
retention-days: 5
|
||||
path: |
|
||||
logs/*.err
|
||||
logs/*.log
|
||||
|
||||
e2e-home-server:
|
||||
name: (Home Server) E2E Test Suite
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
suite: ['base', 'vaults']
|
||||
db_type: [mysql, sqlite]
|
||||
cache_type: [redis, memory]
|
||||
secondary_db_enabled: [true, false]
|
||||
@@ -159,8 +163,13 @@ jobs:
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html
|
||||
run: yarn dlx mocha-headless-chrome --timeout 3600000 -f http://localhost:9001/mocha/test.html?suite=${{ matrix.suite }}
|
||||
|
||||
- name: Show logs on failure
|
||||
- name: Archive failed run logs
|
||||
if: ${{ failure() }}
|
||||
run: tail -n 500 logs/output.log
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: home-server-failure-logs-${{ matrix.suite }}-${{ matrix.db_type }}-${{ matrix.cache_type }}-${{ matrix.secondary_db_enabled }}
|
||||
retention-days: 5
|
||||
path: |
|
||||
logs/output.log
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
|
||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
"e2e": "yarn workspace @standardnotes/home-server run build && PORT=3123 yarn workspace @standardnotes/home-server start",
|
||||
"e2e": "yarn build && PORT=3123 yarn workspace @standardnotes/home-server start",
|
||||
"start": "yarn workspace @standardnotes/home-server run build && yarn workspace @standardnotes/home-server start"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.27.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.27.1...@standardnotes/analytics@2.27.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.27.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.27.0...@standardnotes/analytics@2.27.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
# [2.27.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.24...@standardnotes/analytics@2.27.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [2.26.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.23...@standardnotes/analytics@2.26.24) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.26.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.22...@standardnotes/analytics@2.26.23) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.26.23",
|
||||
"version": "2.27.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"report": "yarn node dist/bin/report.js",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.75.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.6...@standardnotes/api-gateway@1.75.7) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.75.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.5...@standardnotes/api-gateway@1.75.6) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.75.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.4...@standardnotes/api-gateway@1.75.5) (2023-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.75.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.3...@standardnotes/api-gateway@1.75.4) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.75.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.2...@standardnotes/api-gateway@1.75.3) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.75.3",
|
||||
"version": "1.75.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.148.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.147.1...@standardnotes/auth-server@1.148.0) (2023-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* block file operations during transition ([#856](https://github.com/standardnotes/server/issues/856)) ([676cf36](https://github.com/standardnotes/server/commit/676cf36f8d769aa433880b2800f0457d06fbbf14))
|
||||
|
||||
## [1.147.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.147.0...@standardnotes/auth-server@1.147.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.147.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.146.4...@standardnotes/auth-server@1.147.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.146.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.146.3...@standardnotes/auth-server@1.146.4) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.146.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.146.2...@standardnotes/auth-server@1.146.3) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.146.3",
|
||||
"version": "1.148.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -19,7 +19,7 @@
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --fix --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"cleanup": "yarn node dist/bin/cleanup.js",
|
||||
|
||||
@@ -369,7 +369,7 @@ export class ContainerConfigLoader {
|
||||
// Mapping
|
||||
container
|
||||
.bind<MapperInterface<SessionTrace, TypeORMSessionTrace>>(TYPES.Auth_SessionTracePersistenceMapper)
|
||||
.toConstantValue(new SessionTracePersistenceMapper())
|
||||
.toConstantValue(new SessionTracePersistenceMapper(container.get<TimerInterface>(TYPES.Auth_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<Authenticator, TypeORMAuthenticator>>(TYPES.Auth_AuthenticatorPersistenceMapper)
|
||||
.toConstantValue(new AuthenticatorPersistenceMapper())
|
||||
@@ -458,8 +458,9 @@ export class ContainerConfigLoader {
|
||||
.bind<SessionTraceRepositoryInterface>(TYPES.Auth_SessionTraceRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMSessionTraceRepository(
|
||||
container.get(TYPES.Auth_ORMSessionTraceRepository),
|
||||
container.get(TYPES.Auth_SessionTracePersistenceMapper),
|
||||
container.get<Repository<TypeORMSessionTrace>>(TYPES.Auth_ORMSessionTraceRepository),
|
||||
container.get<MapperInterface<SessionTrace, TypeORMSessionTrace>>(TYPES.Auth_SessionTracePersistenceMapper),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -1011,7 +1012,6 @@ export class ContainerConfigLoader {
|
||||
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
|
||||
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
|
||||
container.get<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository),
|
||||
container.get<RemoveSharedVaultUser>(TYPES.Auth_RemoveSharedVaultUser),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSession
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { RemoveSharedVaultUser } from '../UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@@ -14,7 +13,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
private sessionRepository: SessionRepositoryInterface,
|
||||
private ephemeralSessionRepository: EphemeralSessionRepositoryInterface,
|
||||
private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
private removeSharedVaultUser: RemoveSharedVaultUser,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -37,13 +35,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
await this.removeSessions(userUuid.value)
|
||||
|
||||
const result = await this.removeSharedVaultUser.execute({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not remove shared vault user: ${result.getError()}`)
|
||||
}
|
||||
|
||||
await this.userRepository.remove(user)
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${userUuid.value}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
|
||||
import { SessionTrace } from './SessionTrace'
|
||||
|
||||
export interface SessionTraceRepositoryInterface {
|
||||
save(sessionTrace: SessionTrace): Promise<void>
|
||||
insert(sessionTrace: SessionTrace): Promise<void>
|
||||
removeExpiredBefore(date: Date): Promise<void>
|
||||
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
|
||||
countByDate(date: Date): Promise<number>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetToken } from './CreateValetToken'
|
||||
import { TransitionStatus } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
import { CreateValetToken } from './CreateValetToken'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { User } from '../../User/User'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
describe('CreateValetToken', () => {
|
||||
let tokenEncoder: TokenEncoderInterface<ValetTokenData>
|
||||
@@ -20,6 +23,7 @@ describe('CreateValetToken', () => {
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
let user: User
|
||||
let transitionStatusRepository: TransitionStatusRepositoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new CreateValetToken(
|
||||
@@ -29,6 +33,7 @@ describe('CreateValetToken', () => {
|
||||
userSubscriptionService,
|
||||
timer,
|
||||
valetTokenTTL,
|
||||
transitionStatusRepository,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -66,6 +71,11 @@ describe('CreateValetToken', () => {
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(100)
|
||||
|
||||
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
|
||||
transitionStatusRepository.getStatus = jest
|
||||
.fn()
|
||||
.mockReturnValue(TransitionStatus.create(TransitionStatus.STATUSES.Verified).getValue())
|
||||
})
|
||||
|
||||
it('should create a read valet token', async () => {
|
||||
@@ -166,6 +176,7 @@ describe('CreateValetToken', () => {
|
||||
{
|
||||
sharedSubscriptionUuid: undefined,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
ongoingTransition: false,
|
||||
permittedOperation: 'write',
|
||||
permittedResources: [
|
||||
{
|
||||
@@ -206,6 +217,7 @@ describe('CreateValetToken', () => {
|
||||
{
|
||||
sharedSubscriptionUuid: '2-3-4',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
ongoingTransition: false,
|
||||
permittedOperation: 'write',
|
||||
permittedResources: [
|
||||
{
|
||||
@@ -266,6 +278,7 @@ describe('CreateValetToken', () => {
|
||||
{
|
||||
sharedSubscriptionUuid: undefined,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
ongoingTransition: false,
|
||||
permittedOperation: 'write',
|
||||
permittedResources: [
|
||||
{
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CreateValetTokenDTO } from './CreateValetTokenDTO'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload'
|
||||
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
|
||||
import { TransitionStatus } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class CreateValetToken implements UseCaseInterface {
|
||||
@@ -25,6 +27,8 @@ export class CreateValetToken implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_VALET_TOKEN_TTL) private valetTokenTTL: number,
|
||||
@inject(TYPES.Auth_TransitionStatusRepository)
|
||||
private transitionStatusRepository: TransitionStatusRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateValetTokenDTO): Promise<CreateValetTokenResponseData> {
|
||||
@@ -83,6 +87,8 @@ export class CreateValetToken implements UseCaseInterface {
|
||||
sharedSubscriptionUuid = sharedSubscription.uuid
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid, 'items')
|
||||
|
||||
const tokenData: ValetTokenData = {
|
||||
userUuid: dto.userUuid,
|
||||
permittedOperation: dto.operation,
|
||||
@@ -91,6 +97,7 @@ export class CreateValetToken implements UseCaseInterface {
|
||||
uploadBytesLimit,
|
||||
sharedSubscriptionUuid,
|
||||
regularSubscriptionUuid: regularSubscription.uuid,
|
||||
ongoingTransition: transitionStatus?.value === TransitionStatus.STATUSES.InProgress,
|
||||
}
|
||||
|
||||
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('TraceSession', () => {
|
||||
beforeEach(() => {
|
||||
sessionTraceRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
|
||||
sessionTraceRepository.findOneByUserUuidAndDate = jest.fn().mockReturnValue(null)
|
||||
sessionTraceRepository.save = jest.fn()
|
||||
sessionTraceRepository.insert = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date())
|
||||
@@ -30,7 +30,7 @@ describe('TraceSession', () => {
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(result.getValue().props.userUuid.value).toEqual('0702b137-4f5c-438a-915e-8f8b46572ce5')
|
||||
expect(sessionTraceRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(sessionTraceRepository.insert).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not save a session trace if one already exists for the same user and date', async () => {
|
||||
@@ -43,7 +43,7 @@ describe('TraceSession', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
|
||||
expect(sessionTraceRepository.insert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return an error if userUuid is invalid', async () => {
|
||||
@@ -54,7 +54,7 @@ describe('TraceSession', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
|
||||
expect(sessionTraceRepository.insert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return an error if username is invalid', async () => {
|
||||
@@ -65,7 +65,7 @@ describe('TraceSession', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
|
||||
expect(sessionTraceRepository.insert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return an error if subscriptionPlanName is invalid', async () => {
|
||||
@@ -76,7 +76,7 @@ describe('TraceSession', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
|
||||
expect(sessionTraceRepository.insert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save a session trace if creating of the session trace fails', async () => {
|
||||
@@ -90,7 +90,7 @@ describe('TraceSession', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sessionTraceRepository.save).not.toHaveBeenCalled()
|
||||
expect(sessionTraceRepository.insert).not.toHaveBeenCalled()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export class TraceSession implements UseCaseInterface<SessionTrace> {
|
||||
}
|
||||
const sessionTrace = sessionTraceOrError.getValue()
|
||||
|
||||
await this.sessionTraceRepository.save(sessionTrace)
|
||||
await this.sessionTraceRepository.insert(sessionTrace)
|
||||
|
||||
return Result.ok<SessionTrace>(sessionTrace)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { MapperInterface, SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import { SessionTrace } from '../../Domain/Session/SessionTrace'
|
||||
import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface'
|
||||
import { TypeORMSessionTrace } from './TypeORMSessionTrace'
|
||||
@@ -8,13 +10,14 @@ export class TypeORMSessionTraceRepository implements SessionTraceRepositoryInte
|
||||
constructor(
|
||||
private ormRepository: Repository<TypeORMSessionTrace>,
|
||||
private mapper: MapperInterface<SessionTrace, TypeORMSessionTrace>,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('trace')
|
||||
.where('trace.creation_date = :creationDate', {
|
||||
creationDate: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
|
||||
creationDate: this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD'),
|
||||
})
|
||||
.andWhere('trace.subscription_plan_name = :subscriptionPlanName', {
|
||||
subscriptionPlanName: subscriptionPlanName.value,
|
||||
@@ -26,7 +29,7 @@ export class TypeORMSessionTraceRepository implements SessionTraceRepositoryInte
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('trace')
|
||||
.where('trace.creation_date = :creationDate', {
|
||||
creationDate: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
|
||||
creationDate: this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD'),
|
||||
})
|
||||
.getCount()
|
||||
}
|
||||
@@ -44,7 +47,7 @@ export class TypeORMSessionTraceRepository implements SessionTraceRepositoryInte
|
||||
.createQueryBuilder('trace')
|
||||
.where('trace.user_uuid = :userUuid AND trace.creation_date = :creationDate', {
|
||||
userUuid: userUuid.value,
|
||||
creationDate: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
|
||||
creationDate: this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD'),
|
||||
})
|
||||
.getOne()
|
||||
|
||||
@@ -55,9 +58,9 @@ export class TypeORMSessionTraceRepository implements SessionTraceRepositoryInte
|
||||
return this.mapper.toDomain(typeOrm)
|
||||
}
|
||||
|
||||
async save(sessionTrace: SessionTrace): Promise<void> {
|
||||
async insert(sessionTrace: SessionTrace): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(sessionTrace)
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
await this.ormRepository.insert(persistence)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { MapperInterface, SubscriptionPlanName, UniqueEntityId, Username, Uuid } from '@standardnotes/domain-core'
|
||||
import { SessionTrace } from '../Domain/Session/SessionTrace'
|
||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
export class SessionTracePersistenceMapper implements MapperInterface<SessionTrace, TypeORMSessionTrace> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
toDomain(projection: TypeORMSessionTrace): SessionTrace {
|
||||
const userUuidOrError = Uuid.create(projection.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
@@ -50,11 +53,7 @@ export class SessionTracePersistenceMapper implements MapperInterface<SessionTra
|
||||
typeOrm.username = domain.props.username.value
|
||||
typeOrm.subscriptionPlanName = domain.props.subscriptionPlanName ? domain.props.subscriptionPlanName.value : null
|
||||
typeOrm.createdAt = domain.props.createdAt
|
||||
typeOrm.creationDate = new Date(
|
||||
domain.props.createdAt.getFullYear(),
|
||||
domain.props.createdAt.getMonth(),
|
||||
domain.props.createdAt.getDate(),
|
||||
)
|
||||
typeOrm.creationDate = new Date(this.timer.convertDateToFormattedString(domain.props.createdAt, 'YYYY-MM-DD'))
|
||||
typeOrm.expiresAt = domain.props.expiresAt
|
||||
|
||||
return typeOrm
|
||||
|
||||
@@ -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.51.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.50.4...@standardnotes/common@1.51.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.50.4](https://github.com/standardnotes/server/compare/@standardnotes/common@1.50.3...@standardnotes/common@1.50.4) (2023-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/common
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.50.4",
|
||||
"version": "1.51.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "rm -fr dist",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest --coverage --no-cache"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.1",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.34.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.34.0...@standardnotes/domain-core@1.34.1) (2023-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing items in a vault and notifying about designated survivor ([#855](https://github.com/standardnotes/server/issues/855)) ([1d06ffe](https://github.com/standardnotes/server/commit/1d06ffe9d51722ada7baa040e1d5ed351fc28f39))
|
||||
|
||||
# [1.34.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.33.2...@standardnotes/domain-core@1.34.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.33.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.33.1...@standardnotes/domain-core@1.33.2) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **domain-core:** notification paylod to string casting ([a02a287](https://github.com/standardnotes/server/commit/a02a28774b6d500200043faefb9ebac3719e7661))
|
||||
|
||||
## [1.33.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.33.0...@standardnotes/domain-core@1.33.1) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.33.1",
|
||||
"version": "1.34.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -21,7 +21,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
"test": "jest --coverage --no-cache --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^9.0.0"
|
||||
|
||||
@@ -15,10 +15,10 @@ export class NotificationPayload extends ValueObject<NotificationPayloadProps> {
|
||||
return JSON.stringify({
|
||||
version: this.props.version,
|
||||
type: this.props.type.value,
|
||||
primaryIdentifier: this.props.primaryIdentifier,
|
||||
primaryIndentifierType: this.props.primaryIndentifierType,
|
||||
secondaryIdentifier: this.props.secondaryIdentifier,
|
||||
secondaryIdentifierType: this.props.secondaryIdentifierType,
|
||||
primaryIdentifier: this.props.primaryIdentifier.value,
|
||||
primaryIndentifierType: this.props.primaryIndentifierType.value,
|
||||
secondaryIdentifier: this.props.secondaryIdentifier?.value,
|
||||
secondaryIdentifierType: this.props.secondaryIdentifierType?.value,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NotificationPayloadIdentifierTypeProps } from './NotificationPayloadIde
|
||||
export class NotificationPayloadIdentifierType extends ValueObject<NotificationPayloadIdentifierTypeProps> {
|
||||
static readonly TYPES = {
|
||||
SharedVaultUuid: 'shared_vault_uuid',
|
||||
UserUuid: 'user_uuid',
|
||||
SharedVaultInviteUuid: 'shared_vault_invite_uuid',
|
||||
ItemUuid: 'item_uuid',
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
|
||||
SharedVaultItemRemoved: 'shared_vault_item_removed',
|
||||
SelfRemovedFromSharedVault: 'self_removed_from_shared_vault',
|
||||
UserRemovedFromSharedVault: 'user_removed_from_shared_vault',
|
||||
UserDesignatedAsSurvivor: 'user_designated_as_survivor',
|
||||
UserAddedToSharedVault: 'user_added_to_shared_vault',
|
||||
SharedVaultInviteCanceled: 'shared_vault_invite_canceled',
|
||||
SharedVaultFileUploaded: 'shared_vault_file_uploaded',
|
||||
|
||||
@@ -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.13.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.13.0...@standardnotes/domain-events-infra@1.13.1) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.34...@standardnotes/domain-events-infra@1.13.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.12.34](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.33...@standardnotes/domain-events-infra@1.12.34) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.34",
|
||||
"version": "1.13.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -21,7 +21,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest --coverage --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sns": "^3.332.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.
|
||||
|
||||
## [2.131.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.131.0...@standardnotes/domain-events@2.131.1) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [2.131.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.130.0...@standardnotes/domain-events@2.131.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
# [2.130.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.129.1...@standardnotes/domain-events@2.130.0) (2023-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.130.0",
|
||||
"version": "2.131.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "rm -fr dist",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
"test": "jest --coverage --no-cache --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.2](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.12.1...@standardnotes/event-store@1.12.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.12.1](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.12.0...@standardnotes/event-store@1.12.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.52...@standardnotes/event-store@1.12.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.11.52](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.51...@standardnotes/event-store@1.11.52) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.51](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.50...@standardnotes/event-store@1.11.51) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.51",
|
||||
"version": "1.12.2",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
@@ -13,7 +13,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js"
|
||||
},
|
||||
"author": "Karol Sójko <karol@standardnotes.com>",
|
||||
|
||||
@@ -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.25.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.24.1...@standardnotes/files-server@1.25.0) (2023-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* block file operations during transition ([#856](https://github.com/standardnotes/files/issues/856)) ([676cf36](https://github.com/standardnotes/files/commit/676cf36f8d769aa433880b2800f0457d06fbbf14))
|
||||
|
||||
## [1.24.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.24.0...@standardnotes/files-server@1.24.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.24.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.23.2...@standardnotes/files-server@1.24.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/files/issues/854)) ([ca6dbc0](https://github.com/standardnotes/files/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.23.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.23.1...@standardnotes/files-server@1.23.2) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.23.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.23.0...@standardnotes/files-server@1.23.1) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.23.1",
|
||||
"version": "1.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --fix --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js"
|
||||
},
|
||||
|
||||
@@ -198,7 +198,9 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new FSFileUploader(container.get(TYPES.Files_FILE_UPLOAD_PATH), container.get(TYPES.Files_Logger)),
|
||||
)
|
||||
container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(FSFileRemover)
|
||||
container
|
||||
.bind<FileRemoverInterface>(TYPES.Files_FileRemover)
|
||||
.toConstantValue(new FSFileRemover(container.get<string>(TYPES.Files_FILE_UPLOAD_PATH)))
|
||||
container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(FSFileMover)
|
||||
}
|
||||
|
||||
@@ -247,12 +249,26 @@ export class ContainerConfigLoader {
|
||||
// Handlers
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedSubscriptionInvitationCanceledEventHandler>(
|
||||
TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler,
|
||||
)
|
||||
.to(SharedSubscriptionInvitationCanceledEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedSubscriptionInvitationCanceledEventHandler(
|
||||
container.get<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Files_AccountDeletionRequestedEventHandler)],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type RemovedFileDescription = {
|
||||
userUuid: string
|
||||
userOrSharedVaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
|
||||
@@ -3,18 +3,17 @@ import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
@injectable()
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Files_MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
@inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
@@ -27,16 +26,23 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not mark files for removal for user ${event.payload.userUuid}: ${result.getError()}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const filesRemoved = result.getValue()
|
||||
|
||||
this.logger.debug(`Marked ${filesRemoved.length} files for removal for user ${event.payload.userUuid}`)
|
||||
|
||||
for (const fileRemoved of filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
userUuid: fileRemoved.userOrSharedVaultUuid,
|
||||
filePath: fileRemoved.filePath,
|
||||
fileName: fileRemoved.fileName,
|
||||
fileByteSize: fileRemoved.fileByteSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,17 @@ import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
|
||||
@injectable()
|
||||
export class SharedSubscriptionInvitationCanceledEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Files_MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
@inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
private markFilesToBeRemoved: MarkFilesToBeRemoved,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SharedSubscriptionInvitationCanceledEvent): Promise<void> {
|
||||
@@ -27,16 +26,25 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Could not mark files to be removed for invitee: ${event.payload.inviteeIdentifier}: ${result.getError()}`,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const filesRemoved = result.getValue()
|
||||
|
||||
this.logger.debug(`Marked ${filesRemoved.length} files for removal for invitee ${event.payload.inviteeIdentifier}`)
|
||||
|
||||
for (const fileRemoved of filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
userUuid: fileRemoved.userOrSharedVaultUuid,
|
||||
filePath: fileRemoved.filePath,
|
||||
fileName: fileRemoved.fileName,
|
||||
fileByteSize: fileRemoved.fileByteSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ export class SharedVaultRemovedEventHandler implements DomainEventHandlerInterfa
|
||||
|
||||
const filesRemoved = result.getValue()
|
||||
|
||||
this.logger.debug(
|
||||
`Marked ${filesRemoved.length} files for removal for shared vault ${event.payload.sharedVaultUuid}`,
|
||||
)
|
||||
|
||||
for (const fileRemoved of filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedVaultFileRemovedEvent({
|
||||
|
||||
@@ -2,5 +2,5 @@ import { RemovedFileDescription } from '../File/RemovedFileDescription'
|
||||
|
||||
export interface FileRemoverInterface {
|
||||
remove(filePath: string): Promise<number>
|
||||
markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>>
|
||||
markFilesToBeRemoved(userOrSharedVaultUuid: string): Promise<Array<RemovedFileDescription>>
|
||||
}
|
||||
|
||||
@@ -1,18 +1,42 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { promises } from 'fs'
|
||||
|
||||
import { FileRemoverInterface } from '../../Domain/Services/FileRemoverInterface'
|
||||
import { RemovedFileDescription } from '../../Domain/File/RemovedFileDescription'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class FSFileRemover implements FileRemoverInterface {
|
||||
constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
|
||||
constructor(private fileUploadPath: string) {}
|
||||
|
||||
async markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>> {
|
||||
await promises.rmdir(`${this.fileUploadPath}/${userUuid}`)
|
||||
async markFilesToBeRemoved(userOrSharedVaultUuid: string): Promise<Array<RemovedFileDescription>> {
|
||||
const removedFileDescriptions: RemovedFileDescription[] = []
|
||||
|
||||
return []
|
||||
let directoryExists: boolean
|
||||
try {
|
||||
await promises.access(`${this.fileUploadPath}/${userOrSharedVaultUuid}`)
|
||||
directoryExists = true
|
||||
} catch (error) {
|
||||
directoryExists = false
|
||||
}
|
||||
|
||||
if (!directoryExists) {
|
||||
return []
|
||||
}
|
||||
|
||||
const files = await promises.readdir(`${this.fileUploadPath}/${userOrSharedVaultUuid}`, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = `${this.fileUploadPath}/${userOrSharedVaultUuid}/${file.name}`
|
||||
|
||||
const fileByteSize = await this.remove(`${userOrSharedVaultUuid}/${file.name}`)
|
||||
|
||||
removedFileDescriptions.push({
|
||||
filePath,
|
||||
fileByteSize,
|
||||
userOrSharedVaultUuid,
|
||||
fileName: file.name,
|
||||
})
|
||||
}
|
||||
|
||||
return removedFileDescriptions
|
||||
}
|
||||
|
||||
async remove(filePath: string): Promise<number> {
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('ValetTokenAuthMiddleware', () => {
|
||||
|
||||
const logger = {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
} as unknown as jest.Mocked<Logger>
|
||||
|
||||
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, logger)
|
||||
@@ -222,4 +223,27 @@ describe('ValetTokenAuthMiddleware', () => {
|
||||
|
||||
expect(next).toHaveBeenCalledWith(error)
|
||||
})
|
||||
|
||||
it('should throw an error if the valet token indicates an ongoing transition', async () => {
|
||||
request.headers['x-valet-token'] = 'valet-token'
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
userUuid: '1-2-3',
|
||||
permittedResources: [
|
||||
{
|
||||
remoteIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
unencryptedFileSize: 30,
|
||||
},
|
||||
],
|
||||
permittedOperation: 'write',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 80,
|
||||
ongoingTransition: true,
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(500)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,19 @@ export class ValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
if (valetTokenData.ongoingTransition === true) {
|
||||
this.logger.error(`Cannot perform file operations for user ${valetTokenData.userUuid} during transition`)
|
||||
|
||||
response.status(500).send({
|
||||
error: {
|
||||
tag: 'ongoing-transition',
|
||||
message: 'Cannot perform file operations during transition',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const resource of valetTokenData.permittedResources) {
|
||||
const resourceUuidOrError = Uuid.create(resource.remoteIdentifier)
|
||||
if (resourceUuidOrError.isFailed()) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export class S3FileRemover implements FileRemoverInterface {
|
||||
fileByteSize: file.Size as number,
|
||||
fileName: file.Key.replace(`${userUuid}/`, ''),
|
||||
filePath: file.Key,
|
||||
userUuid,
|
||||
userOrSharedVaultUuid: userUuid,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.16.9](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.8...@standardnotes/home-server@1.16.9) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.7...@standardnotes/home-server@1.16.8) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.7](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.6...@standardnotes/home-server@1.16.7) (2023-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.6](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.5...@standardnotes/home-server@1.16.6) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.5](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.4...@standardnotes/home-server@1.16.5) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.16.5",
|
||||
"version": "1.16.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.7.0](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.6.11...@standardnotes/predicates@1.7.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.6.11](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.6.10...@standardnotes/predicates@1.6.11) (2023-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/predicates",
|
||||
"version": "1.6.11",
|
||||
"version": "1.7.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
"test": "jest --coverage --no-cache --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.1",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.38.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.38.1...@standardnotes/revisions-server@1.38.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.38.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.38.0...@standardnotes/revisions-server@1.38.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
# [1.38.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.37.3...@standardnotes/revisions-server@1.38.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.37.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.37.2...@standardnotes/revisions-server@1.37.3) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.37.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.37.1...@standardnotes/revisions-server@1.37.2) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.37.2",
|
||||
"version": "1.38.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js"
|
||||
},
|
||||
|
||||
@@ -71,6 +71,7 @@ import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionReq
|
||||
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
|
||||
import { TransitionRepositoryInterface } from '../Domain/Transition/TransitionRepositoryInterface'
|
||||
import { RedisTransitionRepository } from '../Infra/Redis/RedisTransitionRepository'
|
||||
import { CreateRevisionFromDump } from '../Domain/UseCase/CreateRevisionFromDump/CreateRevisionFromDump'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -330,6 +331,21 @@ export class ContainerConfigLoader {
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new RevisionMetadataHttpMapper(context.container.get(TYPES.Revisions_GetRequiredRoleToViewRevision))
|
||||
})
|
||||
container
|
||||
.bind<MapperInterface<Revision, string>>(TYPES.Revisions_RevisionItemStringMapper)
|
||||
.toDynamicValue(() => new RevisionItemStringMapper())
|
||||
|
||||
container
|
||||
.bind<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository)
|
||||
.toConstantValue(
|
||||
env.get('S3_AWS_REGION', true)
|
||||
? new S3DumpRepository(
|
||||
container.get(TYPES.Revisions_S3_BACKUP_BUCKET_NAME),
|
||||
container.get(TYPES.Revisions_S3),
|
||||
container.get(TYPES.Revisions_RevisionItemStringMapper),
|
||||
)
|
||||
: new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)),
|
||||
)
|
||||
|
||||
// use cases
|
||||
container
|
||||
@@ -385,6 +401,14 @@ export class ContainerConfigLoader {
|
||||
: container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<CreateRevisionFromDump>(TYPES.Revisions_CreateRevisionFromDump)
|
||||
.toConstantValue(
|
||||
new CreateRevisionFromDump(
|
||||
container.get<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository),
|
||||
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
|
||||
),
|
||||
)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
@@ -409,31 +433,12 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
})
|
||||
|
||||
// Map
|
||||
container
|
||||
.bind<MapperInterface<Revision, string>>(TYPES.Revisions_RevisionItemStringMapper)
|
||||
.toDynamicValue(() => new RevisionItemStringMapper())
|
||||
|
||||
container
|
||||
.bind<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository)
|
||||
.toConstantValue(
|
||||
env.get('S3_AWS_REGION', true)
|
||||
? new S3DumpRepository(
|
||||
container.get(TYPES.Revisions_S3_BACKUP_BUCKET_NAME),
|
||||
container.get(TYPES.Revisions_S3),
|
||||
container.get(TYPES.Revisions_RevisionItemStringMapper),
|
||||
container.get(TYPES.Revisions_Logger),
|
||||
)
|
||||
: new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)),
|
||||
)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<ItemDumpedEventHandler>(TYPES.Revisions_ItemDumpedEventHandler)
|
||||
.toConstantValue(
|
||||
new ItemDumpedEventHandler(
|
||||
container.get<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository),
|
||||
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
|
||||
container.get<CreateRevisionFromDump>(TYPES.Revisions_CreateRevisionFromDump),
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ const TYPES = {
|
||||
'Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
Revisions_RemoveRevisionsFromSharedVault: Symbol.for('Revisions_RemoveRevisionsFromSharedVault'),
|
||||
Revisions_CreateRevisionFromDump: Symbol.for('Revisions_CreateRevisionFromDump'),
|
||||
// Controller
|
||||
Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
|
||||
Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { Revision } from '../Revision/Revision'
|
||||
|
||||
export interface DumpRepositoryInterface {
|
||||
getRevisionFromDumpPath(path: string): Promise<Revision | null>
|
||||
getRevisionFromDumpPath(path: string): Promise<Result<Revision>>
|
||||
removeDump(path: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ItemDumpedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { Uuid, ContentType, Dates } from '@standardnotes/domain-core'
|
||||
|
||||
import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
|
||||
import { Revision } from '../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
|
||||
import { ItemDumpedEventHandler } from './ItemDumpedEventHandler'
|
||||
import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
|
||||
|
||||
describe('ItemDumpedEventHandler', () => {
|
||||
let dumpRepository: DumpRepositoryInterface
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
let revisionRepositoryResolver: RevisionRepositoryResolverInterface
|
||||
let revision: Revision
|
||||
let event: ItemDumpedEvent
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () => new ItemDumpedEventHandler(dumpRepository, revisionRepositoryResolver, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
revision = Revision.create({
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
}).getValue()
|
||||
|
||||
dumpRepository = {} as jest.Mocked<DumpRepositoryInterface>
|
||||
dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(revision)
|
||||
dumpRepository.removeDump = jest.fn()
|
||||
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.insert = jest.fn()
|
||||
|
||||
revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
|
||||
revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
|
||||
|
||||
event = {} as jest.Mocked<ItemDumpedEvent>
|
||||
event.payload = {
|
||||
fileDumpPath: 'foobar',
|
||||
roleNames: ['CORE_USER'],
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should save a revision from file dump', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionRepository.insert).toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing if role names are not valid', async () => {
|
||||
event.payload.roleNames = ['INVALID_ROLE_NAME']
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionRepository.insert).not.toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save a revision if it could not be created from dump', async () => {
|
||||
dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionRepository.insert).not.toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,44 +1,22 @@
|
||||
import { DomainEventHandlerInterface, ItemDumpedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
|
||||
import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
|
||||
import { RoleNameCollection } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import { CreateRevisionFromDump } from '../UseCase/CreateRevisionFromDump/CreateRevisionFromDump'
|
||||
|
||||
export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private dumpRepository: DumpRepositoryInterface,
|
||||
private revisionRepositoryResolver: RevisionRepositoryResolverInterface,
|
||||
private createRevisionFromDump: CreateRevisionFromDump,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemDumpedEvent): Promise<void> {
|
||||
const revision = await this.dumpRepository.getRevisionFromDumpPath(event.payload.fileDumpPath)
|
||||
if (revision === null) {
|
||||
this.logger.error(`Revision not found for dump path ${event.payload.fileDumpPath}`)
|
||||
const result = await this.createRevisionFromDump.execute({
|
||||
filePath: event.payload.fileDumpPath,
|
||||
roleNames: event.payload.roleNames,
|
||||
})
|
||||
|
||||
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
|
||||
|
||||
return
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Item dumped event handler failed: ${result.getError()}`)
|
||||
}
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
this.logger.error(`Invalid role names ${event.payload.roleNames}`)
|
||||
|
||||
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
|
||||
|
||||
return
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
|
||||
|
||||
const successfullyInserted = await revisionRepository.insert(revision)
|
||||
if (!successfullyInserted) {
|
||||
this.logger.error(`Could not insert revision ${revision.id.toString()}`)
|
||||
}
|
||||
|
||||
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Uuid, ContentType, Dates, Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { DumpRepositoryInterface } from '../../Dump/DumpRepositoryInterface'
|
||||
import { Revision } from '../../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
|
||||
import { CreateRevisionFromDump } from './CreateRevisionFromDump'
|
||||
|
||||
describe('CreateRevisionFromDump', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
let revision: Revision
|
||||
let dumpRepository: DumpRepositoryInterface
|
||||
let revisionRepositoryResolver: RevisionRepositoryResolverInterface
|
||||
|
||||
const createUseCase = () => new CreateRevisionFromDump(dumpRepository, revisionRepositoryResolver)
|
||||
|
||||
beforeEach(() => {
|
||||
revision = Revision.create({
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
}).getValue()
|
||||
|
||||
dumpRepository = {} as jest.Mocked<DumpRepositoryInterface>
|
||||
dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(Result.ok(revision))
|
||||
dumpRepository.removeDump = jest.fn()
|
||||
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.insert = jest.fn().mockReturnValue(true)
|
||||
|
||||
revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
|
||||
revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
|
||||
})
|
||||
|
||||
it('should create a revision from file dump', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
filePath: 'foobar',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(revisionRepository.insert).toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if file path is empty', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
filePath: '',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(revisionRepository.insert).not.toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if role name is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
filePath: 'foobar',
|
||||
roleNames: ['INVALID_ROLE_NAME'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(revisionRepository.insert).not.toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if revision cannot be found', async () => {
|
||||
dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
filePath: 'foobar',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(revisionRepository.insert).not.toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if revision cannot be inserted', async () => {
|
||||
revisionRepository.insert = jest.fn().mockReturnValue(false)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
filePath: 'foobar',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(revisionRepository.insert).toHaveBeenCalled()
|
||||
expect(dumpRepository.removeDump).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Result, RoleNameCollection, UseCaseInterface, Validator } from '@standardnotes/domain-core'
|
||||
import { DumpRepositoryInterface } from '../../Dump/DumpRepositoryInterface'
|
||||
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
|
||||
import { CreateRevisionFromDumpDTO } from './CreateRevisionFromDumpDTO'
|
||||
|
||||
export class CreateRevisionFromDump implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private dumpRepository: DumpRepositoryInterface,
|
||||
private revisionRepositoryResolver: RevisionRepositoryResolverInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateRevisionFromDumpDTO): Promise<Result<void>> {
|
||||
const filePathValidationResult = Validator.isNotEmptyString(dto.filePath)
|
||||
if (filePathValidationResult.isFailed()) {
|
||||
return Result.fail(`Could not create revision from dump: ${filePathValidationResult.getError()}`)
|
||||
}
|
||||
|
||||
const revisionOrError = await this.dumpRepository.getRevisionFromDumpPath(dto.filePath)
|
||||
if (revisionOrError.isFailed()) {
|
||||
await this.dumpRepository.removeDump(dto.filePath)
|
||||
|
||||
return Result.fail(`Could not create revision from dump: ${revisionOrError.getError()}`)
|
||||
}
|
||||
const revision = revisionOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
await this.dumpRepository.removeDump(dto.filePath)
|
||||
|
||||
return Result.fail(`Could not create revision from dump: ${roleNamesOrError.getError()}`)
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
|
||||
|
||||
const successfullyInserted = await revisionRepository.insert(revision)
|
||||
if (!successfullyInserted) {
|
||||
await this.dumpRepository.removeDump(dto.filePath)
|
||||
|
||||
return Result.fail(`Could not insert revision from dump: ${revision.id.toString()}`)
|
||||
}
|
||||
|
||||
await this.dumpRepository.removeDump(dto.filePath)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CreateRevisionFromDumpDTO {
|
||||
filePath: string
|
||||
roleNames: string[]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { MapperInterface, Result } from '@standardnotes/domain-core'
|
||||
import { promises } from 'fs'
|
||||
|
||||
import { DumpRepositoryInterface } from '../../Domain/Dump/DumpRepositoryInterface'
|
||||
@@ -7,12 +7,16 @@ import { Revision } from '../../Domain/Revision/Revision'
|
||||
export class FSDumpRepository implements DumpRepositoryInterface {
|
||||
constructor(private revisionStringItemMapper: MapperInterface<Revision, string>) {}
|
||||
|
||||
async getRevisionFromDumpPath(path: string): Promise<Revision | null> {
|
||||
const contents = (await promises.readFile(path)).toString()
|
||||
async getRevisionFromDumpPath(path: string): Promise<Result<Revision>> {
|
||||
try {
|
||||
const contents = (await promises.readFile(path)).toString()
|
||||
|
||||
const revision = this.revisionStringItemMapper.toDomain(contents)
|
||||
const revision = this.revisionStringItemMapper.toDomain(contents)
|
||||
|
||||
return revision
|
||||
return Result.ok(revision)
|
||||
} catch (error) {
|
||||
return Result.fail(`Failed to read dump file: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async removeDump(path: string): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DeleteObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import { MapperInterface, Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { DumpRepositoryInterface } from '../../Domain/Dump/DumpRepositoryInterface'
|
||||
import { Revision } from '../../Domain/Revision/Revision'
|
||||
@@ -10,26 +9,27 @@ export class S3DumpRepository implements DumpRepositoryInterface {
|
||||
private dumpBucketName: string,
|
||||
private s3Client: S3Client,
|
||||
private revisionStringItemMapper: MapperInterface<Revision, string>,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async getRevisionFromDumpPath(path: string): Promise<Revision | null> {
|
||||
const s3Object = await this.s3Client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: this.dumpBucketName,
|
||||
Key: path,
|
||||
}),
|
||||
)
|
||||
async getRevisionFromDumpPath(path: string): Promise<Result<Revision>> {
|
||||
try {
|
||||
const s3Object = await this.s3Client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: this.dumpBucketName,
|
||||
Key: path,
|
||||
}),
|
||||
)
|
||||
|
||||
if (s3Object.Body === undefined) {
|
||||
this.logger.warn(`Could not find revision dump at path: ${path}`)
|
||||
if (s3Object.Body === undefined) {
|
||||
return Result.fail(`Could not find revision dump at path: ${path}`)
|
||||
}
|
||||
|
||||
return null
|
||||
const revision = this.revisionStringItemMapper.toDomain(await s3Object.Body.transformToString())
|
||||
|
||||
return Result.ok(revision)
|
||||
} catch (error) {
|
||||
return Result.fail(`Failed to read dump file: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
const revision = this.revisionStringItemMapper.toDomain(await s3Object.Body.transformToString())
|
||||
|
||||
return revision
|
||||
}
|
||||
|
||||
async removeDump(path: string): Promise<void> {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.21.1...@standardnotes/scheduler-server@1.21.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.21.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.21.0...@standardnotes/scheduler-server@1.21.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.56...@standardnotes/scheduler-server@1.21.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.20.56](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.55...@standardnotes/scheduler-server@1.20.56) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.55](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.54...@standardnotes/scheduler-server@1.20.55) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.55",
|
||||
"version": "1.21.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -16,7 +16,7 @@
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"verify:jobs": "yarn node dist/bin/verify.js",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
|
||||
@@ -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.15.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.14.0...@standardnotes/security@1.15.0) (2023-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* block file operations during transition ([#856](https://github.com/standardnotes/server/issues/856)) ([676cf36](https://github.com/standardnotes/server/commit/676cf36f8d769aa433880b2800f0457d06fbbf14))
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.13.1...@standardnotes/security@1.14.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.13.0...@standardnotes/security@1.13.1) (2023-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.13.1",
|
||||
"version": "1.15.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest --coverage --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
||||
@@ -11,4 +11,5 @@ export type ValetTokenData = {
|
||||
}>
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
ongoingTransition?: boolean
|
||||
}
|
||||
|
||||
@@ -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.21.41](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.40...@standardnotes/settings@1.21.41) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.40](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.39...@standardnotes/settings@1.21.40) (2023-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.39](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.38...@standardnotes/settings@1.21.39) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.38](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.37...@standardnotes/settings@1.21.38) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.38",
|
||||
"version": "1.21.41",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.109.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.109.1...@standardnotes/syncing-server@1.109.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.109.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.109.0...@standardnotes/syncing-server@1.109.1) (2023-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing items in a vault and notifying about designated survivor ([#855](https://github.com/standardnotes/syncing-server-js/issues/855)) ([1d06ffe](https://github.com/standardnotes/syncing-server-js/commit/1d06ffe9d51722ada7baa040e1d5ed351fc28f39))
|
||||
|
||||
# [1.109.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.108.2...@standardnotes/syncing-server@1.109.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/syncing-server-js/issues/854)) ([ca6dbc0](https://github.com/standardnotes/syncing-server-js/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.108.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.108.1...@standardnotes/syncing-server@1.108.2) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.108.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.108.0...@standardnotes/syncing-server@1.108.1) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.108.1",
|
||||
"version": "1.109.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"content-size": "yarn node dist/bin/content.js",
|
||||
|
||||
@@ -175,6 +175,7 @@ import { TransferSharedVault } from '../Domain/UseCase/SharedVaults/TransferShar
|
||||
import { TransitionRepositoryInterface } from '../Domain/Transition/TransitionRepositoryInterface'
|
||||
import { RedisTransitionRepository } from '../Infra/Redis/RedisTransitionRepository'
|
||||
import { TransferSharedVaultItems } from '../Domain/UseCase/SharedVaults/TransferSharedVaultItems/TransferSharedVaultItems'
|
||||
import { DumpItem } from '../Domain/UseCase/Syncing/DumpItem/DumpItem'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -590,6 +591,24 @@ export class ContainerConfigLoader {
|
||||
]),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toConstantValue(
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
// use cases
|
||||
container
|
||||
.bind<GetItems>(TYPES.Sync_GetItems)
|
||||
@@ -880,6 +899,7 @@ export class ContainerConfigLoader {
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -932,6 +952,16 @@ export class ContainerConfigLoader {
|
||||
container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DumpItem>(TYPES.Sync_DumpItem)
|
||||
.toConstantValue(
|
||||
new DumpItem(
|
||||
container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container
|
||||
@@ -959,24 +989,6 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
})
|
||||
|
||||
container
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toConstantValue(
|
||||
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)
|
||||
@@ -1002,10 +1014,8 @@ export class ContainerConfigLoader {
|
||||
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DumpItem>(TYPES.Sync_DumpItem),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
|
||||
@@ -92,6 +92,7 @@ const TYPES = {
|
||||
Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),
|
||||
Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
|
||||
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
|
||||
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { RoleNameCollection } from '@standardnotes/domain-core'
|
||||
import { RoleNameCollection, Uuid } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
@@ -15,15 +15,25 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.error(`AccountDeletionRequestedEventHandler failed: ${userUuidOrError.getError()}`)
|
||||
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
this.logger.error(`AccountDeletionRequestedEventHandler failed: ${roleNamesOrError.getError()}`)
|
||||
|
||||
return
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
await itemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
await itemRepository.deleteByUserUuidAndNotInSharedVault(userUuid)
|
||||
|
||||
const deletingVaultsResult = await this.deleteSharedVaults.execute({
|
||||
ownerUuid: event.payload.userUuid,
|
||||
@@ -34,6 +44,16 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
)
|
||||
}
|
||||
|
||||
const deletedSharedVaultUuids = Array.from(deletingVaultsResult.getValue().keys())
|
||||
|
||||
this.logger.debug(
|
||||
`Deleting items from shared vaults: ${deletedSharedVaultUuids.map((uuid) => uuid.value).join(', ')}`,
|
||||
)
|
||||
|
||||
if (deletedSharedVaultUuids.length !== 0) {
|
||||
await itemRepository.deleteByUserUuidInSharedVaults(userUuid, deletedSharedVaultUuids)
|
||||
}
|
||||
|
||||
const deletingUserFromOtherVaultsResult = await this.removeUserFromSharedVaults.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DomainEventService,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let event: ItemRevisionCreationRequestedEvent
|
||||
let item: Item
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
itemRepositoryResolver,
|
||||
itemBackupService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
item = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['CORE_USER'],
|
||||
}
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
}
|
||||
|
||||
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
|
||||
itemBackupService.dump = jest.fn().mockReturnValue('foo://bar')
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createItemDumpedEvent = jest.fn()
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a revision for an item', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing if roles names are not valid', async () => {
|
||||
event.payload.roleNames = ['INVALID_ROLE_NAME']
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a revision for an item that does not exist', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a revision for an item if the dump was not created', async () => {
|
||||
itemBackupService.dump = jest.fn().mockReturnValue('')
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a revision if the item uuid is invalid', async () => {
|
||||
event.payload.itemUuid = 'invalid-uuid'
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,59 +1,22 @@
|
||||
import {
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { RoleNameCollection, Uuid } from '@standardnotes/domain-core'
|
||||
import { ItemRevisionCreationRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
import { DumpItem } from '../UseCase/Syncing/DumpItem/DumpItem'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private dumpItem: DumpItem,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
|
||||
const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
const result = await this.dumpItem.execute({
|
||||
itemUuid: event.payload.itemUuid,
|
||||
roleNames: event.payload.roleNames,
|
||||
})
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
await this.createItemDump(event, itemRepository)
|
||||
}
|
||||
|
||||
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 itemRepository.findByUuid(itemUuid)
|
||||
if (item === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileDumpPath = await this.itemBackupService.dump(item)
|
||||
if (fileDumpPath) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemDumpedEvent({
|
||||
fileDumpPath,
|
||||
userUuid: event.meta.correlation.userIdentifier,
|
||||
roleNames: event.payload.roleNames,
|
||||
}),
|
||||
)
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Item revision requested handler failed: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from './Item'
|
||||
|
||||
export interface ItemBackupServiceInterface {
|
||||
backup(items: Array<Item>, authParams: KeyParamsData, contentSizeLimit?: number): Promise<string[]>
|
||||
dump(item: Item): Promise<string>
|
||||
dump(item: Item): Promise<Result<string>>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemRepositoryInterface {
|
||||
deleteByUserUuid(userUuid: string): Promise<void>
|
||||
deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void>
|
||||
deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void>
|
||||
findAll(query: ItemQuery): Promise<Item[]>
|
||||
countAll(query: ItemQuery): Promise<number>
|
||||
findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<Array<ItemContentSizeDescriptor>>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CancelInviteToSharedVault } from '../CancelInviteToSharedVault/CancelIn
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
|
||||
|
||||
export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
export class DeleteSharedVault implements UseCaseInterface<{ status: 'deleted' | 'transitioned' }> {
|
||||
constructor(
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
@@ -22,7 +22,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
private transferSharedVault: TransferSharedVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
|
||||
async execute(dto: DeleteSharedVaultDTO): Promise<Result<{ status: 'deleted' | 'transitioned' }>> {
|
||||
const originatorUuidOrError = Uuid.create(dto.originatorUuid)
|
||||
if (originatorUuidOrError.isFailed()) {
|
||||
return Result.fail(originatorUuidOrError.getError())
|
||||
@@ -79,7 +79,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
return Result.fail(removingOwnerFromSharedVaultResult.getError())
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok({ status: 'transitioned' })
|
||||
}
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
@@ -105,6 +105,6 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
}),
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok({ status: 'deleted' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('DeleteSharedVaults', () => {
|
||||
sharedVaultRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVault])
|
||||
|
||||
deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
|
||||
deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.ok())
|
||||
deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.ok({ status: 'deleted' }))
|
||||
})
|
||||
|
||||
it('should delete all shared vaults for a user', async () => {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { DeleteSharedVaultsDTO } from './DeleteSharedVaultsDTO'
|
||||
import { DeleteSharedVault } from '../DeleteSharedVault/DeleteSharedVault'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
|
||||
export class DeleteSharedVaults implements UseCaseInterface<void> {
|
||||
export class DeleteSharedVaults implements UseCaseInterface<Map<Uuid, 'deleted' | 'transitioned'>> {
|
||||
constructor(
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
private deleteSharedVaultUseCase: DeleteSharedVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteSharedVaultsDTO): Promise<Result<void>> {
|
||||
async execute(dto: DeleteSharedVaultsDTO): Promise<Result<Map<Uuid, 'deleted' | 'transitioned'>>> {
|
||||
const ownerUuidOrError = Uuid.create(dto.ownerUuid)
|
||||
if (ownerUuidOrError.isFailed()) {
|
||||
return Result.fail(ownerUuidOrError.getError())
|
||||
@@ -19,6 +19,7 @@ export class DeleteSharedVaults implements UseCaseInterface<void> {
|
||||
|
||||
const sharedVaults = await this.sharedVaultRepository.findByUserUuid(ownerUuid)
|
||||
|
||||
const results = new Map<Uuid, 'deleted' | 'transitioned'>()
|
||||
for (const sharedVault of sharedVaults) {
|
||||
const result = await this.deleteSharedVaultUseCase.execute({
|
||||
originatorUuid: ownerUuid.value,
|
||||
@@ -27,8 +28,10 @@ export class DeleteSharedVaults implements UseCaseInterface<void> {
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
results.set(sharedVault.uuid, result.getValue().status)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import {
|
||||
NotificationPayload,
|
||||
Result,
|
||||
SharedVaultUser,
|
||||
SharedVaultUserPermission,
|
||||
Timestamps,
|
||||
Uuid,
|
||||
} from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { DesignateSurvivor } from './DesignateSurvivor'
|
||||
@@ -7,6 +14,7 @@ import { DomainEventInterface, DomainEventPublisherInterface } from '@standardno
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
|
||||
describe('DesignateSurvivor', () => {
|
||||
let sharedVault: SharedVault
|
||||
@@ -17,6 +25,7 @@ describe('DesignateSurvivor', () => {
|
||||
let timer: TimerInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let addNotificationForUser: AddNotificationForUser
|
||||
|
||||
const createUseCase = () =>
|
||||
new DesignateSurvivor(
|
||||
@@ -25,6 +34,7 @@ describe('DesignateSurvivor', () => {
|
||||
timer,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
addNotificationForUser,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -68,6 +78,9 @@ describe('DesignateSurvivor', () => {
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
|
||||
addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should fail if shared vault uuid is invalid', async () => {
|
||||
@@ -189,4 +202,39 @@ describe('DesignateSurvivor', () => {
|
||||
expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
|
||||
expect(sharedVaultUserRepository.save).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should fail if it fails to add notification for user', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
|
||||
|
||||
addNotificationForUser.execute = jest.fn().mockReturnValue(Result.fail('Failed to add notification'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if it fails to create notification payload', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
|
||||
|
||||
const mock = jest.spyOn(NotificationPayload, 'create')
|
||||
mock.mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import {
|
||||
NotificationPayload,
|
||||
NotificationPayloadIdentifierType,
|
||||
NotificationType,
|
||||
Result,
|
||||
SharedVaultUser,
|
||||
SharedVaultUserPermission,
|
||||
@@ -13,6 +16,7 @@ import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/Sh
|
||||
import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
|
||||
export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@@ -21,6 +25,7 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
private timer: TimerInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private addNotificationForUser: AddNotificationForUser,
|
||||
) {}
|
||||
|
||||
async execute(dto: DesignateSurvivorDTO): Promise<Result<void>> {
|
||||
@@ -91,6 +96,13 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
|
||||
await this.sharedVaultUserRepository.save(toBeDesignatedAsASurvivor)
|
||||
|
||||
sharedVault.props.timestamps = Timestamps.create(
|
||||
sharedVault.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultRepository.save(sharedVault)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserDesignatedAsSurvivorInSharedVaultEvent({
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
@@ -99,12 +111,32 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
}),
|
||||
)
|
||||
|
||||
sharedVault.props.timestamps = Timestamps.create(
|
||||
sharedVault.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
const notificationPayloadOrError = NotificationPayload.create({
|
||||
primaryIdentifier: sharedVault.uuid,
|
||||
primaryIndentifierType: NotificationPayloadIdentifierType.create(
|
||||
NotificationPayloadIdentifierType.TYPES.SharedVaultUuid,
|
||||
).getValue(),
|
||||
secondaryIdentifier: userUuid,
|
||||
secondaryIdentifierType: NotificationPayloadIdentifierType.create(
|
||||
NotificationPayloadIdentifierType.TYPES.UserUuid,
|
||||
).getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.UserDesignatedAsSurvivor).getValue(),
|
||||
version: '1.0',
|
||||
})
|
||||
if (notificationPayloadOrError.isFailed()) {
|
||||
return Result.fail(notificationPayloadOrError.getError())
|
||||
}
|
||||
const notificationPayload = notificationPayloadOrError.getValue()
|
||||
|
||||
await this.sharedVaultRepository.save(sharedVault)
|
||||
const result = await this.addNotificationForUser.execute({
|
||||
userUuid: sharedVault.props.userUuid.value,
|
||||
type: NotificationType.TYPES.UserDesignatedAsSurvivor,
|
||||
payload: notificationPayload,
|
||||
version: '1.0',
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { ItemBackupServiceInterface } from '../../../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
import { DumpItem } from './DumpItem'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('DumpItem', () => {
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let item: Item
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new DumpItem(itemRepositoryResolver, itemBackupService, domainEventFactory, domainEventPublisher)
|
||||
|
||||
beforeEach(() => {
|
||||
item = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(item)
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
|
||||
itemBackupService.dump = jest.fn().mockResolvedValue(Result.ok('dump-path'))
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createItemDumpedEvent = jest.fn().mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should dump item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
|
||||
expect(itemBackupService.dump).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if item cannot be found', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if item cannot be dumped', async () => {
|
||||
itemBackupService.dump = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if item uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: 'invalid-uuid',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if role names are invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['invalid-role'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DumpItemDTO } from './DumpItemDTO'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { ItemBackupServiceInterface } from '../../../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class DumpItem implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DumpItemDTO): Promise<Result<void>> {
|
||||
const itemUuidOrError = Uuid.create(dto.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return Result.fail(`Failed to dump item: ${itemUuidOrError.getError()}`)
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const itemRepositoryOrError = this.getItemRepository(dto.roleNames)
|
||||
if (itemRepositoryOrError.isFailed()) {
|
||||
return Result.fail(`Failed to dump item: ${itemRepositoryOrError.getError()}`)
|
||||
}
|
||||
const itemRepository = itemRepositoryOrError.getValue()
|
||||
|
||||
const item = await itemRepository.findByUuid(itemUuid)
|
||||
if (item === null) {
|
||||
return Result.fail('Failed to dump item: Item not found')
|
||||
}
|
||||
|
||||
const fileDumpPathOrError = await this.itemBackupService.dump(item)
|
||||
if (fileDumpPathOrError.isFailed()) {
|
||||
return Result.fail(`Failed to dump item: ${fileDumpPathOrError.getError()}`)
|
||||
}
|
||||
const fileDumpPath = fileDumpPathOrError.getValue()
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemDumpedEvent({
|
||||
fileDumpPath,
|
||||
userUuid: item.props.userUuid.value,
|
||||
roleNames: dto.roleNames,
|
||||
}),
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private getItemRepository(stringRoleNames: string[]): Result<ItemRepositoryInterface> {
|
||||
const roleNamesOrError = RoleNameCollection.create(stringRoleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(`Could not obtain item repository based on role names: ${roleNamesOrError.getError()}`)
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
return Result.ok(itemRepository)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DumpItemDTO {
|
||||
itemUuid: string
|
||||
roleNames: string[]
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
try {
|
||||
this.logger.info(`[${userUuid.value}] Cleaning up primary database items`)
|
||||
|
||||
await itemRepository.deleteByUserUuid(userUuid.value)
|
||||
await itemRepository.deleteByUserUuidAndNotInSharedVault(userUuid)
|
||||
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { MapperInterface, Result } from '@standardnotes/domain-core'
|
||||
import { promises } from 'fs'
|
||||
import * as uuid from 'uuid'
|
||||
import { Logger } from 'winston'
|
||||
@@ -20,25 +20,29 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
async dump(item: Item): Promise<string> {
|
||||
const contents = JSON.stringify({
|
||||
item: this.mapper.toProjection(item),
|
||||
})
|
||||
async dump(item: Item): Promise<Result<string>> {
|
||||
try {
|
||||
const contents = JSON.stringify({
|
||||
item: this.mapper.toProjection(item),
|
||||
})
|
||||
|
||||
const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
|
||||
const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
|
||||
|
||||
this.logger.debug(`Dumping item ${item.id.toString()} to ${path}`)
|
||||
this.logger.debug(`Dumping item ${item.id.toString()} to ${path}`)
|
||||
|
||||
await promises.mkdir(dirname(path), { recursive: true })
|
||||
await promises.mkdir(dirname(path), { recursive: true })
|
||||
|
||||
await promises.writeFile(path, contents)
|
||||
await promises.writeFile(path, contents)
|
||||
|
||||
const fileCreated = (await promises.stat(path)).isFile()
|
||||
const fileCreated = (await promises.stat(path)).isFile()
|
||||
|
||||
if (!fileCreated) {
|
||||
throw new Error(`Could not create dump file ${path}`)
|
||||
if (!fileCreated) {
|
||||
return Result.fail(`Could not create dump file ${path}`)
|
||||
}
|
||||
|
||||
return Result.ok(path)
|
||||
} catch (error) {
|
||||
return Result.fail(`Could not dump item: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Logger } from 'winston'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { MapperInterface, Result } from '@standardnotes/domain-core'
|
||||
import { ItemBackupRepresentation } from '../../Mapping/Backup/ItemBackupRepresentation'
|
||||
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
|
||||
|
||||
@@ -18,25 +18,29 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
|
||||
private s3Client?: S3Client,
|
||||
) {}
|
||||
|
||||
async dump(item: Item): Promise<string> {
|
||||
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||
this.logger.warn('S3 backup not configured')
|
||||
async dump(item: Item): Promise<Result<string>> {
|
||||
try {
|
||||
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||
this.logger.warn('S3 backup not configured')
|
||||
|
||||
return ''
|
||||
}
|
||||
return Result.fail('S3 backup not configured')
|
||||
}
|
||||
|
||||
const s3Key = uuid.v4()
|
||||
await this.s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.s3BackupBucketName,
|
||||
Key: s3Key,
|
||||
Body: JSON.stringify({
|
||||
item: this.backupMapper.toProjection(item),
|
||||
const s3Key = uuid.v4()
|
||||
await this.s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.s3BackupBucketName,
|
||||
Key: s3Key,
|
||||
Body: JSON.stringify({
|
||||
item: this.backupMapper.toProjection(item),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
return s3Key
|
||||
return Result.ok(s3Key)
|
||||
} catch (error) {
|
||||
return Result.fail(`Could not dump item: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async backup(items: Item[], authParams: KeyParamsData, contentSizeLimit?: number): Promise<string[]> {
|
||||
|
||||
@@ -17,6 +17,15 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
|
||||
await this.mongoRepository.deleteMany({
|
||||
$and: [
|
||||
{ userUuid: { $eq: userUuid.value } },
|
||||
{ sharedVaultUuid: { $in: sharedVaultUuids.map((uuid) => uuid.value) } },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async updateSharedVaultOwner(dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
|
||||
await this.mongoRepository.updateMany(
|
||||
{
|
||||
@@ -37,8 +46,10 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
await this.mongoRepository.deleteOne({ _id: { $eq: BSON.UUID.createFromHexString(uuid.value) } })
|
||||
}
|
||||
|
||||
async deleteByUserUuid(userUuid: string): Promise<void> {
|
||||
await this.mongoRepository.deleteMany({ userUuid })
|
||||
async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
|
||||
await this.mongoRepository.deleteMany({
|
||||
$and: [{ userUuid: { $eq: userUuid.value } }, { sharedVaultUuid: { $eq: null } }],
|
||||
})
|
||||
}
|
||||
|
||||
async findAll(query: ItemQuery): Promise<Item[]> {
|
||||
|
||||
@@ -16,6 +16,28 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
|
||||
super(ormRepository, mapper, logger)
|
||||
}
|
||||
|
||||
override async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('item')
|
||||
.delete()
|
||||
.from('items')
|
||||
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
|
||||
.andWhere('shared_vault_uuid IN (:...sharedVaultUuids)', {
|
||||
sharedVaultUuids: sharedVaultUuids.map((uuid) => uuid.value),
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
override async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('item')
|
||||
.delete()
|
||||
.from('items')
|
||||
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
|
||||
.andWhere('shared_vault_uuid IS NULL')
|
||||
.execute()
|
||||
}
|
||||
|
||||
override async updateSharedVaultOwner(dto: {
|
||||
sharedVaultUuid: Uuid
|
||||
fromOwnerUuid: Uuid
|
||||
|
||||
@@ -16,6 +16,10 @@ export class SQLLegacyItemRepository implements ItemRepositoryInterface {
|
||||
protected logger: Logger,
|
||||
) {}
|
||||
|
||||
async deleteByUserUuidInSharedVaults(_userUuid: Uuid, _sharedVaultUuids: Uuid[]): Promise<void> {
|
||||
this.logger.error('Method deleteByUserUuidInSharedVaults not supported.')
|
||||
}
|
||||
|
||||
async updateSharedVaultOwner(_dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
|
||||
this.logger.error('Method updateSharedVaultOwner not supported.')
|
||||
}
|
||||
@@ -80,12 +84,12 @@ export class SQLLegacyItemRepository implements ItemRepositoryInterface {
|
||||
return itemContentSizeDescriptors
|
||||
}
|
||||
|
||||
async deleteByUserUuid(userUuid: string): Promise<void> {
|
||||
async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('item')
|
||||
.delete()
|
||||
.from('items')
|
||||
.where('user_uuid = :userUuid', { userUuid })
|
||||
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
|
||||
.execute()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.17.0](https://github.com/standardnotes/server/compare/@standardnotes/time@1.16.0...@standardnotes/time@1.17.0) (2023-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* block file operations during transition ([#856](https://github.com/standardnotes/server/issues/856)) ([676cf36](https://github.com/standardnotes/server/commit/676cf36f8d769aa433880b2800f0457d06fbbf14))
|
||||
|
||||
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/time@1.15.3...@standardnotes/time@1.16.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.15.3](https://github.com/standardnotes/server/compare/@standardnotes/time@1.15.2...@standardnotes/time@1.15.3) (2023-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/time
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/time",
|
||||
"version": "1.15.3",
|
||||
"version": "1.17.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "rm -fr dist",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest --coverage --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.6",
|
||||
|
||||
@@ -76,6 +76,11 @@ describe('Timer', () => {
|
||||
expect(isoString).toEqual('2021-03-29T08:00:05.000Z')
|
||||
})
|
||||
|
||||
it('should convert a date to formatted string', () => {
|
||||
const isoString = createTimer().convertDateToFormattedString(new Date(Date.UTC(2021, 2, 29, 8, 0, 5)), 'YYYY-MM-DD')
|
||||
expect(isoString).toEqual('2021-03-29')
|
||||
})
|
||||
|
||||
it('should convert a string date to microseconds', () => {
|
||||
const timestamp = createTimer().convertStringDateToMicroseconds('2021-03-29 08:00:05.233Z')
|
||||
expect(timestamp).toEqual(1617004805233000)
|
||||
|
||||
@@ -95,6 +95,10 @@ export class Timer implements TimerInterface {
|
||||
return dayjs.utc(date).toISOString()
|
||||
}
|
||||
|
||||
convertDateToFormattedString(date: Date, format: string): string {
|
||||
return dayjs.utc(date).format(format)
|
||||
}
|
||||
|
||||
dateWasNDaysAgo(date: Date): number {
|
||||
return dayjs.utc().diff(date, 'days')
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface TimerInterface {
|
||||
convertDateToMilliseconds(date: Date): number
|
||||
convertDateToMicroseconds(date: Date): number
|
||||
convertDateToISOString(date: Date): string
|
||||
convertDateToFormattedString(date: Date, format: string): string
|
||||
convertStringDateToDate(date: string): Date
|
||||
convertStringDateToMicroseconds(date: string): number
|
||||
convertStringDateToMilliseconds(date: string): number
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.11.1...@standardnotes/websockets-server@1.11.2) (2023-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.11.0...@standardnotes/websockets-server@1.11.1) (2023-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.53...@standardnotes/websockets-server@1.11.0) (2023-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor handling revision creation from dump ([#854](https://github.com/standardnotes/server/issues/854)) ([ca6dbc0](https://github.com/standardnotes/server/commit/ca6dbc00537bb20f508f9310b1a838421f53a643))
|
||||
|
||||
## [1.10.53](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.52...@standardnotes/websockets-server@1.10.53) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.10.52](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.51...@standardnotes/websockets-server@1.10.52) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.10.52",
|
||||
"version": "1.11.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -16,7 +16,7 @@
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"typeorm": "typeorm-ts-node-commonjs"
|
||||
|
||||
Reference in New Issue
Block a user