diff --git a/packages/syncing-server/bin/revisions.ts b/packages/syncing-server/bin/revisions.ts new file mode 100644 index 000000000..738eaf030 --- /dev/null +++ b/packages/syncing-server/bin/revisions.ts @@ -0,0 +1,78 @@ +import 'reflect-metadata' + +import 'newrelic' + +import { Logger } from 'winston' + +import { ContainerConfigLoader } from '../src/Bootstrap/Container' +import TYPES from '../src/Bootstrap/Types' +import { Env } from '../src/Bootstrap/Env' +import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' +import { ItemRepositoryInterface } from '../src/Domain/Item/ItemRepositoryInterface' +import { Stream } from 'stream' + +const fixRevisionsOwnership = async ( + itemRepository: ItemRepositoryInterface, + domainEventFactory: DomainEventFactoryInterface, + domainEventPublisher: DomainEventPublisherInterface, + logger: Logger, +): Promise => { + const stream = await itemRepository.streamAll({ + sortBy: 'updated_at_timestamp', + sortOrder: 'ASC', + createdBefore: new Date('2022-11-23'), + selectFields: ['user_uuid', 'item_uuid'], + }) + + return new Promise((resolve, reject) => { + stream + .pipe( + new Stream.Transform({ + objectMode: true, + transform: async (rawItemData, _encoding, callback) => { + try { + await domainEventPublisher.publish( + domainEventFactory.createRevisionsOwnershipUpdateRequestedEvent({ + userUuid: rawItemData.item_user_uuid, + itemUuid: rawItemData.item_uuid, + }), + ) + } catch (error) { + logger.error(`Could not process item ${rawItemData.item_uuid}: ${(error as Error).message}`) + } + + callback() + }, + }), + ) + .on('finish', resolve) + .on('error', reject) + }) +} + +const container = new ContainerConfigLoader() +void container.load().then((container) => { + const env: Env = new Env() + env.load() + + const logger: Logger = container.get(TYPES.Logger) + + logger.info('Starting revisions ownership fixing') + + const itemRepository: ItemRepositoryInterface = container.get(TYPES.ItemRepository) + const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory) + const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher) + + Promise.resolve(fixRevisionsOwnership(itemRepository, domainEventFactory, domainEventPublisher, logger)) + .then(() => { + logger.info('revisions ownership fix complete.') + + process.exit(0) + }) + .catch((error) => { + logger.error(`Could not finish revisions ownership fix: ${error.message}`) + + process.exit(1) + }) +}) diff --git a/packages/syncing-server/docker/entrypoint.sh b/packages/syncing-server/docker/entrypoint.sh index d8e4f91d6..264722978 100755 --- a/packages/syncing-server/docker/entrypoint.sh +++ b/packages/syncing-server/docker/entrypoint.sh @@ -25,6 +25,11 @@ case "$COMMAND" in yarn workspace @standardnotes/syncing-server content-size $USER_UUID ;; + 'revisions-ownership-fix' ) + echo "Starting Revisions Ownership Fixing..." + yarn workspace @standardnotes/syncing-server revisions-ownership + ;; + * ) echo "Unknown command" ;; diff --git a/packages/syncing-server/package.json b/packages/syncing-server/package.json index 7b0f789a6..5d1be38e2 100644 --- a/packages/syncing-server/package.json +++ b/packages/syncing-server/package.json @@ -22,6 +22,7 @@ "start": "yarn node dist/bin/server.js", "worker": "yarn node dist/bin/worker.js", "content-size": "yarn node dist/bin/content.js", + "revisions-ownership": "yarn node dist/bin/revisions.js", "upgrade:snjs": "yarn ncu -u '@standardnotes/*'" }, "dependencies": { diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts index 8f4613452..93b5f9da9 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts @@ -11,6 +11,7 @@ import { ItemsSyncedEvent, OneDriveBackupFailedEvent, RevisionsCopyRequestedEvent, + RevisionsOwnershipUpdateRequestedEvent, UserContentSizeRecalculationRequestedEvent, } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' @@ -22,6 +23,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface' export class DomainEventFactory implements DomainEventFactoryInterface { constructor(@inject(TYPES.Timer) private timer: TimerInterface) {} + createRevisionsOwnershipUpdateRequestedEvent(dto: { + userUuid: string + itemUuid: string + }): RevisionsOwnershipUpdateRequestedEvent { + return { + type: 'REVISIONS_OWNERSHIP_UPDATE_REQUESTED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: dto.userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.SyncingServer, + }, + payload: dto, + } + } + createRevisionsCopyRequestedEvent( userUuid: string, dto: { diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts index 3e49888cb..87e3dd442 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts @@ -9,6 +9,7 @@ import { ItemsSyncedEvent, OneDriveBackupFailedEvent, RevisionsCopyRequestedEvent, + RevisionsOwnershipUpdateRequestedEvent, UserContentSizeRecalculationRequestedEvent, } from '@standardnotes/domain-events' @@ -40,4 +41,8 @@ export interface DomainEventFactoryInterface { userUuid: string, dto: { originalItemUuid: string; newItemUuid: string }, ): RevisionsCopyRequestedEvent + createRevisionsOwnershipUpdateRequestedEvent(dto: { + userUuid: string + itemUuid: string + }): RevisionsOwnershipUpdateRequestedEvent } diff --git a/packages/syncing-server/src/Domain/Item/ItemQuery.ts b/packages/syncing-server/src/Domain/Item/ItemQuery.ts index b5b10d715..fbd3c6b49 100644 --- a/packages/syncing-server/src/Domain/Item/ItemQuery.ts +++ b/packages/syncing-server/src/Domain/Item/ItemQuery.ts @@ -9,4 +9,6 @@ export type ItemQuery = { deleted?: boolean offset?: number limit?: number + createdBefore?: Date + selectFields?: string[] } diff --git a/packages/syncing-server/src/Infra/MySQL/MySQLItemRepository.ts b/packages/syncing-server/src/Infra/MySQL/MySQLItemRepository.ts index 9c5832fd3..e3f212b2f 100644 --- a/packages/syncing-server/src/Infra/MySQL/MySQLItemRepository.ts +++ b/packages/syncing-server/src/Infra/MySQL/MySQLItemRepository.ts @@ -130,8 +130,12 @@ export class MySQLItemRepository implements ItemRepositoryInterface { private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder { const queryBuilder = this.ormRepository.createQueryBuilder('item') + queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder) + if (query.selectFields !== undefined) { + queryBuilder.select(query.selectFields.map((field) => `item.${field}`)) + } if (query.userUuid !== undefined) { queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid }) } @@ -149,6 +153,9 @@ export class MySQLItemRepository implements ItemRepositoryInterface { lastSyncTime: query.lastSyncTime, }) } + if (query.createdBefore !== undefined) { + queryBuilder.andWhere('item.created_at < :createdAt', { createdAt: query.createdBefore.toISOString() }) + } if (query.offset !== undefined) { queryBuilder.skip(query.offset) }