Compare commits

..

7 Commits

Author SHA1 Message Date
standardci b283bbaca9 chore(release): publish new version
- @standardnotes/api-gateway@1.39.0
2022-11-22 11:30:16 +00:00
Karol Sójko 92ba759b1c feat(api-gateway): add v2 revisions controller 2022-11-22 12:28:03 +01:00
standardci 0acc9d8d68 chore(release): publish new version
- @standardnotes/revisions-server@1.4.1
 - @standardnotes/syncing-server@1.18.0
2022-11-22 11:20:59 +00:00
Karol Sójko daa7a9ff61 fix(revisions): add more verbose error messages 2022-11-22 12:18:26 +01:00
Karol Sójko 455f35e0c1 feat(syncing-server): add dump projection for revisions 2022-11-22 12:18:26 +01:00
standardci 1fa655b56e chore(release): publish new version
- @standardnotes/revisions-server@1.4.0
2022-11-22 10:42:49 +00:00
Karol Sójko e553222b4b feat(revisions): add database 2022-11-22 11:40:30 +01:00
33 changed files with 192 additions and 31 deletions
+1
View File
@@ -10,6 +10,7 @@ WORKSPACE_SERVER_URL=http://workspace:3000
WEB_SOCKET_SERVER_URL=http://websockets:3000
PAYMENTS_SERVER_URL=http://payments:3000
FILES_SERVER_URL=http://files:3000
REVISIONS_SERVER_URL=http://revisions:3000
HTTP_CALL_TIMEOUT=60000
+6
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.39.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.9...@standardnotes/api-gateway@1.39.0) (2022-11-22)
### Features
* **api-gateway:** add v2 revisions controller ([92ba759](https://github.com/standardnotes/api-gateway/commit/92ba759b1c3719e773f989707ddd6d7a9ec57d1c))
## [1.38.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.38.8...@standardnotes/api-gateway@1.38.9) (2022-11-22)
**Note:** Version bump only for package @standardnotes/api-gateway
+1
View File
@@ -24,6 +24,7 @@ import '../src/Controller/v1/InvitesController'
import '../src/Controller/v2/PaymentsControllerV2'
import '../src/Controller/v2/ActionsControllerV2'
import '../src/Controller/v2/RevisionsControllerV2'
import helmet from 'helmet'
import * as cors from 'cors'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.38.9",
"version": "1.39.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -54,6 +54,7 @@ export class ContainerConfigLoader {
// env vars
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL'))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -7,6 +7,7 @@ const TYPES = {
AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'),
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
@@ -0,0 +1,17 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, httpGet } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v2/items/:item_id/revisions', TYPES.AuthMiddleware)
export class RevisionsControllerV2 extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpGet('/')
async getRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(request, response, `items/${request.params.item_id}/revisions`)
}
}
@@ -18,6 +18,7 @@ export class HttpService implements HttpServiceInterface {
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger,
@@ -32,6 +33,15 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.syncingServerJsUrl, request, response, endpoint, payload)
}
async callRevisionsServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
await this.callServer(this.revisionsServerUrl, request, response, endpoint, payload)
}
async callLegacySyncingServer(
request: Request,
response: Response,
@@ -13,6 +13,12 @@ export interface HttpServiceInterface {
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callRevisionsServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callSyncingServer(
request: Request,
response: Response,
+5 -5
View File
@@ -6,12 +6,12 @@ AUTH_JWT_SECRET=auth_jwt_secret
PORT=3000
DB_HOST=db
DB_REPLICA_HOST=db
DB_HOST=127.0.0.1
DB_REPLICA_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=std_notes_user
DB_PASSWORD=changeme123
DB_DATABASE=standard_notes_db
DB_USERNAME=revisions
DB_PASSWORD=revisionspassword
DB_DATABASE=revisions
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
DB_MIGRATIONS_PATH=dist/migrations/*.js
+12
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.4.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.4.0...@standardnotes/revisions-server@1.4.1) (2022-11-22)
### Bug Fixes
* **revisions:** add more verbose error messages ([daa7a9f](https://github.com/standardnotes/server/commit/daa7a9ff61d389e573960b443faff77e0abe01dc))
# [1.4.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.3.0...@standardnotes/revisions-server@1.4.0) (2022-11-22)
### Features
* **revisions:** add database ([e553222](https://github.com/standardnotes/server/commit/e553222b4b0f185bea5146d440834483b140339d))
# [1.3.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.2.2...@standardnotes/revisions-server@1.3.0) (2022-11-22)
### Features
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class init1669113322388 implements MigrationInterface {
name = 'init1669113322388'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
await queryRunner.query('DROP INDEX `user_uuid` ON `revisions`')
await queryRunner.query('DROP INDEX `item_uuid` ON `revisions`')
await queryRunner.query('DROP TABLE `revisions`')
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.3.0",
"version": "1.4.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -20,14 +20,14 @@ describe('RevisionsController', () => {
})
it('should get revisions list', async () => {
const response = await createController().getRevisions({ itemUuid: '1-2-3' })
const response = await createController().getRevisions({ itemUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(200)
})
it('should indicate failure to get revisions list', async () => {
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const response = await createController().getRevisions({ itemUuid: '1-2-3' })
const response = await createController().getRevisions({ itemUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(400)
})
@@ -8,7 +8,10 @@ export class RevisionsController {
constructor(private getRevisionsMetadata: GetRevisionsMetada, private logger: Logger) {}
async getRevisions(params: GetRevisionsMetadataRequestParams): Promise<HttpResponse> {
const revisionMetadataOrError = await this.getRevisionsMetadata.execute({ itemUuid: params.itemUuid })
const revisionMetadataOrError = await this.getRevisionsMetadata.execute({
itemUuid: params.itemUuid,
userUuid: params.userUuid,
})
if (revisionMetadataOrError.isFailed()) {
this.logger.warn(revisionMetadataOrError.getError())
@@ -4,6 +4,7 @@ import { ContentType } from './ContentType'
export interface RevisionProps {
itemUuid: Uuid
userUuid: Uuid
content: string | null
contentType: ContentType
itemsKeyId: string | null
@@ -4,6 +4,6 @@ import { Revision } from './Revision'
import { RevisionMetadata } from './RevisionMetadata'
export interface RevisionRepositoryInterface {
findMetadataByItemId(itemUuid: Uuid): Promise<Array<RevisionMetadata>>
findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
save(revision: Revision): Promise<Revision>
}
@@ -13,14 +13,29 @@ describe('GetRevisionsMetada', () => {
})
it('should return revisions metadata for a given item', async () => {
const result = await createUseCase().execute({ itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d' })
const result = await createUseCase().execute({
itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().length).toEqual(1)
})
it('should not return revisions metadata for a an invalid item uuid', async () => {
const result = await createUseCase().execute({ itemUuid: '1-2-3' })
const result = await createUseCase().execute({
itemUuid: '1-2-3',
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
})
expect(result.isFailed()).toBeTruthy()
})
it('should not return revisions metadata for a an invalid user uuid', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
})
expect(result.isFailed()).toBeTruthy()
})
@@ -14,7 +14,15 @@ export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]>
return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${itemUuidOrError.getError()}`)
}
const revisionsMetdata = await this.revisionRepository.findMetadataByItemId(itemUuidOrError.getValue())
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${userUuidOrError.getError()}`)
}
const revisionsMetdata = await this.revisionRepository.findMetadataByItemId(
itemUuidOrError.getValue(),
userUuidOrError.getValue(),
)
return Result.ok<RevisionMetadata[]>(revisionsMetdata)
}
@@ -1,3 +1,4 @@
export interface GetRevisionsMetadaDTO {
itemUuid: string
userUuid: string
}
@@ -1,3 +1,4 @@
export interface GetRevisionsMetadataRequestParams {
itemUuid: string
userUuid: string
}
@@ -1,4 +1,4 @@
import { Request } from 'express'
import { Request, Response } from 'express'
import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils'
import { inject } from 'inversify'
@@ -12,9 +12,10 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
}
@httpGet('/')
public async getRevisions(req: Request): Promise<results.JsonResult> {
public async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
const result = await this.revisionsController.getRevisions({
itemUuid: req.params.itemUuid,
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
@@ -21,16 +21,15 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
return revision
}
async findMetadataByItemId(itemUuid: Uuid): Promise<Array<RevisionMetadata>> {
async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>> {
const queryBuilder = this.ormRepository
.createQueryBuilder()
.select('uuid', 'uuid')
.addSelect('content_type', 'contentType')
.addSelect('created_at', 'createdAt')
.addSelect('updated_at', 'updatedAt')
.where('item_uuid = :item_uuid', {
item_uuid: itemUuid,
})
.where('item_uuid = :itemUuid', { itemUuid })
.andWhere('user_uuid = :userUuid', { userUuid })
.orderBy('created_at', 'DESC')
const simplifiedRevisions = await queryBuilder.getMany()
@@ -9,8 +9,16 @@ export class TypeORMRevision {
name: 'item_uuid',
length: 36,
})
@Index('item_uuid')
declare itemUuid: string
@Column({
name: 'user_uuid',
length: 36,
})
@Index('user_uuid')
declare userUuid: string
@Column({
type: 'mediumtext',
nullable: true,
@@ -53,7 +61,7 @@ export class TypeORMRevision {
type: 'date',
nullable: true,
})
@Index('index_revisions_on_creation_date')
@Index('creation_date')
declare creationDate: Date
@Column({
@@ -62,7 +70,7 @@ export class TypeORMRevision {
precision: 6,
nullable: true,
})
@Index('index_revisions_on_created_at')
@Index('created_at')
declare createdAt: Date
@Column({
@@ -5,22 +5,29 @@ import { Revision } from '../Domain/Revision/Revision'
export class RevisionItemStringMapper implements MapperInterface<Revision, string> {
toDomain(projection: string): Revision {
const item = JSON.parse(projection)
const item = JSON.parse(projection).item
const contentTypeOrError = ContentType.create(item.content_type)
if (contentTypeOrError.isFailed()) {
throw new Error(`Could not map item string to revision: ${contentTypeOrError.getError()}`)
throw new Error(`Could not map item string to revision [content type]: ${contentTypeOrError.getError()}`)
}
const contentType = contentTypeOrError.getValue()
const itemUuidOrError = Uuid.create(item.uuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not map item string to revision: ${itemUuidOrError.getError()}`)
throw new Error(`Could not map item string to revision [item uuid]: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
const userUuidOrError = Uuid.create(item.user_uuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Could not map item string to revision [user uuid]: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const revisionOrError = Revision.create({
itemUuid,
userUuid,
authHash: item.auth_hash,
content: item.content,
contentType,
@@ -31,7 +38,7 @@ export class RevisionItemStringMapper implements MapperInterface<Revision, strin
})
if (revisionOrError.isFailed()) {
throw new Error(`Could not map item string to revision: ${revisionOrError.getError()}`)
throw new Error(`Could not map item string to revision [revision]: ${revisionOrError.getError()}`)
}
return revisionOrError.getValue()
@@ -23,6 +23,12 @@ export class RevisionPersistenceMapper implements MapperInterface<Revision, Type
}
const itemUuid = itemUuidOrError.getValue()
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const revisionOrError = Revision.create(
{
authHash: projection.authHash,
@@ -32,6 +38,7 @@ export class RevisionPersistenceMapper implements MapperInterface<Revision, Type
encItemKey: projection.encItemKey,
itemsKeyId: projection.itemsKeyId,
itemUuid,
userUuid,
timestamps,
},
new UniqueEntityId(projection.uuid),
@@ -55,6 +62,7 @@ export class RevisionPersistenceMapper implements MapperInterface<Revision, Type
typeormRevision.encItemKey = domain.props.encItemKey
typeormRevision.itemUuid = domain.props.itemUuid.value
typeormRevision.itemsKeyId = domain.props.itemsKeyId
typeormRevision.userUuid = domain.props.userUuid.value
typeormRevision.uuid = domain.id.toString()
return typeormRevision
+6
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.18.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.17.0...@standardnotes/syncing-server@1.18.0) (2022-11-22)
### Features
* **syncing-server:** add dump projection for revisions ([455f35e](https://github.com/standardnotes/syncing-server-js/commit/455f35e0c1ac811720b67592a9017a3470a7740c))
# [1.17.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.16.1...@standardnotes/syncing-server@1.17.0) (2022-11-22)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.17.0",
"version": "1.18.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -22,7 +22,7 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
async dump(item: Item): Promise<string> {
const contents = JSON.stringify({
item: await this.itemProjector.projectFull(item),
item: await this.itemProjector.projectCustom('dump', item),
})
const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
@@ -31,7 +31,7 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
Bucket: this.s3BackupBucketName,
Key: uuid.v4(),
Body: JSON.stringify({
item: await this.itemProjector.projectFull(item),
item: await this.itemProjector.projectCustom('dump', item),
}),
})
.promise()
@@ -0,0 +1,5 @@
import { ItemProjection } from './ItemProjection'
export type ItemProjectionWithUser = ItemProjection & {
user_uuid: string
}
@@ -27,6 +27,7 @@ describe('ItemProjector', () => {
item.createdAtTimestamp = 123
item.updatedAtTimestamp = 123
item.updatedWithSession = '7-6-5'
item.userUuid = 'u1-2-3'
})
it('should create a full projection of an item', async () => {
@@ -45,6 +46,23 @@ describe('ItemProjector', () => {
})
})
it('should create a custom projection of an item', async () => {
expect(await createProjector().projectCustom('dump', item)).toMatchObject({
uuid: '1-2-3',
items_key_id: '2-3-4',
duplicate_of: null,
enc_item_key: '3-4-5',
content: 'test',
content_type: 'Note',
auth_hash: 'asd',
deleted: false,
created_at: '2021-04-15T08:00:00.123456Z',
updated_at: '2021-04-15T08:00:00.123456Z',
updated_with_session: '7-6-5',
user_uuid: 'u1-2-3',
})
})
it('should throw error on custom projection', async () => {
let error = null
try {
@@ -5,6 +5,7 @@ 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> {
@@ -14,8 +15,13 @@ export class ItemProjector implements ProjectorInterface<Item, ItemProjection> {
throw Error('not implemented')
}
async projectCustom(_projectionType: string, _item: Item): Promise<ItemProjection> {
throw Error('not implemented')
async projectCustom(_projectionType: string, item: Item): Promise<ItemProjectionWithUser> {
const fullProjection = await this.projectFull(item)
return {
...fullProjection,
user_uuid: item.userUuid,
}
}
async projectFull(item: Item): Promise<ItemProjection> {