mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff7c52a05e | ||
|
|
d5684326b1 | ||
|
|
017c55d190 | ||
|
|
2504887e8d | ||
|
|
805e63379c | ||
|
|
dcb20e6ea6 | ||
|
|
786b94380b | ||
|
|
460d6a8d0f | ||
|
|
0dbc929c8e | ||
|
|
0c5305acf6 | ||
|
|
34139efafb | ||
|
|
eb53c3896f | ||
|
|
2af4c6fb55 | ||
|
|
d66f784538 | ||
|
|
f127241857 | ||
|
|
5b0d9dd394 | ||
|
|
ee29d18484 | ||
|
|
2255f856f9 | ||
|
|
f2415527f0 | ||
|
|
59eb70ce62 | ||
|
|
1d18725bc5 | ||
|
|
d4af1d743e | ||
|
|
9d1a357b5b | ||
|
|
5160cc36dd | ||
|
|
f05e1dbdf0 | ||
|
|
7b797f0cba | ||
|
|
f823826044 | ||
|
|
9589403c9d | ||
|
|
2757b18e17 | ||
|
|
6e8481bb2f |
@@ -0,0 +1,39 @@
|
||||
name: Analytics Server
|
||||
|
||||
concurrency:
|
||||
group: analytics
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/analytics*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call_server_application_workflow:
|
||||
name: Server Application
|
||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
||||
with:
|
||||
service_name: analytics
|
||||
workspace_name: "@standardnotes/analytics"
|
||||
e2e_tag_parameter_name: analytics_image_tag
|
||||
deploy_web: false
|
||||
package_path: packages/analytics
|
||||
secrets: inherit
|
||||
|
||||
newrelic:
|
||||
needs: call_server_application_workflow
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_ANALYTICS_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_REPLICA_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=analytics
|
||||
DB_PASSWORD=changeme123
|
||||
DB_DATABASE=analytics
|
||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=Analytics
|
||||
NEW_RELIC_LICENSE_KEY=
|
||||
NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.38.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* add analytics worker service ([d568432](https://github.com/standardnotes/server/commit/d5684326b1301855d0e07415195d4b246292f9a9))
|
||||
|
||||
# [1.37.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add analytics for subscription reactivating ([460d6a8](https://github.com/standardnotes/server/commit/460d6a8d0f17eee624feb5d2588086ae6f0996e4))
|
||||
|
||||
# [1.36.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM node:16.15.1-alpine
|
||||
|
||||
RUN apk add --update \
|
||||
curl \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY ./ /workspace
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/analytics/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-worker" ]
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
dayjs.extend(utc)
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
yarn workspace @standardnotes/analytics worker
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.36.0",
|
||||
"version": "1.38.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
@@ -20,19 +20,40 @@
|
||||
"clean": "rm -fr dist",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage"
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
"typeorm": "typeorm-ts-node-commonjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^29.1.1",
|
||||
"@types/newrelic": "^7.0.3",
|
||||
"@types/node": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.1.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"aws-sdk": "^2.1158.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.2.3",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
"mysql2": "^2.3.3",
|
||||
"newrelic": "^9.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"typeorm": "^0.3.6",
|
||||
"winston": "^3.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import * as winston from 'winston'
|
||||
import Redis from 'ioredis'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import { Container } from 'inversify'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { AppDataSource } from './DataSource'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import {
|
||||
RedisDomainEventPublisher,
|
||||
RedisDomainEventSubscriberFactory,
|
||||
RedisEventMessageHandler,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
await AppDataSource.initialize()
|
||||
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Redis).toConstantValue(redis)
|
||||
|
||||
const newrelicWinstonFormatter = newrelicFormatter(winston)
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
winstonFormatters.push(newrelicWinstonFormatter())
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
if (env.get('SNS_AWS_REGION', true)) {
|
||||
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(
|
||||
new AWS.SNS({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
const sqsConfig: AWS.SQS.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SQS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig))
|
||||
}
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
|
||||
// Repositories
|
||||
|
||||
// ORM
|
||||
|
||||
// Use Case
|
||||
|
||||
// Hanlders
|
||||
|
||||
// Services
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||
} else {
|
||||
container
|
||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||
)
|
||||
}
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(
|
||||
env.get('NEW_RELIC_ENABLED', true) === 'true'
|
||||
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
|
||||
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new SQSDomainEventSubscriberFactory(
|
||||
container.get(TYPES.SQS),
|
||||
container.get(TYPES.SQS_QUEUE_URL),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(new RedisEventMessageHandler(eventHandlers, container.get(TYPES.Logger)))
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventSubscriberFactory(
|
||||
container.get(TYPES.Redis),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
container.get(TYPES.REDIS_EVENTS_CHANNEL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
import { Env } from './Env'
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
replication: {
|
||||
master: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
host: env.get('DB_REPLICA_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
],
|
||||
removeNodeErrorCount: 10,
|
||||
},
|
||||
entities: [],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
// env vars
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
// Repositories
|
||||
// ORM
|
||||
// Use Case
|
||||
// Handlers
|
||||
// Services
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
DomainEventFactory: Symbol.for('DomainEventFactory'),
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
@@ -12,6 +12,7 @@ export enum AnalyticsActivity {
|
||||
SubscriptionRefunded = 'subscription-refunded',
|
||||
SubscriptionCancelled = 'subscription-cancelled',
|
||||
SubscriptionExpired = 'subscription-expired',
|
||||
SubscriptionReactivated = 'subscription-reactivated',
|
||||
EmailUnbackedUpData = 'email-unbacked-up-data',
|
||||
EmailBackup = 'email-backup',
|
||||
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
import { DomainEventFactory } from './DomainEventFactory'
|
||||
|
||||
describe('DomainEventFactory', () => {
|
||||
let timer: TimerInterface
|
||||
|
||||
const createFactory = () => new DomainEventFactory(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a DAILY_ANALYTICS_REPORT_GENERATED event', () => {
|
||||
expect(
|
||||
createFactory().createDailyAnalyticsReportGeneratedEvent({
|
||||
snjsStatistics: [
|
||||
{
|
||||
version: '1-2-3',
|
||||
count: 2,
|
||||
},
|
||||
],
|
||||
applicationStatistics: [
|
||||
{
|
||||
version: '2-3-4',
|
||||
count: 45,
|
||||
},
|
||||
],
|
||||
activityStatistics: [
|
||||
{
|
||||
name: AnalyticsActivity.Register,
|
||||
retention: 24,
|
||||
totalCount: 45,
|
||||
},
|
||||
],
|
||||
statisticMeasures: [
|
||||
{
|
||||
name: StatisticsMeasure.Income,
|
||||
totalValue: 43,
|
||||
average: 23,
|
||||
increments: 5,
|
||||
period: Period.Today,
|
||||
},
|
||||
],
|
||||
activityStatisticsOverTime: [
|
||||
{
|
||||
name: AnalyticsActivity.Register,
|
||||
period: Period.Last30Days,
|
||||
counts: [
|
||||
{
|
||||
periodKey: '2022-10-9',
|
||||
totalCount: 3,
|
||||
},
|
||||
],
|
||||
totalCount: 123,
|
||||
},
|
||||
],
|
||||
outOfSyncIncidents: 324,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
secondActivity: AnalyticsActivity.Login,
|
||||
retention: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
firstPeriodKey: AnalyticsActivity.Register,
|
||||
secondPeriodKey: AnalyticsActivity.Login,
|
||||
value: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
churn: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
rate: 12,
|
||||
periodKey: '2022-10-9',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'analytics',
|
||||
},
|
||||
payload: {
|
||||
activityStatistics: [
|
||||
{
|
||||
name: 'register',
|
||||
retention: 24,
|
||||
totalCount: 45,
|
||||
},
|
||||
],
|
||||
activityStatisticsOverTime: [
|
||||
{
|
||||
counts: [
|
||||
{
|
||||
periodKey: '2022-10-9',
|
||||
totalCount: 3,
|
||||
},
|
||||
],
|
||||
name: 'register',
|
||||
period: 9,
|
||||
totalCount: 123,
|
||||
},
|
||||
],
|
||||
applicationStatistics: [
|
||||
{
|
||||
count: 45,
|
||||
version: '2-3-4',
|
||||
},
|
||||
],
|
||||
churn: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
periodKey: '2022-10-9',
|
||||
rate: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
outOfSyncIncidents: 324,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: 'register',
|
||||
retention: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
firstPeriodKey: 'register',
|
||||
secondPeriodKey: 'login',
|
||||
value: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
secondActivity: 'login',
|
||||
},
|
||||
],
|
||||
snjsStatistics: [
|
||||
{
|
||||
count: 2,
|
||||
version: '1-2-3',
|
||||
},
|
||||
],
|
||||
statisticMeasures: [
|
||||
{
|
||||
average: 23,
|
||||
increments: 5,
|
||||
name: 'income',
|
||||
period: 0,
|
||||
totalValue: 43,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
||||
snjsStatistics: Array<{
|
||||
version: string
|
||||
count: number
|
||||
}>
|
||||
applicationStatistics: Array<{
|
||||
version: string
|
||||
count: number
|
||||
}>
|
||||
activityStatistics: Array<{
|
||||
name: string
|
||||
retention: number
|
||||
totalCount: number
|
||||
}>
|
||||
statisticMeasures: Array<{
|
||||
name: string
|
||||
totalValue: number
|
||||
average: number
|
||||
increments: number
|
||||
period: number
|
||||
}>
|
||||
activityStatisticsOverTime: Array<{
|
||||
name: string
|
||||
period: number
|
||||
counts: Array<{
|
||||
periodKey: string
|
||||
totalCount: number
|
||||
}>
|
||||
totalCount: number
|
||||
}>
|
||||
outOfSyncIncidents: number
|
||||
retentionStatistics: Array<{
|
||||
firstActivity: string
|
||||
secondActivity: string
|
||||
retention: {
|
||||
periodKeys: Array<string>
|
||||
values: Array<{
|
||||
firstPeriodKey: string
|
||||
secondPeriodKey: string
|
||||
value: number
|
||||
}>
|
||||
}
|
||||
}>
|
||||
churn: {
|
||||
periodKeys: Array<string>
|
||||
values: Array<{
|
||||
rate: number
|
||||
periodKey: string
|
||||
}>
|
||||
}
|
||||
}): DailyAnalyticsReportGeneratedEvent {
|
||||
return {
|
||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Analytics,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
||||
snjsStatistics: Array<{
|
||||
version: string
|
||||
count: number
|
||||
}>
|
||||
applicationStatistics: Array<{
|
||||
version: string
|
||||
count: number
|
||||
}>
|
||||
activityStatistics: Array<{
|
||||
name: string
|
||||
retention: number
|
||||
totalCount: number
|
||||
}>
|
||||
statisticMeasures: Array<{
|
||||
name: string
|
||||
totalValue: number
|
||||
average: number
|
||||
increments: number
|
||||
period: number
|
||||
}>
|
||||
activityStatisticsOverTime: Array<{
|
||||
name: string
|
||||
period: number
|
||||
counts: Array<{
|
||||
periodKey: string
|
||||
totalCount: number
|
||||
}>
|
||||
totalCount: number
|
||||
}>
|
||||
outOfSyncIncidents: number
|
||||
retentionStatistics: Array<{
|
||||
firstActivity: string
|
||||
secondActivity: string
|
||||
retention: {
|
||||
periodKeys: Array<string>
|
||||
values: Array<{
|
||||
firstPeriodKey: string
|
||||
secondPeriodKey: string
|
||||
value: number
|
||||
}>
|
||||
}
|
||||
}>
|
||||
churn: {
|
||||
periodKeys: Array<string>
|
||||
values: Array<{
|
||||
rate: number
|
||||
periodKey: string
|
||||
}>
|
||||
}
|
||||
}): DailyAnalyticsReportGeneratedEvent
|
||||
}
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.35.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.35.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add analytics for subscription reactivating ([460d6a8](https://github.com/standardnotes/api-gateway/commit/460d6a8d0f17eee624feb5d2588086ae6f0996e4))
|
||||
|
||||
## [1.34.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.34.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/api-gateway/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
## [1.33.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.33.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.33.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.33.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.33.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -40,6 +40,7 @@ const requestReport = async (
|
||||
AnalyticsActivity.SubscriptionRefunded,
|
||||
AnalyticsActivity.ExistingCustomersChurn,
|
||||
AnalyticsActivity.NewCustomersChurn,
|
||||
AnalyticsActivity.SubscriptionReactivated,
|
||||
]
|
||||
|
||||
for (const analyticsName of thirtyDaysAnalyticsNames) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.33.2",
|
||||
"version": "1.35.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -147,4 +147,9 @@ export class UsersController extends BaseHttpController {
|
||||
async deleteUser(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/:userUuid/requests', TYPES.AuthMiddleware)
|
||||
async submitRequest(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/requests`, request.body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,60 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.49.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.49.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** updating offline subscription end date ([2504887](https://github.com/standardnotes/server/commit/2504887e8d2d06608927e17a766cf0794dda2d4d))
|
||||
|
||||
# [1.49.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add analytics for subscription reactivating ([460d6a8](https://github.com/standardnotes/server/commit/460d6a8d0f17eee624feb5d2588086ae6f0996e4))
|
||||
|
||||
## [1.48.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.48.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** controller name ([5b0d9dd](https://github.com/standardnotes/server/commit/5b0d9dd3949d4da9d5ac3ca86a0e54ead2ce730d))
|
||||
|
||||
# [1.48.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/server/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
## [1.47.7](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.47.6](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.47.5](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* force utf8mb4 charset on typeorm ([5160cc3](https://github.com/standardnotes/server/commit/5160cc36ddc9e30551d5ad40a9e210d87091eec3))
|
||||
|
||||
## [1.47.4](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.47.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.47.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../src/Controller/SubscriptionSettingsController'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.47.2",
|
||||
"version": "1.49.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/api": "^1.17.2",
|
||||
"@standardnotes/api": "^1.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
@@ -49,7 +49,7 @@
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "^1.11.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"inversify": "^6.0.1",
|
||||
|
||||
@@ -203,6 +203,9 @@ import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEven
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
|
||||
import { UserRequestsController } from '../Controller/UserRequestsController'
|
||||
import { SubscriptionReactivatedEventHandler } from '../Domain/Handler/SubscriptionReactivatedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -266,6 +269,7 @@ export class ContainerConfigLoader {
|
||||
// Controller
|
||||
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
|
||||
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
|
||||
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
|
||||
@@ -438,6 +442,7 @@ export class ContainerConfigLoader {
|
||||
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
|
||||
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
@@ -489,6 +494,9 @@ export class ContainerConfigLoader {
|
||||
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
|
||||
container.bind<PaymentSuccessEventHandler>(TYPES.PaymentSuccessEventHandler).to(PaymentSuccessEventHandler)
|
||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||
container
|
||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||
.to(SubscriptionReactivatedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
|
||||
@@ -600,6 +608,7 @@ export class ContainerConfigLoader {
|
||||
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
|
||||
['PAYMENT_SUCCESS', container.get(TYPES.PaymentSuccessEventHandler)],
|
||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
|
||||
@@ -22,6 +22,7 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
|
||||
@@ -6,6 +6,7 @@ const TYPES = {
|
||||
// Controller
|
||||
AuthController: Symbol.for('AuthController'),
|
||||
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
|
||||
UserRequestsController: Symbol.for('UserRequestsController'),
|
||||
// Repositories
|
||||
UserRepository: Symbol.for('UserRepository'),
|
||||
SessionRepository: Symbol.for('SessionRepository'),
|
||||
@@ -121,6 +122,7 @@ const TYPES = {
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
VerifyPredicate: Symbol.for('VerifyPredicate'),
|
||||
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
||||
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
@@ -143,6 +145,7 @@ const TYPES = {
|
||||
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
|
||||
PaymentSuccessEventHandler: Symbol.for('PaymentSuccessEventHandler'),
|
||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||
// Services
|
||||
DeviceDetector: Symbol.for('DeviceDetector'),
|
||||
SessionService: Symbol.for('SessionService'),
|
||||
|
||||
@@ -109,4 +109,15 @@ describe('AuthController', () => {
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should throw error on the delete user method as it is still a part of the payments server', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createController().deleteAccount({ userUuid: '1-2-3' })
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
UserDeletionResponse,
|
||||
UserRegistrationRequestParams,
|
||||
UserRegistrationResponse,
|
||||
UserServerInterface,
|
||||
@@ -12,6 +13,7 @@ import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
|
||||
import { Register } from '../Domain/UseCase/Register'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { UserDeletionRequestParams } from '@standardnotes/api/dist/Domain/Request/User/UserDeletionRequestParams'
|
||||
|
||||
@injectable()
|
||||
export class AuthController implements UserServerInterface {
|
||||
@@ -22,6 +24,10 @@ export class AuthController implements UserServerInterface {
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async deleteAccount(_params: UserDeletionRequestParams): Promise<UserDeletionResponse> {
|
||||
throw new Error('This method is implemented on the payments server.')
|
||||
}
|
||||
|
||||
async register(params: UserRegistrationRequestParams): Promise<UserRegistrationResponse> {
|
||||
if (!params.email || !params.password) {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
|
||||
|
||||
import { UserRequestsController } from './UserRequestsController'
|
||||
|
||||
describe('UserRequestsController', () => {
|
||||
let processUserRequest: ProcessUserRequest
|
||||
|
||||
const createController = () => new UserRequestsController(processUserRequest)
|
||||
|
||||
beforeEach(() => {
|
||||
processUserRequest = {} as jest.Mocked<ProcessUserRequest>
|
||||
processUserRequest.execute = jest.fn().mockReturnValue({ success: true })
|
||||
})
|
||||
|
||||
it('should process user request', async () => {
|
||||
expect(
|
||||
await createController().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
}),
|
||||
).toEqual({
|
||||
status: 200,
|
||||
data: { success: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not process user request', async () => {
|
||||
processUserRequest.execute = jest.fn().mockReturnValue({ success: false })
|
||||
expect(
|
||||
await createController().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
}),
|
||||
).toEqual({
|
||||
status: 400,
|
||||
data: { success: false },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
HttpStatusCode,
|
||||
UserRequestRequestParams,
|
||||
UserRequestResponse,
|
||||
UserRequestServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
|
||||
|
||||
@injectable()
|
||||
export class UserRequestsController implements UserRequestServerInterface {
|
||||
constructor(@inject(TYPES.ProcessUserRequest) private processUserRequest: ProcessUserRequest) {}
|
||||
|
||||
async submitUserRequest(params: UserRequestRequestParams): Promise<UserRequestResponse> {
|
||||
const result = await this.processUserRequest.execute({
|
||||
requestType: params.requestType,
|
||||
userEmail: params.userEmail as string,
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,29 @@ describe('DomainEventFactory', () => {
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a EXIT_DISCOUNT_APPLY_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createExitDiscountApplyRequestedEvent({
|
||||
userEmail: '[email protected]',
|
||||
discountCode: 'exit-20',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '[email protected]',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'auth',
|
||||
},
|
||||
payload: {
|
||||
userEmail: '[email protected]',
|
||||
discountCode: 'exit-20',
|
||||
},
|
||||
type: 'EXIT_DISCOUNT_APPLY_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a WEB_SOCKET_MESSAGE_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createWebSocketMessageRequestedEvent({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DomainEventService,
|
||||
EmailMessageRequestedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
ExitDiscountApplyRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -28,6 +29,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createExitDiscountApplyRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}): ExitDiscountApplyRequestedEvent {
|
||||
return {
|
||||
type: 'EXIT_DISCOUNT_APPLY_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userEmail,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: Uuid; message: JSONString }): WebSocketMessageRequestedEvent {
|
||||
return {
|
||||
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
PredicateVerifiedEvent,
|
||||
EmailMessageRequestedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
ExitDiscountApplyRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
|
||||
@@ -83,4 +84,8 @@ export interface DomainEventFactoryInterface {
|
||||
predicate: Predicate
|
||||
predicateVerificationResult: PredicateVerificationResult
|
||||
}): PredicateVerifiedEvent
|
||||
createExitDiscountApplyRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}): ExitDiscountApplyRequestedEvent
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
const limitedDiscountPurchased = ['limited-10', 'limited-20'].includes(event.payload.discountCode as string)
|
||||
const limitedDiscountPurchased = ['limited-10', 'limited-20', 'exit-20'].includes(
|
||||
event.payload.discountCode as string,
|
||||
)
|
||||
if (limitedDiscountPurchased) {
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.LimitedDiscountOfferPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { RoleName, SubscriptionName } from '@standardnotes/common'
|
||||
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
|
||||
import { AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
describe('SubscriptionReactivatedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: SubscriptionReactivatedEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionReactivatedEventHandler(userRepository, analyticsStore, getUserAnalyticsId, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: '[email protected]',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.ProUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionReactivatedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
previousSubscriptionId: 1,
|
||||
currentSubscriptionId: 2,
|
||||
userEmail: '[email protected]',
|
||||
subscriptionName: SubscriptionName.PlusPlan,
|
||||
subscriptionExpiresAt: 5,
|
||||
discountCode: 'exit-20',
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should mark subscription reactivated activity for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionReactivatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with email: ${event.payload.userEmail}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionReactivated], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,11 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateOfflineSubscriptionEndsAt(offlineUserSubscription, event.payload.timestamp)
|
||||
await this.updateOfflineSubscriptionEndsAt(
|
||||
offlineUserSubscription,
|
||||
event.payload.subscriptionExpiresAt,
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
await this.roleService.setOfflineUserRole(offlineUserSubscription)
|
||||
|
||||
@@ -92,9 +96,10 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
|
||||
private async updateOfflineSubscriptionEndsAt(
|
||||
offlineUserSubscription: OfflineUserSubscription,
|
||||
subscriptionExpiresAt: number,
|
||||
timestamp: number,
|
||||
): Promise<void> {
|
||||
offlineUserSubscription.endsAt = timestamp
|
||||
offlineUserSubscription.endsAt = subscriptionExpiresAt
|
||||
offlineUserSubscription.updatedAt = timestamp
|
||||
|
||||
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DomainEventPublisherInterface, ExitDiscountApplyRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
|
||||
import { ProcessUserRequest } from './ProcessUserRequest'
|
||||
|
||||
describe('ProcessUserRequest', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new ProcessUserRequest(userSubscriptionRepository, domainEventFactory, domainEventPublisher)
|
||||
|
||||
beforeEach(() => {
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({
|
||||
cancelled: true,
|
||||
} as jest.Mocked<UserSubscription>)
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createExitDiscountApplyRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<ExitDiscountApplyRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should not process unsupported requests', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userEmail: '[email protected]',
|
||||
userUuid: '1-2-3',
|
||||
requestType: 'foobar' as UserRequestType,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not process uncancelled subscriptions', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({} as jest.Mocked<UserSubscription>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userEmail: '[email protected]',
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not process non existing subscriptions', async () => {
|
||||
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userEmail: '[email protected]',
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should publish an exit discount apply requested event', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userEmail: '[email protected]',
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { ProcessUserRequestDTO } from './ProcessUserRequestDTO'
|
||||
import { ProcessUserRequestResponse } from './ProcessUserRequestResponse'
|
||||
|
||||
@injectable()
|
||||
export class ProcessUserRequest implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: ProcessUserRequestDTO): Promise<ProcessUserRequestResponse> {
|
||||
if (dto.requestType !== UserRequestType.ExitDiscount) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const subscription = await this.userSubscriptionRepository.findOneByUserUuid(dto.userUuid)
|
||||
if (subscription === null || !subscription.cancelled) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createExitDiscountApplyRequestedEvent({
|
||||
userEmail: dto.userEmail,
|
||||
discountCode: 'exit-20',
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UserRequestType, Uuid } from '@standardnotes/common'
|
||||
|
||||
export type ProcessUserRequestDTO = {
|
||||
userUuid: Uuid
|
||||
userEmail: string
|
||||
requestType: UserRequestType
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type ProcessUserRequestResponse = {
|
||||
success: boolean
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { controller, BaseHttpController, results, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UserRequestsController } from '../../Controller/UserRequestsController'
|
||||
|
||||
@controller('/users/:userUuid/requests')
|
||||
export class InversifyExpressUserRequestsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.UserRequestsController) private userRequestsController: UserRequestsController) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
|
||||
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.userRequestsController.submitUserRequest({
|
||||
requestType: request.body.requestType,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userEmail: response.locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.44.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** add subscription cancelled email message identifier ([0c5305a](https://github.com/standardnotes/server/commit/0c5305acf62aae047caedca25de25049c37f7653))
|
||||
|
||||
# [1.43.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** add user request type ([d4af1d7](https://github.com/standardnotes/server/commit/d4af1d743ef914228a15f15b36a15dc9b612c704))
|
||||
|
||||
# [1.42.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add exit discount applied event ([7b797f0](https://github.com/standardnotes/server/commit/7b797f0cbabfbca4f8bf8859c613dcff38d91df3))
|
||||
|
||||
# [1.41.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.41.0",
|
||||
"version": "1.44.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -25,4 +25,6 @@ export enum EmailMessageIdentifier {
|
||||
REFUND_REQUESTED = 'REFUND_REQUESTED',
|
||||
RATE_ADJUSTMENT_NOTICE = 'RATE_ADJUSTMENT_NOTICE',
|
||||
WORKSPACE_INVITE_CREATED = 'WORKSPACE_INVITE_CREATED',
|
||||
EXIT_DISCOUNT = 'EXIT_DISCOUNT',
|
||||
SUBSCRIPTION_CANCELLED = 'SUBSCRIPTION_CANCELLED',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum UserRequestType {
|
||||
ExitDiscount = 'exit-discount',
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export * from './Subscription/SubscriptionBillingFrequency'
|
||||
export * from './Subscription/SubscriptionName'
|
||||
export * from './Type/Either'
|
||||
export * from './Type/Only'
|
||||
export * from './User/UserRequestType'
|
||||
export * from './Validator/UuidValidator'
|
||||
export * from './Validator/ValidatorInterface'
|
||||
export * from './Workspace/WorkspaceAccessLevel'
|
||||
|
||||
@@ -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.9.7](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.5](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.4](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.7",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
# [2.74.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* add analytics worker service ([d568432](https://github.com/standardnotes/server/commit/d5684326b1301855d0e07415195d4b246292f9a9))
|
||||
|
||||
## [2.73.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [2.73.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add exit discount events ([59eb70c](https://github.com/standardnotes/server/commit/59eb70ce62eddd1ac8031667bc38e495e9c17bba))
|
||||
|
||||
## [2.72.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
# [2.72.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add exit discount applied event ([7b797f0](https://github.com/standardnotes/server/commit/7b797f0cbabfbca4f8bf8859c613dcff38d91df3))
|
||||
|
||||
# [2.71.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-events:** add subscription reactivated event ([6e8481b](https://github.com/standardnotes/server/commit/6e8481bb2f271f3b9e86ca7d2ac683f1fd6f6516))
|
||||
|
||||
# [2.70.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.70.0",
|
||||
"version": "2.74.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum DomainEventService {
|
||||
Files = 'files',
|
||||
Scheduler = 'scheduler',
|
||||
Workspace = 'workspace',
|
||||
Analytics = 'analytics',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { ExitDiscountAppliedEventPayload } from './ExitDiscountAppliedEventPayload'
|
||||
|
||||
export interface ExitDiscountAppliedEvent extends DomainEventInterface {
|
||||
type: 'EXIT_DISCOUNT_APPLIED'
|
||||
payload: ExitDiscountAppliedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ExitDiscountAppliedEventPayload {
|
||||
userEmail: string
|
||||
discountRate: number
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { ExitDiscountApplyRequestedEventPayload } from './ExitDiscountApplyRequestedEventPayload'
|
||||
|
||||
export interface ExitDiscountApplyRequestedEvent extends DomainEventInterface {
|
||||
type: 'EXIT_DISCOUNT_APPLY_REQUESTED'
|
||||
payload: ExitDiscountApplyRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ExitDiscountApplyRequestedEventPayload {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { ExitDiscountWithdrawRequestedEventPayload } from './ExitDiscountWithdrawRequestedEventPayload'
|
||||
|
||||
export interface ExitDiscountWithdrawRequestedEvent extends DomainEventInterface {
|
||||
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED'
|
||||
payload: ExitDiscountWithdrawRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ExitDiscountWithdrawRequestedEventPayload {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { SubscriptionReactivatedEventPayload } from './SubscriptionReactivatedEventPayload'
|
||||
|
||||
export interface SubscriptionReactivatedEvent extends DomainEventInterface {
|
||||
type: 'SUBSCRIPTION_REACTIVATED'
|
||||
payload: SubscriptionReactivatedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
|
||||
export interface SubscriptionReactivatedEventPayload {
|
||||
userEmail: string
|
||||
previousSubscriptionId: number
|
||||
currentSubscriptionId: number
|
||||
subscriptionName: SubscriptionName
|
||||
subscriptionExpiresAt: number
|
||||
discountCode: string | null
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { SubscriptionReactivationDiscountRequestedEventPayload } from './SubscriptionReactivationDiscountRequestedEventPayload'
|
||||
|
||||
export interface SubscriptionReactivationDiscountRequestedEvent extends DomainEventInterface {
|
||||
type: 'SUBSCRIPTION_REACTIVATION_DISCOUNT_REQUESTED'
|
||||
payload: SubscriptionReactivationDiscountRequestedEventPayload
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export interface SubscriptionReactivationDiscountRequestedEventPayload {
|
||||
userUuid: string
|
||||
discountCode: string
|
||||
}
|
||||
@@ -30,6 +30,12 @@ export * from './Event/EmailBackupRequestedEvent'
|
||||
export * from './Event/EmailBackupRequestedEventPayload'
|
||||
export * from './Event/EmailMessageRequestedEvent'
|
||||
export * from './Event/EmailMessageRequestedEventPayload'
|
||||
export * from './Event/ExitDiscountAppliedEvent'
|
||||
export * from './Event/ExitDiscountAppliedEventPayload'
|
||||
export * from './Event/ExitDiscountApplyRequestedEvent'
|
||||
export * from './Event/ExitDiscountApplyRequestedEventPayload'
|
||||
export * from './Event/ExitDiscountWithdrawRequestedEvent'
|
||||
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
|
||||
export * from './Event/ExtensionKeyGrantedEvent'
|
||||
export * from './Event/ExtensionKeyGrantedEventPayload'
|
||||
export * from './Event/FileRemovedEvent'
|
||||
@@ -78,8 +84,8 @@ export * from './Event/SubscriptionPurchasedEvent'
|
||||
export * from './Event/SubscriptionPurchasedEventPayload'
|
||||
export * from './Event/SubscriptionRateAdjustedEvent'
|
||||
export * from './Event/SubscriptionRateAdjustedEventPayload'
|
||||
export * from './Event/SubscriptionReactivationDiscountRequestedEvent'
|
||||
export * from './Event/SubscriptionReactivationDiscountRequestedEventPayload'
|
||||
export * from './Event/SubscriptionReactivatedEvent'
|
||||
export * from './Event/SubscriptionReactivatedEventPayload'
|
||||
export * from './Event/SubscriptionReassignedEvent'
|
||||
export * from './Event/SubscriptionReassignedEventPayload'
|
||||
export * from './Event/SubscriptionRefundedEvent'
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **event-store:** add discount events to event store ([eb53c38](https://github.com/standardnotes/server/commit/eb53c3896f8c64747e30901722c0bdc63125128f))
|
||||
|
||||
## [1.5.8](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.5.7](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.5.6](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.5.5](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* force utf8mb4 charset on typeorm ([5160cc3](https://github.com/standardnotes/server/commit/5160cc36ddc9e30551d5ad40a9e210d87091eec3))
|
||||
|
||||
## [1.5.4](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **event-store:** add subscription reactivate handler ([9589403](https://github.com/standardnotes/server/commit/9589403c9de9304f1183789e111f6e4cf58cb7ff))
|
||||
|
||||
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.5.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -82,11 +82,17 @@ export class ContainerConfigLoader {
|
||||
['SUBSCRIPTION_REVERT_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['REFUND_PROCESSED', container.get(TYPES.EventHandler)],
|
||||
['ACCOUNT_RESET_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['DISCOUNT_APPLY_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['DISCOUNT_APPLIED', container.get(TYPES.EventHandler)],
|
||||
['DISCOUNT_WITHDRAW_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_RATE_ADJUSTED', container.get(TYPES.EventHandler)],
|
||||
['REFUND_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['INVOICE_GENERATED', container.get(TYPES.EventHandler)],
|
||||
['WORKSPACE_INVITE_CREATED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.EventHandler)],
|
||||
['EXIT_DISCOUNT_APPLY_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['EXIT_DISCOUNT_APPLIED', container.get(TYPES.EventHandler)],
|
||||
['EXIT_DISCOUNT_WITHDRAW_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
])
|
||||
|
||||
container
|
||||
|
||||
@@ -11,6 +11,7 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.2](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.1](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/files/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
## [1.7.5](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.7.4](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.7.3](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.7.2](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.7.1](https://github.com/standardnotes/files/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.7.1",
|
||||
"version": "1.8.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"aws-sdk": "^2.1158.0",
|
||||
"connect-busboy": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-winston": "^4.0.5",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/predicates",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scheduler:** specs ([dcb20e6](https://github.com/standardnotes/server/commit/dcb20e6ea612d8ca2e169e12e33c268855517962))
|
||||
|
||||
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **scheduler:** add publishing exit discount withdraw requested event ([d66f784](https://github.com/standardnotes/server/commit/d66f78453820c8a97d090f858d9a4fc2557773fa))
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/server/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
## [1.11.7](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.11.6](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.11.5](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* force utf8mb4 charset on typeorm ([5160cc3](https://github.com/standardnotes/server/commit/5160cc36ddc9e30551d5ad40a9e210d87091eec3))
|
||||
|
||||
## [1.11.4](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.11.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.11.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.11.2",
|
||||
"version": "1.13.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -32,7 +32,7 @@
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"aws-sdk": "^2.1158.0",
|
||||
"dayjs": "^1.11.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.2.0",
|
||||
|
||||
@@ -36,6 +36,7 @@ import { PredicateVerifiedEventHandler } from '../Domain/Handler/PredicateVerifi
|
||||
import { VerifyPredicates } from '../Domain/UseCase/VerifyPredicates/VerifyPredicates'
|
||||
import { UserRegisteredEventHandler } from '../Domain/Handler/UserRegisteredEventHandler'
|
||||
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
|
||||
import { ExitDiscountAppliedEventHandler } from '../Domain/Handler/ExitDiscountAppliedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -124,6 +125,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionCancelledEventHandler>(TYPES.SubscriptionCancelledEventHandler)
|
||||
.to(SubscriptionCancelledEventHandler)
|
||||
container
|
||||
.bind<ExitDiscountAppliedEventHandler>(TYPES.ExitDiscountAppliedEventHandler)
|
||||
.to(ExitDiscountAppliedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
@@ -146,6 +150,7 @@ export class ContainerConfigLoader {
|
||||
['PREDICATE_VERIFIED', container.get(TYPES.PredicateVerifiedEventHandler)],
|
||||
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
||||
['SUBSCRIPTION_CANCELLED', container.get(TYPES.SubscriptionCancelledEventHandler)],
|
||||
['EXIT_DISCOUNT_APPLIED', container.get(TYPES.ExitDiscountAppliedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
|
||||
@@ -24,6 +24,7 @@ const TYPES = {
|
||||
PredicateVerifiedEventHandler: Symbol.for('PredicateVerifiedEventHandler'),
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
SubscriptionCancelledEventHandler: Symbol.for('SubscriptionCancelledEventHandler'),
|
||||
ExitDiscountAppliedEventHandler: Symbol.for('ExitDiscountAppliedEventHandler'),
|
||||
// Services
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -65,6 +65,29 @@ describe('DomainEventFactory', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a EXIT_DISCOUNT_WITHDRAW_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createExitDiscountWithdrawRequestedEvent({
|
||||
userEmail: '[email protected]',
|
||||
discountCode: 'exit-20',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '[email protected]',
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: 'scheduler',
|
||||
},
|
||||
payload: {
|
||||
userEmail: '[email protected]',
|
||||
discountCode: 'exit-20',
|
||||
},
|
||||
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
|
||||
expect(
|
||||
createFactory().createEmailMessageRequestedEvent({
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DiscountWithdrawRequestedEvent,
|
||||
DomainEventService,
|
||||
EmailMessageRequestedEvent,
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
PredicateVerificationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { PredicateAuthority } from '@standardnotes/predicates'
|
||||
@@ -51,6 +52,24 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createExitDiscountWithdrawRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}): ExitDiscountWithdrawRequestedEvent {
|
||||
return {
|
||||
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userEmail,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Scheduler,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createEmailMessageRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: EmailMessageIdentifier
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DiscountApplyRequestedEvent,
|
||||
DiscountWithdrawRequestedEvent,
|
||||
EmailMessageRequestedEvent,
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
PredicateVerificationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
@@ -18,4 +19,8 @@ export interface DomainEventFactoryInterface {
|
||||
}): EmailMessageRequestedEvent
|
||||
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
|
||||
createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
|
||||
createExitDiscountWithdrawRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
discountCode: string
|
||||
}): ExitDiscountWithdrawRequestedEvent
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ExitDiscountAppliedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { JobRepositoryInterface } from '../Job/JobRepositoryInterface'
|
||||
|
||||
import { ExitDiscountAppliedEventHandler } from './ExitDiscountAppliedEventHandler'
|
||||
|
||||
describe('ExitDiscountAppliedEventHandler', () => {
|
||||
let timer: TimerInterface
|
||||
let jobRepository: JobRepositoryInterface
|
||||
|
||||
const createHandler = () => new ExitDiscountAppliedEventHandler(timer, jobRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDateNHoursAhead = jest.fn().mockReturnValue(new Date(2))
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123)
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
jobRepository = {} as jest.Mocked<JobRepositoryInterface>
|
||||
jobRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should schedule a job to do an exit discount withdrawal', async () => {
|
||||
await createHandler().handle({
|
||||
payload: { userEmail: '[email protected]', discountRate: 20 },
|
||||
} as jest.Mocked<ExitDiscountAppliedEvent>)
|
||||
|
||||
expect(jobRepository.save).toHaveBeenNthCalledWith(1, {
|
||||
createdAt: 1,
|
||||
name: 'withdraw-subscription-exit-discount',
|
||||
scheduledAt: 123,
|
||||
status: 'pending',
|
||||
userIdentifier: '[email protected]',
|
||||
userIdentifierType: 'email',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DomainEventHandlerInterface, ExitDiscountAppliedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Job } from '../Job/Job'
|
||||
import { JobName } from '../Job/JobName'
|
||||
import { JobRepositoryInterface } from '../Job/JobRepositoryInterface'
|
||||
import { JobStatus } from '../Job/JobStatus'
|
||||
|
||||
@injectable()
|
||||
export class ExitDiscountAppliedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.JobRepository) private jobRepository: JobRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: ExitDiscountAppliedEvent): Promise<void> {
|
||||
await this.scheduleExitDiscountWithdraw(event)
|
||||
}
|
||||
|
||||
private async scheduleExitDiscountWithdraw(event: ExitDiscountAppliedEvent): Promise<void> {
|
||||
const job = new Job()
|
||||
job.name = JobName.WITHDRAW_SUBSCRIPTION_EXIT_DISCOUNT
|
||||
job.scheduledAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNHoursAhead(24))
|
||||
job.createdAt = this.timer.getTimestampInMicroseconds()
|
||||
job.status = JobStatus.Pending
|
||||
job.userIdentifier = event.payload.userEmail
|
||||
job.userIdentifierType = 'email'
|
||||
|
||||
await this.jobRepository.save(job)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DiscountWithdrawRequestedEvent,
|
||||
DomainEventPublisherInterface,
|
||||
EmailMessageRequestedEvent,
|
||||
ExitDiscountWithdrawRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { PredicateName } from '@standardnotes/predicates'
|
||||
import 'reflect-metadata'
|
||||
@@ -48,6 +49,9 @@ describe('JobDoneInterpreter', () => {
|
||||
domainEventFactory.createDiscountWithdrawRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DiscountWithdrawRequestedEvent>)
|
||||
domainEventFactory.createExitDiscountWithdrawRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<ExitDiscountWithdrawRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
@@ -253,6 +257,35 @@ describe('JobDoneInterpreter', () => {
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should request exit discount withdraw', async () => {
|
||||
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
|
||||
name: JobName.WITHDRAW_SUBSCRIPTION_EXIT_DISCOUNT,
|
||||
userIdentifier: '[email protected]',
|
||||
userIdentifierType: 'email',
|
||||
} as jest.Mocked<Job>)
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createExitDiscountWithdrawRequestedEvent).toHaveBeenCalledWith({
|
||||
userEmail: '[email protected]',
|
||||
discountCode: 'exit-20',
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not request exit discount withdraw if email is missing', async () => {
|
||||
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
|
||||
name: JobName.WITHDRAW_SUBSCRIPTION_EXIT_DISCOUNT,
|
||||
userIdentifier: '2-3-4',
|
||||
userIdentifierType: 'uuid',
|
||||
} as jest.Mocked<Job>)
|
||||
|
||||
await createInterpreter().interpret('1-2-3')
|
||||
|
||||
expect(domainEventFactory.createExitDiscountWithdrawRequestedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing if there is no interpretation for a given job', async () => {
|
||||
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
|
||||
name: 'foobar' as JobName,
|
||||
|
||||
@@ -65,6 +65,11 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
await this.requestDiscountWithdraw(job)
|
||||
}
|
||||
return
|
||||
case JobName.WITHDRAW_SUBSCRIPTION_EXIT_DISCOUNT:
|
||||
if (job.userIdentifierType === 'email') {
|
||||
await this.requestExitDiscountWithdraw(job)
|
||||
}
|
||||
return
|
||||
default:
|
||||
this.logger.warn(`[${jobUuid}]${job.name}: job is not interpretable.`)
|
||||
|
||||
@@ -132,6 +137,17 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
|
||||
)
|
||||
}
|
||||
|
||||
private async requestExitDiscountWithdraw(job: Job): Promise<void> {
|
||||
this.logger.debug(`[${job.uuid}]${job.name}: requesting exit discount withdraw.`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createExitDiscountWithdrawRequestedEvent({
|
||||
userEmail: job.userIdentifier,
|
||||
discountCode: 'exit-20',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async predicatesAreFulfilled(job: Job): Promise<boolean> {
|
||||
const predicates = await this.predicateRepository.findByJobUuid(job.uuid)
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export enum JobName {
|
||||
EXIT_INTERVIEW = 'exit-interview',
|
||||
APPLY_SUBSCRIPTION_DISCOUNT = 'apply-subscription-discount',
|
||||
WITHDRAW_SUBSCRIPTION_DISCOUNT = 'withdraw-subscription-discount',
|
||||
WITHDRAW_SUBSCRIPTION_EXIT_DISCOUNT = 'withdraw-subscription-exit-discount',
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* force utf8mb4 charset on typeorm ([5160cc3](https://github.com/standardnotes/syncing-server-js/commit/5160cc36ddc9e30551d5ad40a9e210d87091eec3))
|
||||
|
||||
## [1.10.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.10.1",
|
||||
"version": "1.10.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/server/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/time",
|
||||
"version": "1.12.0",
|
||||
"version": "1.13.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"test": "jest spec --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.10.8",
|
||||
"dayjs": "^1.11.6",
|
||||
"microtime": "^3.1.0",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
},
|
||||
|
||||
@@ -35,9 +35,9 @@ describe('Timer', () => {
|
||||
})
|
||||
|
||||
it('should calculate days difference between now and a given date', () => {
|
||||
const dateNDaysAgo = createTimer().getUTCDateNDaysAgo(4)
|
||||
const dateNDaysAgo = createTimer().getUTCDateNDaysAgo(3)
|
||||
|
||||
expect(createTimer().dateWasNDaysAgo(dateNDaysAgo)).toEqual(4)
|
||||
expect(createTimer().dateWasNDaysAgo(dateNDaysAgo)).toEqual(3)
|
||||
})
|
||||
|
||||
it('should return a utc date n hours ago', () => {
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
# [1.4.0](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add processing user requests ([2255f85](https://github.com/standardnotes/server/commit/2255f856f928e855ac94f8aca4e1fb81047f58f7))
|
||||
|
||||
## [1.3.5](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.3.4](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.3.3](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.3.2](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.3.1](https://github.com/standardnotes/server/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user