UpdateExistingItem.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {
  2. ContentType,
  3. Dates,
  4. NotificationPayload,
  5. NotificationPayloadIdentifierType,
  6. NotificationType,
  7. Result,
  8. Timestamps,
  9. UseCaseInterface,
  10. Uuid,
  11. Validator,
  12. } from '@standardnotes/domain-core'
  13. import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
  14. import { TimerInterface } from '@standardnotes/time'
  15. import { Item } from '../../../Item/Item'
  16. import { UpdateExistingItemDTO } from './UpdateExistingItemDTO'
  17. import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
  18. import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
  19. import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
  20. import { DetermineSharedVaultOperationOnItem } from '../../SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
  21. import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
  22. import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
  23. import { ItemHash } from '../../../Item/ItemHash'
  24. import { AddNotificationsForUsers } from '../../Messaging/AddNotificationsForUsers/AddNotificationsForUsers'
  25. import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
  26. export class UpdateExistingItem implements UseCaseInterface<Item> {
  27. constructor(
  28. private itemRepository: ItemRepositoryInterface,
  29. private timer: TimerInterface,
  30. private domainEventPublisher: DomainEventPublisherInterface,
  31. private domainEventFactory: DomainEventFactoryInterface,
  32. private freeRevisionFrequency: number,
  33. private premiumRevisionFrequency: number,
  34. private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
  35. private addNotificationForUsers: AddNotificationsForUsers,
  36. private removeNotificationsForUser: RemoveNotificationsForUser,
  37. ) {}
  38. async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
  39. const userUuidOrError = Uuid.create(dto.performingUserUuid)
  40. if (userUuidOrError.isFailed()) {
  41. return Result.fail(`User uuid is invalid: ${userUuidOrError.getError()}`)
  42. }
  43. const userUuid = userUuidOrError.getValue()
  44. let sharedVaultOperation: SharedVaultOperationOnItem | null = null
  45. if (dto.itemHash.representsASharedVaultItem() || dto.existingItem.isAssociatedWithASharedVault()) {
  46. const sharedVaultOperationOrError = await this.determineSharedVaultOperationOnItem.execute({
  47. existingItem: dto.existingItem,
  48. itemHash: dto.itemHash,
  49. userUuid: userUuid.value,
  50. })
  51. if (sharedVaultOperationOrError.isFailed()) {
  52. return Result.fail(sharedVaultOperationOrError.getError())
  53. }
  54. sharedVaultOperation = sharedVaultOperationOrError.getValue()
  55. }
  56. let sessionUuid = null
  57. if (dto.sessionUuid) {
  58. const sessionUuidOrError = Uuid.create(dto.sessionUuid)
  59. if (sessionUuidOrError.isFailed()) {
  60. return Result.fail(`Session uuid is invalid: ${sessionUuidOrError.getError()}`)
  61. }
  62. sessionUuid = sessionUuidOrError.getValue()
  63. }
  64. dto.existingItem.props.updatedWithSession = sessionUuid
  65. if (dto.itemHash.props.content) {
  66. dto.existingItem.props.content = dto.itemHash.props.content
  67. }
  68. if (dto.itemHash.props.content_type) {
  69. const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
  70. if (contentTypeOrError.isFailed()) {
  71. return Result.fail(contentTypeOrError.getError())
  72. }
  73. const contentType = contentTypeOrError.getValue()
  74. dto.existingItem.props.contentType = contentType
  75. }
  76. if (dto.itemHash.props.deleted !== undefined) {
  77. dto.existingItem.props.deleted = dto.itemHash.props.deleted
  78. }
  79. let wasMarkedAsDuplicate = false
  80. if (dto.itemHash.props.duplicate_of) {
  81. const duplicateOfOrError = Uuid.create(dto.itemHash.props.duplicate_of)
  82. if (duplicateOfOrError.isFailed()) {
  83. return Result.fail(`Duplicate of uuid is invalid: ${duplicateOfOrError.getError()}`)
  84. }
  85. wasMarkedAsDuplicate = dto.existingItem.props.duplicateOf === null
  86. dto.existingItem.props.duplicateOf = duplicateOfOrError.getValue()
  87. }
  88. if (dto.itemHash.props.auth_hash) {
  89. dto.existingItem.props.authHash = dto.itemHash.props.auth_hash
  90. }
  91. if (dto.itemHash.props.enc_item_key) {
  92. dto.existingItem.props.encItemKey = dto.itemHash.props.enc_item_key
  93. }
  94. if (dto.itemHash.props.items_key_id) {
  95. dto.existingItem.props.itemsKeyId = dto.itemHash.props.items_key_id
  96. }
  97. const updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
  98. const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
  99. updatedAtTimestamp - dto.existingItem.props.timestamps.updatedAt,
  100. )
  101. const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp)
  102. const { createdAtDate, createdAtTimestamp } = this.determineCreatedAt(dto.itemHash)
  103. const datesOrError = Dates.create(createdAtDate, updatedAtDate)
  104. if (datesOrError.isFailed()) {
  105. return Result.fail(datesOrError.getError())
  106. }
  107. dto.existingItem.props.dates = datesOrError.getValue()
  108. const timestampsOrError = Timestamps.create(createdAtTimestamp, updatedAtTimestamp)
  109. if (timestampsOrError.isFailed()) {
  110. return Result.fail(timestampsOrError.getError())
  111. }
  112. dto.existingItem.props.timestamps = timestampsOrError.getValue()
  113. dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
  114. if (dto.itemHash.representsASharedVaultItem()) {
  115. const sharedVaultAssociationOrError = SharedVaultAssociation.create({
  116. lastEditedBy: userUuid,
  117. sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
  118. })
  119. if (sharedVaultAssociationOrError.isFailed()) {
  120. return Result.fail(sharedVaultAssociationOrError.getError())
  121. }
  122. dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
  123. } else {
  124. dto.existingItem.props.sharedVaultAssociation = undefined
  125. }
  126. if (dto.itemHash.hasDedicatedKeySystemAssociation()) {
  127. const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier)
  128. if (keySystemIdentifiedValidationResult.isFailed()) {
  129. return Result.fail(keySystemIdentifiedValidationResult.getError())
  130. }
  131. const keySystemIdentifier = dto.itemHash.props.key_system_identifier as string
  132. const keySystemAssociationOrError = KeySystemAssociation.create(keySystemIdentifier)
  133. if (keySystemAssociationOrError.isFailed()) {
  134. return Result.fail(keySystemAssociationOrError.getError())
  135. }
  136. dto.existingItem.props.keySystemAssociation = keySystemAssociationOrError.getValue()
  137. } else {
  138. dto.existingItem.props.keySystemAssociation = undefined
  139. }
  140. if (dto.itemHash.props.deleted === true) {
  141. dto.existingItem.props.deleted = true
  142. dto.existingItem.props.content = null
  143. dto.existingItem.props.contentSize = 0
  144. dto.existingItem.props.encItemKey = null
  145. dto.existingItem.props.authHash = null
  146. dto.existingItem.props.itemsKeyId = null
  147. }
  148. await this.itemRepository.update(dto.existingItem)
  149. /* istanbul ignore next */
  150. const revisionsFrequency = dto.isFreeUser ? this.freeRevisionFrequency : this.premiumRevisionFrequency
  151. if (secondsFromLastUpdate >= revisionsFrequency) {
  152. if (
  153. dto.existingItem.props.contentType.value !== null &&
  154. [ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)
  155. ) {
  156. await this.domainEventPublisher.publish(
  157. this.domainEventFactory.createItemRevisionCreationRequested({
  158. itemUuid: dto.existingItem.id.toString(),
  159. userUuid: dto.existingItem.props.userUuid.value,
  160. }),
  161. )
  162. }
  163. }
  164. if (wasMarkedAsDuplicate) {
  165. await this.domainEventPublisher.publish(
  166. this.domainEventFactory.createDuplicateItemSyncedEvent({
  167. itemUuid: dto.existingItem.id.toString(),
  168. userUuid: dto.existingItem.props.userUuid.value,
  169. }),
  170. )
  171. }
  172. const notificationsResult = await this.addNotificationsAndPublishEvents(userUuid, sharedVaultOperation, dto)
  173. if (notificationsResult.isFailed()) {
  174. return Result.fail(notificationsResult.getError())
  175. }
  176. return Result.ok(dto.existingItem)
  177. }
  178. private determineCreatedAt(itemHash: ItemHash): { createdAtDate: Date; createdAtTimestamp: number } {
  179. let createdAtTimestamp: number
  180. let createdAtDate: Date
  181. if (itemHash.props.created_at_timestamp) {
  182. createdAtTimestamp = itemHash.props.created_at_timestamp
  183. createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
  184. } else if (itemHash.props.created_at) {
  185. createdAtTimestamp = this.timer.convertStringDateToMicroseconds(itemHash.props.created_at)
  186. createdAtDate = this.timer.convertStringDateToDate(itemHash.props.created_at)
  187. } else if (itemHash.props.updated_at_timestamp) {
  188. createdAtTimestamp = itemHash.props.updated_at_timestamp
  189. createdAtDate = this.timer.convertMicrosecondsToDate(itemHash.props.updated_at_timestamp)
  190. } else if (itemHash.props.updated_at) {
  191. createdAtTimestamp = this.timer.convertStringDateToMicroseconds(itemHash.props.updated_at)
  192. createdAtDate = this.timer.convertStringDateToDate(itemHash.props.updated_at)
  193. } else {
  194. createdAtTimestamp = 0
  195. createdAtDate = new Date(0)
  196. }
  197. return { createdAtDate, createdAtTimestamp }
  198. }
  199. private async addNotificationsAndPublishEvents(
  200. userUuid: Uuid,
  201. sharedVaultOperation: SharedVaultOperationOnItem | null,
  202. dto: UpdateExistingItemDTO,
  203. ): Promise<Result<void>> {
  204. if (
  205. sharedVaultOperation &&
  206. sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault
  207. ) {
  208. const notificationPayloadOrError = NotificationPayload.create({
  209. primaryIdentifier: sharedVaultOperation.props.sharedVaultUuid,
  210. primaryIndentifierType: NotificationPayloadIdentifierType.create(
  211. NotificationPayloadIdentifierType.TYPES.SharedVaultUuid,
  212. ).getValue(),
  213. type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
  214. secondaryIdentifier: dto.existingItem.uuid,
  215. secondaryIdentifierType: NotificationPayloadIdentifierType.create(
  216. NotificationPayloadIdentifierType.TYPES.ItemUuid,
  217. ).getValue(),
  218. version: '1.0',
  219. })
  220. if (notificationPayloadOrError.isFailed()) {
  221. return Result.fail(notificationPayloadOrError.getError())
  222. }
  223. const payload = notificationPayloadOrError.getValue()
  224. const result = await this.addNotificationForUsers.execute({
  225. payload,
  226. type: NotificationType.TYPES.SharedVaultItemRemoved,
  227. sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid.value,
  228. version: '1.0',
  229. })
  230. if (result.isFailed()) {
  231. return Result.fail(result.getError())
  232. }
  233. await this.domainEventPublisher.publish(
  234. this.domainEventFactory.createItemRemovedFromSharedVaultEvent({
  235. sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid.value,
  236. itemUuid: dto.existingItem.uuid.value,
  237. userUuid: userUuid.value,
  238. }),
  239. )
  240. }
  241. if (sharedVaultOperation && sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.AddToSharedVault) {
  242. const result = await this.removeNotificationsForUser.execute({
  243. type: NotificationType.TYPES.SharedVaultItemRemoved,
  244. userUuid: userUuid.value,
  245. })
  246. if (result.isFailed()) {
  247. return Result.fail(result.getError())
  248. }
  249. }
  250. return Result.ok()
  251. }
  252. }