feat(snjs): add revisions api v2 (#2154)
* feat(snjs): add revisions api v2 * fix(snjs): reference listing and getting revisions in specs * fix(snjs): revisions specs * fix(web): usage of revision metadata * fix(snjs): add specs for decryption revision * fix(snjs): issue with building mocked specs * fix(snjs): adjust revision creation delay
This commit is contained in:
@@ -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, <string>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<Responses.RevisionListResponse | Responses.HttpResponse> {
|
||||
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<Responses.SingleRevisionResponse | Responses.HttpResponse> {
|
||||
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<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
||||
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<Responses.MinimalHttpResponse> {
|
||||
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<Responses.HttpResponse> {
|
||||
return this.request({
|
||||
verb: HttpVerb.Get,
|
||||
|
||||
@@ -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<Responses.RevisionListEntry[] | undefined> {
|
||||
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<Models.HistoryEntry | undefined> {
|
||||
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<Models.NoteContent>({
|
||||
usesItemsKeyWithKeyLookup: { items: [encryptedPayload] },
|
||||
})
|
||||
|
||||
if (isErrorDecryptingPayload(decryptedPayload)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new Models.HistoryEntry(decryptedPayload)
|
||||
}
|
||||
|
||||
async deleteRemoteRevision(note: SNNote, entry: Responses.RevisionListEntry): Promise<Responses.MinimalHttpResponse> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user