Compare commits

..

46 Commits

Author SHA1 Message Date
standardci
fdf8809e13 chore(release): publish new version
- @standardnotes/auth-server@1.36.0
2022-10-03 08:33:51 +00:00
Karol Sójko
6a9d479f71 feat(auth): disallow v1 sign in for users with 004 protocol version 2022-10-03 10:31:58 +02:00
standardci
82c9637f37 chore(release): publish new version
- @standardnotes/api-gateway@1.23.0
2022-09-30 12:02:50 +00:00
Karol Sójko
dfab849f48 feat(api-gateway): add churn metrics to the report 2022-09-30 14:01:15 +02:00
standardci
ad60b95537 chore(release): publish new version
- @standardnotes/analytics@1.32.0
 - @standardnotes/api-gateway@1.22.6
 - @standardnotes/auth-server@1.35.0
 - @standardnotes/syncing-server@1.8.15
2022-09-30 11:49:00 +00:00
Karol Sójko
8a98f746eb feat(auth): add tracking total customers count 2022-09-30 13:47:33 +02:00
standardci
27cfd0ccf6 chore(release): publish new version
- @standardnotes/analytics@1.31.1
 - @standardnotes/api-gateway@1.22.5
 - @standardnotes/auth-server@1.34.1
 - @standardnotes/syncing-server@1.8.14
2022-09-30 09:24:21 +00:00
Karol Sójko
82bb85174d fix(auth): fix calculating new and existing customers churn 2022-09-30 11:22:46 +02:00
standardci
8ceef4acbf chore(release): publish new version
- @standardnotes/analytics@1.31.0
 - @standardnotes/api-gateway@1.22.4
 - @standardnotes/auth-server@1.34.0
 - @standardnotes/syncing-server@1.8.13
2022-09-30 08:39:59 +00:00
Karol Sójko
b6118c17e1 feat(auth): add measuring new customers 2022-09-30 10:38:27 +02:00
standardci
a7fb622e69 chore(release): publish new version
- @standardnotes/analytics@1.30.0
 - @standardnotes/api-gateway@1.22.3
 - @standardnotes/auth-server@1.33.0
 - @standardnotes/syncing-server@1.8.12
2022-09-30 08:30:22 +00:00
Karol Sójko
39337c1c4f feat(auth): add tracking churn activity 2022-09-30 10:28:37 +02:00
standardci
1f970aaf69 chore(release): publish new version
- @standardnotes/auth-server@1.32.13
2022-09-29 12:19:46 +00:00
Karol Sójko
0a5b7e13cd fix(auth): finding previous subscription setting for irreplacable subscription settings 2022-09-29 14:18:16 +02:00
standardci
1ce2b9eb44 chore(release): publish new version
- @standardnotes/auth-server@1.32.12
2022-09-29 11:15:07 +00:00
Karol Sójko
477f146725 fix(auth): reassign not replaceable subscription settings 2022-09-29 13:13:39 +02:00
standardci
d7b02c4da9 chore(release): publish new version
- @standardnotes/auth-server@1.32.11
2022-09-28 13:36:17 +00:00
Karol Sójko
40e673379b fix(auth): prevent replacing files bytes used subscription setting upon renewal 2022-09-28 15:34:43 +02:00
standardci
6ce9a4e834 chore(release): publish new version
- @standardnotes/auth-server@1.32.10
2022-09-28 12:05:17 +00:00
Karol Sójko
c5a07a888a fix(auth): exclude legacy 5 year plans from subscription length statistics 2022-09-28 14:03:49 +02:00
standardci
55587f6207 chore(release): publish new version
- @standardnotes/api-gateway@1.22.2
 - @standardnotes/auth-server@1.32.9
 - @standardnotes/domain-events-infra@1.8.13
 - @standardnotes/domain-events@2.60.7
 - @standardnotes/event-store@1.3.18
 - @standardnotes/files-server@1.6.4
 - @standardnotes/scheduler-server@1.10.32
 - @standardnotes/syncing-server@1.8.11
2022-09-28 11:33:44 +00:00
Karol Sójko
0d6b45c795 chore(deps): upgrade @standardnotes/features 2022-09-28 13:31:15 +02:00
standardci
95f64d9952 chore(release): publish new version
- @standardnotes/auth-server@1.32.8
2022-09-27 13:22:13 +00:00
Karol Sójko
54da5def4b fix(auth): ttl for lock counter on login lockout 2022-09-27 15:20:42 +02:00
standardci
d2fc1e057d chore(release): publish new version
- @standardnotes/api-gateway@1.22.1
2022-09-27 10:35:15 +00:00
Karol Sójko
0a90d98c71 fix(api-gateway): remove admin graphql endpoint from being publicly available 2022-09-27 12:33:29 +02:00
standardci
cc269e3b35 chore(release): publish new version
- @standardnotes/auth-server@1.32.7
2022-09-27 08:29:51 +00:00
Karol Sójko
b19093179b fix(auth): allow resending canceled subscription invites 2022-09-27 10:28:13 +02:00
standardci
e2cc0bc003 chore(release): publish new version
- @standardnotes/auth-server@1.32.6
2022-09-22 18:50:24 +00:00
Karol Sójko
644c52ae36 Revert "fix(auth): subscription token ttl"
This reverts commit 6efd336f34.
2022-09-22 20:48:51 +02:00
Karol Sójko
2554273a3f Revert "fix(auth): increase subscription token ttl"
This reverts commit 07def20f6b.
2022-09-22 20:48:51 +02:00
Karol Sójko
a8ee149d7a Revert "tmp(auth): disable expiring of subscription tokens"
This reverts commit 053092031c.
2022-09-22 20:48:51 +02:00
standardci
dcf92d58f9 chore(release): publish new version
- @standardnotes/auth-server@1.32.5
2022-09-22 18:00:23 +00:00
Karol Sójko
053092031c tmp(auth): disable expiring of subscription tokens 2022-09-22 19:58:35 +02:00
standardci
c12e3eb3ec chore(release): publish new version
- @standardnotes/auth-server@1.32.4
2022-09-22 15:30:14 +00:00
Karol Sójko
07def20f6b fix(auth): increase subscription token ttl 2022-09-22 17:28:28 +02:00
standardci
6c2cca66bd chore(release): publish new version
- @standardnotes/auth-server@1.32.3
2022-09-22 14:26:30 +00:00
Karol Sójko
6efd336f34 fix(auth): subscription token ttl 2022-09-22 16:24:33 +02:00
standardci
81eb4be200 chore(release): publish new version
- @standardnotes/auth-server@1.32.2
2022-09-22 13:48:33 +00:00
Karol Sójko
76cee6dbad fix(auth): add throwing an error if the subscription token was not persisted 2022-09-22 15:46:23 +02:00
standardci
dcc35a5738 chore(release): publish new version
- @standardnotes/syncing-server@1.8.10
2022-09-22 12:38:18 +00:00
Karol Sójko
5628de6445 fix(syncing-server-js): binding of sync limit 2022-09-22 14:36:47 +02:00
standardci
53bea47727 chore(release): publish new version
- @standardnotes/auth-server@1.32.1
2022-09-22 12:36:39 +00:00
Karol Sójko
d6cf8d400a fix(auth): settings and subscription settings projection 2022-09-22 14:34:56 +02:00
standardci
b58cc335f2 chore(release): publish new version
- @standardnotes/syncing-server@1.8.9
2022-09-22 11:56:22 +00:00
Karol Sójko
03d1bc611c fix(syncing-server): introduce upper bound for sync items limit as an env var 2022-09-22 13:54:26 +02:00
82 changed files with 925 additions and 159 deletions

60
.pnp.cjs generated
View File

