Compare commits

...

16 Commits

Author SHA1 Message Date
standardci
f0c85910bc chore(release): publish new version
- @standardnotes/syncing-server@1.25.3
2022-12-13 11:07:23 +00:00
Karol Sójko
124c443528 fix(syncing-server): revisions ownership procedure destructured 2022-12-13 12:05:10 +01:00
standardci
37c7f8d39f chore(release): publish new version
- @standardnotes/syncing-server@1.25.2
2022-12-13 07:19:55 +00:00
Karol Sójko
c419f1ce22 fix(syncing-server): change revisions migration to notes 2022-12-13 08:17:55 +01:00
standardci
4949cdfe2f chore(release): publish new version
- @standardnotes/syncing-server@1.25.1
2022-12-13 06:03:06 +00:00
Karol Sójko
cd101b96ea fix(syncing-server): revisions procedure properties 2022-12-13 07:01:06 +01:00
standardci
40d0e4631f chore(release): publish new version
- @standardnotes/syncing-server@1.25.0
2022-12-12 19:06:24 +00:00
Karol Sójko
a55a995660 feat(syncing-server): fix streaming items for revisions update 2022-12-12 20:03:45 +01:00
standardci
1d576d48ad chore(release): publish new version
- @standardnotes/auth-server@1.67.1
 - @standardnotes/syncing-server@1.24.7
2022-12-12 13:20:47 +00:00
Karol Sójko
4ff8030f87 fix(syncing-server): revisions updating - select fields 2022-12-12 14:18:45 +01:00
Karol Sójko
c15e2e2c8f fix: user signed in email template 2022-12-12 14:18:45 +01:00
standardci
41d31a8d75 chore(release): publish new version
- @standardnotes/auth-server@1.67.0
2022-12-12 13:00:40 +00:00
Karol Sójko
10e2a26352 feat(auth): add email subscription unsubscribed event handler 2022-12-12 13:58:35 +01:00
standardci
6e547f77d0 chore(release): publish new version
- @standardnotes/revisions-server@1.9.26
2022-12-12 12:14:52 +00:00
Karol Sójko
530a426601 fix(revisions): responses to match previous response structure 2022-12-12 13:12:46 +01:00
Karol Sójko
642d6bab77 chore: fix triggers for other repos dep 2022-12-12 12:56:28 +01:00
26 changed files with 301 additions and 436 deletions

View File

@@ -187,7 +187,7 @@ jobs:
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
- name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@v1.6.3
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: e2e

View File

