Compare commits

..

14 Commits

Author SHA1 Message Date
standardci
b68ede1f63 chore(release): publish new version
- @standardnotes/revisions-server@1.11.5
2023-02-13 11:11:12 +00:00
Karol Sójko
d333c1393c fix(revisions): api gateway middleware binding 2023-02-13 11:56:56 +01:00
Karol Sójko
35d6f2f961 Revert "fix(revisions): add missing inversify annotation for api gateway auth middleware"
This reverts commit 3d619a0c61.
2023-02-13 11:56:30 +01:00
Karol Sójko
3d619a0c61 fix(revisions): add missing inversify annotation for api gateway auth middleware 2023-02-13 11:53:51 +01:00
standardci
bda812f170 chore(release): publish new version
- @standardnotes/revisions-server@1.11.4
2023-02-13 10:40:48 +00:00
Karol Sójko
826719bb67 fix(revisions): missing api gateway middleware 2023-02-13 11:26:45 +01:00
standardci
631101eae5 chore(release): publish new version
- @standardnotes/syncing-server@1.30.1
2023-02-13 10:06:28 +00:00
Karol Sójko
fa0b9bf935 fix(syncing-server): worker container configuration 2023-02-13 10:52:06 +01:00
standardci
708af5e83c chore(release): publish new version
- @standardnotes/api-gateway@1.47.0
 - @standardnotes/syncing-server@1.30.0
2023-02-13 09:28:02 +00:00
Karol Sójko
993d31167b feat(syncing-server): refactor container config into server and worker (#443)
* feat(syncing-server): refactor container config into server and worker

* fix(syncing-server): yarn lock

* fix(api-gateway): add client update response on v1 revision endpoints

* fix(syncing-server): linter issue
2023-02-13 10:12:32 +01:00
standardci
ac5fc7d28a chore(release): publish new version
- @standardnotes/revisions-server@1.11.3
2023-02-13 08:18:04 +00:00
Karol Sójko
73f3fad13d Revert "fix(revisions): remove inversify.js in favour of simple di container implementation (#442)"
This reverts commit 89ee103303.
2023-02-13 09:02:37 +01:00
standardci
85e0e2165f chore(release): publish new version
- @standardnotes/revisions-server@1.11.2
2023-02-10 14:44:29 +00:00
Karol Sójko
89ee103303 fix(revisions): remove inversify.js in favour of simple di container implementation (#442) 2023-02-10 15:28:03 +01:00
76 changed files with 731 additions and 1690 deletions

5
.pnp.cjs generated
View File

@@ -4501,6 +4501,7 @@ const RAW_RUNTIME_STATE =
["@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"],\
@@ -4687,7 +4688,6 @@ const RAW_RUNTIME_STATE =
["@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/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:9.4.0"],\
@@ -4704,7 +4704,6 @@ const RAW_RUNTIME_STATE =
["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"],\
["jsonwebtoken", "npm:9.0.0"],\
["mysql2", "npm:3.0.1"],\
@@ -4714,7 +4713,7 @@ const RAW_RUNTIME_STATE =
["prettyjson", "npm:1.2.5"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["ua-parser-js", "npm:1.0.32"],\
["uuid", "npm:9.0.0"],\

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.47.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.46.13...@standardnotes/api-gateway@1.47.0) (2023-02-13)
### Features
* **syncing-server:** refactor container config into server and worker ([#443](https://github.com/standardnotes/api-gateway/issues/443)) ([993d311](https://github.com/standardnotes/api-gateway/commit/993d31167b8b0ac11e3df530d2d1ee566940df6e))
## [1.46.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.46.12...@standardnotes/api-gateway@1.46.13) (2023-02-09)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.46.13",
"version": "1.47.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,35 +1,41 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/items/:item_id/revisions', TYPES.AuthMiddleware)
export class RevisionsController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpGet('/')
async getRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callSyncingServer(request, response, `items/${request.params.item_id}/revisions`)
async getRevisions(): Promise<results.JsonResult> {
return this.json(
{
error: {
message: 'Please update your client application.',
},
},
410,
)
}
@httpGet('/:id')
async getRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callSyncingServer(
request,
response,
`items/${request.params.item_id}/revisions/${request.params.id}`,
async getRevision(): Promise<results.JsonResult> {
return this.json(
{
error: {
message: 'Please update your client application.',
},
},
410,
)
}
@httpDelete('/:id')
async deleteRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callSyncingServer(
request,
response,
`items/${request.params.item_id}/revisions/${request.params.id}`,
async deleteRevision(): Promise<results.JsonResult> {
return this.json(
{
error: {
message: 'Please update your client application.',
},
},
410,
)
}
}

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.11.4...@standardnotes/revisions-server@1.11.5) (2023-02-13)
### Bug Fixes
* **revisions:** api gateway middleware binding ([d333c13](https://github.com/standardnotes/server/commit/d333c1393c67899a6303dbec40527584cd1b48ab))
## [1.11.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.11.3...@standardnotes/revisions-server@1.11.4) (2023-02-13)
### Bug Fixes
* **revisions:** missing api gateway middleware ([826719b](https://github.com/standardnotes/server/commit/826719bb67dd403e4880ab61b25c92c4506e3c57))
## [1.11.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.11.2...@standardnotes/revisions-server@1.11.3) (2023-02-13)
### Reverts
* Revert "fix(revisions): remove inversify.js in favour of simple di container implementation (#442)" ([73f3fad](https://github.com/standardnotes/server/commit/73f3fad13da57619f008ed486c1d4114f3bad3b6)), closes [#442](https://github.com/standardnotes/server/issues/442)
## [1.11.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.11.1...@standardnotes/revisions-server@1.11.2) (2023-02-10)
### Bug Fixes
* **revisions:** remove inversify.js in favour of simple di container implementation ([#442](https://github.com/standardnotes/server/issues/442)) ([89ee103](https://github.com/standardnotes/server/commit/89ee103303651b2b181c3b79b7a500358165dcf2))
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.11.0...@standardnotes/revisions-server@1.11.1) (2023-02-10)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.11.1",
"version": "1.11.5",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -36,6 +36,7 @@
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/security": "workspace:^",
"@standardnotes/time": "workspace:^",
"cors": "2.8.5",
"dotenv": "^16.0.1",

View File

@@ -13,11 +13,16 @@ import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper'
import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper'
import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
import { ApiGatewayAuthMiddleware } from '../Controller/ApiGatewayAuthMiddleware'
import { CrossServiceTokenData, TokenDecoder, TokenDecoderInterface } from '@standardnotes/security'
import { Env } from './Env'
export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
override async load(): Promise<Container> {
const container = await super.load()
const env: Env = container.get(TYPES.Env)
container.bind<TimerInterface>(TYPES.Timer).toDynamicValue(() => new Timer())
container
@@ -72,6 +77,9 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
return new DeleteRevision(context.container.get(TYPES.RevisionRepository))
})
// env vars
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
// Controller
container.bind<RevisionsController>(TYPES.RevisionsController).toDynamicValue((context: interfaces.Context) => {
return new RevisionsController(
@@ -84,6 +92,21 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
)
})
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toDynamicValue((context: interfaces.Context) => {
return new TokenDecoder<CrossServiceTokenData>(context.container.get(TYPES.AUTH_JWT_SECRET))
})
container
.bind<ApiGatewayAuthMiddleware>(TYPES.ApiGatewayAuthMiddleware)
.toDynamicValue((context: interfaces.Context) => {
return new ApiGatewayAuthMiddleware(
context.container.get(TYPES.CrossServiceTokenDecoder),
context.container.get(TYPES.Logger),
)
})
return container
}
}

View File

@@ -16,6 +16,7 @@ const TYPES = {
RevisionRepository: Symbol.for('RevisionRepository'),
DumpRepository: Symbol.for('DumpRepository'),
// env vars
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
@@ -30,6 +31,7 @@ const TYPES = {
GetRequiredRoleToViewRevision: Symbol.for('GetRequiredRoleToViewRevision'),
// Controller
RevisionsController: Symbol.for('RevisionsController'),
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
// Handlers
ItemDumpedEventHandler: Symbol.for('ItemDumpedEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

View File

@@ -0,0 +1,53 @@
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston'
export class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor(private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>, 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)
}
}
}

View File

@@ -5,7 +5,7 @@ import { inject } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { RevisionsController } from '../../Controller/RevisionsController'
@controller('/items/:itemUuid/revisions')
@controller('/items/:itemUuid/revisions', TYPES.ApiGatewayAuthMiddleware)
export class InversifyExpressRevisionsController extends BaseHttpController {
constructor(@inject(TYPES.RevisionsController) private revisionsController: RevisionsController) {
super()

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.30.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.30.0...@standardnotes/syncing-server@1.30.1) (2023-02-13)
### Bug Fixes
* **syncing-server:** worker container configuration ([fa0b9bf](https://github.com/standardnotes/syncing-server-js/commit/fa0b9bf9353b78542ca02352cbb59232dadbe8b9))
# [1.30.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.29.15...@standardnotes/syncing-server@1.30.0) (2023-02-13)
### Features
* **syncing-server:** refactor container config into server and worker ([#443](https://github.com/standardnotes/syncing-server-js/issues/443)) ([993d311](https://github.com/standardnotes/syncing-server-js/commit/993d31167b8b0ac11e3df530d2d1ee566940df6e))
## [1.29.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.29.14...@standardnotes/syncing-server@1.29.15) (2023-02-09)
### Bug Fixes

View File

@@ -4,11 +4,11 @@ import 'newrelic'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { WorkerContainerConfigLoader } from '../src/Bootstrap/WorkerContainerConfigLoader'
const inputArgs = process.argv.slice(2)
const userUuid = inputArgs[0]
@@ -20,7 +20,7 @@ const fixContentSize = async (
await domainEventPublisher.publish(domainEventFactory.createUserContentSizeRecalculationRequestedEvent(userUuid))
}
const container = new ContainerConfigLoader()
const container = new WorkerContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -1,136 +0,0 @@
import 'reflect-metadata'
import 'newrelic'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { ItemRepositoryInterface } from '../src/Domain/Item/ItemRepositoryInterface'
import { ContentType } from '@standardnotes/common'
const fixRevisionsOwnership = async (
year: number,
month: number,
revisionsProcessingLimit: number,
itemRepository: ItemRepositoryInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
logger: Logger,
): Promise<void> => {
const createdAfter = new Date(`${year}-${month}-1`)
const createdBefore = new Date(`${month !== 12 ? year : year + 1}-${month !== 12 ? month + 1 : 1}-1`)
logger.info(`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processing items`)
const itemsCount = await itemRepository.countAll({
createdBetween: [createdAfter, createdBefore],
selectString: 'item.uuid as uuid, item.user_uuid as userUuid',
contentType: [ContentType.Note, ContentType.File],
sortOrder: 'ASC',
sortBy: 'uuid',
})
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] There are ${itemsCount} items to process.`,
)
const amountOfPages = Math.ceil(itemsCount / revisionsProcessingLimit)
const tenPercentOfPages = Math.ceil(amountOfPages / 10)
let itemsProcessedCounter = 0
let itemsSkippedCounter = 0
for (let page = 1; page <= amountOfPages; page++) {
if (page % tenPercentOfPages === 0) {
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processing page ${page}/${amountOfPages} of items.`,
)
logger.info(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] Processed successfully/skipped items: ${itemsProcessedCounter}/${itemsSkippedCounter}.`,
)
}
const items = await itemRepository.findAllRaw<{ uuid: string; userUuid: string }>({
createdBetween: [createdAfter, createdBefore],
selectString: 'item.uuid as uuid, item.user_uuid as userUuid',
contentType: [ContentType.Note, ContentType.File],
offset: (page - 1) * revisionsProcessingLimit,
limit: revisionsProcessingLimit,
sortOrder: 'ASC',
sortBy: 'uuid',
})
if (items.length === 0) {
logger.warn(
`[${createdAfter.toISOString()} - ${createdBefore.toISOString()}] No items fetched for offset ${
(page - 1) * revisionsProcessingLimit
} and limit ${revisionsProcessingLimit}.`,
)
}
for (const item of items) {
if (!item.userUuid || !item.uuid) {
itemsSkippedCounter++
continue
}
await domainEventPublisher.publish(
domainEventFactory.createRevisionsOwnershipUpdateRequestedEvent({
userUuid: item.userUuid,
itemUuid: item.uuid,
}),
)
itemsProcessedCounter++
}
}
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info('Starting revisions ownership fixing')
const itemRepository: ItemRepositoryInterface = container.get(TYPES.ItemRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const years = env.get('REVISION_YEARS').split(',')
const months = env.get('REVISION_MONTHS').split(',')
const revisionsProcessingLimit = env.get('REVISIONS_PROCESSING_LIMIT')
const promises = []
for (const year of years) {
for (const month of months) {
promises.push(
fixRevisionsOwnership(
+year,
+month,
+revisionsProcessingLimit,
itemRepository,
domainEventFactory,
domainEventPublisher,
logger,
),
)
}
}
Promise.all(promises)
.then(() => {
logger.info('revisions ownership fix complete.')
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish revisions ownership fix: ${error.message}`)
process.exit(1)
})
})

View File

@@ -6,7 +6,6 @@ import * as Sentry from '@sentry/node'
import * as Tracing from '@sentry/tracing'
import '../src/Controller/HealthCheckController'
import '../src/Controller/RevisionsController'
import '../src/Controller/ItemsController'
import helmet from 'helmet'
@@ -15,11 +14,11 @@ import { urlencoded, json, Request, Response, NextFunction, ErrorRequestHandler
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'
import { ServerContainerConfigLoader } from '../src/Bootstrap/ServerContainerConfigLoader'
const container = new ContainerConfigLoader()
const container = new ServerContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -4,12 +4,12 @@ import 'newrelic'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
import { WorkerContainerConfigLoader } from '../src/Bootstrap/WorkerContainerConfigLoader'
const container = new ContainerConfigLoader()
const container = new WorkerContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -25,11 +25,6 @@ case "$COMMAND" in
yarn workspace @standardnotes/syncing-server content-size $USER_UUID
;;
'revisions-ownership-fix' )
echo "[Docker] Starting Revisions Ownership Fixing..."
yarn workspace @standardnotes/syncing-server revisions-ownership
;;
* )
echo "[Docker] Unknown command"
;;

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.29.15",
"version": "1.30.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -28,7 +28,6 @@
"worker": "yarn node dist/bin/worker.js",
"supervisor:worker": "yarn wait-for:syncing-server && yarn node dist/bin/worker.js",
"content-size": "yarn node dist/bin/content.js",
"revisions-ownership": "yarn node dist/bin/revisions.js",
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
},
"dependencies": {
@@ -54,7 +53,6 @@
"helmet": "^6.0.0",
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"ioredis": "^5.2.4",
"jsonwebtoken": "^9.0.0",
"mysql2": "^3.0.1",
"newrelic": "^9.8.0",
@@ -71,7 +69,6 @@
"@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/jsonwebtoken": "^9.0.1",
"@types/newrelic": "^9.4.0",

View File

@@ -0,0 +1,119 @@
import * as winston from 'winston'
import { Container, interfaces } from 'inversify'
import { Env } from './Env'
import TYPES from './Types'
import { AppDataSource } from './DataSource'
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
import { MySQLItemRepository } from '../Infra/MySQL/MySQLItemRepository'
import { Repository } from 'typeorm'
import { Item } from '../Domain/Item/Item'
import { ItemProjection } from '../Projection/ItemProjection'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { ItemProjector } from '../Projection/ItemProjector'
import { SNSDomainEventPublisher } from '@standardnotes/domain-events-infra'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
import { Timer, TimerInterface } from '@standardnotes/time'
import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
export class CommonContainerConfigLoader {
async load(): Promise<Container> {
const env: Env = new Env()
env.load()
const container = new Container({
defaultScope: 'Singleton',
})
await AppDataSource.initialize()
container.bind<Env>(TYPES.Env).toConstantValue(env)
container.bind<winston.Logger>(TYPES.Logger).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
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' })],
})
return logger
})
container.bind<SNSClient>(TYPES.SNS).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const snsConfig: SNSClientConfig = {
apiVersion: 'latest',
region: env.get('SNS_AWS_REGION', true),
}
if (env.get('SNS_ENDPOINT', true)) {
snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
}
if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
snsConfig.credentials = {
accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
}
}
return new SNSClient(snsConfig)
})
// Repositories
container.bind<ItemRepositoryInterface>(TYPES.ItemRepository).toDynamicValue((context: interfaces.Context) => {
return new MySQLItemRepository(context.container.get(TYPES.ORMItemRepository))
})
// ORM
container.bind<Repository<Item>>(TYPES.ORMItemRepository).toDynamicValue(() => AppDataSource.getRepository(Item))
// Projectors
container
.bind<ProjectorInterface<Item, ItemProjection>>(TYPES.ItemProjector)
.toDynamicValue((context: interfaces.Context) => {
return new ItemProjector(context.container.get(TYPES.Timer))
})
// env vars
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
container.bind<TimerInterface>(TYPES.Timer).toDynamicValue(() => new Timer())
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toDynamicValue((context: interfaces.Context) => {
return new SNSDomainEventPublisher(context.container.get(TYPES.SNS), context.container.get(TYPES.SNS_TOPIC_ARN))
})
container
.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory)
.toDynamicValue((context: interfaces.Context) => {
return new DomainEventFactory(context.container.get(TYPES.Timer))
})
container
.bind<ItemTransferCalculatorInterface>(TYPES.ItemTransferCalculator)
.toDynamicValue((context: interfaces.Context) => {
return new ItemTransferCalculator(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.Logger),
)
})
return container
}
}

