Compare commits

..

38 Commits

Author SHA1 Message Date
standardci
9ca373e208 chore(release): publish new version
- @standardnotes/analytics@2.12.27
 - @standardnotes/auth-server@1.67.3
 - @standardnotes/domain-core@1.11.0
 - @standardnotes/revisions-server@1.9.28
 - @standardnotes/scheduler-server@1.15.8
 - @standardnotes/syncing-server@1.26.7
 - @standardnotes/workspace-server@1.18.6
2022-12-15 11:26:36 +00:00
Karol Sójko
4084f2f5ec feat(domain-core): add legacy session model 2022-12-15 12:24:11 +01:00
standardci
684ffbadbc chore(release): publish new version
- @standardnotes/analytics@2.12.26
 - @standardnotes/auth-server@1.67.2
 - @standardnotes/domain-core@1.10.0
 - @standardnotes/revisions-server@1.9.27
 - @standardnotes/scheduler-server@1.15.7
 - @standardnotes/syncing-server@1.26.6
 - @standardnotes/workspace-server@1.18.5
2022-12-15 10:38:10 +00:00
Karol Sójko
1c4d4c57de feat(domain-core): add session model 2022-12-15 11:35:49 +01:00
standardci
d83111a199 chore(release): publish new version
- @standardnotes/syncing-server@1.26.5
2022-12-15 09:40:56 +00:00
Karol Sójko
f10fa839fb fix(syncing-server): revisions processing limit 2022-12-15 10:38:25 +01:00
standardci
1f20395ff3 chore(release): publish new version
- @standardnotes/syncing-server@1.26.4
2022-12-15 07:33:29 +00:00
Karol Sójko
bfe6f4255a fix(syncing-server): user uuid field name 2022-12-15 08:31:30 +01:00
standardci
b9032f3012 chore(release): publish new version
- @standardnotes/syncing-server@1.26.3
2022-12-15 06:31:07 +00:00
Karol Sójko
ce53c459e6 fix(syncing-server): select fields in query for revisions 2022-12-15 07:28:43 +01:00
standardci
6df42fb0d5 chore(release): publish new version
- @standardnotes/syncing-server@1.26.2
2022-12-14 19:08:28 +00:00
Karol Sójko
1e2b496f4f fix(syncing-server): revisions procedure logs 2022-12-14 20:06:07 +01:00
standardci
528c1b0d57 chore(release): publish new version
- @standardnotes/syncing-server@1.26.1
2022-12-14 18:58:11 +00:00
Karol Sójko
22fba8ba80 fix(syncing-server): revisions procedure logs 2022-12-14 19:56:17 +01:00
standardci
6f26261ebe chore(release): publish new version
- @standardnotes/syncing-server@1.26.0
2022-12-14 15:51:14 +00:00
Karol Sójko
4b1fe3ba91 feat(syncing-server): change revisions procedure to pagination instead of streaming 2022-12-14 16:49:21 +01:00
standardci
9f95262bd4 chore(release): publish new version
- @standardnotes/syncing-server@1.25.6
2022-12-14 09:33:24 +00:00
Karol Sójko
2ec28e541e fix(syncing-server): additional stream events handling on revisions procedure 2022-12-14 10:31:06 +01:00
standardci
4764d4b19a chore(release): publish new version
- @standardnotes/syncing-server@1.25.5
2022-12-14 09:00:42 +00:00
Karol Sójko
9b27547dae fix(syncing-server): revisions procedure with env var defined ranges 2022-12-14 09:58:41 +01:00
standardci
a96f2c9153 chore(release): publish new version
- @standardnotes/syncing-server@1.25.4
2022-12-13 11:14:35 +00:00
Karol Sójko
225e0aaf88 fix(syncing-server): logs on revisions procedure 2022-12-13 12:12:42 +01:00
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
46 changed files with 635 additions and 475 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

4
.pnp.cjs generated
View File