@@ -3,6 +3,18 @@
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/server/compare/@standardnotes/auth-server@1.67.0...@standardnotes/auth-server@1.67.1) (2022-12-12)
### Bug Fixes
* user signed in email template ([c15e2e2](https://github.com/standardnotes/server/commit/c15e2e2c8f3a6c177e227d25440501fa38dd3d0e))
# [1.67.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.9...@standardnotes/auth-server@1.67.0) (2022-12-12)
### Features
* **auth:** add email subscription unsubscribed event handler ([10e2a26](https://github.com/standardnotes/server/commit/10e2a263522dfa33c06940f29cb77f783f66b20c))
## [1.66.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.8...@standardnotes/auth-server@1.66.9) (2022-12-12)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.66.9",
"version": "1.67.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -193,6 +193,7 @@ import { SubscriptionInvitesController } from '../Controller/SubscriptionInvites
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
import { UserRequestsController } from '../Controller/UserRequestsController'
import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -560,6 +561,15 @@ export class ContainerConfigLoader {
)
}
container
.bind<EmailSubscriptionUnsubscribedEventHandler>(TYPES.EmailSubscriptionUnsubscribedEventHandler)
.toConstantValue(
new EmailSubscriptionUnsubscribedEventHandler(
container.get(TYPES.UserRepository),
container.get(TYPES.SettingService),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -582,6 +592,7 @@ export class ContainerConfigLoader {
],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.EmailSubscriptionUnsubscribedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -138,6 +138,7 @@ const TYPES = {
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
EmailSubscriptionUnsubscribedEventHandler: Symbol.for('EmailSubscriptionUnsubscribedEventHandler'),
// Services
DeviceDetector: Symbol.for('DeviceDetector'),
SessionService: Symbol.for('SessionService'),

View File

@@ -20,6 +20,5 @@ export const html = (email: string, device: string, browser: string, timeAndDate
<br />
SN
</p>
<a href="https://app.standardnotes.com/?settings=account">Mute these emails</a>
</div>
`

View File

@@ -0,0 +1,109 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { EmailSubscriptionUnsubscribedEventHandler } from './EmailSubscriptionUnsubscribedEventHandler'
describe('EmailSubscriptionUnsubscribedEventHandler', () => {
let userRepository: UserRepositoryInterface
let settingsService: SettingServiceInterface
let event: EmailSubscriptionUnsubscribedEvent
const createHandler = () => new EmailSubscriptionUnsubscribedEventHandler(userRepository, settingsService)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({} as jest.Mocked<User>)
settingsService = {} as jest.Mocked<SettingServiceInterface>
settingsService.createOrReplace = jest.fn()
event = {
payload: {
userEmail: 'test@test.te',
level: EmailLevel.LEVELS.Marketing,
},
} as jest.Mocked<EmailSubscriptionUnsubscribedEvent>
})
it('should not do anything if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(settingsService.createOrReplace).not.toHaveBeenCalled()
})
it('should update user marketing email settings', async () => {
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_MARKETING_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user sign in email settings', async () => {
event.payload.level = EmailLevel.LEVELS.SignIn
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_SIGN_IN_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedEmailBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedCloudBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should throw error for unrecognized level', async () => {
event.payload.level = 'foobar'
let caughtError = null
try {
await createHandler().handle(event)
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})

View File

@@ -0,0 +1,41 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHandlerInterface {
constructor(private userRepository: UserRepositoryInterface, private settingsService: SettingServiceInterface) {}
async handle(event: EmailSubscriptionUnsubscribedEvent): Promise<void> {
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user === null) {
return
}
await this.settingsService.createOrReplace({
user,
props: {
name: this.getSettingNameFromLevel(event.payload.level),
unencryptedValue: 'muted',
sensitive: false,
},
})
}
private getSettingNameFromLevel(level: string): string {
switch (level) {
case EmailLevel.LEVELS.FailedCloudBackup:
return SettingName.MuteFailedCloudBackupsEmails
case EmailLevel.LEVELS.FailedEmailBackup:
return SettingName.MuteFailedBackupsEmails
case EmailLevel.LEVELS.Marketing:
return SettingName.MuteMarketingEmails
case EmailLevel.LEVELS.SignIn:
return SettingName.MuteSignInEmails
default:
throw new Error(`Unknown level: ${level}`)
}
}
}

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.9.26](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.25...@standardnotes/revisions-server@1.9.26) (2022-12-12)
### Bug Fixes
* **revisions:** responses to match previous response structure ([530a426](https://github.com/standardnotes/server/commit/530a42660157b63d034cd2228fc83a3fcce921e0))
## [1.9.25](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.24...@standardnotes/revisions-server@1.9.25) (2022-12-12)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.9.25",
"version": "1.9.26",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -2,11 +2,13 @@ import { Logger } from 'winston'
import { HttpResponse, HttpStatusCode } from '@standardnotes/api'
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
import { GetRevisionsMetadataRequestParams } from '../Infra/Http/GetRevisionsMetadataRequestParams'
import { GetRevisionRequestParams } from '../Infra/Http/GetRevisionRequestParams'
import { GetRevisionsMetadataRequestParams } from '../Infra/Http/Request/GetRevisionsMetadataRequestParams'
import { GetRevisionRequestParams } from '../Infra/Http/Request/GetRevisionRequestParams'
import { DeleteRevisionRequestParams } from '../Infra/Http/Request/DeleteRevisionRequestParams'
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
import { DeleteRevisionRequestParams } from '../Infra/Http/DeleteRevisionRequestParams'
import { GetRevisionsMetadataResponse } from '../Infra/Http/Response/GetRevisionsMetadataResponse'
import { GetRevisionResponse } from '../Infra/Http/Response/GetRevisionResponse'
export class RevisionsController {
constructor(
@@ -16,7 +18,7 @@ export class RevisionsController {
private logger: Logger,
) {}
async getRevisions(params: GetRevisionsMetadataRequestParams): Promise<HttpResponse> {
async getRevisions(params: GetRevisionsMetadataRequestParams): Promise<GetRevisionsMetadataResponse> {
const revisionMetadataOrError = await this.getRevisionsMetadata.execute({
itemUuid: params.itemUuid,
userUuid: params.userUuid,
@@ -41,7 +43,7 @@ export class RevisionsController {
}
}
async getRevision(params: GetRevisionRequestParams): Promise<HttpResponse> {
async getRevision(params: GetRevisionRequestParams): Promise<GetRevisionResponse> {
const revisionOrError = await this.doGetRevision.execute({
revisionUuid: params.revisionUuid,
userUuid: params.userUuid,

View File

@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { GetRevisionResponseBody } from './GetRevisionResponseBody'
export interface GetRevisionResponse extends HttpResponse {
data: Either<GetRevisionResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,5 @@
import { Revision } from '../../../Domain/Revision/Revision'
export interface GetRevisionResponseBody {
revision: Revision
}

View File

@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { GetRevisionsMetadataResponseBody } from './GetRevisionsMetadataResponseBody'
export interface GetRevisionsMetadataResponse extends HttpResponse {
data: Either<GetRevisionsMetadataResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,5 @@
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
export interface GetRevisionsMetadataResponseBody {
revisions: Array<RevisionMetadata>
}

View File

@@ -18,7 +18,7 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
return this.json(result.data.error ? result.data : result.data.revisions, result.status)
}
@httpGet('/:uuid')
@@ -28,7 +28,7 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
return this.json(result.data.error ? result.data : result.data.revision, result.status)
}
@httpDelete('/:uuid')

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.25.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.2...@standardnotes/syncing-server@1.25.3) (2022-12-13)
### Bug Fixes
* **syncing-server:** revisions ownership procedure destructured ([124c443](https://github.com/standardnotes/syncing-server-js/commit/124c4435285c2c2e8d0ce8b47907ebd47af27576))
## [1.25.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.1...@standardnotes/syncing-server@1.25.2) (2022-12-13)
### Bug Fixes
* **syncing-server:** change revisions migration to notes ([c419f1c](https://github.com/standardnotes/syncing-server-js/commit/c419f1ce220c27acabfc813a30b3edd6c4aadaa1))
## [1.25.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.0...@standardnotes/syncing-server@1.25.1) (2022-12-13)
### Bug Fixes
* **syncing-server:** revisions procedure properties ([cd101b9](https://github.com/standardnotes/syncing-server-js/commit/cd101b96eae8969a4dd2387deb1d4e8679ead216))
# [1.25.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.7...@standardnotes/syncing-server@1.25.0) (2022-12-12)
### Features
* **syncing-server:** fix streaming items for revisions update ([a55a995](https://github.com/standardnotes/syncing-server-js/commit/a55a9956602bee7dbb0f93f058aceff7a2136ffd))
## [1.24.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.6...@standardnotes/syncing-server@1.24.7) (2022-12-12)
### Bug Fixes
* **syncing-server:** revisions updating - select fields ([4ff8030](https://github.com/standardnotes/syncing-server-js/commit/4ff8030f8709ee18853c2e782cfc5d99c826f074))
## [1.24.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.5...@standardnotes/syncing-server@1.24.6) (2022-12-12)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -11,18 +11,25 @@ import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFact
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { ItemRepositoryInterface } from '../src/Domain/Item/ItemRepositoryInterface'
import { Stream } from 'stream'
import { ContentType } from '@standardnotes/common'
const fixRevisionsOwnership = async (
year: number,
month: number,
itemRepository: ItemRepositoryInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
logger: Logger,
): Promise<void> => {
const createdAfter = new Date(`${year}-${month}-1`)
const createdBefore = new Date(`${month !== 12 ? year : year + 1}-${month !== 12 ? month + 1 : 1}-1`)
logger.info(`Processing items between ${createdAfter.toISOString} and ${createdBefore.toISOString}`)
const stream = await itemRepository.streamAll({
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
createdBefore: new Date('2022-11-23'),
selectFields: ['user_uuid', 'item_uuid'],
createdBetween: [createdAfter, createdBefore],
selectFields: ['user_uuid', 'uuid'],
contentType: [ContentType.Note, ContentType.File],
})
return new Promise((resolve, reject) => {
@@ -32,7 +39,7 @@ const fixRevisionsOwnership = async (
objectMode: true,
transform: async (rawItemData, _encoding, callback) => {
try {
if (!rawItemData.item_user_uuid || !rawItemData.item_uuid) {
if (!rawItemData.user_uuid || !rawItemData.item_uuid) {
logger.error('Could not process item %O', rawItemData)
return callback()
@@ -40,7 +47,7 @@ const fixRevisionsOwnership = async (
await domainEventPublisher.publish(
domainEventFactory.createRevisionsOwnershipUpdateRequestedEvent({
userUuid: rawItemData.item_user_uuid,
userUuid: rawItemData.user_uuid,
itemUuid: rawItemData.item_uuid,
}),
)
@@ -48,11 +55,15 @@ const fixRevisionsOwnership = async (
logger.error(`Could not process item ${rawItemData.item_uuid}: ${(error as Error).message}`)
}
callback()
return callback()
},
}),
)
.on('finish', resolve)
.on('finish', () => {
logger.info(`Finished processing items between ${createdAfter.toISOString} and ${createdBefore.toISOString}`)
resolve()
})
.on('error', reject)
})
}
@@ -70,7 +81,16 @@ void container.load().then((container) => {
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
Promise.resolve(fixRevisionsOwnership(itemRepository, domainEventFactory, domainEventPublisher, logger))
const years = [2016, 2017, 2018, 2019, 2020, 2021, 2022]
const promises = []
for (const year of years) {
for (let i = 1; i <= 12; i++) {
promises.push(fixRevisionsOwnership(year, i, itemRepository, domainEventFactory, domainEventPublisher, logger))
}
}
Promise.all(promises)
.then(() => {
logger.info('revisions ownership fix complete.')

View File

@@ -5,33 +5,33 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-local')
echo "Starting Web in Local Mode..."
echo "[Docker] Starting Web in Local Mode..."
yarn workspace @standardnotes/syncing-server start:local
;;
'start-web' )
echo "Starting Web..."
echo "[Docker] Starting Web..."
yarn workspace @standardnotes/syncing-server start
;;
'start-worker' )
echo "Starting Worker..."
echo "[Docker] Starting Worker..."
yarn workspace @standardnotes/syncing-server worker
;;
'content-size-recalculate' )
echo "Starting Content Size Recalculation..."
echo "[Docker] Starting Content Size Recalculation..."
USER_UUID=$1 && shift 1
yarn workspace @standardnotes/syncing-server content-size $USER_UUID
;;
'revisions-ownership-fix' )
echo "Starting Revisions Ownership Fixing..."
echo "[Docker] Starting Revisions Ownership Fixing..."
yarn workspace @standardnotes/syncing-server revisions-ownership
;;
* )
echo "Unknown command"
echo "[Docker] Unknown command"
;;
esac

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.24.6",
"version": "1.25.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,14 +1,14 @@
export type ItemQuery = {
userUuid?: string
sortBy: string
sortOrder: 'ASC' | 'DESC'
sortBy?: string
sortOrder?: 'ASC' | 'DESC'
uuids?: Array<string>
lastSyncTime?: number
syncTimeComparison?: '>' | '>='
contentType?: string
contentType?: string | string[]
deleted?: boolean
offset?: number
limit?: number
createdBefore?: Date
createdBetween?: Date[]
selectFields?: string[]
}

View File

@@ -1,401 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { ContentType } from '@standardnotes/common'
import { Item } from '../../Domain/Item/Item'
import { MySQLItemRepository } from './MySQLItemRepository'
import { TimerInterface } from '@standardnotes/time'
import { ReadStream } from 'fs'
describe('MySQLItemRepository', () => {
let queryBuilder: SelectQueryBuilder<Item>
let ormRepository: Repository<Item>
let item: Item
let timer: TimerInterface
const createRepository = () => new MySQLItemRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Item>>
item = {} as jest.Mocked<Item>
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn(() => 1616161616161616)
ormRepository = {} as jest.Mocked<Repository<Item>>
ormRepository.save = jest.fn()
ormRepository.remove = jest.fn()
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
})
it('should save', async () => {
await createRepository().save(item)
expect(ormRepository.save).toHaveBeenCalledWith(item)
})
it('should remove', async () => {
await createRepository().remove(item)
expect(ormRepository.remove).toHaveBeenCalledWith(item)
})
it('should delete all items for a given user', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.delete = jest.fn().mockReturnThis()
queryBuilder.from = jest.fn().mockReturnThis()
queryBuilder.execute = jest.fn()
await createRepository().deleteByUserUuid('123')
expect(queryBuilder.delete).toHaveBeenCalled()
expect(queryBuilder.from).toHaveBeenCalledWith('items')
expect(queryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '123' })
expect(queryBuilder.execute).toHaveBeenCalled()
})
it('should find one item by uuid and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(item)
const result = await createRepository().findByUuidAndUserUuid('1-2-3', '2-3-4')
expect(queryBuilder.where).toHaveBeenCalledWith('item.uuid = :uuid AND item.user_uuid = :userUuid', {
uuid: '1-2-3',
userUuid: '2-3-4',
})
expect(result).toEqual(item)
})
it('should find one item by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(item)
const result = await createRepository().findByUuid('1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('item.uuid = :uuid', {
uuid: '1-2-3',
})
expect(result).toEqual(item)
})
it('should find items by all query criteria filled in', async () => {
queryBuilder.getMany = jest.fn().mockReturnValue([item])
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
queryBuilder.orderBy = jest.fn()
queryBuilder.skip = jest.fn()
queryBuilder.take = jest.fn()
const result = await createRepository().findAll({
userUuid: '1-2-3',
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
deleted: false,
contentType: ContentType.Note,
lastSyncTime: 123,
syncTimeComparison: '>=',
uuids: ['2-3-4'],
offset: 1,
limit: 10,
})
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(4)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.uuid IN (:...uuids)', { uuids: ['2-3-4'] })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(2, 'item.deleted = :deleted', { deleted: false })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(3, 'item.content_type = :contentType', {
contentType: 'Note',
})
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(4, 'item.updated_at_timestamp >= :lastSyncTime', {
lastSyncTime: 123,
})
expect(queryBuilder.skip).toHaveBeenCalledWith(1)
expect(queryBuilder.take).toHaveBeenCalledWith(10)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual([item])
})
it('should stream items by all query criteria filled in', async () => {
const stream = {} as jest.Mocked<ReadStream>
queryBuilder.stream = jest.fn().mockReturnValue(stream)
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
queryBuilder.orderBy = jest.fn()
queryBuilder.skip = jest.fn()
queryBuilder.take = jest.fn()
const result = await createRepository().streamAll({
userUuid: '1-2-3',
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
deleted: false,
contentType: ContentType.Note,
lastSyncTime: 123,
syncTimeComparison: '>=',
uuids: ['2-3-4'],
offset: 1,
limit: 10,
})
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(4)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.uuid IN (:...uuids)', { uuids: ['2-3-4'] })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(2, 'item.deleted = :deleted', { deleted: false })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(3, 'item.content_type = :contentType', {
contentType: 'Note',
})
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(4, 'item.updated_at_timestamp >= :lastSyncTime', {
lastSyncTime: 123,
})
expect(queryBuilder.skip).toHaveBeenCalledWith(1)
expect(queryBuilder.take).toHaveBeenCalledWith(10)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual(stream)
})
it('should find items content sizes by all query criteria filled in', async () => {
queryBuilder.getRawMany = jest.fn().mockReturnValue([{ uuid: item.uuid, contentSize: item.contentSize }])
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
queryBuilder.orderBy = jest.fn()
queryBuilder.select = jest.fn()
queryBuilder.addSelect = jest.fn()
queryBuilder.skip = jest.fn()
queryBuilder.take = jest.fn()
const result = await createRepository().findContentSizeForComputingTransferLimit({
userUuid: '1-2-3',
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
deleted: false,
contentType: ContentType.Note,
lastSyncTime: 123,
syncTimeComparison: '>=',
uuids: ['2-3-4'],
offset: 1,
limit: 10,
})
expect(queryBuilder.select).toHaveBeenCalledWith('item.uuid', 'uuid')
expect(queryBuilder.addSelect).toHaveBeenCalledWith('item.content_size', 'contentSize')
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(4)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.uuid IN (:...uuids)', { uuids: ['2-3-4'] })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(2, 'item.deleted = :deleted', { deleted: false })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(3, 'item.content_type = :contentType', {
contentType: 'Note',
})
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(4, 'item.updated_at_timestamp >= :lastSyncTime', {
lastSyncTime: 123,
})
expect(queryBuilder.skip).toHaveBeenCalledWith(1)
expect(queryBuilder.take).toHaveBeenCalledWith(10)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual([item])
})
it('should find items by all query criteria filled in', async () => {
queryBuilder.getMany = jest.fn().mockReturnValue([item])
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
queryBuilder.orderBy = jest.fn()
queryBuilder.skip = jest.fn()
queryBuilder.take = jest.fn()
const result = await createRepository().findAll({
userUuid: '1-2-3',
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
deleted: false,
contentType: ContentType.Note,
lastSyncTime: 123,
syncTimeComparison: '>=',
uuids: ['2-3-4'],
offset: 1,
limit: 10,
})
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(4)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.uuid IN (:...uuids)', { uuids: ['2-3-4'] })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(2, 'item.deleted = :deleted', { deleted: false })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(3, 'item.content_type = :contentType', {
contentType: 'Note',
})
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(4, 'item.updated_at_timestamp >= :lastSyncTime', {
lastSyncTime: 123,
})
expect(queryBuilder.skip).toHaveBeenCalledWith(1)
expect(queryBuilder.take).toHaveBeenCalledWith(10)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual([item])
})
it('should count items by all query criteria filled in', async () => {
queryBuilder.getCount = jest.fn().mockReturnValue(1)
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
queryBuilder.orderBy = jest.fn()
queryBuilder.skip = jest.fn()
queryBuilder.take = jest.fn()
const result = await createRepository().countAll({
userUuid: '1-2-3',
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
deleted: false,
contentType: ContentType.Note,
lastSyncTime: 123,
syncTimeComparison: '>=',
uuids: ['2-3-4'],
offset: 1,
limit: 10,
})
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(4)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.uuid IN (:...uuids)', { uuids: ['2-3-4'] })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(2, 'item.deleted = :deleted', { deleted: false })
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(3, 'item.content_type = :contentType', {
contentType: 'Note',
})
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(4, 'item.updated_at_timestamp >= :lastSyncTime', {
lastSyncTime: 123,
})
expect(queryBuilder.skip).toHaveBeenCalledWith(1)
expect(queryBuilder.take).toHaveBeenCalledWith(10)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual(1)
})
it('should find items by only mandatory query criteria', async () => {
queryBuilder.getMany = jest.fn().mockReturnValue([item])
queryBuilder.where = jest.fn()
queryBuilder.orderBy = jest.fn()
const result = await createRepository().findAll({
sortBy: 'updated_at_timestamp',
sortOrder: 'DESC',
})
expect(queryBuilder.orderBy).toHaveBeenCalledWith('item.updated_at_timestamp', 'DESC')
expect(result).toEqual([item])
})
it('should find dates for computing integrity hash', async () => {
queryBuilder.getRawMany = jest
.fn()
.mockReturnValue([{ updated_at_timestamp: 1616164633241312 }, { updated_at_timestamp: 1616164633242313 }])
queryBuilder.select = jest.fn()
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
const result = await createRepository().findDatesForComputingIntegrityHash('1-2-3')
expect(queryBuilder.select).toHaveBeenCalledWith('item.updated_at_timestamp')
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.deleted = :deleted', { deleted: false })
expect(result.length).toEqual(2)
expect(result[0]).toEqual({ updated_at_timestamp: 1616164633242313 })
expect(result[1]).toEqual({ updated_at_timestamp: 1616164633241312 })
})
it('should find items for computing integrity payloads', async () => {
queryBuilder.getRawMany = jest.fn().mockReturnValue([
{ uuid: '1-2-3', updated_at_timestamp: 1616164633241312, content_type: ContentType.Note },
{ uuid: '2-3-4', updated_at_timestamp: 1616164633242313, content_type: ContentType.ItemsKey },
])
queryBuilder.select = jest.fn()
queryBuilder.addSelect = jest.fn()
queryBuilder.where = jest.fn()
queryBuilder.andWhere = jest.fn()
const result = await createRepository().findItemsForComputingIntegrityPayloads('1-2-3')
expect(queryBuilder.select).toHaveBeenCalledWith('item.uuid', 'uuid')
expect(queryBuilder.addSelect).toHaveBeenNthCalledWith(1, 'item.updated_at_timestamp', 'updated_at_timestamp')
expect(queryBuilder.addSelect).toHaveBeenNthCalledWith(2, 'item.content_type', 'content_type')
expect(queryBuilder.where).toHaveBeenCalledTimes(1)
expect(queryBuilder.where).toHaveBeenNthCalledWith(1, 'item.user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(queryBuilder.andWhere).toHaveBeenCalledTimes(1)
expect(queryBuilder.andWhere).toHaveBeenNthCalledWith(1, 'item.deleted = :deleted', { deleted: false })
expect(result.length).toEqual(2)
expect(result[0]).toEqual({
uuid: '2-3-4',
updated_at_timestamp: 1616164633242313,
content_type: ContentType.ItemsKey,
})
expect(result[1]).toEqual({ uuid: '1-2-3', updated_at_timestamp: 1616164633241312, content_type: ContentType.Note })
})
it('should find item by uuid and mark it for deletion', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.update = jest.fn().mockReturnThis()
queryBuilder.update().set = jest.fn().mockReturnThis()
queryBuilder.execute = jest.fn()
const item = { uuid: 'e-1-2-3' } as jest.Mocked<Item>
const updatedAtTimestamp = timer.getTimestampInMicroseconds()
await createRepository().markItemsAsDeleted([item.uuid], updatedAtTimestamp)
expect(queryBuilder.update).toHaveBeenCalled()
expect(queryBuilder.update().set).toHaveBeenCalledWith(
expect.objectContaining({
deleted: true,
content: null,
encItemKey: null,
authHash: null,
updatedAtTimestamp: expect.anything(),
}),
)
expect(queryBuilder.where).toHaveBeenCalledWith('uuid IN (:...uuids)', {
uuids: ['e-1-2-3'],
})
expect(queryBuilder.execute).toHaveBeenCalled()
})
it('should update item content size', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.update = jest.fn().mockReturnThis()
queryBuilder.update().set = jest.fn().mockReturnThis()
queryBuilder.execute = jest.fn()
await createRepository().updateContentSize('1-2-3', 345)
expect(queryBuilder.update).toHaveBeenCalled()
expect(queryBuilder.update().set).toHaveBeenCalledWith(
expect.objectContaining({
contentSize: 345,
}),
)
expect(queryBuilder.where).toHaveBeenCalledWith('uuid = :itemUuid', {
itemUuid: '1-2-3',
})
expect(queryBuilder.execute).toHaveBeenCalled()
})
})

View File

@@ -131,7 +131,9 @@ export class MySQLItemRepository implements ItemRepositoryInterface {
private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<Item> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder)
if (query.sortBy !== undefined && query.sortOrder !== undefined) {
queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder)
}
if (query.selectFields !== undefined) {
queryBuilder.select(query.selectFields.map((field) => `item.${field}`))
@@ -146,15 +148,22 @@ export class MySQLItemRepository implements ItemRepositoryInterface {
queryBuilder.andWhere('item.deleted = :deleted', { deleted: query.deleted })
}
if (query.contentType) {
queryBuilder.andWhere('item.content_type = :contentType', { contentType: query.contentType })
if (Array.isArray(query.contentType)) {
queryBuilder.andWhere('item.content_type IN (:...contentTypes)', { contentTypes: query.contentType })
} else {
queryBuilder.andWhere('item.content_type = :contentType', { contentType: query.contentType })
}
}
if (query.lastSyncTime && query.syncTimeComparison) {
queryBuilder.andWhere(`item.updated_at_timestamp ${query.syncTimeComparison} :lastSyncTime`, {
lastSyncTime: query.lastSyncTime,
})
}
if (query.createdBefore !== undefined) {
queryBuilder.andWhere('item.created_at < :createdAt', { createdAt: query.createdBefore.toISOString() })
if (query.createdBetween !== undefined) {
queryBuilder.andWhere('item.created_at BETWEEN :createdAfter AND :createdBefore', {
createdAfter: query.createdBetween[0].toISOString(),
createdBefore: query.createdBetween[1].toISOString(),
})
}
if (query.offset !== undefined) {
queryBuilder.skip(query.offset)