View File

@@ -1,331 +0,0 @@
import * as winston from 'winston'
import Redis from 'ioredis'
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
import { S3Client } from '@aws-sdk/client-s3'
import { Container } from 'inversify'
import {
DomainEventHandlerInterface,
DomainEventMessageHandlerInterface,
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import { Env } from './Env'
import TYPES from './Types'
import { AuthMiddleware } from '../Controller/AuthMiddleware'
import { MySQLRevisionRepository } from '../Infra/MySQL/MySQLRevisionRepository'
import { Item } from '../Domain/Item/Item'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionProjector } from '../Projection/RevisionProjector'
import { MySQLItemRepository } from '../Infra/MySQL/MySQLItemRepository'
import { ContentDecoder } from '../Domain/Item/ContentDecoder'
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
import { SyncResponseFactory20161215 } from '../Domain/Item/SyncResponse/SyncResponseFactory20161215'
import { SyncResponseFactory20200115 } from '../Domain/Item/SyncResponse/SyncResponseFactory20200115'
import { SyncResponseFactoryResolverInterface } from '../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponseFactoryResolver } from '../Domain/Item/SyncResponse/SyncResponseFactoryResolver'
import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
import { ItemService } from '../Domain/Item/ItemService'
import { AuthHttpServiceInterface } from '../Domain/Auth/AuthHttpServiceInterface'
import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
import { SyncItems } from '../Domain/UseCase/SyncItems'
import { ExtensionsHttpServiceInterface } from '../Domain/Extension/ExtensionsHttpServiceInterface'
import { ExtensionsHttpService } from '../Domain/Extension/ExtensionsHttpService'
import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInterface'
import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
import { RevisionService } from '../Domain/Revision/RevisionService'
import { DuplicateItemSyncedEventHandler } from '../Domain/Handler/DuplicateItemSyncedEventHandler'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { ItemProjector } from '../Projection/ItemProjector'
import { ItemConflictProjector } from '../Projection/ItemConflictProjector'
import { Timer, TimerInterface } from '@standardnotes/time'
import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface'
import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator'
import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter'
import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface'
import { ItemFactory } from '../Domain/Item/ItemFactory'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import { UuidFilter } from '../Domain/Item/SaveRule/UuidFilter'
import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
import {
SNSDomainEventPublisher,
SQSDomainEventSubscriberFactory,
SQSEventMessageHandler,
SQSNewRelicEventMessageHandler,
} from '@standardnotes/domain-events-infra'
import { EmailBackupRequestedEventHandler } from '../Domain/Handler/EmailBackupRequestedEventHandler'
import { CloudBackupRequestedEventHandler } from '../Domain/Handler/CloudBackupRequestedEventHandler'
import { CheckIntegrity } from '../Domain/UseCase/CheckIntegrity/CheckIntegrity'
import { GetItem } from '../Domain/UseCase/GetItem/GetItem'
import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { SavedItemProjection } from '../Projection/SavedItemProjection'
import { SavedItemProjector } from '../Projection/SavedItemProjector'
import { ItemProjection } from '../Projection/ItemProjection'
import { RevisionProjection } from '../Projection/RevisionProjection'
import { ItemConflict } from '../Domain/Item/ItemConflict'
import { ItemConflictProjection } from '../Projection/ItemConflictProjection'
import { AppDataSource } from './DataSource'
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
import { Repository } from 'typeorm'
import { UserContentSizeRecalculationRequestedEventHandler } from '../Domain/Handler/UserContentSizeRecalculationRequestedEventHandler'
import { RevisionMetadataMap } from '../Domain/Map/RevisionMetadataMap'
import { MapperInterface } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/ItemRevisionCreationRequestedEventHandler'
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
private readonly DEFAULT_MAX_ITEMS_LIMIT = 300
private readonly DEFAULT_FILE_UPLOAD_PATH = `${__dirname}/../../uploads`
async load(): Promise<Container> {
const env: Env = new Env()
env.load()
const container = new Container()
await AppDataSource.initialize()
const redisUrl = env.get('REDIS_URL')
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
let redis
if (isRedisInClusterMode) {
redis = new Redis.Cluster(redisUrl.split(','))
} else {
redis = new Redis(redisUrl)
}
container.bind(TYPES.Redis).toConstantValue(redis)
const newrelicWinstonFormatter = newrelicFormatter(winston)
const winstonFormatters = [winston.format.splat(), winston.format.json()]
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
winstonFormatters.push(newrelicWinstonFormatter())
}
const logger = winston.createLogger({
level: env.get('LOG_LEVEL') || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
if (env.get('SNS_TOPIC_ARN', true)) {
const snsConfig: SNSClientConfig = {
apiVersion: 'latest',
region: env.get('SNS_AWS_REGION', true),
}
if (env.get('SNS_ENDPOINT', true)) {
snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
}
if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
snsConfig.credentials = {
accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
}
}
container.bind<SNSClient>(TYPES.SNS).toConstantValue(new SNSClient(snsConfig))
}
if (env.get('SQS_QUEUE_URL', true)) {
const sqsConfig: SQSClientConfig = {
region: env.get('SQS_AWS_REGION', true),
}
if (env.get('SQS_ENDPOINT', true)) {
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
}
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
sqsConfig.credentials = {
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
}
}
container.bind<SQSClient>(TYPES.SQS).toConstantValue(new SQSClient(sqsConfig))
}
let s3Client = undefined
if (env.get('S3_AWS_REGION', true)) {
s3Client = new S3Client({
apiVersion: 'latest',
region: env.get('S3_AWS_REGION', true),
})
}
container.bind<S3Client | undefined>(TYPES.S3).toConstantValue(s3Client)
// Repositories
container.bind<RevisionRepositoryInterface>(TYPES.RevisionRepository).to(MySQLRevisionRepository)
container.bind<ItemRepositoryInterface>(TYPES.ItemRepository).to(MySQLItemRepository)
// ORM
container
.bind<Repository<Revision>>(TYPES.ORMRevisionRepository)
.toConstantValue(AppDataSource.getRepository(Revision))
container.bind<Repository<Item>>(TYPES.ORMItemRepository).toConstantValue(AppDataSource.getRepository(Item))
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
// Projectors
container.bind<ProjectorInterface<Revision, RevisionProjection>>(TYPES.RevisionProjector).to(RevisionProjector)
container.bind<ProjectorInterface<Item, ItemProjection>>(TYPES.ItemProjector).to(ItemProjector)
container.bind<ProjectorInterface<Item, SavedItemProjection>>(TYPES.SavedItemProjector).to(SavedItemProjector)
container
.bind<ProjectorInterface<ItemConflict, ItemConflictProjection>>(TYPES.ItemConflictProjector)
.to(ItemConflictProjector)
// env vars
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container
.bind(TYPES.INTERNAL_DNS_REROUTE_ENABLED)
.toConstantValue(env.get('INTERNAL_DNS_REROUTE_ENABLED', true) === 'true')
container.bind(TYPES.EXTENSIONS_SERVER_URL).toConstantValue(env.get('EXTENSIONS_SERVER_URL', true))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
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.EMAIL_ATTACHMENT_MAX_BYTE_SIZE).toConstantValue(env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE'))
container.bind(TYPES.REVISIONS_FREQUENCY).toConstantValue(env.get('REVISIONS_FREQUENCY'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container
.bind(TYPES.CONTENT_SIZE_TRANSFER_LIMIT)
.toConstantValue(
env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
? +env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
: this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT,
)
container
.bind(TYPES.MAX_ITEMS_LIMIT)
.toConstantValue(
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
)
container
.bind(TYPES.FILE_UPLOAD_PATH)
.toConstantValue(
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
)
// use cases
container.bind<SyncItems>(TYPES.SyncItems).to(SyncItems)
container.bind<CheckIntegrity>(TYPES.CheckIntegrity).to(CheckIntegrity)
container.bind<GetItem>(TYPES.GetItem).to(GetItem)
// Handlers
container
.bind<DuplicateItemSyncedEventHandler>(TYPES.DuplicateItemSyncedEventHandler)
.to(DuplicateItemSyncedEventHandler)
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
.to(AccountDeletionRequestedEventHandler)
container
.bind<EmailBackupRequestedEventHandler>(TYPES.EmailBackupRequestedEventHandler)
.to(EmailBackupRequestedEventHandler)
container
.bind<CloudBackupRequestedEventHandler>(TYPES.CloudBackupRequestedEventHandler)
.to(CloudBackupRequestedEventHandler)
container
.bind<UserContentSizeRecalculationRequestedEventHandler>(TYPES.UserContentSizeRecalculationRequestedEventHandler)
.to(UserContentSizeRecalculationRequestedEventHandler)
container
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.ItemRevisionCreationRequestedEventHandler)
.to(ItemRevisionCreationRequestedEventHandler)
// Map
container
.bind<MapperInterface<RevisionMetadata, SimpleRevisionProjection>>(TYPES.RevisionMetadataMap)
.to(RevisionMetadataMap)
// Services
container.bind<ContentDecoder>(TYPES.ContentDecoder).to(ContentDecoder)
container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container.bind<ItemServiceInterface>(TYPES.ItemService).to(ItemService)
container.bind<ItemTransferCalculatorInterface>(TYPES.ItemTransferCalculator).to(ItemTransferCalculator)
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
container.bind<SyncResponseFactory20161215>(TYPES.SyncResponseFactory20161215).to(SyncResponseFactory20161215)
container.bind<SyncResponseFactory20200115>(TYPES.SyncResponseFactory20200115).to(SyncResponseFactory20200115)
container
.bind<SyncResponseFactoryResolverInterface>(TYPES.SyncResponseFactoryResolver)
.to(SyncResponseFactoryResolver)
container.bind<AuthHttpServiceInterface>(TYPES.AuthHttpService).to(AuthHttpService)
container.bind<ExtensionsHttpServiceInterface>(TYPES.ExtensionsHttpService).to(ExtensionsHttpService)
if (env.get('S3_AWS_REGION', true)) {
container.bind<ItemBackupServiceInterface>(TYPES.ItemBackupService).to(S3ItemBackupService)
} else {
container.bind<ItemBackupServiceInterface>(TYPES.ItemBackupService).to(FSItemBackupService)
}
container.bind<RevisionServiceInterface>(TYPES.RevisionService).to(RevisionService)
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['DUPLICATE_ITEM_SYNCED', container.get(TYPES.DuplicateItemSyncedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
['EMAIL_BACKUP_REQUESTED', container.get(TYPES.EmailBackupRequestedEventHandler)],
['CLOUD_BACKUP_REQUESTED', container.get(TYPES.CloudBackupRequestedEventHandler)],
[
'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
container.get(TYPES.UserContentSizeRecalculationRequestedEventHandler),
],
['ITEM_REVISION_CREATION_REQUESTED', container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
])
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),
),
)
container.bind<ItemFactoryInterface>(TYPES.ItemFactory).to(ItemFactory)
container.bind<OwnershipFilter>(TYPES.OwnershipFilter).to(OwnershipFilter)
container.bind<TimeDifferenceFilter>(TYPES.TimeDifferenceFilter).to(TimeDifferenceFilter)
container.bind<UuidFilter>(TYPES.UuidFilter).to(UuidFilter)
container.bind<ContentTypeFilter>(TYPES.ContentTypeFilter).to(ContentTypeFilter)
container.bind<ContentFilter>(TYPES.ContentFilter).to(ContentFilter)
container
.bind<ItemSaveValidatorInterface>(TYPES.ItemSaveValidator)
.toConstantValue(
new ItemSaveValidator([
container.get(TYPES.OwnershipFilter),
container.get(TYPES.TimeDifferenceFilter),
container.get(TYPES.UuidFilter),
container.get(TYPES.ContentTypeFilter),
container.get(TYPES.ContentFilter),
]),
)
return container
}
}

