fix: stop revisions propagation (#1008)
This commit is contained in:
parent
ce0450becf
commit
7962b245b5
27 changed files with 357 additions and 0 deletions
71
packages/auth/bin/revisions_cleanup.ts
Normal file
71
packages/auth/bin/revisions_cleanup.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { SubscriptionPlanName } from '@standardnotes/domain-core'
|
||||
|
||||
const requestCleanup = async (
|
||||
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
): Promise<void> => {
|
||||
const proSubscriptionPlan = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.ProPlan).getValue()
|
||||
const plusSubscriptionPlan = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue()
|
||||
|
||||
const totalSubscriptions = await userSubscriptionRepository.countByPlanName([
|
||||
proSubscriptionPlan,
|
||||
plusSubscriptionPlan,
|
||||
])
|
||||
|
||||
const limitPerPage = 100
|
||||
const numberOfPages = Math.ceil(totalSubscriptions / limitPerPage)
|
||||
for (let i = 0; i < numberOfPages; i++) {
|
||||
const subscriptions = await userSubscriptionRepository.findByPlanName(
|
||||
[proSubscriptionPlan, plusSubscriptionPlan],
|
||||
i * limitPerPage,
|
||||
limitPerPage,
|
||||
)
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createRevisionsCleanupRequestedEvent({
|
||||
userUuid: subscription.userUuid,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const container = new ContainerConfigLoader('worker')
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info('Starting revisions cleanup triggering...')
|
||||
|
||||
const domainEventFactory = container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory)
|
||||
const domainEventPublisher = container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher)
|
||||
const userSubscriptionRepository = container.get<UserSubscriptionRepositoryInterface>(
|
||||
TYPES.Auth_UserSubscriptionRepository,
|
||||
)
|
||||
|
||||
Promise.resolve(requestCleanup(userSubscriptionRepository, domainEventFactory, domainEventPublisher))
|
||||
.then(() => {
|
||||
logger.info('Finished revisions cleanup triggering')
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Failed to trigger revisions cleanup: ${(error as Error).message}`)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
11
packages/auth/docker/entrypoint-revisions-cleanup.js
Normal file
11
packages/auth/docker/entrypoint-revisions-cleanup.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
|
||||
|
||||
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/revisions_cleanup.js')))
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true })
|
||||
|
||||
exports.default = index
|
|
@ -44,6 +44,10 @@ case "$COMMAND" in
|
|||
exec node docker/entrypoint-delete-accounts.js $FILE_NAME $MODE
|
||||
;;
|
||||
|
||||
'revisions-cleanup' )
|
||||
exec node docker/entrypoint-revisions-cleanup.js
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "[Docker] Unknown command"
|
||||
;;
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
RevisionsCleanupRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
@ -35,6 +36,21 @@ import { KeyParamsData } from '@standardnotes/responses'
|
|||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
||||
|
||||
createRevisionsCleanupRequestedEvent(dto: { userUuid: string }): RevisionsCleanupRequestedEvent {
|
||||
return {
|
||||
type: 'REVISIONS_CLEANUP_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
|
||||
return {
|
||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
|
||||
|
|
|
@ -20,11 +20,13 @@ import {
|
|||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
RevisionsCleanupRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createRevisionsCleanupRequestedEvent(dto: { userUuid: string }): RevisionsCleanupRequestedEvent
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
||||
createEmailRequestedEvent(dto: {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SubscriptionPlanName } from '@standardnotes/domain-core'
|
||||
import { UserSubscription } from './UserSubscription'
|
||||
import { UserSubscriptionType } from './UserSubscriptionType'
|
||||
|
||||
|
@ -7,6 +8,8 @@ export interface UserSubscriptionRepositoryInterface {
|
|||
findOneByUserUuid(userUuid: string): Promise<UserSubscription | null>
|
||||
findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise<UserSubscription | null>
|
||||
findByUserUuid(userUuid: string): Promise<UserSubscription[]>
|
||||
countByPlanName(planNames: SubscriptionPlanName[]): Promise<number>
|
||||
findByPlanName(planNames: SubscriptionPlanName[], offset: number, limit: number): Promise<UserSubscription[]>
|
||||
findOneByUserUuidAndSubscriptionId(userUuid: string, subscriptionId: number): Promise<UserSubscription | null>
|
||||
findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>
|
||||
findBySubscriptionId(subscriptionId: number): Promise<UserSubscription[]>
|
||||
|
|
|
@ -6,6 +6,7 @@ import TYPES from '../../Bootstrap/Types'
|
|||
import { UserSubscription } from '../../Domain/Subscription/UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Domain/Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserSubscriptionType } from '../../Domain/Subscription/UserSubscriptionType'
|
||||
import { SubscriptionPlanName } from '@standardnotes/domain-core'
|
||||
|
||||
@injectable()
|
||||
export class TypeORMUserSubscriptionRepository implements UserSubscriptionRepositoryInterface {
|
||||
|
@ -15,6 +16,27 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi
|
|||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async countByPlanName(planNames: SubscriptionPlanName[]): Promise<number> {
|
||||
return await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('plan_name IN (:...planNames)', {
|
||||
planNames: planNames.map((planName) => planName.value),
|
||||
})
|
||||
.getCount()
|
||||
}
|
||||
|
||||
async findByPlanName(planNames: SubscriptionPlanName[], offset: number, limit: number): Promise<UserSubscription[]> {
|
||||
return await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('plan_name IN (:...planNames)', {
|
||||
planNames: planNames.map((planName) => planName.value),
|
||||
})
|
||||
.orderBy('created_at', 'ASC')
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async countActiveSubscriptions(): Promise<number> {
|
||||
return await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { ItemDeletedEventPayload } from './ItemDeletedEventPayload'
|
||||
|
||||
export interface ItemDeletedEvent extends DomainEventInterface {
|
||||
type: 'ITEM_DELETED'
|
||||
payload: ItemDeletedEventPayload
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface ItemDeletedEventPayload {
|
||||
userUuid: string
|
||||
itemUuid: string
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { RevisionsCleanupRequestedEventPayload } from './RevisionsCleanupRequestedEventPayload'
|
||||
|
||||
export interface RevisionsCleanupRequestedEvent extends DomainEventInterface {
|
||||
type: 'REVISIONS_CLEANUP_REQUESTED'
|
||||
payload: RevisionsCleanupRequestedEventPayload
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface RevisionsCleanupRequestedEventPayload {
|
||||
userUuid: string
|
||||
}
|
|
@ -40,6 +40,8 @@ export * from './Event/FileUploadedEvent'
|
|||
export * from './Event/FileUploadedEventPayload'
|
||||
export * from './Event/ItemDumpedEvent'
|
||||
export * from './Event/ItemDumpedEventPayload'
|
||||
export * from './Event/ItemDeletedEvent'
|
||||
export * from './Event/ItemDeletedEventPayload'
|
||||
export * from './Event/ItemRemovedFromSharedVaultEvent'
|
||||
export * from './Event/ItemRemovedFromSharedVaultEventPayload'
|
||||
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||
|
@ -70,6 +72,8 @@ export * from './Event/PredicateVerifiedEvent'
|
|||
export * from './Event/PredicateVerifiedEventPayload'
|
||||
export * from './Event/RefundProcessedEvent'
|
||||
export * from './Event/RefundProcessedEventPayload'
|
||||
export * from './Event/RevisionsCleanupRequestedEvent'
|
||||
export * from './Event/RevisionsCleanupRequestedEventPayload'
|
||||
export * from './Event/RevisionsCopyRequestedEvent'
|
||||
export * from './Event/RevisionsCopyRequestedEventPayload'
|
||||
export * from './Event/SessionCreatedEvent'
|
||||
|
|
|
@ -52,6 +52,8 @@ import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevision
|
|||
import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler'
|
||||
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
|
||||
import { CreateRevisionFromDump } from '../Domain/UseCase/CreateRevisionFromDump/CreateRevisionFromDump'
|
||||
import { DeleteRevisions } from '../Domain/UseCase/DeleteRevisions/DeleteRevisions'
|
||||
import { ItemDeletedEventHandler } from '../Domain/Handler/ItemDeletedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
|
@ -226,6 +228,11 @@ export class ContainerConfigLoader {
|
|||
.toConstantValue(
|
||||
new DeleteRevision(container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)),
|
||||
)
|
||||
container
|
||||
.bind<DeleteRevisions>(TYPES.Revisions_DeleteRevisions)
|
||||
.toConstantValue(
|
||||
new DeleteRevisions(container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)),
|
||||
)
|
||||
container
|
||||
.bind<CopyRevisions>(TYPES.Revisions_CopyRevisions)
|
||||
.toConstantValue(
|
||||
|
@ -311,6 +318,14 @@ export class ContainerConfigLoader {
|
|||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemDeletedEventHandler>(TYPES.Revisions_ItemDeletedEventHandler)
|
||||
.toConstantValue(
|
||||
new ItemDeletedEventHandler(
|
||||
container.get<DeleteRevisions>(TYPES.Revisions_DeleteRevisions),
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
|
||||
|
@ -318,6 +333,7 @@ export class ContainerConfigLoader {
|
|||
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
|
||||
['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)],
|
||||
['SHARED_VAULT_REMOVED', container.get(TYPES.Revisions_SharedVaultRemovedEventHandler)],
|
||||
['ITEM_DELETED', container.get(TYPES.Revisions_ItemDeletedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
|
|
|
@ -33,6 +33,7 @@ const TYPES = {
|
|||
Revisions_GetRevisionsMetada: Symbol.for('Revisions_GetRevisionsMetada'),
|
||||
Revisions_GetRevision: Symbol.for('Revisions_GetRevision'),
|
||||
Revisions_DeleteRevision: Symbol.for('Revisions_DeleteRevision'),
|
||||
Revisions_DeleteRevisions: Symbol.for('Revisions_DeleteRevisions'),
|
||||
Revisions_CopyRevisions: Symbol.for('Revisions_CopyRevisions'),
|
||||
Revisions_GetRequiredRoleToViewRevision: Symbol.for('Revisions_GetRequiredRoleToViewRevision'),
|
||||
Revisions_RemoveRevisionsFromSharedVault: Symbol.for('Revisions_RemoveRevisionsFromSharedVault'),
|
||||
|
@ -47,6 +48,7 @@ const TYPES = {
|
|||
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
|
||||
Revisions_ItemRemovedFromSharedVaultEventHandler: Symbol.for('Revisions_ItemRemovedFromSharedVaultEventHandler'),
|
||||
Revisions_SharedVaultRemovedEventHandler: Symbol.for('Revisions_SharedVaultRemovedEventHandler'),
|
||||
Revisions_ItemDeletedEventHandler: Symbol.for('Revisions_ItemDeletedEventHandler'),
|
||||
// Services
|
||||
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
|
||||
Revisions_DomainEventSubscriber: Symbol.for('Revisions_DomainEventSubscriber'),
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { DomainEventHandlerInterface, ItemDeletedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DeleteRevisions } from '../UseCase/DeleteRevisions/DeleteRevisions'
|
||||
|
||||
export class ItemDeletedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private deleteRevisions: DeleteRevisions,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemDeletedEvent): Promise<void> {
|
||||
const result = await this.deleteRevisions.execute({ itemUuid: event.payload.itemUuid })
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not delete revisions for item ${event.payload.itemUuid}: ${result.getError()}`, {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import { RevisionMetadata } from './RevisionMetadata'
|
|||
export interface RevisionRepositoryInterface {
|
||||
countByUserUuid(userUuid: Uuid): Promise<number>
|
||||
removeByUserUuid(userUuid: Uuid): Promise<void>
|
||||
removeByItemUuid(itemUuid: Uuid): Promise<void>
|
||||
removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
|
||||
findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Revision | null>
|
||||
findByItemUuid(itemUuid: Uuid): Promise<Array<Revision>>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { DeleteRevisions } from './DeleteRevisions'
|
||||
|
||||
describe('DeleteRevisions', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
|
||||
const createUseCase = () => new DeleteRevisions(revisionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.removeByItemUuid = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove revisions by item uuid', async () => {
|
||||
const useCase = createUseCase()
|
||||
const itemUuid = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
const result = await useCase.execute({ itemUuid })
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(revisionRepository.removeByItemUuid).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return failed result if item uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
const itemUuid = 'invalid'
|
||||
|
||||
const result = await useCase.execute({ itemUuid })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { DeleteRevisionsDTO } from './DeleteRevisionsDTO'
|
||||
|
||||
export class DeleteRevisions implements UseCaseInterface<void> {
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: DeleteRevisionsDTO): Promise<Result<void>> {
|
||||
const itemUuidOrError = Uuid.create(dto.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return Result.fail(`Could not delete revisions: ${itemUuidOrError.getError()}`)
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
await this.revisionRepository.removeByItemUuid(itemUuid)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface DeleteRevisionsDTO {
|
||||
itemUuid: string
|
||||
}
|
|
@ -15,6 +15,15 @@ export class SQLRevisionRepository implements RevisionRepositoryInterface {
|
|||
protected logger: Logger,
|
||||
) {}
|
||||
|
||||
async removeByItemUuid(itemUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('revisions_revisions')
|
||||
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async removeByUserUuid(userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
|
|
|
@ -162,6 +162,7 @@ import { SyncResponse } from '@standardnotes/grpc'
|
|||
import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper'
|
||||
import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler'
|
||||
import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients/SendEventToClients'
|
||||
import { RevisionsCleanupRequestedEventHandler } from '../Domain/Handler/RevisionsCleanupRequestedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
|
@ -980,6 +981,16 @@ export class ContainerConfigLoader {
|
|||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<RevisionsCleanupRequestedEventHandler>(TYPES.Sync_RevisionsCleanupRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new RevisionsCleanupRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container.bind<ContentDecoderInterface>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
||||
|
@ -1008,6 +1019,7 @@ export class ContainerConfigLoader {
|
|||
'SHARED_VAULT_REMOVED',
|
||||
container.get<SharedVaultRemovedEventHandler>(TYPES.Sync_SharedVaultRemovedEventHandler),
|
||||
],
|
||||
['REVISIONS_CLEANUP_REQUESTED', container.get(TYPES.Sync_RevisionsCleanupRequestedEventHandler)],
|
||||
])
|
||||
if (!isConfiguredForHomeServer) {
|
||||
container
|
||||
|
|
|
@ -97,6 +97,7 @@ const TYPES = {
|
|||
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
|
||||
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
|
||||
Sync_SharedVaultRemovedEventHandler: Symbol.for('Sync_SharedVaultRemovedEventHandler'),
|
||||
Sync_RevisionsCleanupRequestedEventHandler: Symbol.for('Sync_RevisionsCleanupRequestedEventHandler'),
|
||||
// Services
|
||||
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
|
||||
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
DomainEventService,
|
||||
DuplicateItemSyncedEvent,
|
||||
EmailRequestedEvent,
|
||||
ItemDeletedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRemovedFromSharedVaultEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
|
@ -316,6 +317,21 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
|||
}
|
||||
}
|
||||
|
||||
createItemDeletedEvent(dto: { itemUuid: string; userUuid: string }): ItemDeletedEvent {
|
||||
return {
|
||||
type: 'ITEM_DELETED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
AccountDeletionVerificationPassedEvent,
|
||||
DuplicateItemSyncedEvent,
|
||||
EmailRequestedEvent,
|
||||
ItemDeletedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRemovedFromSharedVaultEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
|
@ -77,6 +78,7 @@ export interface DomainEventFactoryInterface {
|
|||
userUuid?: string
|
||||
}): EmailRequestedEvent
|
||||
createDuplicateItemSyncedEvent(dto: { itemUuid: string; userUuid: string }): DuplicateItemSyncedEvent
|
||||
createItemDeletedEvent(dto: { itemUuid: string; userUuid: string }): ItemDeletedEvent
|
||||
createItemRevisionCreationRequested(dto: { itemUuid: string; userUuid: string }): ItemRevisionCreationRequestedEvent
|
||||
createItemDumpedEvent(dto: { fileDumpPath: string; userUuid: string }): ItemDumpedEvent
|
||||
createRevisionsCopyRequestedEvent(
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
RevisionsCleanupRequestedEvent,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class RevisionsCleanupRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: RevisionsCleanupRequestedEvent): Promise<void> {
|
||||
const totalDeletedItems = await this.itemRepository.countAll({
|
||||
userUuid: event.payload.userUuid,
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
this.logger.info(`Found ${totalDeletedItems} deleted items`, {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
|
||||
const limitPerPage = 100
|
||||
const numberOfPages = Math.ceil(totalDeletedItems / limitPerPage)
|
||||
|
||||
for (let i = 0; i < numberOfPages; i++) {
|
||||
const items = await this.itemRepository.findAll({
|
||||
userUuid: event.payload.userUuid,
|
||||
deleted: true,
|
||||
offset: i * limitPerPage,
|
||||
limit: limitPerPage,
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'created_at_timestamp',
|
||||
})
|
||||
|
||||
for (const item of items) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemDeletedEvent({
|
||||
itemUuid: item.id.toString(),
|
||||
userUuid: item.props.userUuid.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Finished processing ${totalDeletedItems} deleted items`, {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -109,6 +109,7 @@ describe('UpdateExistingItem', () => {
|
|||
domainEventFactory.createItemRemovedFromSharedVaultEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
domainEventFactory.createItemDeletedEvent = jest.fn().mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
|
||||
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockResolvedValue(
|
||||
|
|
|
@ -159,6 +159,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
|||
dto.existingItem.props.keySystemAssociation = undefined
|
||||
}
|
||||
|
||||
let wasMarkedAsDeleted = false
|
||||
if (dto.itemHash.props.deleted === true) {
|
||||
dto.existingItem.props.deleted = true
|
||||
dto.existingItem.props.content = null
|
||||
|
@ -166,6 +167,8 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
|||
dto.existingItem.props.encItemKey = null
|
||||
dto.existingItem.props.authHash = null
|
||||
dto.existingItem.props.itemsKeyId = null
|
||||
|
||||
wasMarkedAsDeleted = true
|
||||
}
|
||||
|
||||
await this.itemRepository.update(dto.existingItem)
|
||||
|
@ -196,6 +199,15 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
|||
)
|
||||
}
|
||||
|
||||
if (wasMarkedAsDeleted) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemDeletedEvent({
|
||||
itemUuid: dto.existingItem.id.toString(),
|
||||
userUuid: dto.existingItem.props.userUuid.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const notificationsResult = await this.addNotificationsAndPublishEvents(userUuid, sharedVaultOperation, dto)
|
||||
if (notificationsResult.isFailed()) {
|
||||
return Result.fail(notificationsResult.getError())
|
||||
|
|
Loading…
Reference in a new issue