mirror of
https://github.com/standardnotes/server
synced 2026-01-28 11:02:23 -05:00
Compare commits
14 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80df28a0c4 | ||
|
|
1c6c6a9296 | ||
|
|
7bb698e442 | ||
|
|
784728cd54 | ||
|
|
4b883b68de | ||
|
|
dec2cc2aaf | ||
|
|
b4e8971ad2 | ||
|
|
84e436265e | ||
|
|
ac8a69f8d4 | ||
|
|
b912e050ea | ||
|
|
284561d093 | ||
|
|
efc355982c | ||
|
|
8907879a19 | ||
|
|
86f6057207 |
116
.github/workflows/common-server-application.yml
vendored
116
.github/workflows/common-server-application.yml
vendored
@@ -130,77 +130,77 @@ jobs:
|
||||
- name: Test
|
||||
run: yarn test ${{ inputs.package_path }}
|
||||
|
||||
# e2e:
|
||||
# runs-on: ubuntu-latest
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# needs: build
|
||||
needs: build
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# - name: Cache build
|
||||
# id: cache-build
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: |
|
||||
# packages/**/dist
|
||||
# ${{ needs.build.outputs.temp_dir }}
|
||||
# key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
|
||||
- name: Cache build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
packages/**/dist
|
||||
${{ needs.build.outputs.temp_dir }}
|
||||
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
|
||||
|
||||
# - name: Set up Node
|
||||
# uses: actions/setup-node@v3
|
||||
# with:
|
||||
# registry-url: 'https://registry.npmjs.org'
|
||||
# node-version-file: '.nvmrc'
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# - name: Build
|
||||
# if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
# run: yarn build ${{ inputs.package_path }}
|
||||
- name: Build
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
run: yarn build ${{ inputs.package_path }}
|
||||
|
||||
# - name: Bundle
|
||||
# if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
# run: yarn workspace ${{ inputs.workspace_name }} bundle --no-compress --output-directory ${{ needs.build.outputs.temp_dir }}
|
||||
- name: Bundle
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
run: yarn workspace ${{ inputs.workspace_name }} bundle --no-compress --output-directory ${{ needs.build.outputs.temp_dir }}
|
||||
|
||||
# - name: Login to Docker Hub
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@master
|
||||
# with:
|
||||
# platforms: all
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# id: buildx
|
||||
# uses: docker/setup-buildx-action@master
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
# - name: Publish Docker image for E2E testing
|
||||
# uses: docker/build-push-action@v3
|
||||
# with:
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# context: ${{ needs.build.outputs.temp_dir }}
|
||||
# file: ${{ needs.build.outputs.temp_dir }}/${{ inputs.package_path }}/Dockerfile
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
# push: true
|
||||
# tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
||||
- name: Publish Docker image for E2E testing
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: ${{ needs.build.outputs.temp_dir }}
|
||||
file: ${{ needs.build.outputs.temp_dir }}/${{ inputs.package_path }}/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
||||
|
||||
# - name: Run E2E test suite
|
||||
# uses: convictional/trigger-workflow-and-wait@v1.6.3
|
||||
# with:
|
||||
# owner: standardnotes
|
||||
# repo: e2e
|
||||
# github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
# workflow_file_name: testing-with-stable-client.yml
|
||||
# wait_interval: 30
|
||||
# client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
||||
# propagate_failure: true
|
||||
# trigger_workflow: true
|
||||
# wait_workflow: true
|
||||
- name: Run E2E test suite
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.3
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
workflow_file_name: testing-with-stable-client.yml
|
||||
wait_interval: 30
|
||||
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
||||
propagate_failure: true
|
||||
trigger_workflow: true
|
||||
wait_workflow: true
|
||||
|
||||
publish:
|
||||
needs: [ build, test, lint ] #, e2e ]
|
||||
needs: [ build, test, lint, e2e ]
|
||||
|
||||
name: Publish Docker Image
|
||||
uses: standardnotes/server/.github/workflows/common-docker-image.yml@main
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.11.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.13...@standardnotes/analytics@2.11.14) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.12...@standardnotes/analytics@2.11.13) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.11.13",
|
||||
"version": "2.11.14",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.4...@standardnotes/api-gateway@1.39.5) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.39.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.3...@standardnotes/api-gateway@1.39.4) (2022-11-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** make revisions and workspace server urls optional ([8907879](https://github.com/standardnotes/api-gateway/commit/8907879a194d2d8328fbd3ca8ec9d0b608c2da50))
|
||||
|
||||
## [1.39.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.2...@standardnotes/api-gateway@1.39.3) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.39.3",
|
||||
"version": "1.39.5",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -54,11 +54,11 @@ 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.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
|
||||
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'))
|
||||
container.bind(TYPES.WORKSPACE_SERVER_URL).toConstantValue(env.get('WORKSPACE_SERVER_URL'))
|
||||
container.bind(TYPES.WORKSPACE_SERVER_URL).toConstantValue(env.get('WORKSPACE_SERVER_URL', true))
|
||||
container.bind(TYPES.WEB_SOCKET_SERVER_URL).toConstantValue(env.get('WEB_SOCKET_SERVER_URL', true))
|
||||
container
|
||||
.bind(TYPES.HTTP_CALL_TIMEOUT)
|
||||
|
||||
@@ -39,6 +39,11 @@ export class HttpService implements HttpServiceInterface {
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
if (!this.revisionsServerUrl) {
|
||||
response.status(400).send({ message: 'Revisions Server not configured' })
|
||||
|
||||
return
|
||||
}
|
||||
await this.callServer(this.revisionsServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
@@ -66,6 +71,12 @@ export class HttpService implements HttpServiceInterface {
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
if (!this.workspaceServerUrl) {
|
||||
response.status(400).send({ message: 'Workspace Server not configured' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.callServer(this.workspaceServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.60.14](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.13...@standardnotes/auth-server@1.60.14) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.60.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.12...@standardnotes/auth-server@1.60.13) (2022-11-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.60.13",
|
||||
"version": "1.60.14",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.35](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.34...@standardnotes/domain-events-infra@1.9.35) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.34](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.33...@standardnotes/domain-events-infra@1.9.34) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.9.34",
|
||||
"version": "1.9.35",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
# [2.91.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.90.2...@standardnotes/domain-events@2.91.0) (2022-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add copying revisions on duplicated items ([7bb698e](https://github.com/standardnotes/server/commit/7bb698e44222ef128d9642d625e96b7d26ee4dbf))
|
||||
|
||||
## [2.90.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.90.1...@standardnotes/domain-events@2.90.2) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.90.2",
|
||||
"version": "2.91.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { RevisionsCopyRequestedEventPayload } from './RevisionsCopyRequestedEventPayload'
|
||||
|
||||
export interface RevisionsCopyRequestedEvent extends DomainEventInterface {
|
||||
type: 'REVISIONS_COPY_REQUESTED'
|
||||
payload: RevisionsCopyRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface RevisionsCopyRequestedEventPayload {
|
||||
newItemUuid: string
|
||||
originalItemUuid: string
|
||||
}
|
||||
@@ -74,6 +74,8 @@ export * from './Event/RefundRequestedEvent'
|
||||
export * from './Event/RefundRequestedEventPayload'
|
||||
export * from './Event/RefundProcessedEvent'
|
||||
export * from './Event/RefundProcessedEventPayload'
|
||||
export * from './Event/RevisionsCopyRequestedEvent'
|
||||
export * from './Event/RevisionsCopyRequestedEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.31](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.30...@standardnotes/event-store@1.6.31) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.30](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.29...@standardnotes/event-store@1.6.30) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.6.30",
|
||||
"version": "1.6.31",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.31](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.30...@standardnotes/files-server@1.8.31) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.30](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.29...@standardnotes/files-server@1.8.30) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.8.30",
|
||||
"version": "1.8.31",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.7.1...@standardnotes/revisions-server@1.8.0) (2022-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** binding for revisions copy request handler ([1c6c6a9](https://github.com/standardnotes/server/commit/1c6c6a9296d91c35699a15b2cb4182e26233eeb2))
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add copying revisions on duplicated items ([7bb698e](https://github.com/standardnotes/server/commit/7bb698e44222ef128d9642d625e96b7d26ee4dbf))
|
||||
|
||||
## [1.7.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.7.0...@standardnotes/revisions-server@1.7.1) (2022-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** remove unnecessary indexes ([4b883b6](https://github.com/standardnotes/server/commit/4b883b68def777b0c0682cc6a8af6fd968b18d9f))
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.6.0...@standardnotes/revisions-server@1.7.0) (2022-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add handling account deletion requests ([b4e8971](https://github.com/standardnotes/server/commit/b4e8971ad27fd198239f6eb976b8286575373ed6))
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.5.0...@standardnotes/revisions-server@1.6.0) (2022-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add deleting revisions ([ac8a69f](https://github.com/standardnotes/server/commit/ac8a69f8d428e3cf8e4df5269db3cb31d9b118d5))
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.4.8...@standardnotes/revisions-server@1.5.0) (2022-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add fetching single revision ([284561d](https://github.com/standardnotes/server/commit/284561d093eaa6d73af888142583ec705ba18f79))
|
||||
|
||||
## [1.4.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.4.7...@standardnotes/revisions-server@1.4.8) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class removeDateIndexes1669636497932 implements MigrationInterface {
|
||||
name = 'removeDateIndexes1669636497932'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
|
||||
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE INDEX `creation_date` ON `revisions` (`creation_date`)')
|
||||
await queryRunner.query('CREATE INDEX `created_at` ON `revisions` (`created_at`)')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.4.8",
|
||||
"version": "1.8.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -36,6 +36,11 @@ import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler
|
||||
import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface'
|
||||
import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
|
||||
import { FSDumpRepository } from '../Infra/FS/FSDumpRepository'
|
||||
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
|
||||
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler'
|
||||
import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -152,11 +157,27 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<GetRevisionsMetada>(TYPES.GetRevisionsMetada)
|
||||
.toConstantValue(new GetRevisionsMetada(container.get(TYPES.RevisionRepository)))
|
||||
container
|
||||
.bind<GetRevision>(TYPES.GetRevision)
|
||||
.toConstantValue(new GetRevision(container.get(TYPES.RevisionRepository)))
|
||||
container
|
||||
.bind<DeleteRevision>(TYPES.DeleteRevision)
|
||||
.toConstantValue(new DeleteRevision(container.get(TYPES.RevisionRepository)))
|
||||
container
|
||||
.bind<CopyRevisions>(TYPES.CopyRevisions)
|
||||
.toConstantValue(new CopyRevisions(container.get(TYPES.RevisionRepository)))
|
||||
|
||||
// Controller
|
||||
container
|
||||
.bind<RevisionsController>(TYPES.RevisionsController)
|
||||
.toConstantValue(new RevisionsController(container.get(TYPES.GetRevisionsMetada), container.get(TYPES.Logger)))
|
||||
.toConstantValue(
|
||||
new RevisionsController(
|
||||
container.get(TYPES.GetRevisionsMetada),
|
||||
container.get(TYPES.GetRevision),
|
||||
container.get(TYPES.DeleteRevision),
|
||||
container.get(TYPES.Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
@@ -164,6 +185,16 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new ItemDumpedEventHandler(container.get(TYPES.DumpRepository), container.get(TYPES.RevisionRepository)),
|
||||
)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(container.get(TYPES.RevisionRepository), container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<RevisionsCopyRequestedEventHandler>(TYPES.RevisionsCopyRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new RevisionsCopyRequestedEventHandler(container.get(TYPES.CopyRevisions), container.get(TYPES.Logger)),
|
||||
)
|
||||
|
||||
// Services
|
||||
container
|
||||
@@ -177,6 +208,8 @@ export class ContainerConfigLoader {
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ITEM_DUMPED', container.get(TYPES.ItemDumpedEventHandler)],
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||
['REVISIONS_COPY_REQUESTED', container.get(TYPES.RevisionsCopyRequestedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
|
||||
@@ -25,10 +25,15 @@ const TYPES = {
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
// use cases
|
||||
GetRevisionsMetada: Symbol.for('GetRevisionsMetada'),
|
||||
GetRevision: Symbol.for('GetRevision'),
|
||||
DeleteRevision: Symbol.for('DeleteRevision'),
|
||||
CopyRevisions: Symbol.for('CopyRevisions'),
|
||||
// Controller
|
||||
RevisionsController: Symbol.for('RevisionsController'),
|
||||
// Handlers
|
||||
ItemDumpedEventHandler: Symbol.for('ItemDumpedEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
RevisionsCopyRequestedEventHandler: Symbol.for('RevisionsCopyRequestedEventHandler'),
|
||||
// Services
|
||||
CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
|
||||
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
|
||||
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||
|
||||
import { RevisionsController } from './RevisionsController'
|
||||
|
||||
describe('RevisionsController', () => {
|
||||
let getRevisionsMetadata: GetRevisionsMetada
|
||||
let getRevision: GetRevision
|
||||
let deleteRevision: DeleteRevision
|
||||
let logger: Logger
|
||||
|
||||
const createController = () => new RevisionsController(getRevisionsMetadata, logger)
|
||||
const createController = () => new RevisionsController(getRevisionsMetadata, getRevision, deleteRevision, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
getRevisionsMetadata = {} as jest.Mocked<GetRevisionsMetada>
|
||||
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
getRevision = {} as jest.Mocked<GetRevision>
|
||||
getRevision.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
deleteRevision = {} as jest.Mocked<DeleteRevision>
|
||||
deleteRevision.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
@@ -31,4 +41,30 @@ describe('RevisionsController', () => {
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should get revision', async () => {
|
||||
const response = await createController().getRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should indicate failure to get revision', async () => {
|
||||
getRevision.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
const response = await createController().getRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
|
||||
it('should delete revision', async () => {
|
||||
const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('should indicate failure to delete revision', async () => {
|
||||
deleteRevision.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,9 +3,18 @@ import { HttpResponse, HttpStatusCode } from '@standardnotes/api'
|
||||
|
||||
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||
import { GetRevisionsMetadataRequestParams } from '../Infra/Http/GetRevisionsMetadataRequestParams'
|
||||
import { GetRevisionRequestParams } from '../Infra/Http/GetRevisionRequestParams'
|
||||
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
|
||||
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
|
||||
import { DeleteRevisionRequestParams } from '../Infra/Http/DeleteRevisionRequestParams'
|
||||
|
||||
export class RevisionsController {
|
||||
constructor(private getRevisionsMetadata: GetRevisionsMetada, private logger: Logger) {}
|
||||
constructor(
|
||||
private getRevisionsMetadata: GetRevisionsMetada,
|
||||
private doGetRevision: GetRevision,
|
||||
private doDeleteRevision: DeleteRevision,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async getRevisions(params: GetRevisionsMetadataRequestParams): Promise<HttpResponse> {
|
||||
const revisionMetadataOrError = await this.getRevisionsMetadata.execute({
|
||||
@@ -31,4 +40,54 @@ export class RevisionsController {
|
||||
data: { revisions: revisionMetadataOrError.getValue() },
|
||||
}
|
||||
}
|
||||
|
||||
async getRevision(params: GetRevisionRequestParams): Promise<HttpResponse> {
|
||||
const revisionOrError = await this.doGetRevision.execute({
|
||||
revisionUuid: params.revisionUuid,
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (revisionOrError.isFailed()) {
|
||||
this.logger.warn(revisionOrError.getError())
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not retrieve revision.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { revision: revisionOrError.getValue() },
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRevision(params: DeleteRevisionRequestParams): Promise<HttpResponse> {
|
||||
const revisionOrError = await this.doDeleteRevision.execute({
|
||||
revisionUuid: params.revisionUuid,
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (revisionOrError.isFailed()) {
|
||||
this.logger.warn(revisionOrError.getError())
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not delete revision.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { message: revisionOrError.getValue() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
||||
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
let logger: Logger
|
||||
let event: AccountDeletionRequestedEvent
|
||||
|
||||
const createHandler = () => new AccountDeletionRequestedEventHandler(revisionRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.removeByUserUuid = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '2-3-4',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
}
|
||||
})
|
||||
|
||||
it('should remove all revisions for a user', async () => {
|
||||
event.payload.userUuid = '84c0f8e8-544a-4c7e-9adf-26209303bc1d'
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionRepository.removeByUserUuid).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not remove all revisions for an invalid user uuid', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface, private logger: Logger) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(`Failed account cleanup: ${userUuidOrError.getError()}`)
|
||||
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
await this.revisionRepository.removeByUserUuid(userUuid)
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { RevisionsCopyRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { CopyRevisions } from '../UseCase/CopyRevisions/CopyRevisions'
|
||||
import { RevisionsCopyRequestedEventHandler } from './RevisionsCopyRequestedEventHandler'
|
||||
|
||||
describe('RevisionsCopyRequestedEventHandler', () => {
|
||||
let copyRevisions: CopyRevisions
|
||||
let logger: Logger
|
||||
let event: RevisionsCopyRequestedEvent
|
||||
|
||||
const createHandler = () => new RevisionsCopyRequestedEventHandler(copyRevisions, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
copyRevisions = {} as jest.Mocked<CopyRevisions>
|
||||
copyRevisions.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<RevisionsCopyRequestedEvent>
|
||||
event.payload = {
|
||||
newItemUuid: '1-2-3',
|
||||
originalItemUuid: '2-3-4',
|
||||
}
|
||||
})
|
||||
|
||||
it('should copy revisions', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(copyRevisions.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should indicate if copying revisions fail', async () => {
|
||||
copyRevisions.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(logger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DomainEventHandlerInterface, RevisionsCopyRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { CopyRevisions } from '../UseCase/CopyRevisions/CopyRevisions'
|
||||
|
||||
export class RevisionsCopyRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private copyRevisions: CopyRevisions, private logger: Logger) {}
|
||||
|
||||
async handle(event: RevisionsCopyRequestedEvent): Promise<void> {
|
||||
const result = await this.copyRevisions.execute({
|
||||
newItemUuid: event.payload.newItemUuid,
|
||||
originalItemUuid: event.payload.originalItemUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not copy revisions: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/revisions/src/Domain/Revision/ContentType.spec.ts
Normal file
16
packages/revisions/src/Domain/Revision/ContentType.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ContentType } from './ContentType'
|
||||
|
||||
describe('ContentType', () => {
|
||||
it('should create a value obejct', () => {
|
||||
const valueOrError = ContentType.create('Note')
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should fail to create a value obejct', () => {
|
||||
const valueOrError = ContentType.create('test')
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
22
packages/revisions/src/Domain/Revision/Revision.spec.ts
Normal file
22
packages/revisions/src/Domain/Revision/Revision.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType } from './ContentType'
|
||||
import { Revision } from './Revision'
|
||||
|
||||
describe('Revision', () => {
|
||||
it('should create an entity', () => {
|
||||
const entityOrError = Revision.create({
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
timestamps: Timestamps.create(new Date(1), new Date(2)).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,10 @@ import { Revision } from './Revision'
|
||||
import { RevisionMetadata } from './RevisionMetadata'
|
||||
|
||||
export interface RevisionRepositoryInterface {
|
||||
removeByUserUuid(userUuid: Uuid): Promise<void>
|
||||
removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
|
||||
findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null>
|
||||
findByItemUuid(itemUuid: Uuid): Promise<Array<Revision>>
|
||||
findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
|
||||
save(revision: Revision): Promise<Revision>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { Revision } from '../../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { CopyRevisions } from './CopyRevisions'
|
||||
|
||||
describe('CopyRevisions', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
|
||||
const createUseCase = () => new CopyRevisions(revisionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.findByItemUuid = jest.fn().mockReturnValue([{} as jest.Mocked<Revision>])
|
||||
revisionRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not copy revisions to new item if revision creation fails', async () => {
|
||||
const revisionMock = jest.spyOn(Revision, 'create')
|
||||
revisionMock.mockImplementation(() => Result.fail('Oops'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
revisionMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should copy revisions to new item', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(revisionRepository.save).toHaveBeenCalled()
|
||||
expect(result.getValue()).toEqual('Revisions copied')
|
||||
})
|
||||
|
||||
it('should not copy revisions for an invalid item uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
originalItemUuid: '1-2-3',
|
||||
newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not delete revision for a an invalid new item uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
newItemUuid: '1-2-3',
|
||||
originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Revision } from '../../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
|
||||
import { CopyRevisionsDTO } from './CopyRevisionsDTO'
|
||||
|
||||
export class CopyRevisions implements UseCaseInterface<string> {
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: CopyRevisionsDTO): Promise<Result<string>> {
|
||||
const orignalItemUuidOrError = Uuid.create(dto.originalItemUuid)
|
||||
if (orignalItemUuidOrError.isFailed()) {
|
||||
return Result.fail<string>(`Could not copy revisions: ${orignalItemUuidOrError.getError()}`)
|
||||
}
|
||||
const originalItemUuid = orignalItemUuidOrError.getValue()
|
||||
|
||||
const newItemUuidOrError = Uuid.create(dto.newItemUuid)
|
||||
if (newItemUuidOrError.isFailed()) {
|
||||
return Result.fail<string>(`Could not copy revisions: ${newItemUuidOrError.getError()}`)
|
||||
}
|
||||
const newItemUuid = newItemUuidOrError.getValue()
|
||||
|
||||
const revisions = await this.revisionRepository.findByItemUuid(originalItemUuid)
|
||||
|
||||
for (const existingRevision of revisions) {
|
||||
const revisionCopyOrError = Revision.create({
|
||||
...existingRevision.props,
|
||||
itemUuid: newItemUuid,
|
||||
})
|
||||
|
||||
if (revisionCopyOrError.isFailed()) {
|
||||
return Result.fail<string>(`Could not create revision copy: ${revisionCopyOrError.getError()}`)
|
||||
}
|
||||
|
||||
const revisionCopy = revisionCopyOrError.getValue()
|
||||
|
||||
await this.revisionRepository.save(revisionCopy)
|
||||
}
|
||||
|
||||
return Result.ok<string>('Revisions copied')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CopyRevisionsDTO {
|
||||
originalItemUuid: string
|
||||
newItemUuid: string
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { DeleteRevision } from './DeleteRevision'
|
||||
|
||||
describe('DeleteRevision', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
|
||||
const createUseCase = () => new DeleteRevision(revisionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.removeOneByUuid = jest.fn()
|
||||
})
|
||||
|
||||
it('should delete revision', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual('Revision removed')
|
||||
})
|
||||
|
||||
it('should not delete revision for an invalid item uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
revisionUuid: '1-2-3',
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not delete revision for a an invalid user uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { DeleteRevisionDTO } from './DeleteRevisionDTO'
|
||||
|
||||
export class DeleteRevision implements UseCaseInterface<string> {
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: DeleteRevisionDTO): Promise<Result<string>> {
|
||||
const revisionUuidOrError = Uuid.create(dto.revisionUuid)
|
||||
if (revisionUuidOrError.isFailed()) {
|
||||
return Result.fail<string>(`Could not delete revision: ${revisionUuidOrError.getError()}`)
|
||||
}
|
||||
const revisionUuid = revisionUuidOrError.getValue()
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail<string>(`Could not delete revision: ${userUuidOrError.getError()}`)
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
await this.revisionRepository.removeOneByUuid(revisionUuid, userUuid)
|
||||
|
||||
return Result.ok<string>('Revision removed')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DeleteRevisionDTO {
|
||||
userUuid: string
|
||||
revisionUuid: string
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Revision } from '../../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { GetRevision } from './GetRevision'
|
||||
|
||||
describe('GetRevision', () => {
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
|
||||
const createUseCase = () => new GetRevision(revisionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked<Revision>)
|
||||
})
|
||||
|
||||
it('should return revision for a given item', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not return revision for a given item if not found', async () => {
|
||||
revisionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not return revision for a an invalid item uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
revisionUuid: '1-2-3',
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not return revision for a an invalid user uuid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Revision } from '../../Revision/Revision'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { GetRevisionDTO } from './GetRevisionDTO'
|
||||
|
||||
export class GetRevision implements UseCaseInterface<Revision> {
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: GetRevisionDTO): Promise<Result<Revision>> {
|
||||
const revisionUuidOrError = Uuid.create(dto.revisionUuid)
|
||||
if (revisionUuidOrError.isFailed()) {
|
||||
return Result.fail<Revision>(`Could not get revision: ${revisionUuidOrError.getError()}`)
|
||||
}
|
||||
const revisionUuid = revisionUuidOrError.getValue()
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail<Revision>(`Could not get revision: ${userUuidOrError.getError()}`)
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const revision = await this.revisionRepository.findOneByUuid(revisionUuid, userUuid)
|
||||
|
||||
if (revision === null) {
|
||||
return Result.fail<Revision>(`Could not find revision with uuid: ${revisionUuid.value}`)
|
||||
}
|
||||
|
||||
return Result.ok<Revision>(revision)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GetRevisionDTO {
|
||||
userUuid: string
|
||||
revisionUuid: string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DeleteRevisionRequestParams {
|
||||
revisionUuid: string
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GetRevisionRequestParams {
|
||||
revisionUuid: string
|
||||
userUuid: string
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
|
||||
import { inject } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
@@ -20,4 +20,24 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
public async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.revisionsController.getRevision({
|
||||
revisionUuid: req.params.uuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpDelete('/:uuid')
|
||||
public async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.revisionsController.deleteRevision({
|
||||
revisionUuid: req.params.uuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Repository } from 'typeorm'
|
||||
import { Revision } from '../../Domain/Revision/Revision'
|
||||
|
||||
import { Revision } from '../../Domain/Revision/Revision'
|
||||
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
|
||||
import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
|
||||
import { TypeORMRevision } from '../TypeORM/TypeORMRevision'
|
||||
@@ -13,6 +13,52 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
|
||||
private revisionMapper: MapperInterface<Revision, TypeORMRevision>,
|
||||
) {}
|
||||
|
||||
async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
|
||||
const typeormRevisions = await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('item_uuid = :itemUuid', { itemUuid })
|
||||
.getMany()
|
||||
|
||||
const revisions = []
|
||||
for (const revision of typeormRevisions) {
|
||||
revisions.push(this.revisionMapper.toDomain(revision))
|
||||
}
|
||||
|
||||
return revisions
|
||||
}
|
||||
|
||||
async removeByUserUuid(userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('revisions')
|
||||
.where('user_uuid = :userUuid', { userUuid })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('revisions')
|
||||
.where('uuid = :revisionUuid AND user_uuid = :userUuid', { userUuid, revisionUuid })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null> {
|
||||
const typeormRevision = await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('uuid = :revisionUuid', { revisionUuid })
|
||||
.andWhere('user_uuid = :userUuid', { userUuid })
|
||||
.getOne()
|
||||
|
||||
if (typeormRevision === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.revisionMapper.toDomain(typeormRevision)
|
||||
}
|
||||
|
||||
async save(revision: Revision): Promise<Revision> {
|
||||
const typeormRevision = this.revisionMapper.toProjection(revision)
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ export class TypeORMRevision {
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
})
|
||||
@Index('creation_date')
|
||||
declare creationDate: Date
|
||||
|
||||
@Column({
|
||||
@@ -70,7 +69,6 @@ export class TypeORMRevision {
|
||||
precision: 6,
|
||||
nullable: true,
|
||||
})
|
||||
@Index('created_at')
|
||||
declare createdAt: Date
|
||||
|
||||
@Column({
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.32](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.31...@standardnotes/scheduler-server@1.13.32) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.31](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.30...@standardnotes/scheduler-server@1.13.31) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.13.31",
|
||||
"version": "1.13.32",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.19.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.18.12...@standardnotes/syncing-server@1.19.0) (2022-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add copying revisions on duplicated items ([7bb698e](https://github.com/standardnotes/syncing-server-js/commit/7bb698e44222ef128d9642d625e96b7d26ee4dbf))
|
||||
|
||||
## [1.18.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.18.11...@standardnotes/syncing-server@1.18.12) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.18.12",
|
||||
"version": "1.19.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
ItemsSyncedEvent,
|
||||
OneDriveBackupFailedEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -21,6 +22,27 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createRevisionsCopyRequestedEvent(
|
||||
userUuid: string,
|
||||
dto: {
|
||||
originalItemUuid: string
|
||||
newItemUuid: string
|
||||
},
|
||||
): RevisionsCopyRequestedEvent {
|
||||
return {
|
||||
type: 'REVISIONS_COPY_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent {
|
||||
return {
|
||||
type: 'ITEM_DUMPED',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
ItemsSyncedEvent,
|
||||
OneDriveBackupFailedEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
UserContentSizeRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
@@ -35,4 +36,8 @@ export interface DomainEventFactoryInterface {
|
||||
createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
|
||||
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
|
||||
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent
|
||||
createRevisionsCopyRequestedEvent(
|
||||
userUuid: string,
|
||||
dto: { originalItemUuid: string; newItemUuid: string },
|
||||
): RevisionsCopyRequestedEvent
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { DuplicateItemSyncedEvent } from '@standardnotes/domain-events'
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DuplicateItemSyncedEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler'
|
||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
@@ -14,8 +19,17 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let duplicateItem: Item
|
||||
let originalItem: Item
|
||||
let event: DuplicateItemSyncedEvent
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () => new DuplicateItemSyncedEventHandler(itemRepository, revisionService, logger)
|
||||
const createHandler = () =>
|
||||
new DuplicateItemSyncedEventHandler(
|
||||
itemRepository,
|
||||
revisionService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
originalItem = {
|
||||
@@ -45,6 +59,14 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
userUuid: '1-2-3',
|
||||
itemUuid: '2-3-4',
|
||||
}
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createRevisionsCopyRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<RevisionsCopyRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should copy revisions from original item to the duplicate item', async () => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { DomainEventHandlerInterface, DuplicateItemSyncedEvent } from '@standardnotes/domain-events'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
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'
|
||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||
|
||||
@@ -10,6 +15,8 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
|
||||
constructor(
|
||||
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
||||
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -35,6 +42,13 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
|
||||
|
||||
if (existingOriginalItem !== null) {
|
||||
await this.revisionService.copyRevisions(existingOriginalItem.uuid, item.uuid)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createRevisionsCopyRequestedEvent(event.payload.userUuid, {
|
||||
originalItemUuid: existingOriginalItem.uuid,
|
||||
newItemUuid: item.uuid,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.32](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.31...@standardnotes/websockets-server@1.4.32) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.31](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.30...@standardnotes/websockets-server@1.4.31) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.4.31",
|
||||
"version": "1.4.32",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.17.31](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.30...@standardnotes/workspace-server@1.17.31) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.30](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.29...@standardnotes/workspace-server@1.17.30) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.17.30",
|
||||
"version": "1.17.31",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user