View File

@@ -1,6 +1,5 @@
import { DataSource, LoggerOptions } from 'typeorm'
import { Item } from '../Domain/Item/Item'
import { Revision } from '../Domain/Revision/Revision'
import { Env } from './Env'
const env: Env = new Env()
@@ -45,7 +44,7 @@ export const AppDataSource = new DataSource({
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [Item, Revision],
entities: [Item],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),

View File

@@ -1,7 +1,5 @@
import { config, DotenvParseOutput } from 'dotenv'
import { injectable } from 'inversify'
@injectable()
export class Env {
private env?: DotenvParseOutput

View File

@@ -0,0 +1,154 @@
import { Container, interfaces } from 'inversify'
import { Env } from './Env'
import TYPES from './Types'
import { AuthMiddleware } from '../Controller/AuthMiddleware'
import { Item } from '../Domain/Item/Item'
import { SyncResponseFactory20161215 } from '../Domain/Item/SyncResponse/SyncResponseFactory20161215'
import { SyncResponseFactory20200115 } from '../Domain/Item/SyncResponse/SyncResponseFactory20200115'
import { SyncResponseFactoryResolverInterface } from '../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponseFactoryResolver } from '../Domain/Item/SyncResponse/SyncResponseFactoryResolver'
import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
import { ItemService } from '../Domain/Item/ItemService'
import { SyncItems } from '../Domain/UseCase/SyncItems'
import { ItemConflictProjector } from '../Projection/ItemConflictProjector'
import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface'
import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator'
import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter'
import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface'
import { ItemFactory } from '../Domain/Item/ItemFactory'
import { UuidFilter } from '../Domain/Item/SaveRule/UuidFilter'
import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
import { CheckIntegrity } from '../Domain/UseCase/CheckIntegrity/CheckIntegrity'
import { GetItem } from '../Domain/UseCase/GetItem/GetItem'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { SavedItemProjection } from '../Projection/SavedItemProjection'
import { SavedItemProjector } from '../Projection/SavedItemProjector'
import { ItemConflict } from '../Domain/Item/ItemConflict'
import { ItemConflictProjection } from '../Projection/ItemConflictProjection'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
private readonly DEFAULT_MAX_ITEMS_LIMIT = 300
override async load(): Promise<Container> {
const container = await super.load()
const env: Env = container.get(TYPES.Env)
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).toDynamicValue((context: interfaces.Context) => {
return new AuthMiddleware(context.container.get(TYPES.AUTH_JWT_SECRET), context.container.get(TYPES.Logger))
})
// Projectors
container
.bind<ProjectorInterface<Item, SavedItemProjection>>(TYPES.SavedItemProjector)
.toDynamicValue((context: interfaces.Context) => {
return new SavedItemProjector(context.container.get(TYPES.Timer))
})
container
.bind<ProjectorInterface<ItemConflict, ItemConflictProjection>>(TYPES.ItemConflictProjector)
.toDynamicValue((context: interfaces.Context) => {
return new ItemConflictProjector(context.container.get(TYPES.ItemProjector))
})
// env vars
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container.bind(TYPES.REVISIONS_FREQUENCY).toConstantValue(env.get('REVISIONS_FREQUENCY'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container
.bind(TYPES.CONTENT_SIZE_TRANSFER_LIMIT)
.toConstantValue(
env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
? +env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
: this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT,
)
container
.bind(TYPES.MAX_ITEMS_LIMIT)
.toConstantValue(
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
)
// use cases
container.bind<SyncItems>(TYPES.SyncItems).toDynamicValue((context: interfaces.Context) => {
return new SyncItems(context.container.get(TYPES.ItemService))
})
container.bind<CheckIntegrity>(TYPES.CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
return new CheckIntegrity(context.container.get(TYPES.ItemRepository))
})
container.bind<GetItem>(TYPES.GetItem).toDynamicValue((context: interfaces.Context) => {
return new GetItem(context.container.get(TYPES.ItemRepository))
})
// Services
container.bind<ItemServiceInterface>(TYPES.ItemService).toDynamicValue((context: interfaces.Context) => {
return new ItemService(
context.container.get(TYPES.ItemSaveValidator),
context.container.get(TYPES.ItemFactory),
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.DomainEventPublisher),
context.container.get(TYPES.DomainEventFactory),
context.container.get(TYPES.REVISIONS_FREQUENCY),
context.container.get(TYPES.CONTENT_SIZE_TRANSFER_LIMIT),
context.container.get(TYPES.ItemTransferCalculator),
context.container.get(TYPES.Timer),
context.container.get(TYPES.ItemProjector),
context.container.get(TYPES.MAX_ITEMS_LIMIT),
context.container.get(TYPES.Logger),
)
})
container
.bind<SyncResponseFactory20161215>(TYPES.SyncResponseFactory20161215)
.toDynamicValue((context: interfaces.Context) => {
return new SyncResponseFactory20161215(context.container.get(TYPES.ItemProjector))
})
container
.bind<SyncResponseFactory20200115>(TYPES.SyncResponseFactory20200115)
.toDynamicValue((context: interfaces.Context) => {
return new SyncResponseFactory20200115(
context.container.get(TYPES.ItemProjector),
context.container.get(TYPES.ItemConflictProjector),
context.container.get(TYPES.SavedItemProjector),
)
})
container
.bind<SyncResponseFactoryResolverInterface>(TYPES.SyncResponseFactoryResolver)
.toDynamicValue((context: interfaces.Context) => {
return new SyncResponseFactoryResolver(
context.container.get(TYPES.SyncResponseFactory20161215),
context.container.get(TYPES.SyncResponseFactory20200115),
)
})
container.bind<ItemFactoryInterface>(TYPES.ItemFactory).toDynamicValue((context: interfaces.Context) => {
return new ItemFactory(context.container.get(TYPES.Timer), context.container.get(TYPES.ItemProjector))
})
container.bind<OwnershipFilter>(TYPES.OwnershipFilter).toDynamicValue(() => new OwnershipFilter())
container
.bind<TimeDifferenceFilter>(TYPES.TimeDifferenceFilter)
.toDynamicValue((context: interfaces.Context) => new TimeDifferenceFilter(context.container.get(TYPES.Timer)))
container.bind<UuidFilter>(TYPES.UuidFilter).toDynamicValue(() => new UuidFilter())
container.bind<ContentTypeFilter>(TYPES.ContentTypeFilter).toDynamicValue(() => new ContentTypeFilter())
container.bind<ContentFilter>(TYPES.ContentFilter).toDynamicValue(() => new ContentFilter())
container
.bind<ItemSaveValidatorInterface>(TYPES.ItemSaveValidator)
.toDynamicValue((context: interfaces.Context) => {
return new ItemSaveValidator([
context.container.get(TYPES.OwnershipFilter),
context.container.get(TYPES.TimeDifferenceFilter),
context.container.get(TYPES.UuidFilter),
context.container.get(TYPES.ContentTypeFilter),
context.container.get(TYPES.ContentFilter),
])
})
return container
}
}

