From ba31907e91e9137a40dbba8d3171661f4324fa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 16 Jan 2023 15:46:01 +0100 Subject: [PATCH 1/7] feat(snjs): add revisions api v2 --- .../Client/Revision/RevisionApiOperations.ts | 5 ++ .../Client/Revision/RevisionApiService.ts | 79 +++++++++++++++++ .../Revision/RevisionApiServiceInterface.ts | 9 ++ packages/api/src/Domain/Client/index.ts | 3 + .../Revision/DeleteRevisionRequestParams.ts | 4 + .../Revision/GetRevisionRequestParams.ts | 4 + .../Revision/ListRevisionsRequestParams.ts | 3 + packages/api/src/Domain/Request/index.ts | 3 + .../Revision/DeleteRevisionResponse.ts | 10 +++ .../Revision/DeleteRevisionResponseBody.ts | 3 + .../Response/Revision/GetRevisionResponse.ts | 10 +++ .../Revision/GetRevisionResponseBody.ts | 13 +++ .../Revision/ListRevisionsResponse.ts | 10 +++ .../Revision/ListRevisionsResponseBody.ts | 9 ++ packages/api/src/Domain/Response/index.ts | 6 ++ .../api/src/Domain/Server/Revision/Paths.ts | 11 +++ .../Domain/Server/Revision/RevisionServer.ts | 30 +++++++ .../Revision/RevisionServerInterface.ts | 12 +++ packages/api/src/Domain/Server/index.ts | 2 + .../Encryption/EncryptionProviderInterface.ts | 6 ++ .../src/Domain/Item/RevisionListEntry.ts | 10 --- .../src/Domain/Item/RevisionListResponse.ts | 4 - .../src/Domain/Item/SingleRevision.ts | 15 ---- .../src/Domain/Item/SingleRevisionResponse.ts | 6 -- packages/responses/src/Domain/index.ts | 4 - .../Revision/RevisionClientInterface.ts | 28 +++++++ .../src/Domain/Revision/RevisionManager.ts | 72 ++++++++++++++++ .../services/src/Domain/Strings/Messages.ts | 2 - packages/services/src/Domain/index.ts | 2 + packages/snjs/lib/Application/Application.ts | 46 +++++++++- packages/snjs/lib/Domain/Revision/Revision.ts | 11 +++ .../lib/Domain/Revision/RevisionMetadata.ts | 7 ++ .../DeleteRevision/DeleteRevision.spec.ts | 49 +++++++++++ .../UseCase/DeleteRevision/DeleteRevision.ts | 26 ++++++ .../DeleteRevision/DeleteRevisionDTO.ts | 4 + .../UseCase/GetRevision/GetRevision.spec.ts | 71 ++++++++++++++++ .../Domain/UseCase/GetRevision/GetRevision.ts | 80 ++++++++++++++++++ .../UseCase/GetRevision/GetRevisionDTO.ts | 4 + .../ListRevisions/ListRevisions.spec.ts | 32 +++++++ .../UseCase/ListRevisions/ListRevisions.ts | 22 +++++ .../UseCase/ListRevisions/ListRevisionsDTO.ts | 3 + .../UseCase/UseCaseContainerInterface.ts | 6 ++ packages/snjs/lib/Services/Api/ApiService.ts | 64 +------------- .../lib/Services/History/HistoryManager.ts | 84 +------------------ packages/snjs/mocha/history.test.js | 26 +++--- .../NoteHistory/NoteHistoryController.ts | 27 ++++-- 46 files changed, 732 insertions(+), 205 deletions(-) create mode 100644 packages/api/src/Domain/Client/Revision/RevisionApiOperations.ts create mode 100644 packages/api/src/Domain/Client/Revision/RevisionApiService.ts create mode 100644 packages/api/src/Domain/Client/Revision/RevisionApiServiceInterface.ts create mode 100644 packages/api/src/Domain/Request/Revision/DeleteRevisionRequestParams.ts create mode 100644 packages/api/src/Domain/Request/Revision/GetRevisionRequestParams.ts create mode 100644 packages/api/src/Domain/Request/Revision/ListRevisionsRequestParams.ts create mode 100644 packages/api/src/Domain/Response/Revision/DeleteRevisionResponse.ts create mode 100644 packages/api/src/Domain/Response/Revision/DeleteRevisionResponseBody.ts create mode 100644 packages/api/src/Domain/Response/Revision/GetRevisionResponse.ts create mode 100644 packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts create mode 100644 packages/api/src/Domain/Response/Revision/ListRevisionsResponse.ts create mode 100644 packages/api/src/Domain/Response/Revision/ListRevisionsResponseBody.ts create mode 100644 packages/api/src/Domain/Server/Revision/Paths.ts create mode 100644 packages/api/src/Domain/Server/Revision/RevisionServer.ts create mode 100644 packages/api/src/Domain/Server/Revision/RevisionServerInterface.ts delete mode 100644 packages/responses/src/Domain/Item/RevisionListEntry.ts delete mode 100644 packages/responses/src/Domain/Item/RevisionListResponse.ts delete mode 100644 packages/responses/src/Domain/Item/SingleRevision.ts delete mode 100644 packages/responses/src/Domain/Item/SingleRevisionResponse.ts create mode 100644 packages/services/src/Domain/Revision/RevisionClientInterface.ts create mode 100644 packages/services/src/Domain/Revision/RevisionManager.ts create mode 100644 packages/snjs/lib/Domain/Revision/Revision.ts create mode 100644 packages/snjs/lib/Domain/Revision/RevisionMetadata.ts create mode 100644 packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts create mode 100644 packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.ts create mode 100644 packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetRevision/GetRevisionDTO.ts create mode 100644 packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.spec.ts create mode 100644 packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.ts create mode 100644 packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisionsDTO.ts diff --git a/packages/api/src/Domain/Client/Revision/RevisionApiOperations.ts b/packages/api/src/Domain/Client/Revision/RevisionApiOperations.ts new file mode 100644 index 00000000000..c5445550e80 --- /dev/null +++ b/packages/api/src/Domain/Client/Revision/RevisionApiOperations.ts @@ -0,0 +1,5 @@ +export enum RevisionApiOperations { + List, + Delete, + Get, +} diff --git a/packages/api/src/Domain/Client/Revision/RevisionApiService.ts b/packages/api/src/Domain/Client/Revision/RevisionApiService.ts new file mode 100644 index 00000000000..a2f4a46d1a2 --- /dev/null +++ b/packages/api/src/Domain/Client/Revision/RevisionApiService.ts @@ -0,0 +1,79 @@ +import { ErrorMessage } from '../../Error/ErrorMessage' +import { ApiCallError } from '../../Error/ApiCallError' + +import { RevisionApiServiceInterface } from './RevisionApiServiceInterface' +import { RevisionApiOperations } from './RevisionApiOperations' +import { RevisionServerInterface } from '../../Server' +import { DeleteRevisionResponse } from '../../Response/Revision/DeleteRevisionResponse' +import { GetRevisionResponse } from '../../Response/Revision/GetRevisionResponse' +import { ListRevisionsResponse } from '../../Response/Revision/ListRevisionsResponse' + +export class RevisionApiService implements RevisionApiServiceInterface { + private operationsInProgress: Map + + constructor(private revisionServer: RevisionServerInterface) { + this.operationsInProgress = new Map() + } + + async listRevisions(itemUuid: string): Promise { + if (this.operationsInProgress.get(RevisionApiOperations.List)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(RevisionApiOperations.List, true) + + try { + const response = await this.revisionServer.listRevisions({ + itemUuid, + }) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } finally { + this.operationsInProgress.set(RevisionApiOperations.List, false) + } + } + + async getRevision(itemUuid: string, revisionUuid: string): Promise { + if (this.operationsInProgress.get(RevisionApiOperations.Get)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(RevisionApiOperations.Get, true) + + try { + const response = await this.revisionServer.getRevision({ + itemUuid, + revisionUuid, + }) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } finally { + this.operationsInProgress.set(RevisionApiOperations.Get, false) + } + } + + async deleteRevision(itemUuid: string, revisionUuid: string): Promise { + if (this.operationsInProgress.get(RevisionApiOperations.Delete)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(RevisionApiOperations.Delete, true) + + try { + const response = await this.revisionServer.deleteRevision({ + itemUuid, + revisionUuid, + }) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } finally { + this.operationsInProgress.set(RevisionApiOperations.Delete, false) + } + } +} diff --git a/packages/api/src/Domain/Client/Revision/RevisionApiServiceInterface.ts b/packages/api/src/Domain/Client/Revision/RevisionApiServiceInterface.ts new file mode 100644 index 00000000000..4805e0be6bd --- /dev/null +++ b/packages/api/src/Domain/Client/Revision/RevisionApiServiceInterface.ts @@ -0,0 +1,9 @@ +import { DeleteRevisionResponse } from '../../Response/Revision/DeleteRevisionResponse' +import { GetRevisionResponse } from '../../Response/Revision/GetRevisionResponse' +import { ListRevisionsResponse } from '../../Response/Revision/ListRevisionsResponse' + +export interface RevisionApiServiceInterface { + listRevisions(itemUuid: string): Promise + getRevision(itemUuid: string, revisionUuid: string): Promise + deleteRevision(itemUuid: string, revisionUuid: string): Promise +} diff --git a/packages/api/src/Domain/Client/index.ts b/packages/api/src/Domain/Client/index.ts index a17152697c0..f464c6ba76b 100644 --- a/packages/api/src/Domain/Client/index.ts +++ b/packages/api/src/Domain/Client/index.ts @@ -4,6 +4,9 @@ export * from './Auth/AuthApiServiceInterface' export * from './Authenticator/AuthenticatorApiOperations' export * from './Authenticator/AuthenticatorApiService' export * from './Authenticator/AuthenticatorApiServiceInterface' +export * from './Revision/RevisionApiOperations' +export * from './Revision/RevisionApiService' +export * from './Revision/RevisionApiServiceInterface' export * from './Subscription/SubscriptionApiOperations' export * from './Subscription/SubscriptionApiService' export * from './Subscription/SubscriptionApiServiceInterface' diff --git a/packages/api/src/Domain/Request/Revision/DeleteRevisionRequestParams.ts b/packages/api/src/Domain/Request/Revision/DeleteRevisionRequestParams.ts new file mode 100644 index 00000000000..4f0d5885786 --- /dev/null +++ b/packages/api/src/Domain/Request/Revision/DeleteRevisionRequestParams.ts @@ -0,0 +1,4 @@ +export interface DeleteRevisionRequestParams { + itemUuid: string + revisionUuid: string +} diff --git a/packages/api/src/Domain/Request/Revision/GetRevisionRequestParams.ts b/packages/api/src/Domain/Request/Revision/GetRevisionRequestParams.ts new file mode 100644 index 00000000000..b1d275c9018 --- /dev/null +++ b/packages/api/src/Domain/Request/Revision/GetRevisionRequestParams.ts @@ -0,0 +1,4 @@ +export interface GetRevisionRequestParams { + itemUuid: string + revisionUuid: string +} diff --git a/packages/api/src/Domain/Request/Revision/ListRevisionsRequestParams.ts b/packages/api/src/Domain/Request/Revision/ListRevisionsRequestParams.ts new file mode 100644 index 00000000000..8f0aed84642 --- /dev/null +++ b/packages/api/src/Domain/Request/Revision/ListRevisionsRequestParams.ts @@ -0,0 +1,3 @@ +export interface ListRevisionsRequestParams { + itemUuid: string +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index ed13a52cabe..e4744d42b41 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -5,6 +5,9 @@ export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseRequestP export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams' export * from './Recovery/RecoveryKeyParamsRequestParams' export * from './Recovery/SignInWithRecoveryCodesRequestParams' +export * from './Revision/DeleteRevisionRequestParams' +export * from './Revision/GetRevisionRequestParams' +export * from './Revision/ListRevisionsRequestParams' export * from './Subscription/AppleIAPConfirmRequestParams' export * from './Subscription/SubscriptionInviteAcceptRequestParams' export * from './Subscription/SubscriptionInviteCancelRequestParams' diff --git a/packages/api/src/Domain/Response/Revision/DeleteRevisionResponse.ts b/packages/api/src/Domain/Response/Revision/DeleteRevisionResponse.ts new file mode 100644 index 00000000000..fe6fda7d620 --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/DeleteRevisionResponse.ts @@ -0,0 +1,10 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' + +import { DeleteRevisionResponseBody } from './DeleteRevisionResponseBody' + +export interface DeleteRevisionResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/Revision/DeleteRevisionResponseBody.ts b/packages/api/src/Domain/Response/Revision/DeleteRevisionResponseBody.ts new file mode 100644 index 00000000000..2734142bc1a --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/DeleteRevisionResponseBody.ts @@ -0,0 +1,3 @@ +export interface DeleteRevisionResponseBody { + message: string +} diff --git a/packages/api/src/Domain/Response/Revision/GetRevisionResponse.ts b/packages/api/src/Domain/Response/Revision/GetRevisionResponse.ts new file mode 100644 index 00000000000..ec8b467cf53 --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/GetRevisionResponse.ts @@ -0,0 +1,10 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' + +import { GetRevisionResponseBody } from './GetRevisionResponseBody' + +export interface GetRevisionResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts b/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts new file mode 100644 index 00000000000..694aab4415f --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts @@ -0,0 +1,13 @@ +export interface GetRevisionResponseBody { + revision: { + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string + } +} diff --git a/packages/api/src/Domain/Response/Revision/ListRevisionsResponse.ts b/packages/api/src/Domain/Response/Revision/ListRevisionsResponse.ts new file mode 100644 index 00000000000..5c4947535bc --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/ListRevisionsResponse.ts @@ -0,0 +1,10 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' + +import { ListRevisionsResponseBody } from './ListRevisionsResponseBody' + +export interface ListRevisionsResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/Revision/ListRevisionsResponseBody.ts b/packages/api/src/Domain/Response/Revision/ListRevisionsResponseBody.ts new file mode 100644 index 00000000000..49324317ee1 --- /dev/null +++ b/packages/api/src/Domain/Response/Revision/ListRevisionsResponseBody.ts @@ -0,0 +1,9 @@ +export interface ListRevisionsResponseBody { + revisions: Array<{ + uuid: string + content_type: string + created_at: string + updated_at: string + required_role: string + }> +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index 21b17b3c886..2a8b6870aeb 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -18,6 +18,12 @@ export * from './Recovery/RecoveryKeyParamsResponse' export * from './Recovery/RecoveryKeyParamsResponseBody' export * from './Recovery/SignInWithRecoveryCodesResponse' export * from './Recovery/SignInWithRecoveryCodesResponseBody' +export * from './Recovery/GenerateRecoveryCodesResponse' +export * from './Recovery/GenerateRecoveryCodesResponseBody' +export * from './Recovery/RecoveryKeyParamsResponse' +export * from './Recovery/RecoveryKeyParamsResponseBody' +export * from './Recovery/SignInWithRecoveryCodesResponse' +export * from './Recovery/SignInWithRecoveryCodesResponseBody' export * from './Subscription/AppleIAPConfirmResponse' export * from './Subscription/AppleIAPConfirmResponseBody' export * from './Subscription/SubscriptionInviteAcceptResponse' diff --git a/packages/api/src/Domain/Server/Revision/Paths.ts b/packages/api/src/Domain/Server/Revision/Paths.ts new file mode 100644 index 00000000000..af658d723ce --- /dev/null +++ b/packages/api/src/Domain/Server/Revision/Paths.ts @@ -0,0 +1,11 @@ +const RevisionsPaths = { + listRevisions: (itemUuid: string) => `/v2/items/${itemUuid}/revisions`, + getRevision: (itemUuid: string, revisionUuid: string) => `/v2/items/${itemUuid}/revisions/${revisionUuid}`, + deleteRevision: (itemUuid: string, revisionUuid: string) => `/v2/items/${itemUuid}/revisions/${revisionUuid}`, +} + +export const Paths = { + v2: { + ...RevisionsPaths, + }, +} diff --git a/packages/api/src/Domain/Server/Revision/RevisionServer.ts b/packages/api/src/Domain/Server/Revision/RevisionServer.ts new file mode 100644 index 00000000000..f9227b6e334 --- /dev/null +++ b/packages/api/src/Domain/Server/Revision/RevisionServer.ts @@ -0,0 +1,30 @@ +import { HttpServiceInterface } from '../../Http/HttpServiceInterface' +import { DeleteRevisionRequestParams, GetRevisionRequestParams, ListRevisionsRequestParams } from '../../Request' +import { DeleteRevisionResponse } from '../../Response/Revision/DeleteRevisionResponse' +import { GetRevisionResponse } from '../../Response/Revision/GetRevisionResponse' +import { ListRevisionsResponse } from '../../Response/Revision/ListRevisionsResponse' + +import { Paths } from './Paths' +import { RevisionServerInterface } from './RevisionServerInterface' + +export class RevisionServer implements RevisionServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + async listRevisions(params: ListRevisionsRequestParams): Promise { + const response = await this.httpService.get(Paths.v2.listRevisions(params.itemUuid)) + + return response as ListRevisionsResponse + } + + async getRevision(params: GetRevisionRequestParams): Promise { + const response = await this.httpService.post(Paths.v2.getRevision(params.itemUuid, params.revisionUuid)) + + return response as GetRevisionResponse + } + + async deleteRevision(params: DeleteRevisionRequestParams): Promise { + const response = await this.httpService.delete(Paths.v2.deleteRevision(params.itemUuid, params.revisionUuid)) + + return response as DeleteRevisionResponse + } +} diff --git a/packages/api/src/Domain/Server/Revision/RevisionServerInterface.ts b/packages/api/src/Domain/Server/Revision/RevisionServerInterface.ts new file mode 100644 index 00000000000..f6b76b73806 --- /dev/null +++ b/packages/api/src/Domain/Server/Revision/RevisionServerInterface.ts @@ -0,0 +1,12 @@ +import { DeleteRevisionRequestParams } from '../../Request/Revision/DeleteRevisionRequestParams' +import { GetRevisionRequestParams } from '../../Request/Revision/GetRevisionRequestParams' +import { ListRevisionsRequestParams } from '../../Request/Revision/ListRevisionsRequestParams' +import { DeleteRevisionResponse } from '../../Response/Revision/DeleteRevisionResponse' +import { GetRevisionResponse } from '../../Response/Revision/GetRevisionResponse' +import { ListRevisionsResponse } from '../../Response/Revision/ListRevisionsResponse' + +export interface RevisionServerInterface { + listRevisions(params: ListRevisionsRequestParams): Promise + getRevision(params: GetRevisionRequestParams): Promise + deleteRevision(params: DeleteRevisionRequestParams): Promise +} diff --git a/packages/api/src/Domain/Server/index.ts b/packages/api/src/Domain/Server/index.ts index 2b3ff06317f..d4cbd91fafb 100644 --- a/packages/api/src/Domain/Server/index.ts +++ b/packages/api/src/Domain/Server/index.ts @@ -2,6 +2,8 @@ export * from './Auth/AuthServer' export * from './Auth/AuthServerInterface' export * from './Authenticator/AuthenticatorServer' export * from './Authenticator/AuthenticatorServerInterface' +export * from './Revision/RevisionServer' +export * from './Revision/RevisionServerInterface' export * from './Subscription/SubscriptionServer' export * from './Subscription/SubscriptionServerInterface' export * from './User/UserServer' diff --git a/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts index cd49b9a4618..133133c7791 100644 --- a/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts +++ b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts @@ -12,6 +12,9 @@ import { ClientDisplayableError } from '@standardnotes/responses' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit' import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit' +import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' +import { LegacyAttachedData } from '../../Types/LegacyAttachedData' +import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' export interface EncryptionProviderInterface { encryptSplitSingle(split: KeyedEncryptionSplit): Promise @@ -67,4 +70,7 @@ export interface EncryptionProviderInterface { reencryptItemsKeys(): Promise getSureDefaultItemsKey(): ItemsKeyInterface getRootKeyParams(): Promise + getEmbeddedPayloadAuthenticatedData( + payload: EncryptedPayloadInterface, + ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined } diff --git a/packages/responses/src/Domain/Item/RevisionListEntry.ts b/packages/responses/src/Domain/Item/RevisionListEntry.ts deleted file mode 100644 index 2ca4e722202..00000000000 --- a/packages/responses/src/Domain/Item/RevisionListEntry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RoleName } from '@standardnotes/common' - -export type RevisionListEntry = { - content_type: string - created_at: string - updated_at: string - /** The uuid of the revision */ - uuid: string - required_role: RoleName -} diff --git a/packages/responses/src/Domain/Item/RevisionListResponse.ts b/packages/responses/src/Domain/Item/RevisionListResponse.ts deleted file mode 100644 index a00f4f22106..00000000000 --- a/packages/responses/src/Domain/Item/RevisionListResponse.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { HttpResponse } from '../Http/HttpResponse' -import { RevisionListEntry } from './RevisionListEntry' - -export type RevisionListResponse = HttpResponse & { data: RevisionListEntry[] } diff --git a/packages/responses/src/Domain/Item/SingleRevision.ts b/packages/responses/src/Domain/Item/SingleRevision.ts deleted file mode 100644 index b5e725a17c2..00000000000 --- a/packages/responses/src/Domain/Item/SingleRevision.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ContentType, Uuid } from '@standardnotes/common' - -export type SingleRevision = { - auth_hash?: string - content_type: ContentType - content: string - created_at: string - enc_item_key: string - /** The uuid of the item this revision was created with */ - item_uuid: string - items_key_id: string - updated_at: string - /** The uuid of the revision */ - uuid: Uuid -} diff --git a/packages/responses/src/Domain/Item/SingleRevisionResponse.ts b/packages/responses/src/Domain/Item/SingleRevisionResponse.ts deleted file mode 100644 index 3fdbc1634a3..00000000000 --- a/packages/responses/src/Domain/Item/SingleRevisionResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HttpResponse } from '../Http/HttpResponse' -import { SingleRevision } from './SingleRevision' - -export type SingleRevisionResponse = HttpResponse & { - data: SingleRevision -} diff --git a/packages/responses/src/Domain/index.ts b/packages/responses/src/Domain/index.ts index c73ebd03293..2b49b6e7f83 100644 --- a/packages/responses/src/Domain/index.ts +++ b/packages/responses/src/Domain/index.ts @@ -34,11 +34,7 @@ export * from './Item/ConflictType' export * from './Item/GetSingleItemResponse' export * from './Item/RawSyncData' export * from './Item/RawSyncResponse' -export * from './Item/RevisionListEntry' -export * from './Item/RevisionListResponse' export * from './Item/ServerItemResponse' -export * from './Item/SingleRevision' -export * from './Item/SingleRevisionResponse' export * from './Item/IntegrityPayload' export * from './Listed/ActionResponse' export * from './Listed/ListedAccount' diff --git a/packages/services/src/Domain/Revision/RevisionClientInterface.ts b/packages/services/src/Domain/Revision/RevisionClientInterface.ts new file mode 100644 index 00000000000..95d446cc5cb --- /dev/null +++ b/packages/services/src/Domain/Revision/RevisionClientInterface.ts @@ -0,0 +1,28 @@ +import { Uuid } from '@standardnotes/domain-core' + +export interface RevisionClientInterface { + listRevisions(itemUuid: Uuid): Promise< + Array<{ + uuid: string + content_type: string + created_at: string + updated_at: string + required_role: string + }> + > + deleteRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise + getRevision( + itemUuid: Uuid, + revisionUuid: Uuid, + ): Promise<{ + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string + } | null> +} diff --git a/packages/services/src/Domain/Revision/RevisionManager.ts b/packages/services/src/Domain/Revision/RevisionManager.ts new file mode 100644 index 00000000000..dd0705ea7ba --- /dev/null +++ b/packages/services/src/Domain/Revision/RevisionManager.ts @@ -0,0 +1,72 @@ +import { RevisionApiServiceInterface } from '@standardnotes/api' +import { Uuid } from '@standardnotes/domain-core' + +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { AbstractService } from '../Service/AbstractService' +import { RevisionClientInterface } from './RevisionClientInterface' + +export class RevisionManager extends AbstractService implements RevisionClientInterface { + constructor( + private revisionApiService: RevisionApiServiceInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { + super(internalEventBus) + } + + async listRevisions( + itemUuid: Uuid, + ): Promise<{ uuid: string; content_type: string; created_at: string; updated_at: string; required_role: string }[]> { + try { + const result = await this.revisionApiService.listRevisions(itemUuid.value) + + if (result.data.error) { + return [] + } + + return result.data.revisions + } catch (error) { + return [] + } + } + + async deleteRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise { + try { + const result = await this.revisionApiService.deleteRevision(itemUuid.value, revisionUuid.value) + + if (result.data.error) { + return result.data.error.message + } + + return result.data.message + } catch (error) { + return 'An error occurred while deleting the revision.' + } + } + + async getRevision( + itemUuid: Uuid, + revisionUuid: Uuid, + ): Promise<{ + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string + } | null> { + try { + const result = await this.revisionApiService.getRevision(itemUuid.value, revisionUuid.value) + + if (result.data.error) { + return null + } + + return result.data.revision + } catch (error) { + return null + } + } +} diff --git a/packages/services/src/Domain/Strings/Messages.ts b/packages/services/src/Domain/Strings/Messages.ts index 4268778407b..5b68848fc72 100644 --- a/packages/services/src/Domain/Strings/Messages.ts +++ b/packages/services/src/Domain/Strings/Messages.ts @@ -42,8 +42,6 @@ export const API_MESSAGE_FAILED_SUBSCRIPTION_INFO = "Failed to get subscription' export const API_MESSAGE_FAILED_ACCESS_PURCHASE = 'Failed to access purchase flow.' -export const API_MESSAGE_FAILED_DELETE_REVISION = 'Failed to delete revision.' - export const API_MESSAGE_FAILED_OFFLINE_FEATURES = 'Failed to get offline features.' export const API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING = `The extension you are attempting to install comes from an untrusted source. Untrusted extensions may lower the security of your data. Do you want to continue?` diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 2324cc6f89c..fa357421152 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -68,6 +68,8 @@ export * from './Preferences/PreferenceServiceInterface' export * from './Protection/MobileUnlockTiming' export * from './Protection/ProtectionClientInterface' export * from './Protection/TimingDisplayOption' +export * from './Revision/RevisionClientInterface' +export * from './Revision/RevisionManager' export * from './Service/AbstractService' export * from './Service/ServiceInterface' export * from './Session/SessionManagerResponse' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index aa1375ac9d8..0322ed32b41 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -5,6 +5,8 @@ import { AuthServer, HttpService, HttpServiceInterface, + RevisionApiService, + RevisionServer, SubscriptionApiService, SubscriptionApiServiceInterface, SubscriptionServer, @@ -71,6 +73,8 @@ import { AuthenticatorManager, AuthClientInterface, AuthManager, + RevisionClientInterface, + RevisionManager, } from '@standardnotes/services' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { ComputePrivateUsername } from '@standardnotes/encryption' @@ -80,6 +84,7 @@ import { DecryptedItemInterface, EncryptedItemInterface, Environment, + HistoryEntry, ItemStream, Platform, } from '@standardnotes/models' @@ -100,6 +105,10 @@ import { AddAuthenticator } from '@Lib/Domain/UseCase/AddAuthenticator/AddAuthen import { ListAuthenticators } from '@Lib/Domain/UseCase/ListAuthenticators/ListAuthenticators' import { DeleteAuthenticator } from '@Lib/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator' import { VerifyAuthenticator } from '@Lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator' +import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions' +import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision' +import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision' +import { RevisionMetadata } from '@Lib/Domain/Revision/RevisionMetadata' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 @@ -176,6 +185,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare legacySessionStorageMapper: MapperInterface> private declare authenticatorManager: AuthenticatorClientInterface private declare authManager: AuthClientInterface + private declare revisionManager: RevisionClientInterface private declare _signInWithRecoveryCodes: SignInWithRecoveryCodes private declare _getRecoveryCodes: GetRecoveryCodes @@ -183,6 +193,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare _listAuthenticators: ListAuthenticators private declare _deleteAuthenticator: DeleteAuthenticator private declare _verifyAuthenticator: VerifyAuthenticator + private declare _listRevisions: ListRevisions + private declare _getRevision: GetRevision + private declare _deleteRevision: DeleteRevision private internalEventBus!: ExternalServices.InternalEventBusInterface @@ -289,6 +302,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this._verifyAuthenticator } + get listRevisions(): UseCaseInterface> { + return this._listRevisions + } + + get getRevision(): UseCaseInterface { + return this._getRevision + } + + get deleteRevision(): UseCaseInterface { + return this._deleteRevision + } + public get files(): FilesClientInterface { return this.fileService } @@ -1188,6 +1213,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.createActionsManager() this.createAuthenticatorManager() this.createAuthManager() + this.createRevisionManager() this.createUseCases() } @@ -1239,12 +1265,16 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ;(this.legacySessionStorageMapper as unknown) = undefined ;(this.authenticatorManager as unknown) = undefined ;(this.authManager as unknown) = undefined + ;(this.revisionManager as unknown) = undefined ;(this._signInWithRecoveryCodes as unknown) = undefined ;(this._getRecoveryCodes as unknown) = undefined ;(this._addAuthenticator as unknown) = undefined ;(this._listAuthenticators as unknown) = undefined ;(this._deleteAuthenticator as unknown) = undefined ;(this._verifyAuthenticator as unknown) = undefined + ;(this._listRevisions as unknown) = undefined + ;(this._getRevision as unknown) = undefined + ;(this._deleteRevision as unknown) = undefined this.services = [] } @@ -1683,8 +1713,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.historyManager = new InternalServices.SNHistoryManager( this.itemManager, this.diskStorageService, - this.apiService, - this.protocolService, this.deviceInterface, this.internalEventBus, ) @@ -1790,6 +1818,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.authManager = new AuthManager(authApiService, this.internalEventBus) } + private createRevisionManager() { + const revisionServer = new RevisionServer(this.httpService) + + const revisionApiService = new RevisionApiService(revisionServer) + + this.revisionManager = new RevisionManager(revisionApiService, this.internalEventBus) + } + private createUseCases() { this._signInWithRecoveryCodes = new SignInWithRecoveryCodes( this.authManager, @@ -1815,5 +1851,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.authenticatorManager, this.options.u2fAuthenticatorVerificationPromptFunction, ) + + this._listRevisions = new ListRevisions(this.revisionManager) + + this._getRevision = new GetRevision(this.revisionManager, this.protocolService) + + this._deleteRevision = new DeleteRevision(this.revisionManager) } } diff --git a/packages/snjs/lib/Domain/Revision/Revision.ts b/packages/snjs/lib/Domain/Revision/Revision.ts new file mode 100644 index 00000000000..7bfb4043337 --- /dev/null +++ b/packages/snjs/lib/Domain/Revision/Revision.ts @@ -0,0 +1,11 @@ +export interface Revision { + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string +} diff --git a/packages/snjs/lib/Domain/Revision/RevisionMetadata.ts b/packages/snjs/lib/Domain/Revision/RevisionMetadata.ts new file mode 100644 index 00000000000..9471e029899 --- /dev/null +++ b/packages/snjs/lib/Domain/Revision/RevisionMetadata.ts @@ -0,0 +1,7 @@ +export interface RevisionMetadata { + uuid: string + content_type: string + created_at: string + updated_at: string + required_role: string +} diff --git a/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts new file mode 100644 index 00000000000..9d06d88e902 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts @@ -0,0 +1,49 @@ +import { RevisionClientInterface } from '@standardnotes/services' + +import { DeleteRevision } from './DeleteRevision' + +describe('DeleteRevision', () => { + let revisionManager: RevisionClientInterface + + const createUseCase = () => new DeleteRevision(revisionManager) + + beforeEach(() => { + revisionManager = {} as jest.Mocked + revisionManager.deleteRevision = jest.fn() + }) + + it('should delete revision', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: '00000000-0000-0000-0000-000000000000', + revisionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(false) + }) + + it('should fail if item uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: 'invalid', + revisionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not delete revision: Given value is not a valid uuid: invalid') + }) + + it('should fail if revision uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: '00000000-0000-0000-0000-000000000000', + revisionUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not delete revision: Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.ts b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.ts new file mode 100644 index 00000000000..ba7b6cd9325 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevision.ts @@ -0,0 +1,26 @@ +import { RevisionClientInterface } from '@standardnotes/services' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { DeleteRevisionDTO } from './DeleteRevisionDTO' + +export class DeleteRevision implements UseCaseInterface { + constructor(private revisionManager: RevisionClientInterface) {} + + async execute(dto: DeleteRevisionDTO): Promise> { + const itemUuidOrError = Uuid.create(dto.itemUuid) + if (itemUuidOrError.isFailed()) { + return Result.fail(`Could not delete revision: ${itemUuidOrError.getError()}`) + } + const itemUuid = itemUuidOrError.getValue() + + const revisionUuidOrError = Uuid.create(dto.revisionUuid) + if (revisionUuidOrError.isFailed()) { + return Result.fail(`Could not delete revision: ${revisionUuidOrError.getError()}`) + } + const revisionUuid = revisionUuidOrError.getValue() + + await this.revisionManager.deleteRevision(itemUuid, revisionUuid) + + return Result.ok() + } +} diff --git a/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts new file mode 100644 index 00000000000..8100b332125 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts @@ -0,0 +1,4 @@ +export interface DeleteRevisionDTO { + itemUuid: string + revisionUuid: string +} diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts new file mode 100644 index 00000000000..fae00ee2f90 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts @@ -0,0 +1,71 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { RevisionClientInterface } from '@standardnotes/services' + +import { Revision } from '../../Revision/Revision' + +import { GetRevision } from './GetRevision' + +describe('GetRevision', () => { + let revisionManager: RevisionClientInterface + let protocolService: EncryptionProviderInterface + + const createUseCase = () => new GetRevision(revisionManager, protocolService) + + beforeEach(() => { + revisionManager = {} as jest.Mocked + revisionManager.getRevision = jest.fn().mockReturnValue({} as jest.Mocked) + + protocolService = {} as jest.Mocked + protocolService.getEmbeddedPayloadAuthenticatedData = jest.fn().mockReturnValue({ u: '00000000-0000-0000-0000-000000000000' }) + }) + + it('should get revision', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: '00000000-0000-0000-0000-000000000000', + revisionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue()).toEqual({}) + }) + + it('should fail if item uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: 'invalid', + revisionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not list item revisions: Given value is not a valid uuid: invalid') + }) + + it('should fail if revision uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: '00000000-0000-0000-0000-000000000000', + revisionUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not list item revisions: Given value is not a valid uuid: invalid') + }) + + it('should fail if revision is not found', async () => { + revisionManager.getRevision = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ + itemUuid: '00000000-0000-0000-0000-000000000000', + revisionUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not get revision: Revision not found') + }) +}) diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts new file mode 100644 index 00000000000..267722c7bb0 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts @@ -0,0 +1,80 @@ +import { RevisionClientInterface } from '@standardnotes/services' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { + EncryptedPayload, + HistoryEntry, + isErrorDecryptingPayload, + isRemotePayloadAllowed, + NoteContent, + PayloadTimestampDefaults, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { EncryptionProviderInterface } from '@standardnotes/encryption' + +import { GetRevisionDTO } from './GetRevisionDTO' + +export class GetRevision implements UseCaseInterface { + constructor(private revisionManager: RevisionClientInterface, private protocolService: EncryptionProviderInterface) {} + + async execute(dto: GetRevisionDTO): Promise> { + const itemUuidOrError = Uuid.create(dto.itemUuid) + if (itemUuidOrError.isFailed()) { + return Result.fail(`Could not get revision: ${itemUuidOrError.getError()}`) + } + const itemUuid = itemUuidOrError.getValue() + + const revisionUuidOrError = Uuid.create(dto.revisionUuid) + if (revisionUuidOrError.isFailed()) { + return Result.fail(`Could not get revision: ${revisionUuidOrError.getError()}`) + } + const revisionUuid = revisionUuidOrError.getValue() + + const revision = await this.revisionManager.getRevision(itemUuid, revisionUuid) + if (revision === null) { + return Result.fail('Could not get revision: Revision not found') + } + + const serverPayload = new EncryptedPayload({ + ...PayloadTimestampDefaults(), + uuid: revision.uuid, + content: revision.content as string, + enc_item_key: revision.enc_item_key as string, + items_key_id: revision.items_key_id as string, + auth_hash: revision.auth_hash as string, + content_type: revision.content_type as ContentType, + updated_at: new Date(revision.updated_at), + created_at: new Date(revision.created_at), + waitingForKey: false, + errorDecrypting: false, + }) + + /** + * When an item is duplicated, its revisions also carry over to the newly created item. + * However since the new item has a different UUID than the source item, we must decrypt + * these olders revisions (which have not been mutated after copy) with the source item's + * uuid. + */ + const embeddedParams = this.protocolService.getEmbeddedPayloadAuthenticatedData(serverPayload) + const sourceItemUuid = embeddedParams?.u as string | undefined + + const payload = serverPayload.copy({ + uuid: sourceItemUuid || revision.item_uuid, + }) + + if (!isRemotePayloadAllowed(payload)) { + return Result.fail(`Remote payload is disallowed: ${JSON.stringify(payload)}`) + } + + const encryptedPayload = new EncryptedPayload(payload) + + const decryptedPayload = await this.protocolService.decryptSplitSingle({ + usesItemsKeyWithKeyLookup: { items: [encryptedPayload] }, + }) + + if (isErrorDecryptingPayload(decryptedPayload)) { + return Result.fail('Could not decrypt revision.') + } + + return Result.ok(new HistoryEntry(decryptedPayload)) + } +} diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevisionDTO.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevisionDTO.ts new file mode 100644 index 00000000000..d95f717ff3c --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevisionDTO.ts @@ -0,0 +1,4 @@ +export interface GetRevisionDTO { + itemUuid: string + revisionUuid: string +} diff --git a/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.spec.ts b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.spec.ts new file mode 100644 index 00000000000..d54d6adbeb4 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.spec.ts @@ -0,0 +1,32 @@ +import { RevisionClientInterface } from '@standardnotes/services' + +import { ListRevisions } from './ListRevisions' + +describe('ListRevisions', () => { + let revisionManager: RevisionClientInterface + + const createUseCase = () => new ListRevisions(revisionManager) + + beforeEach(() => { + revisionManager = {} as jest.Mocked + revisionManager.listRevisions = jest.fn().mockReturnValue([]) + }) + + it('should list revisions', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ itemUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue()).toEqual([]) + }) + + it('should fail if item uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ itemUuid: 'invalid' }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not list item revisions: Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.ts b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.ts new file mode 100644 index 00000000000..41992590347 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisions.ts @@ -0,0 +1,22 @@ +import { RevisionClientInterface } from '@standardnotes/services' +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { RevisionMetadata } from '../../Revision/RevisionMetadata' + +import { ListRevisionsDTO } from './ListRevisionsDTO' + +export class ListRevisions implements UseCaseInterface> { + constructor(private revisionManager: RevisionClientInterface) {} + + async execute(dto: ListRevisionsDTO): Promise> { + const itemUuidOrError = Uuid.create(dto.itemUuid) + if (itemUuidOrError.isFailed()) { + return Result.fail(`Could not list item revisions: ${itemUuidOrError.getError()}`) + } + const itemUuid = itemUuidOrError.getValue() + + const revisions = await this.revisionManager.listRevisions(itemUuid) + + return Result.ok(revisions) + } +} diff --git a/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisionsDTO.ts b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisionsDTO.ts new file mode 100644 index 00000000000..737a738d321 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/ListRevisions/ListRevisionsDTO.ts @@ -0,0 +1,3 @@ +export interface ListRevisionsDTO { + itemUuid: string +} diff --git a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts index f4be778f084..26ea0c7fe47 100644 --- a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts +++ b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts @@ -1,5 +1,8 @@ +import { HistoryEntry } from '@standardnotes/models' import { UseCaseInterface } from '@standardnotes/domain-core' +import { RevisionMetadata } from '../Revision/RevisionMetadata' + export interface UseCaseContainerInterface { get signInWithRecoveryCodes(): UseCaseInterface get getRecoveryCodes(): UseCaseInterface @@ -7,4 +10,7 @@ export interface UseCaseContainerInterface { get listAuthenticators(): UseCaseInterface> get deleteAuthenticator(): UseCaseInterface get verifyAuthenticator(): UseCaseInterface + get listRevisions(): UseCaseInterface> + get getRevision(): UseCaseInterface + get deleteRevision(): UseCaseInterface } diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 909ebe67500..bf70a4b01a6 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -18,7 +18,6 @@ import { API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS, API_MESSAGE_FAILED_ACCESS_PURCHASE, API_MESSAGE_FAILED_CREATE_FILE_TOKEN, - API_MESSAGE_FAILED_DELETE_REVISION, API_MESSAGE_FAILED_GET_SETTINGS, API_MESSAGE_FAILED_LISTED_REGISTRATION, API_MESSAGE_FAILED_OFFLINE_ACTIVATION, @@ -488,7 +487,7 @@ export class SNApiService return preprocessingError } const url = joinPaths(this.host, Paths.v1.session(sessionId)) - const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService + const response: Responses.SessionListResponse | Responses.HttpResponse = await this.httpService .deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken()) .catch((error: Responses.HttpResponse) => { const errorResponse = error as Responses.HttpResponse @@ -505,53 +504,6 @@ export class SNApiService return response } - async getItemRevisions(itemId: UuidString): Promise { - const preprocessingError = this.preprocessingError() - if (preprocessingError) { - return preprocessingError - } - const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId)) - const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService - .getAbsolute(url, undefined, this.getSessionAccessToken()) - .catch((errorResponse: Responses.HttpResponse) => { - this.preprocessAuthenticatedErrorResponse(errorResponse) - if (Responses.isErrorResponseExpiredToken(errorResponse)) { - return this.refreshSessionThenRetryRequest({ - verb: HttpVerb.Get, - url, - }) - } - return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL) - }) - this.processResponse(response) - return response - } - - async getRevision( - entry: Responses.RevisionListEntry, - itemId: UuidString, - ): Promise { - const preprocessingError = this.preprocessingError() - if (preprocessingError) { - return preprocessingError - } - const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid)) - const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService - .getAbsolute(url, undefined, this.getSessionAccessToken()) - .catch((errorResponse: Responses.HttpResponse) => { - this.preprocessAuthenticatedErrorResponse(errorResponse) - if (Responses.isErrorResponseExpiredToken(errorResponse)) { - return this.refreshSessionThenRetryRequest({ - verb: HttpVerb.Get, - url, - }) - } - return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL) - }) - this.processResponse(response) - return response - } - async getUserFeatures(userUuid: UuidString): Promise { const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid)) const response = await this.httpService @@ -652,20 +604,6 @@ export class SNApiService }) } - async deleteRevision( - itemUuid: UuidString, - entry: Responses.RevisionListEntry, - ): Promise { - const url = joinPaths(this.host, Paths.v1.itemRevision(itemUuid, entry.uuid)) - const response = await this.tokenRefreshableRequest({ - verb: HttpVerb.Delete, - url, - fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION, - authentication: this.getSessionAccessToken(), - }) - return response - } - public downloadFeatureUrl(url: string): Promise { return this.request({ verb: HttpVerb.Get, diff --git a/packages/snjs/lib/Services/History/HistoryManager.ts b/packages/snjs/lib/Services/History/HistoryManager.ts index 97d7e9623e1..fd6c4b7bef5 100644 --- a/packages/snjs/lib/Services/History/HistoryManager.ts +++ b/packages/snjs/lib/Services/History/HistoryManager.ts @@ -1,13 +1,11 @@ -import { ContentType, Uuid } from '@standardnotes/common' -import { isNullOrUndefined, removeFromArray } from '@standardnotes/utils' +import { ContentType } from '@standardnotes/common' +import { removeFromArray } from '@standardnotes/utils' import { ItemManager } from '@Lib/Services/Items/ItemManager' -import { SNApiService } from '@Lib/Services/Api/ApiService' import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService' import { UuidString } from '../../Types/UuidString' import * as Models from '@standardnotes/models' -import * as Responses from '@standardnotes/responses' -import { isErrorDecryptingPayload, PayloadTimestampDefaults, SNNote } from '@standardnotes/models' -import { AbstractService, EncryptionService, DeviceInterface, InternalEventBusInterface } from '@standardnotes/services' +import { SNNote } from '@standardnotes/models' +import { AbstractService, DeviceInterface, InternalEventBusInterface } from '@standardnotes/services' /** The amount of revisions per item above which should call for an optimization. */ const DefaultItemRevisionsThreshold = 20 @@ -46,8 +44,6 @@ export class SNHistoryManager extends AbstractService { constructor( private itemManager: ItemManager, private storageService: DiskStorageService, - private apiService: SNApiService, - private protocolService: EncryptionService, public deviceInterface: DeviceInterface, protected override internalEventBus: InternalEventBusInterface, ) { @@ -120,78 +116,6 @@ export class SNHistoryManager extends AbstractService { return Object.freeze(copy) } - /** - * Fetches a list of revisions from the server for an item. These revisions do not - * include the item's content. Instead, each revision's content must be fetched - * individually upon selection via `fetchRemoteRevision`. - */ - async remoteHistoryForItem(item: Models.SNNote): Promise { - const response = await this.apiService.getItemRevisions(item.uuid) - if (response.error || isNullOrUndefined(response.data)) { - return undefined - } - return (response as Responses.RevisionListResponse).data - } - - /** - * Expands on a revision fetched via `remoteHistoryForItem` by getting a revision's - * complete fields (including encrypted content). - */ - async fetchRemoteRevision( - note: Models.SNNote, - entry: Responses.RevisionListEntry, - ): Promise { - const revisionResponse = await this.apiService.getRevision(entry, note.uuid) - if (revisionResponse.error || isNullOrUndefined(revisionResponse.data)) { - return undefined - } - const revision = (revisionResponse as Responses.SingleRevisionResponse).data - - const serverPayload = new Models.EncryptedPayload({ - ...PayloadTimestampDefaults(), - ...revision, - updated_at: new Date(revision.updated_at), - created_at: new Date(revision.created_at), - waitingForKey: false, - errorDecrypting: false, - }) - - /** - * When an item is duplicated, its revisions also carry over to the newly created item. - * However since the new item has a different UUID than the source item, we must decrypt - * these olders revisions (which have not been mutated after copy) with the source item's - * uuid. - */ - const embeddedParams = this.protocolService.getEmbeddedPayloadAuthenticatedData(serverPayload) - const sourceItemUuid = embeddedParams?.u as Uuid | undefined - - const payload = serverPayload.copy({ - uuid: sourceItemUuid || revision.item_uuid, - }) - - if (!Models.isRemotePayloadAllowed(payload)) { - console.error('Remote payload is disallowed', payload) - return undefined - } - - const encryptedPayload = new Models.EncryptedPayload(payload) - - const decryptedPayload = await this.protocolService.decryptSplitSingle({ - usesItemsKeyWithKeyLookup: { items: [encryptedPayload] }, - }) - - if (isErrorDecryptingPayload(decryptedPayload)) { - return undefined - } - - return new Models.HistoryEntry(decryptedPayload) - } - - async deleteRemoteRevision(note: SNNote, entry: Responses.RevisionListEntry): Promise { - const response = await this.apiService.deleteRevision(note.uuid, entry) - return response - } - /** * Clean up if there are too many revisions. Note itemRevisionThreshold * is the amount of revisions which above, call for an optimization. An diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index 99625a88ffa..ef4e6c3b716 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -25,6 +25,8 @@ describe('history manager', () => { beforeEach(async function () { this.application = await Factory.createInitAppWithFakeCrypto() this.historyManager = this.application.historyManager + this.listRevisions = this.application.listRevisions + this.getRevision = this.application.getRevision this.payloadManager = this.application.payloadManager /** Automatically optimize after every revision by setting this to 0 */ this.historyManager.itemRevisionThreshold = 0 @@ -282,13 +284,13 @@ describe('history manager', () => { this.payloadManager = this.application.payloadManager const item = await Factory.createSyncedNote(this.application) await this.application.syncService.sync(syncOptions) - const itemHistory = await this.historyManager.remoteHistoryForItem(item) + const itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory).to.be.undefined }) it('create basic history entries 2', async function () { const item = await Factory.createSyncedNote(this.application) - let itemHistory = await this.historyManager.remoteHistoryForItem(item) + let itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() /** Server history should save initial revision */ expect(itemHistory).to.be.ok @@ -296,7 +298,7 @@ describe('history manager', () => { /** Sync within 5 minutes, should not create a new entry */ await Factory.markDirtyAndSyncItem(this.application, item) - itemHistory = await this.historyManager.remoteHistoryForItem(item) + itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(1) /** Sync with different contents, should not create a new entry */ @@ -309,7 +311,7 @@ describe('history manager', () => { undefined, syncOptions, ) - itemHistory = await this.historyManager.remoteHistoryForItem(item) + itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(1) }) @@ -328,11 +330,11 @@ describe('history manager', () => { undefined, syncOptions, ) - let itemHistory = await this.historyManager.remoteHistoryForItem(item) + let itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(2) const oldestEntry = lastElement(itemHistory) - let revisionFromServer = await this.historyManager.fetchRemoteRevision(item, oldestEntry) + let revisionFromServer = await this.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }).getValue() expect(revisionFromServer).to.be.ok let payloadFromServer = revisionFromServer.payload @@ -350,8 +352,8 @@ describe('history manager', () => { const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) - const dupeHistory = await this.historyManager.remoteHistoryForItem(dupe) - const dupeRevision = await this.historyManager.fetchRemoteRevision(dupe, dupeHistory[0]) + const dupeHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const dupeRevision = await this.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }).getValue() expect(dupeRevision.payload.uuid).to.equal(dupe.uuid) }) @@ -376,8 +378,8 @@ describe('history manager', () => { await Factory.markDirtyAndSyncItem(this.application, dupe) const expectedRevisions = 3 - const noteHistory = await this.historyManager.remoteHistoryForItem(note) - const dupeHistory = await this.historyManager.remoteHistoryForItem(dupe) + const noteHistory = await this.listRevisions.execute({ itemUuid: note.uuid }).getValue() + const dupeHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() expect(noteHistory.length).to.equal(expectedRevisions) expect(dupeHistory.length).to.equal(expectedRevisions) }) @@ -398,11 +400,11 @@ describe('history manager', () => { const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) - const itemHistory = await this.historyManager.remoteHistoryForItem(dupe) + const itemHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() expect(itemHistory.length).to.be.above(1) const oldestRevision = lastElement(itemHistory) - const fetched = await this.historyManager.fetchRemoteRevision(dupe, oldestRevision) + const fetched = await this.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: oldestRevision.uuid }).getValue() expect(fetched.payload.errorDecrypting).to.not.be.ok expect(fetched.payload.content.title).to.equal(changedText) }) diff --git a/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts b/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts index b4dbf121449..7347124cb8d 100644 --- a/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts +++ b/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts @@ -130,7 +130,14 @@ export class NoteHistoryController { try { this.setSelectedEntry(entry) - const remoteRevision = await this.application.historyManager.fetchRemoteRevision(this.note, entry) + const remoteRevisionOrError = await this.application.getRevision.execute({ + itemUuid: this.note.uuid, + revisionUuid: entry.uuid, + }) + if (remoteRevisionOrError.isFailed()) { + throw new Error(remoteRevisionOrError.getError()) + } + const remoteRevision = remoteRevisionOrError.getValue() this.setSelectedRevision(remoteRevision) } catch (err) { this.clearSelection() @@ -234,9 +241,13 @@ export class NoteHistoryController { if (this.note) { this.setIsFetchingRemoteHistory(true) try { - const initialRemoteHistory = await this.application.historyManager.remoteHistoryForItem(this.note) + const revisionsListOrError = await this.application.listRevisions.execute({ itemUuid: this.note.uuid }) + if (revisionsListOrError.isFailed()) { + throw new Error(revisionsListOrError.getError()) + } + const revisionsList = revisionsListOrError.getValue() - this.setRemoteHistory(sortRevisionListIntoGroups(initialRemoteHistory)) + this.setRemoteHistory(sortRevisionListIntoGroups(revisionsList)) } catch (err) { console.error(err) } finally { @@ -354,10 +365,12 @@ export class NoteHistoryController { return } - const response = await this.application.historyManager.deleteRemoteRevision(this.note, revisionEntry) - - if (response.error?.message) { - throw new Error(response.error.message) + const deleteRevisionOrError = await this.application.deleteRevision.execute({ + itemUuid: this.note.uuid, + revisionUuid: revisionEntry.uuid, + }) + if (deleteRevisionOrError.isFailed()) { + throw new Error(deleteRevisionOrError.getError()) } this.clearSelection() From 8aa054810ec8c56a5427704991e21cebcc4ee9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 16 Jan 2023 20:21:05 +0100 Subject: [PATCH 2/7] fix(snjs): reference listing and getting revisions in specs --- packages/snjs/mocha/history.test.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index ef4e6c3b716..7f7055e3888 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -25,8 +25,6 @@ describe('history manager', () => { beforeEach(async function () { this.application = await Factory.createInitAppWithFakeCrypto() this.historyManager = this.application.historyManager - this.listRevisions = this.application.listRevisions - this.getRevision = this.application.getRevision this.payloadManager = this.application.payloadManager /** Automatically optimize after every revision by setting this to 0 */ this.historyManager.itemRevisionThreshold = 0 @@ -284,13 +282,13 @@ describe('history manager', () => { this.payloadManager = this.application.payloadManager const item = await Factory.createSyncedNote(this.application) await this.application.syncService.sync(syncOptions) - const itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() + const itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory).to.be.undefined }) it('create basic history entries 2', async function () { const item = await Factory.createSyncedNote(this.application) - let itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() + let itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() /** Server history should save initial revision */ expect(itemHistory).to.be.ok @@ -298,7 +296,7 @@ describe('history manager', () => { /** Sync within 5 minutes, should not create a new entry */ await Factory.markDirtyAndSyncItem(this.application, item) - itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() + itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(1) /** Sync with different contents, should not create a new entry */ @@ -311,7 +309,7 @@ describe('history manager', () => { undefined, syncOptions, ) - itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() + itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(1) }) @@ -330,11 +328,11 @@ describe('history manager', () => { undefined, syncOptions, ) - let itemHistory = await this.listRevisions.execute({ itemUuid: item.uuid }).getValue() + let itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() expect(itemHistory.length).to.equal(2) const oldestEntry = lastElement(itemHistory) - let revisionFromServer = await this.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }).getValue() + let revisionFromServer = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }).getValue() expect(revisionFromServer).to.be.ok let payloadFromServer = revisionFromServer.payload @@ -352,8 +350,8 @@ describe('history manager', () => { const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) - const dupeHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() - const dupeRevision = await this.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }).getValue() + const dupeHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const dupeRevision = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }).getValue() expect(dupeRevision.payload.uuid).to.equal(dupe.uuid) }) @@ -378,8 +376,8 @@ describe('history manager', () => { await Factory.markDirtyAndSyncItem(this.application, dupe) const expectedRevisions = 3 - const noteHistory = await this.listRevisions.execute({ itemUuid: note.uuid }).getValue() - const dupeHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const noteHistory = await this.application.listRevisions.execute({ itemUuid: note.uuid }).getValue() + const dupeHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() expect(noteHistory.length).to.equal(expectedRevisions) expect(dupeHistory.length).to.equal(expectedRevisions) }) @@ -400,11 +398,11 @@ describe('history manager', () => { const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) - const itemHistory = await this.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const itemHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() expect(itemHistory.length).to.be.above(1) const oldestRevision = lastElement(itemHistory) - const fetched = await this.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: oldestRevision.uuid }).getValue() + const fetched = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: oldestRevision.uuid }).getValue() expect(fetched.payload.errorDecrypting).to.not.be.ok expect(fetched.payload.content.title).to.equal(changedText) }) From cc29aaed487bd671d707721e42d494be132a13f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 17 Jan 2023 16:02:31 +0100 Subject: [PATCH 3/7] fix(snjs): revisions specs --- .../Domain/Server/Revision/RevisionServer.ts | 2 +- packages/snjs/mocha/history.test.js | 67 +++++++++++++++---- packages/snjs/mocha/lib/factory.js | 1 + 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/api/src/Domain/Server/Revision/RevisionServer.ts b/packages/api/src/Domain/Server/Revision/RevisionServer.ts index f9227b6e334..a25ff521e38 100644 --- a/packages/api/src/Domain/Server/Revision/RevisionServer.ts +++ b/packages/api/src/Domain/Server/Revision/RevisionServer.ts @@ -17,7 +17,7 @@ export class RevisionServer implements RevisionServerInterface { } async getRevision(params: GetRevisionRequestParams): Promise { - const response = await this.httpService.post(Paths.v2.getRevision(params.itemUuid, params.revisionUuid)) + const response = await this.httpService.get(Paths.v2.getRevision(params.itemUuid, params.revisionUuid)) return response as GetRevisionResponse } diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index 7f7055e3888..09888e84bbc 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -282,21 +282,30 @@ describe('history manager', () => { this.payloadManager = this.application.payloadManager const item = await Factory.createSyncedNote(this.application) await this.application.syncService.sync(syncOptions) - const itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() - expect(itemHistory).to.be.undefined + const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid }) + + expect(itemHistoryOrError.isFailed()).to.equal(false) + expect(itemHistoryOrError.getValue().length).to.equal(0) }) it('create basic history entries 2', async function () { const item = await Factory.createSyncedNote(this.application) - let itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() + await Factory.sleep(Factory.ServerRevisionCreationDelay) + + let itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid }) + expect(itemHistoryOrError.isFailed()).to.equal(false) + + let itemHistory = itemHistoryOrError.getValue() /** Server history should save initial revision */ - expect(itemHistory).to.be.ok expect(itemHistory.length).to.equal(1) /** Sync within 5 minutes, should not create a new entry */ await Factory.markDirtyAndSyncItem(this.application, item) - itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() + itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid }) + expect(itemHistoryOrError.isFailed()).to.equal(false) + + itemHistory = itemHistoryOrError.getValue() expect(itemHistory.length).to.equal(1) /** Sync with different contents, should not create a new entry */ @@ -309,7 +318,10 @@ describe('history manager', () => { undefined, syncOptions, ) - itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() + itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid }) + expect(itemHistoryOrError.isFailed()).to.equal(false) + + itemHistory = itemHistoryOrError.getValue() expect(itemHistory.length).to.equal(1) }) @@ -328,11 +340,19 @@ describe('history manager', () => { undefined, syncOptions, ) - let itemHistory = await this.application.listRevisions.execute({ itemUuid: item.uuid }).getValue() + await Factory.sleep(Factory.ServerRevisionCreationDelay) + + const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid }) + expect(itemHistoryOrError.isFailed()).to.equal(false) + + const itemHistory = itemHistoryOrError.getValue() expect(itemHistory.length).to.equal(2) const oldestEntry = lastElement(itemHistory) - let revisionFromServer = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }).getValue() + let revisionFromServerOrError = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }) + expect(revisionFromServerOrError.isFailed()).to.equal(false) + + const revisionFromServer = revisionFromServerOrError.getValue() expect(revisionFromServer).to.be.ok let payloadFromServer = revisionFromServer.payload @@ -349,9 +369,16 @@ describe('history manager', () => { await Factory.markDirtyAndSyncItem(this.application, note) const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) + await Factory.sleep(Factory.ServerRevisionCreationDelay) + + const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }) + expect(dupeHistoryOrError.isFailed()).to.equal(false) + const dupeHistory = dupeHistoryOrError.getValue() - const dupeHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() - const dupeRevision = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }).getValue() + const dupeRevisionOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }) + expect(dupeRevisionOrError.isFailed()).to.equal(false) + + const dupeRevision = dupeRevisionOrError.getValue() expect(dupeRevision.payload.uuid).to.equal(dupe.uuid) }) @@ -376,8 +403,14 @@ describe('history manager', () => { await Factory.markDirtyAndSyncItem(this.application, dupe) const expectedRevisions = 3 - const noteHistory = await this.application.listRevisions.execute({ itemUuid: note.uuid }).getValue() - const dupeHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const noteHistoryOrError = await this.application.listRevisions.execute({ itemUuid: note.uuid }) + expect(noteHistoryOrError.isFailed()).to.equal(false) + const noteHistory = noteHistoryOrError.getValue() + + const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }) + expect(dupeHistoryOrError.isFailed()).to.equal(false) + const dupeHistory = dupeHistoryOrError.getValue() + expect(noteHistory.length).to.equal(expectedRevisions) expect(dupeHistory.length).to.equal(expectedRevisions) }) @@ -398,11 +431,17 @@ describe('history manager', () => { const dupe = await this.application.itemManager.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) - const itemHistory = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }).getValue() + const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }) + expect(itemHistoryOrError.isFailed()).to.equal(false) + + const itemHistory = itemHistoryOrError.getValue() expect(itemHistory.length).to.be.above(1) const oldestRevision = lastElement(itemHistory) - const fetched = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: oldestRevision.uuid }).getValue() + const fetchedOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: oldestRevision.uuid }) + expect(fetchedOrError.isFailed()).to.equal(false) + + const fetched = fetchedOrError.getValue() expect(fetched.payload.errorDecrypting).to.not.be.ok expect(fetched.payload.content.title).to.equal(changedText) }) diff --git a/packages/snjs/mocha/lib/factory.js b/packages/snjs/mocha/lib/factory.js index ef46abff70e..27f572f47f9 100644 --- a/packages/snjs/mocha/lib/factory.js +++ b/packages/snjs/mocha/lib/factory.js @@ -290,6 +290,7 @@ export async function storagePayloadCount(application) { * Controlled via docker/syncing-server-js.env */ export const ServerRevisionFrequency = 1.1 +export const ServerRevisionCreationDelay = 1 export function yesterday() { return new Date(new Date().setDate(new Date().getDate() - 1)) From e920ec919b91d512695769815b4ddf2dfd189cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 18 Jan 2023 06:43:13 +0100 Subject: [PATCH 4/7] fix(web): usage of revision metadata --- packages/snjs/lib/Domain/index.ts | 2 ++ packages/snjs/lib/index.ts | 1 + .../RevisionHistoryModal/HistoryModalFooter.tsx | 8 ++++---- .../RevisionHistoryModal/RemoteHistoryList.tsx | 6 +++--- .../Components/RevisionHistoryModal/utils.ts | 12 ++++++------ .../NoteHistory/NoteHistoryController.ts | 15 ++++++++------- 6 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 packages/snjs/lib/Domain/index.ts diff --git a/packages/snjs/lib/Domain/index.ts b/packages/snjs/lib/Domain/index.ts new file mode 100644 index 00000000000..a5ff50e6f89 --- /dev/null +++ b/packages/snjs/lib/Domain/index.ts @@ -0,0 +1,2 @@ +export * from './Revision/Revision' +export * from './Revision/RevisionMetadata' diff --git a/packages/snjs/lib/index.ts b/packages/snjs/lib/index.ts index 75264e0d80c..d3821b4a67f 100644 --- a/packages/snjs/lib/index.ts +++ b/packages/snjs/lib/index.ts @@ -1,6 +1,7 @@ export * from './Application' export * from './ApplicationGroup' export * from './Client' +export * from './Domain' export * from './Log' export * from './Migrations' export * from './Services' diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalFooter.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalFooter.tsx index 4963ab789a8..38d71c9d4f3 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalFooter.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalFooter.tsx @@ -1,5 +1,5 @@ import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController' -import { RevisionListEntry } from '@standardnotes/snjs' +import { RevisionMetadata } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { useCallback, useState } from 'react' import Button from '@/Components/Button/Button' @@ -36,7 +36,7 @@ const HistoryModalFooter = ({ dismissModal, noteHistoryController }: Props) => { } setIsDeletingRevision(true) - await deleteRemoteRevision(selectedEntry as RevisionListEntry) + await deleteRemoteRevision(selectedEntry as RevisionMetadata) setIsDeletingRevision(false) }, [deleteRemoteRevision, selectedEntry]) @@ -45,13 +45,13 @@ const HistoryModalFooter = ({ dismissModal, noteHistoryController }: Props) => { )}