mirror of
https://github.com/standardnotes/server
synced 2026-05-10 18:57:20 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0406dd8aa | |||
| 8d152ddfcb | |||
| 1a16d2e4f4 | |||
| 1ca8531305 | |||
| 6190e7d092 | |||
| a6542dd638 | |||
| 840777a851 | |||
| 5c9dff38c9 | |||
| abfbacb8c2 | |||
| 03afdbf431 | |||
| 507d43b328 | |||
| be214c0599 | |||
| 91f36c3a3f | |||
| f60c15ed2e | |||
| 1ec072373d | |||
| a7d039082e | |||
| d5c06bfa58 | |||
| c8f3a0ce7b | |||
| edbedc181b | |||
| 94afa34780 |
@@ -0,0 +1,46 @@
|
|||||||
|
name: Revisions Server
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: revisions_server
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*standardnotes/revisions-server*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
call_server_application_workflow:
|
||||||
|
name: Server Application
|
||||||
|
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
||||||
|
with:
|
||||||
|
service_name: revisions
|
||||||
|
workspace_name: "@standardnotes/revisions-server"
|
||||||
|
e2e_tag_parameter_name: revisions_image_tag
|
||||||
|
package_path: packages/revisions
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
newrelic:
|
||||||
|
needs: call_server_application_workflow
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create New Relic deployment marker for Web
|
||||||
|
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_REVISIONS_WEB_PROD }}
|
||||||
|
revision: "${{ github.sha }}"
|
||||||
|
description: "Automated Deployment via Github Actions"
|
||||||
|
user: "${{ github.actor }}"
|
||||||
|
- 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_REVISIONS_WORKER_PROD }}
|
||||||
|
revision: "${{ github.sha }}"
|
||||||
|
description: "Automated Deployment via Github Actions"
|
||||||
|
user: "${{ github.actor }}"
|
||||||
@@ -53,6 +53,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
"name": "@standardnotes/predicates",\
|
"name": "@standardnotes/predicates",\
|
||||||
"reference": "workspace:packages/predicates"\
|
"reference": "workspace:packages/predicates"\
|
||||||
},\
|
},\
|
||||||
|
{\
|
||||||
|
"name": "@standardnotes/revisions-server",\
|
||||||
|
"reference": "workspace:packages/revisions"\
|
||||||
|
},\
|
||||||
{\
|
{\
|
||||||
"name": "@standardnotes/scheduler-server",\
|
"name": "@standardnotes/scheduler-server",\
|
||||||
"reference": "workspace:packages/scheduler"\
|
"reference": "workspace:packages/scheduler"\
|
||||||
@@ -99,6 +103,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@standardnotes/event-store", ["workspace:packages/event-store"]],\
|
["@standardnotes/event-store", ["workspace:packages/event-store"]],\
|
||||||
["@standardnotes/files-server", ["workspace:packages/files"]],\
|
["@standardnotes/files-server", ["workspace:packages/files"]],\
|
||||||
["@standardnotes/predicates", ["workspace:packages/predicates"]],\
|
["@standardnotes/predicates", ["workspace:packages/predicates"]],\
|
||||||
|
["@standardnotes/revisions-server", ["workspace:packages/revisions"]],\
|
||||||
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
|
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
|
||||||
["@standardnotes/security", ["workspace:packages/security"]],\
|
["@standardnotes/security", ["workspace:packages/security"]],\
|
||||||
["@standardnotes/server-monorepo", ["workspace:."]],\
|
["@standardnotes/server-monorepo", ["workspace:."]],\
|
||||||
@@ -3004,6 +3009,52 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@standardnotes/revisions-server", [\
|
||||||
|
["workspace:packages/revisions", {\
|
||||||
|
"packageLocation": "./packages/revisions/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||||
|
["@newrelic/native-metrics", "npm:9.0.0"],\
|
||||||
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
|
["@sentry/node", "npm:7.19.0"],\
|
||||||
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
|
["@standardnotes/security", "workspace:packages/security"],\
|
||||||
|
["@standardnotes/time", "workspace:packages/time"],\
|
||||||
|
["@types/cors", "npm:2.8.12"],\
|
||||||
|
["@types/dotenv", "npm:8.2.0"],\
|
||||||
|
["@types/express", "npm:4.17.14"],\
|
||||||
|
["@types/inversify-express-utils", "npm:2.0.0"],\
|
||||||
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
|
["@types/jest", "npm:29.1.1"],\
|
||||||
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
|
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
|
||||||
|
["aws-sdk", "npm:2.1253.0"],\
|
||||||
|
["cors", "npm:2.8.5"],\
|
||||||
|
["dotenv", "npm:16.0.1"],\
|
||||||
|
["eslint", "npm:8.25.0"],\
|
||||||
|
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
||||||
|
["express", "npm:4.18.2"],\
|
||||||
|
["helmet", "npm:6.0.0"],\
|
||||||
|
["inversify", "npm:6.0.1"],\
|
||||||
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
|
["ioredis", "npm:5.2.4"],\
|
||||||
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
|
["mysql2", "npm:2.3.3"],\
|
||||||
|
["newrelic", "npm:9.6.0"],\
|
||||||
|
["npm-check-updates", "npm:16.0.1"],\
|
||||||
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
|
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
||||||
|
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
|
||||||
|
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
|
||||||
|
["winston", "npm:3.8.2"]\
|
||||||
|
],\
|
||||||
|
"linkType": "SOFT"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@standardnotes/scheduler-server", [\
|
["@standardnotes/scheduler-server", [\
|
||||||
["workspace:packages/scheduler", {\
|
["workspace:packages/scheduler", {\
|
||||||
"packageLocation": "./packages/scheduler/",\
|
"packageLocation": "./packages/scheduler/",\
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"lint:websockets": "yarn workspace @standardnotes/websockets-server lint",
|
"lint:websockets": "yarn workspace @standardnotes/websockets-server lint",
|
||||||
"lint:workspace": "yarn workspace @standardnotes/workspace-server lint",
|
"lint:workspace": "yarn workspace @standardnotes/workspace-server lint",
|
||||||
"lint:analytics": "yarn workspace @standardnotes/analytics lint",
|
"lint:analytics": "yarn workspace @standardnotes/analytics lint",
|
||||||
|
"lint:revisions": "yarn workspace @standardnotes/revisions-server lint",
|
||||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||||
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
|
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
|
||||||
"start:auth": "yarn workspace @standardnotes/auth-server start",
|
"start:auth": "yarn workspace @standardnotes/auth-server start",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"start:websockets": "yarn workspace @standardnotes/websockets-server start",
|
"start:websockets": "yarn workspace @standardnotes/websockets-server start",
|
||||||
"start:workspace": "yarn workspace @standardnotes/workspace-server start",
|
"start:workspace": "yarn workspace @standardnotes/workspace-server start",
|
||||||
"start:analytics": "yarn workspace @standardnotes/analytics worker",
|
"start:analytics": "yarn workspace @standardnotes/analytics worker",
|
||||||
|
"start:revisions": "yarn workspace @standardnotes/revisions-server start",
|
||||||
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
|
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
|
||||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.11.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.4...@standardnotes/analytics@2.11.5) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.3...@standardnotes/analytics@2.11.4) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.2...@standardnotes/analytics@2.11.3) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.1...@standardnotes/analytics@2.11.2) (2022-11-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** specs ([507d43b](https://github.com/standardnotes/server/commit/507d43b3289d1e178644df6d3e15d1d55e56c7bb))
|
||||||
|
|
||||||
|
## [2.11.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.0...@standardnotes/analytics@2.11.1) (2022-11-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mapper interface imports ([1ec0723](https://github.com/standardnotes/server/commit/1ec072373d640c4e2f24b9bb12fec0c678b48032))
|
||||||
|
|
||||||
# [2.11.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.3...@standardnotes/analytics@2.11.0) (2022-11-16)
|
# [2.11.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.3...@standardnotes/analytics@2.11.0) (2022-11-16)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/analytics",
|
"name": "@standardnotes/analytics",
|
||||||
"version": "2.11.0",
|
"version": "2.11.5",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
DomainEventMessageHandlerInterface,
|
DomainEventMessageHandlerInterface,
|
||||||
DomainEventSubscriberFactoryInterface,
|
DomainEventSubscriberFactoryInterface,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { MapInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { Env } from './Env'
|
import { Env } from './Env'
|
||||||
import TYPES from './Types'
|
import TYPES from './Types'
|
||||||
@@ -172,7 +172,7 @@ export class ContainerConfigLoader {
|
|||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
container
|
container
|
||||||
.bind<MapInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
||||||
.to(RevenueModificationMap)
|
.to(RevenueModificationMap)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { injectable } from 'inversify'
|
import { injectable } from 'inversify'
|
||||||
import { Email, MapInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
import { Email, MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
|
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
|
||||||
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
|
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
|
||||||
@@ -10,7 +10,7 @@ import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
|||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class RevenueModificationMap implements MapInterface<RevenueModification, TypeORMRevenueModification> {
|
export class RevenueModificationMap implements MapperInterface<RevenueModification, TypeORMRevenueModification> {
|
||||||
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
|
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
|
||||||
const userOrError = User.create(
|
const userOrError = User.create(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('GetUserAnalyticsId', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
analyticsEntity = {
|
analyticsEntity = {
|
||||||
id: 123,
|
id: 123,
|
||||||
userUuid: '1-2-3',
|
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||||
userEmail: 'test@test.te',
|
userEmail: 'test@test.te',
|
||||||
} as jest.Mocked<AnalyticsEntity>
|
} as jest.Mocked<AnalyticsEntity>
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ describe('GetUserAnalyticsId', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return analytics id for a user by uuid', async () => {
|
it('should return analytics id for a user by uuid', async () => {
|
||||||
expect(await (await createUseCase().execute({ userUuid: '1-2-3' })).analyticsId).toEqual(123)
|
expect((await createUseCase().execute({ userUuid: '1-2-3' })).analyticsId).toEqual(123)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return analytics id for a user by email', async () => {
|
it('should return analytics id for a user by email', async () => {
|
||||||
expect(await (await createUseCase().execute({ userEmail: 'test@test.te' })).analyticsId).toEqual(123)
|
expect((await createUseCase().execute({ userEmail: 'test@test.te' })).analyticsId).toEqual(123)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error if user is missing analytics entity', async () => {
|
it('should throw error if user is missing analytics entity', async () => {
|
||||||
|
|||||||
+9
-9
@@ -46,7 +46,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeFalsy()
|
expect(revenueOrError.isFailed()).toBeFalsy()
|
||||||
@@ -64,7 +64,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeFalsy()
|
expect(revenueOrError.isFailed()).toBeFalsy()
|
||||||
@@ -82,7 +82,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeFalsy()
|
expect(revenueOrError.isFailed()).toBeFalsy()
|
||||||
@@ -102,7 +102,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeFalsy()
|
expect(revenueOrError.isFailed()).toBeFalsy()
|
||||||
@@ -123,7 +123,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeTruthy()
|
expect(revenueOrError.isFailed()).toBeTruthy()
|
||||||
@@ -143,7 +143,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeTruthy()
|
expect(revenueOrError.isFailed()).toBeTruthy()
|
||||||
@@ -163,7 +163,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeTruthy()
|
expect(revenueOrError.isFailed()).toBeTruthy()
|
||||||
@@ -183,7 +183,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeTruthy()
|
expect(revenueOrError.isFailed()).toBeTruthy()
|
||||||
@@ -203,7 +203,7 @@ describe('SaveRevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
subscriptionId: 1234,
|
subscriptionId: 1234,
|
||||||
userEmail: Email.create('test@test.te').getValue(),
|
userEmail: Email.create('test@test.te').getValue(),
|
||||||
userUuid: Uuid.create('1-2-3').getValue(),
|
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revenueOrError.isFailed()).toBeTruthy()
|
expect(revenueOrError.isFailed()).toBeTruthy()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { MapInterface, Uuid } from '@standardnotes/domain-core'
|
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { RevenueModification } from '../../Domain/Revenue/RevenueModification'
|
import { RevenueModification } from '../../Domain/Revenue/RevenueModification'
|
||||||
@@ -13,7 +13,7 @@ export class MySQLRevenueModificationRepository implements RevenueModificationRe
|
|||||||
@inject(TYPES.ORMRevenueModificationRepository)
|
@inject(TYPES.ORMRevenueModificationRepository)
|
||||||
private ormRepository: Repository<TypeORMRevenueModification>,
|
private ormRepository: Repository<TypeORMRevenueModification>,
|
||||||
@inject(TYPES.RevenueModificationMap)
|
@inject(TYPES.RevenueModificationMap)
|
||||||
private revenueModificationMap: MapInterface<RevenueModification, TypeORMRevenueModification>,
|
private revenueModificationMap: MapperInterface<RevenueModification, TypeORMRevenueModification>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sumMRRDiff(dto: { billingFrequencies: number[]; planNames?: string[] }): Promise<number> {
|
async sumMRRDiff(dto: { billingFrequencies: number[]; planNames?: string[] }): Promise<number> {
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.38.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.7...@standardnotes/api-gateway@1.38.8) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.38.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.6...@standardnotes/api-gateway@1.38.7) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
## [1.38.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.5...@standardnotes/api-gateway@1.38.6) (2022-11-16)
|
## [1.38.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.5...@standardnotes/api-gateway@1.38.6) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/api-gateway",
|
"name": "@standardnotes/api-gateway",
|
||||||
"version": "1.38.6",
|
"version": "1.38.8",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.60.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.2...@standardnotes/auth-server@1.60.3) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/auth-server
|
||||||
|
|
||||||
|
## [1.60.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.1...@standardnotes/auth-server@1.60.2) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/auth-server
|
||||||
|
|
||||||
## [1.60.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.0...@standardnotes/auth-server@1.60.1) (2022-11-16)
|
## [1.60.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.0...@standardnotes/auth-server@1.60.1) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/auth-server
|
**Note:** Version bump only for package @standardnotes/auth-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/auth-server",
|
"name": "@standardnotes/auth-server",
|
||||||
"version": "1.60.1",
|
"version": "1.60.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,19 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.2.0...@standardnotes/domain-core@1.2.1) (2022-11-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **domain-core:** remove revisions related models to revisions microservice ([a6542dd](https://github.com/standardnotes/server/commit/a6542dd63870a8ada5fd8143d8e2133a570d9329))
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.1.1...@standardnotes/domain-core@1.2.0) (2022-11-18)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **domain-core:** add revision definition to domain core ([c8f3a0c](https://github.com/standardnotes/server/commit/c8f3a0ce7b589a6fbc47941fc5d1a44b6cf04fe3))
|
||||||
|
* **revisions:** add revisions microservice ([d5c06bf](https://github.com/standardnotes/server/commit/d5c06bfa58a987685fbd8fbab0d22df3fcff3377))
|
||||||
|
|
||||||
## [1.1.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.1.0...@standardnotes/domain-core@1.1.1) (2022-11-14)
|
## [1.1.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.1.0...@standardnotes/domain-core@1.1.1) (2022-11-14)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/domain-core",
|
"name": "@standardnotes/domain-core",
|
||||||
"version": "1.1.1",
|
"version": "1.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Timestamps } from './Timestamps'
|
||||||
|
|
||||||
|
describe('Timestamps', () => {
|
||||||
|
it('should create a value object', () => {
|
||||||
|
const valueOrError = Timestamps.create(new Date(1), new Date(2))
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeFalsy()
|
||||||
|
expect(valueOrError.getValue().createdAt).toEqual(new Date(1))
|
||||||
|
expect(valueOrError.getValue().updatedAt).toEqual(new Date(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create an invalid value object', () => {
|
||||||
|
let valueOrError = Timestamps.create(null as unknown as Date, '2' as unknown as Date)
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeTruthy()
|
||||||
|
|
||||||
|
valueOrError = Timestamps.create(new Date(2), '2' as unknown as Date)
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Result } from '../Core/Result'
|
||||||
|
import { ValueObject } from '../Core/ValueObject'
|
||||||
|
import { TimestampsProps } from './TimestampsProps'
|
||||||
|
|
||||||
|
export class Timestamps extends ValueObject<TimestampsProps> {
|
||||||
|
get createdAt(): Date {
|
||||||
|
return this.props.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
get updatedAt(): Date {
|
||||||
|
return this.props.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: TimestampsProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(createdAt: Date, updatedAt: Date): Result<Timestamps> {
|
||||||
|
if (!(createdAt instanceof Date)) {
|
||||||
|
return Result.fail<Timestamps>(
|
||||||
|
`Could not create Timestamps. Creation date should be a date object, given: ${createdAt}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!(updatedAt instanceof Date)) {
|
||||||
|
return Result.fail<Timestamps>(
|
||||||
|
`Could not create Timestamps. Update date should be a date object, given: ${createdAt}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<Timestamps>(new Timestamps({ createdAt, updatedAt }))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface TimestampsProps {
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@ import { Uuid } from './Uuid'
|
|||||||
|
|
||||||
describe('Uuid', () => {
|
describe('Uuid', () => {
|
||||||
it('should create a value object', () => {
|
it('should create a value object', () => {
|
||||||
const valueOrError = Uuid.create('1-2-3')
|
const valueOrError = Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d')
|
||||||
|
|
||||||
expect(valueOrError.isFailed()).toBeFalsy()
|
expect(valueOrError.isFailed()).toBeFalsy()
|
||||||
expect(valueOrError.getValue().value).toEqual('1-2-3')
|
expect(valueOrError.getValue().value).toEqual('84c0f8e8-544a-4c7e-9adf-26209303bc1d')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not create an invalid value object', () => {
|
it('should not create an invalid value object', () => {
|
||||||
const valueOrError = Uuid.create('')
|
const valueOrError = Uuid.create('1-2-3')
|
||||||
|
|
||||||
expect(valueOrError.isFailed()).toBeTruthy()
|
expect(valueOrError.isFailed()).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ValueObject } from '../Core/ValueObject'
|
import { ValueObject } from '../Core/ValueObject'
|
||||||
import { Result } from '../Core/Result'
|
import { Result } from '../Core/Result'
|
||||||
import { UuidProps } from './UuidProps'
|
import { UuidProps } from './UuidProps'
|
||||||
|
import { Validator } from '../Core/Validator'
|
||||||
|
|
||||||
export class Uuid extends ValueObject<UuidProps> {
|
export class Uuid extends ValueObject<UuidProps> {
|
||||||
get value(): string {
|
get value(): string {
|
||||||
@@ -12,8 +13,9 @@ export class Uuid extends ValueObject<UuidProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static create(uuid: string): Result<Uuid> {
|
static create(uuid: string): Result<Uuid> {
|
||||||
if (!!uuid === false || uuid.length === 0) {
|
const validUuidOrError = Validator.isValidUuid(uuid)
|
||||||
return Result.fail<Uuid>('Uuid cannot be empty')
|
if (validUuidOrError.isFailed()) {
|
||||||
|
return Result.fail<Uuid>(validUuidOrError.getError())
|
||||||
} else {
|
} else {
|
||||||
return Result.ok<Uuid>(new Uuid({ value: uuid }))
|
return Result.ok<Uuid>(new Uuid({ value: uuid }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Validator } from './Validator'
|
||||||
|
|
||||||
|
describe('Validator', () => {
|
||||||
|
const validUuids = [
|
||||||
|
'2221101c-1da9-4d2b-9b32-b8be2a8d1c82',
|
||||||
|
'c08f2f29-a74b-42b4-aefd-98af9832391c',
|
||||||
|
'b453fa64-1493-443b-b5bb-bca7b9c696c7',
|
||||||
|
]
|
||||||
|
|
||||||
|
const invalidUuids = [
|
||||||
|
123,
|
||||||
|
'someone@127.0.0.1',
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
'b453fa64-1493-443b-b5bb-ca7b9c696c7',
|
||||||
|
'c08f*f29-a74b-42b4-aefd-98af9832391c',
|
||||||
|
'c08f*f29-a74b-42b4-aefd-98af9832391c',
|
||||||
|
'../../escaped.sh',
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should validate proper uuids', () => {
|
||||||
|
for (const validUuid of validUuids) {
|
||||||
|
expect(Validator.isValidUuid(validUuid).isFailed()).toBeFalsy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not validate invalid uuids', () => {
|
||||||
|
for (const invalidUuid of invalidUuids) {
|
||||||
|
expect(Validator.isValidUuid(invalidUuid as string).isFailed()).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Result } from './Result'
|
||||||
|
|
||||||
|
export class Validator {
|
||||||
|
private static readonly UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||||
|
|
||||||
|
static isValidUuid(value: string): Result<string> {
|
||||||
|
const matchesUuidRegex = String(value).toLowerCase().match(Validator.UUID_REGEX) !== null
|
||||||
|
if (matchesUuidRegex) {
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.fail(`Given value is not a valid uuid: ${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface MapInterface<T, U> {
|
|
||||||
toDomain(persistence: U): T
|
|
||||||
toProjection(domain: T): U
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface MapperInterface<T, U> {
|
||||||
|
toDomain(projection: U): T
|
||||||
|
toProjection(domain: T): U
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Result } from '../Core/Result'
|
||||||
|
|
||||||
|
export interface UseCaseInterface<T> {
|
||||||
|
execute(...args: any[]): Promise<Result<T>>
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './Common/Email'
|
export * from './Common/Email'
|
||||||
export * from './Common/EmailProps'
|
export * from './Common/EmailProps'
|
||||||
|
export * from './Common/Timestamps'
|
||||||
|
export * from './Common/TimestampsProps'
|
||||||
export * from './Common/Uuid'
|
export * from './Common/Uuid'
|
||||||
export * from './Common/UuidProps'
|
export * from './Common/UuidProps'
|
||||||
|
|
||||||
@@ -8,7 +10,10 @@ export * from './Core/Entity'
|
|||||||
export * from './Core/Id'
|
export * from './Core/Id'
|
||||||
export * from './Core/Result'
|
export * from './Core/Result'
|
||||||
export * from './Core/UniqueEntityId'
|
export * from './Core/UniqueEntityId'
|
||||||
|
export * from './Core/Validator'
|
||||||
export * from './Core/ValueObject'
|
export * from './Core/ValueObject'
|
||||||
export * from './Core/ValueObjectProps'
|
export * from './Core/ValueObjectProps'
|
||||||
|
|
||||||
export * from './Map/MapInterface'
|
export * from './Mapping/MapperInterface'
|
||||||
|
|
||||||
|
export * from './UseCase/UseCaseInterface'
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.9.30](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.29...@standardnotes/domain-events-infra@1.9.30) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|
||||||
|
## [1.9.29](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.28...@standardnotes/domain-events-infra@1.9.29) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|
||||||
## [1.9.28](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.27...@standardnotes/domain-events-infra@1.9.28) (2022-11-16)
|
## [1.9.28](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.27...@standardnotes/domain-events-infra@1.9.28) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/domain-events-infra",
|
"name": "@standardnotes/domain-events-infra",
|
||||||
"version": "1.9.28",
|
"version": "1.9.30",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,18 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [2.90.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.89.0...@standardnotes/domain-events@2.90.0) (2022-11-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **syncing-server:** add creating item dumps for revision service ([8d152dd](https://github.com/standardnotes/server/commit/8d152ddfcb3c88cbbf9df04e3ed6e2c02571d821))
|
||||||
|
|
||||||
|
# [2.89.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.88.0...@standardnotes/domain-events@2.89.0) (2022-11-18)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add item revision creation requested event ([5c9dff3](https://github.com/standardnotes/server/commit/5c9dff38c9006d39150ea95b2ca17c4ab7175ec2))
|
||||||
|
|
||||||
# [2.88.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.87.0...@standardnotes/domain-events@2.88.0) (2022-11-16)
|
# [2.88.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.87.0...@standardnotes/domain-events@2.88.0) (2022-11-16)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/domain-events",
|
"name": "@standardnotes/domain-events",
|
||||||
"version": "2.88.0",
|
"version": "2.90.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEventInterface } from './DomainEventInterface'
|
||||||
|
import { ItemDumpedEventPayload } from './ItemDumpedEventPayload'
|
||||||
|
|
||||||
|
export interface ItemDumpedEvent extends DomainEventInterface {
|
||||||
|
type: 'ITEM_DUMPED'
|
||||||
|
payload: ItemDumpedEventPayload
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ItemDumpedEventPayload {
|
||||||
|
fileDumpPath: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEventInterface } from './DomainEventInterface'
|
||||||
|
import { ItemRevisionCreationRequestedEventPayload } from './ItemRevisionCreationRequestedEventPayload'
|
||||||
|
|
||||||
|
export interface ItemRevisionCreationRequestedEvent extends DomainEventInterface {
|
||||||
|
type: 'ITEM_REVISION_CREATION_REQUESTED'
|
||||||
|
payload: ItemRevisionCreationRequestedEventPayload
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ItemRevisionCreationRequestedEventPayload {
|
||||||
|
itemUuid: string
|
||||||
|
}
|
||||||
@@ -46,6 +46,10 @@ export * from './Event/GoogleDriveBackupFailedEvent'
|
|||||||
export * from './Event/GoogleDriveBackupFailedEventPayload'
|
export * from './Event/GoogleDriveBackupFailedEventPayload'
|
||||||
export * from './Event/InvoiceGeneratedEvent'
|
export * from './Event/InvoiceGeneratedEvent'
|
||||||
export * from './Event/InvoiceGeneratedEventPayload'
|
export * from './Event/InvoiceGeneratedEventPayload'
|
||||||
|
export * from './Event/ItemDumpedEvent'
|
||||||
|
export * from './Event/ItemDumpedEventPayload'
|
||||||
|
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||||
|
export * from './Event/ItemRevisionCreationRequestedEventPayload'
|
||||||
export * from './Event/ItemsSyncedEvent'
|
export * from './Event/ItemsSyncedEvent'
|
||||||
export * from './Event/ItemsSyncedEventPayload'
|
export * from './Event/ItemsSyncedEventPayload'
|
||||||
export * from './Event/ListedAccountCreatedEvent'
|
export * from './Event/ListedAccountCreatedEvent'
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.6.25](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.24...@standardnotes/event-store@1.6.25) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/event-store
|
||||||
|
|
||||||
|
## [1.6.24](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.23...@standardnotes/event-store@1.6.24) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/event-store
|
||||||
|
|
||||||
## [1.6.23](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.22...@standardnotes/event-store@1.6.23) (2022-11-16)
|
## [1.6.23](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.22...@standardnotes/event-store@1.6.23) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/event-store
|
**Note:** Version bump only for package @standardnotes/event-store
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/event-store",
|
"name": "@standardnotes/event-store",
|
||||||
"version": "1.6.23",
|
"version": "1.6.25",
|
||||||
"description": "Event Store Service",
|
"description": "Event Store Service",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.8.25](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.24...@standardnotes/files-server@1.8.25) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/files-server
|
||||||
|
|
||||||
|
## [1.8.24](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.23...@standardnotes/files-server@1.8.24) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/files-server
|
||||||
|
|
||||||
## [1.8.23](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.22...@standardnotes/files-server@1.8.23) (2022-11-16)
|
## [1.8.23](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.22...@standardnotes/files-server@1.8.23) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/files-server
|
**Note:** Version bump only for package @standardnotes/files-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/files-server",
|
"name": "@standardnotes/files-server",
|
||||||
"version": "1.8.23",
|
"version": "1.8.25",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
LOG_LEVEL=info
|
||||||
|
NODE_ENV=development
|
||||||
|
VERSION=development
|
||||||
|
|
||||||
|
AUTH_JWT_SECRET=auth_jwt_secret
|
||||||
|
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
DB_HOST=db
|
||||||
|
DB_REPLICA_HOST=db
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=std_notes_user
|
||||||
|
DB_PASSWORD=changeme123
|
||||||
|
DB_DATABASE=standard_notes_db
|
||||||
|
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||||
|
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||||
|
|
||||||
|
REDIS_URL=redis://cache
|
||||||
|
|
||||||
|
SQS_QUEUE_URL=
|
||||||
|
SQS_AWS_REGION=
|
||||||
|
S3_AWS_REGION=
|
||||||
|
S3_BACKUP_BUCKET_NAME=
|
||||||
|
|
||||||
|
REDIS_EVENTS_CHANNEL=revisions
|
||||||
|
|
||||||
|
# (Optional) New Relic Setup
|
||||||
|
NEW_RELIC_ENABLED=false
|
||||||
|
NEW_RELIC_APP_NAME="Revisions Server"
|
||||||
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
test-setup.ts
|
||||||
|
data
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../.eslintrc",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./linter.tsconfig.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.1.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.1.2...@standardnotes/revisions-server@1.1.3) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||||
|
|
||||||
|
## [1.1.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.1.1...@standardnotes/revisions-server@1.1.2) (2022-11-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **domain-core:** remove revisions related models to revisions microservice ([a6542dd](https://github.com/standardnotes/server/commit/a6542dd63870a8ada5fd8143d8e2133a570d9329))
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.1.0...@standardnotes/revisions-server@1.1.1) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||||
|
|
||||||
|
# 1.1.0 (2022-11-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **revisions:** docker entrypoint ([a7d0390](https://github.com/standardnotes/server/commit/a7d039082e570f522824631d7e274398dac34f22))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **revisions:** add revisions microservice ([d5c06bf](https://github.com/standardnotes/server/commit/d5c06bfa58a987685fbd8fbab0d22df3fcff3377))
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:18.12.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/revisions/docker/entrypoint.sh" ]
|
||||||
|
|
||||||
|
CMD [ "start-web" ]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import 'newrelic'
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/node'
|
||||||
|
|
||||||
|
import '../src/Infra/InversifyExpress/InversifyExpressRevisionsController'
|
||||||
|
import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController'
|
||||||
|
|
||||||
|
import * as cors from 'cors'
|
||||||
|
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||||
|
import * as winston from 'winston'
|
||||||
|
|
||||||
|
import { InversifyExpressServer } from 'inversify-express-utils'
|
||||||
|
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) => {
|
||||||
|
const env: Env = new Env()
|
||||||
|
env.load()
|
||||||
|
|
||||||
|
const server = new InversifyExpressServer(container)
|
||||||
|
|
||||||
|
server.setConfig((app) => {
|
||||||
|
app.use((_request: Request, response: Response, next: NextFunction) => {
|
||||||
|
response.setHeader('X-Revisions-Version', container.get(TYPES.VERSION))
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
app.use(json())
|
||||||
|
app.use(urlencoded({ extended: true }))
|
||||||
|
app.use(cors())
|
||||||
|
|
||||||
|
if (env.get('SENTRY_DSN', true)) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: env.get('SENTRY_DSN'),
|
||||||
|
integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })],
|
||||||
|
tracesSampleRate: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(Sentry.Handlers.requestHandler() as RequestHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const logger: winston.Logger = container.get(TYPES.Logger)
|
||||||
|
|
||||||
|
server.setErrorConfig((app) => {
|
||||||
|
if (env.get('SENTRY_DSN', true)) {
|
||||||
|
app.use(Sentry.Handlers.errorHandler() as ErrorRequestHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
|
||||||
|
logger.error(error.stack)
|
||||||
|
|
||||||
|
response.status(500).send({
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverInstance = server.build()
|
||||||
|
|
||||||
|
serverInstance.listen(env.get('PORT'))
|
||||||
|
|
||||||
|
logger.info(`Server started on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
COMMAND=$1 && shift 1
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
'start-web' )
|
||||||
|
echo "Starting Web..."
|
||||||
|
yarn workspace @standardnotes/revisions-server start
|
||||||
|
;;
|
||||||
|
|
||||||
|
'start-worker' )
|
||||||
|
echo "Starting Worker..."
|
||||||
|
yarn workspace @standardnotes/revisions-server worker
|
||||||
|
;;
|
||||||
|
|
||||||
|
* )
|
||||||
|
echo "Unknown command"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const base = require('../../jest.config')
|
||||||
|
const { defaults: tsjPreset } = require('ts-jest/presets')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...base,
|
||||||
|
transform: {
|
||||||
|
...tsjPreset.transform,
|
||||||
|
},
|
||||||
|
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/', '/Mapping/'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["dist", "test-setup.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "@standardnotes/revisions-server",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0 <19.0.0"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"description": "Revisions Server",
|
||||||
|
"main": "dist/src/index.js",
|
||||||
|
"typings": "dist/src/index.d.ts",
|
||||||
|
"repository": "git@github.com:standardnotes/server.git",
|
||||||
|
"author": "Karol Sójko <karolsojko@standardnotes.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -fr dist",
|
||||||
|
"setup:env": "cp .env.sample .env",
|
||||||
|
"build": "tsc --build",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
|
"pretest": "yarn lint && yarn build",
|
||||||
|
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||||
|
"start": "yarn node dist/bin/server.js",
|
||||||
|
"worker": "yarn node dist/bin/worker.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@newrelic/native-metrics": "^9.0.0",
|
||||||
|
"@newrelic/winston-enricher": "^4.0.0",
|
||||||
|
"@sentry/node": "^7.19.0",
|
||||||
|
"@standardnotes/api": "^1.19.0",
|
||||||
|
"@standardnotes/common": "workspace:^",
|
||||||
|
"@standardnotes/domain-core": "workspace:^",
|
||||||
|
"@standardnotes/domain-events": "workspace:*",
|
||||||
|
"@standardnotes/domain-events-infra": "workspace:*",
|
||||||
|
"@standardnotes/security": "workspace:^",
|
||||||
|
"@standardnotes/time": "workspace:^",
|
||||||
|
"aws-sdk": "^2.1253.0",
|
||||||
|
"cors": "2.8.5",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"helmet": "^6.0.0",
|
||||||
|
"inversify": "^6.0.1",
|
||||||
|
"inversify-express-utils": "^6.4.3",
|
||||||
|
"ioredis": "^5.2.4",
|
||||||
|
"mysql2": "^2.3.3",
|
||||||
|
"newrelic": "^9.6.0",
|
||||||
|
"reflect-metadata": "0.1.13",
|
||||||
|
"typeorm": "^0.3.10",
|
||||||
|
"winston": "^3.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.9",
|
||||||
|
"@types/dotenv": "^8.2.0",
|
||||||
|
"@types/express": "^4.17.14",
|
||||||
|
"@types/inversify-express-utils": "^2.0.0",
|
||||||
|
"@types/ioredis": "^5.0.0",
|
||||||
|
"@types/jest": "^29.1.1",
|
||||||
|
"@types/newrelic": "^7.0.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||||
|
"eslint": "^8.14.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "^29.1.2",
|
||||||
|
"npm-check-updates": "^16.0.1",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import * as winston from 'winston'
|
||||||
|
import Redis from 'ioredis'
|
||||||
|
import * as AWS from 'aws-sdk'
|
||||||
|
import { Container } from 'inversify'
|
||||||
|
import { Repository } from 'typeorm'
|
||||||
|
import {
|
||||||
|
DomainEventHandlerInterface,
|
||||||
|
DomainEventMessageHandlerInterface,
|
||||||
|
DomainEventSubscriberFactoryInterface,
|
||||||
|
} from '@standardnotes/domain-events'
|
||||||
|
import { TokenDecoderInterface, CrossServiceTokenData, TokenDecoder } from '@standardnotes/security'
|
||||||
|
import {
|
||||||
|
RedisDomainEventSubscriberFactory,
|
||||||
|
RedisEventMessageHandler,
|
||||||
|
SQSDomainEventSubscriberFactory,
|
||||||
|
SQSEventMessageHandler,
|
||||||
|
SQSNewRelicEventMessageHandler,
|
||||||
|
} from '@standardnotes/domain-events-infra'
|
||||||
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { Env } from './Env'
|
||||||
|
import TYPES from './Types'
|
||||||
|
import { AppDataSource } from './DataSource'
|
||||||
|
import { InversifyExpressApiGatewayAuthMiddleware } from '../Infra/InversifyExpress/InversifyExpressApiGatewayAuthMiddleware'
|
||||||
|
import { RevisionsController } from '../Controller/RevisionsController'
|
||||||
|
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||||
|
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
|
||||||
|
import { MySQLRevisionRepository } from '../Infra/MySQL/MySQLRevisionRepository'
|
||||||
|
import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper'
|
||||||
|
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
|
||||||
|
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||||
|
|
||||||
|
// 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('SQS_AWS_REGION', true)) {
|
||||||
|
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(
|
||||||
|
new AWS.SQS({
|
||||||
|
apiVersion: 'latest',
|
||||||
|
region: env.get('SQS_AWS_REGION', true),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let s3Client = undefined
|
||||||
|
if (env.get('S3_AWS_REGION', true)) {
|
||||||
|
s3Client = new AWS.S3({
|
||||||
|
apiVersion: 'latest',
|
||||||
|
region: env.get('S3_AWS_REGION', true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
container.bind<AWS.S3 | undefined>(TYPES.S3).toConstantValue(s3Client)
|
||||||
|
|
||||||
|
// Map
|
||||||
|
container
|
||||||
|
.bind<MapperInterface<RevisionMetadata, TypeORMRevision>>(TYPES.RevisionMetadataPersistenceMapper)
|
||||||
|
.toConstantValue(new RevisionMetadataPersistenceMapper())
|
||||||
|
|
||||||
|
// ORM
|
||||||
|
container
|
||||||
|
.bind<Repository<TypeORMRevision>>(TYPES.ORMRevisionRepository)
|
||||||
|
.toConstantValue(AppDataSource.getRepository(TypeORMRevision))
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
container
|
||||||
|
.bind<RevisionRepositoryInterface>(TYPES.RevisionRepository)
|
||||||
|
.toConstantValue(
|
||||||
|
new MySQLRevisionRepository(
|
||||||
|
container.get(TYPES.ORMRevisionRepository),
|
||||||
|
container.get(TYPES.RevisionMetadataPersistenceMapper),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// env vars
|
||||||
|
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||||
|
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.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||||
|
container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||||
|
container.bind(TYPES.S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true))
|
||||||
|
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||||
|
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||||
|
|
||||||
|
// use cases
|
||||||
|
container
|
||||||
|
.bind<GetRevisionsMetada>(TYPES.GetRevisionsMetada)
|
||||||
|
.toConstantValue(new GetRevisionsMetada(container.get(TYPES.RevisionRepository)))
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
container
|
||||||
|
.bind<RevisionsController>(TYPES.RevisionsController)
|
||||||
|
.toConstantValue(new RevisionsController(container.get(TYPES.GetRevisionsMetada), container.get(TYPES.Logger)))
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
|
||||||
|
// Services
|
||||||
|
container
|
||||||
|
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
|
||||||
|
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
container
|
||||||
|
.bind<InversifyExpressApiGatewayAuthMiddleware>(TYPES.ApiGatewayAuthMiddleware)
|
||||||
|
.to(InversifyExpressApiGatewayAuthMiddleware)
|
||||||
|
|
||||||
|
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,44 @@
|
|||||||
|
import { DataSource, LoggerOptions } from 'typeorm'
|
||||||
|
|
||||||
|
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
|
||||||
|
|
||||||
|
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,
|
||||||
|
restoreNodeTimeout: 5,
|
||||||
|
},
|
||||||
|
entities: [TypeORMRevision],
|
||||||
|
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,37 @@
|
|||||||
|
const TYPES = {
|
||||||
|
DBConnection: Symbol.for('DBConnection'),
|
||||||
|
Logger: Symbol.for('Logger'),
|
||||||
|
Redis: Symbol.for('Redis'),
|
||||||
|
SQS: Symbol.for('SQS'),
|
||||||
|
S3: Symbol.for('S3'),
|
||||||
|
// Map
|
||||||
|
RevisionMetadataPersistenceMapper: Symbol.for('RevisionMetadataPersistenceMapper'),
|
||||||
|
// ORM
|
||||||
|
ORMRevisionRepository: Symbol.for('ORMRevisionRepository'),
|
||||||
|
// Repositories
|
||||||
|
RevisionRepository: Symbol.for('RevisionRepository'),
|
||||||
|
// env vars
|
||||||
|
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||||
|
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||||
|
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||||
|
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||||
|
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
||||||
|
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
|
||||||
|
S3_BACKUP_BUCKET_NAME: Symbol.for('S3_BACKUP_BUCKET_NAME'),
|
||||||
|
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||||
|
VERSION: Symbol.for('VERSION'),
|
||||||
|
// use cases
|
||||||
|
GetRevisionsMetada: Symbol.for('GetRevisionsMetada'),
|
||||||
|
// Controller
|
||||||
|
RevisionsController: Symbol.for('RevisionsController'),
|
||||||
|
// Handlers
|
||||||
|
// Services
|
||||||
|
CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'),
|
||||||
|
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||||
|
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||||
|
Timer: Symbol.for('Timer'),
|
||||||
|
// Middleware
|
||||||
|
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TYPES
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||||
|
|
||||||
|
import { RevisionsController } from './RevisionsController'
|
||||||
|
|
||||||
|
describe('RevisionsController', () => {
|
||||||
|
let getRevisionsMetadata: GetRevisionsMetada
|
||||||
|
let logger: Logger
|
||||||
|
|
||||||
|
const createController = () => new RevisionsController(getRevisionsMetadata, logger)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getRevisionsMetadata = {} as jest.Mocked<GetRevisionsMetada>
|
||||||
|
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.ok())
|
||||||
|
|
||||||
|
logger = {} as jest.Mocked<Logger>
|
||||||
|
logger.warn = jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get revisions list', async () => {
|
||||||
|
const response = await createController().getRevisions({ itemUuid: '1-2-3' })
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should indicate failure to get revisions list', async () => {
|
||||||
|
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||||
|
const response = await createController().getRevisions({ itemUuid: '1-2-3' })
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Logger } from 'winston'
|
||||||
|
import { HttpResponse, HttpStatusCode } from '@standardnotes/api'
|
||||||
|
|
||||||
|
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||||
|
import { GetRevisionsMetadataRequestParams } from '../Infra/Http/GetRevisionsMetadataRequestParams'
|
||||||
|
|
||||||
|
export class RevisionsController {
|
||||||
|
constructor(private getRevisionsMetadata: GetRevisionsMetada, private logger: Logger) {}
|
||||||
|
|
||||||
|
async getRevisions(params: GetRevisionsMetadataRequestParams): Promise<HttpResponse> {
|
||||||
|
const revisionMetadataOrError = await this.getRevisionsMetadata.execute({ itemUuid: params.itemUuid })
|
||||||
|
|
||||||
|
if (revisionMetadataOrError.isFailed()) {
|
||||||
|
this.logger.warn(revisionMetadataOrError.getError())
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: HttpStatusCode.BadRequest,
|
||||||
|
data: {
|
||||||
|
error: {
|
||||||
|
message: 'Could not retrieve revisions.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: HttpStatusCode.Success,
|
||||||
|
data: { revisions: revisionMetadataOrError.getValue() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ContentType as ContentTypeValues } from '@standardnotes/common'
|
||||||
|
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { ContentTypeProps } from './ContentTypeProps'
|
||||||
|
|
||||||
|
export class ContentType extends ValueObject<ContentTypeProps> {
|
||||||
|
get value(): string | null {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: ContentTypeProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(contentType: string | null): Result<ContentType> {
|
||||||
|
if (contentType !== null && !Object.values(ContentTypeValues).includes(contentType as ContentTypeValues)) {
|
||||||
|
return Result.fail<ContentType>(`Value is not a valid content type: ${contentType}`)
|
||||||
|
} else {
|
||||||
|
return Result.ok<ContentType>(new ContentType({ value: contentType }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ContentTypeProps {
|
||||||
|
value: string | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { RevisionProps } from './RevisionProps'
|
||||||
|
|
||||||
|
export class Revision extends Entity<RevisionProps> {
|
||||||
|
get id(): UniqueEntityId {
|
||||||
|
return this._id
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: RevisionProps, id?: UniqueEntityId) {
|
||||||
|
super(props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: RevisionProps, id?: UniqueEntityId): Result<Revision> {
|
||||||
|
return Result.ok<Revision>(new Revision(props, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { RevisionMetadataProps } from './RevisionMetadataProps'
|
||||||
|
|
||||||
|
export class RevisionMetadata extends Entity<RevisionMetadataProps> {
|
||||||
|
get id(): UniqueEntityId {
|
||||||
|
return this._id
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: RevisionMetadataProps, id?: UniqueEntityId) {
|
||||||
|
super(props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: RevisionMetadataProps, id?: UniqueEntityId): Result<RevisionMetadata> {
|
||||||
|
return Result.ok<RevisionMetadata>(new RevisionMetadata(props, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Timestamps } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { ContentType } from './ContentType'
|
||||||
|
|
||||||
|
export interface RevisionMetadataProps {
|
||||||
|
contentType: ContentType
|
||||||
|
timestamps: Timestamps
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { ContentType } from './ContentType'
|
||||||
|
|
||||||
|
export interface RevisionProps {
|
||||||
|
itemUuid: Uuid
|
||||||
|
content: string | null
|
||||||
|
contentType: ContentType
|
||||||
|
itemsKeyId: string | null
|
||||||
|
encItemKey: string | null
|
||||||
|
authHash: string | null
|
||||||
|
creationDate: Date
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { RevisionMetadata } from './RevisionMetadata'
|
||||||
|
|
||||||
|
export interface RevisionRepositoryInterface {
|
||||||
|
findMetadataByItemId(itemUuid: Uuid): Promise<Array<RevisionMetadata>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { RevisionMetadata } from '../../Revision/RevisionMetadata'
|
||||||
|
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||||
|
import { GetRevisionsMetada } from './GetRevisionsMetada'
|
||||||
|
|
||||||
|
describe('GetRevisionsMetada', () => {
|
||||||
|
let revisionRepository: RevisionRepositoryInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new GetRevisionsMetada(revisionRepository)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||||
|
revisionRepository.findMetadataByItemId = jest.fn().mockReturnValue([{} as jest.Mocked<RevisionMetadata>])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return revisions metadata for a given item', async () => {
|
||||||
|
const result = await createUseCase().execute({ itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBeFalsy()
|
||||||
|
expect(result.getValue().length).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return revisions metadata for a an invalid item uuid', async () => {
|
||||||
|
const result = await createUseCase().execute({ itemUuid: '1-2-3' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { RevisionMetadata } from '../../Revision/RevisionMetadata'
|
||||||
|
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||||
|
|
||||||
|
import { GetRevisionsMetadaDTO } from './GetRevisionsMetadaDTO'
|
||||||
|
|
||||||
|
export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]> {
|
||||||
|
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||||
|
|
||||||
|
async execute(dto: GetRevisionsMetadaDTO): Promise<Result<RevisionMetadata[]>> {
|
||||||
|
const itemUuidOrError = Uuid.create(dto.itemUuid)
|
||||||
|
if (itemUuidOrError.isFailed()) {
|
||||||
|
return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${itemUuidOrError.getError()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisionsMetdata = await this.revisionRepository.findMetadataByItemId(itemUuidOrError.getValue())
|
||||||
|
|
||||||
|
return Result.ok<RevisionMetadata[]>(revisionsMetdata)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GetRevisionsMetadaDTO {
|
||||||
|
itemUuid: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GetRevisionsMetadataRequestParams {
|
||||||
|
itemUuid: string
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
|
||||||
|
import { NextFunction, Request, Response } from 'express'
|
||||||
|
import { inject, injectable } from 'inversify'
|
||||||
|
import { BaseMiddleware } from 'inversify-express-utils'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import TYPES from '../../Bootstrap/Types'
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class InversifyExpressApiGatewayAuthMiddleware extends BaseMiddleware {
|
||||||
|
constructor(
|
||||||
|
@inject(TYPES.CrossServiceTokenDecoder) private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
|
||||||
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!request.headers['x-auth-token']) {
|
||||||
|
this.logger.debug('ApiGatewayAuthMiddleware missing x-auth-token header.')
|
||||||
|
|
||||||
|
response.status(401).send({
|
||||||
|
error: {
|
||||||
|
tag: 'invalid-auth',
|
||||||
|
message: 'Invalid login credentials.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token: CrossServiceTokenData | undefined = this.tokenDecoder.decodeToken(
|
||||||
|
request.headers['x-auth-token'] as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (token === undefined) {
|
||||||
|
this.logger.debug('ApiGatewayAuthMiddleware authentication failure.')
|
||||||
|
|
||||||
|
response.status(401).send({
|
||||||
|
error: {
|
||||||
|
tag: 'invalid-auth',
|
||||||
|
message: 'Invalid login credentials.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.locals.user = token.user
|
||||||
|
response.locals.roles = token.roles
|
||||||
|
response.locals.session = token.session
|
||||||
|
response.locals.readOnlyAccess = token.session?.readonly_access ?? false
|
||||||
|
|
||||||
|
return next()
|
||||||
|
} catch (error) {
|
||||||
|
return next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { controller, httpGet } from 'inversify-express-utils'
|
||||||
|
|
||||||
|
@controller('/healthcheck')
|
||||||
|
export class InversifyExpressHealthCheckController {
|
||||||
|
@httpGet('/')
|
||||||
|
public async get(): Promise<string> {
|
||||||
|
return 'OK'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils'
|
||||||
|
import { inject } from 'inversify'
|
||||||
|
|
||||||
|
import TYPES from '../../Bootstrap/Types'
|
||||||
|
import { RevisionsController } from '../../Controller/RevisionsController'
|
||||||
|
|
||||||
|
@controller('/items/:itemUuid/revisions', TYPES.ApiGatewayAuthMiddleware)
|
||||||
|
export class InversifyExpressRevisionsController extends BaseHttpController {
|
||||||
|
constructor(@inject(TYPES.RevisionsController) private revisionsController: RevisionsController) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
@httpGet('/')
|
||||||
|
public async getRevisions(req: Request): Promise<results.JsonResult> {
|
||||||
|
const result = await this.revisionsController.getRevisions({
|
||||||
|
itemUuid: req.params.itemUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.json(result.data, result.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||||
|
import { Repository } from 'typeorm'
|
||||||
|
|
||||||
|
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
|
||||||
|
import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
|
||||||
|
import { TypeORMRevision } from '../TypeORM/TypeORMRevision'
|
||||||
|
|
||||||
|
export class MySQLRevisionRepository implements RevisionRepositoryInterface {
|
||||||
|
constructor(
|
||||||
|
private ormRepository: Repository<TypeORMRevision>,
|
||||||
|
private revisionMapper: MapperInterface<RevisionMetadata, TypeORMRevision>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findMetadataByItemId(itemUuid: Uuid): Promise<Array<RevisionMetadata>> {
|
||||||
|
const queryBuilder = this.ormRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('uuid', 'uuid')
|
||||||
|
.addSelect('content_type', 'contentType')
|
||||||
|
.addSelect('created_at', 'createdAt')
|
||||||
|
.addSelect('updated_at', 'updatedAt')
|
||||||
|
.where('item_uuid = :item_uuid', {
|
||||||
|
item_uuid: itemUuid,
|
||||||
|
})
|
||||||
|
.orderBy('created_at', 'DESC')
|
||||||
|
|
||||||
|
const simplifiedRevisions = await queryBuilder.getMany()
|
||||||
|
|
||||||
|
const metadata = []
|
||||||
|
for (const simplifiedRevision of simplifiedRevisions) {
|
||||||
|
metadata.push(this.revisionMapper.toDomain(simplifiedRevision))
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
|
||||||
|
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||||
|
|
||||||
|
@Entity({ name: 'revisions' })
|
||||||
|
export class TypeORMRevision {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
declare uuid: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'item_uuid',
|
||||||
|
length: 36,
|
||||||
|
})
|
||||||
|
declare itemUuid: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'mediumtext',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare content: string | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'content_type',
|
||||||
|
type: 'varchar',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare contentType: ContentType | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'varchar',
|
||||||
|
name: 'items_key_id',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare itemsKeyId: string | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'enc_item_key',
|
||||||
|
type: 'text',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare encItemKey: string | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'auth_hash',
|
||||||
|
type: 'varchar',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare authHash: string | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'creation_date',
|
||||||
|
type: 'date',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@Index('index_revisions_on_creation_date')
|
||||||
|
declare creationDate: Date
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'created_at',
|
||||||
|
type: 'datetime',
|
||||||
|
precision: 6,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@Index('index_revisions_on_created_at')
|
||||||
|
declare createdAt: Date
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'updated_at',
|
||||||
|
type: 'datetime',
|
||||||
|
precision: 6,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
declare updatedAt: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { MapperInterface, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { ContentType } from '../Domain/Revision/ContentType'
|
||||||
|
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||||
|
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
|
||||||
|
|
||||||
|
export class RevisionMetadataPersistenceMapper implements MapperInterface<RevisionMetadata, TypeORMRevision> {
|
||||||
|
toDomain(projection: TypeORMRevision): RevisionMetadata {
|
||||||
|
const contentTypeOrError = ContentType.create(projection.contentType)
|
||||||
|
if (contentTypeOrError.isFailed()) {
|
||||||
|
throw new Error(`Could not create content type: ${contentTypeOrError.getError()}`)
|
||||||
|
}
|
||||||
|
const contentType = contentTypeOrError.getValue()
|
||||||
|
|
||||||
|
const timestampsOrError = Timestamps.create(projection.createdAt, projection.updatedAt)
|
||||||
|
if (timestampsOrError.isFailed()) {
|
||||||
|
throw new Error(`Could not create timestamps: ${timestampsOrError.getError()}`)
|
||||||
|
}
|
||||||
|
const timestamps = timestampsOrError.getValue()
|
||||||
|
|
||||||
|
const revisionMetadataOrError = RevisionMetadata.create(
|
||||||
|
{
|
||||||
|
contentType,
|
||||||
|
timestamps,
|
||||||
|
},
|
||||||
|
new UniqueEntityId(projection.uuid),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (revisionMetadataOrError.isFailed()) {
|
||||||
|
throw new Error(`Could not create revision metdata: ${revisionMetadataOrError.getError()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return revisionMetadataOrError.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
toProjection(_domain: RevisionMetadata): TypeORMRevision {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"bin/**/*",
|
||||||
|
"migrations/**/*",
|
||||||
|
],
|
||||||
|
"references": []
|
||||||
|
}
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
host="$1"
|
||||||
|
shift
|
||||||
|
port="$1"
|
||||||
|
shift
|
||||||
|
cmd="$@"
|
||||||
|
|
||||||
|
while ! nc -vz $host $port; do
|
||||||
|
>&2 echo "$host:$port is unavailable yet - waiting for it to start"
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
>&2 echo "$host:$port is up - executing command"
|
||||||
|
exec $cmd
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.13.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.25...@standardnotes/scheduler-server@1.13.26) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||||
|
|
||||||
|
## [1.13.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.24...@standardnotes/scheduler-server@1.13.25) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||||
|
|
||||||
## [1.13.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.23...@standardnotes/scheduler-server@1.13.24) (2022-11-16)
|
## [1.13.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.23...@standardnotes/scheduler-server@1.13.24) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/scheduler-server",
|
"name": "@standardnotes/scheduler-server",
|
||||||
"version": "1.13.24",
|
"version": "1.13.26",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,44 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.15.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.14.0...@standardnotes/syncing-server@1.15.0) (2022-11-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **syncing-server:** add creating item dumps for revision service ([8d152dd](https://github.com/standardnotes/syncing-server-js/commit/8d152ddfcb3c88cbbf9df04e3ed6e2c02571d821))
|
||||||
|
|
||||||
|
# [1.14.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.17...@standardnotes/syncing-server@1.14.0) (2022-11-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **syncing-server:** add creating revisions in async way ([1ca8531](https://github.com/standardnotes/syncing-server-js/commit/1ca853130547ebfc26bdd9abce0dfb550e8217f6))
|
||||||
|
|
||||||
|
## [1.13.17](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.16...@standardnotes/syncing-server@1.13.17) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||||
|
|
||||||
|
## [1.13.16](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.15...@standardnotes/syncing-server@1.13.16) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||||
|
|
||||||
|
## [1.13.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.14...@standardnotes/syncing-server@1.13.15) (2022-11-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **syncing-server:** mapper interface imports in specs ([91f36c3](https://github.com/standardnotes/syncing-server-js/commit/91f36c3a3f37e1d53e2203bdfc932fe98cf57b13))
|
||||||
|
|
||||||
|
## [1.13.14](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.13...@standardnotes/syncing-server@1.13.14) (2022-11-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mapper interface imports ([1ec0723](https://github.com/standardnotes/syncing-server-js/commit/1ec072373d640c4e2f24b9bb12fec0c678b48032))
|
||||||
|
|
||||||
|
## [1.13.13](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.12...@standardnotes/syncing-server@1.13.13) (2022-11-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **syncing-server:** paginating with upper bound limit ([94afa34](https://github.com/standardnotes/syncing-server-js/commit/94afa347807d757b46d507086832fbfb3c73353d))
|
||||||
|
|
||||||
## [1.13.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.11...@standardnotes/syncing-server@1.13.12) (2022-11-16)
|
## [1.13.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.13.11...@standardnotes/syncing-server@1.13.12) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
...tsjPreset.transform,
|
...tsjPreset.transform,
|
||||||
},
|
},
|
||||||
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController'],
|
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/'],
|
||||||
setupFilesAfterEnv: ['./test-setup.ts'],
|
setupFilesAfterEnv: ['./test-setup.ts'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/syncing-server",
|
"name": "@standardnotes/syncing-server",
|
||||||
"version": "1.13.12",
|
"version": "1.15.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,9 +81,10 @@ import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
|
|||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { UserContentSizeRecalculationRequestedEventHandler } from '../Domain/Handler/UserContentSizeRecalculationRequestedEventHandler'
|
import { UserContentSizeRecalculationRequestedEventHandler } from '../Domain/Handler/UserContentSizeRecalculationRequestedEventHandler'
|
||||||
import { RevisionMetadataMap } from '../Domain/Map/RevisionMetadataMap'
|
import { RevisionMetadataMap } from '../Domain/Map/RevisionMetadataMap'
|
||||||
import { MapInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||||
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
||||||
|
import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/ItemRevisionCreationRequestedEventHandler'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||||
@@ -228,10 +229,13 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<UserContentSizeRecalculationRequestedEventHandler>(TYPES.UserContentSizeRecalculationRequestedEventHandler)
|
.bind<UserContentSizeRecalculationRequestedEventHandler>(TYPES.UserContentSizeRecalculationRequestedEventHandler)
|
||||||
.to(UserContentSizeRecalculationRequestedEventHandler)
|
.to(UserContentSizeRecalculationRequestedEventHandler)
|
||||||
|
container
|
||||||
|
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.ItemRevisionCreationRequestedEventHandler)
|
||||||
|
.to(ItemRevisionCreationRequestedEventHandler)
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
container
|
container
|
||||||
.bind<MapInterface<RevisionMetadata, SimpleRevisionProjection>>(TYPES.RevisionMetadataMap)
|
.bind<MapperInterface<RevisionMetadata, SimpleRevisionProjection>>(TYPES.RevisionMetadataMap)
|
||||||
.to(RevisionMetadataMap)
|
.to(RevisionMetadataMap)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
@@ -274,6 +278,7 @@ export class ContainerConfigLoader {
|
|||||||
'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
|
'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
|
||||||
container.get(TYPES.UserContentSizeRecalculationRequestedEventHandler),
|
container.get(TYPES.UserContentSizeRecalculationRequestedEventHandler),
|
||||||
],
|
],
|
||||||
|
['ITEM_REVISION_CREATION_REQUESTED', container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (env.get('SQS_QUEUE_URL', true)) {
|
if (env.get('SQS_QUEUE_URL', true)) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const TYPES = {
|
|||||||
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
|
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
|
||||||
CloudBackupRequestedEventHandler: Symbol.for('CloudBackupRequestedEventHandler'),
|
CloudBackupRequestedEventHandler: Symbol.for('CloudBackupRequestedEventHandler'),
|
||||||
UserContentSizeRecalculationRequestedEventHandler: Symbol.for('UserContentSizeRecalculationRequestedEventHandler'),
|
UserContentSizeRecalculationRequestedEventHandler: Symbol.for('UserContentSizeRecalculationRequestedEventHandler'),
|
||||||
|
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
|
||||||
// Map
|
// Map
|
||||||
RevisionMetadataMap: Symbol.for('RevisionMetadataMap'),
|
RevisionMetadataMap: Symbol.for('RevisionMetadataMap'),
|
||||||
// Services
|
// Services
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { results } from 'inversify-express-utils'
|
|||||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||||
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
|
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
|
||||||
import { RevisionProjection } from '../Projection/RevisionProjection'
|
import { RevisionProjection } from '../Projection/RevisionProjection'
|
||||||
import { MapInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||||
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
||||||
|
|
||||||
describe('RevisionsController', () => {
|
describe('RevisionsController', () => {
|
||||||
let revisionProjector: ProjectorInterface<Revision, RevisionProjection>
|
let revisionProjector: ProjectorInterface<Revision, RevisionProjection>
|
||||||
let revisionMap: MapInterface<RevisionMetadata, SimpleRevisionProjection>
|
let revisionMap: MapperInterface<RevisionMetadata, SimpleRevisionProjection>
|
||||||
let revisionService: RevisionServiceInterface
|
let revisionService: RevisionServiceInterface
|
||||||
let revision: Revision
|
let revision: Revision
|
||||||
let revisionMetadata: RevisionMetadata
|
let revisionMetadata: RevisionMetadata
|
||||||
@@ -28,7 +28,7 @@ describe('RevisionsController', () => {
|
|||||||
|
|
||||||
revisionMetadata = {} as jest.Mocked<RevisionMetadata>
|
revisionMetadata = {} as jest.Mocked<RevisionMetadata>
|
||||||
|
|
||||||
revisionMap = {} as jest.Mocked<MapInterface<RevisionMetadata, SimpleRevisionProjection>>
|
revisionMap = {} as jest.Mocked<MapperInterface<RevisionMetadata, SimpleRevisionProjection>>
|
||||||
|
|
||||||
revisionProjector = {} as jest.Mocked<ProjectorInterface<Revision, RevisionProjection>>
|
revisionProjector = {} as jest.Mocked<ProjectorInterface<Revision, RevisionProjection>>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Revision } from '../Domain/Revision/Revision'
|
|||||||
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
|
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
|
||||||
import { ErrorTag } from '@standardnotes/common'
|
import { ErrorTag } from '@standardnotes/common'
|
||||||
import { RevisionProjection } from '../Projection/RevisionProjection'
|
import { RevisionProjection } from '../Projection/RevisionProjection'
|
||||||
import { MapInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||||
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export class RevisionsController extends BaseHttpController {
|
|||||||
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
||||||
@inject(TYPES.RevisionProjector) private revisionProjector: ProjectorInterface<Revision, RevisionProjection>,
|
@inject(TYPES.RevisionProjector) private revisionProjector: ProjectorInterface<Revision, RevisionProjection>,
|
||||||
@inject(TYPES.RevisionMetadataMap)
|
@inject(TYPES.RevisionMetadataMap)
|
||||||
private revisionMetadataMap: MapInterface<RevisionMetadata, SimpleRevisionProjection>,
|
private revisionMetadataMap: MapperInterface<RevisionMetadata, SimpleRevisionProjection>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
EmailArchiveExtensionSyncedEvent,
|
EmailArchiveExtensionSyncedEvent,
|
||||||
EmailBackupAttachmentCreatedEvent,
|
EmailBackupAttachmentCreatedEvent,
|
||||||
GoogleDriveBackupFailedEvent,
|
GoogleDriveBackupFailedEvent,
|
||||||
|
ItemDumpedEvent,
|
||||||
|
ItemRevisionCreationRequestedEvent,
|
||||||
ItemsSyncedEvent,
|
ItemsSyncedEvent,
|
||||||
OneDriveBackupFailedEvent,
|
OneDriveBackupFailedEvent,
|
||||||
UserContentSizeRecalculationRequestedEvent,
|
UserContentSizeRecalculationRequestedEvent,
|
||||||
@@ -19,6 +21,40 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||||
|
|
||||||
|
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent {
|
||||||
|
return {
|
||||||
|
type: 'ITEM_DUMPED',
|
||||||
|
createdAt: this.timer.getUTCDate(),
|
||||||
|
meta: {
|
||||||
|
correlation: {
|
||||||
|
userIdentifier: userUuid,
|
||||||
|
userIdentifierType: 'uuid',
|
||||||
|
},
|
||||||
|
origin: DomainEventService.SyncingServer,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
fileDumpPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent {
|
||||||
|
return {
|
||||||
|
type: 'ITEM_REVISION_CREATION_REQUESTED',
|
||||||
|
createdAt: this.timer.getUTCDate(),
|
||||||
|
meta: {
|
||||||
|
correlation: {
|
||||||
|
userIdentifier: userUuid,
|
||||||
|
userIdentifierType: 'uuid',
|
||||||
|
},
|
||||||
|
origin: DomainEventService.SyncingServer,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
itemUuid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent {
|
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent {
|
||||||
return {
|
return {
|
||||||
type: 'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
|
type: 'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
EmailArchiveExtensionSyncedEvent,
|
EmailArchiveExtensionSyncedEvent,
|
||||||
EmailBackupAttachmentCreatedEvent,
|
EmailBackupAttachmentCreatedEvent,
|
||||||
GoogleDriveBackupFailedEvent,
|
GoogleDriveBackupFailedEvent,
|
||||||
|
ItemDumpedEvent,
|
||||||
|
ItemRevisionCreationRequestedEvent,
|
||||||
ItemsSyncedEvent,
|
ItemsSyncedEvent,
|
||||||
OneDriveBackupFailedEvent,
|
OneDriveBackupFailedEvent,
|
||||||
UserContentSizeRecalculationRequestedEvent,
|
UserContentSizeRecalculationRequestedEvent,
|
||||||
@@ -31,4 +33,6 @@ export interface DomainEventFactoryInterface {
|
|||||||
email: string
|
email: string
|
||||||
}): EmailBackupAttachmentCreatedEvent
|
}): EmailBackupAttachmentCreatedEvent
|
||||||
createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
|
createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
|
||||||
|
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
|
||||||
|
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DomainEventPublisherInterface,
|
||||||
|
DomainEventService,
|
||||||
|
ItemRevisionCreationRequestedEvent,
|
||||||
|
} from '@standardnotes/domain-events'
|
||||||
|
import { Item } from '../Item/Item'
|
||||||
|
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||||
|
import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler'
|
||||||
|
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||||
|
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||||
|
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||||
|
|
||||||
|
describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||||
|
let itemRepository: ItemRepositoryInterface
|
||||||
|
let revisionService: RevisionServiceInterface
|
||||||
|
let event: ItemRevisionCreationRequestedEvent
|
||||||
|
let item: Item
|
||||||
|
let itemBackupService: ItemBackupServiceInterface
|
||||||
|
let domainEventFactory: DomainEventFactoryInterface
|
||||||
|
let domainEventPublisher: DomainEventPublisherInterface
|
||||||
|
|
||||||
|
const createHandler = () =>
|
||||||
|
new ItemRevisionCreationRequestedEventHandler(
|
||||||
|
itemRepository,
|
||||||
|
revisionService,
|
||||||
|
itemBackupService,
|
||||||
|
domainEventFactory,
|
||||||
|
domainEventPublisher,
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
item = {
|
||||||
|
uuid: '1-2-3',
|
||||||
|
content: 'test',
|
||||||
|
} as jest.Mocked<Item>
|
||||||
|
|
||||||
|
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||||
|
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||||
|
|
||||||
|
revisionService = {} as jest.Mocked<RevisionServiceInterface>
|
||||||
|
revisionService.createRevision = jest.fn()
|
||||||
|
|
||||||
|
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
|
||||||
|
event.createdAt = new Date(1)
|
||||||
|
event.payload = {
|
||||||
|
itemUuid: '2-3-4',
|
||||||
|
}
|
||||||
|
event.meta = {
|
||||||
|
correlation: {
|
||||||
|
userIdentifier: '1-2-3',
|
||||||
|
userIdentifierType: 'uuid',
|
||||||
|
},
|
||||||
|
origin: DomainEventService.SyncingServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
|
||||||
|
itemBackupService.dump = jest.fn().mockReturnValue('foo://bar')
|
||||||
|
|
||||||
|
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||||
|
domainEventFactory.createItemDumpedEvent = jest.fn()
|
||||||
|
|
||||||
|
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||||
|
domainEventPublisher.publish = jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a revision for an item', async () => {
|
||||||
|
await createHandler().handle(event)
|
||||||
|
|
||||||
|
expect(revisionService.createRevision).toHaveBeenCalled()
|
||||||
|
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||||
|
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create a revision for an item that does not exist', async () => {
|
||||||
|
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||||
|
|
||||||
|
await createHandler().handle(event)
|
||||||
|
|
||||||
|
expect(revisionService.createRevision).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create a revision for an item if the dump was not created', async () => {
|
||||||
|
itemBackupService.dump = jest.fn().mockReturnValue('')
|
||||||
|
|
||||||
|
await createHandler().handle(event)
|
||||||
|
|
||||||
|
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||||
|
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
ItemRevisionCreationRequestedEvent,
|
||||||
|
DomainEventHandlerInterface,
|
||||||
|
DomainEventPublisherInterface,
|
||||||
|
} from '@standardnotes/domain-events'
|
||||||
|
import { inject, injectable } from 'inversify'
|
||||||
|
|
||||||
|
import TYPES from '../../Bootstrap/Types'
|
||||||
|
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||||
|
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||||
|
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||||
|
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
|
||||||
|
constructor(
|
||||||
|
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
||||||
|
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
||||||
|
@inject(TYPES.ItemBackupService) private itemBackupService: ItemBackupServiceInterface,
|
||||||
|
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||||
|
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
|
||||||
|
const item = await this.itemRepository.findByUuid(event.payload.itemUuid)
|
||||||
|
if (item === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileDumpPath = await this.itemBackupService.dump(item)
|
||||||
|
if (fileDumpPath) {
|
||||||
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createItemDumpedEvent(fileDumpPath, event.meta.correlation.userIdentifier),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.revisionService.createRevision(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@ import { Item } from './Item'
|
|||||||
|
|
||||||
export interface ItemBackupServiceInterface {
|
export interface ItemBackupServiceInterface {
|
||||||
backup(items: Array<Item>, authParams: KeyParamsData): Promise<string>
|
backup(items: Array<Item>, authParams: KeyParamsData): Promise<string>
|
||||||
|
dump(item: Item): Promise<string>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ItemHash } from './ItemHash'
|
|||||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||||
import { ItemService } from './ItemService'
|
import { ItemService } from './ItemService'
|
||||||
import { ApiVersion } from '../Api/ApiVersion'
|
import { ApiVersion } from '../Api/ApiVersion'
|
||||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
|
||||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
@@ -21,7 +20,6 @@ import { ItemProjection } from '../../Projection/ItemProjection'
|
|||||||
|
|
||||||
describe('ItemService', () => {
|
describe('ItemService', () => {
|
||||||
let itemRepository: ItemRepositoryInterface
|
let itemRepository: ItemRepositoryInterface
|
||||||
let revisionService: RevisionServiceInterface
|
|
||||||
let domainEventPublisher: DomainEventPublisherInterface
|
let domainEventPublisher: DomainEventPublisherInterface
|
||||||
let domainEventFactory: DomainEventFactoryInterface
|
let domainEventFactory: DomainEventFactoryInterface
|
||||||
const revisionFrequency = 300
|
const revisionFrequency = 300
|
||||||
@@ -47,7 +45,6 @@ describe('ItemService', () => {
|
|||||||
itemSaveValidator,
|
itemSaveValidator,
|
||||||
itemFactory,
|
itemFactory,
|
||||||
itemRepository,
|
itemRepository,
|
||||||
revisionService,
|
|
||||||
domainEventPublisher,
|
domainEventPublisher,
|
||||||
domainEventFactory,
|
domainEventFactory,
|
||||||
revisionFrequency,
|
revisionFrequency,
|
||||||
@@ -125,9 +122,6 @@ describe('ItemService', () => {
|
|||||||
itemRepository.countAll = jest.fn().mockReturnValue(2)
|
itemRepository.countAll = jest.fn().mockReturnValue(2)
|
||||||
itemRepository.save = jest.fn().mockImplementation((item: Item) => item)
|
itemRepository.save = jest.fn().mockImplementation((item: Item) => item)
|
||||||
|
|
||||||
revisionService = {} as jest.Mocked<RevisionServiceInterface>
|
|
||||||
revisionService.createRevision = jest.fn()
|
|
||||||
|
|
||||||
timer = {} as jest.Mocked<TimerInterface>
|
timer = {} as jest.Mocked<TimerInterface>
|
||||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
||||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date())
|
timer.getUTCDate = jest.fn().mockReturnValue(new Date())
|
||||||
@@ -147,6 +141,7 @@ describe('ItemService', () => {
|
|||||||
|
|
||||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||||
domainEventFactory.createDuplicateItemSyncedEvent = jest.fn()
|
domainEventFactory.createDuplicateItemSyncedEvent = jest.fn()
|
||||||
|
domainEventFactory.createItemRevisionCreationRequested = jest.fn()
|
||||||
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
logger = {} as jest.Mocked<Logger>
|
||||||
logger.error = jest.fn()
|
logger.error = jest.fn()
|
||||||
@@ -491,7 +486,8 @@ describe('ItemService', () => {
|
|||||||
syncToken: 'MjpOYU4=',
|
syncToken: 'MjpOYU4=',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revisionService.createRevision).toHaveBeenCalledTimes(1)
|
expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
|
||||||
|
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not save new items in read only access mode', async () => {
|
it('should not save new items in read only access mode', async () => {
|
||||||
@@ -515,8 +511,6 @@ describe('ItemService', () => {
|
|||||||
savedItems: [],
|
savedItems: [],
|
||||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revisionService.createRevision).toHaveBeenCalledTimes(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should save new items that are duplicates', async () => {
|
it('should save new items that are duplicates', async () => {
|
||||||
@@ -538,8 +532,8 @@ describe('ItemService', () => {
|
|||||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
|
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(revisionService.createRevision).toHaveBeenCalledTimes(1)
|
expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
|
||||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||||
expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
|
expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -933,8 +927,9 @@ describe('ItemService', () => {
|
|||||||
],
|
],
|
||||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||||
})
|
})
|
||||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||||
expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
|
expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should skip saving conflicting items and mark them as sync conflicts when saving to database fails', async () => {
|
it('should skip saving conflicting items and mark them as sync conflicts when saving to database fails', async () => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Logger } from 'winston'
|
|||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
|
||||||
import { GetItemsDTO } from './GetItemsDTO'
|
import { GetItemsDTO } from './GetItemsDTO'
|
||||||
import { GetItemsResult } from './GetItemsResult'
|
import { GetItemsResult } from './GetItemsResult'
|
||||||
import { Item } from './Item'
|
import { Item } from './Item'
|
||||||
@@ -33,7 +32,6 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
@inject(TYPES.ItemSaveValidator) private itemSaveValidator: ItemSaveValidatorInterface,
|
@inject(TYPES.ItemSaveValidator) private itemSaveValidator: ItemSaveValidatorInterface,
|
||||||
@inject(TYPES.ItemFactory) private itemFactory: ItemFactoryInterface,
|
@inject(TYPES.ItemFactory) private itemFactory: ItemFactoryInterface,
|
||||||
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
||||||
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
|
||||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||||
@inject(TYPES.REVISIONS_FREQUENCY) private revisionFrequency: number,
|
@inject(TYPES.REVISIONS_FREQUENCY) private revisionFrequency: number,
|
||||||
@@ -49,6 +47,7 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
const lastSyncTime = this.getLastSyncTime(dto)
|
const lastSyncTime = this.getLastSyncTime(dto)
|
||||||
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
|
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
|
||||||
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
|
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
|
||||||
|
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
|
||||||
|
|
||||||
const itemQuery: ItemQuery = {
|
const itemQuery: ItemQuery = {
|
||||||
userUuid: dto.userUuid,
|
userUuid: dto.userUuid,
|
||||||
@@ -58,7 +57,7 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
deleted: lastSyncTime ? undefined : false,
|
deleted: lastSyncTime ? undefined : false,
|
||||||
sortBy: 'updated_at_timestamp',
|
sortBy: 'updated_at_timestamp',
|
||||||
sortOrder: 'ASC',
|
sortOrder: 'ASC',
|
||||||
limit: limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit,
|
limit: upperBoundLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||||
@@ -76,7 +75,7 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
||||||
|
|
||||||
let cursorToken = undefined
|
let cursorToken = undefined
|
||||||
if (totalItemsCount > limit) {
|
if (totalItemsCount > upperBoundLimit) {
|
||||||
const lastSyncTime = items[items.length - 1].updatedAtTimestamp / Time.MicrosecondsInASecond
|
const lastSyncTime = items[items.length - 1].updatedAtTimestamp / Time.MicrosecondsInASecond
|
||||||
cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
|
cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
|
||||||
}
|
}
|
||||||
@@ -252,7 +251,9 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
const savedItem = await this.itemRepository.save(dto.existingItem)
|
const savedItem = await this.itemRepository.save(dto.existingItem)
|
||||||
|
|
||||||
if (secondsFromLastUpdate >= this.revisionFrequency) {
|
if (secondsFromLastUpdate >= this.revisionFrequency) {
|
||||||
await this.revisionService.createRevision(savedItem)
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasMarkedAsDuplicate) {
|
if (wasMarkedAsDuplicate) {
|
||||||
@@ -269,7 +270,9 @@ export class ItemService implements ItemServiceInterface {
|
|||||||
|
|
||||||
const savedItem = await this.itemRepository.save(newItem)
|
const savedItem = await this.itemRepository.save(newItem)
|
||||||
|
|
||||||
await this.revisionService.createRevision(savedItem)
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
|
||||||
|
)
|
||||||
|
|
||||||
if (savedItem.duplicateOf) {
|
if (savedItem.duplicateOf) {
|
||||||
await this.domainEventPublisher.publish(
|
await this.domainEventPublisher.publish(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { MapInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
import { MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import { RevisionMetadata } from '../Revision/RevisionMetadata'
|
|||||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class RevisionMetadataMap implements MapInterface<RevisionMetadata, SimpleRevisionProjection> {
|
export class RevisionMetadataMap implements MapperInterface<RevisionMetadata, SimpleRevisionProjection> {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
||||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { KeyParamsData } from '@standardnotes/responses'
|
|
||||||
import { S3 } from 'aws-sdk'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
import { Item } from '../../Domain/Item/Item'
|
|
||||||
import { S3ItemBackupService } from './S3ItemBackupService'
|
|
||||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
|
||||||
import { ItemProjection } from '../../Projection/ItemProjection'
|
|
||||||
|
|
||||||
describe('S3ItemBackupService', () => {
|
|
||||||
let s3Client: S3 | undefined
|
|
||||||
let itemProjector: ProjectorInterface<Item, ItemProjection>
|
|
||||||
let s3BackupBucketName = 'backup-bucket'
|
|
||||||
let logger: Logger
|
|
||||||
let item: Item
|
|
||||||
let keyParams: KeyParamsData
|
|
||||||
|
|
||||||
const createService = () => new S3ItemBackupService(s3BackupBucketName, itemProjector, logger, s3Client)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
s3Client = {} as jest.Mocked<S3>
|
|
||||||
s3Client.upload = jest.fn().mockReturnValue({
|
|
||||||
promise: jest.fn().mockReturnValue(Promise.resolve({ Key: 'test' })),
|
|
||||||
})
|
|
||||||
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.warn = jest.fn()
|
|
||||||
|
|
||||||
item = {} as jest.Mocked<Item>
|
|
||||||
|
|
||||||
keyParams = {} as jest.Mocked<KeyParamsData>
|
|
||||||
|
|
||||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
|
||||||
itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should upload items to S3 as a backup file', async () => {
|
|
||||||
await createService().backup([item], keyParams)
|
|
||||||
|
|
||||||
expect((<S3>s3Client).upload).toHaveBeenCalledWith({
|
|
||||||
Body: '{"items":[{"foo":"bar"}],"auth_params":{}}',
|
|
||||||
Bucket: 'backup-bucket',
|
|
||||||
Key: expect.any(String),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not upload items to S3 if bucket name is not configured', async () => {
|
|
||||||
s3BackupBucketName = ''
|
|
||||||
await createService().backup([item], keyParams)
|
|
||||||
|
|
||||||
expect((<S3>s3Client).upload).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not upload items to S3 if S3 client is not configured', async () => {
|
|
||||||
s3Client = undefined
|
|
||||||
expect(await createService().backup([item], keyParams)).toEqual('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -3,6 +3,7 @@ import { KeyParamsData } from '@standardnotes/responses'
|
|||||||
import { S3 } from 'aws-sdk'
|
import { S3 } from 'aws-sdk'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { Item } from '../../Domain/Item/Item'
|
import { Item } from '../../Domain/Item/Item'
|
||||||
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
|
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
|
||||||
@@ -18,6 +19,26 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
|
|||||||
@inject(TYPES.S3) private s3Client?: S3,
|
@inject(TYPES.S3) private s3Client?: S3,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async dump(item: Item): Promise<string> {
|
||||||
|
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||||
|
this.logger.warn('S3 backup not configured')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadResult = await this.s3Client
|
||||||
|
.upload({
|
||||||
|
Bucket: this.s3BackupBucketName,
|
||||||
|
Key: uuid.v4(),
|
||||||
|
Body: JSON.stringify({
|
||||||
|
item: await this.itemProjector.projectFull(item),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.promise()
|
||||||
|
|
||||||
|
return uploadResult.Key
|
||||||
|
}
|
||||||
|
|
||||||
async backup(items: Item[], authParams: KeyParamsData): Promise<string> {
|
async backup(items: Item[], authParams: KeyParamsData): Promise<string> {
|
||||||
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||||
this.logger.warn('S3 backup not configured')
|
this.logger.warn('S3 backup not configured')
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.4.27](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.26...@standardnotes/websockets-server@1.4.27) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||||
|
|
||||||
|
## [1.4.26](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.25...@standardnotes/websockets-server@1.4.26) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||||
|
|
||||||
## [1.4.25](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.24...@standardnotes/websockets-server@1.4.25) (2022-11-16)
|
## [1.4.25](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.24...@standardnotes/websockets-server@1.4.25) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/websockets-server",
|
"name": "@standardnotes/websockets-server",
|
||||||
"version": "1.4.25",
|
"version": "1.4.27",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.17.25](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.24...@standardnotes/workspace-server@1.17.25) (2022-11-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||||
|
|
||||||
|
## [1.17.24](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.23...@standardnotes/workspace-server@1.17.24) (2022-11-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||||
|
|
||||||
## [1.17.23](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.22...@standardnotes/workspace-server@1.17.23) (2022-11-16)
|
## [1.17.23](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.22...@standardnotes/workspace-server@1.17.23) (2022-11-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/workspace-server",
|
"name": "@standardnotes/workspace-server",
|
||||||
"version": "1.17.23",
|
"version": "1.17.25",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,6 +53,9 @@
|
|||||||
{
|
{
|
||||||
"path": "./packages/predicates"
|
"path": "./packages/predicates"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "./packages/revisions"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "./packages/scheduler"
|
"path": "./packages/scheduler"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2239,6 +2239,50 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@standardnotes/revisions-server@workspace:packages/revisions":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@standardnotes/revisions-server@workspace:packages/revisions"
|
||||||
|
dependencies:
|
||||||
|
"@newrelic/native-metrics": "npm:^9.0.0"
|
||||||
|
"@newrelic/winston-enricher": "npm:^4.0.0"
|
||||||
|
"@sentry/node": "npm:^7.19.0"
|
||||||
|
"@standardnotes/api": "npm:^1.19.0"
|
||||||
|
"@standardnotes/common": "workspace:^"
|
||||||
|
"@standardnotes/domain-core": "workspace:^"
|
||||||
|
"@standardnotes/domain-events": "workspace:*"
|
||||||
|
"@standardnotes/domain-events-infra": "workspace:*"
|
||||||
|
"@standardnotes/security": "workspace:^"
|
||||||
|
"@standardnotes/time": "workspace:^"
|
||||||
|
"@types/cors": "npm:^2.8.9"
|
||||||
|
"@types/dotenv": "npm:^8.2.0"
|
||||||
|
"@types/express": "npm:^4.17.14"
|
||||||
|
"@types/inversify-express-utils": "npm:^2.0.0"
|
||||||
|
"@types/ioredis": "npm:^5.0.0"
|
||||||
|
"@types/jest": "npm:^29.1.1"
|
||||||
|
"@types/newrelic": "npm:^7.0.4"
|
||||||
|
"@typescript-eslint/eslint-plugin": "npm:^5.29.0"
|
||||||
|
aws-sdk: "npm:^2.1253.0"
|
||||||
|
cors: "npm:2.8.5"
|
||||||
|
dotenv: "npm:^16.0.1"
|
||||||
|
eslint: "npm:^8.14.0"
|
||||||
|
eslint-plugin-prettier: "npm:^4.0.0"
|
||||||
|
express: "npm:^4.18.2"
|
||||||
|
helmet: "npm:^6.0.0"
|
||||||
|
inversify: "npm:^6.0.1"
|
||||||
|
inversify-express-utils: "npm:^6.4.3"
|
||||||
|
ioredis: "npm:^5.2.4"
|
||||||
|
jest: "npm:^29.1.2"
|
||||||
|
mysql2: "npm:^2.3.3"
|
||||||
|
newrelic: "npm:^9.6.0"
|
||||||
|
npm-check-updates: "npm:^16.0.1"
|
||||||
|
reflect-metadata: "npm:0.1.13"
|
||||||
|
ts-jest: "npm:^29.0.3"
|
||||||
|
typeorm: "npm:^0.3.10"
|
||||||
|
typescript: "npm:^4.8.4"
|
||||||
|
winston: "npm:^3.8.1"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@standardnotes/scheduler-server@workspace:packages/scheduler":
|
"@standardnotes/scheduler-server@workspace:packages/scheduler":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@standardnotes/scheduler-server@workspace:packages/scheduler"
|
resolution: "@standardnotes/scheduler-server@workspace:packages/scheduler"
|
||||||
|
|||||||
Reference in New Issue
Block a user