View File

@@ -5,16 +5,14 @@ const TYPES = {
SNS: Symbol.for('SNS'),
SQS: Symbol.for('SQS'),
S3: Symbol.for('S3'),
Env: Symbol.for('Env'),
// Repositories
RevisionRepository: Symbol.for('RevisionRepository'),
ItemRepository: Symbol.for('ItemRepository'),
// ORM
ORMRevisionRepository: Symbol.for('ORMRevisionRepository'),
ORMItemRepository: Symbol.for('ORMItemRepository'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
// Projectors
RevisionProjector: Symbol.for('RevisionProjector'),
ItemProjector: Symbol.for('ItemProjector'),
SavedItemProjector: Symbol.for('SavedItemProjector'),
ItemConflictProjector: Symbol.for('ItemConflictProjector'),
@@ -25,7 +23,6 @@ const TYPES = {
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
INTERNAL_DNS_REROUTE_ENABLED: Symbol.for('INTERNAL_DNS_REROUTE_ENABLED'),
EXTENSIONS_SERVER_URL: Symbol.for('EXTENSIONS_SERVER_URL'),
AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'),
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
@@ -46,10 +43,7 @@ const TYPES = {
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
CloudBackupRequestedEventHandler: Symbol.for('CloudBackupRequestedEventHandler'),
UserContentSizeRecalculationRequestedEventHandler: Symbol.for('UserContentSizeRecalculationRequestedEventHandler'),
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
// Map
RevisionMetadataMap: Symbol.for('RevisionMetadataMap'),
// Services
ContentDecoder: Symbol.for('ContentDecoder'),
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
@@ -65,7 +59,6 @@ const TYPES = {
AuthHttpService: Symbol.for('AuthHttpService'),
ExtensionsHttpService: Symbol.for('ExtensionsHttpService'),
ItemBackupService: Symbol.for('ItemBackupService'),
RevisionService: Symbol.for('RevisionService'),
ItemSaveValidator: Symbol.for('ItemSaveValidator'),
OwnershipFilter: Symbol.for('OwnershipFilter'),
TimeDifferenceFilter: Symbol.for('TimeDifferenceFilter'),

View File

@@ -0,0 +1,221 @@
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { S3Client } from '@aws-sdk/client-s3'
import { Container, interfaces } from 'inversify'
import {
DomainEventHandlerInterface,
DomainEventMessageHandlerInterface,
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import { Env } from './Env'
import TYPES from './Types'
import { ContentDecoder } from '../Domain/Item/ContentDecoder'
import { AuthHttpServiceInterface } from '../Domain/Auth/AuthHttpServiceInterface'
import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
import { ExtensionsHttpServiceInterface } from '../Domain/Extension/ExtensionsHttpServiceInterface'
import { ExtensionsHttpService } from '../Domain/Extension/ExtensionsHttpService'
import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInterface'
import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
import { DuplicateItemSyncedEventHandler } from '../Domain/Handler/DuplicateItemSyncedEventHandler'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import {
SQSDomainEventSubscriberFactory,
SQSEventMessageHandler,
SQSNewRelicEventMessageHandler,
} from '@standardnotes/domain-events-infra'
import { EmailBackupRequestedEventHandler } from '../Domain/Handler/EmailBackupRequestedEventHandler'
import { CloudBackupRequestedEventHandler } from '../Domain/Handler/CloudBackupRequestedEventHandler'
import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/ItemRevisionCreationRequestedEventHandler'
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
private readonly DEFAULT_FILE_UPLOAD_PATH = `${__dirname}/../../uploads`
override async load(): Promise<Container> {
const container = await super.load()
const env: Env = container.get(TYPES.Env)
container.bind<SQSClient>(TYPES.SQS).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const sqsConfig: SQSClientConfig = {
region: env.get('SQS_AWS_REGION'),
}
if (env.get('SQS_ENDPOINT', true)) {
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
}
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
sqsConfig.credentials = {
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
}
}
return new SQSClient(sqsConfig)
})
container.bind<S3Client | undefined>(TYPES.S3).toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
let s3Client = undefined
if (env.get('S3_AWS_REGION', true)) {
s3Client = new S3Client({
apiVersion: 'latest',
region: env.get('S3_AWS_REGION', true),
})
}
return s3Client
})
// env vars
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
container.bind(TYPES.EXTENSIONS_SERVER_URL).toConstantValue(env.get('EXTENSIONS_SERVER_URL', true))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
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.EMAIL_ATTACHMENT_MAX_BYTE_SIZE).toConstantValue(env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container
.bind(TYPES.FILE_UPLOAD_PATH)
.toConstantValue(
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
)
// Handlers
container
.bind<DuplicateItemSyncedEventHandler>(TYPES.DuplicateItemSyncedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new DuplicateItemSyncedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.DomainEventFactory),
context.container.get(TYPES.DomainEventPublisher),
context.container.get(TYPES.Logger),
)
})
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new AccountDeletionRequestedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.Logger),
)
})
container
.bind<EmailBackupRequestedEventHandler>(TYPES.EmailBackupRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new EmailBackupRequestedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.AuthHttpService),
context.container.get(TYPES.ItemBackupService),
context.container.get(TYPES.DomainEventPublisher),
context.container.get(TYPES.DomainEventFactory),
context.container.get(TYPES.EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
context.container.get(TYPES.ItemTransferCalculator),
context.container.get(TYPES.S3_BACKUP_BUCKET_NAME),
context.container.get(TYPES.Logger),
)
})
container
.bind<CloudBackupRequestedEventHandler>(TYPES.CloudBackupRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new CloudBackupRequestedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.AuthHttpService),
context.container.get(TYPES.ExtensionsHttpService),
context.container.get(TYPES.ItemBackupService),
context.container.get(TYPES.EXTENSIONS_SERVER_URL),
context.container.get(TYPES.Logger),
)
})
container
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.ItemRevisionCreationRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new ItemRevisionCreationRequestedEventHandler(
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.ItemBackupService),
context.container.get(TYPES.DomainEventFactory),
context.container.get(TYPES.DomainEventPublisher),
)
})
// Services
container.bind<ContentDecoder>(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder())
container.bind<AxiosInstance>(TYPES.HTTPClient).toDynamicValue(() => axios.create())
container.bind<AuthHttpServiceInterface>(TYPES.AuthHttpService).toDynamicValue((context: interfaces.Context) => {
return new AuthHttpService(context.container.get(TYPES.HTTPClient), context.container.get(TYPES.AUTH_SERVER_URL))
})
container
.bind<ExtensionsHttpServiceInterface>(TYPES.ExtensionsHttpService)
.toDynamicValue((context: interfaces.Context) => {
return new ExtensionsHttpService(
context.container.get(TYPES.HTTPClient),
context.container.get(TYPES.ItemRepository),
context.container.get(TYPES.ContentDecoder),
context.container.get(TYPES.DomainEventPublisher),
context.container.get(TYPES.DomainEventFactory),
context.container.get(TYPES.Logger),
)
})
container
.bind<ItemBackupServiceInterface>(TYPES.ItemBackupService)
.toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
if (env.get('S3_AWS_REGION', true)) {
return new S3ItemBackupService(
context.container.get(TYPES.S3_BACKUP_BUCKET_NAME),
context.container.get(TYPES.ItemProjector),
context.container.get(TYPES.Logger),
context.container.get(TYPES.S3),
)
} else {
return new FSItemBackupService(
context.container.get(TYPES.FILE_UPLOAD_PATH),
context.container.get(TYPES.ItemProjector),
context.container.get(TYPES.Logger),
)
}
})
container
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
.toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Env)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['DUPLICATE_ITEM_SYNCED', context.container.get(TYPES.DuplicateItemSyncedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', context.container.get(TYPES.AccountDeletionRequestedEventHandler)],
['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)],
['CLOUD_BACKUP_REQUESTED', context.container.get(TYPES.CloudBackupRequestedEventHandler)],
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
])
const handler =
env.get('NEW_RELIC_ENABLED', true) === 'true'
? new SQSNewRelicEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger))
: new SQSEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger))
return handler
})
container
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
.toDynamicValue((context: interfaces.Context) => {
return new SQSDomainEventSubscriberFactory(
context.container.get(TYPES.SQS),
context.container.get(TYPES.SQS_QUEUE_URL),
context.container.get(TYPES.DomainEventMessageHandler),
)
})
return container
}
}

