Compare commits

..

8 Commits

Author SHA1 Message Date
standardci
352e02028d chore(release): publish new version
- @standardnotes/analytics@2.25.2
 - @standardnotes/api-gateway@1.67.1
 - @standardnotes/auth-server@1.126.1
 - @standardnotes/domain-core@1.23.2
 - @standardnotes/event-store@1.11.9
 - @standardnotes/files-server@1.19.11
 - @standardnotes/home-server@1.13.4
 - @standardnotes/revisions-server@1.25.2
 - @standardnotes/scheduler-server@1.20.11
 - @standardnotes/settings@1.21.16
 - @standardnotes/syncing-server@1.68.2
 - @standardnotes/websockets-server@1.10.4
2023-07-21 10:53:53 +00:00
Karol Sójko
1bbb639c83 fix: user notifications structure (#667) 2023-07-21 12:39:19 +02:00
standardci
c14265f103 chore(release): publish new version
- @standardnotes/home-server@1.13.3
 - @standardnotes/syncing-server@1.68.1
2023-07-21 07:37:04 +00:00
Karol Sójko
c030a6b3d8 fix(syncing-server): fetching items associated with shared vaults (#666) 2023-07-20 14:29:31 +02:00
standardci
af997ea658 chore(release): publish new version
- @standardnotes/api-gateway@1.67.0
 - @standardnotes/auth-server@1.126.0
 - @standardnotes/home-server@1.13.2
 - @standardnotes/syncing-server@1.68.0
2023-07-20 10:07:30 +00:00
Karol Sójko
efa4d7fc60 feat(syncing-server): add shared vaults, invites, messages and notifications to sync response (#665)
* feat(syncing-server): add shared vaults, invites, messages and notifications to sync response

* fix(syncing-server): migration timestamps

* fix: issue with migrations for notifications
2023-07-20 11:52:45 +02:00
standardci
f714aaa0e9 chore(release): publish new version
- @standardnotes/api-gateway@1.66.1
 - @standardnotes/home-server@1.13.1
 - @standardnotes/syncing-server@1.67.1
2023-07-19 07:42:57 +00:00
Karol Sójko
aee6e60583 fix: add missing imports and exports for controllers (#664) 2023-07-19 09:28:09 +02:00
91 changed files with 1047 additions and 213 deletions

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.1...@standardnotes/analytics@2.25.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.0...@standardnotes/analytics@2.25.1) (2023-07-19)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.1",
"version": "2.25.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.67.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.0...@standardnotes/api-gateway@1.67.1) (2023-07-21)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.67.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.66.1...@standardnotes/api-gateway@1.67.0) (2023-07-20)
### Features
* **syncing-server:** add shared vaults, invites, messages and notifications to sync response ([#665](https://github.com/standardnotes/api-gateway/issues/665)) ([efa4d7f](https://github.com/standardnotes/api-gateway/commit/efa4d7fc6007ef668e3de3b04853ac11b2d13c30))
## [1.66.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.66.0...@standardnotes/api-gateway@1.66.1) (2023-07-19)
### Bug Fixes
* add missing imports and exports for controllers ([#664](https://github.com/standardnotes/api-gateway/issues/664)) ([aee6e60](https://github.com/standardnotes/api-gateway/commit/aee6e6058359e2b5231cc13387656f837699300f))
# [1.66.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.7...@standardnotes/api-gateway@1.66.0) (2023-07-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.66.0",
"version": "1.67.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -13,6 +13,8 @@ export * from './v1/OfflineController'
export * from './v1/PaymentsController'
export * from './v1/RevisionsController'
export * from './v1/SessionsController'
export * from './v1/SharedVaultInvitesController'
export * from './v1/SharedVaultUsersController'
export * from './v1/SharedVaultsController'
export * from './v1/SubscriptionInvitesController'
export * from './v1/TokensController'

View File

@@ -21,7 +21,7 @@ export class SharedVaultUsersController extends BaseHttpController {
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier(
'GET',
'/shared-vaults/:sharedVaultUuid/users',
'shared-vaults/:sharedVaultUuid/users',
request.params.sharedVaultUuid,
),
request.body,
@@ -35,7 +35,7 @@ export class SharedVaultUsersController extends BaseHttpController {
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier(
'DELETE',
'/shared-vaults/:sharedVaultUuid/users/:userUuid',
'shared-vaults/:sharedVaultUuid/users/:userUuid',
request.params.sharedVaultUuid,
request.params.userUuid,
),

View File

@@ -100,7 +100,7 @@ export class EndpointResolver implements EndpointResolverInterface {
const identifier = this.endpointToIdentifierMap.get(`[${method}]:${endpoint}`)
if (!identifier) {
throw new Error(`Endpoint ${endpoint} not found`)
throw new Error(`Endpoint [${method}]:${endpoint} not found`)
}
return identifier

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.126.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.0...@standardnotes/auth-server@1.126.1) (2023-07-21)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.126.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.125.1...@standardnotes/auth-server@1.126.0) (2023-07-20)
### Features
* **syncing-server:** add shared vaults, invites, messages and notifications to sync response ([#665](https://github.com/standardnotes/server/issues/665)) ([efa4d7f](https://github.com/standardnotes/server/commit/efa4d7fc6007ef668e3de3b04853ac11b2d13c30))
## [1.125.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.125.0...@standardnotes/auth-server@1.125.1) (2023-07-19)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,16 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540448427 implements MigrationInterface {
name = 'AddNotifications1688540448427'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
}

View File

@@ -1,16 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveNotifications1688540448428 implements MigrationInterface {
name = 'RemoveNotifications1688540448428'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
}

View File

@@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540623272 implements MigrationInterface {
name = 'AddNotifications1688540623272'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
await queryRunner.query('DROP TABLE "notifications"')
}
}

View File

@@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveNotifications1688540623273 implements MigrationInterface {
name = 'RemoveNotifications1688540623273'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
await queryRunner.query('DROP TABLE "notifications"')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.125.1",
"version": "1.126.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.23.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.1...@standardnotes/domain-core@1.23.2) (2023-07-21)
### Bug Fixes
* user notifications structure ([#667](https://github.com/standardnotes/server/issues/667)) ([1bbb639](https://github.com/standardnotes/server/commit/1bbb639c83922ec09e3778f85419d76669d36ae3))
## [1.23.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.0...@standardnotes/domain-core@1.23.1) (2023-07-19)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.23.1",
"version": "1.23.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -0,0 +1,38 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { NotificationPayloadProps } from './NotificationPayloadProps'
import { NotificationType } from './NotificationType'
export class NotificationPayload extends ValueObject<NotificationPayloadProps> {
private constructor(props: NotificationPayloadProps) {
super(props)
}
override toString(): string {
return JSON.stringify(this.props)
}
static createFromString(jsonPayload: string): Result<NotificationPayload> {
try {
const props = JSON.parse(jsonPayload)
return NotificationPayload.create(props)
} catch (error) {
return Result.fail<NotificationPayload>((error as Error).message)
}
}
static create(props: NotificationPayloadProps): Result<NotificationPayload> {
if (
props.itemUuid === undefined &&
props.type.equals(NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue())
) {
return Result.fail<NotificationPayload>(
`Item uuid is required for ${NotificationType.TYPES.SharedVaultItemRemoved} notification type`,
)
}
return Result.ok<NotificationPayload>(new NotificationPayload(props))
}
}

View File

@@ -0,0 +1,9 @@
import { Uuid } from '../Common/Uuid'
import { NotificationType } from './NotificationType'
export interface NotificationPayloadProps {
type: NotificationType
sharedVaultUuid: Uuid
version: string
itemUuid?: Uuid
}

View File

@@ -1,5 +1,5 @@
import { ValueObject, Result } from '@standardnotes/domain-core'
import { Result } from '../Core/Result'
import { ValueObject } from '../Core/ValueObject'
import { NotificationTypeProps } from './NotificationTypeProps'
export class NotificationType extends ValueObject<NotificationTypeProps> {
@@ -17,9 +17,9 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
}
static create(notificationType: string): Result<NotificationType> {
const isValidPermission = Object.values(this.TYPES).includes(notificationType)
if (!isValidPermission) {
return Result.fail<NotificationType>(`Invalid shared vault user permission ${notificationType}`)
const isValidType = Object.values(this.TYPES).includes(notificationType)
if (!isValidType) {
return Result.fail<NotificationType>(`Invalid notification type: ${notificationType}`)
} else {
return Result.ok<NotificationType>(new NotificationType({ value: notificationType }))
}

View File

@@ -45,6 +45,11 @@ export * from './Env/AbstractEnv'
export * from './Mapping/MapperInterface'
export * from './Notification/NotificationPayload'
export * from './Notification/NotificationPayloadProps'
export * from './Notification/NotificationType'
export * from './Notification/NotificationTypeProps'
export * from './Service/ServiceConfiguration'
export * from './Service/ServiceContainer'
export * from './Service/ServiceContainerInterface'

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.9](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.8...@standardnotes/event-store@1.11.9) (2023-07-21)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.8](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.7...@standardnotes/event-store@1.11.8) (2023-07-19)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.8",
"version": "1.11.9",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.11](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.10...@standardnotes/files-server@1.19.11) (2023-07-21)
**Note:** Version bump only for package @standardnotes/files-server
## [1.19.10](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.9...@standardnotes/files-server@1.19.10) (2023-07-19)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.19.10",
"version": "1.19.11",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.3...@standardnotes/home-server@1.13.4) (2023-07-21)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.3](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.2...@standardnotes/home-server@1.13.3) (2023-07-21)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.1...@standardnotes/home-server@1.13.2) (2023-07-20)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.0...@standardnotes/home-server@1.13.1) (2023-07-19)
**Note:** Version bump only for package @standardnotes/home-server
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.6...@standardnotes/home-server@1.13.0) (2023-07-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.0",
"version": "1.13.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.1...@standardnotes/revisions-server@1.25.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.25.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.0...@standardnotes/revisions-server@1.25.1) (2023-07-19)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.25.1",
"version": "1.25.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.11](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.10...@standardnotes/scheduler-server@1.20.11) (2023-07-21)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.10](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.9...@standardnotes/scheduler-server@1.20.10) (2023-07-19)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.10",
"version": "1.20.11",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.16](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.15...@standardnotes/settings@1.21.16) (2023-07-21)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.15](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.14...@standardnotes/settings@1.21.15) (2023-07-19)
**Note:** Version bump only for package @standardnotes/settings

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.21.15",
"version": "1.21.16",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.68.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.1...@standardnotes/syncing-server@1.68.2) (2023-07-21)
### Bug Fixes
* user notifications structure ([#667](https://github.com/standardnotes/syncing-server-js/issues/667)) ([1bbb639](https://github.com/standardnotes/syncing-server-js/commit/1bbb639c83922ec09e3778f85419d76669d36ae3))
## [1.68.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.0...@standardnotes/syncing-server@1.68.1) (2023-07-21)
### Bug Fixes
* **syncing-server:** fetching items associated with shared vaults ([#666](https://github.com/standardnotes/syncing-server-js/issues/666)) ([c030a6b](https://github.com/standardnotes/syncing-server-js/commit/c030a6b3d838b1f09593905d28ace65097a3a940))
# [1.68.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.67.1...@standardnotes/syncing-server@1.68.0) (2023-07-20)
### Features
* **syncing-server:** add shared vaults, invites, messages and notifications to sync response ([#665](https://github.com/standardnotes/syncing-server-js/issues/665)) ([efa4d7f](https://github.com/standardnotes/syncing-server-js/commit/efa4d7fc6007ef668e3de3b04853ac11b2d13c30))
## [1.67.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.67.0...@standardnotes/syncing-server@1.67.1) (2023-07-19)
### Bug Fixes
* add missing imports and exports for controllers ([#664](https://github.com/standardnotes/syncing-server-js/issues/664)) ([aee6e60](https://github.com/standardnotes/syncing-server-js/commit/aee6e6058359e2b5231cc13387656f837699300f))
# [1.67.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.66.0...@standardnotes/syncing-server@1.67.0) (2023-07-19)
### Bug Fixes

View File

@@ -2,6 +2,10 @@ import 'reflect-metadata'
import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressItemsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressMessagesController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultInvitesController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultUsersController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultsController'
import helmet from 'helmet'
import * as cors from 'cors'

View File

@@ -1,16 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540448427 implements MigrationInterface {
name = 'AddNotifications1688540448427'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
}

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1689671563304 implements MigrationInterface {
name = 'AddNotifications1689671563304'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
}

View File

@@ -1,11 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1688540623272 implements MigrationInterface {
name = 'AddNotifications1688540623272'
export class AddNotifications1689672099828 implements MigrationInterface {
name = 'AddNotifications1689672099828'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
'CREATE TABLE IF NOT EXISTS "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.67.0",
"version": "1.68.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -147,6 +147,9 @@ import { DeleteAllMessagesSentToUser } from '../Domain/UseCase/Messaging/DeleteA
import { DeleteMessage } from '../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage'
import { MessageHttpRepresentation } from '../Mapping/Http/MessageHttpRepresentation'
import { MessageHttpMapper } from '../Mapping/Http/MessageHttpMapper'
import { GetUserNotifications } from '../Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications'
import { NotificationHttpMapper } from '../Mapping/Http/NotificationHttpMapper'
import { NotificationHttpRepresentation } from '../Mapping/Http/NotificationHttpRepresentation'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -340,6 +343,9 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<Message, MessageHttpRepresentation>>(TYPES.Sync_MessageHttpMapper)
.toConstantValue(new MessageHttpMapper())
container
.bind<MapperInterface<Notification, NotificationHttpRepresentation>>(TYPES.Sync_NotificationHttpMapper)
.toConstantValue(new NotificationHttpMapper())
// ORM
container
@@ -511,6 +517,7 @@ export class ContainerConfigLoader {
.toConstantValue(
new GetItems(
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
container.get(TYPES.Sync_ItemTransferCalculator),
container.get(TYPES.Sync_Timer),
@@ -550,6 +557,24 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_Logger),
),
)
container
.bind<GetUserNotifications>(TYPES.Sync_GetUserNotifications)
.toConstantValue(new GetUserNotifications(container.get(TYPES.Sync_NotificationRepository)))
container
.bind<GetSharedVaults>(TYPES.Sync_GetSharedVaults)
.toConstantValue(
new GetSharedVaults(
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_SharedVaultRepository),
),
)
container
.bind<GetSharedVaultInvitesSentToUser>(TYPES.Sync_GetSharedVaultInvitesSentToUser)
.toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetMessagesSentToUser>(TYPES.Sync_GetMessagesSentToUser)
.toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository)))
container
.bind<SyncItems>(TYPES.Sync_SyncItems)
.toConstantValue(
@@ -557,6 +582,10 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_GetItems),
container.get(TYPES.Sync_SaveItems),
container.get(TYPES.Sync_GetSharedVaults),
container.get(TYPES.Sync_GetSharedVaultInvitesSentToUser),
container.get(TYPES.Sync_GetMessagesSentToUser),
container.get(TYPES.Sync_GetUserNotifications),
),
)
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
@@ -621,9 +650,6 @@ export class ContainerConfigLoader {
container
.bind<GetSharedVaultInvitesSentByUser>(TYPES.Sync_GetSharedVaultInvitesSentByUser)
.toConstantValue(new GetSharedVaultInvitesSentByUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetSharedVaultInvitesSentToUser>(TYPES.Sync_GetSharedVaultInvitesSentToUser)
.toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetSharedVaultUsers>(TYPES.Sync_GetSharedVaultUsers)
.toConstantValue(
@@ -646,14 +672,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_AddNotificationForUser),
),
)
container
.bind<GetSharedVaults>(TYPES.Sync_GetSharedVaults)
.toConstantValue(
new GetSharedVaults(
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_SharedVaultRepository),
),
)
container
.bind<CreateSharedVault>(TYPES.Sync_CreateSharedVault)
.toConstantValue(
@@ -683,9 +701,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_VALET_TOKEN_TTL),
),
)
container
.bind<GetMessagesSentToUser>(TYPES.Sync_GetMessagesSentToUser)
.toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository)))
container
.bind<GetMessagesSentByUser>(TYPES.Sync_GetMessagesSentByUser)
.toConstantValue(new GetMessagesSentByUser(container.get(TYPES.Sync_MessageRepository)))
@@ -717,6 +732,10 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_ItemHttpMapper),
container.get(TYPES.Sync_ItemConflictHttpMapper),
container.get(TYPES.Sync_SavedItemHttpMapper),
container.get(TYPES.Sync_SharedVaultHttpMapper),
container.get(TYPES.Sync_SharedVaultInviteHttpMapper),
container.get(TYPES.Sync_MessageHttpMapper),
container.get(TYPES.Sync_NotificationHttpMapper),
),
)
container