@@ -2484,14 +2484,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/api", [\
["npm:1.8.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.8.1-15c2e051d4-76c5d1a2d2.zip/node_modules/@standardnotes/api/",\
["npm:1.9.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.9.0-507434ff00-cc3feac393.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.8.1"],\
["@standardnotes/api", "npm:1.9.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/encryption", "npm:1.15.3"],\
["@standardnotes/models", "npm:1.18.3"],\
["@standardnotes/responses", "npm:1.10.2"],\
["@standardnotes/encryption", "npm:1.15.9"],\
["@standardnotes/models", "npm:1.22.0"],\
["@standardnotes/responses", "npm:1.10.3"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
@@ -2563,11 +2563,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
["@sentry/node", "npm:7.5.0"],\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@standardnotes/api", "npm:1.8.1"],\
["@standardnotes/api", "npm:1.9.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/features", "npm:1.50.0"],\
["@standardnotes/features", "npm:1.52.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -2651,7 +2651,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"packageDependencies": [\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.50.0"],\
["@standardnotes/features", "npm:1.52.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/security", "workspace:packages/security"],\
["@types/jest", "npm:28.1.4"],\
@@ -2688,14 +2688,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/encryption", [\
["npm:1.15.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.3-3580c52c1f-1a7863299f.zip/node_modules/@standardnotes/encryption/",\
["npm:1.15.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.9-00c7fac9f6-7595ac08ce.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.15.3"],\
["@standardnotes/encryption", "npm:1.15.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.18.3"],\
["@standardnotes/responses", "npm:1.10.2"],\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["@standardnotes/models", "npm:1.22.0"],\
["@standardnotes/responses", "npm:1.10.3"],\
["@standardnotes/sncrypto-common", "npm:1.12.0"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
@@ -2743,10 +2743,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "HARD"\
}],\
["npm:1.52.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.0-8c1adf7881-3e6014272f.zip/node_modules/@standardnotes/features/",\
["npm:1.52.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.1-1fee85cf4e-ff3684399e.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/features", "npm:1.52.1"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -2808,13 +2808,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/models", [\
["npm:1.18.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.3-6c65a62f30-21830c805f.zip/node_modules/@standardnotes/models/",\
["npm:1.22.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.22.0-2cc72f987b-9928246368.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.18.3"],\
["@standardnotes/models", "npm:1.22.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/responses", "npm:1.10.2"],\
["@standardnotes/features", "npm:1.52.1"],\
["@standardnotes/responses", "npm:1.10.3"],\
["@standardnotes/utils", "npm:1.9.0"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
@@ -2851,12 +2851,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/responses", [\
["npm:1.10.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.2-39d2d1f9b5-364724b5c7.zip/node_modules/@standardnotes/responses/",\
["npm:1.10.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.3-7cdb15f83a-4a1e31eb89.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.10.2"],\
["@standardnotes/responses", "npm:1.10.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/features", "npm:1.52.1"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
@@ -2967,10 +2967,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/sncrypto-common", [\
["npm:1.11.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.11.1-58d12d6912-69d698abb7.zip/node_modules/@standardnotes/sncrypto-common/",\
["npm:1.12.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.12.0-1a093ff006-b89a14bd23.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["@standardnotes/sncrypto-common", "npm:1.12.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.31.1...@standardnotes/analytics@1.32.0) (2022-09-30)
### Features
* **auth:** add tracking total customers count ([8a98f74](https://github.com/standardnotes/server/commit/8a98f746eb13c25f7940286aca594e2304232bdf))
## [1.31.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.31.0...@standardnotes/analytics@1.31.1) (2022-09-30)
### Bug Fixes
* **auth:** fix calculating new and existing customers churn ([82bb851](https://github.com/standardnotes/server/commit/82bb85174d94a5e03f364604a1c07a9b1633920d))
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.30.0...@standardnotes/analytics@1.31.0) (2022-09-30)
### Features
* **auth:** add measuring new customers ([b6118c1](https://github.com/standardnotes/server/commit/b6118c17e176ba0acc93b95a38e32748ac851410))
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.29.1...@standardnotes/analytics@1.30.0) (2022-09-30)
### Features
* **auth:** add tracking churn activity ([39337c1](https://github.com/standardnotes/server/commit/39337c1c4f799f39672eeb8c9d050e7cbb19878a))
## [1.29.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.29.0...@standardnotes/analytics@1.29.1) (2022-09-16)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.29.1",
"version": "1.32.0",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -11,9 +11,12 @@ export enum AnalyticsActivity {
SubscriptionRenewed = 'subscription-renewed',
SubscriptionRefunded = 'subscription-refunded',
SubscriptionCancelled = 'subscription-cancelled',
SubscriptionExpired = 'subscription-expired',
EmailUnbackedUpData = 'email-unbacked-up-data',
EmailBackup = 'email-backup',
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
PaymentFailed = 'payment-failed',
PaymentSuccess = 'payment-success',
NewCustomersChurn = 'new-customers-churn',
ExistingCustomersChurn = 'existing-customers-churn',
}

View File

@@ -8,4 +8,6 @@ export enum StatisticsMeasure {
NotesCountFreeUsers = 'notes-count-free-users',
NotesCountPaidUsers = 'notes-count-paid-users',
FilesCount = 'files-count',
NewCustomers = 'new-customers',
TotalCustomers = 'total-customers',
}

View File

@@ -9,6 +9,7 @@ export interface StatisticsStoreInterface {
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
getYesterdayOutOfSyncIncidents(): Promise<number>
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
getMeasureTotal(measure: StatisticsMeasure, period: Period): Promise<number>
}

View File

@@ -7,6 +7,7 @@ export enum Period {
WeekBeforeLastWeek,
ThisMonth,
LastMonth,
ThisYear,
Last30Days,
Last7Days,
Q1ThisYear,

View File

@@ -49,11 +49,19 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
return this.getMonthlyKey()
case Period.LastMonth:
return this.getMonthlyKey(this.getLastMonthDate())
case Period.ThisYear:
return this.getYearlyKey()
default:
throw new Error(`Unsuporrted period: ${period}`)
}
}
private getYearlyKey(date?: Date): string {
date = date ?? new Date()
return this.getYear(date)
}
private getMonthlyKey(date?: Date): string {
date = date ?? new Date()

View File

@@ -16,6 +16,7 @@ describe('RedisStatisticsStore', () => {
pipeline = {} as jest.Mocked<IORedis.Pipeline>
pipeline.incr = jest.fn()
pipeline.incrbyfloat = jest.fn()
pipeline.set = jest.fn()
pipeline.setbit = jest.fn()
pipeline.exec = jest.fn()
@@ -92,6 +93,13 @@ describe('RedisStatisticsStore', () => {
expect(pipeline.exec).toHaveBeenCalled()
})
it('should set a value to a measure', async () => {
await createStore().setMeasure(StatisticsMeasure.Income, 2, [Period.Today, Period.ThisMonth])
expect(pipeline.set).toHaveBeenCalledTimes(2)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should increment measure by a value', async () => {
await createStore().incrementMeasure(StatisticsMeasure.Income, 2, [Period.Today, Period.ThisMonth])

View File

@@ -8,6 +8,16 @@ import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStor
export class RedisStatisticsStore implements StatisticsStoreInterface {
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
async setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
const pipeline = this.redisClient.pipeline()
for (const period of periods) {
pipeline.set(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value)
}
await pipeline.exec()
}
async getMeasureTotal(measure: StatisticsMeasure, period: Period): Promise<number> {
const totalValue = await this.redisClient.get(
`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.23.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.6...@standardnotes/api-gateway@1.23.0) (2022-09-30)
### Features
* **api-gateway:** add churn metrics to the report ([dfab849](https://github.com/standardnotes/api-gateway/commit/dfab849f48ab782c3cd2e97f52fdb72b7143002f))
## [1.22.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.5...@standardnotes/api-gateway@1.22.6) (2022-09-30)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.22.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.4...@standardnotes/api-gateway@1.22.5) (2022-09-30)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.22.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.3...@standardnotes/api-gateway@1.22.4) (2022-09-30)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.22.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.2...@standardnotes/api-gateway@1.22.3) (2022-09-30)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.22.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.1...@standardnotes/api-gateway@1.22.2) (2022-09-28)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.22.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.0...@standardnotes/api-gateway@1.22.1) (2022-09-27)
### Bug Fixes
* **api-gateway:** remove admin graphql endpoint from being publicly available ([0a90d98](https://github.com/standardnotes/api-gateway/commit/0a90d98c71c6023b700f852c91aedfe1ad23af55))
# [1.22.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.21.1...@standardnotes/api-gateway@1.22.0) (2022-09-22)
### Features

View File

@@ -38,6 +38,8 @@ const requestReport = async (
AnalyticsActivity.DeleteAccount,
AnalyticsActivity.SubscriptionCancelled,
AnalyticsActivity.SubscriptionRefunded,
AnalyticsActivity.ExistingCustomersChurn,
AnalyticsActivity.NewCustomersChurn,
]
for (const analyticsName of thirtyDaysAnalyticsNames) {
@@ -74,6 +76,8 @@ const requestReport = async (
AnalyticsActivity.GeneralActivityPaidUsers,
AnalyticsActivity.PaymentFailed,
AnalyticsActivity.PaymentSuccess,
AnalyticsActivity.NewCustomersChurn,
AnalyticsActivity.ExistingCustomersChurn,
]
for (const activityName of yesterdayActivityNames) {
@@ -98,6 +102,8 @@ const requestReport = async (
StatisticsMeasure.NotesCountFreeUsers,
StatisticsMeasure.NotesCountPaidUsers,
StatisticsMeasure.FilesCount,
StatisticsMeasure.NewCustomers,
StatisticsMeasure.TotalCustomers,
]
const statisticMeasures = []
for (const statisticMeasureName of statisticMeasureNames) {

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.22.0",
"version": "1.23.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -70,11 +70,6 @@ export class PaymentsController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'admin/events/registration', request.body)
}
@httpPost('/admin/graphql')
async adminGraphql(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'admin/graphql', request.body)
}
@httpPost('/admin/auth/login')
async adminLogin(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'admin/auth/login', request.body)

View File

@@ -3,6 +3,112 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.36.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.35.0...@standardnotes/auth-server@1.36.0) (2022-10-03)
### Features
* **auth:** disallow v1 sign in for users with 004 protocol version ([6a9d479](https://github.com/standardnotes/server/commit/6a9d479f7173268bc0c79b1c7583021989be783a))
# [1.35.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.34.1...@standardnotes/auth-server@1.35.0) (2022-09-30)
### Features
* **auth:** add tracking total customers count ([8a98f74](https://github.com/standardnotes/server/commit/8a98f746eb13c25f7940286aca594e2304232bdf))
## [1.34.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.34.0...@standardnotes/auth-server@1.34.1) (2022-09-30)
### Bug Fixes
* **auth:** fix calculating new and existing customers churn ([82bb851](https://github.com/standardnotes/server/commit/82bb85174d94a5e03f364604a1c07a9b1633920d))
# [1.34.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.33.0...@standardnotes/auth-server@1.34.0) (2022-09-30)
### Features
* **auth:** add measuring new customers ([b6118c1](https://github.com/standardnotes/server/commit/b6118c17e176ba0acc93b95a38e32748ac851410))
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.13...@standardnotes/auth-server@1.33.0) (2022-09-30)
### Features
* **auth:** add tracking churn activity ([39337c1](https://github.com/standardnotes/server/commit/39337c1c4f799f39672eeb8c9d050e7cbb19878a))
## [1.32.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.12...@standardnotes/auth-server@1.32.13) (2022-09-29)
### Bug Fixes
* **auth:** finding previous subscription setting for irreplacable subscription settings ([0a5b7e1](https://github.com/standardnotes/server/commit/0a5b7e13cd51ddbad40f67d629b0daf50b176fac))
## [1.32.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.11...@standardnotes/auth-server@1.32.12) (2022-09-29)
### Bug Fixes
* **auth:** reassign not replaceable subscription settings ([477f146](https://github.com/standardnotes/server/commit/477f146725c8e83b86a8224708046d0fd86bfa0b))
## [1.32.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.10...@standardnotes/auth-server@1.32.11) (2022-09-28)
### Bug Fixes
* **auth:** prevent replacing files bytes used subscription setting upon renewal ([40e6733](https://github.com/standardnotes/server/commit/40e673379bb84bd21bcc8dbcb1aa36caaa2adbf8))
## [1.32.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.9...@standardnotes/auth-server@1.32.10) (2022-09-28)
### Bug Fixes
* **auth:** exclude legacy 5 year plans from subscription length statistics ([c5a07a8](https://github.com/standardnotes/server/commit/c5a07a888aadc22f62a92a236977c266f8d8e1c0))
## [1.32.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.8...@standardnotes/auth-server@1.32.9) (2022-09-28)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.32.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.7...@standardnotes/auth-server@1.32.8) (2022-09-27)
### Bug Fixes
* **auth:** ttl for lock counter on login lockout ([54da5de](https://github.com/standardnotes/server/commit/54da5def4bbfbb4f74cbf02ae23e45103d250dd9))
## [1.32.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.6...@standardnotes/auth-server@1.32.7) (2022-09-27)
### Bug Fixes
* **auth:** allow resending canceled subscription invites ([b190931](https://github.com/standardnotes/server/commit/b19093179baaa1fb8cdf3f9d9bee20e625ed0b9b))
## [1.32.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.5...@standardnotes/auth-server@1.32.6) (2022-09-22)
### Reverts
* Revert "fix(auth): subscription token ttl" ([644c52a](https://github.com/standardnotes/server/commit/644c52ae36d3720dee0712e2cb826c7e617ab7b7))
* Revert "fix(auth): increase subscription token ttl" ([2554273](https://github.com/standardnotes/server/commit/2554273a3f85a968fed4286d109bed5413ef9908))
* Revert "tmp(auth): disable expiring of subscription tokens" ([a8ee149](https://github.com/standardnotes/server/commit/a8ee149d7ac78775bf447ab924458b116414a15e))
## [1.32.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.4...@standardnotes/auth-server@1.32.5) (2022-09-22)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.32.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.3...@standardnotes/auth-server@1.32.4) (2022-09-22)
### Bug Fixes
* **auth:** increase subscription token ttl ([07def20](https://github.com/standardnotes/server/commit/07def20f6b47f9d1c678cfe5206b924dd5e6014a))
## [1.32.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.2...@standardnotes/auth-server@1.32.3) (2022-09-22)
### Bug Fixes
* **auth:** subscription token ttl ([6efd336](https://github.com/standardnotes/server/commit/6efd336f3407e7204a0c5d385ea9df5c02c7e5f5))
## [1.32.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.1...@standardnotes/auth-server@1.32.2) (2022-09-22)
### Bug Fixes
* **auth:** add throwing an error if the subscription token was not persisted ([76cee6d](https://github.com/standardnotes/server/commit/76cee6dbad9bff041d8d5a1d4435046509c14f71))
## [1.32.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.0...@standardnotes/auth-server@1.32.1) (2022-09-22)
### Bug Fixes
* **auth:** settings and subscription settings projection ([d6cf8d4](https://github.com/standardnotes/server/commit/d6cf8d400a0177ee9030a171cf2ca47ade293fd9))
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.2...@standardnotes/auth-server@1.32.0) (2022-09-22)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.32.0",
"version": "1.36.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -34,11 +34,11 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.3.0",
"@standardnotes/analytics": "workspace:*",
"@standardnotes/api": "^1.8.1",
"@standardnotes/api": "^1.9.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/features": "^1.47.0",
"@standardnotes/features": "^1.52.1",
"@standardnotes/predicates": "workspace:*",
"@standardnotes/responses": "^1.6.39",
"@standardnotes/security": "workspace:*",

View File

@@ -87,6 +87,20 @@ describe('SubscriptionCancelledEventHandler', () => {
])
})
it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => {
event.payload.timestamp = 1642395451516000
const userSubscription = {
createdAt: 1642395451515000,
endsAt: 1642395451515000 + 126_230_400_000_001,
} as jest.Mocked<UserSubscription>
userSubscriptionRepository.findBySubscriptionId = jest.fn().mockReturnValue([userSubscription])
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
})
it('should update subscription cancelled - user not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)

View File

@@ -35,31 +35,10 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
Period.ThisWeek,
Period.ThisMonth,
])
const subscriptions = await this.userSubscriptionRepository.findBySubscriptionId(event.payload.subscriptionId)
if (subscriptions.length !== 0) {
const lastSubscription = subscriptions.shift() as UserSubscription
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
await this.trackSubscriptionStatistics(event)
if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
@@ -76,4 +55,39 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
private async updateOfflineSubscriptionCancelled(subscriptionId: number, timestamp: number): Promise<void> {
await this.offlineUserSubscriptionRepository.updateCancelled(subscriptionId, true, timestamp)
}
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
const subscriptions = await this.userSubscriptionRepository.findBySubscriptionId(event.payload.subscriptionId)
if (subscriptions.length !== 0) {
const lastSubscription = subscriptions.shift() as UserSubscription
if (this.isLegacy5yearSubscriptionPlan(lastSubscription)) {
return
}
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
private isLegacy5yearSubscriptionPlan(subscription: UserSubscription) {
const fourYearsInMicroseconds = 126_230_400_000_000
return subscription.endsAt - subscription.createdAt > fourYearsInMicroseconds
}
}

View File

@@ -13,6 +13,8 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { AnalyticsStoreInterface, StatisticsStoreInterface } from '@standardnotes/analytics'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
describe('SubscriptionExpiredEventHandler', () => {
let userRepository: UserRepositoryInterface
@@ -23,6 +25,9 @@ describe('SubscriptionExpiredEventHandler', () => {
let user: User
let event: SubscriptionExpiredEvent
let timestamp: number
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
const createHandler = () =>
new SubscriptionExpiredEventHandler(
@@ -30,6 +35,9 @@ describe('SubscriptionExpiredEventHandler', () => {
userSubscriptionRepository,
offlineUserSubscriptionRepository,
roleService,
getUserAnalyticsId,
analyticsStore,
statisticsStore,
logger,
)
@@ -50,6 +58,7 @@ describe('SubscriptionExpiredEventHandler', () => {
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.updateEndsAt = jest.fn()
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
userSubscriptionRepository.findBySubscriptionId = jest
.fn()
.mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
@@ -72,6 +81,15 @@ describe('SubscriptionExpiredEventHandler', () => {
offline: false,
}
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.setMeasure = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
logger.warn = jest.fn()

View File

@@ -8,6 +8,14 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import {
AnalyticsStoreInterface,
AnalyticsActivity,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@injectable()
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
@@ -17,6 +25,9 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
@inject(TYPES.OfflineUserSubscriptionRepository)
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -36,6 +47,21 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
await this.updateSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
await this.removeRoleFromSubscriptionUsers(event.payload.subscriptionId, event.payload.subscriptionName)
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
await this.analyticsStore.markActivity(
[AnalyticsActivity.SubscriptionExpired, AnalyticsActivity.ExistingCustomersChurn],
analyticsId,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
}
private async removeRoleFromSubscriptionUsers(

View File

@@ -73,12 +73,14 @@ describe('SubscriptionPurchasedEventHandler', () => {
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
statisticsStore.setMeasure = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
@@ -113,6 +115,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
analyticsStore.unmarkActivity = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
@@ -132,6 +135,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
subscription,
SubscriptionName.ProPlan,
'123',
)
})

View File

@@ -76,6 +76,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
event.payload.subscriptionName,
user.uuid,
)
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
@@ -84,6 +85,11 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
Period.ThisWeek,
Period.ThisMonth,
])
await this.analyticsStore.unmarkActivity(
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
analyticsId,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
const limitedDiscountPurchased = ['limited-10', 'limited-20'].includes(event.payload.discountCode as string)
if (limitedDiscountPurchased) {
@@ -98,6 +104,19 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
event.payload.timestamp - this.timer.convertDateToMicroseconds(user.createdAt),
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
}
}

View File

@@ -94,6 +94,7 @@ describe('SubscriptionReassignedEventHandler', () => {
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
subscription,
SubscriptionName.ProPlan,
'123',
)
})

View File

@@ -58,6 +58,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
event.payload.subscriptionName,
user.uuid,
)
}

View File

@@ -14,7 +14,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
import { AnalyticsActivity, AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
describe('SubscriptionRefundedEventHandler', () => {
let userRepository: UserRepositoryInterface
@@ -27,6 +27,7 @@ describe('SubscriptionRefundedEventHandler', () => {
let timestamp: number
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
const createHandler = () =>
new SubscriptionRefundedEventHandler(
@@ -36,6 +37,7 @@ describe('SubscriptionRefundedEventHandler', () => {
roleService,
getUserAnalyticsId,
analyticsStore,
statisticsStore,
logger,
)
@@ -56,6 +58,8 @@ describe('SubscriptionRefundedEventHandler', () => {
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.updateEndsAt = jest.fn()
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1)
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
userSubscriptionRepository.findBySubscriptionId = jest
.fn()
.mockReturnValue([{ user: Promise.resolve(user) } as jest.Mocked<UserSubscription>])
@@ -83,6 +87,10 @@ describe('SubscriptionRefundedEventHandler', () => {
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.setMeasure = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
@@ -119,4 +127,33 @@ describe('SubscriptionRefundedEventHandler', () => {
expect(roleService.removeUserRole).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
})
it('should mark churn for new customer', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [
Period.ThisMonth,
])
})
it('should mark churn for existing customer', async () => {
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(3)
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
Period.ThisMonth,
])
})
it('should not mark churn if customer did not purchase subscription in defined analytic periods', async () => {
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(3)
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
Period.ThisMonth,
])
})
})

View File

@@ -1,4 +1,4 @@
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionName, Uuid } from '@standardnotes/common'
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -8,7 +8,13 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@injectable()
@@ -21,6 +27,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -47,6 +54,8 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
Period.ThisWeek,
Period.ThisMonth,
])
await this.markChurnActivity(analyticsId, user.uuid)
}
private async removeRoleFromSubscriptionUsers(
@@ -66,4 +75,30 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
private async updateOfflineSubscriptionEndsAt(subscriptionId: number, timestamp: number): Promise<void> {
await this.offlineUserSubscriptionRepository.updateEndsAt(subscriptionId, timestamp, timestamp)
}
private async markChurnActivity(analyticsId: number, userUuid: Uuid): Promise<void> {
const existingSubscriptionsCount = await this.userSubscriptionRepository.countByUserUuid(userUuid)
const churnActivity =
existingSubscriptionsCount > 1 ? AnalyticsActivity.ExistingCustomersChurn : AnalyticsActivity.NewCustomersChurn
for (const period of [Period.ThisMonth, Period.ThisWeek, Period.Today]) {
const customerPurchasedInPeriod = await this.analyticsStore.wasActivityDone(
AnalyticsActivity.SubscriptionPurchased,
analyticsId,
period,
)
if (customerPurchasedInPeriod) {
await this.analyticsStore.markActivity([churnActivity], analyticsId, [period])
}
}
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
}
}

View File

@@ -94,6 +94,7 @@ describe('SubscriptionRenewedEventHandler', () => {
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
analyticsStore.unmarkActivity = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()

View File

@@ -66,6 +66,11 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
Period.ThisWeek,
Period.ThisMonth,
])
await this.analyticsStore.unmarkActivity(
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
analyticsId,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
private async addRoleToSubscriptionUsers(subscriptionId: number, subscriptionName: SubscriptionName): Promise<void> {

View File

@@ -130,6 +130,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
subscription,
SubscriptionName.ProPlan,
'123',
)
expect(settingService.createOrReplace).toHaveBeenCalledWith({

View File

@@ -89,6 +89,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
event.payload.subscriptionName,
user.uuid,
)
await this.settingService.createOrReplace({

View File

@@ -4,4 +4,5 @@ export type SettingDescription = {
value: string
sensitive: boolean
serverEncryptionVersion: EncryptionVersion
replaceable: boolean
}

View File

@@ -52,6 +52,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteSignInEmailsOption.NotMuted,
replaceable: false,
},
],
[
@@ -60,6 +61,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteMarketingEmailsOption.NotMuted,
replaceable: false,
},
],
[
@@ -68,6 +70,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Enabled,
replaceable: false,
},
],
])
@@ -79,6 +82,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: LogSessionUserAgentOption.Disabled,
replaceable: false,
},
],
])

View File

@@ -13,6 +13,7 @@ import { SubscriptionName } from '@standardnotes/common'
import { User } from '../User/User'
import { SettingFactoryInterface } from './SettingFactoryInterface'
import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
describe('SubscriptionSettingService', () => {
let setting: SubscriptionSetting
@@ -22,6 +23,7 @@ describe('SubscriptionSettingService', () => {
let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface
let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface
let settingDecrypter: SettingDecrypterInterface
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
let logger: Logger
const createService = () =>
@@ -30,6 +32,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingRepository,
subscriptionSettingsAssociationService,
settingDecrypter,
userSubscriptionRepository,
logger,
)
@@ -51,6 +54,16 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
subscriptionSettingRepository.save = jest.fn().mockImplementation((setting) => setting)
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
{
uuid: 's-1-2-3',
} as jest.Mocked<UserSubscription>,
{
uuid: 's-2-3-4',
} as jest.Mocked<UserSubscription>,
])
subscriptionSettingsAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
@@ -60,6 +73,7 @@ describe('SubscriptionSettingService', () => {
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: true,
},
],
]),
@@ -75,7 +89,91 @@ describe('SubscriptionSettingService', () => {
})
it('should create default settings for a subscription', async () => {
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan)
await createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
)
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
})
it('should reassign existing default settings for a subscription if it is not replaceable', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: false,
},
],
]),
)
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting)
await createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
)
expect(subscriptionSettingRepository.save).toHaveBeenCalled()
})
it('should create default settings for a subscription if it is not replaceable and not existing', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: false,
},
],
]),
)
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
await createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
)
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
})
it('should create default settings for a subscription if it is not replaceable and no previous subscription existed', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: false,
},
],
]),
)
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
} as jest.Mocked<UserSubscription>,
])
await createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
)
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
})
@@ -85,7 +183,11 @@ describe('SubscriptionSettingService', () => {
.fn()
.mockReturnValue(undefined)
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan)
await createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
)
expect(subscriptionSettingRepository.save).not.toHaveBeenCalled()
})

View File

@@ -1,4 +1,4 @@
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionName, Uuid } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -16,6 +16,7 @@ import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO'
import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface'
import { SettingFactoryInterface } from './SettingFactoryInterface'
import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
@injectable()
export class SubscriptionSettingService implements SubscriptionSettingServiceInterface {
@@ -26,12 +27,14 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
@inject(TYPES.SubscriptionSettingsAssociationService)
private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface,
@inject(TYPES.SettingDecrypter) private settingDecrypter: SettingDecrypterInterface,
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async applyDefaultSubscriptionSettingsForSubscription(
userSubscription: UserSubscription,
subscriptionName: SubscriptionName,
userUuid: Uuid,
): Promise<void> {
const defaultSettingsWithValues =
await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(subscriptionName)
@@ -43,6 +46,15 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
for (const settingName of defaultSettingsWithValues.keys()) {
const setting = defaultSettingsWithValues.get(settingName) as SettingDescription
if (!setting.replaceable) {
const existingSetting = await this.findPreviousSubscriptionSetting(settingName, userSubscription.uuid, userUuid)
if (existingSetting !== null) {
existingSetting.userSubscription = Promise.resolve(userSubscription)
await this.subscriptionSettingRepository.save(existingSetting)
continue
}
}
await this.createOrReplace({
userSubscription,
@@ -114,4 +126,22 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
subscriptionSetting,
}
}
private async findPreviousSubscriptionSetting(
settingName: SubscriptionSettingName,
currentUserSubscriptionUuid: Uuid,
userUuid: Uuid,
): Promise<SubscriptionSetting | null> {
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid)
const previousSubscriptions = userSubscriptions.filter(
(subscription) => subscription.uuid !== currentUserSubscriptionUuid,
)
const lastSubscription = previousSubscriptions.shift()
if (!lastSubscription) {
return null
}
return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(settingName, lastSubscription.uuid)
}
}

View File

@@ -1,4 +1,4 @@
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionName, Uuid } from '@standardnotes/common'
import { UserSubscription } from '../Subscription/UserSubscription'
import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO'
@@ -10,6 +10,7 @@ export interface SubscriptionSettingServiceInterface {
applyDefaultSubscriptionSettingsForSubscription(
userSubscription: UserSubscription,
subscriptionName: SubscriptionName,
userUuid: Uuid,
): Promise<void>
createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise<CreateOrReplaceSubscriptionSettingResponse>
findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise<SubscriptionSetting | null>

View File

@@ -61,6 +61,7 @@ describe('SubscriptionSettingsAssociationService', () => {
sensitive: false,
serverEncryptionVersion: 0,
value: '107374182400',
replaceable: true,
})
})
@@ -88,6 +89,7 @@ describe('SubscriptionSettingsAssociationService', () => {
sensitive: false,
serverEncryptionVersion: 0,
value: '104857600',
replaceable: true,
})
})

View File

@@ -28,7 +28,7 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0' },
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
],
]),
],
@@ -37,7 +37,7 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0' },
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
],
]),
],
@@ -56,6 +56,7 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: (await this.getFileUploadLimit(subscriptionName)).toString(),
replaceable: true,
})
return defaultSettings

View File

@@ -3,6 +3,6 @@ import { Uuid } from '@standardnotes/common'
import { SubscriptionToken } from './SubscriptionToken'
export interface SubscriptionTokenRepositoryInterface {
save(subscriptionToken: SubscriptionToken): Promise<void>
save(subscriptionToken: SubscriptionToken): Promise<boolean>
getUserUuidByToken(token: string): Promise<Uuid | undefined>
}

View File

@@ -6,10 +6,12 @@ export interface UserSubscriptionRepositoryInterface {
findOneByUuid(uuid: Uuid): Promise<UserSubscription | null>
countByUserUuid(userUuid: Uuid): Promise<number>
findOneByUserUuid(userUuid: Uuid): Promise<UserSubscription | null>
findByUserUuid(userUuid: Uuid): Promise<UserSubscription[]>
findOneByUserUuidAndSubscriptionId(userUuid: Uuid, subscriptionId: number): Promise<UserSubscription | null>
findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>
findBySubscriptionId(subscriptionId: number): Promise<UserSubscription[]>
updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void>
updateCancelled(subscriptionId: number, cancelled: boolean, updatedAt: number): Promise<void>
countActiveSubscriptions(): Promise<number>
save(subscription: UserSubscription): Promise<UserSubscription>
}

View File

@@ -104,6 +104,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription,
'PLUS_PLAN',
'123',
)
})

View File

@@ -75,6 +75,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
inviteeSubscription,
inviteeSubscription.planName as SubscriptionName,
invitee.uuid,
)
return {

View File

@@ -5,17 +5,19 @@ import { TimerInterface } from '@standardnotes/time'
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
import { CreateSubscriptionToken } from './CreateSubscriptionToken'
import { Logger } from 'winston'
describe('CreateSubscriptionToken', () => {
let subscriptionTokenRepository: SubscriptionTokenRepositoryInterface
let cryptoNode: CryptoNode
let timer: TimerInterface
let logger: Logger
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer)
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer, logger)
beforeEach(() => {
subscriptionTokenRepository = {} as jest.Mocked<SubscriptionTokenRepositoryInterface>
subscriptionTokenRepository.save = jest.fn()
subscriptionTokenRepository.save = jest.fn().mockReturnValue(true)
cryptoNode = {} as jest.Mocked<CryptoNode>
cryptoNode.generateRandomKey = jest.fn().mockReturnValueOnce('random-string')
@@ -23,6 +25,9 @@ describe('CreateSubscriptionToken', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
timer.getUTCDateNHoursAhead = jest.fn().mockReturnValue(new Date(1))
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should create an subscription token and persist it', async () => {
@@ -36,4 +41,19 @@ describe('CreateSubscriptionToken', () => {
expiresAt: 1,
})
})
it('should throw an error if the subscription token was not created', async () => {
subscriptionTokenRepository.save = jest.fn().mockReturnValue(false)
let caughtError = null
try {
await createUseCase().execute({
userUuid: '1-2-3',
})
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})

View File

@@ -1,6 +1,7 @@
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
@@ -15,6 +16,7 @@ export class CreateSubscriptionToken implements UseCaseInterface {
private subscriptionTokenRepository: SubscriptionTokenRepositoryInterface,
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: CreateSubscriptionTokenDTO): Promise<CreateSubscriptionTokenResponse> {
@@ -26,7 +28,13 @@ export class CreateSubscriptionToken implements UseCaseInterface {
expiresAt: this.timer.convertStringDateToMicroseconds(this.timer.getUTCDateNHoursAhead(3).toString()),
}
await this.subscriptionTokenRepository.save(subscriptionToken)
const subscriptionTokenWasSaved = await this.subscriptionTokenRepository.save(subscriptionToken)
if (!subscriptionTokenWasSaved) {
this.logger.error(`Could not create subscription token for user ${dto.userUuid}`)
throw new Error('Could not create subscription token')
}
return {
subscriptionToken,

View File

@@ -11,6 +11,7 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { RoleName } from '@standardnotes/common'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
describe('InviteToSharedSubscription', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -187,7 +188,7 @@ describe('InviteToSharedSubscription', () => {
it('should not create an invitation if it already exists', async () => {
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedSubscriptionInvitation>)
.mockReturnValue({ status: InvitationStatus.Sent } as jest.Mocked<SharedSubscriptionInvitation>)
expect(
await createUseCase().execute({
@@ -205,4 +206,27 @@ describe('InviteToSharedSubscription', () => {
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should create an invitation if it already exists but was canceled', async () => {
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
.fn()
.mockReturnValue({ status: InvitationStatus.Canceled } as jest.Mocked<SharedSubscriptionInvitation>)
expect(
await createUseCase().execute({
inviteeIdentifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'inviter@test.te',
inviterRoles: [RoleName.ProUser],
}),
).toEqual({
success: true,
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
})

View File

@@ -57,7 +57,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
dto.inviteeIdentifier,
dto.inviterEmail,
)
if (existingInvitation !== null) {
if (existingInvitation !== null && existingInvitation.status !== InvitationStatus.Canceled) {
return {
success: false,
}

View File

@@ -16,6 +16,7 @@ import { Setting } from '../Setting/Setting'
import { MuteSignInEmailsOption } from '@standardnotes/settings'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { ProtocolVersion } from '@standardnotes/common'
describe('SignIn', () => {
let user: User
@@ -50,6 +51,7 @@ describe('SignIn', () => {
user = {
uuid: '1-2-3',
email: 'test@test.com',
version: ProtocolVersion.V004,
} as jest.Mocked<User>
user.encryptedPassword = '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a'
@@ -99,7 +101,10 @@ describe('SignIn', () => {
logger.error = jest.fn()
})
it('should sign in a user', async () => {
it('should sign in a legacy user without code verifier', async () => {
user.version = ProtocolVersion.V003
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
expect(
await createUseCase().execute({
email: 'test@test.te',
@@ -124,6 +129,22 @@ describe('SignIn', () => {
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not sign in a user without code verifier', async () => {
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'qweqwe123123',
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
}),
).toEqual({
success: false,
errorCode: 410,
errorMessage: 'Please update your client application.',
})
})
it('should sign in a user with valid code verifier', async () => {
expect(
await createUseCase().execute({
@@ -165,6 +186,7 @@ describe('SignIn', () => {
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: true,
@@ -192,6 +214,7 @@ describe('SignIn', () => {
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: true,
@@ -218,6 +241,7 @@ describe('SignIn', () => {
email: 'test@test.com',
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
uuid: '1-2-3',
version: '004',
},
})
})
@@ -234,6 +258,7 @@ describe('SignIn', () => {
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: true,
@@ -249,6 +274,7 @@ describe('SignIn', () => {
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: false,
@@ -284,6 +310,7 @@ describe('SignIn', () => {
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: false,

View File

@@ -21,6 +21,8 @@ import { UseCaseInterface } from './UseCaseInterface'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { SignInDTOV2Challenged } from './SignInDTOV2Challenged'
import { ProtocolVersion } from '@standardnotes/common'
import { HttpStatusCode } from '@standardnotes/api'
@injectable()
export class SignIn implements UseCaseInterface {
@@ -39,7 +41,8 @@ export class SignIn implements UseCaseInterface {
) {}
async execute(dto: SignInDTO): Promise<SignInResponse> {
if (this.isCodeChallengedVersion(dto)) {
const performingCodeChallengedSignIn = this.isCodeChallengedVersion(dto)
if (performingCodeChallengedSignIn) {
const validCodeVerifier = await this.validateCodeVerifier(dto.codeVerifier)
if (!validCodeVerifier) {
this.logger.debug('Code verifier does not match')
@@ -62,6 +65,14 @@ export class SignIn implements UseCaseInterface {
}
}
if (user.version === ProtocolVersion.V004 && !performingCodeChallengedSignIn) {
return {
success: false,
errorMessage: 'Please update your client application.',
errorCode: HttpStatusCode.Gone,
}
}
const passwordMatches = await bcrypt.compare(dto.password, user.encryptedPassword)
if (!passwordMatches) {
this.logger.debug('Password does not match')

View File

@@ -1,3 +1,5 @@
import { HttpStatusCode } from '@standardnotes/api'
import { AuthResponse20161215 } from '../Auth/AuthResponse20161215'
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
@@ -5,4 +7,5 @@ export type SignInResponse = {
success: boolean
authResponse?: AuthResponse20161215 | AuthResponse20200115
errorMessage?: string
errorCode?: HttpStatusCode
}

View File

@@ -137,7 +137,7 @@ export class InversifyExpressAuthController extends BaseHttpController {
message: signInResult.errorMessage,
},
},
401,
signInResult.errorCode ?? 401,
)
}

View File

@@ -7,14 +7,16 @@ import { UserSubscription } from '../../Domain/Subscription/UserSubscription'
import { MySQLUserSubscriptionRepository } from './MySQLUserSubscriptionRepository'
import { UserSubscriptionType } from '../../Domain/Subscription/UserSubscriptionType'
import { TimerInterface } from '@standardnotes/time'
describe('MySQLUserSubscriptionRepository', () => {
let ormRepository: Repository<UserSubscription>
let selectQueryBuilder: SelectQueryBuilder<UserSubscription>
let updateQueryBuilder: UpdateQueryBuilder<UserSubscription>
let subscription: UserSubscription
let timer: TimerInterface
const createRepository = () => new MySQLUserSubscriptionRepository(ormRepository)
const createRepository = () => new MySQLUserSubscriptionRepository(ormRepository, timer)
beforeEach(() => {
selectQueryBuilder = {} as jest.Mocked<SelectQueryBuilder<UserSubscription>>
@@ -28,6 +30,9 @@ describe('MySQLUserSubscriptionRepository', () => {
ormRepository = {} as jest.Mocked<Repository<UserSubscription>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
ormRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should save', async () => {
@@ -36,6 +41,47 @@ describe('MySQLUserSubscriptionRepository', () => {
expect(ormRepository.save).toHaveBeenCalledWith(subscription)
})
it('should find all subscriptions by user uuid', async () => {
const canceledSubscription = {
planName: SubscriptionName.ProPlan,
cancelled: true,
} as jest.Mocked<UserSubscription>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription])
const result = await createRepository().findByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([canceledSubscription, subscription])
})
it('should count all active subscriptions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.select = jest.fn().mockReturnThis()
selectQueryBuilder.distinct = jest.fn().mockReturnThis()
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
const result = await createRepository().countActiveSubscriptions()
expect(selectQueryBuilder.select).toHaveBeenCalledWith('user_uuid')
expect(selectQueryBuilder.distinct).toHaveBeenCalled()
expect(selectQueryBuilder.where).toHaveBeenCalledWith('ends_at > :timestamp', {
timestamp: 123,
})
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
expect(result).toEqual(2)
})
it('should find one longest lasting uncanceled subscription by user uuid if there are canceled ones', async () => {
const canceledSubscription = {
planName: SubscriptionName.ProPlan,

View File

@@ -1,4 +1,5 @@
import { Uuid } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { Repository } from 'typeorm'
import TYPES from '../../Bootstrap/Types'
@@ -12,8 +13,28 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
constructor(
@inject(TYPES.ORMUserSubscriptionRepository)
private ormRepository: Repository<UserSubscription>,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async countActiveSubscriptions(): Promise<number> {
return await this.ormRepository
.createQueryBuilder()
.select('user_uuid')
.distinct()
.where('ends_at > :timestamp', { timestamp: this.timer.getTimestampInMicroseconds() })
.getCount()
}
async findByUserUuid(userUuid: string): Promise<UserSubscription[]> {
return await this.ormRepository
.createQueryBuilder()
.where('user_uuid = :user_uuid', {
user_uuid: userUuid,
})
.orderBy('ends_at', 'DESC')
.getMany()
}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return await this.ormRepository
.createQueryBuilder()

View File

@@ -15,7 +15,6 @@ describe('LockRepository', () => {
redisClient.expire = jest.fn()
redisClient.del = jest.fn()
redisClient.get = jest.fn()
redisClient.set = jest.fn()
redisClient.setex = jest.fn()
})
@@ -88,6 +87,6 @@ describe('LockRepository', () => {
it('should update a lock counter', async () => {
await createRepository().updateLockCounter('123', 3)
expect(redisClient.set).toHaveBeenCalledWith('lock:123', 3)
expect(redisClient.setex).toHaveBeenCalledWith('lock:123', 120, 3)
})
})

View File

@@ -30,7 +30,7 @@ export class LockRepository implements LockRepositoryInterface {
}
async updateLockCounter(userIdentifier: string, counter: number): Promise<void> {
await this.redisClient.set(`${this.PREFIX}:${userIdentifier}`, counter)
await this.redisClient.setex(`${this.PREFIX}:${userIdentifier}`, this.failedLoginLockout, counter)
}
async getLockCounter(userIdentifier: string): Promise<number> {

View File

@@ -14,9 +14,9 @@ describe('RedisSubscriptionTokenRepository', () => {
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.set = jest.fn()
redisClient.set = jest.fn().mockReturnValue('OK')
redisClient.get = jest.fn()
redisClient.expireat = jest.fn()
redisClient.expireat = jest.fn().mockReturnValue(1)
timer = {} as jest.Mocked<TimerInterface>
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
@@ -45,7 +45,23 @@ describe('RedisSubscriptionTokenRepository', () => {
expiresAt: 123,
}
await createRepository().save(subscriptionToken)
expect(await createRepository().save(subscriptionToken)).toBeTruthy()
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
})
it('should indicate subscription token was not saved', async () => {
redisClient.set = jest.fn().mockReturnValue(null)
const subscriptionToken: SubscriptionToken = {
userUuid: '1-2-3',
token: 'random-string',
expiresAt: 123,
}
expect(await createRepository().save(subscriptionToken)).toBeFalsy()
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')

View File

@@ -24,11 +24,13 @@ export class RedisSubscriptionTokenRepository implements SubscriptionTokenReposi
return userUuid
}
async save(subscriptionToken: SubscriptionToken): Promise<void> {
async save(subscriptionToken: SubscriptionToken): Promise<boolean> {
const key = `${this.PREFIX}:${subscriptionToken.token}`
const expiresAtTimestampInSeconds = this.timer.convertMicrosecondsToSeconds(subscriptionToken.expiresAt)
await this.redisClient.set(key, subscriptionToken.userUuid)
await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
const wasSet = await this.redisClient.set(key, subscriptionToken.userUuid)
const timeoutWasSet = await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
return wasSet === 'OK' && timeoutWasSet !== 0
}
}

View File

@@ -17,28 +17,31 @@ describe('SettingProjector', () => {
serverEncryptionVersion: 1,
createdAt: 1,
updatedAt: 2,
sensitive: false,
} as jest.Mocked<Setting>
})
it('should create a simple projection of a setting', async () => {
const projection = await createProjector().projectSimple(setting)
expect(projection).toEqual({
expect(projection).toStrictEqual({
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
})
})
it('should create a simple projection of list of settings', async () => {
const projection = await createProjector().projectManySimple([setting])
expect(projection).toEqual([
expect(projection).toStrictEqual([
{
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
},
])
})

View File

@@ -6,16 +6,16 @@ import { SimpleSetting } from '../Domain/Setting/SimpleSetting'
@injectable()
export class SettingProjector {
async projectSimple(setting: Setting): Promise<SimpleSetting> {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
user,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
serverEncryptionVersion,
...rest
} = setting
return rest
return {
uuid: setting.uuid,
name: setting.name,
value: setting.value,
createdAt: setting.createdAt,
updatedAt: setting.updatedAt,
sensitive: setting.sensitive,
}
}
async projectManySimple(settings: Setting[]): Promise<SimpleSetting[]> {
return Promise.all(
settings.map(async (setting) => {

View File

@@ -17,28 +17,31 @@ describe('SubscriptionSettingProjector', () => {
serverEncryptionVersion: 1,
createdAt: 1,
updatedAt: 2,
sensitive: false,
} as jest.Mocked<SubscriptionSetting>
})
it('should create a simple projection of a setting', async () => {
const projection = await createProjector().projectSimple(setting)
expect(projection).toEqual({
expect(projection).toStrictEqual({
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
})
})
it('should create a simple projection of list of settings', async () => {
const projection = await createProjector().projectManySimple([setting])
expect(projection).toEqual([
expect(projection).toStrictEqual([
{
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
},
])
})

View File

@@ -6,16 +6,16 @@ import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
@injectable()
export class SubscriptionSettingProjector {
async projectSimple(setting: SubscriptionSetting): Promise<SimpleSubscriptionSetting> {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userSubscription,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
serverEncryptionVersion,
...rest
} = setting
return rest
return {
uuid: setting.uuid,
name: setting.name,
value: setting.value,
createdAt: setting.createdAt,
updatedAt: setting.updatedAt,
sensitive: setting.sensitive,
}
}
async projectManySimple(settings: SubscriptionSetting[]): Promise<SimpleSubscriptionSetting[]> {
return Promise.all(
settings.map(async (setting) => {

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.12...@standardnotes/domain-events-infra@1.8.13) (2022-09-28)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.11...@standardnotes/domain-events-infra@1.8.12) (2022-09-21)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.8.12",
"version": "1.8.13",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.60.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.6...@standardnotes/domain-events@2.60.7) (2022-09-28)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.60.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.5...@standardnotes/domain-events@2.60.6) (2022-09-21)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.60.6",
"version": "2.60.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -25,7 +25,7 @@
},
"dependencies": {
"@standardnotes/common": "workspace:*",
"@standardnotes/features": "^1.47.0",
"@standardnotes/features": "^1.52.1",
"@standardnotes/predicates": "workspace:*",
"@standardnotes/security": "workspace:*",
"reflect-metadata": "^0.1.13"

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.6.3",
"version": "1.6.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.10.31",
"version": "1.10.32",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.14...@standardnotes/syncing-server@1.8.15) (2022-09-30)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.14](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.13...@standardnotes/syncing-server@1.8.14) (2022-09-30)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.13](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.12...@standardnotes/syncing-server@1.8.13) (2022-09-30)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.11...@standardnotes/syncing-server@1.8.12) (2022-09-30)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.10...@standardnotes/syncing-server@1.8.11) (2022-09-28)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.9...@standardnotes/syncing-server@1.8.10) (2022-09-22)
### Bug Fixes
* **syncing-server-js:** binding of sync limit ([5628de6](https://github.com/standardnotes/syncing-server-js/commit/5628de6445a90901735b449488fb8c8374f2171e))
## [1.8.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.8...@standardnotes/syncing-server@1.8.9) (2022-09-22)
### Bug Fixes
* **syncing-server:** introduce upper bound for sync items limit as an env var ([03d1bc6](https://github.com/standardnotes/syncing-server-js/commit/03d1bc611c39c0d1f0bcaa00824825304e08d30b))
## [1.8.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.7...@standardnotes/syncing-server@1.8.8) (2022-09-21)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.8.8",
"version": "1.8.15",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -90,6 +90,7 @@ const newrelicFormatter = require('@newrelic/winston-enricher')
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
private readonly DEFAULT_MAX_ITEMS_LIMIT = 300
async load(): Promise<Container> {
const env: Env = new Env()
@@ -191,7 +192,16 @@ export class ContainerConfigLoader {
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container
.bind(TYPES.CONTENT_SIZE_TRANSFER_LIMIT)
.toConstantValue(env.get('CONTENT_SIZE_TRANSFER_LIMIT', true) ?? this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT)
.toConstantValue(
env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
? +env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
: this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT,
)
container
.bind(TYPES.MAX_ITEMS_LIMIT)
.toConstantValue(
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
)
// use cases
container.bind<SyncItems>(TYPES.SyncItems).to(SyncItems)

View File

@@ -36,6 +36,7 @@ const TYPES = {
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
VERSION: Symbol.for('VERSION'),
CONTENT_SIZE_TRANSFER_LIMIT: Symbol.for('CONTENT_SIZE_TRANSFER_LIMIT'),
MAX_ITEMS_LIMIT: Symbol.for('MAX_ITEMS_LIMIT'),
// use cases
SyncItems: Symbol.for('SyncItems'),
CheckIntegrity: Symbol.for('CheckIntegrity'),

View File

@@ -40,6 +40,7 @@ describe('ItemService', () => {
let timeHelper: Timer
let itemTransferCalculator: ItemTransferCalculatorInterface
let itemProjector: ProjectorInterface<Item, ItemProjection>
const maxItemsSyncLimit = 300
const createService = () =>
new ItemService(
@@ -54,6 +55,7 @@ describe('ItemService', () => {
itemTransferCalculator,
timer,
itemProjector,
maxItemsSyncLimit,
logger,
)

View File

@@ -27,7 +27,6 @@ import { ItemProjection } from '../../Projection/ItemProjection'
@injectable()
export class ItemService implements ItemServiceInterface {
private readonly DEFAULT_ITEMS_LIMIT = 150
private readonly MAX_ITEMS_LIMIT = 300
private readonly SYNC_TOKEN_VERSION = 2
constructor(
@@ -42,6 +41,7 @@ export class ItemService implements ItemServiceInterface {
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.MAX_ITEMS_LIMIT) private maxItemsSyncLimit: number,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -58,7 +58,7 @@ export class ItemService implements ItemServiceInterface {
deleted: lastSyncTime ? undefined : false,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
limit: limit < this.MAX_ITEMS_LIMIT ? limit : this.MAX_ITEMS_LIMIT,
limit: limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit,
}
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(

View File

@@ -1803,18 +1803,18 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/api@npm:^1.8.1":
version: 1.8.1
resolution: "@standardnotes/api@npm:1.8.1"
"@standardnotes/api@npm:^1.9.0":
version: 1.9.0
resolution: "@standardnotes/api@npm:1.9.0"
dependencies:
"@standardnotes/common": ^1.32.0
"@standardnotes/encryption": 1.15.3
"@standardnotes/models": 1.18.3
"@standardnotes/responses": 1.10.2
"@standardnotes/encryption": 1.15.9
"@standardnotes/models": 1.22.0
"@standardnotes/responses": 1.10.3
"@standardnotes/security": ^1.1.0
"@standardnotes/utils": 1.9.0
reflect-metadata: ^0.1.13
checksum: 76c5d1a2d29cf7f407813246febf54fe02c5d7cacedcfd1bf5f6ee6630d847f58cae0b5827fbba1c5c5d5a30e56095833c9eff8b413111f8aae9cc17802ffa63
checksum: cc3feac3935a382e0ce1fcaf233206a547b6c998cb99ab362d5c7030b3f4e7cbbd3a083eab40bdecbcdc9497dcd283e4513e29dbf200e815ffa46b192ed61b01
languageName: node
linkType: hard
@@ -1825,11 +1825,11 @@ __metadata:
"@newrelic/winston-enricher": ^4.0.0
"@sentry/node": ^7.3.0
"@standardnotes/analytics": "workspace:*"
"@standardnotes/api": ^1.8.1
"@standardnotes/api": ^1.9.0
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-events": "workspace:*"
"@standardnotes/domain-events-infra": "workspace:*"
"@standardnotes/features": ^1.47.0
"@standardnotes/features": ^1.52.1
"@standardnotes/predicates": "workspace:*"
"@standardnotes/responses": ^1.6.39
"@standardnotes/security": "workspace:*"
@@ -1939,7 +1939,7 @@ __metadata:
resolution: "@standardnotes/domain-events@workspace:packages/domain-events"
dependencies:
"@standardnotes/common": "workspace:*"
"@standardnotes/features": ^1.47.0
"@standardnotes/features": ^1.52.1
"@standardnotes/predicates": "workspace:*"
"@standardnotes/security": "workspace:*"
"@types/jest": ^28.1.4
@@ -1951,17 +1951,17 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/encryption@npm:1.15.3":
version: 1.15.3
resolution: "@standardnotes/encryption@npm:1.15.3"
"@standardnotes/encryption@npm:1.15.9":
version: 1.15.9
resolution: "@standardnotes/encryption@npm:1.15.9"
dependencies:
"@standardnotes/common": ^1.32.0
"@standardnotes/models": 1.18.3
"@standardnotes/responses": 1.10.2
"@standardnotes/sncrypto-common": 1.11.1
"@standardnotes/models": 1.22.0
"@standardnotes/responses": 1.10.3
"@standardnotes/sncrypto-common": 1.12.0
"@standardnotes/utils": 1.9.0
reflect-metadata: ^0.1.13
checksum: 1a7863299f86ee28de1640b93277f8b3e206bec2b34a205eb6e6fd6c6899a4908623acd9f2452d71a83c542b1f181408e7743386e5e1079239c6c0fa384242c9
checksum: 7595ac08cea6e54e1456cbff3958969318d90ae237aff166bc4429da5bcf6167c5eb03aa8658a1747486a0639b80b523dae46106ab253c75d24579643cf3e948
languageName: node
linkType: hard
@@ -1993,15 +1993,15 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/features@npm:1.52.0":
version: 1.52.0
resolution: "@standardnotes/features@npm:1.52.0"
"@standardnotes/features@npm:1.52.1, @standardnotes/features@npm:^1.52.1":
version: 1.52.1
resolution: "@standardnotes/features@npm:1.52.1"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.32.0
"@standardnotes/security": ^1.2.0
reflect-metadata: ^0.1.13
checksum: 3e6014272f72ed33bc7de3cefb33a63a02866c01bfd4a54bc95426e2719f4997940de382cfd83982eaeafdbdf9afac558aecb9139117facfe9c7479089e2952d
checksum: ff3684399e0e0c0e799f11e69dddea2e1f65f315e5a5dd3ca5640e24e836ee85faf1f5ee15fc804411bf083004527fcef08411d5c2d0b5894491bf2f28ceca68
languageName: node
linkType: hard
@@ -2066,17 +2066,17 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/models@npm:1.18.3":
version: 1.18.3
resolution: "@standardnotes/models@npm:1.18.3"
"@standardnotes/models@npm:1.22.0":
version: 1.22.0
resolution: "@standardnotes/models@npm:1.22.0"
dependencies:
"@standardnotes/common": ^1.32.0
"@standardnotes/features": 1.52.0
"@standardnotes/responses": 1.10.2
"@standardnotes/features": 1.52.1
"@standardnotes/responses": 1.10.3
"@standardnotes/utils": 1.9.0
lodash: ^4.17.21
reflect-metadata: ^0.1.13
checksum: 21830c805ffa1ac2184c64903f88915b7b439eb4eb80ac0686c4920a9a4c86cc6c71a3daeb1ede8f3fe6cf0ce106f7ba396f994165306c1c59c05902a7ec075a
checksum: 9928246368b7de7062314374219065507642ed3b6764c27f14ed8d42f0c5a9370fab8731a43a57885a000e461ea60694ba4caa7d9940350839d487cedb7079b5
languageName: node
linkType: hard
@@ -2105,15 +2105,15 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/responses@npm:1.10.2":
version: 1.10.2
resolution: "@standardnotes/responses@npm:1.10.2"
"@standardnotes/responses@npm:1.10.3":
version: 1.10.3
resolution: "@standardnotes/responses@npm:1.10.3"
dependencies:
"@standardnotes/common": ^1.32.0
"@standardnotes/features": 1.52.0
"@standardnotes/features": 1.52.1
"@standardnotes/security": ^1.1.0
reflect-metadata: ^0.1.13
checksum: 364724b5c7efa06948a240da82320817bce58049d6ea226cc2e828e86144ba4d19e5ae6fa437311438008b16aff7bd8cbe7e3f7475a2e9c89cf47650e1e1e9b0
checksum: 4a1e31eb89342461488f00c65884839d7d935d47d4e217da83967c7cbe181fd0341ee58b744fe607def30d06cd1a8069c3b54f40b3933765303db35ee63f6a0d
languageName: node
linkType: hard
@@ -2213,12 +2213,12 @@ __metadata:
languageName: unknown
linkType: soft
"@standardnotes/sncrypto-common@npm:1.11.1":
version: 1.11.1
resolution: "@standardnotes/sncrypto-common@npm:1.11.1"
"@standardnotes/sncrypto-common@npm:1.12.0":
version: 1.12.0
resolution: "@standardnotes/sncrypto-common@npm:1.12.0"
dependencies:
reflect-metadata: ^0.1.13
checksum: 69d698abb7ffc2aecfffd9ccf3e023adca73e5b27cfa1106dfdf10a13d6455b9581c9bf854b333f00255317ec62c384c516b218f40a55ee84fd4f659b8aef16b
checksum: b89a14bd233cb781213b2e25dd8d7bfe911820d341903f2987647c168745c3afb710ad94fa42e50aaafb644ee4a7b5fd64ec137f622cfc082951fd584af0d230
languageName: node
linkType: hard