View File

@@ -1,19 +1,12 @@
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston'
import { RoleName } from '@standardnotes/domain-core'
import TYPES from '../Bootstrap/Types'
@injectable()
export class AuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.AUTH_JWT_SECRET) private authJWTSecret: string,
@inject(TYPES.Logger) private logger: winston.Logger,
) {
constructor(private authJWTSecret: string, private logger: winston.Logger) {
super()
}

View File

@@ -1,99 +0,0 @@
import 'reflect-metadata'
import { Revision } from '../Domain/Revision/Revision'
import * as express from 'express'
import { RevisionsController } from './RevisionsController'
import { results } from 'inversify-express-utils'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
import { RevisionProjection } from '../Projection/RevisionProjection'
import { MapperInterface } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
describe('RevisionsController', () => {
let revisionProjector: ProjectorInterface<Revision, RevisionProjection>
let revisionMap: MapperInterface<RevisionMetadata, SimpleRevisionProjection>
let revisionService: RevisionServiceInterface
let revision: Revision
let revisionMetadata: RevisionMetadata
let request: express.Request
let response: express.Response
const createController = () => new RevisionsController(revisionService, revisionProjector, revisionMap)
beforeEach(() => {
revision = {} as jest.Mocked<Revision>
revisionMetadata = {} as jest.Mocked<RevisionMetadata>
revisionMap = {} as jest.Mocked<MapperInterface<RevisionMetadata, SimpleRevisionProjection>>
revisionProjector = {} as jest.Mocked<ProjectorInterface<Revision, RevisionProjection>>
revisionService = {} as jest.Mocked<RevisionServiceInterface>
revisionService.getRevisionsMetadata = jest.fn().mockReturnValue([revisionMetadata])
revisionService.getRevision = jest.fn().mockReturnValue(revision)
revisionService.removeRevision = jest.fn().mockReturnValue(true)
request = {
params: {},
} as jest.Mocked<express.Request>
response = {
locals: {},
} as jest.Mocked<express.Response>
response.locals.user = {
uuid: '123',
}
response.locals.roleNames = ['BASIC_USER']
})
it('should return revisions for an item', async () => {
revisionMap.toProjection = jest.fn().mockReturnValue({ foo: 'bar' })
const revisionResponse = await createController().getRevisions(request, response)
expect(revisionResponse.json).toEqual([{ foo: 'bar' }])
})
it('should return a specific revision for an item', async () => {
revisionProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
const httpResponse = <results.JsonResult>await createController().getRevision(request, response)
expect(httpResponse.json).toEqual({ foo: 'bar' })
})
it('should remove a specific revision for an item', async () => {
const httpResponse = await createController().deleteRevision(request, response)
expect(httpResponse).toBeInstanceOf(results.OkResult)
})
it('should not remove a specific revision for an item if it fails', async () => {
revisionService.removeRevision = jest.fn().mockReturnValue(false)
const httpResponse = await createController().deleteRevision(request, response)
expect(httpResponse).toBeInstanceOf(results.BadRequestResult)
})
it('should not remove a specific revision for an item the session is read only', async () => {
response.locals.readOnlyAccess = true
const httpResponse = await createController().deleteRevision(request, response)
const result = await httpResponse.executeAsync()
expect(result.statusCode).toEqual(401)
})
it('should return a 404 for a not found specific revision in an item', async () => {
revisionService.getRevision = jest.fn().mockReturnValue(null)
const httpResponse = await createController().getRevision(request, response)
expect(httpResponse).toBeInstanceOf(results.NotFoundResult)
})
})

View File

@@ -1,85 +0,0 @@
import { Request, Response } from 'express'
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { inject } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { ProjectorInterface } from '../Projection/ProjectorInterface'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
import { ErrorTag } from '@standardnotes/api'
import { RevisionProjection } from '../Projection/RevisionProjection'
import { MapperInterface } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
import { SimpleRevisionProjection } from '../Projection/SimpleRevisionProjection'
@controller('/items/:itemUuid/revisions', TYPES.AuthMiddleware)
export class RevisionsController extends BaseHttpController {
constructor(
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
@inject(TYPES.RevisionProjector) private revisionProjector: ProjectorInterface<Revision, RevisionProjection>,
@inject(TYPES.RevisionMetadataMap)
private revisionMetadataMap: MapperInterface<RevisionMetadata, SimpleRevisionProjection>,
) {
super()
}
@httpGet('/')
public async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
const metadatas = await this.revisionService.getRevisionsMetadata(response.locals.user.uuid, req.params.itemUuid)
const metadataProjections = []
for (const metadata of metadatas) {
metadataProjections.push(this.revisionMetadataMap.toProjection(metadata))
}
return this.json(metadataProjections)
}
@httpGet('/:uuid')
public async getRevision(request: Request, response: Response): Promise<results.JsonResult | results.NotFoundResult> {
const revision = await this.revisionService.getRevision({
userRoles: response.locals.roleNames,
userUuid: response.locals.user.uuid,
itemUuid: request.params.itemUuid,
revisionUuid: request.params.uuid,
})
if (!revision) {
return this.notFound()
}
const revisionProjection = await this.revisionProjector.projectFull(revision)
return this.json(revisionProjection)
}
@httpDelete('/:uuid')
public async deleteRevision(
request: Request,
response: Response,
): Promise<results.BadRequestResult | results.OkResult | results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json(
{
error: {
tag: ErrorTag.ReadOnlyAccess,
message: 'Session has read-only access.',
},
},
401,
)
}
const success = await this.revisionService.removeRevision({
userUuid: response.locals.user.uuid,
itemUuid: request.params.itemUuid,
revisionUuid: request.params.uuid,
})
if (!success) {
return this.badRequest()
}
return this.ok()
}
}

View File

@@ -6,35 +6,13 @@ import {
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
RevisionsCopyRequestedEvent,
RevisionsOwnershipUpdateRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@injectable()
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createRevisionsOwnershipUpdateRequestedEvent(dto: {
userUuid: string
itemUuid: string
}): RevisionsOwnershipUpdateRequestedEvent {
return {
type: 'REVISIONS_OWNERSHIP_UPDATE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.SyncingServer,
},
payload: dto,
}
}
constructor(private timer: TimerInterface) {}
createRevisionsCopyRequestedEvent(
userUuid: string,

View File

@@ -4,7 +4,6 @@ import {
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
RevisionsCopyRequestedEvent,
RevisionsOwnershipUpdateRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
} from '@standardnotes/domain-events'
@@ -31,8 +30,4 @@ export interface DomainEventFactoryInterface {
userUuid: string,
dto: { originalItemUuid: string; newItemUuid: string },
): RevisionsCopyRequestedEvent
createRevisionsOwnershipUpdateRequestedEvent(dto: {
userUuid: string
itemUuid: string
}): RevisionsOwnershipUpdateRequestedEvent
}

View File

