mirror of
https://github.com/standardnotes/server
synced 2026-01-20 14:04:34 -05:00
Compare commits
48 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f20395ff3 | ||
|
|
bfe6f4255a | ||
|
|
b9032f3012 | ||
|
|
ce53c459e6 | ||
|
|
6df42fb0d5 | ||
|
|
1e2b496f4f | ||
|
|
528c1b0d57 | ||
|
|
22fba8ba80 | ||
|
|
6f26261ebe | ||
|
|
4b1fe3ba91 | ||
|
|
9f95262bd4 | ||
|
|
2ec28e541e | ||
|
|
4764d4b19a | ||
|
|
9b27547dae | ||
|
|
a96f2c9153 | ||
|
|
225e0aaf88 | ||
|
|
f0c85910bc | ||
|
|
124c443528 | ||
|
|
37c7f8d39f | ||
|
|
c419f1ce22 | ||
|
|
4949cdfe2f | ||
|
|
cd101b96ea | ||
|
|
40d0e4631f | ||
|
|
a55a995660 | ||
|
|
1d576d48ad | ||
|
|
4ff8030f87 | ||
|
|
c15e2e2c8f | ||
|
|
41d31a8d75 | ||
|
|
10e2a26352 | ||
|
|
6e547f77d0 | ||
|
|
530a426601 | ||
|
|
642d6bab77 | ||
|
|
7980af3d82 | ||
|
|
2980c42e88 | ||
|
|
b03994f9db | ||
|
|
41906ec2f9 | ||
|
|
4d1e7ff2a5 | ||
|
|
7f18fcfc13 | ||
|
|
ff02ce0747 | ||
|
|
a6056600eb | ||
|
|
24c94326d5 | ||
|
|
48c0cb5e62 | ||
|
|
9968efe1b2 | ||
|
|
6368342149 | ||
|
|
b5f73db210 | ||
|
|
22d6a02d04 | ||
|
|
4e0bcfcccf | ||
|
|
104313c15d |
@@ -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
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
|
||||
|
||||
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.22](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.21...@standardnotes/analytics@2.12.22) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** report event publishing ([a605660](https://github.com/standardnotes/server/commit/a6056600eb96bf175189ad6d62870c9d736f331b))
|
||||
|
||||
## [2.12.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.20...@standardnotes/analytics@2.12.21) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add debug logs for report ([48c0cb5](https://github.com/standardnotes/server/commit/48c0cb5e62dc8af930de191deaa1eb3ff6c5a29f))
|
||||
|
||||
## [2.12.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.19...@standardnotes/analytics@2.12.20) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -219,26 +219,27 @@ const requestReport = async (
|
||||
}
|
||||
|
||||
for (const adminEmail of adminEmails) {
|
||||
const event = domainEventFactory.createEmailRequestedEvent({
|
||||
messageIdentifier: 'VERSION_ADOPTION_REPORT',
|
||||
subject: getSubject(),
|
||||
body: getBody(
|
||||
{
|
||||
activityStatistics: yesterdayActivityStatistics,
|
||||
activityStatisticsOverTime: analyticsOverTime,
|
||||
statisticsOverTime,
|
||||
statisticMeasures,
|
||||
churn: {
|
||||
periodKeys: monthlyPeriodKeys,
|
||||
values: churnRates,
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailRequestedEvent({
|
||||
messageIdentifier: 'VERSION_ADOPTION_REPORT',
|
||||
subject: getSubject(),
|
||||
body: getBody(
|
||||
{
|
||||
activityStatistics: yesterdayActivityStatistics,
|
||||
activityStatisticsOverTime: analyticsOverTime,
|
||||
statisticsOverTime,
|
||||
statisticMeasures,
|
||||
churn: {
|
||||
periodKeys: monthlyPeriodKeys,
|
||||
values: churnRates,
|
||||
},
|
||||
},
|
||||
},
|
||||
timer,
|
||||
),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
userEmail: adminEmail,
|
||||
})
|
||||
await domainEventPublisher.publish(event)
|
||||
timer,
|
||||
),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
userEmail: adminEmail,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +261,9 @@ void container.load().then((container) => {
|
||||
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
||||
TYPES.CalculateMonthlyRecurringRevenue,
|
||||
)
|
||||
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
|
||||
|
||||
logger.info(`Sending report to following admins: ${adminEmails}`)
|
||||
|
||||
Promise.resolve(
|
||||
requestReport(
|
||||
@@ -270,7 +274,7 @@ void container.load().then((container) => {
|
||||
periodKeyGenerator,
|
||||
calculateMonthlyRecurringRevenue,
|
||||
timer,
|
||||
container.get(TYPES.ADMIN_EMAILS),
|
||||
adminEmails,
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
@@ -5,17 +5,17 @@ COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
echo "[Docker] Starting Worker..."
|
||||
yarn workspace @standardnotes/analytics worker
|
||||
;;
|
||||
|
||||
'report' )
|
||||
echo "Starting Usage Report Generation..."
|
||||
echo "[Docker] Starting Usage Report Generation..."
|
||||
yarn workspace @standardnotes/analytics report
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
echo "[Docker] Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.12.20",
|
||||
"version": "2.12.25",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -377,7 +377,7 @@ const getChartUrls = (
|
||||
}
|
||||
|
||||
export const html = (data: any, timer: TimerInterface) => {
|
||||
const chartUrls = getChartUrls(event)
|
||||
const chartUrls = getChartUrls(data)
|
||||
|
||||
const successfullPaymentsActivity = data.activityStatistics.find(
|
||||
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
|
||||
|
||||
@@ -11,6 +11,7 @@ WEB_SOCKET_SERVER_URL=http://websockets:3000
|
||||
PAYMENTS_SERVER_URL=http://payments:3000
|
||||
FILES_SERVER_URL=http://files:3000
|
||||
REVISIONS_SERVER_URL=http://revisions:3000
|
||||
EMAIL_SERVER_URL=http://email:3000
|
||||
|
||||
HTTP_CALL_TIMEOUT=60000
|
||||
|
||||
|
||||
@@ -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.40.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.1...@standardnotes/api-gateway@1.40.2) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.40.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.0...@standardnotes/api-gateway@1.40.1) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.40.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.24...@standardnotes/api-gateway@1.40.0) (2022-12-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **api-gateway:** add unsubscribe from emails endpoint ([22d6a02](https://github.com/standardnotes/api-gateway/commit/22d6a02d049ba3bde890c7def91e19f013ba3e22))
|
||||
|
||||
## [1.39.24](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.23...@standardnotes/api-gateway@1.39.24) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.39.24",
|
||||
"version": "1.40.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,7 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
|
||||
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
||||
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
|
||||
container.bind(TYPES.EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
|
||||
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
|
||||
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
|
||||
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
|
||||
@@ -8,6 +8,7 @@ const TYPES = {
|
||||
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
|
||||
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
|
||||
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
|
||||
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
|
||||
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
|
||||
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
|
||||
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
||||
|
||||
@@ -29,4 +29,14 @@ export class ActionsController extends BaseHttpController {
|
||||
async methods(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/unsubscribe/:token')
|
||||
async emailUnsubscribe(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callEmailServer(
|
||||
request,
|
||||
response,
|
||||
`subscriptions/actions/unsubscribe/${request.params.token}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
|
||||
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
|
||||
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
|
||||
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
|
||||
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
|
||||
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
@@ -65,6 +66,21 @@ export class HttpService implements HttpServiceInterface {
|
||||
await this.callServer(this.authServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callEmailServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
if (!this.emailServerUrl) {
|
||||
response.status(400).send({ message: 'Email Server not configured' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.callServer(this.emailServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callWorkspaceServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
callEmailServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
callAuthServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
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
|
||||
|
||||
## [1.66.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.7...@standardnotes/auth-server@1.66.8) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.66.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.6...@standardnotes/auth-server@1.66.7) (2022-12-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** linter issue ([104313c](https://github.com/standardnotes/server/commit/104313c15df79f6308d23e21f65111e5bd3d9c72))
|
||||
|
||||
## [1.66.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.5...@standardnotes/auth-server@1.66.6) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.66.6",
|
||||
"version": "1.67.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,7 @@ describe('CreateOfflineSubscriptionToken', () => {
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createEmailRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
|
||||
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
@@ -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.9.56](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.55...@standardnotes/domain-events-infra@1.9.56) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.55](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.54...@standardnotes/domain-events-infra@1.9.55) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.54](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.53...@standardnotes/domain-events-infra@1.9.54) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.9.54",
|
||||
"version": "1.9.56",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [2.104.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.0...@standardnotes/domain-events@2.104.1) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **domain-events:** add additional domain event services ([2980c42](https://github.com/standardnotes/server/commit/2980c42e88b6be5f065c91c86bf85a706975f801))
|
||||
|
||||
# [2.104.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.2...@standardnotes/domain-events@2.104.0) (2022-12-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add event for email subscription unsubscribed ([7f18fcf](https://github.com/standardnotes/server/commit/7f18fcfc139911620f2ea72729357aefd0613315))
|
||||
|
||||
## [2.103.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.1...@standardnotes/domain-events@2.103.2) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.103.2",
|
||||
"version": "2.104.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -10,4 +10,7 @@ export enum DomainEventService {
|
||||
Scheduler = 'scheduler',
|
||||
Workspace = 'workspace',
|
||||
Analytics = 'analytics',
|
||||
Revisions = 'revisions',
|
||||
Email = 'email',
|
||||
Settings = 'settings',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { EmailSubscriptionUnsubscribedEventPayload } from './EmailSubscriptionUnsubscribedEventPayload'
|
||||
|
||||
export interface EmailSubscriptionUnsubscribedEvent extends DomainEventInterface {
|
||||
type: 'EMAIL_SUBSCRIPTION_UNSUBSCRIBED'
|
||||
payload: EmailSubscriptionUnsubscribedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface EmailSubscriptionUnsubscribedEventPayload {
|
||||
userEmail: string
|
||||
level: string
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export * from './Event/EmailBackupRequestedEvent'
|
||||
export * from './Event/EmailBackupRequestedEventPayload'
|
||||
export * from './Event/EmailRequestedEvent'
|
||||
export * from './Event/EmailRequestedEventPayload'
|
||||
export * from './Event/EmailSubscriptionUnsubscribedEvent'
|
||||
export * from './Event/EmailSubscriptionUnsubscribedEventPayload'
|
||||
export * from './Event/ExitDiscountAppliedEvent'
|
||||
export * from './Event/ExitDiscountAppliedEventPayload'
|
||||
export * from './Event/ExitDiscountApplyRequestedEvent'
|
||||
|
||||
@@ -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.6.53](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.52...@standardnotes/event-store@1.6.53) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.52](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.51...@standardnotes/event-store@1.6.52) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.51](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.50...@standardnotes/event-store@1.6.51) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.6.51",
|
||||
"version": "1.6.53",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -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.8.52](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.51...@standardnotes/files-server@1.8.52) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.51](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.50...@standardnotes/files-server@1.8.51) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.50](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.49...@standardnotes/files-server@1.8.50) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.8.50",
|
||||
"version": "1.8.52",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.9.24](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.23...@standardnotes/revisions-server@1.9.24) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.9.23](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.22...@standardnotes/revisions-server@1.9.23) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.9.23",
|
||||
"version": "1.9.26",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Revision } from '../../../Domain/Revision/Revision'
|
||||
|
||||
export interface GetRevisionResponseBody {
|
||||
revision: Revision
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
|
||||
|
||||
export interface GetRevisionsMetadataResponseBody {
|
||||
revisions: Array<RevisionMetadata>
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.15.5](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.4...@standardnotes/scheduler-server@1.15.5) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.15.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.3...@standardnotes/scheduler-server@1.15.4) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.15.4",
|
||||
"version": "1.15.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.24.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.4...@standardnotes/syncing-server@1.24.5) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.24.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.3...@standardnotes/syncing-server@1.24.4) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** data integrity check on revisions fix ([6368342](https://github.com/standardnotes/syncing-server-js/commit/6368342149d658898aef62651bfafddf51c26dbe))
|
||||
|
||||
## [1.24.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.24.2...@standardnotes/syncing-server@1.24.3) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -10,45 +10,69 @@ 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,
|
||||
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(`Processing items between ${createdAfter.toISOString()} and ${createdBefore.toISOString()}`)
|
||||
|
||||
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 {
|
||||
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}`)
|
||||
}
|
||||
logger.info(`There are ${itemsCount} items between ${createdAfter.toISOString()} and ${createdBefore.toISOString()}`)
|
||||
|
||||
callback()
|
||||
},
|
||||
const limit = 500
|
||||
const amountOfPages = Math.ceil(itemsCount / limit)
|
||||
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(
|
||||
`Processing page ${page} of ${amountOfPages} items between ${createdAfter.toISOString()} and ${createdBefore.toISOString()}. Processed successfully ${itemsProcessedCounter} items. Skipped ${itemsSkippedCounter} items.`,
|
||||
)
|
||||
}
|
||||
|
||||
const items = await itemRepository.findAll({
|
||||
createdBetween: [createdAfter, createdBefore],
|
||||
selectFields: ['uuid', 'user_uuid'],
|
||||
contentType: [ContentType.Note, ContentType.File],
|
||||
offset: (page - 1) * limit,
|
||||
limit,
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'uuid',
|
||||
})
|
||||
|
||||
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()
|
||||
@@ -64,7 +88,19 @@ 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 promises = []
|
||||
for (const year of years) {
|
||||
for (const month of months) {
|
||||
promises.push(
|
||||
fixRevisionsOwnership(+year, +month, itemRepository, domainEventFactory, domainEventPublisher, logger),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
logger.info('revisions ownership fix complete.')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.24.3",
|
||||
"version": "1.26.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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.4.53](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.52...@standardnotes/websockets-server@1.4.53) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.52](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.51...@standardnotes/websockets-server@1.4.52) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.51](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.50...@standardnotes/websockets-server@1.4.51) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.4.51",
|
||||
"version": "1.4.53",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.18.3](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.2...@standardnotes/workspace-server@1.18.3) (2022-12-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.18.2](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.1...@standardnotes/workspace-server@1.18.2) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user