View File

@@ -75,6 +75,7 @@ const TYPES = {
Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'),
Sync_GetItems: Symbol.for('Sync_GetItems'),
Sync_SaveItems: Symbol.for('Sync_SaveItems'),
Sync_GetUserNotifications: Symbol.for('Sync_GetUserNotifications'),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
@@ -113,6 +114,7 @@ const TYPES = {
Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'),
Sync_MessagePersistenceMapper: Symbol.for('Sync_MessagePersistenceMapper'),
Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
Sync_NotificationHttpMapper: Symbol.for('Sync_NotificationHttpMapper'),
Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'),

View File

@@ -0,0 +1,47 @@
import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Item } from './Item'
describe('Item', () => {
it('should create an aggregate', () => {
const entityOrError = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
expect(entityOrError.getValue().uuid.value).toEqual(entityOrError.getValue().id.toString())
})
it('should throw an error if id cannot be cast to uuid', () => {
const entityOrError = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId(1),
)
expect(entityOrError.isFailed()).toBeFalsy()
expect(() => entityOrError.getValue().uuid).toThrow()
})
})

View File

@@ -1,8 +1,17 @@
import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { Aggregate, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { ItemProps } from './ItemProps'
export class Item extends Aggregate<ItemProps> {
get uuid(): Uuid {
const uuidOrError = Uuid.create(this._id.toString())
if (uuidOrError.isFailed()) {
throw new Error(uuidOrError.getError())
}
return uuidOrError.getValue()
}
private constructor(props: ItemProps, id?: UniqueEntityId) {
super(props, id)
}

View File

@@ -11,4 +11,6 @@ export type ItemQuery = {
limit?: number
createdBetween?: Date[]
selectString?: string
includeSharedVaultUuids?: string[]
exclusiveSharedVaultUuids?: string[]
}

View File

@@ -1,11 +1,19 @@
import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation'
import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation'
import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation'
import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation'
export type SyncResponse20200115 = {
retrieved_items: Array<ItemHttpRepresentation>
saved_items: Array<SavedItemHttpRepresentation>
conflicts: Array<ItemConflictHttpRepresentation>
retrieved_items: ItemHttpRepresentation[]
saved_items: SavedItemHttpRepresentation[]
conflicts: ItemConflictHttpRepresentation[]
sync_token: string
cursor_token?: string
messages: MessageHttpRepresentation[]
shared_vaults: SharedVaultHttpRepresentation[]
shared_vault_invites: SharedVaultInviteHttpRepresentation[]
notifications: NotificationHttpRepresentation[]
}

View File

@@ -88,6 +88,10 @@ describe('SyncResponseFactory20161215', () => {
],
syncToken: 'sync-test',
cursorToken: 'cursor-test',
sharedVaults: [],
sharedVaultInvites: [],
messages: [],
notifications: [],
}),
).toEqual({
retrieved_items: [item1Projection],
@@ -133,6 +137,10 @@ describe('SyncResponseFactory20161215', () => {
],
syncToken: 'sync-test',
cursorToken: 'cursor-test',
sharedVaults: [],
sharedVaultInvites: [],
messages: [],
notifications: [],
}),
).toEqual({
retrieved_items: [],

View File

@@ -8,6 +8,14 @@ import { SyncResponseFactory20200115 } from './SyncResponseFactory20200115'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation'
import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation'
import { Notification } from '../../Notifications/Notification'
import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation'
import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation'
import { Message } from '../../Message/Message'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
describe('SyncResponseFactory20200115', () => {
let itemMapper: MapperInterface<Item, ItemHttpRepresentation>
@@ -19,8 +27,25 @@ describe('SyncResponseFactory20200115', () => {
let item1: Item
let item2: Item
let itemConflict: ItemConflict
let sharedVault: SharedVault
let sharedVaultInvite: SharedVaultInvite
let message: Message
let notification: Notification
let sharedVaultMapper: MapperInterface<SharedVault, SharedVaultHttpRepresentation>
let sharedVaultInvitesMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>
let messageMapper: MapperInterface<Message, MessageHttpRepresentation>
let notificationMapper: MapperInterface<Notification, NotificationHttpRepresentation>
const createFactory = () => new SyncResponseFactory20200115(itemMapper, itemConflictMapper, savedItemMapper)
const createFactory = () =>
new SyncResponseFactory20200115(
itemMapper,
itemConflictMapper,
savedItemMapper,
sharedVaultMapper,
sharedVaultInvitesMapper,
messageMapper,
notificationMapper,
)
beforeEach(() => {
itemProjection = {
@@ -45,6 +70,27 @@ describe('SyncResponseFactory20200115', () => {
item2 = {} as jest.Mocked<Item>
itemConflict = {} as jest.Mocked<ItemConflict>
sharedVaultMapper = {} as jest.Mocked<MapperInterface<SharedVault, SharedVaultHttpRepresentation>>
sharedVaultMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<SharedVaultHttpRepresentation>)
sharedVaultInvitesMapper = {} as jest.Mocked<
MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>
>
sharedVaultInvitesMapper.toProjection = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultInviteHttpRepresentation>)
messageMapper = {} as jest.Mocked<MapperInterface<Message, MessageHttpRepresentation>>
messageMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<MessageHttpRepresentation>)
notificationMapper = {} as jest.Mocked<MapperInterface<Notification, NotificationHttpRepresentation>>
notificationMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<NotificationHttpRepresentation>)
sharedVault = {} as jest.Mocked<SharedVault>
sharedVaultInvite = {} as jest.Mocked<SharedVaultInvite>
message = {} as jest.Mocked<Message>
notification = {} as jest.Mocked<Notification>
})
it('should turn sync items response into a sync response for API Version 20200115', async () => {
@@ -55,6 +101,10 @@ describe('SyncResponseFactory20200115', () => {
conflicts: [itemConflict],
syncToken: 'sync-test',
cursorToken: 'cursor-test',
sharedVaults: [sharedVault],
sharedVaultInvites: [sharedVaultInvite],
messages: [message],
notifications: [notification],
}),
).toEqual({
retrieved_items: [itemProjection],
@@ -62,6 +112,10 @@ describe('SyncResponseFactory20200115', () => {
conflicts: [itemConflictProjection],
sync_token: 'sync-test',
cursor_token: 'cursor-test',
shared_vaults: [{} as jest.Mocked<SharedVaultHttpRepresentation>],
shared_vault_invites: [{} as jest.Mocked<SharedVaultInviteHttpRepresentation>],
messages: [{} as jest.Mocked<MessageHttpRepresentation>],
notifications: [{} as jest.Mocked<NotificationHttpRepresentation>],
})
})
})

View File

@@ -8,12 +8,24 @@ import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResp
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation'
import { Message } from '../../Message/Message'
import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation'
import { Notification } from '../../Notifications/Notification'
import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation'
export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface {
constructor(
private httpMapper: MapperInterface<Item, ItemHttpRepresentation>,
private itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>,
private savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>,
private sharedVaultMapper: MapperInterface<SharedVault, SharedVaultHttpRepresentation>,
private sharedVaultInvitesMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>,
private messageMapper: MapperInterface<Message, MessageHttpRepresentation>,
private notificationMapper: MapperInterface<Notification, NotificationHttpRepresentation>,
) {}
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
@@ -32,12 +44,36 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface
conflicts.push(this.itemConflictMapper.toProjection(itemConflict))
}
const sharedVaults = []
for (const sharedVault of syncItemsResponse.sharedVaults) {
sharedVaults.push(this.sharedVaultMapper.toProjection(sharedVault))
}
const sharedVaultInvites = []
for (const sharedVaultInvite of syncItemsResponse.sharedVaultInvites) {
sharedVaultInvites.push(this.sharedVaultInvitesMapper.toProjection(sharedVaultInvite))
}
const messages = []
for (const contact of syncItemsResponse.messages) {
messages.push(this.messageMapper.toProjection(contact))
}
const notifications = []
for (const notification of syncItemsResponse.notifications) {
notifications.push(this.notificationMapper.toProjection(notification))
}
return {
retrieved_items: retrievedItems,
saved_items: savedItems,
conflicts,
sync_token: syncItemsResponse.syncToken,
cursor_token: syncItemsResponse.cursorToken,
messages,
shared_vaults: sharedVaults,
shared_vault_invites: sharedVaultInvites,
notifications,
}
}
}

View File

@@ -5,6 +5,7 @@ import { Message } from './Message'
export interface MessageRepositoryInterface {
findByUuid: (uuid: Uuid) => Promise<Message | null>
findByRecipientUuid: (uuid: Uuid) => Promise<Message[]>
findByRecipientUuidUpdatedAfter: (uuid: Uuid, updatedAtTimestamp: number) => Promise<Message[]>
findBySenderUuid: (uuid: Uuid) => Promise<Message[]>
findByRecipientUuidAndReplaceabilityIdentifier: (dto: {
recipientUuid: Uuid

View File

@@ -1,14 +1,19 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { NotificationPayload, NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
import { Notification } from './Notification'
import { NotificationType } from './NotificationType'
describe('Notification', () => {
it('should create an entity', () => {
const payload = NotificationPayload.create({
sharedVaultUuid: Uuid.create('0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e').getValue(),
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
version: '1.0',
}).getValue()
const entityOrError = Notification.create({
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
payload: 'payload',
payload,
type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
})

View File

@@ -1,10 +1,8 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { NotificationType } from './NotificationType'
import { NotificationPayload, NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
export interface NotificationProps {
userUuid: Uuid
type: NotificationType
payload: string
payload: NotificationPayload
timestamps: Timestamps
}

View File

@@ -1,5 +1,9 @@
import { Uuid } from '@standardnotes/domain-core'
import { Notification } from './Notification'
export interface NotificationRepositoryInterface {
save(notification: Notification): Promise<void>
findByUserUuidUpdatedAfter(userUuid: Uuid, lastSyncTime: number): Promise<Notification[]>
findByUserUuid(userUuid: Uuid): Promise<Notification[]>
}

View File

@@ -1,4 +1,4 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { SharedVault } from './SharedVault'
@@ -13,5 +13,21 @@ describe('SharedVault', () => {
expect(entityOrError.isFailed()).toBeFalsy()
expect(entityOrError.getValue().id).not.toBeNull()
expect(entityOrError.getValue().uuid.value).toEqual(entityOrError.getValue().id.toString())
})
it('should throw an error if id cannot be cast to uuid', () => {
const entityOrError = SharedVault.create(
{
fileUploadBytesLimit: 1_000_000,
fileUploadBytesUsed: 0,
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
},
new UniqueEntityId(1),
)
expect(entityOrError.isFailed()).toBeFalsy()
expect(() => entityOrError.getValue().uuid).toThrow()
})
})

View File

@@ -1,4 +1,4 @@
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
import { Entity, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { SharedVaultProps } from './SharedVaultProps'
@@ -7,6 +7,15 @@ export class SharedVault extends Entity<SharedVaultProps> {
super(props, id)
}
get uuid(): Uuid {
const uuidOrError = Uuid.create(this._id.toString())
if (uuidOrError.isFailed()) {
throw new Error(uuidOrError.getError())
}
return uuidOrError.getValue()
}
static create(props: SharedVaultProps, id?: UniqueEntityId): Result<SharedVault> {
return Result.ok<SharedVault>(new SharedVault(props, id))
}

View File

@@ -8,6 +8,7 @@ export interface SharedVaultInviteRepositoryInterface {
remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
findByUserUuid(userUuid: Uuid): Promise<SharedVaultInvite[]>
findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise<SharedVaultInvite[]>
findBySenderUuid(senderUuid: Uuid): Promise<SharedVaultInvite[]>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite[]>

View File

@@ -1,14 +1,14 @@
import { TimerInterface } from '@standardnotes/time'
import { Result } from '@standardnotes/domain-core'
import { NotificationPayload, NotificationType, Result, Uuid } from '@standardnotes/domain-core'
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
import { Notification } from '../../../Notifications/Notification'
import { AddNotificationForUser } from './AddNotificationForUser'
import { NotificationType } from '../../../Notifications/NotificationType'
describe('AddNotificationForUser', () => {
let notificationRepository: NotificationRepositoryInterface
let timer: TimerInterface
let payload: NotificationPayload
const createUseCase = () => new AddNotificationForUser(notificationRepository, timer)
@@ -18,6 +18,12 @@ describe('AddNotificationForUser', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
payload = NotificationPayload.create({
sharedVaultUuid: Uuid.create('0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e').getValue(),
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
version: '1.0',
}).getValue()
})
it('should save notification', async () => {
@@ -26,7 +32,7 @@ describe('AddNotificationForUser', () => {
const result = await useCase.execute({
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
type: NotificationType.TYPES.RemovedFromSharedVault,
payload: 'payload',
payload,
version: '1.0',
})
@@ -39,7 +45,7 @@ describe('AddNotificationForUser', () => {
const result = await useCase.execute({
userUuid: 'invalid',
type: NotificationType.TYPES.RemovedFromSharedVault,
payload: 'payload',
payload,
version: '1.0',
})
@@ -52,20 +58,7 @@ describe('AddNotificationForUser', () => {
const result = await useCase.execute({
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
type: 'invalid',
payload: 'payload',
version: '1.0',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if notification payload is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
type: NotificationType.TYPES.RemovedFromSharedVault,
payload: '',
payload,
version: '1.0',
})
@@ -83,7 +76,7 @@ describe('AddNotificationForUser', () => {
const result = await useCase.execute({
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
type: NotificationType.TYPES.RemovedFromSharedVault,
payload: 'payload',
payload,
version: '1.0',
})

View File

@@ -1,10 +1,9 @@
import { Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
import { NotificationType, Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { AddNotificationForUserDTO } from './AddNotificationForUserDTO'
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
import { Notification } from '../../../Notifications/Notification'
import { NotificationType } from '../../../Notifications/NotificationType'
export class AddNotificationForUser implements UseCaseInterface<Notification> {
constructor(private notificationRepository: NotificationRepositoryInterface, private timer: TimerInterface) {}
@@ -22,11 +21,6 @@ export class AddNotificationForUser implements UseCaseInterface<Notification> {
}
const type = typeOrError.getValue()
const paylodNotEmptyValidationResult = Validator.isNotEmpty(dto.payload)
if (paylodNotEmptyValidationResult.isFailed()) {
return Result.fail(paylodNotEmptyValidationResult.getError())
}
const notificationOrError = Notification.create({
userUuid,
type,

View File

@@ -1,6 +1,8 @@
import { NotificationPayload } from '@standardnotes/domain-core'
export interface AddNotificationForUserDTO {
version: string
type: string
userUuid: string
payload: string
payload: NotificationPayload
}

View File

@@ -9,6 +9,7 @@ describe('GetMessagesSentToUser', () => {
beforeEach(() => {
messageRepository = {} as jest.Mocked<MessageRepositoryInterface>
messageRepository.findByRecipientUuid = jest.fn().mockReturnValue([])
messageRepository.findByRecipientUuidUpdatedAfter = jest.fn().mockReturnValue([])
})
it('should return messages sent to user', async () => {
@@ -20,6 +21,16 @@ describe('GetMessagesSentToUser', () => {
expect(result.getValue()).toEqual([])
})
it('should return messages sent to user updated after given time', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
recipientUuid: '00000000-0000-0000-0000-000000000000',
lastSyncTime: 123,
})
expect(result.getValue()).toEqual([])
})
it('should return error when recipient uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({

View File

@@ -14,8 +14,10 @@ export class GetMessagesSentToUser implements UseCaseInterface<Message[]> {
}
const recipientUuid = recipientUuidOrError.getValue()
const messages = await this.messageRepository.findByRecipientUuid(recipientUuid)
if (dto.lastSyncTime) {
return Result.ok(await this.messageRepository.findByRecipientUuidUpdatedAfter(recipientUuid, dto.lastSyncTime))
}
return Result.ok(messages)
return Result.ok(await this.messageRepository.findByRecipientUuid(recipientUuid))
}
}

View File

@@ -1,3 +1,4 @@
export interface GetMessagesSentToUserDTO {
recipientUuid: string
lastSyncTime?: number
}

View File

@@ -0,0 +1,43 @@
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
import { GetUserNotifications } from './GetUserNotifications'
describe('GetUserNotifications', () => {
let notificationRepository: NotificationRepositoryInterface
const createUseCase = () => new GetUserNotifications(notificationRepository)
beforeEach(() => {
notificationRepository = {} as jest.Mocked<NotificationRepositoryInterface>
notificationRepository.findByUserUuid = jest.fn().mockReturnValue([])
notificationRepository.findByUserUuidUpdatedAfter = jest.fn().mockReturnValue([])
})
it('should return notification for user', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.getValue()).toEqual([])
})
it('should return notifications for user updated after given time', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
lastSyncTime: 123,
})
expect(result.getValue()).toEqual([])
})
it('should return error when user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
})

View File

@@ -0,0 +1,22 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Notification } from '../../../Notifications/Notification'
import { GetUserNotificationsDTO } from './GetUserNotificationsDTO'
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
export class GetUserNotifications implements UseCaseInterface<Notification[]> {
constructor(private notificationRepository: NotificationRepositoryInterface) {}
async execute(dto: GetUserNotificationsDTO): Promise<Result<Notification[]>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
if (dto.lastSyncTime) {
return Result.ok(await this.notificationRepository.findByUserUuidUpdatedAfter(userUuid, dto.lastSyncTime))
}
return Result.ok(await this.notificationRepository.findByUserUuid(userUuid))
}
}

View File

@@ -0,0 +1,4 @@
export interface GetUserNotificationsDTO {
userUuid: string
lastSyncTime?: number
}

View File

@@ -23,6 +23,7 @@ describe('GetSharedVaultInvitesSentToUser', () => {
sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
sharedVaultInviteRepository.findByUserUuid = jest.fn().mockResolvedValue([invite])
sharedVaultInviteRepository.findByUserUuidUpdatedAfter = jest.fn().mockResolvedValue([invite])
})
it('should return invites sent to user', async () => {
@@ -35,6 +36,17 @@ describe('GetSharedVaultInvitesSentToUser', () => {
expect(result.getValue()).toEqual([invite])
})
it('should return invites sent to user updated after given time', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
lastSyncTime: 123,
})
expect(result.getValue()).toEqual([invite])
})
it('should return empty array if no invites found', async () => {
const useCase = createUseCase()

View File

@@ -13,6 +13,10 @@ export class GetSharedVaultInvitesSentToUser implements UseCaseInterface<SharedV
}
const userUuid = userUuidOrError.getValue()
if (dto.lastSyncTime) {
return Result.ok(await this.sharedVaultInviteRepository.findByUserUuidUpdatedAfter(userUuid, dto.lastSyncTime))
}
return Result.ok(await this.sharedVaultInviteRepository.findByUserUuid(userUuid))
}
}

View File

@@ -1,3 +1,4 @@
export interface GetSharedVaultInvitesSentToUserDTO {
userUuid: string
lastSyncTime?: number
}

View File

@@ -1,4 +1,4 @@
import { Uuid, Timestamps, Result } from '@standardnotes/domain-core'
import { Uuid, Timestamps, Result, NotificationPayload } from '@standardnotes/domain-core'
import { SharedVault } from '../../../SharedVault/SharedVault'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
@@ -173,4 +173,19 @@ describe('RemoveUserFromSharedVault', () => {
expect(result.isFailed()).toBe(true)
})
it('should return error if notification payload could not be created', async () => {
const mock = jest.spyOn(NotificationPayload, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const useCase = createUseCase()
const result = await useCase.execute({
originatorUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
userUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
})
})

View File

@@ -1,9 +1,8 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { NotificationPayload, NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { RemoveUserFromSharedVaultDTO } from './RemoveUserFromSharedVaultDTO'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { NotificationType } from '../../../Notifications/NotificationType'
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
@@ -57,12 +56,20 @@ export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
await this.sharedVaultUsersRepository.remove(sharedVaultUser)
const notificationPayloadOrError = NotificationPayload.create({
sharedVaultUuid: sharedVault.uuid,
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
version: '1.0',
})
if (notificationPayloadOrError.isFailed()) {
return Result.fail(notificationPayloadOrError.getError())
}
const notificationPayload = notificationPayloadOrError.getValue()
const result = await this.addNotificationForUser.execute({
userUuid: sharedVaultUser.props.userUuid.value,
type: NotificationType.TYPES.RemovedFromSharedVault,
payload: JSON.stringify({
sharedVaultUuid: sharedVault.id.toString(),
}),
payload: notificationPayload,
version: '1.0',
})
if (result.isFailed()) {

View File

@@ -4,6 +4,7 @@ import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalcu
import { GetItems } from './GetItems'
import { Item } from '../../../Item/Item'
import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
describe('GetItems', () => {
let itemRepository: ItemRepositoryInterface
@@ -12,9 +13,17 @@ describe('GetItems', () => {
let timer: TimerInterface
const maxItemsSyncLimit = 100
let item: Item
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
const createUseCase = () =>
new GetItems(itemRepository, contentSizeTransferLimit, itemTransferCalculator, timer, maxItemsSyncLimit)
new GetItems(
itemRepository,
sharedVaultUserRepository,
contentSizeTransferLimit,
itemTransferCalculator,
timer,
maxItemsSyncLimit,
)
beforeEach(() => {
item = Item.create({
@@ -41,13 +50,16 @@ describe('GetItems', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123)
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([])
})
it('returns items', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
cursorToken: undefined,
contentType: undefined,
limit: 10,
@@ -57,6 +69,7 @@ describe('GetItems', () => {
expect(result.getValue()).toEqual({
items: [item],
cursorToken: undefined,
lastSyncTime: null,
})
})
@@ -66,7 +79,7 @@ describe('GetItems', () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
cursorToken: undefined,
contentType: undefined,
limit: undefined,
@@ -76,6 +89,7 @@ describe('GetItems', () => {
expect(result.getValue()).toEqual({
items: [item],
cursorToken: 'MjowLjAwMDEyMw==',
lastSyncTime: null,
})
})
@@ -83,7 +97,7 @@ describe('GetItems', () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
cursorToken: 'MjowLjAwMDEyMw==',
contentType: undefined,
limit: undefined,
@@ -93,6 +107,7 @@ describe('GetItems', () => {
expect(result.getValue()).toEqual({
items: [item],
cursorToken: undefined,
lastSyncTime: 123.00000000000001,
})
})
@@ -103,7 +118,7 @@ describe('GetItems', () => {
const syncToken = Buffer.from(syncTokenData, 'utf-8').toString('base64')
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
syncToken,
contentType: undefined,
limit: undefined,
@@ -113,6 +128,7 @@ describe('GetItems', () => {
expect(result.getValue()).toEqual({
items: [item],
cursorToken: undefined,
lastSyncTime: 123,
})
})
@@ -123,7 +139,7 @@ describe('GetItems', () => {
const syncToken = Buffer.from(syncTokenData, 'utf-8').toString('base64')
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
syncToken,
contentType: undefined,
limit: undefined,
@@ -137,7 +153,7 @@ describe('GetItems', () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
cursorToken: undefined,
contentType: undefined,
limit: 200,
@@ -147,6 +163,48 @@ describe('GetItems', () => {
expect(result.getValue()).toEqual({
items: [item],
cursorToken: undefined,
lastSyncTime: null,
})
})
it('should return error for invalid user uuid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
cursorToken: undefined,
contentType: undefined,
limit: undefined,
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('should filter shared vault uuids user wants to sync with the ones it has access to', async () => {
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([
{
props: {
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
},
},
])
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
cursorToken: undefined,
contentType: undefined,
limit: undefined,
sharedVaultUuids: ['00000000-0000-0000-0000-000000000000', '11111111-1111-1111-1111-111111111111'],
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
items: [item],
cursorToken: undefined,
lastSyncTime: null,
})
})
})

View File

@@ -1,4 +1,4 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Time, TimerInterface } from '@standardnotes/time'
import { Item } from '../../../Item/Item'
@@ -7,6 +7,7 @@ import { ItemQuery } from '../../../Item/ItemQuery'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
import { GetItemsDTO } from './GetItemsDTO'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
export class GetItems implements UseCaseInterface<GetItemsResult> {
private readonly DEFAULT_ITEMS_LIMIT = 150
@@ -14,6 +15,7 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
constructor(
private itemRepository: ItemRepositoryInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private contentSizeTransferLimit: number,
private itemTransferCalculator: ItemTransferCalculatorInterface,
private timer: TimerInterface,
@@ -27,12 +29,25 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
}
const lastSyncTime = lastSyncTimeOrError.getValue()
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid)
const userSharedVaultUuids = sharedVaultUsers.map((sharedVaultUser) => sharedVaultUser.props.sharedVaultUuid.value)
const exclusiveSharedVaultUuids = dto.sharedVaultUuids
? dto.sharedVaultUuids.filter((sharedVaultUuid) => userSharedVaultUuids.includes(sharedVaultUuid))
: undefined
const itemQuery: ItemQuery = {
userUuid: dto.userUuid,
userUuid: userUuid.value,
lastSyncTime: lastSyncTime ?? undefined,
syncTimeComparison,
contentType: dto.contentType,
@@ -40,6 +55,8 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
limit: upperBoundLimit,
includeSharedVaultUuids: !dto.sharedVaultUuids ? userSharedVaultUuids : undefined,
exclusiveSharedVaultUuids,
}
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
@@ -65,6 +82,7 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
return Result.ok({
items,
cursorToken,
lastSyncTime,
})
}

View File

@@ -4,4 +4,5 @@ export interface GetItemsDTO {
cursorToken?: string | null
limit?: number
contentType?: string
sharedVaultUuids?: string[]
}

View File

@@ -3,4 +3,5 @@ import { Item } from '../../../Item/Item'
export interface GetItemsResult {
items: Item[]
cursorToken?: string
lastSyncTime: number | null
}

View File

@@ -31,15 +31,6 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
for (const itemHash of dto.itemHashes) {
if (dto.readOnlyAccess) {
conflicts.push({
unsavedItem: itemHash,
type: ConflictType.ReadOnlyError,
})
continue
}
const itemUuidOrError = Uuid.create(itemHash.props.uuid)
if (itemUuidOrError.isFailed()) {
conflicts.push({
@@ -52,6 +43,17 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
const itemUuid = itemUuidOrError.getValue()
const existingItem = await this.itemRepository.findByUuid(itemUuid)
if (dto.readOnlyAccess) {
conflicts.push({
unsavedItem: itemHash,
serverItem: existingItem ?? undefined,
type: ConflictType.ReadOnlyError,
})
continue
}
const processingResult = await this.itemSaveValidator.validate({
userUuid: dto.userUuid,
apiVersion: dto.apiVersion,

View File

@@ -9,6 +9,10 @@ import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@s
import { GetItems } from '../GetItems/GetItems'
import { SaveItems } from '../SaveItems/SaveItems'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVaults'
import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser'
import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications'
import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
describe('SyncItems', () => {
let getItemsUseCase: GetItems
@@ -18,8 +22,21 @@ describe('SyncItems', () => {
let item2: Item
let item3: Item
let itemHash: ItemHash
let getSharedVaultsUseCase: GetSharedVaults
let getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser
let getMessagesSentToUser: GetMessagesSentToUser
let getUserNotifications: GetUserNotifications
const createUseCase = () => new SyncItems(itemRepository, getItemsUseCase, saveItemsUseCase)
const createUseCase = () =>
new SyncItems(
itemRepository,
getItemsUseCase,
saveItemsUseCase,
getSharedVaultsUseCase,
getSharedVaultInvitesSentToUserUseCase,
getMessagesSentToUser,
getUserNotifications,
)
beforeEach(() => {
item1 = Item.create(
@@ -104,6 +121,18 @@ describe('SyncItems', () => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([item3, item1])
getSharedVaultsUseCase = {} as jest.Mocked<GetSharedVaults>
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked<GetSharedVaultInvitesSentToUser>
getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
getMessagesSentToUser = {} as jest.Mocked<GetMessagesSentToUser>
getMessagesSentToUser.execute = jest.fn().mockReturnValue(Result.ok([]))
getUserNotifications = {} as jest.Mocked<GetUserNotifications>
getUserNotifications.execute = jest.fn().mockReturnValue(Result.ok([]))
})
it('should sync items', async () => {
@@ -126,6 +155,10 @@ describe('SyncItems', () => {
retrievedItems: [item1],
savedItems: [item2],
syncToken: 'qwerty',
sharedVaults: [],
sharedVaultInvites: [],
notifications: [],
messages: [],
})
expect(getItemsUseCase.execute).toHaveBeenCalledWith({
@@ -162,6 +195,10 @@ describe('SyncItems', () => {
retrievedItems: [item3, item1],
savedItems: [item2],
syncToken: 'qwerty',
sharedVaults: [],
sharedVaultInvites: [],
notifications: [],
messages: [],
})
})
@@ -219,6 +256,10 @@ describe('SyncItems', () => {
retrievedItems: [item1],
savedItems: [],
syncToken: 'qwerty',
sharedVaults: [],
sharedVaultInvites: [],
notifications: [],
messages: [],
})
})
@@ -261,4 +302,84 @@ describe('SyncItems', () => {
expect(result.isFailed()).toBeTruthy()
})
it('should return error if get shared vaults fails', async () => {
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.fail('error'))
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemHashes: [itemHash],
computeIntegrityHash: false,
syncToken: 'foo',
readOnlyAccess: false,
sessionUuid: '2-3-4',
cursorToken: 'bar',
limit: 10,
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if get shared vault invites fails', async () => {
getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.fail('error'))
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemHashes: [itemHash],
computeIntegrityHash: false,
syncToken: 'foo',
readOnlyAccess: false,
sessionUuid: '2-3-4',
cursorToken: 'bar',
limit: 10,
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if get messages fails', async () => {
getMessagesSentToUser.execute = jest.fn().mockReturnValue(Result.fail('error'))
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemHashes: [itemHash],
computeIntegrityHash: false,
syncToken: 'foo',
readOnlyAccess: false,
sessionUuid: '2-3-4',
cursorToken: 'bar',
limit: 10,
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if get user notifications fails', async () => {
getUserNotifications.execute = jest.fn().mockReturnValue(Result.fail('error'))
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemHashes: [itemHash],
computeIntegrityHash: false,
syncToken: 'foo',
readOnlyAccess: false,
sessionUuid: '2-3-4',
cursorToken: 'bar',
limit: 10,
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
})
expect(result.isFailed()).toBeTruthy()
})
})

View File

@@ -7,12 +7,20 @@ import { SyncItemsResponse } from './SyncItemsResponse'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { GetItems } from '../GetItems/GetItems'
import { SaveItems } from '../SaveItems/SaveItems'
import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVaults'
import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser'
import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications'
export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
constructor(
private itemRepository: ItemRepositoryInterface,
private getItemsUseCase: GetItems,
private saveItemsUseCase: SaveItems,
private getSharedVaultsUseCase: GetSharedVaults,
private getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser,
private getMessagesSentToUser: GetMessagesSentToUser,
private getUserNotifications: GetUserNotifications,
) {}
async execute(dto: SyncItemsDTO): Promise<Result<SyncItemsResponse>> {
@@ -22,6 +30,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
cursorToken: dto.cursorToken,
limit: dto.limit,
contentType: dto.contentType,
sharedVaultUuids: dto.sharedVaultUuids,
})
if (getItemsResultOrError.isFailed()) {
return Result.fail(getItemsResultOrError.getError())
@@ -45,12 +54,52 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
}
const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({
userUuid: dto.userUuid,
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
})
if (sharedVaultsOrError.isFailed()) {
return Result.fail(sharedVaultsOrError.getError())
}
const sharedVaults = sharedVaultsOrError.getValue()
const sharedVaultInvitesOrError = await this.getSharedVaultInvitesSentToUserUseCase.execute({
userUuid: dto.userUuid,
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
})
if (sharedVaultInvitesOrError.isFailed()) {
return Result.fail(sharedVaultInvitesOrError.getError())
}
const sharedVaultInvites = sharedVaultInvitesOrError.getValue()
const messagesOrError = await this.getMessagesSentToUser.execute({
recipientUuid: dto.userUuid,
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
})
if (messagesOrError.isFailed()) {
return Result.fail(messagesOrError.getError())
}
const messages = messagesOrError.getValue()
const notificationsOrError = await this.getUserNotifications.execute({
userUuid: dto.userUuid,
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
})
if (notificationsOrError.isFailed()) {
return Result.fail(notificationsOrError.getError())
}
const notifications = notificationsOrError.getValue()
const syncResponse: SyncItemsResponse = {
retrievedItems,
syncToken: saveItemsResult.syncToken,
savedItems: saveItemsResult.savedItems,
conflicts: saveItemsResult.conflicts,
cursorToken: getItemsResult.cursorToken,
sharedVaultInvites,
sharedVaults,
messages,
notifications,
}
return Result.ok(syncResponse)

View File

@@ -5,7 +5,7 @@ export type SyncItemsDTO = {
itemHashes: Array<ItemHash>
computeIntegrityHash: boolean
limit: number
sharedVaultUuids?: string[] | null
sharedVaultUuids?: string[]
syncToken?: string | null
cursorToken?: string | null
contentType?: string

View File

@@ -1,10 +1,18 @@
import { Item } from '../../../Item/Item'
import { ItemConflict } from '../../../Item/ItemConflict'
import { Message } from '../../../Message/Message'
import { Notification } from '../../../Notifications/Notification'
import { SharedVault } from '../../../SharedVault/SharedVault'
import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite'
export type SyncItemsResponse = {
retrievedItems: Array<Item>
savedItems: Array<Item>
conflicts: Array<ItemConflict>
syncToken: string
sharedVaults: SharedVault[]
sharedVaultInvites: SharedVaultInvite[]
messages: Message[]
notifications: Notification[]
cursorToken?: string
}

View File

@@ -1,4 +1,4 @@
import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
import { ControllerContainerInterface, MapperInterface, Validator } from '@standardnotes/domain-core'
import { BaseHttpController, results } from 'inversify-express-utils'
import { Request, Response } from 'express'
@@ -49,19 +49,27 @@ export class HomeServerItemsController extends BaseHttpController {
}
}
let sharedVaultUuids: string[] | undefined = undefined
if ('shared_vault_uuids' in request.body) {
const sharedVaultUuidsValidation = Validator.isNotEmpty(sharedVaultUuids)
if (!sharedVaultUuidsValidation.isFailed()) {
sharedVaultUuids = request.body.shared_vault_uuids
}
}
const syncResult = await this.syncItems.execute({
userUuid: response.locals.user.uuid,
itemHashes,
computeIntegrityHash: request.body.compute_integrity === true,
syncToken: request.body.sync_token,
cursorToken: request.body.cursor_token,
sharedVaultUuids: request.body.shared_vault_uuids,
limit: request.body.limit,
contentType: request.body.content_type,
apiVersion: request.body.api ?? ApiVersion.v20161215,
snjsVersion: <string>request.headers['x-snjs-version'],
readOnlyAccess: response.locals.readOnlyAccess,
sessionUuid: response.locals.session ? response.locals.session.uuid : null,
sharedVaultUuids,
})
if (syncResult.isFailed()) {
return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)
@@ -102,7 +110,12 @@ export class HomeServerItemsController extends BaseHttpController {
})
if (result.isFailed()) {
return this.json({ error: { message: result.getError() } }, 404)
return this.json(
{
error: { message: 'Item not found' },
},
404,
)
}
return this.json({ item: this.itemHttpMapper.toProjection(result.getValue()) })

View File

@@ -62,7 +62,7 @@ export class HomeServerSharedVaultInvitesController extends BaseHttpController {
const result = await this.inviteUserToSharedVaultUseCase.execute({
sharedVaultUuid: request.params.sharedVaultUuid,
senderUuid: response.locals.user.uuid,
recipientUuid: request.body.recipient_uid,
recipientUuid: request.body.recipient_uuid,
encryptedMessage: request.body.encrypted_message,
permission: request.body.permission,
})

View File

@@ -9,6 +9,7 @@ import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPay
import { TypeORMItem } from './TypeORMItem'
import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
import { TypeORMSharedVaultAssociation } from './TypeORMSharedVaultAssociation'
export class TypeORMItemRepository implements ItemRepositoryInterface {
constructor(
@@ -92,15 +93,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
const item = this.mapper.toDomain(persistence)
const keySystemAssociation = await this.keySystemAssociationRepository.findByItemUuid(uuid)
if (keySystemAssociation) {
item.props.keySystemAssociation = keySystemAssociation
}
const sharedVaultAssociation = await this.sharedVaultAssociationRepository.findByItemUuid(uuid)
if (sharedVaultAssociation) {
item.props.sharedVaultAssociation = sharedVaultAssociation
}
await this.decorateItemWithAssociations(item)
return item
}
@@ -142,13 +135,21 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
return null
}
return this.mapper.toDomain(persistence)
const item = this.mapper.toDomain(persistence)
await this.decorateItemWithAssociations(item)
return item
}
async findAll(query: ItemQuery): Promise<Item[]> {
const persistence = await this.createFindAllQueryBuilder(query).getMany()
return persistence.map((p) => this.mapper.toDomain(p))
const domainItems = persistence.map((p) => this.mapper.toDomain(p))
await Promise.all(domainItems.map((item) => this.decorateItemWithAssociations(item)))
return domainItems
}
async findAllRaw<T>(query: ItemQuery): Promise<T[]> {
@@ -187,12 +188,37 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder)
}
if (query.includeSharedVaultUuids !== undefined && query.includeSharedVaultUuids.length > 0) {
queryBuilder
.leftJoin(
TypeORMSharedVaultAssociation,
'sharedVaultAssociation',
'sharedVaultAssociation.itemUuid = item.uuid',
)
.where('sharedVaultAssociation.sharedVaultUuid IN (:...sharedVaultUuids)', {
sharedVaultUuids: query.includeSharedVaultUuids,
})
if (query.userUuid) {
queryBuilder.orWhere('item.user_uuid = :userUuid', { userUuid: query.userUuid })
}
} else if (query.exclusiveSharedVaultUuids !== undefined && query.exclusiveSharedVaultUuids.length > 0) {
queryBuilder
.innerJoin(
TypeORMSharedVaultAssociation,
'sharedVaultAssociation',
'sharedVaultAssociation.itemUuid = item.uuid',
)
.where('sharedVaultAssociation.sharedVaultUuid IN (:...sharedVaultUuids)', {
sharedVaultUuids: query.exclusiveSharedVaultUuids,
})
} else if (query.userUuid !== undefined) {
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid })
}
if (query.selectString !== undefined) {
queryBuilder.select(query.selectString)
}
if (query.userUuid !== undefined) {
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid })
}
if (query.uuids && query.uuids.length > 0) {
queryBuilder.andWhere('item.uuid IN (:...uuids)', { uuids: query.uuids })
}
@@ -226,4 +252,25 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
return queryBuilder
}
private async decorateItemWithAssociations(item: Item): Promise<void> {
await Promise.all([
this.decorateItemWithKeySystemAssociation(item),
this.decorateItemWithSharedVaultAssociation(item),
])
}
private async decorateItemWithKeySystemAssociation(item: Item): Promise<void> {
const keySystemAssociation = await this.keySystemAssociationRepository.findByItemUuid(item.uuid)
if (keySystemAssociation) {
item.props.keySystemAssociation = keySystemAssociation
}
}
private async decorateItemWithSharedVaultAssociation(item: Item): Promise<void> {
const sharedVaultAssociation = await this.sharedVaultAssociationRepository.findByItemUuid(item.uuid)
if (sharedVaultAssociation) {
item.props.sharedVaultAssociation = sharedVaultAssociation
}
}
}

View File

@@ -11,6 +11,20 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface {
private mapper: MapperInterface<Message, TypeORMMessage>,
) {}
async findByRecipientUuidUpdatedAfter(uuid: Uuid, updatedAtTimestamp: number): Promise<Message[]> {
const persistence = await this.ormRepository
.createQueryBuilder('message')
.where('message.recipient_uuid = :recipientUuid', {
recipientUuid: uuid.value,
})
.andWhere('message.updated_at_timestamp > :updatedAtTimestamp', {
updatedAtTimestamp,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
async findByRecipientUuid(uuid: Uuid): Promise<Message[]> {
const persistence = await this.ormRepository
.createQueryBuilder('message')

View File

@@ -1,5 +1,5 @@
import { Repository } from 'typeorm'
import { MapperInterface } from '@standardnotes/domain-core'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { NotificationRepositoryInterface } from '../../Domain/Notifications/NotificationRepositoryInterface'
import { TypeORMNotification } from './TypeORMNotification'
@@ -16,4 +16,29 @@ export class TypeORMNotificationRepository implements NotificationRepositoryInte
await this.ormRepository.save(persistence)
}
async findByUserUuidUpdatedAfter(uuid: Uuid, updatedAtTimestamp: number): Promise<Notification[]> {
const persistence = await this.ormRepository
.createQueryBuilder('notification')
.where('notification.user_uuid = :userUuid', {
userUuid: uuid.value,
})
.andWhere('notification.updated_at_timestamp > :updatedAtTimestamp', {
updatedAtTimestamp,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
async findByUserUuid(uuid: Uuid): Promise<Notification[]> {
const persistence = await this.ormRepository
.createQueryBuilder('notification')
.where('notification.user_uuid = :userUuid', {
userUuid: uuid.value,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
}

View File

@@ -11,6 +11,20 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
) {}
async findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise<SharedVaultInvite[]> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_invite')
.where('shared_vault_invite.user_uuid = :userUuid', {
userUuid: userUuid.value,
})
.andWhere('shared_vault_invite.updated_at_timestamp > :updatedAtTimestamp', {
updatedAtTimestamp,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
async findBySenderUuidAndSharedVaultUuid(dto: {
senderUuid: Uuid
sharedVaultUuid: Uuid

View File

@@ -0,0 +1,21 @@
import { MapperInterface } from '@standardnotes/domain-core'
import { Notification } from '../../Domain/Notifications/Notification'
import { NotificationHttpRepresentation } from './NotificationHttpRepresentation'
export class NotificationHttpMapper implements MapperInterface<Notification, NotificationHttpRepresentation> {
toDomain(_projection: NotificationHttpRepresentation): Notification {
throw new Error('Mapping from http representation to domain is not implemented.')
}
toProjection(domain: Notification): NotificationHttpRepresentation {
return {
uuid: domain.id.toString(),
user_uuid: domain.props.userUuid.value,
type: domain.props.type.value,
payload: domain.props.payload.toString(),
created_at_timestamp: domain.props.timestamps.createdAt,
updated_at_timestamp: domain.props.timestamps.updatedAt,
}
}
}

View File

@@ -0,0 +1,8 @@
export interface NotificationHttpRepresentation {
uuid: string
user_uuid: string
type: string
payload: string
created_at_timestamp: number
updated_at_timestamp: number
}

View File

@@ -17,7 +17,7 @@ export class SharedVaultInviteHttpMapper
user_uuid: domain.props.userUuid.value,
sender_uuid: domain.props.senderUuid.value,
encrypted_message: domain.props.encryptedMessage,
permissions: domain.props.permission.value,
permission: domain.props.permission.value,
created_at_timestamp: domain.props.timestamps.createdAt,
updated_at_timestamp: domain.props.timestamps.updatedAt,
}

View File

@@ -4,7 +4,7 @@ export interface SharedVaultInviteHttpRepresentation {
user_uuid: string
sender_uuid: string
encrypted_message: string
permissions: string
permission: string
created_at_timestamp: number
updated_at_timestamp: number
}

View File

@@ -1,9 +1,15 @@
import { Timestamps, MapperInterface, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import {
Timestamps,
MapperInterface,
UniqueEntityId,
Uuid,
NotificationType,
NotificationPayload,
} from '@standardnotes/domain-core'
import { Notification } from '../../Domain/Notifications/Notification'
import { TypeORMNotification } from '../../Infra/TypeORM/TypeORMNotification'
import { NotificationType } from '../../Domain/Notifications/NotificationType'
export class NotificationPersistenceMapper implements MapperInterface<Notification, TypeORMNotification> {
toDomain(projection: TypeORMNotification): Notification {
@@ -25,10 +31,16 @@ export class NotificationPersistenceMapper implements MapperInterface<Notificati
}
const type = typeOrError.getValue()
const payloadOrError = NotificationPayload.createFromString(projection.payload)
if (payloadOrError.isFailed()) {
throw new Error(`Failed to create notification from projection: ${payloadOrError.getError()}`)
}
const payload = payloadOrError.getValue()
const notificationOrError = Notification.create(
{
userUuid,
payload: projection.payload,
payload,
type,
timestamps,
},
@@ -47,7 +59,7 @@ export class NotificationPersistenceMapper implements MapperInterface<Notificati
typeorm.uuid = domain.id.toString()
typeorm.userUuid = domain.props.userUuid.value
typeorm.payload = domain.props.payload
typeorm.payload = domain.props.payload.toString()
typeorm.type = domain.props.type.value
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.4](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.3...@standardnotes/websockets-server@1.10.4) (2023-07-21)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.10.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.2...@standardnotes/websockets-server@1.10.3) (2023-07-19)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.10.3",
"version": "1.10.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},