@@ -2,9 +2,8 @@ import { KeyParamsData } from '@standardnotes/responses'
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { AxiosInstance } from 'axios'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ContentDecoderInterface } from '../Item/ContentDecoderInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
@@ -15,15 +14,14 @@ import { getBody as googleDriveBody, getSubject as googleDriveSubject } from '..
import { getBody as dropboxBody, getSubject as dropboxSubject } from '../Email/DropboxBackupFailed'
import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email/OneDriveBackupFailed'
@injectable()
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
constructor(
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.ContentDecoder) private contentDecoder: ContentDecoderInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Logger) private logger: Logger,
private httpClient: AxiosInstance,
private itemRepository: ItemRepositoryInterface,
private contentDecoder: ContentDecoderInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
async triggerCloudBackupOnExtensionsServer(dto: {

View File

@@ -36,7 +36,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
}
})
it('should remove all items and revision for a user', async () => {
it('should remove all items for a user', async () => {
await createHandler().handle(event)
expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')

View File

@@ -1,15 +1,9 @@
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
@injectable()
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
await this.itemRepository.deleteByUserUuid(event.payload.userUuid)

View File

@@ -1,7 +1,5 @@
import { DomainEventHandlerInterface, CloudBackupRequestedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { ItemQuery } from '../Item/ItemQuery'
import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
@@ -11,15 +9,14 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
import { Logger } from 'winston'
import { KeyParamsData } from '@standardnotes/responses'
@injectable()
export class CloudBackupRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.AuthHttpService) private authHttpService: AuthHttpServiceInterface,
@inject(TYPES.ExtensionsHttpService) private extensionsHttpService: ExtensionsHttpServiceInterface,
@inject(TYPES.ItemBackupService) private itemBackupService: ItemBackupServiceInterface,
@inject(TYPES.EXTENSIONS_SERVER_URL) private extensionsServerUrl: string,
@inject(TYPES.Logger) private logger: Logger,
private itemRepository: ItemRepositoryInterface,
private authHttpService: AuthHttpServiceInterface,
private extensionsHttpService: ExtensionsHttpServiceInterface,
private itemBackupService: ItemBackupServiceInterface,
private extensionsServerUrl: string,
private logger: Logger,
) {}
async handle(event: CloudBackupRequestedEvent): Promise<void> {

View File

@@ -3,19 +3,16 @@ import {
DomainEventPublisherInterface,
DuplicateItemSyncedEvent,
} from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
@injectable()
export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Logger) private logger: Logger,
private itemRepository: ItemRepositoryInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private logger: Logger,
) {}
async handle(event: DuplicateItemSyncedEvent): Promise<void> {

View File

@@ -5,9 +5,7 @@ import {
EmailBackupRequestedEvent,
} from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
@@ -16,18 +14,17 @@ import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorI
import { ItemQuery } from '../Item/ItemQuery'
import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
@injectable()
export class EmailBackupRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.AuthHttpService) private authHttpService: AuthHttpServiceInterface,
@inject(TYPES.ItemBackupService) private itemBackupService: ItemBackupServiceInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.EMAIL_ATTACHMENT_MAX_BYTE_SIZE) private emailAttachmentMaxByteSize: number,
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
@inject(TYPES.S3_BACKUP_BUCKET_NAME) private s3BackupBucketName: string,
@inject(TYPES.Logger) private logger: Logger,
private itemRepository: ItemRepositoryInterface,
private authHttpService: AuthHttpServiceInterface,
private itemBackupService: ItemBackupServiceInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private emailAttachmentMaxByteSize: number,
private itemTransferCalculator: ItemTransferCalculatorInterface,
private s3BackupBucketName: string,
private logger: Logger,
) {}
async handle(event: EmailBackupRequestedEvent): Promise<void> {

View File

@@ -3,20 +3,17 @@ import {
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'
@injectable()
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.ItemBackupService) private itemBackupService: ItemBackupServiceInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
private itemRepository: ItemRepositoryInterface,
private itemBackupService: ItemBackupServiceInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
) {}
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {

View File

@@ -1,73 +0,0 @@
/* istanbul ignore file */
import { DomainEventHandlerInterface, UserContentSizeRecalculationRequestedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Stream } from 'stream'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Item } from '../Item/Item'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
@injectable()
export class UserContentSizeRecalculationRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: UserContentSizeRecalculationRequestedEvent): Promise<void> {
this.logger.debug(`Starting content size recalculation for user: ${event.payload.userUuid}`)
const stream = await this.itemRepository.streamAll({
deleted: false,
userUuid: event.payload.userUuid,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
})
const loggerHandle = this.logger
await new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (item, _encoding, callback) => {
if (!item.item_uuid) {
callback()
return
}
loggerHandle.debug(`Fixing content size for item ${item.item_uuid}`)
const modelItem = await this.itemRepository.findByUuid(item.item_uuid)
if (modelItem !== null) {
const fixedContentSize = Buffer.byteLength(
JSON.stringify(await this.itemProjector.projectFull(modelItem)),
)
if (modelItem.contentSize !== fixedContentSize) {
loggerHandle.debug(`Fixing content size from ${modelItem.contentSize} to ${fixedContentSize}`)
modelItem.contentSize = fixedContentSize
await this.itemRepository.save(modelItem)
}
callback()
return
}
callback()
return
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
}

View File

@@ -1,7 +1,5 @@
import { injectable } from 'inversify'
import { ContentDecoderInterface } from './ContentDecoderInterface'
@injectable()
export class ContentDecoder implements ContentDecoderInterface {
decode(content: string): Record<string, unknown> {
try {

View File

@@ -1,6 +1,5 @@
import { ContentType } from '@standardnotes/common'
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
import { Revision } from '../Revision/Revision'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'items' })
@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
@@ -110,14 +109,6 @@ export class Item {
@Index('updated_at_timestamp')
declare updatedAtTimestamp: number
@OneToMany(
/* istanbul ignore next */
() => Revision,
/* istanbul ignore next */
(revision) => revision.item,
)
declare revisions: Promise<Revision[]>
@Column({
name: 'updated_with_session',
type: 'varchar',

View File

@@ -1,19 +1,13 @@
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Item } from './Item'
import { ItemFactoryInterface } from './ItemFactoryInterface'
import { ItemHash } from './ItemHash'
@injectable()
export class ItemFactory implements ItemFactoryInterface {
constructor(
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
) {}
constructor(private timer: TimerInterface, private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item {
const item = this.create(dto)

View File

@@ -1,10 +1,8 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { Time, TimerInterface } from '@standardnotes/time'
import { ContentType } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { GetItemsDTO } from './GetItemsDTO'
import { GetItemsResult } from './GetItemsResult'
@@ -23,24 +21,23 @@ import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterfa
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
@injectable()
export class ItemService implements ItemServiceInterface {
private readonly DEFAULT_ITEMS_LIMIT = 150
private readonly SYNC_TOKEN_VERSION = 2
constructor(
@inject(TYPES.ItemSaveValidator) private itemSaveValidator: ItemSaveValidatorInterface,
@inject(TYPES.ItemFactory) private itemFactory: ItemFactoryInterface,
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.REVISIONS_FREQUENCY) private revisionFrequency: number,
@inject(TYPES.CONTENT_SIZE_TRANSFER_LIMIT) private contentSizeTransferLimit: number,
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.MAX_ITEMS_LIMIT) private maxItemsSyncLimit: number,
@inject(TYPES.Logger) private logger: Logger,
private itemSaveValidator: ItemSaveValidatorInterface,
private itemFactory: ItemFactoryInterface,
private itemRepository: ItemRepositoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private revisionFrequency: number,
private contentSizeTransferLimit: number,
private itemTransferCalculator: ItemTransferCalculatorInterface,
private timer: TimerInterface,
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private maxItemsSyncLimit: number,
private logger: Logger,
) {}
async getItems(dto: GetItemsDTO): Promise<GetItemsResult> {

View File

@@ -1,18 +1,11 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ItemQuery } from './ItemQuery'
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
@injectable()
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
async computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>> {
const itemUuidsToFetch = []

View File

@@ -1,10 +1,8 @@
import { injectable } from 'inversify'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class ContentFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
if (dto.itemHash.content === undefined || dto.itemHash.content === null) {

View File

@@ -1,11 +1,10 @@
import { ContentType } from '@standardnotes/common'
import { injectable } from 'inversify'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class ContentTypeFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const validContentType = Object.values(ContentType).includes(dto.itemHash.content_type as ContentType)

View File

@@ -1,10 +1,8 @@
import { injectable } from 'inversify'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class OwnershipFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const itemBelongsToADifferentUser = dto.existingItem !== null && dto.existingItem.userUuid !== dto.userUuid

View File

@@ -1,6 +1,5 @@
import { Time, TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { ApiVersion } from '../../Api/ApiVersion'
import { ItemHash } from '../ItemHash'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
@@ -8,9 +7,8 @@ import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class TimeDifferenceFilter implements ItemSaveRuleInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
constructor(private timer: TimerInterface) {}
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
if (!dto.existingItem) {

View File

@@ -1,11 +1,9 @@
import { injectable } from 'inversify'
import { validate } from 'uuid'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class UuidFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const validUuid = validate(dto.itemHash.uuid)

View File

@@ -1,10 +1,8 @@
import { injectable } from 'inversify'
import { ItemSaveRuleInterface } from '../SaveRule/ItemSaveRuleInterface'
import { ItemSaveValidationDTO } from './ItemSaveValidationDTO'
import { ItemSaveValidationResult } from './ItemSaveValidationResult'
import { ItemSaveValidatorInterface } from './ItemSaveValidatorInterface'
@injectable()
export class ItemSaveValidator implements ItemSaveValidatorInterface {
constructor(private rules: Array<ItemSaveRuleInterface>) {}

View File

@@ -1,5 +1,3 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { SyncItemsResponse } from '../../UseCase/SyncItemsResponse'
import { Item } from '../Item'
@@ -10,11 +8,10 @@ import { SyncResponse20161215 } from './SyncResponse20161215'
import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
import { ConflictType } from '@standardnotes/responses'
@injectable()
export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface {
private readonly LEGACY_MIN_CONFLICT_INTERVAL = 20_000_000
constructor(@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
constructor(private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20161215> {
const conflicts = syncItemsResponse.conflicts.filter(

View File

@@ -1,5 +1,3 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { SyncItemsResponse } from '../../UseCase/SyncItemsResponse'
import { Item } from '../Item'
@@ -10,13 +8,11 @@ import { SyncResponse20200115 } from './SyncResponse20200115'
import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
@injectable()
export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface {
constructor(
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.ItemConflictProjector)
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>,
@inject(TYPES.SavedItemProjector) private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
) {}
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {

View File

@@ -1,16 +1,13 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { ApiVersion } from '../../Api/ApiVersion'
import { SyncResponseFactory20161215 } from './SyncResponseFactory20161215'
import { SyncResponseFactory20200115 } from './SyncResponseFactory20200115'
import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
import { SyncResponseFactoryResolverInterface } from './SyncResponseFactoryResolverInterface'
@injectable()
export class SyncResponseFactoryResolver implements SyncResponseFactoryResolverInterface {
constructor(
@inject(TYPES.SyncResponseFactory20161215) private syncResponseFactory20161215: SyncResponseFactory20161215,
@inject(TYPES.SyncResponseFactory20200115) private syncResponseFactory20200115: SyncResponseFactory20200115,
private syncResponseFactory20161215: SyncResponseFactory20161215,
private syncResponseFactory20200115: SyncResponseFactory20200115,
) {}
resolveSyncResponseFactoryVersion(apiVersion?: string): SyncResponseFactoryInterface {

View File

@@ -1,44 +0,0 @@
/* istanbul ignore file */
import { ContentType } from '@standardnotes/common'
import { MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { SimpleRevisionProjection } from '../../Projection/SimpleRevisionProjection'
import { RevisionMetadata } from '../Revision/RevisionMetadata'
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
@injectable()
export class RevisionMetadataMap implements MapperInterface<RevisionMetadata, SimpleRevisionProjection> {
constructor(
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
toDomain(persistence: SimpleRevisionProjection): RevisionMetadata {
const revisionMetadatOrError = RevisionMetadata.create(
{
contentType: persistence.content_type,
createdAt: this.timer.convertStringDateToDate(persistence.created_at),
updatedAt: this.timer.convertStringDateToDate(persistence.updated_at),
},
new UniqueEntityId(persistence.uuid),
)
if (revisionMetadatOrError.isFailed()) {
throw new Error(revisionMetadatOrError.getError())
}
return revisionMetadatOrError.getValue()
}
toProjection(domain: RevisionMetadata): SimpleRevisionProjection {
return {
uuid: domain.id.toString(),
content_type: domain.props.contentType as ContentType | null,
required_role: this.revisionService.calculateRequiredRoleBasedOnRevisionDate(domain.props.createdAt),
created_at: this.timer.convertDateToISOString(domain.props.createdAt),
updated_at: this.timer.convertDateToISOString(domain.props.updatedAt),
}
}
}

View File

@@ -1,7 +0,0 @@
import { Revision } from './Revision'
describe('Revision', () => {
it('should instantiate', () => {
expect(new Revision()).toBeInstanceOf(Revision)
})
})

View File

@@ -1,81 +0,0 @@
import { ContentType } from '@standardnotes/common'
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { Item } from '../Item/Item'
@Entity({ name: 'revisions' })
export class Revision {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@ManyToOne(
/* istanbul ignore next */
() => Item,
/* istanbul ignore next */
(item) => item.revisions,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'item_uuid', referencedColumnName: 'uuid' })
declare item: Promise<Item>
@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
}

View File

@@ -1,28 +0,0 @@
import { UniqueEntityId, Entity, Result } 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> {
if (!(props.createdAt instanceof Date)) {
return Result.fail<RevisionMetadata>(
`Could not create Revision Metadata. Creation date should be a date object, given: ${props.createdAt}`,
)
}
if (!(props.updatedAt instanceof Date)) {
return Result.fail<RevisionMetadata>(
`Could not create Revision Metadata. Update date should be a date object, given: ${props.updatedAt}`,
)
}
return Result.ok<RevisionMetadata>(new RevisionMetadata(props, id))
}
}

View File

@@ -1,5 +0,0 @@
export interface RevisionMetadataProps {
contentType: string | null
createdAt: Date
updatedAt: Date
}

View File

@@ -1,10 +0,0 @@
import { Revision } from './Revision'
import { RevisionMetadata } from './RevisionMetadata'
export interface RevisionRepositoryInterface {
findByItemId(parameters: { itemUuid: string; afterDate?: Date }): Promise<Array<Revision>>
findOneById(itemId: string, id: string): Promise<Revision | null>
save(revision: Revision): Promise<Revision>
removeByUuid(itemUuid: string, revisionUuid: string): Promise<void>
findMetadataByItemId(itemUuid: string): Promise<Array<RevisionMetadata>>
}

View File

@@ -1,193 +0,0 @@
import 'reflect-metadata'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { Item } from '../Item/Item'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { Revision } from './Revision'
import { RevisionMetadata } from './RevisionMetadata'
import { RevisionRepositoryInterface } from './RevisionRepositoryInterface'
import { RevisionService } from './RevisionService'
describe('RevisionService', () => {
let revisionRepository: RevisionRepositoryInterface
let timer: TimerInterface
let itemRepository: ItemRepositoryInterface
let revision1: Revision
let revision2: Revision
const createService = () => new RevisionService(revisionRepository, itemRepository, timer)
beforeEach(() => {
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
revisionRepository.save = jest.fn().mockImplementation((revision: Revision) => {
revision.uuid = '3-4-5'
return revision
})
timer = {} as jest.Mocked<TimerInterface>
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(0)
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuid = jest.fn().mockReturnValue({
userUuid: '1-2-3',
} as jest.Mocked<Item>)
revision1 = {
uuid: '1-2-3',
item: Promise.resolve({
uuid: '1-2-3',
}),
content: 'content1',
} as jest.Mocked<Revision>
revision2 = {
uuid: '2-3-4',
item: Promise.resolve({
uuid: '1-2-3',
}),
content: 'content2',
} as jest.Mocked<Revision>
revisionRepository.findByItemId = jest.fn().mockReturnValue([revision1, revision2])
revisionRepository.findMetadataByItemId = jest.fn().mockReturnValue([{} as jest.Mocked<RevisionMetadata>])
revisionRepository.findOneById = jest.fn().mockReturnValue(revision1)
revisionRepository.removeByUuid = jest.fn()
})
it('should not remove a revision for a non existing item', async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
expect(
await createService().removeRevision({
itemUuid: '1-2-3',
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeFalsy()
})
it("should not remove a revision for another user's item", async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
expect(
await createService().removeRevision({
itemUuid: '1-2-3',
userUuid: '3-4-5',
revisionUuid: '3-4-5',
}),
).toBeFalsy()
})
it('should remove a revision if user has rights', async () => {
expect(
await createService().removeRevision({
itemUuid: '1-2-3',
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeTruthy()
expect(revisionRepository.removeByUuid).toHaveBeenCalledWith('1-2-3', '3-4-5')
})
it('should not get a revision for a non existing item', async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser],
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeNull()
})
it("should not get a revision for another user's item", async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser],
userUuid: '3-4-5',
revisionUuid: '3-4-5',
}),
).toBeNull()
})
it('should not get a revision that does not exist', async () => {
revisionRepository.findOneById = jest.fn().mockReturnValue(null)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser],
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeNull()
})
it('should get a revision if user has enough permissions', async () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(2)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser],
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toEqual(revision1)
})
it('should not get a revision if user has not enough permissions - plus user', async () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(45)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser],
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeNull()
})
it('should not get a revision if user has not enough permissions - pro user', async () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(500)
expect(
await createService().getRevision({
itemUuid: '1-2-3',
userRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser],
userUuid: '1-2-3',
revisionUuid: '3-4-5',
}),
).toBeNull()
})
it('should get revisions metadata for an item', async () => {
await createService().getRevisionsMetadata('1-2-3', '2-3-4')
expect(revisionRepository.findMetadataByItemId).toHaveBeenCalledWith('2-3-4')
})
it('should not get revisions metadata for an non existing item', async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
expect(await createService().getRevisionsMetadata('1-2-3', '2-3-4')).toEqual([])
expect(revisionRepository.findMetadataByItemId).not.toHaveBeenCalled()
})
it("should not get revisions metadata for another user's item", async () => {
expect(await createService().getRevisionsMetadata('3-4-5', '4-5-6')).toEqual([])
expect(revisionRepository.findMetadataByItemId).not.toHaveBeenCalled()
})
})

View File

@@ -1,89 +0,0 @@
import { inject, injectable } from 'inversify'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types'
import { Revision } from './Revision'
import { RevisionRepositoryInterface } from './RevisionRepositoryInterface'
import { RevisionServiceInterface } from './RevisionServiceInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { RevisionMetadata } from './RevisionMetadata'
@injectable()
export class RevisionService implements RevisionServiceInterface {
constructor(
@inject(TYPES.RevisionRepository) private revisionRepository: RevisionRepositoryInterface,
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async removeRevision(dto: { userUuid: string; itemUuid: string; revisionUuid: string }): Promise<boolean> {
const userItem = await this.itemRepository.findByUuid(dto.itemUuid)
if (userItem === null || userItem.userUuid !== dto.userUuid) {
return false
}
await this.revisionRepository.removeByUuid(dto.itemUuid, dto.revisionUuid)
return true
}
async getRevisionsMetadata(userUuid: string, itemUuid: string): Promise<RevisionMetadata[]> {
const userItem = await this.itemRepository.findByUuid(itemUuid)
if (userItem === null || userItem.userUuid !== userUuid) {
return []
}
return this.revisionRepository.findMetadataByItemId(itemUuid)
}
async getRevision(dto: {
userUuid: string
userRoles: string[]
itemUuid: string
revisionUuid: string
}): Promise<Revision | null> {
const userItem = await this.itemRepository.findByUuid(dto.itemUuid)
if (userItem === null || userItem.userUuid !== dto.userUuid) {
return null
}
const revision = await this.revisionRepository.findOneById(dto.itemUuid, dto.revisionUuid)
if (revision !== null && !this.userHasEnoughPermissionsToSeeRevision(dto.userRoles, revision.createdAt)) {
return null
}
return revision
}
calculateRequiredRoleBasedOnRevisionDate(createdAt: Date): string {
const revisionCreatedNDaysAgo = this.timer.dateWasNDaysAgo(createdAt)
if (revisionCreatedNDaysAgo > 30 && revisionCreatedNDaysAgo < 365) {
return RoleName.NAMES.PlusUser
}
if (revisionCreatedNDaysAgo > 365) {
return RoleName.NAMES.ProUser
}
return RoleName.NAMES.CoreUser
}
private userHasEnoughPermissionsToSeeRevision(userRoles: string[], revisionCreatedAt: Date): boolean {
const roleRequired = this.calculateRequiredRoleBasedOnRevisionDate(revisionCreatedAt)
switch (roleRequired) {
case RoleName.NAMES.PlusUser:
return (
userRoles.filter((userRole) => [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser].includes(userRole)).length >
0
)
case RoleName.NAMES.ProUser:
return userRoles.includes(RoleName.NAMES.ProUser)
default:
return true
}
}
}

View File

@@ -1,14 +0,0 @@
import { Revision } from './Revision'
import { RevisionMetadata } from './RevisionMetadata'
export interface RevisionServiceInterface {
getRevisionsMetadata(userUuid: string, itemUuid: string): Promise<RevisionMetadata[]>
getRevision(dto: {
userUuid: string
userRoles: string[]
itemUuid: string
revisionUuid: string
}): Promise<Revision | null>
removeRevision(dto: { userUuid: string; itemUuid: string; revisionUuid: string }): Promise<boolean>
calculateRequiredRoleBasedOnRevisionDate(createdAt: Date): string
}

View File

@@ -1,17 +1,14 @@
import { inject, injectable } from 'inversify'
import { IntegrityPayload } from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
import TYPES from '../../../Bootstrap/Types'
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { CheckIntegrityDTO } from './CheckIntegrityDTO'
import { CheckIntegrityResponse } from './CheckIntegrityResponse'
import { ExtendedIntegrityPayload } from '../../Item/ExtendedIntegrityPayload'
@injectable()
export class CheckIntegrity implements UseCaseInterface {
constructor(@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface) {}
constructor(private itemRepository: ItemRepositoryInterface) {}
async execute(dto: CheckIntegrityDTO): Promise<CheckIntegrityResponse> {
const serverItemIntegrityPayloads = await this.itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)

View File

@@ -1,13 +1,10 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { GetItemDTO } from './GetItemDTO'
import { GetItemResponse } from './GetItemResponse'
@injectable()
export class GetItem implements UseCaseInterface {
constructor(@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface) {}
constructor(private itemRepository: ItemRepositoryInterface) {}
async execute(dto: GetItemDTO): Promise<GetItemResponse> {
const item = await this.itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)

View File

@@ -1,5 +1,3 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { Item } from '../Item/Item'
import { ItemConflict } from '../Item/ItemConflict'
import { ItemServiceInterface } from '../Item/ItemServiceInterface'
@@ -7,9 +5,8 @@ import { SyncItemsDTO } from './SyncItemsDTO'
import { SyncItemsResponse } from './SyncItemsResponse'
import { UseCaseInterface } from './UseCaseInterface'
@injectable()
export class SyncItems implements UseCaseInterface {
constructor(@inject(TYPES.ItemService) private itemService: ItemServiceInterface) {}
constructor(private itemService: ItemServiceInterface) {}
async execute(dto: SyncItemsDTO): Promise<SyncItemsResponse> {
const getItemsResult = await this.itemService.getItems({

View File

@@ -1,22 +1,19 @@
import { KeyParamsData } from '@standardnotes/responses'
import { promises } from 'fs'
import * as uuid from 'uuid'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { dirname } from 'path'
import TYPES from '../../Bootstrap/Types'
import { Item } from '../../Domain/Item/Item'
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
@injectable()
export class FSItemBackupService implements ItemBackupServiceInterface {
constructor(
@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
private fileUploadPath: string,
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private logger: Logger,
) {}
async backup(_items: Item[], _authParams: KeyParamsData): Promise<string> {

View File

@@ -1,15 +1,10 @@
import { KeyParamsData } from '@standardnotes/responses'
import { AxiosInstance } from 'axios'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AuthHttpServiceInterface } from '../../Domain/Auth/AuthHttpServiceInterface'
@injectable()
export class AuthHttpService implements AuthHttpServiceInterface {
constructor(
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
) {}
constructor(private httpClient: AxiosInstance, private authServerUrl: string) {}
async getUserSetting(userUuid: string, settingName: string): Promise<{ uuid: string; value: string | null }> {
const response = await this.httpClient.request({

View File

@@ -1,18 +1,12 @@
import { inject, injectable } from 'inversify'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { Item } from '../../Domain/Item/Item'
import { ItemQuery } from '../../Domain/Item/ItemQuery'
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
import { ReadStream } from 'fs'
import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
import TYPES from '../../Bootstrap/Types'
@injectable()
export class MySQLItemRepository implements ItemRepositoryInterface {
constructor(
@inject(TYPES.ORMItemRepository)
private ormRepository: Repository<Item>,
) {}
constructor(private ormRepository: Repository<Item>) {}
async save(item: Item): Promise<Item> {
return this.ormRepository.save(item)

View File

@@ -1,83 +0,0 @@
/* istanbul ignore file */
import { UniqueEntityId } from '@standardnotes/domain-core'
import { inject, injectable } from 'inversify'
import { Repository } from 'typeorm'
import TYPES from '../../Bootstrap/Types'
import { Revision } from '../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
@injectable()
export class MySQLRevisionRepository implements RevisionRepositoryInterface {
constructor(
@inject(TYPES.ORMRevisionRepository)
private ormRepository: Repository<Revision>,
) {}
async save(revision: Revision): Promise<Revision> {
return this.ormRepository.save(revision)
}
async removeByUuid(itemUuid: string, revisionUuid: string): Promise<void> {
await this.ormRepository
.createQueryBuilder('revision')
.delete()
.from('revisions')
.where('uuid = :revisionUuid AND item_uuid = :itemUuid', { itemUuid, revisionUuid })
.execute()
}
async findByItemId(parameters: { itemUuid: string; afterDate?: Date }): Promise<Array<Revision>> {
const queryBuilder = this.ormRepository.createQueryBuilder('revision').where('revision.item_uuid = :item_uuid', {
item_uuid: parameters.itemUuid,
})
if (parameters.afterDate !== undefined) {
queryBuilder.andWhere('revision.creation_date >= :after_date', { after_date: parameters.afterDate })
}
return queryBuilder.orderBy('revision.created_at', 'DESC').getMany()
}
async findMetadataByItemId(itemUuid: string): 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,
})
const simplifiedRevisions = await queryBuilder.orderBy('created_at', 'DESC').getRawMany()
const metadata = []
for (const simplifiedRevision of simplifiedRevisions) {
const revisionMetadataOrError = RevisionMetadata.create(
{
contentType: simplifiedRevision.contentType,
updatedAt: simplifiedRevision.updatedAt,
createdAt: simplifiedRevision.createdAt,
},
new UniqueEntityId(simplifiedRevision.uuid),
)
if (revisionMetadataOrError.isFailed()) {
throw new Error(revisionMetadataOrError.getError())
}
metadata.push(revisionMetadataOrError.getValue())
}
return metadata
}
async findOneById(itemId: string, id: string): Promise<Revision | null> {
return this.ormRepository
.createQueryBuilder('revision')
.where('revision.uuid = :uuid AND revision.item_uuid = :item_uuid', { uuid: id, item_uuid: itemId })
.getOne()
}
}

View File

@@ -1,22 +1,19 @@
import * as uuid from 'uuid'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { KeyParamsData } from '@standardnotes/responses'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { Item } from '../../Domain/Item/Item'
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
@injectable()
export class S3ItemBackupService implements ItemBackupServiceInterface {
constructor(
@inject(TYPES.S3_BACKUP_BUCKET_NAME) private s3BackupBucketName: string,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.S3) private s3Client?: S3Client,
private s3BackupBucketName: string,
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private logger: Logger,
private s3Client?: S3Client,
) {}
async dump(item: Item): Promise<string> {

View File

@@ -1,5 +1,3 @@
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { ProjectorInterface } from './ProjectorInterface'
import { Item } from '../Domain/Item/Item'
@@ -7,9 +5,8 @@ import { ItemConflict } from '../Domain/Item/ItemConflict'
import { ItemConflictProjection } from './ItemConflictProjection'
import { ItemProjection } from './ItemProjection'
@injectable()
export class ItemConflictProjector implements ProjectorInterface<ItemConflict, ItemConflictProjection> {
constructor(@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
constructor(private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
async projectSimple(_itemConflict: ItemConflict): Promise<ItemConflictProjection> {
throw Error('not implemented')

View File

@@ -1,15 +1,12 @@
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { ProjectorInterface } from './ProjectorInterface'
import { Item } from '../Domain/Item/Item'
import { ItemProjection } from './ItemProjection'
import { ItemProjectionWithUser } from './ItemProjectionWithUser'
@injectable()
export class ItemProjector implements ProjectorInterface<Item, ItemProjection> {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
constructor(private timer: TimerInterface) {}
async projectSimple(_item: Item): Promise<ItemProjection> {
throw Error('not implemented')

View File

@@ -1,15 +0,0 @@
import { ContentType } from '@standardnotes/common'
export type RevisionProjection = {
uuid: string
item_uuid: string
content: string | null
content_type: ContentType | null
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
creation_date: string
required_role: string
created_at: string
updated_at: string
}

View File

@@ -1,74 +0,0 @@
import { ContentType } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { Item } from '../Domain/Item/Item'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
import { RevisionProjector } from './RevisionProjector'
describe('RevisionProjector', () => {
let revision: Revision
let timer: TimerInterface
let revisionService: RevisionServiceInterface
const createProjector = () => new RevisionProjector(timer, revisionService)
beforeEach(() => {
revision = new Revision()
revision.content = 'test'
revision.contentType = ContentType.Note
;(revision.uuid = '123'),
(revision.itemsKeyId = '123'),
(revision.item = Promise.resolve({ uuid: '1-2-3' } as Item))
timer = {} as jest.Mocked<TimerInterface>
timer.convertDateToISOString = jest.fn().mockReturnValue('2020-11-26T13:34:00.000Z')
timer.formatDate = jest.fn().mockReturnValue('2020-11-26')
revisionService = {} as jest.Mocked<RevisionServiceInterface>
revisionService.calculateRequiredRoleBasedOnRevisionDate = jest.fn().mockReturnValue(RoleName.NAMES.CoreUser)
revision.creationDate = new Date(1)
revision.createdAt = new Date(1)
revision.updatedAt = new Date(1)
})
it('should create a simple projection of a revision', async () => {
const projection = await createProjector().projectSimple(revision)
expect(projection).toMatchObject({
content_type: 'Note',
created_at: '2020-11-26T13:34:00.000Z',
updated_at: '2020-11-26T13:34:00.000Z',
required_role: 'CORE_USER',
uuid: '123',
})
})
it('should create a full projection of a revision', async () => {
const projection = await createProjector().projectFull(revision)
expect(projection).toMatchObject({
auth_hash: undefined,
content: 'test',
content_type: 'Note',
created_at: '2020-11-26T13:34:00.000Z',
creation_date: '2020-11-26',
enc_item_key: undefined,
required_role: 'CORE_USER',
item_uuid: '1-2-3',
items_key_id: '123',
updated_at: '2020-11-26T13:34:00.000Z',
uuid: '123',
})
})
it('should throw error on not implemetned custom projection', async () => {
let error = null
try {
await createProjector().projectCustom('test', revision)
} catch (e) {
error = e
}
expect((error as Error).message).toEqual('not implemented')
})
})

View File

@@ -1,47 +0,0 @@
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
import { ProjectorInterface } from './ProjectorInterface'
import { RevisionProjection } from './RevisionProjection'
import { SimpleRevisionProjection } from './SimpleRevisionProjection'
@injectable()
export class RevisionProjector implements ProjectorInterface<Revision, RevisionProjection> {
constructor(
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
) {}
async projectSimple(revision: Revision): Promise<SimpleRevisionProjection> {
return {
uuid: revision.uuid,
content_type: revision.contentType,
required_role: this.revisionService.calculateRequiredRoleBasedOnRevisionDate(revision.createdAt),
created_at: this.timer.convertDateToISOString(revision.createdAt),
updated_at: this.timer.convertDateToISOString(revision.updatedAt),
}
}
async projectFull(revision: Revision): Promise<RevisionProjection> {
return {
uuid: revision.uuid,
item_uuid: (await revision.item).uuid,
content: revision.content,
content_type: revision.contentType,
items_key_id: revision.itemsKeyId,
enc_item_key: revision.encItemKey,
auth_hash: revision.authHash,
creation_date: this.timer.formatDate(revision.creationDate, 'YYYY-MM-DD'),
required_role: this.revisionService.calculateRequiredRoleBasedOnRevisionDate(revision.createdAt),
created_at: this.timer.convertDateToISOString(revision.createdAt),
updated_at: this.timer.convertDateToISOString(revision.updatedAt),
}
}
async projectCustom(_projectionType: string, _revision: Revision, ..._args: any[]): Promise<RevisionProjection> {
throw new Error('not implemented')
}
}

View File

@@ -1,14 +1,12 @@
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { ProjectorInterface } from './ProjectorInterface'
import { Item } from '../Domain/Item/Item'
import { ProjectorInterface } from './ProjectorInterface'
import { SavedItemProjection } from './SavedItemProjection'
@injectable()
export class SavedItemProjector implements ProjectorInterface<Item, SavedItemProjection> {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
constructor(private timer: TimerInterface) {}
async projectSimple(_item: Item): Promise<SavedItemProjection> {
throw Error('not implemented')

View File

@@ -1,9 +0,0 @@
import { ContentType } from '@standardnotes/common'
export type SimpleRevisionProjection = {
uuid: string
content_type: ContentType | null
required_role: string
created_at: string
updated_at: string
}

View File

@@ -3598,6 +3598,7 @@ __metadata:
"@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"
@@ -3772,7 +3773,6 @@ __metadata:
"@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/jsonwebtoken": "npm:^9.0.1"
"@types/newrelic": "npm:^9.4.0"
@@ -3789,7 +3789,6 @@ __metadata:
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"
jsonwebtoken: "npm:^9.0.0"
mysql2: "npm:^3.0.1"