@@ -2737,10 +2737,6 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/domain-core/",\
"packageDependencies": [\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.53.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/security", "workspace:packages/security"],\
["@types/jest", "npm:29.1.1"],\
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.27](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.26...@standardnotes/analytics@2.12.27) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.26](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.25...@standardnotes/analytics@2.12.26) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.12.25",
"version": "2.12.27",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.67.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.2...@standardnotes/auth-server@1.67.3) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.67.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.1...@standardnotes/auth-server@1.67.2) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [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.3",
"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,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.10.0...@standardnotes/domain-core@1.11.0) (2022-12-15)
### Features
* **domain-core:** add legacy session model ([4084f2f](https://github.com/standardnotes/server/commit/4084f2f5ecf8379ff69d619d3d12495c010c3980))
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.9.0...@standardnotes/domain-core@1.10.0) (2022-12-15)
### Features
* **domain-core:** add session model ([1c4d4c5](https://github.com/standardnotes/server/commit/1c4d4c57dea1187dc130f1ae8b7dc8ede332320f))
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.8.0...@standardnotes/domain-core@1.9.0) (2022-12-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.9.0",
"version": "1.11.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -23,10 +23,6 @@
"test": "jest spec --coverage --passWithNoTests"
},
"dependencies": {
"@standardnotes/common": "workspace:*",
"@standardnotes/features": "^1.52.1",
"@standardnotes/predicates": "workspace:*",
"@standardnotes/security": "workspace:*",
"reflect-metadata": "^0.1.13",
"shallow-equal-object": "^1.1.1",
"uuid": "^9.0.0"

View File

@@ -0,0 +1,16 @@
import { LegacySession } from './LegacySession'
describe('LegacySession', () => {
it('should create a value object', () => {
const valueOrError = LegacySession.create('foobar')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().accessToken).toEqual('foobar')
})
it('should not create an invalid value object', () => {
const valueOrError = LegacySession.create('')
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,22 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { LegacySessionProps } from './LegacySessionProps'
import { Validator } from '../Core/Validator'
export class LegacySession extends ValueObject<LegacySessionProps> {
get accessToken(): string {
return this.props.token
}
private constructor(props: LegacySessionProps) {
super(props)
}
static create(token: string): Result<LegacySession> {
if (Validator.isNotEmpty(token).isFailed()) {
return Result.fail<LegacySession>('Could not create legacy session. Token value is empty')
}
return Result.ok<LegacySession>(new LegacySession({ token }))
}
}

View File

@@ -0,0 +1,3 @@
export interface LegacySessionProps {
token: string
}

View File

@@ -0,0 +1,26 @@
import { Session } from './Session'
import { SessionToken } from './SessionToken'
describe('Session', () => {
it('should create a session value object', () => {
const accessToken = SessionToken.create('foobar1', 1234567890).getValue()
const refreshToken = SessionToken.create('foobar2', 1234567890).getValue()
const valueOrError = Session.create(accessToken, refreshToken)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().accessToken.value).toEqual('foobar1')
expect(valueOrError.getValue().refreshToken.value).toEqual('foobar2')
expect(valueOrError.getValue().isReadOnly()).toEqual(false)
})
it('should create a session reado-only value object', () => {
const accessToken = SessionToken.create('foobar', 1234567890).getValue()
const refreshToken = SessionToken.create('foobar', 1234567890).getValue()
const valueOrError = Session.create(accessToken, refreshToken, true)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().isReadOnly()).toEqual(true)
})
})

View File

@@ -0,0 +1,26 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { SessionProps } from './SessionProps'
import { SessionToken } from './SessionToken'
export class Session extends ValueObject<SessionProps> {
get accessToken(): SessionToken {
return this.props.accessToken
}
get refreshToken(): SessionToken {
return this.props.refreshToken
}
isReadOnly(): boolean {
return this.props.readonlyAccess || false
}
private constructor(props: SessionProps) {
super(props)
}
static create(accessToken: SessionToken, refreshToken: SessionToken, readonlyAccess?: boolean): Result<Session> {
return Result.ok<Session>(new Session({ accessToken, refreshToken, readonlyAccess }))
}
}

View File

@@ -0,0 +1,7 @@
import { SessionToken } from './SessionToken'
export interface SessionProps {
accessToken: SessionToken
refreshToken: SessionToken
readonlyAccess?: boolean
}

View File

@@ -0,0 +1,21 @@
import { SessionToken } from './SessionToken'
describe('SessionToken', () => {
it('should create a value object', () => {
const valueOrError = SessionToken.create('foobar', 1234567890)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('foobar')
expect(valueOrError.getValue().expiresAt).toEqual(1234567890)
})
it('should not create an invalid value object', () => {
let valueOrError = SessionToken.create('', 1234567890)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = SessionToken.create('foobar', undefined as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,29 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { SessionTokenProps } from './SessionTokenProps'
import { Validator } from '../Core/Validator'
export class SessionToken extends ValueObject<SessionTokenProps> {
get value(): string {
return this.props.value
}
get expiresAt(): number {
return this.props.expiresAt
}
private constructor(props: SessionTokenProps) {
super(props)
}
static create(value: string, expiresAt: number): Result<SessionToken> {
if (Validator.isNotEmpty(value).isFailed()) {
return Result.fail<SessionToken>('Could not create session token. Token value is empty')
}
if (Validator.isNotEmpty(expiresAt).isFailed()) {
return Result.fail<SessionToken>('Could not create session token. Token expiration is empty')
}
return Result.ok<SessionToken>(new SessionToken({ value, expiresAt }))
}
}

View File

@@ -0,0 +1,4 @@
export interface SessionTokenProps {
value: string
expiresAt: number
}

View File

@@ -1,3 +1,10 @@
export * from './Auth/LegacySession'
export * from './Auth/LegacySessionProps'
export * from './Auth/Session'
export * from './Auth/SessionProps'
export * from './Auth/SessionToken'
export * from './Auth/SessionTokenProps'
export * from './Common/Dates'
export * from './Common/DatesProps'
export * from './Common/Email'

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.28](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.27...@standardnotes/revisions-server@1.9.28) (2022-12-15)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.27](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.26...@standardnotes/revisions-server@1.9.27) (2022-12-15)
**Note:** Version bump only for package @standardnotes/revisions-server
## [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.28",
"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,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.8](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.7...@standardnotes/scheduler-server@1.15.8) (2022-12-15)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.15.7](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.6...@standardnotes/scheduler-server@1.15.7) (2022-12-15)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.15.6](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.5...@standardnotes/scheduler-server@1.15.6) (2022-12-12)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.15.6",
"version": "1.15.8",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,98 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.6...@standardnotes/syncing-server@1.26.7) (2022-12-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.26.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.5...@standardnotes/syncing-server@1.26.6) (2022-12-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.26.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.4...@standardnotes/syncing-server@1.26.5) (2022-12-15)
### Bug Fixes
* **syncing-server:** revisions processing limit ([f10fa83](https://github.com/standardnotes/syncing-server-js/commit/f10fa839fbe1baec32fd234b41d8cd42fc50931a))
## [1.26.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.3...@standardnotes/syncing-server@1.26.4) (2022-12-15)
### Bug Fixes
* **syncing-server:** user uuid field name ([bfe6f42](https://github.com/standardnotes/syncing-server-js/commit/bfe6f4255a2d3f6e7dfa5eab1509dd770d9bff18))
## [1.26.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.2...@standardnotes/syncing-server@1.26.3) (2022-12-15)
### Bug Fixes
* **syncing-server:** select fields in query for revisions ([ce53c45](https://github.com/standardnotes/syncing-server-js/commit/ce53c459e6ad0d469fcd0ebd7bf4caeb0e1d9c9c))
## [1.26.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.1...@standardnotes/syncing-server@1.26.2) (2022-12-14)
### Bug Fixes
* **syncing-server:** revisions procedure logs ([1e2b496](https://github.com/standardnotes/syncing-server-js/commit/1e2b496f4f87fd49ae8fba8ed9b76d3b6a2c31fa))
## [1.26.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.26.0...@standardnotes/syncing-server@1.26.1) (2022-12-14)
### Bug Fixes
* **syncing-server:** revisions procedure logs ([22fba8b](https://github.com/standardnotes/syncing-server-js/commit/22fba8ba806115b0f4bb4b083ae8595a3f0010b0))
# [1.26.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.6...@standardnotes/syncing-server@1.26.0) (2022-12-14)
### Features
* **syncing-server:** change revisions procedure to pagination instead of streaming ([4b1fe3b](https://github.com/standardnotes/syncing-server-js/commit/4b1fe3ba91594858e15cbdfbc21062c428dd03b4))
## [1.25.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.5...@standardnotes/syncing-server@1.25.6) (2022-12-14)
### Bug Fixes
* **syncing-server:** additional stream events handling on revisions procedure ([2ec28e5](https://github.com/standardnotes/syncing-server-js/commit/2ec28e541efa2bd9172431d45c5c1560692a912c))
## [1.25.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.4...@standardnotes/syncing-server@1.25.5) (2022-12-14)
### Bug Fixes
* **syncing-server:** revisions procedure with env var defined ranges ([9b27547](https://github.com/standardnotes/syncing-server-js/commit/9b27547dae1e5d5e6d071a069803e2bf3f8acdda))
## [1.25.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.25.3...@standardnotes/syncing-server@1.25.4) (2022-12-13)
### Bug Fixes
* **syncing-server:** logs on revisions procedure ([225e0aa](https://github.com/standardnotes/syncing-server-js/commit/225e0aaf88a396bf308c2e5eed0bb6e130cb2d64))
## [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

@@ -10,51 +10,82 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
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,
revisionsProcessingLimit: number,
itemRepository: ItemRepositoryInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
logger: Logger,
): Promise<void> => {
const stream = await itemRepository.streamAll({
sortBy: 'updated_at_timestamp',
const createdAfter = new Date(`${year}-${month}-1`)
const createdBefore = new Date(`${month !== 12 ? year : year + 1}-${month !== 12 ? month + 1 : 1}-1`)
logger.info(`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processing items`)
const itemsCount = await itemRepository.countAll({
createdBetween: [createdAfter, createdBefore],
selectFields: ['uuid', 'user_uuid'],
contentType: [ContentType.Note, ContentType.File],
sortOrder: 'ASC',
createdBefore: new Date('2022-11-23'),
selectFields: ['user_uuid', 'item_uuid'],
sortBy: 'uuid',
})
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (rawItemData, _encoding, callback) => {
try {
if (!rawItemData.item_user_uuid || !rawItemData.item_uuid) {
logger.error('Could not process item %O', rawItemData)
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] There are ${itemsCount} items to process.`,
)
return callback()
}
const amountOfPages = Math.ceil(itemsCount / revisionsProcessingLimit)
const tenPercentOfPages = Math.ceil(amountOfPages / 10)
let itemsProcessedCounter = 0
let itemsSkippedCounter = 0
for (let page = 1; page <= amountOfPages; page++) {
if (page % tenPercentOfPages === 0) {
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processing page ${page}/${amountOfPages} of items.`,
)
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processed successfully/skipped items: ${itemsProcessedCounter}/${itemsSkippedCounter}.`,
)
}
await domainEventPublisher.publish(
domainEventFactory.createRevisionsOwnershipUpdateRequestedEvent({
userUuid: rawItemData.item_user_uuid,
itemUuid: rawItemData.item_uuid,
}),
)
} catch (error) {
logger.error(`Could not process item ${rawItemData.item_uuid}: ${(error as Error).message}`)
}
const items = await itemRepository.findAll({
createdBetween: [createdAfter, createdBefore],
selectFields: ['uuid', 'user_uuid'],
contentType: [ContentType.Note, ContentType.File],
offset: (page - 1) * revisionsProcessingLimit,
limit: revisionsProcessingLimit,
sortOrder: 'ASC',
sortBy: 'uuid',
})
callback()
},
if (items.length === 0) {
logger.warn(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] No items fetched for offset ${
(page - 1) * revisionsProcessingLimit
} and limit ${revisionsProcessingLimit}.`,
)
}
for (const item of items) {
if (!item.userUuid || !item.uuid) {
itemsSkippedCounter++
continue
}
await domainEventPublisher.publish(
domainEventFactory.createRevisionsOwnershipUpdateRequestedEvent({
userUuid: item.userUuid,
itemUuid: item.uuid,
}),
)
.on('finish', resolve)
.on('error', reject)
})
itemsProcessedCounter++
}
}
}
const container = new ContainerConfigLoader()
@@ -70,7 +101,28 @@ 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 = env.get('REVISION_YEARS').split(',')
const months = env.get('REVISION_MONTHS').split(',')
const revisionsProcessingLimit = env.get('REVISIONS_PROCESSING_LIMIT')
const promises = []
for (const year of years) {
for (const month of months) {
promises.push(
fixRevisionsOwnership(
+year,
+month,
+revisionsProcessingLimit,
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.26.7",
"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,10 +131,12 @@ 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}`))
queryBuilder.select(query.selectFields)
}
if (query.userUuid !== undefined) {
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid })
@@ -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)

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.6](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.5...@standardnotes/workspace-server@1.18.6) (2022-12-15)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.18.5](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.4...@standardnotes/workspace-server@1.18.5) (2022-12-15)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.18.4](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.3...@standardnotes/workspace-server@1.18.4) (2022-12-12)
**Note:** Version bump only for package @standardnotes/workspace-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/workspace-server",
"version": "1.18.4",
"version": "1.18.6",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1981,10 +1981,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@standardnotes/domain-core@workspace:packages/domain-core"
dependencies:
"@standardnotes/common": "workspace:*"
"@standardnotes/features": "npm:^1.52.1"
"@standardnotes/predicates": "workspace:*"
"@standardnotes/security": "workspace:*"
"@types/jest": "npm:^29.1.1"
"@types/uuid": "npm:^8.3.0"
"@typescript-eslint/eslint-plugin": "npm:^5.30.0"