From f08c780c0243d3bf9ead057a3f97fb1ea810be8f Mon Sep 17 00:00:00 2001 From: adrigzr Date: Wed, 15 Mar 2023 19:46:42 +0100 Subject: [PATCH 01/38] test: add `test-support` package for testing purposes --- nyc.config.js | 2 +- package.json | 1 + packages/cli/package.json | 1 + .../cli/src/commands/decode.command.test.ts | 2 +- .../cli/src/commands/encode.command.test.ts | 3 +- .../cli/src/commands/read.command.test.ts | 2 +- .../streams/array-transform.stream.test.ts | 2 +- .../json-stringify-transform.stream.test.ts | 2 +- .../src/streams/json-transform.stream.test.ts | 2 +- .../packet-decode-transform.stream.test.ts | 2 +- .../packet-encode-transform.stream.test.ts | 3 +- .../src/streams/pcap-reader.stream.test.ts | 2 +- .../src/streams/pcap-transform.stream.test.ts | 2 +- .../streams/string-transform.stream.test.ts | 2 +- packages/cli/src/test-support.ts | 17 ---- packages/test-support/.gitignore | 6 ++ packages/test-support/README.md | 3 + packages/test-support/package.json | 79 +++++++++++++++++++ packages/test-support/src/index.ts | 1 + .../src/utils/read-stream.util.test.ts | 47 +++++++++++ .../src/utils/read-stream.util.ts | 18 +++++ packages/test-support/tsconfig.build.json | 10 +++ tsconfig.json | 6 +- 23 files changed, 185 insertions(+), 30 deletions(-) create mode 100644 packages/test-support/.gitignore create mode 100644 packages/test-support/README.md create mode 100644 packages/test-support/package.json create mode 100644 packages/test-support/src/index.ts create mode 100644 packages/test-support/src/utils/read-stream.util.test.ts create mode 100644 packages/test-support/src/utils/read-stream.util.ts create mode 100644 packages/test-support/tsconfig.build.json diff --git a/nyc.config.js b/nyc.config.js index 3c1553d7..0117af5e 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -3,5 +3,5 @@ module.exports = { all: true, reporter: ['html', 'lcov', 'json', 'json-summary', 'text-summary'], 'report-dir': 'coverage', - exclude: ['**/*.test.ts', '**/test-support/**/*', '**/test-support.ts', '**/coverage/**', '**/lib/**', '**/types/**'], + exclude: ['**/*.test.ts', '**/coverage/**', '**/lib/**', '**/types/**'], }; diff --git a/package.json b/package.json index 2a18f13c..0b45e952 100755 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "nyc": "^15.1.0", "prettier": "2.8.4", "ts-node": "^10.9.1", + "tsconfig-paths": "^4.1.2", "typedoc": "^0.23.25", "typedoc-plugin-resolve-crossmodule-references": "^0.3.3", "typescript": "^4.9.5", diff --git a/packages/cli/package.json b/packages/cli/package.json index 13735699..de4beeb3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -93,6 +93,7 @@ "pcap": "^3.1.0" }, "devDependencies": { + "@agnoc/test-support": "^0.18.0-next.0", "@johanblumenberg/ts-mockito": "^1.0.35", "chai": "^4.3.7", "execa": "^5.1.1", diff --git a/packages/cli/src/commands/decode.command.test.ts b/packages/cli/src/commands/decode.command.test.ts index 8829cd4e..f4519a74 100644 --- a/packages/cli/src/commands/decode.command.test.ts +++ b/packages/cli/src/commands/decode.command.test.ts @@ -1,10 +1,10 @@ import { PassThrough } from 'stream'; +import { readStream } from '@agnoc/test-support'; import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { imock, when, anything, verify, instance, deepEqual } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import mockFS, { restore } from 'mock-fs'; -import { readStream } from '../test-support'; import { DecodeCommand } from './decode.command'; import type { Stdio } from '../interfaces/stdio'; import type { PacketMapper } from '@agnoc/transport-tcp'; diff --git a/packages/cli/src/commands/encode.command.test.ts b/packages/cli/src/commands/encode.command.test.ts index a39a5910..f4077d24 100644 --- a/packages/cli/src/commands/encode.command.test.ts +++ b/packages/cli/src/commands/encode.command.test.ts @@ -1,10 +1,11 @@ import { PassThrough } from 'stream'; +import { readStream } from '@agnoc/test-support'; import { ID } from '@agnoc/toolkit'; import { OPCode, Packet, PacketSequence, Payload } from '@agnoc/transport-tcp'; import { imock, when, anything, verify, instance, deepEqual } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import mockFS, { restore } from 'mock-fs'; -import { givenAJSONPacket, readStream } from '../test-support'; +import { givenAJSONPacket } from '../test-support'; import { EncodeCommand } from './encode.command'; import type { Stdio } from '../interfaces/stdio'; import type { JSONPacket } from '../streams/packet-encode-transform.stream'; diff --git a/packages/cli/src/commands/read.command.test.ts b/packages/cli/src/commands/read.command.test.ts index 3e085a79..4dfd208b 100644 --- a/packages/cli/src/commands/read.command.test.ts +++ b/packages/cli/src/commands/read.command.test.ts @@ -1,10 +1,10 @@ import path from 'path'; import { PassThrough } from 'stream'; +import { readStream } from '@agnoc/test-support'; import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { ReadCommand } from './read.command'; import type { Stdio } from '../interfaces/stdio'; import type { PacketMapper } from '@agnoc/transport-tcp'; diff --git a/packages/cli/src/streams/array-transform.stream.test.ts b/packages/cli/src/streams/array-transform.stream.test.ts index c9ae9ec7..0c517e39 100644 --- a/packages/cli/src/streams/array-transform.stream.test.ts +++ b/packages/cli/src/streams/array-transform.stream.test.ts @@ -1,5 +1,5 @@ +import { readStream } from '@agnoc/test-support'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { ArrayTransform } from './array-transform.stream'; describe('ArrayTransform', function () { diff --git a/packages/cli/src/streams/json-stringify-transform.stream.test.ts b/packages/cli/src/streams/json-stringify-transform.stream.test.ts index 4af5f39d..e29bbb06 100644 --- a/packages/cli/src/streams/json-stringify-transform.stream.test.ts +++ b/packages/cli/src/streams/json-stringify-transform.stream.test.ts @@ -1,5 +1,5 @@ +import { readStream } from '@agnoc/test-support'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { JSONStringifyTransform } from './json-stringify-transform.stream'; describe('JSONStringifystream', function () { diff --git a/packages/cli/src/streams/json-transform.stream.test.ts b/packages/cli/src/streams/json-transform.stream.test.ts index ddaa9757..9a029d7e 100644 --- a/packages/cli/src/streams/json-transform.stream.test.ts +++ b/packages/cli/src/streams/json-transform.stream.test.ts @@ -1,5 +1,5 @@ +import { readStream } from '@agnoc/test-support'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { JSONTransform } from './json-transform.stream'; describe('JSONTransform', function () { diff --git a/packages/cli/src/streams/packet-decode-transform.stream.test.ts b/packages/cli/src/streams/packet-decode-transform.stream.test.ts index ac8d27ca..0049b64c 100644 --- a/packages/cli/src/streams/packet-decode-transform.stream.test.ts +++ b/packages/cli/src/streams/packet-decode-transform.stream.test.ts @@ -1,9 +1,9 @@ +import { readStream } from '@agnoc/test-support'; import { DomainException } from '@agnoc/toolkit'; import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { verify, anything, imock, instance, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { PacketDecodeTransform } from './packet-decode-transform.stream'; import type { PacketMapper } from '@agnoc/transport-tcp'; diff --git a/packages/cli/src/streams/packet-encode-transform.stream.test.ts b/packages/cli/src/streams/packet-encode-transform.stream.test.ts index 5dce8488..ab3d15c4 100644 --- a/packages/cli/src/streams/packet-encode-transform.stream.test.ts +++ b/packages/cli/src/streams/packet-encode-transform.stream.test.ts @@ -1,8 +1,9 @@ +import { readStream } from '@agnoc/test-support'; import { ID } from '@agnoc/toolkit'; import { OPCode, Packet, PacketSequence, Payload } from '@agnoc/transport-tcp'; import { anything, imock, instance, when, verify, deepEqual } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { givenAJSONPacket, readStream } from '../test-support'; +import { givenAJSONPacket } from '../test-support'; import { PacketEncodeTransform } from './packet-encode-transform.stream'; import type { PacketMapper } from '@agnoc/transport-tcp'; diff --git a/packages/cli/src/streams/pcap-reader.stream.test.ts b/packages/cli/src/streams/pcap-reader.stream.test.ts index f9bc9029..18a09a07 100644 --- a/packages/cli/src/streams/pcap-reader.stream.test.ts +++ b/packages/cli/src/streams/pcap-reader.stream.test.ts @@ -1,6 +1,6 @@ +import { readStream } from '@agnoc/test-support'; import { anything, fnmock, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { PCapReader } from './pcap-reader.stream'; import type { PCapReaderOptions } from './pcap-reader.stream'; import type { Decode, LiveSessionOptions, PacketWithHeader, PcapPacket, PcapSession } from 'pcap'; diff --git a/packages/cli/src/streams/pcap-transform.stream.test.ts b/packages/cli/src/streams/pcap-transform.stream.test.ts index 3691f4b2..49ccbc55 100644 --- a/packages/cli/src/streams/pcap-transform.stream.test.ts +++ b/packages/cli/src/streams/pcap-transform.stream.test.ts @@ -1,6 +1,6 @@ +import { readStream } from '@agnoc/test-support'; import { imock, instance, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { PCapTransform } from './pcap-transform.stream'; import type { EthernetPacket, IPv4, PcapPacket, TCP } from 'pcap'; diff --git a/packages/cli/src/streams/string-transform.stream.test.ts b/packages/cli/src/streams/string-transform.stream.test.ts index 078f6e1e..8e42e10c 100644 --- a/packages/cli/src/streams/string-transform.stream.test.ts +++ b/packages/cli/src/streams/string-transform.stream.test.ts @@ -1,5 +1,5 @@ +import { readStream } from '@agnoc/test-support'; import { expect } from 'chai'; -import { readStream } from '../test-support'; import { StringTransform } from './string-transform.stream'; describe('StringTransform', function () { diff --git a/packages/cli/src/test-support.ts b/packages/cli/src/test-support.ts index 75dba7f4..3e7b7943 100644 --- a/packages/cli/src/test-support.ts +++ b/packages/cli/src/test-support.ts @@ -1,21 +1,4 @@ import type { JSONPacket } from './streams/packet-encode-transform.stream'; -import type { Readable } from 'stream'; - -export function readStream(stream: Readable): Promise; -export function readStream(stream: Readable, encoding: BufferEncoding): Promise; -export function readStream(stream: Readable, encoding?: BufferEncoding): Promise<(T | string)[]> { - if (encoding) { - stream.setEncoding(encoding); - } - - return new Promise((resolve, reject) => { - const data: unknown[] = []; - - stream.on('data', (chunk) => data.push(chunk)); - stream.on('end', () => resolve(data as T[])); - stream.on('error', (error) => reject(error)); - }); -} export function givenAJSONPacket(): JSONPacket<'DEVICE_GETTIME_RSP'> { return { diff --git a/packages/test-support/.gitignore b/packages/test-support/.gitignore new file mode 100644 index 00000000..4d8631aa --- /dev/null +++ b/packages/test-support/.gitignore @@ -0,0 +1,6 @@ +*.tgz +.nyc_output/ +lib/ +coverage/ +node_modules/ + diff --git a/packages/test-support/README.md b/packages/test-support/README.md new file mode 100644 index 00000000..309e6d6b --- /dev/null +++ b/packages/test-support/README.md @@ -0,0 +1,3 @@ +# @agnoc/test-support + +The test support component of the library. diff --git a/packages/test-support/package.json b/packages/test-support/package.json new file mode 100644 index 00000000..c2acae4f --- /dev/null +++ b/packages/test-support/package.json @@ -0,0 +1,79 @@ +{ + "name": "@agnoc/test-support", + "version": "0.18.0-next.0", + "description": "Agnoc test-support library", + "keywords": [ + "agnoc", + "test-support", + "conga", + "cecotec", + "driver", + "reverse", + "engineering" + ], + "homepage": "https://github.com/adrigzr/agnoc", + "bugs": { + "url": "https://github.com/adrigzr/agnoc/issues" + }, + "license": "MIT", + "author": "Adrián González Rus (https://github.com/adrigzr)", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/adrigzr/agnoc.git", + "directory": "packages/test-support" + }, + "scripts": { + "build": "npm-run-all build:lib", + "build:lib": "tsc -b tsconfig.build.json", + "clean": "rm -rf .build-cache lib coverage", + "posttest": "test \"$(cat coverage/coverage-summary.json | json total.lines.total)\" -gt 0", + "test": "nyc mocha src" + }, + "nx": { + "targets": { + "build": { + "inputs": [ + "{workspaceRoot}/package.json", + "{workspaceRoot}/tsconfig.*", + "{projectRoot}/src/**/*", + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.*" + ], + "outputs": [ + "{projectRoot}/lib", + "{projectRoot}/.build-cache" + ] + }, + "test": { + "inputs": [ + "{workspaceRoot}/.mocharc.yml", + "{workspaceRoot}/nyc.config.js", + "{projectRoot}/src/**/*" + ], + "outputs": [ + "{projectRoot}/coverage" + ] + } + } + }, + "dependencies": { + "tslib": "^2.5.0" + }, + "devDependencies": { + "chai": "^4.3.7" + }, + "engines": { + "node": ">=18.12" + }, + "typedoc": { + "displayName": "Test Support" + } +} diff --git a/packages/test-support/src/index.ts b/packages/test-support/src/index.ts new file mode 100644 index 00000000..95d0e040 --- /dev/null +++ b/packages/test-support/src/index.ts @@ -0,0 +1 @@ +export * from './utils/read-stream.util'; diff --git a/packages/test-support/src/utils/read-stream.util.test.ts b/packages/test-support/src/utils/read-stream.util.test.ts new file mode 100644 index 00000000..1cfa5b18 --- /dev/null +++ b/packages/test-support/src/utils/read-stream.util.test.ts @@ -0,0 +1,47 @@ +import { Readable } from 'stream'; +import { expect } from 'chai'; +import { readStream } from './read-stream.util'; + +describe('readStream', function () { + it('should read data from stream', async function () { + const stream = new Readable(); + const reader = readStream(stream); + + stream.push('a'); + stream.push('b'); + stream.push(null); + + expect(await reader).to.deep.equal([Buffer.from('a'), Buffer.from('b')]); + }); + + it('should read data from stream with encoding', async function () { + const stream = new Readable(); + const reader = readStream(stream, 'utf8'); + + stream.push('a'); + stream.push('b'); + stream.push(null); + + expect(await reader).to.deep.equal(['a', 'b']); + }); + + it('should reject if stream emits error', async function () { + const stream = new Readable(); + const reader = readStream(stream); + + stream.emit('error', new Error('test')); + + await expect(reader).to.be.rejectedWith('test'); + }); + + it('should read object data from stream', async function () { + const stream = new Readable({ objectMode: true }); + const reader = readStream(stream); + + stream.push({ a: 1 }); + stream.push({ b: 2 }); + stream.push(null); + + expect(await reader).to.deep.equal([{ a: 1 }, { b: 2 }]); + }); +}); diff --git a/packages/test-support/src/utils/read-stream.util.ts b/packages/test-support/src/utils/read-stream.util.ts new file mode 100644 index 00000000..c10d0d82 --- /dev/null +++ b/packages/test-support/src/utils/read-stream.util.ts @@ -0,0 +1,18 @@ +import type { Readable } from 'stream'; + +/** Reads all data from a stream. */ +export function readStream(stream: Readable): Promise; +export function readStream(stream: Readable, encoding: BufferEncoding): Promise; +export function readStream(stream: Readable, encoding?: BufferEncoding): Promise<(T | string)[]> { + if (encoding) { + stream.setEncoding(encoding); + } + + return new Promise((resolve, reject) => { + const data: unknown[] = []; + + stream.on('data', (chunk) => data.push(chunk)); + stream.on('end', () => resolve(data as T[])); + stream.on('error', (error) => reject(error)); + }); +} diff --git a/packages/test-support/tsconfig.build.json b/packages/test-support/tsconfig.build.json new file mode 100644 index 00000000..978537b4 --- /dev/null +++ b/packages/test-support/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": ".build-cache/tsconfig.build.tsbuildinfo" + }, + "include": ["src"], + "exclude": ["**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 16834190..819e004b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "packages/adapter-tcp", "packages/transport-tcp", "packages/domain", - "packages/toolkit" + "packages/toolkit", + "packages/test-support" ], "entryPointStrategy": "packages", "out": "public/typedoc", @@ -18,5 +19,8 @@ "includeVersion": true, "sort": ["kind", "instance-first", "static-first"], "hideGenerator": true + }, + "ts-node": { + "files": true } } From 81387029c52afb350a994e990d810f4b5b0ad424 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Wed, 15 Mar 2023 20:10:20 +0100 Subject: [PATCH 02/38] feat(transport-tcp): add `PacketFactory` --- .../src/factories/packet.factory.test.ts | 45 +++++++++++++++ .../src/factories/packet.factory.ts | 55 +++++++++++++++++++ packages/transport-tcp/src/index.ts | 9 +-- 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/transport-tcp/src/factories/packet.factory.test.ts create mode 100644 packages/transport-tcp/src/factories/packet.factory.ts diff --git a/packages/transport-tcp/src/factories/packet.factory.test.ts b/packages/transport-tcp/src/factories/packet.factory.test.ts new file mode 100644 index 00000000..21a33ace --- /dev/null +++ b/packages/transport-tcp/src/factories/packet.factory.test.ts @@ -0,0 +1,45 @@ +import { ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { PacketSequence } from '../domain-primitives/packet-sequence.domain-primitive'; +import { givenSomePacketProps } from '../test-support'; +import { Packet } from '../value-objects/packet.value-object'; +import { PacketFactory } from './packet.factory'; + +describe('PacketFactory', function () { + let packetFactory: PacketFactory; + + beforeEach(function () { + packetFactory = new PacketFactory(); + }); + + it('should create a packet from some props', function () { + const object = { result: 0, body: { deviceTime: 1606129555 } }; + const props = { userId: new ID(1), deviceId: new ID(2) }; + const packet = packetFactory.create('DEVICE_GETTIME_RSP', object, props); + + expect(packet).to.be.instanceOf(Packet); + expect(packet.ctype).to.equal(2); + expect(packet.flow).to.equal(0); + expect(packet.userId).to.equal(props.deviceId); + expect(packet.deviceId).to.equal(props.userId); + expect(packet.sequence).to.be.instanceOf(PacketSequence); + expect(packet.payload.opcode.name).to.equal('DEVICE_GETTIME_RSP'); + expect(packet.payload.object).to.equal(object); + }); + + it('should create a packet from another packet', function () { + const sourcePacketProps = { ...givenSomePacketProps(), flow: 0 }; + const sourcePacket = new Packet(sourcePacketProps); + const object = { result: 0, body: { deviceTime: 1606129555 } }; + const packet = packetFactory.create('DEVICE_GETTIME_RSP', object, sourcePacket); + + expect(packet).to.be.instanceOf(Packet); + expect(packet.ctype).to.equal(sourcePacketProps.ctype); + expect(packet.flow).to.equal(1); + expect(packet.userId).to.equal(sourcePacketProps.deviceId); + expect(packet.deviceId).to.equal(sourcePacketProps.userId); + expect(packet.sequence).to.be.equal(sourcePacketProps.sequence); + expect(packet.payload.opcode.name).to.equal('DEVICE_GETTIME_RSP'); + expect(packet.payload.object).to.equal(object); + }); +}); diff --git a/packages/transport-tcp/src/factories/packet.factory.ts b/packages/transport-tcp/src/factories/packet.factory.ts new file mode 100644 index 00000000..45f74d7a --- /dev/null +++ b/packages/transport-tcp/src/factories/packet.factory.ts @@ -0,0 +1,55 @@ +import { OPCode } from '../domain-primitives/opcode.domain-primitive'; +import { PacketSequence } from '../domain-primitives/packet-sequence.domain-primitive'; +import { Packet } from '../value-objects/packet.value-object'; +import { Payload } from '../value-objects/payload.value-object'; +import type { PayloadObjectFrom, PayloadObjectName } from '../constants/payloads.constant'; +import type { Factory, ID } from '@agnoc/toolkit'; + +/** The props to create a packet. */ +export interface CreatePacketProps { + userId: ID; + deviceId: ID; +} + +/** A factory to create packets. */ +export class PacketFactory implements Factory { + /** + * Creates a packet from a payload object. + * + * To use this method you must provider a `deviceId` and `userId` in the `props` object, + * or a `Packet` object in the `packet` argument to copy the packet props from. + */ + create( + name: Name, + object: PayloadObjectFrom, + props: CreatePacketProps, + ): Packet; + create(name: Name, object: PayloadObjectFrom, packet: Packet): Packet; + create( + name: Name, + object: PayloadObjectFrom, + propsOrPacket: CreatePacketProps | Packet, + ): Packet { + if (propsOrPacket instanceof Packet) { + return new Packet({ + ctype: propsOrPacket.ctype, + flow: propsOrPacket.flow + 1, + // This swap is intended. + userId: propsOrPacket.deviceId, + deviceId: propsOrPacket.userId, + sequence: propsOrPacket.sequence, + payload: new Payload({ opcode: OPCode.fromName(name), object }), + }); + } + + return new Packet({ + ctype: 2, + flow: 0, + // This swap is intended. + userId: propsOrPacket.deviceId, + deviceId: propsOrPacket.userId, + sequence: PacketSequence.generate(), + payload: new Payload({ opcode: OPCode.fromName(name), object }), + }); + } +} diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts index 3de6b966..835156b7 100644 --- a/packages/transport-tcp/src/index.ts +++ b/packages/transport-tcp/src/index.ts @@ -2,17 +2,18 @@ export * from './constants/opcodes.constant'; export * from './constants/payloads.constant'; export * from './decoders/area.decoder'; export * from './decoders/charge-position.decoder'; -export * from './utils/get-custom-decoders.util'; export * from './decoders/map.decoder'; +export * from './decoders/map.interface'; export * from './decoders/robot-position.decoder'; export * from './domain-primitives/opcode.domain-primitive'; export * from './domain-primitives/packet-sequence.domain-primitive'; -export * from './decoders/map.interface'; +export * from './factories/packet.factory'; export * from './mappers/packet.mapper'; export * from './mappers/payload.mapper'; -export * from './services/payload-object-parser.service'; +export * from './packet.server'; export * from './packet.socket'; +export * from './services/payload-object-parser.service'; +export * from './utils/get-custom-decoders.util'; export * from './utils/get-protobuf-root.util'; export * from './value-objects/packet.value-object'; export * from './value-objects/payload.value-object'; -export * from './packet.server'; From 7095a5e407ae59c2aef52a334b46d266ad4bc62f Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 16 Mar 2023 13:02:55 +0100 Subject: [PATCH 03/38] feat(transport-tcp): add packet event handler blocks --- .../src/base-classes/event-handler.base.ts | 5 +++ .../toolkit/src/event-handler.manager.test.ts | 17 +++++++ packages/toolkit/src/event-handler.manager.ts | 14 ++++++ packages/toolkit/src/index.ts | 2 + packages/transport-tcp/src/index.ts | 2 + .../src/packet.event-bus.test.ts | 11 +++++ .../transport-tcp/src/packet.event-bus.ts | 18 ++++++++ .../src/packet.event-handler.test.ts | 44 +++++++++++++++++++ .../transport-tcp/src/packet.event-handler.ts | 22 ++++++++++ 9 files changed, 135 insertions(+) create mode 100644 packages/toolkit/src/base-classes/event-handler.base.ts create mode 100644 packages/toolkit/src/event-handler.manager.test.ts create mode 100644 packages/toolkit/src/event-handler.manager.ts create mode 100644 packages/transport-tcp/src/packet.event-bus.test.ts create mode 100644 packages/transport-tcp/src/packet.event-bus.ts create mode 100644 packages/transport-tcp/src/packet.event-handler.test.ts create mode 100644 packages/transport-tcp/src/packet.event-handler.ts diff --git a/packages/toolkit/src/base-classes/event-handler.base.ts b/packages/toolkit/src/base-classes/event-handler.base.ts new file mode 100644 index 00000000..a27adfe2 --- /dev/null +++ b/packages/toolkit/src/base-classes/event-handler.base.ts @@ -0,0 +1,5 @@ +/** Base class for event handlers. */ +export abstract class EventHandler { + /** Listen to the event. */ + abstract listen(): void; +} diff --git a/packages/toolkit/src/event-handler.manager.test.ts b/packages/toolkit/src/event-handler.manager.test.ts new file mode 100644 index 00000000..9aad3414 --- /dev/null +++ b/packages/toolkit/src/event-handler.manager.test.ts @@ -0,0 +1,17 @@ +import { imock, instance, verify } from '@johanblumenberg/ts-mockito'; +import { EventHandlerManager } from './event-handler.manager'; +import type { EventHandler } from './base-classes/event-handler.base'; + +describe('EventHandlerManager', function () { + let eventHandler: EventHandler; + + beforeEach(function () { + eventHandler = imock(); + }); + + it('should listen up event handlers', function () { + new EventHandlerManager([instance(eventHandler), instance(eventHandler)]); + + verify(eventHandler.listen()).twice(); + }); +}); diff --git a/packages/toolkit/src/event-handler.manager.ts b/packages/toolkit/src/event-handler.manager.ts new file mode 100644 index 00000000..81c9cf67 --- /dev/null +++ b/packages/toolkit/src/event-handler.manager.ts @@ -0,0 +1,14 @@ +import type { EventHandler } from './base-classes/event-handler.base'; + +/** Manages event handlers. */ +export class EventHandlerManager { + constructor(private readonly eventHandlers: EventHandler[]) { + this.addListeners(); + } + + private addListeners() { + this.eventHandlers.forEach((eventHandler) => { + eventHandler.listen(); + }); + } +} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 83db5e47..88dcd1a5 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -1,5 +1,6 @@ export * from './base-classes/domain-primitive.base'; export * from './base-classes/entity.base'; +export * from './base-classes/event-handler.base'; export * from './base-classes/exception.base'; export * from './base-classes/factory.base'; export * from './base-classes/mapper.base'; @@ -30,3 +31,4 @@ export * from './utils/is-present.util'; export * from './utils/stream.util'; export * from './utils/to-stream.util'; export * from './utils/wait-for.util'; +export * from './event-handler.manager'; diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts index 835156b7..6bec7810 100644 --- a/packages/transport-tcp/src/index.ts +++ b/packages/transport-tcp/src/index.ts @@ -10,6 +10,8 @@ export * from './domain-primitives/packet-sequence.domain-primitive'; export * from './factories/packet.factory'; export * from './mappers/packet.mapper'; export * from './mappers/payload.mapper'; +export * from './packet.event-bus'; +export * from './packet.event-handler'; export * from './packet.server'; export * from './packet.socket'; export * from './services/payload-object-parser.service'; diff --git a/packages/transport-tcp/src/packet.event-bus.test.ts b/packages/transport-tcp/src/packet.event-bus.test.ts new file mode 100644 index 00000000..845b756f --- /dev/null +++ b/packages/transport-tcp/src/packet.event-bus.test.ts @@ -0,0 +1,11 @@ +import { expect } from 'chai'; +import { TypedEmitter } from 'tiny-typed-emitter'; +import { PacketEventBus } from './packet.event-bus'; + +describe('PacketEventBus', function () { + it('should be created', function () { + const packetEventBus = new PacketEventBus(); + + expect(packetEventBus).to.be.instanceOf(TypedEmitter); + }); +}); diff --git a/packages/transport-tcp/src/packet.event-bus.ts b/packages/transport-tcp/src/packet.event-bus.ts new file mode 100644 index 00000000..bf74c3d4 --- /dev/null +++ b/packages/transport-tcp/src/packet.event-bus.ts @@ -0,0 +1,18 @@ +import { TypedEmitter } from 'tiny-typed-emitter'; +import type { PayloadObjectName } from './constants/payloads.constant'; +import type { PacketSocket } from './packet.socket'; +import type { Packet } from './value-objects/packet.value-object'; + +/** Parameter for the packet event bus event. */ +export type PacketEventBusEventParameter = { + packet: Packet; + socket: PacketSocket; +}; + +/** Events for the packet event bus. */ +export type PacketEventBusEvents = { + [Name in PayloadObjectName]: ({ packet, socket }: PacketEventBusEventParameter) => void; +}; + +/** Event bus for packets. */ +export class PacketEventBus extends TypedEmitter {} diff --git a/packages/transport-tcp/src/packet.event-handler.test.ts b/packages/transport-tcp/src/packet.event-handler.test.ts new file mode 100644 index 00000000..8d7384ac --- /dev/null +++ b/packages/transport-tcp/src/packet.event-handler.test.ts @@ -0,0 +1,44 @@ +import { anything, imock, instance, spy, verify, capture } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketEventHandler } from './packet.event-handler'; +import type { PacketEventBus } from './packet.event-bus'; +import type { PacketEventHandleParameter } from './packet.event-handler'; + +describe('PacketEventHandler', function () { + let packetEventBus: PacketEventBus; + let dummyPacketEventHandler: DummyPacketEventHandler; + let dummyPacketEventHandlerSpy: DummyPacketEventHandler; + + beforeEach(function () { + packetEventBus = imock(); + dummyPacketEventHandler = new DummyPacketEventHandler(instance(packetEventBus)); + dummyPacketEventHandlerSpy = spy(dummyPacketEventHandler); + }); + + it('should listen for events on the bus', function () { + dummyPacketEventHandler.listen(); + + verify(packetEventBus.on('DEVICE_GETTIME_RSP', anything())).once(); + }); + + it('should call handle when event is emitted', function () { + const eventParameter: PacketEventHandleParameter = instance(imock()); + + dummyPacketEventHandler.listen(); + + const [eventName, callback] = capture(packetEventBus.on<'DEVICE_GETTIME_RSP'>).first(); + + expect(eventName).to.equal('DEVICE_GETTIME_RSP'); + + callback(eventParameter); + + verify(dummyPacketEventHandlerSpy.handle(eventParameter)).once(); + }); +}); + +class DummyPacketEventHandler extends PacketEventHandler { + eventName = 'DEVICE_GETTIME_RSP' as const; + handle(_: PacketEventHandleParameter) { + // noop + } +} diff --git a/packages/transport-tcp/src/packet.event-handler.ts b/packages/transport-tcp/src/packet.event-handler.ts new file mode 100644 index 00000000..6f87aa73 --- /dev/null +++ b/packages/transport-tcp/src/packet.event-handler.ts @@ -0,0 +1,22 @@ +import type { PayloadObjectName } from './constants/payloads.constant'; +import type { PacketEventBusEventParameter, PacketEventBus } from './packet.event-bus'; +import type { EventHandler } from '@agnoc/toolkit'; + +export type PacketEventHandleParameter = PacketEventBusEventParameter; + +/** Base class for packet event handlers. */ +export abstract class PacketEventHandler implements EventHandler { + constructor(private readonly eventBus: PacketEventBus) {} + + listen(): void { + this.eventBus.on(this.eventName, (arg) => { + void this.handle(arg as PacketEventHandleParameter); + }); + } + + /** The name of the event to listen to. */ + abstract eventName: PayloadObjectName; + + /** Handle the event. */ + abstract handle(arg: PacketEventHandleParameter): Promise | void; +} From 665add1ae25fba7ebc1b2aeb18c15411a7342abf Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sat, 18 Mar 2023 14:03:35 +0100 Subject: [PATCH 04/38] feat(adapter-tcp): add event handlers --- packages/adapter-tcp/src/device.connection.ts | 73 +++++++ .../src/emitters/cloud-server.emitter.ts | 26 +-- .../src/emitters/connection.emitter.ts | 31 ++- .../src/emitters/multiplexer.emitter.ts | 20 +- .../adapter-tcp/src/emitters/robot.emitter.ts | 65 +++--- .../src}/event-handler.base.ts | 4 +- .../src/event-handler.manager.test.ts | 41 ++++ .../adapter-tcp/src/event-handler.manager.ts | 17 ++ .../client-heartbeat.event-handler.ts | 9 + .../client-login.event-handler.ts | 18 ++ .../device-battery-update.event-handler.ts | 21 ++ ...ice-clean-map-data-report.event-handler.ts | 11 + .../device-clean-map-report.event-handler.ts | 11 + .../device-clean-task-report.event-handler.ts | 9 + .../device-register.event-handler.ts | 26 +++ .../device-settings-update.event-handler.ts | 38 ++++ .../device-version-update.event-handler.ts | 19 ++ .../src/packet.event-bus.test.ts | 0 packages/adapter-tcp/src/packet.event-bus.ts | 11 + .../adapter-tcp/src/packet.event-handler.ts | 14 ++ packages/adapter-tcp/src/packet.message.ts | 15 ++ .../src/value-objects/message.value-object.ts | 2 +- .../domain/src/entities/device.entity.test.ts | 17 ++ packages/domain/src/entities/device.entity.ts | 12 +- .../domain/src/entities/user.entity.test.ts | 14 -- packages/domain/src/entities/user.entity.ts | 12 -- packages/domain/src/index.ts | 2 +- .../repositories/device.repository.test.ts | 19 ++ .../src/repositories/device.repository.ts | 4 + packages/domain/src/test-support.ts | 9 +- .../device-system.value-object.test.ts | 31 ++- .../device-system.value-object.ts | 14 +- .../src/adapters/memory.adapter.test.ts | 39 ++++ .../toolkit/src/adapters/memory.adapter.ts | 22 ++ .../toolkit/src/base-classes/adapter.base.ts | 8 + .../src/base-classes/repository.base.ts | 24 +++ .../toolkit/src/event-handler.manager.test.ts | 17 -- packages/toolkit/src/event-handler.manager.ts | 14 -- packages/toolkit/src/index.ts | 5 +- packages/transport-tcp/src/index.ts | 3 +- .../transport-tcp/src/packet.event-bus.ts | 18 -- .../src/packet.event-handler.test.ts | 44 ---- .../transport-tcp/src/packet.event-handler.ts | 22 -- packages/transport-tcp/src/packet.server.ts | 2 +- .../transport-tcp/src/packet.socket.test.ts | 194 +++++++++++++++++- packages/transport-tcp/src/packet.socket.ts | 100 ++++++++- .../src/time-sync.server.test.ts | 72 +++++++ .../transport-tcp/src/time-sync.server.ts | 33 +++ 48 files changed, 967 insertions(+), 265 deletions(-) create mode 100644 packages/adapter-tcp/src/device.connection.ts rename packages/{toolkit/src/base-classes => adapter-tcp/src}/event-handler.base.ts (50%) create mode 100644 packages/adapter-tcp/src/event-handler.manager.test.ts create mode 100644 packages/adapter-tcp/src/event-handler.manager.ts create mode 100644 packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts rename packages/{transport-tcp => adapter-tcp}/src/packet.event-bus.test.ts (100%) create mode 100644 packages/adapter-tcp/src/packet.event-bus.ts create mode 100644 packages/adapter-tcp/src/packet.event-handler.ts create mode 100644 packages/adapter-tcp/src/packet.message.ts delete mode 100644 packages/domain/src/entities/user.entity.test.ts delete mode 100644 packages/domain/src/entities/user.entity.ts create mode 100644 packages/domain/src/repositories/device.repository.test.ts create mode 100644 packages/domain/src/repositories/device.repository.ts create mode 100644 packages/toolkit/src/adapters/memory.adapter.test.ts create mode 100644 packages/toolkit/src/adapters/memory.adapter.ts create mode 100644 packages/toolkit/src/base-classes/adapter.base.ts create mode 100644 packages/toolkit/src/base-classes/repository.base.ts delete mode 100644 packages/toolkit/src/event-handler.manager.test.ts delete mode 100644 packages/toolkit/src/event-handler.manager.ts delete mode 100644 packages/transport-tcp/src/packet.event-bus.ts delete mode 100644 packages/transport-tcp/src/packet.event-handler.test.ts delete mode 100644 packages/transport-tcp/src/packet.event-handler.ts create mode 100644 packages/transport-tcp/src/time-sync.server.test.ts create mode 100644 packages/transport-tcp/src/time-sync.server.ts diff --git a/packages/adapter-tcp/src/device.connection.ts b/packages/adapter-tcp/src/device.connection.ts new file mode 100644 index 00000000..bed95eec --- /dev/null +++ b/packages/adapter-tcp/src/device.connection.ts @@ -0,0 +1,73 @@ +import { Device } from '@agnoc/domain'; +import { ArgumentInvalidException, DomainException, ID } from '@agnoc/toolkit'; +import { PacketSocket } from '@agnoc/transport-tcp'; +import { TypedEmitter } from 'tiny-typed-emitter'; +import type { Packet, PacketFactory, PayloadObjectName, PayloadObjectFrom } from '@agnoc/transport-tcp'; + +export interface DeviceConnectionEvents { + data: (packet: Packet) => void | Promise; + close: () => void; + error: (err: Error) => void; +} + +export class DeviceConnection extends TypedEmitter { + #device?: Device; + + constructor(private readonly packetFactory: PacketFactory, private readonly socket: PacketSocket) { + super(); + this.validateSocket(); + this.addListeners(); + } + + get device(): Device | undefined { + return this.#device; + } + + set device(device: Device | undefined) { + if (device && !(device instanceof Device)) { + throw new ArgumentInvalidException( + `Value '${device as string} for property 'device' of Connection is not an instance of Device`, + ); + } + + this.#device = device; + } + + send(name: Name, object: PayloadObjectFrom): Promise { + const props = { deviceId: this.#device?.id ?? new ID(0), userId: this.#device?.userId ?? new ID(0) }; + const packet = this.packetFactory.create(name, object, props); + + return this.socket.write(packet); + } + + respond(name: Name, object: PayloadObjectFrom, packet: Packet): Promise { + return this.socket.write(this.packetFactory.create(name, object, packet)); + } + + close(): Promise { + return this.socket.end(); + } + + private validateSocket() { + if (!(this.socket instanceof PacketSocket)) { + throw new DomainException('Socket for Connection is not an instance of PacketSocket'); + } + + if (!this.socket.connected) { + throw new DomainException('Socket for Connection is closed'); + } + } + + private addListeners() { + this.socket.on('data', (packet) => { + this.emit('data', packet); + }); + this.socket.on('error', (err) => { + this.emit('error', err); + }); + this.socket.on('close', () => { + this.socket.removeAllListeners(); + this.emit('close'); + }); + } +} diff --git a/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts b/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts index 5025482b..8120fe74 100644 --- a/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts +++ b/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { Device, DeviceSystem, DeviceVersion, User } from '@agnoc/domain'; +import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; import { ID, bind } from '@agnoc/toolkit'; import { PacketServer } from '@agnoc/transport-tcp'; import { TypedEmitter } from 'tiny-typed-emitter'; @@ -53,7 +53,7 @@ export class CloudServer extends TypedEmitter { async close(): Promise { this.getRobots().map((robot) => { - robot.disconnect(); + void robot.disconnect(); }); await Promise.all([this.servers.cmd.close(), this.servers.map.close(), this.servers.rtc.close()]); @@ -66,12 +66,12 @@ export class CloudServer extends TypedEmitter { if (!robot) { const object = message.packet.payload.object; - message.respond('CLIENT_ONLINE_RSP', { + void message.respond('CLIENT_ONLINE_RSP', { result: 12002, reason: `Device not registered(devsn: ${object.deviceSerialNumber})`, }); } else { - message.respond('CLIENT_ONLINE_RSP', { + void message.respond('CLIENT_ONLINE_RSP', { result: 0, }); } @@ -83,16 +83,14 @@ export class CloudServer extends TypedEmitter { const multiplexer = new Multiplexer(); const device = new Device({ id: ID.generate(), - system: new DeviceSystem({ type: props.deviceType }), + userId: ID.generate(), + system: new DeviceSystem({ type: props.deviceType, serialNumber: props.deviceSerialNumber }), version: new DeviceVersion({ software: props.softwareVersion, hardware: props.hardwareVersion, }), }); - const user = new User({ - id: ID.generate(), - }); - const robot = new Robot({ device, user, multiplexer }); + const robot = new Robot({ device, multiplexer }); multiplexer.on('error', (err) => this.emit('error', err)); @@ -102,7 +100,7 @@ export class CloudServer extends TypedEmitter { this.emit('addRobot', robot); - message.respond('DEVICE_REGISTER_RSP', { + void message.respond('DEVICE_REGISTER_RSP', { result: 0, device: { id: device.id.value, @@ -126,7 +124,7 @@ export class CloudServer extends TypedEmitter { return; } - message.respond('COMMON_ERROR_REPLY', { + void message.respond('COMMON_ERROR_REPLY', { result: 1, opcode: message.packet.payload.opcode.code, error: 'Device not registered', @@ -148,10 +146,8 @@ export class CloudServer extends TypedEmitter { private handleRTPConnection(socket: PacketSocket) { const connection = new Connection(socket); - connection.send({ + void connection.send({ opname: 'DEVICE_TIME_SYNC_RSP', - userId: new ID(0), - deviceId: new ID(0), object: { result: 0, body: { @@ -160,7 +156,7 @@ export class CloudServer extends TypedEmitter { }, }); - connection.close(); + void connection.close(); } private addListeners(): void { diff --git a/packages/adapter-tcp/src/emitters/connection.emitter.ts b/packages/adapter-tcp/src/emitters/connection.emitter.ts index a965b251..7acc83cd 100644 --- a/packages/adapter-tcp/src/emitters/connection.emitter.ts +++ b/packages/adapter-tcp/src/emitters/connection.emitter.ts @@ -6,17 +6,17 @@ import { isPresent, ArgumentNotProvidedException, ArgumentInvalidException, + ID, } from '@agnoc/toolkit'; import { Packet, PacketSocket, PacketSequence, OPCode, Payload } from '@agnoc/transport-tcp'; import { TypedEmitter } from 'tiny-typed-emitter'; -import type { ID } from '@agnoc/toolkit'; +import type { Device } from '@agnoc/domain'; import type { PayloadObjectName, PayloadObjectFrom } from '@agnoc/transport-tcp'; import type { Debugger } from 'debug'; export interface ConnectionSendProps { opname: Name; - userId: ID; - deviceId: ID; + device?: Device; object: PayloadObjectFrom; } @@ -53,23 +53,23 @@ export class Connection extends TypedEmitter this.socket.on('close', this.handleClose); } - send({ opname, userId, deviceId, object }: ConnectionSendProps): boolean { + send({ opname, device, object }: ConnectionSendProps): Promise { const packet = new Packet({ ctype: 2, flow: 0, // This swap is intended. - userId: deviceId, - deviceId: userId, + userId: device?.id ?? new ID(0), + deviceId: device?.userId ?? new ID(0), sequence: PacketSequence.generate(), payload: new Payload({ opcode: OPCode.fromName(opname), object }), }); this.debug(`sending packet ${packet.toString()}`); - return this.write(packet); + return this.socket.write(packet); } - respond({ packet, opname, object }: ConnectionRespondProps): boolean { + respond({ packet, opname, object }: ConnectionRespondProps): Promise { const response = new Packet({ ctype: packet.ctype, flow: packet.flow + 1, @@ -82,20 +82,13 @@ export class Connection extends TypedEmitter this.debug(`responding to packet with ${response.toString()}`); - return this.write(response); + return this.socket.write(response); } - private write(packet: Packet): boolean { - if (!this.socket.destroyed && !this.socket.connecting) { - return this.socket.write(packet); - } - - return false; - } - - close(): void { + close(): Promise { this.debug('closing socket...'); - this.socket.end(); + + return this.socket.end(); } @bind diff --git a/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts b/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts index cf6757f8..c446e901 100644 --- a/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts +++ b/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts @@ -2,7 +2,7 @@ import { DomainException, debug, bind } from '@agnoc/toolkit'; import { TypedEmitter } from 'tiny-typed-emitter'; import type { Connection } from './connection.emitter'; -import type { ID } from '@agnoc/toolkit'; +import type { Device } from '@agnoc/domain'; import type { PayloadObjectName, Packet, PayloadObjectFrom } from '@agnoc/transport-tcp'; export type MultiplexerEvents = { @@ -14,8 +14,7 @@ export type MultiplexerEvents = { export interface MultiplexerSendProps { opname: Name; - userId: ID; - deviceId: ID; + device: Device; object: PayloadObjectFrom; } @@ -46,23 +45,24 @@ export class Multiplexer extends TypedEmitter): boolean { + send(props: MultiplexerSendProps): Promise { const connection = this.connections[0]; if (!connection) { - this.emit('error', new DomainException(`No valid connection found to send packet ${props.opname}`)); + const error = new DomainException(`No valid connection found to send packet ${props.opname}`); - return false; + this.emit('error', error); + + throw error; } return connection.send(props); } - close(): void { + async close(): Promise { this.debug('closing connections...'); - this.connections.forEach((connection) => { - connection.close(); - }); + + await Promise.all([this.connections.map((connection) => connection.close())]); } @bind diff --git a/packages/adapter-tcp/src/emitters/robot.emitter.ts b/packages/adapter-tcp/src/emitters/robot.emitter.ts index 1668dfa1..4112af87 100644 --- a/packages/adapter-tcp/src/emitters/robot.emitter.ts +++ b/packages/adapter-tcp/src/emitters/robot.emitter.ts @@ -49,7 +49,6 @@ import type { Multiplexer } from './multiplexer.emitter'; import type { Message, MessageHandlers } from '../value-objects/message.value-object'; import type { Device, - User, DeviceFanSpeed, DeviceWaterLevel, DeviceOrder, @@ -61,7 +60,6 @@ import type { Debugger } from 'debug'; export interface RobotProps { device: Device; - user: User; multiplexer: Multiplexer; } @@ -123,7 +121,6 @@ const deviceBatteryMapper = new DeviceBatteryMapper(); export class Robot extends TypedEmitter { public readonly device: Device; - public readonly user: User; private readonly multiplexer: Multiplexer; private debug: Debugger; private handlers: MessageHandlers = { @@ -146,10 +143,9 @@ export class Robot extends TypedEmitter { DEVICE_SETTIME_REQ: this.handleSetTime, }; - constructor({ device, user, multiplexer }: RobotProps) { + constructor({ device, multiplexer }: RobotProps) { super(); this.device = device; - this.user = user; this.multiplexer = multiplexer; this.debug = debug(__filename).extend(this.device.id.toString()); this.debug('new robot'); @@ -768,10 +764,8 @@ export class Robot extends TypedEmitter { async handshake(): Promise { await this.controlLock(); - - this.send('DEVICE_STATUS_GETTING_REQ', {}); - - void this.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { + await this.send('DEVICE_STATUS_GETTING_REQ', {}); + await this.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { unk1: 0, unk2: '', }); @@ -793,7 +787,7 @@ export class Robot extends TypedEmitter { ); this.emit('updateDevice'); - message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { + void message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { result: 0, }); } @@ -820,19 +814,19 @@ export class Robot extends TypedEmitter { this.device.updateConfig(new DeviceSettings(props)); - message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { + void message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { result: 0, }); } @bind handleClientHeartbeat(message: Message<'CLIENT_HEARTBEAT_REQ'>): void { - message.respond('CLIENT_HEARTBEAT_RSP', {}); + void message.respond('CLIENT_HEARTBEAT_RSP', {}); } @bind handleDevicePackageUpgrade(message: Message<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>): void { - message.respond('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { + void message.respond('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { result: 0, }); } @@ -1071,7 +1065,7 @@ export class Robot extends TypedEmitter { @bind handleDeviceBatteryInfo(message: Message<'PUSH_DEVICE_BATTERY_INFO_REQ'>): void { - message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { + void message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { result: 0, }); @@ -1090,20 +1084,20 @@ export class Robot extends TypedEmitter { @bind handleWorkstatusReport(message: Message<'DEVICE_WORKSTATUS_REPORT_REQ'>): void { - message.respond('DEVICE_WORKSTATUS_REPORT_RSP', { + void message.respond('DEVICE_WORKSTATUS_REPORT_RSP', { result: 0, }); } @bind handleReportCleantask(message: Message<'DEVICE_EVENT_REPORT_CLEANTASK'>): void { - message.respond('UNK_11A4', { unk1: 0 }); + void message.respond('UNK_11A4', { unk1: 0 }); } @bind handleReportCleanmap(message: Message<'DEVICE_EVENT_REPORT_CLEANMAP'>): void { const object = message.packet.payload.object; - message.respond('DEVICE_EVENT_REPORT_RSP', { + void message.respond('DEVICE_EVENT_REPORT_RSP', { result: 0, body: { cleanId: object.cleanId, @@ -1114,7 +1108,7 @@ export class Robot extends TypedEmitter { @bind handleBinDataReport(message: Message<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): void { const object = message.packet.payload.object; - message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { + void message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { result: 0, cleanId: object.cleanId, }); @@ -1122,14 +1116,14 @@ export class Robot extends TypedEmitter { @bind handleEventReport(message: Message<'DEVICE_EVENT_REPORT_REQ'>): void { - message.respond('UNK_11A7', { unk1: 0 }); + void message.respond('UNK_11A7', { unk1: 0 }); } @bind handleSetTime(message: Message<'DEVICE_SETTIME_REQ'>): void { const date = new Date(); - message.respond('DEVICE_SETTIME_RSP', { + void message.respond('DEVICE_SETTIME_RSP', { deviceTime: Math.floor(date.getTime() / 1000), deviceTimezone: -1 * (date.getTimezoneOffset() * 60), }); @@ -1146,8 +1140,8 @@ export class Robot extends TypedEmitter { handleMessage(message: Message): void { const handler = this.handlers[message.opname]; - if (message.packet.userId.value !== 0 && message.packet.userId.value !== this.user.id.value) { - message.respond('COMMON_ERROR_REPLY', { + if (message.packet.userId.value !== 0 && message.packet.userId.value !== this.device.userId.value) { + void message.respond('COMMON_ERROR_REPLY', { result: 11001, error: 'Target user is offline', opcode: message.packet.payload.opcode.code, @@ -1163,25 +1157,24 @@ export class Robot extends TypedEmitter { } override toString(): string { - return [`device: ${this.device.toString()}`, `user: ${this.user.toString()}`].join(' '); + return [`device: ${this.device.toString()}`].join(' '); } - disconnect(): void { + disconnect(): Promise { this.debug('disconnecting...'); return this.multiplexer.close(); } - send(opname: Name, object: PayloadObjectFrom): void { - const ret = this.multiplexer.send({ - opname, - userId: this.user.id, - deviceId: this.device.id, - object, - }); - - if (!ret) { - throw new DomainException(`There was an error sending opcode '${opname}'`); + async send(opname: Name, object: PayloadObjectFrom): Promise { + try { + await this.multiplexer.send({ + opname, + device: this.device, + object, + }); + } catch (err) { + throw new DomainException(`There was an error sending opcode '${opname}'`, {}, { cause: err }); } } @@ -1203,12 +1196,12 @@ export class Robot extends TypedEmitter { }); } - sendRecv( + async sendRecv( sendOPName: SendName, recvOPName: RecvName, sendObject: PayloadObjectFrom, ): Promise> { - this.send(sendOPName, sendObject); + await this.send(sendOPName, sendObject); return this.recv(recvOPName); } diff --git a/packages/toolkit/src/base-classes/event-handler.base.ts b/packages/adapter-tcp/src/event-handler.base.ts similarity index 50% rename from packages/toolkit/src/base-classes/event-handler.base.ts rename to packages/adapter-tcp/src/event-handler.base.ts index a27adfe2..3c3ec80a 100644 --- a/packages/toolkit/src/base-classes/event-handler.base.ts +++ b/packages/adapter-tcp/src/event-handler.base.ts @@ -1,5 +1,5 @@ /** Base class for event handlers. */ export abstract class EventHandler { - /** Listen to the event. */ - abstract listen(): void; + abstract eventName: string; + abstract handle(...args: unknown[]): void; } diff --git a/packages/adapter-tcp/src/event-handler.manager.test.ts b/packages/adapter-tcp/src/event-handler.manager.test.ts new file mode 100644 index 00000000..f1e3f6b3 --- /dev/null +++ b/packages/adapter-tcp/src/event-handler.manager.test.ts @@ -0,0 +1,41 @@ +import { anything, capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { EventHandlerManager } from './event-handler.manager'; +import type { EventHandler } from './event-handler.base'; +import type { EventEmitter } from 'stream'; + +describe('EventHandlerManager', function () { + let eventBus: EventEmitter; + let eventHandler: EventHandler; + let eventHandlerManager: EventHandlerManager; + + beforeEach(function () { + eventBus = imock(); + eventHandler = imock(); + eventHandlerManager = new EventHandlerManager(instance(eventBus)); + }); + + it('should listen for events on the bus', function () { + when(eventHandler.eventName).thenReturn('event'); + + eventHandlerManager.register(instance(eventHandler)); + + verify(eventBus.on('event', anything())).once(); + }); + + it('should call handle when event is emitted', function () { + const data = { foo: 'bar' }; + + when(eventHandler.eventName).thenReturn('event'); + + eventHandlerManager.register(instance(eventHandler)); + + const [eventName, callback] = capture(eventBus.on).first(); + + expect(eventName).to.equal('event'); + + callback(data); + + verify(eventHandler.handle(data)).once(); + }); +}); diff --git a/packages/adapter-tcp/src/event-handler.manager.ts b/packages/adapter-tcp/src/event-handler.manager.ts new file mode 100644 index 00000000..71fbd413 --- /dev/null +++ b/packages/adapter-tcp/src/event-handler.manager.ts @@ -0,0 +1,17 @@ +import type { EventHandler } from './event-handler.base'; +import type { EventEmitter } from 'stream'; + +/** Manages event handlers. */ +export class EventHandlerManager { + private readonly eventHandlers: EventHandler[] = []; + constructor(private readonly eventBus: EventEmitter) {} + + register(...eventHandlers: EventHandler[]): void { + eventHandlers.forEach((eventHandler) => this.addEventHandler(eventHandler)); + } + + private addEventHandler(eventHandler: EventHandler): void { + this.eventHandlers.push(eventHandler); + this.eventBus.on(eventHandler.eventName, eventHandler.handle.bind(eventHandler)); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts new file mode 100644 index 00000000..935612d5 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts @@ -0,0 +1,9 @@ +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class ClientHeartbeatEventHandler implements PacketEventHandler { + eventName = 'CLIENT_HEARTBEAT_REQ' as const; + + async handle(message: PacketEventHandleParameter): Promise { + await message.respond('CLIENT_HEARTBEAT_RSP', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts new file mode 100644 index 00000000..5841808d --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts @@ -0,0 +1,18 @@ +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class ClientLoginEventHandler implements PacketEventHandler { + eventName = 'CLIENT_ONLINE_REQ' as const; + + async handle(message: PacketEventHandleParameter): Promise { + if (!message.device) { + const data = { + result: 12002, + reason: `Device not registered(devsn: ${message.packet.payload.object.deviceSerialNumber})`, + }; + + return message.respond('CLIENT_ONLINE_RSP', data); + } + + await message.respond('CLIENT_ONLINE_RSP', { result: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts new file mode 100644 index 00000000..353711ce --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts @@ -0,0 +1,21 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { + eventName = 'PUSH_DEVICE_BATTERY_INFO_REQ' as const; + + constructor(private readonly deviceBatteryMapper: DeviceBatteryMapper) {} + + async handle(message: PacketEventHandleParameter): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const data = message.packet.payload.object; + + message.device.updateBattery(this.deviceBatteryMapper.toDomain(data.battery.level)); + + await message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { result: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts new file mode 100644 index 00000000..9d6f9014 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts @@ -0,0 +1,11 @@ +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { + eventName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ' as const; + + async handle(message: PacketEventHandleParameter): Promise { + const data = message.packet.payload.object; + + await message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { result: 0, cleanId: data.cleanId }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts new file mode 100644 index 00000000..4f94547f --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts @@ -0,0 +1,11 @@ +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceCleanMapReportEventHandler implements PacketEventHandler { + eventName = 'DEVICE_EVENT_REPORT_CLEANMAP' as const; + + async handle(message: PacketEventHandleParameter): Promise { + const data = message.packet.payload.object; + + await message.respond('DEVICE_EVENT_REPORT_RSP', { result: 0, body: { cleanId: data.cleanId } }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts new file mode 100644 index 00000000..beb59187 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts @@ -0,0 +1,9 @@ +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { + eventName = 'DEVICE_EVENT_REPORT_CLEANTASK' as const; + + async handle(message: PacketEventHandleParameter): Promise { + await message.respond('UNK_11A4', { unk1: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts new file mode 100644 index 00000000..8a0d8084 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts @@ -0,0 +1,26 @@ +import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; +import type { DeviceRepository } from '@agnoc/domain'; + +export class DeviceRegisterEventHandler implements PacketEventHandler { + eventName = 'DEVICE_REGISTER_REQ' as const; + + constructor(private readonly deviceRepository: DeviceRepository) {} + + async handle(message: PacketEventHandleParameter): Promise { + const data = message.packet.payload.object; + const device = new Device({ + id: ID.generate(), + userId: ID.generate(), + system: new DeviceSystem({ type: data.deviceType, serialNumber: data.deviceSerialNumber }), + version: new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion }), + }); + + await this.deviceRepository.saveOne(device); + + const response = { result: 0, device: { id: device.id.value } }; + + await message.respond('DEVICE_REGISTER_RSP', response); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts new file mode 100644 index 00000000..76c00e9a --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts @@ -0,0 +1,38 @@ +import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { DeviceVoiceMapper } from '../mappers/device-voice.mapper'; +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { + eventName = 'PUSH_DEVICE_AGENT_SETTING_REQ' as const; + + constructor(private readonly deviceVoiceMapper: DeviceVoiceMapper) {} + + async handle(message: PacketEventHandleParameter): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const data = message.packet.payload.object; + const deviceSettings = new DeviceSettings({ + voice: this.deviceVoiceMapper.toDomain({ + isEnabled: data.voice.voiceMode, + volume: data.voice.volume, + }), + quietHours: new QuietHoursSetting({ + isEnabled: data.quietHours.isOpen, + beginTime: DeviceTime.fromMinutes(data.quietHours.beginTime), + endTime: DeviceTime.fromMinutes(data.quietHours.endTime), + }), + ecoMode: new DeviceSetting({ isEnabled: data.cleanPreference.ecoMode ?? false }), + repeatClean: new DeviceSetting({ isEnabled: data.cleanPreference.repeatClean ?? false }), + brokenClean: new DeviceSetting({ isEnabled: data.cleanPreference.cleanBroken ?? false }), + carpetMode: new DeviceSetting({ isEnabled: data.cleanPreference.carpetTurbo ?? false }), + historyMap: new DeviceSetting({ isEnabled: data.cleanPreference.historyMap ?? false }), + }); + + message.device.updateConfig(deviceSettings); + + await message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { result: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts new file mode 100644 index 00000000..4e0df525 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts @@ -0,0 +1,19 @@ +import { DeviceVersion } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; + +export class DeviceVersionUpdateEventHandler implements PacketEventHandler { + eventName = 'DEVICE_VERSION_INFO_UPDATE_REQ' as const; + + async handle(message: PacketEventHandleParameter): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const data = message.packet.payload.object; + + message.device.updateVersion(new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion })); + + await message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { result: 0 }); + } +} diff --git a/packages/transport-tcp/src/packet.event-bus.test.ts b/packages/adapter-tcp/src/packet.event-bus.test.ts similarity index 100% rename from packages/transport-tcp/src/packet.event-bus.test.ts rename to packages/adapter-tcp/src/packet.event-bus.test.ts diff --git a/packages/adapter-tcp/src/packet.event-bus.ts b/packages/adapter-tcp/src/packet.event-bus.ts new file mode 100644 index 00000000..2f4e8135 --- /dev/null +++ b/packages/adapter-tcp/src/packet.event-bus.ts @@ -0,0 +1,11 @@ +import { TypedEmitter } from 'tiny-typed-emitter'; +import type { PacketMessage } from './packet.message'; +import type { PayloadObjectName } from '@agnoc/transport-tcp'; + +/** Events for the packet event bus. */ +export type PacketEventBusEvents = { + [Name in PayloadObjectName]: (message: PacketMessage) => void; +}; + +/** Event bus for packets. */ +export class PacketEventBus extends TypedEmitter {} diff --git a/packages/adapter-tcp/src/packet.event-handler.ts b/packages/adapter-tcp/src/packet.event-handler.ts new file mode 100644 index 00000000..73119016 --- /dev/null +++ b/packages/adapter-tcp/src/packet.event-handler.ts @@ -0,0 +1,14 @@ +import type { EventHandler } from './event-handler.base'; +import type { PacketMessage } from './packet.message'; +import type { PayloadObjectName } from '@agnoc/transport-tcp'; + +export type PacketEventHandleParameter = PacketMessage; + +/** Base class for packet event handlers. */ +export abstract class PacketEventHandler implements EventHandler { + /** The name of the event to listen to. */ + abstract eventName: PayloadObjectName; + + /** Handle the event. */ + abstract handle(message: PacketEventHandleParameter): void; +} diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts new file mode 100644 index 00000000..b94c1ee7 --- /dev/null +++ b/packages/adapter-tcp/src/packet.message.ts @@ -0,0 +1,15 @@ +import type { DeviceConnection } from './device.connection'; +import type { Device } from '@agnoc/domain'; +import type { Packet, PayloadObjectFrom, PayloadObjectName } from '@agnoc/transport-tcp'; + +export class PacketMessage { + constructor(private readonly connection: DeviceConnection, readonly packet: Packet) {} + + get device(): Device | undefined { + return this.connection.device; + } + + respond(name: Name, object: PayloadObjectFrom): Promise { + return this.connection.respond(name, object, this.packet); + } +} diff --git a/packages/adapter-tcp/src/value-objects/message.value-object.ts b/packages/adapter-tcp/src/value-objects/message.value-object.ts index 96387d0c..a6e1fc86 100644 --- a/packages/adapter-tcp/src/value-objects/message.value-object.ts +++ b/packages/adapter-tcp/src/value-objects/message.value-object.ts @@ -31,7 +31,7 @@ export class Message extends ValueObject(opname: RName, object: PayloadObjectFrom): boolean { + respond(opname: RName, object: PayloadObjectFrom): Promise { return this.connection.respond({ packet: this.packet, opname, object }); } diff --git a/packages/domain/src/entities/device.entity.test.ts b/packages/domain/src/entities/device.entity.test.ts index cf18a4bc..192148c3 100644 --- a/packages/domain/src/entities/device.entity.test.ts +++ b/packages/domain/src/entities/device.entity.test.ts @@ -38,10 +38,19 @@ describe('Device', function () { expect(device).to.be.instanceOf(Entity); expect(device.id).to.be.equal(deviceProps.id); + expect(device.userId).to.be.equal(deviceProps.userId); expect(device.system).to.be.equal(deviceProps.system); expect(device.version).to.be.equal(deviceProps.version); }); + it("should throw an error when 'userId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new Device({ ...givenSomeDeviceProps(), userId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'userId' for Device not provided`, + ); + }); + it("should throw an error when 'system' is not provided", function () { // @ts-expect-error - missing property expect(() => new Device({ ...givenSomeDeviceProps(), system: undefined })).to.throw( @@ -58,6 +67,14 @@ describe('Device', function () { ); }); + it("should throw an error when 'userId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new Device({ ...givenSomeDeviceProps(), userId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'userId' of Device is not an instance of ID`, + ); + }); + it("should throw an error when 'system' is not a DeviceSystem", function () { // @ts-expect-error - invalid property expect(() => new Device({ ...givenSomeDeviceProps(), system: 'foo' })).to.throw( diff --git a/packages/domain/src/entities/device.entity.ts b/packages/domain/src/entities/device.entity.ts index 8f88656d..b10d277c 100644 --- a/packages/domain/src/entities/device.entity.ts +++ b/packages/domain/src/entities/device.entity.ts @@ -1,4 +1,4 @@ -import { Entity } from '@agnoc/toolkit'; +import { Entity, ID } from '@agnoc/toolkit'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -17,6 +17,8 @@ import type { EntityProps } from '@agnoc/toolkit'; /** Describes the properties of a device. */ export interface DeviceProps extends EntityProps { + /** The user id. */ + userId: ID; /** The device system. */ system: DeviceSystem; /** The device version. */ @@ -53,6 +55,11 @@ export interface DeviceProps extends EntityProps { /** Describes a device. */ export class Device extends Entity { + /** Returns the user id. */ + get userId(): ID { + return this.props.userId; + } + /** Returns the device system. */ get system(): DeviceSystem { return this.props.system; @@ -232,12 +239,13 @@ export class Device extends Entity { } protected validate(props: DeviceProps): void { - const keys: (keyof DeviceProps)[] = ['system', 'version']; + const keys: (keyof DeviceProps)[] = ['userId', 'system', 'version']; keys.forEach((prop) => { this.validateDefinedProp(props, prop); }); + this.validateInstanceProp(props, 'userId', ID); this.validateInstanceProp(props, 'system', DeviceSystem); this.validateInstanceProp(props, 'version', DeviceVersion); this.validateInstanceProp(props, 'config', DeviceSettings); diff --git a/packages/domain/src/entities/user.entity.test.ts b/packages/domain/src/entities/user.entity.test.ts deleted file mode 100644 index 988f9de0..00000000 --- a/packages/domain/src/entities/user.entity.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Entity } from '@agnoc/toolkit'; -import { expect } from 'chai'; -import { givenSomeUserProps } from '../test-support'; -import { User } from './user.entity'; - -describe('User', function () { - it('should be created', function () { - const userProps = givenSomeUserProps(); - const user = new User(userProps); - - expect(user).to.be.instanceOf(Entity); - expect(user.id).to.be.equal(userProps.id); - }); -}); diff --git a/packages/domain/src/entities/user.entity.ts b/packages/domain/src/entities/user.entity.ts deleted file mode 100644 index 31fdec4a..00000000 --- a/packages/domain/src/entities/user.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Entity } from '@agnoc/toolkit'; -import type { EntityProps } from '@agnoc/toolkit'; - -/** Describes the user properties. */ -export type UserProps = EntityProps; - -/** Describes a user. */ -export class User extends Entity { - protected validate(_: EntityProps): void { - // noop - } -} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index de5aed10..3a4bc83a 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -11,8 +11,8 @@ export * from './entities/device-map.entity'; export * from './entities/device-order.entity'; export * from './entities/device.entity'; export * from './entities/room.entity'; -export * from './entities/user.entity'; export * from './entities/zone.entity'; +export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; export * from './value-objects/device-consumable.value-object'; export * from './value-objects/device-setting.value-object'; diff --git a/packages/domain/src/repositories/device.repository.test.ts b/packages/domain/src/repositories/device.repository.test.ts new file mode 100644 index 00000000..dce75dfb --- /dev/null +++ b/packages/domain/src/repositories/device.repository.test.ts @@ -0,0 +1,19 @@ +import { Repository } from '@agnoc/toolkit'; +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceRepository } from './device.repository'; +import type { Adapter } from '@agnoc/toolkit'; + +describe('DeviceRepository', function () { + let adapter: Adapter; + let repository: DeviceRepository; + + beforeEach(function () { + adapter = imock(); + repository = new DeviceRepository(instance(adapter)); + }); + + it('should be a repository', function () { + expect(repository).to.be.an.instanceOf(Repository); + }); +}); diff --git a/packages/domain/src/repositories/device.repository.ts b/packages/domain/src/repositories/device.repository.ts new file mode 100644 index 00000000..504e2a99 --- /dev/null +++ b/packages/domain/src/repositories/device.repository.ts @@ -0,0 +1,4 @@ +import { Repository } from '@agnoc/toolkit'; +import type { Device } from '../entities/device.entity'; + +export class DeviceRepository extends Repository {} diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index 07509766..b5b19074 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -19,7 +19,6 @@ import type { DeviceMapProps } from './entities/device-map.entity'; import type { DeviceOrderProps } from './entities/device-order.entity'; import type { DeviceProps } from './entities/device.entity'; import type { RoomProps } from './entities/room.entity'; -import type { UserProps } from './entities/user.entity'; import type { ZoneProps } from './entities/zone.entity'; import type { DeviceCleanWorkProps } from './value-objects/device-clean-work.value-object'; import type { DeviceConsumableProps } from './value-objects/device-consumable.value-object'; @@ -159,12 +158,6 @@ export function givenSomeDeviceMapProps(): DeviceMapProps { }; } -export function givenSomeUserProps(): UserProps { - return { - id: ID.generate(), - }; -} - export function givenSomeDeviceOrderProps(): DeviceOrderProps { return { id: ID.generate(), @@ -184,12 +177,14 @@ export function givenSomeDeviceOrderProps(): DeviceOrderProps { export function givenSomeDeviceSystemProps(): DeviceSystemProps { return { type: 9, + serialNumber: 'serialNumber', }; } export function givenSomeDeviceProps(): DeviceProps { return { id: ID.generate(), + userId: ID.generate(), system: new DeviceSystem(givenSomeDeviceSystemProps()), version: new DeviceVersion(givenSomeDeviceVersionProps()), }; diff --git a/packages/domain/src/value-objects/device-system.value-object.test.ts b/packages/domain/src/value-objects/device-system.value-object.test.ts index 52afed15..12f37143 100644 --- a/packages/domain/src/value-objects/device-system.value-object.test.ts +++ b/packages/domain/src/value-objects/device-system.value-object.test.ts @@ -1,13 +1,16 @@ import { ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; import { expect } from 'chai'; +import { givenSomeDeviceSystemProps } from '../test-support'; import { DeviceCapability, DeviceSystem } from './device-system.value-object'; describe('DeviceSystem', function () { it('should be created with a 3090 model', function () { - const deviceSystem = new DeviceSystem({ type: 3 }); + const deviceSystemProps = { ...givenSomeDeviceSystemProps(), type: 3 }; + const deviceSystem = new DeviceSystem(deviceSystemProps); expect(deviceSystem).to.be.instanceOf(DeviceSystem); expect(deviceSystem.type).to.be.equal(3); + expect(deviceSystem.serialNumber).to.be.equal(deviceSystemProps.serialNumber); expect(deviceSystem.model).to.be.equal('C3090'); expect(deviceSystem.supports(DeviceCapability.MAP_PLANS)).to.be.equal(false); expect(deviceSystem.supports(DeviceCapability.CONSUMABLES)).to.be.equal(false); @@ -15,10 +18,12 @@ describe('DeviceSystem', function () { }); it('should be created with a 3490 model', function () { - const deviceSystem = new DeviceSystem({ type: 9 }); + const deviceSystemProps = { ...givenSomeDeviceSystemProps(), type: 9 }; + const deviceSystem = new DeviceSystem(deviceSystemProps); expect(deviceSystem).to.be.instanceOf(DeviceSystem); expect(deviceSystem.type).to.be.equal(9); + expect(deviceSystem.serialNumber).to.be.equal(deviceSystemProps.serialNumber); expect(deviceSystem.model).to.be.equal('C3490'); expect(deviceSystem.supports(DeviceCapability.MAP_PLANS)).to.be.equal(true); expect(deviceSystem.supports(DeviceCapability.CONSUMABLES)).to.be.equal(true); @@ -26,7 +31,7 @@ describe('DeviceSystem', function () { }); it('should be created with an unknown model', function () { - const deviceSystem = new DeviceSystem({ type: -1 }); + const deviceSystem = new DeviceSystem({ ...givenSomeDeviceSystemProps(), type: -1 }); expect(deviceSystem).to.be.instanceOf(DeviceSystem); expect(deviceSystem.type).to.be.equal(-1); @@ -38,17 +43,33 @@ describe('DeviceSystem', function () { it("should throw an error when 'type' is not provided", function () { // @ts-expect-error - invalid value - expect(() => new DeviceSystem({ foo: 'bar' })).to.throw( + expect(() => new DeviceSystem({ ...givenSomeDeviceSystemProps(), type: undefined })).to.throw( ArgumentNotProvidedException, `Property 'type' for DeviceSystem not provided`, ); }); + it("should throw an error when 'serialNumber' is not provided", function () { + // @ts-expect-error - invalid value + expect(() => new DeviceSystem({ ...givenSomeDeviceSystemProps(), serialNumber: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'serialNumber' for DeviceSystem not provided`, + ); + }); + it("should throw an error when 'type' is not a number", function () { // @ts-expect-error - invalid value - expect(() => new DeviceSystem({ type: 'foo' })).to.throw( + expect(() => new DeviceSystem({ ...givenSomeDeviceSystemProps(), type: 'foo' })).to.throw( ArgumentInvalidException, `Value 'foo' for property 'type' of DeviceSystem is not a number`, ); }); + + it("should throw an error when 'serialNumber' is not a string", function () { + // @ts-expect-error - invalid value + expect(() => new DeviceSystem({ ...givenSomeDeviceSystemProps(), serialNumber: 1 })).to.throw( + ArgumentInvalidException, + `Value '1' for property 'serialNumber' of DeviceSystem is not a string`, + ); + }); }); diff --git a/packages/domain/src/value-objects/device-system.value-object.ts b/packages/domain/src/value-objects/device-system.value-object.ts index 228c9b22..850920cf 100644 --- a/packages/domain/src/value-objects/device-system.value-object.ts +++ b/packages/domain/src/value-objects/device-system.value-object.ts @@ -4,6 +4,7 @@ import type { ValueOf } from '@agnoc/toolkit'; /** Describes the device system properties. */ export interface DeviceSystemProps { type: number; + serialNumber: string; } /** Describes the device system. */ @@ -13,6 +14,11 @@ export class DeviceSystem extends ValueObject { return this.props.type; } + /** Returns the device serial number. */ + get serialNumber(): string { + return this.props.serialNumber; + } + /** Returns the device model. */ get model(): DeviceModel { return DeviceType[this.props.type as DeviceType] || DeviceModel.UNKNOWN; @@ -29,8 +35,14 @@ export class DeviceSystem extends ValueObject { } protected validate(props: DeviceSystemProps): void { - this.validateDefinedProp(props, 'type'); + const keys: (keyof DeviceSystemProps)[] = ['type', 'serialNumber']; + + keys.forEach((prop) => { + this.validateDefinedProp(props, prop); + }); + this.validateNumberProp(props, 'type'); + this.validateTypeProp(props, 'serialNumber', 'string'); } } diff --git a/packages/toolkit/src/adapters/memory.adapter.test.ts b/packages/toolkit/src/adapters/memory.adapter.test.ts new file mode 100644 index 00000000..7717c9ef --- /dev/null +++ b/packages/toolkit/src/adapters/memory.adapter.test.ts @@ -0,0 +1,39 @@ +import { expect } from 'chai'; +import { ID } from '../domain-primitives/id.domain-primitive'; +import { MemoryAdapter } from './memory.adapter'; + +describe('MemoryAdapter', function () { + it('should get and set data', function () { + const adapter = new MemoryAdapter(); + const id = ID.generate(); + const data = { foo: 'bar' }; + + adapter.set(id, data); + + expect(adapter.get(id)).to.equal(data); + }); + + it('should delete data', function () { + const adapter = new MemoryAdapter(); + const id = ID.generate(); + const data = { foo: 'bar' }; + + adapter.set(id, data); + adapter.delete(id); + + expect(adapter.get(id)).to.be.undefined; + }); + + it('should get all data', function () { + const adapter = new MemoryAdapter(); + const id1 = ID.generate(); + const id2 = ID.generate(); + const data1 = { foo: 'bar' }; + const data2 = { foo: 'baz' }; + + adapter.set(id1, data1); + adapter.set(id2, data2); + + expect(adapter.getAll()).to.deep.equal([data1, data2]); + }); +}); diff --git a/packages/toolkit/src/adapters/memory.adapter.ts b/packages/toolkit/src/adapters/memory.adapter.ts new file mode 100644 index 00000000..d4f4177c --- /dev/null +++ b/packages/toolkit/src/adapters/memory.adapter.ts @@ -0,0 +1,22 @@ +import { Adapter } from '../base-classes/adapter.base'; +import type { ID } from '../domain-primitives/id.domain-primitive'; + +export class MemoryAdapter extends Adapter { + private readonly data = new Map(); + + get(id: ID): unknown { + return this.data.get(id.value); + } + + set(id: ID, value: unknown): void { + this.data.set(id.value, value); + } + + delete(id: ID): void { + this.data.delete(id.value); + } + + getAll(): unknown[] { + return Array.from(this.data.values()); + } +} diff --git a/packages/toolkit/src/base-classes/adapter.base.ts b/packages/toolkit/src/base-classes/adapter.base.ts new file mode 100644 index 00000000..f44aa03e --- /dev/null +++ b/packages/toolkit/src/base-classes/adapter.base.ts @@ -0,0 +1,8 @@ +import type { ID } from '../domain-primitives/id.domain-primitive'; + +export abstract class Adapter { + abstract getAll(): unknown[]; + abstract get(id: ID): unknown; + abstract set(id: ID, value: unknown): void; + abstract delete(id: ID): void; +} diff --git a/packages/toolkit/src/base-classes/repository.base.ts b/packages/toolkit/src/base-classes/repository.base.ts new file mode 100644 index 00000000..e2a98e69 --- /dev/null +++ b/packages/toolkit/src/base-classes/repository.base.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/require-await */ +import type { Adapter } from './adapter.base'; +import type { Entity, EntityProps } from './entity.base'; +import type { ID } from '../domain-primitives/id.domain-primitive'; + +export abstract class Repository> { + constructor(private readonly adapter: Adapter) {} + + async findOneById(id: ID): Promise { + return this.adapter.get(id) as T | undefined; + } + + async findAll(): Promise { + return this.adapter.getAll() as T[]; + } + + async saveOne(entity: T): Promise { + this.adapter.set(entity.id, entity); + } + + async deleteOne(entity: T): Promise { + this.adapter.delete(entity.id); + } +} diff --git a/packages/toolkit/src/event-handler.manager.test.ts b/packages/toolkit/src/event-handler.manager.test.ts deleted file mode 100644 index 9aad3414..00000000 --- a/packages/toolkit/src/event-handler.manager.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { imock, instance, verify } from '@johanblumenberg/ts-mockito'; -import { EventHandlerManager } from './event-handler.manager'; -import type { EventHandler } from './base-classes/event-handler.base'; - -describe('EventHandlerManager', function () { - let eventHandler: EventHandler; - - beforeEach(function () { - eventHandler = imock(); - }); - - it('should listen up event handlers', function () { - new EventHandlerManager([instance(eventHandler), instance(eventHandler)]); - - verify(eventHandler.listen()).twice(); - }); -}); diff --git a/packages/toolkit/src/event-handler.manager.ts b/packages/toolkit/src/event-handler.manager.ts deleted file mode 100644 index 81c9cf67..00000000 --- a/packages/toolkit/src/event-handler.manager.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { EventHandler } from './base-classes/event-handler.base'; - -/** Manages event handlers. */ -export class EventHandlerManager { - constructor(private readonly eventHandlers: EventHandler[]) { - this.addListeners(); - } - - private addListeners() { - this.eventHandlers.forEach((eventHandler) => { - eventHandler.listen(); - }); - } -} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 88dcd1a5..e63a3642 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -1,9 +1,11 @@ +export * from './adapters/memory.adapter'; +export * from './base-classes/adapter.base'; export * from './base-classes/domain-primitive.base'; export * from './base-classes/entity.base'; -export * from './base-classes/event-handler.base'; export * from './base-classes/exception.base'; export * from './base-classes/factory.base'; export * from './base-classes/mapper.base'; +export * from './base-classes/repository.base'; export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; export * from './decorators/bind.decorator'; @@ -31,4 +33,3 @@ export * from './utils/is-present.util'; export * from './utils/stream.util'; export * from './utils/to-stream.util'; export * from './utils/wait-for.util'; -export * from './event-handler.manager'; diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts index 6bec7810..535ecfd2 100644 --- a/packages/transport-tcp/src/index.ts +++ b/packages/transport-tcp/src/index.ts @@ -10,11 +10,10 @@ export * from './domain-primitives/packet-sequence.domain-primitive'; export * from './factories/packet.factory'; export * from './mappers/packet.mapper'; export * from './mappers/payload.mapper'; -export * from './packet.event-bus'; -export * from './packet.event-handler'; export * from './packet.server'; export * from './packet.socket'; export * from './services/payload-object-parser.service'; +export * from './time-sync.server'; export * from './utils/get-custom-decoders.util'; export * from './utils/get-protobuf-root.util'; export * from './value-objects/packet.value-object'; diff --git a/packages/transport-tcp/src/packet.event-bus.ts b/packages/transport-tcp/src/packet.event-bus.ts deleted file mode 100644 index bf74c3d4..00000000 --- a/packages/transport-tcp/src/packet.event-bus.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TypedEmitter } from 'tiny-typed-emitter'; -import type { PayloadObjectName } from './constants/payloads.constant'; -import type { PacketSocket } from './packet.socket'; -import type { Packet } from './value-objects/packet.value-object'; - -/** Parameter for the packet event bus event. */ -export type PacketEventBusEventParameter = { - packet: Packet; - socket: PacketSocket; -}; - -/** Events for the packet event bus. */ -export type PacketEventBusEvents = { - [Name in PayloadObjectName]: ({ packet, socket }: PacketEventBusEventParameter) => void; -}; - -/** Event bus for packets. */ -export class PacketEventBus extends TypedEmitter {} diff --git a/packages/transport-tcp/src/packet.event-handler.test.ts b/packages/transport-tcp/src/packet.event-handler.test.ts deleted file mode 100644 index 8d7384ac..00000000 --- a/packages/transport-tcp/src/packet.event-handler.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { anything, imock, instance, spy, verify, capture } from '@johanblumenberg/ts-mockito'; -import { expect } from 'chai'; -import { PacketEventHandler } from './packet.event-handler'; -import type { PacketEventBus } from './packet.event-bus'; -import type { PacketEventHandleParameter } from './packet.event-handler'; - -describe('PacketEventHandler', function () { - let packetEventBus: PacketEventBus; - let dummyPacketEventHandler: DummyPacketEventHandler; - let dummyPacketEventHandlerSpy: DummyPacketEventHandler; - - beforeEach(function () { - packetEventBus = imock(); - dummyPacketEventHandler = new DummyPacketEventHandler(instance(packetEventBus)); - dummyPacketEventHandlerSpy = spy(dummyPacketEventHandler); - }); - - it('should listen for events on the bus', function () { - dummyPacketEventHandler.listen(); - - verify(packetEventBus.on('DEVICE_GETTIME_RSP', anything())).once(); - }); - - it('should call handle when event is emitted', function () { - const eventParameter: PacketEventHandleParameter = instance(imock()); - - dummyPacketEventHandler.listen(); - - const [eventName, callback] = capture(packetEventBus.on<'DEVICE_GETTIME_RSP'>).first(); - - expect(eventName).to.equal('DEVICE_GETTIME_RSP'); - - callback(eventParameter); - - verify(dummyPacketEventHandlerSpy.handle(eventParameter)).once(); - }); -}); - -class DummyPacketEventHandler extends PacketEventHandler { - eventName = 'DEVICE_GETTIME_RSP' as const; - handle(_: PacketEventHandleParameter) { - // noop - } -} diff --git a/packages/transport-tcp/src/packet.event-handler.ts b/packages/transport-tcp/src/packet.event-handler.ts deleted file mode 100644 index 6f87aa73..00000000 --- a/packages/transport-tcp/src/packet.event-handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PayloadObjectName } from './constants/payloads.constant'; -import type { PacketEventBusEventParameter, PacketEventBus } from './packet.event-bus'; -import type { EventHandler } from '@agnoc/toolkit'; - -export type PacketEventHandleParameter = PacketEventBusEventParameter; - -/** Base class for packet event handlers. */ -export abstract class PacketEventHandler implements EventHandler { - constructor(private readonly eventBus: PacketEventBus) {} - - listen(): void { - this.eventBus.on(this.eventName, (arg) => { - void this.handle(arg as PacketEventHandleParameter); - }); - } - - /** The name of the event to listen to. */ - abstract eventName: PayloadObjectName; - - /** Handle the event. */ - abstract handle(arg: PacketEventHandleParameter): Promise | void; -} diff --git a/packages/transport-tcp/src/packet.server.ts b/packages/transport-tcp/src/packet.server.ts index 6275d7c3..275f71f3 100644 --- a/packages/transport-tcp/src/packet.server.ts +++ b/packages/transport-tcp/src/packet.server.ts @@ -7,7 +7,7 @@ import type { AddressInfo, ListenOptions, Socket, DropArgument } from 'net'; /** Events emitted by the {@link PacketServer}. */ export interface PacketServerEvents { /** Emits a {@link PacketSocket} when a new connection is established. */ - connection: (socket: PacketSocket) => void; + connection: (socket: PacketSocket) => void | Promise; /** Emits an error when an error occurs. */ error: (err: Error) => void; /** Emits when the server has been bound after calling `server.listen`. */ diff --git a/packages/transport-tcp/src/packet.socket.test.ts b/packages/transport-tcp/src/packet.socket.test.ts index c9d092ae..f4ef43da 100644 --- a/packages/transport-tcp/src/packet.socket.test.ts +++ b/packages/transport-tcp/src/packet.socket.test.ts @@ -10,7 +10,7 @@ import { Packet } from './value-objects/packet.value-object'; import type { PacketMapper } from './mappers/packet.mapper'; import type { AddressInfo } from 'net'; -describe('packet.socket', function () { +describe('PacketSocket', function () { let packetMapper: PacketMapper; let packetSocket: PacketSocket; let server: Server; @@ -35,6 +35,9 @@ describe('packet.socket', function () { expect(packetSocket.localPort).to.be.undefined; expect(packetSocket.remoteAddress).to.be.undefined; expect(packetSocket.remotePort).to.be.undefined; + expect(packetSocket.toString()).to.be.equal('unknown:0::unknown:0'); + expect(packetSocket.connecting).to.be.false; + expect(packetSocket.connected).to.be.false; }); it('should connect to a server', function (done) { @@ -42,6 +45,7 @@ describe('packet.socket', function () { void packetSocket.connect((server.address() as AddressInfo).port); expect(packetSocket.connecting).to.be.true; + expect(packetSocket.connected).to.be.false; }); packetSocket.once('connect', () => { @@ -49,8 +53,10 @@ describe('packet.socket', function () { expect(packetSocket.localPort).to.be.a('number'); expect(packetSocket.remoteAddress).to.be.a('string'); expect(packetSocket.remotePort).to.be.a('number'); + expect(packetSocket.toString()).to.be.not.equal('unknown:0::unknown:0'); expect(packetSocket.connecting).to.be.false; - packetSocket.end(); + expect(packetSocket.connected).to.be.true; + void packetSocket.end(); }); packetSocket.once('end', done); @@ -77,7 +83,7 @@ describe('packet.socket', function () { }); packetSocket.once('connect', () => { - packetSocket.end(packet); + void packetSocket.end(packet); }); server.listen(0); @@ -140,7 +146,7 @@ describe('packet.socket', function () { done(); }); - packetSocket.write(packet); + void packetSocket.write(packet); }); it('should not try to end when is not connected', function (done) { @@ -150,7 +156,7 @@ describe('packet.socket', function () { done(); }); - packetSocket.end(); + void packetSocket.end(); }); it('should throw an error when unable to map the packet', function (done) { @@ -217,7 +223,7 @@ describe('packet.socket', function () { server.once('connection', (socket) => { const packetSocketServer = new PacketSocket(instance(packetMapper), socket); - packetSocketServer.end(packet); + void packetSocketServer.end(packet); }); packetSocket.once('data', (data) => { @@ -237,7 +243,7 @@ describe('packet.socket', function () { }); packetSocket.once('connect', () => { - packetSocket.end(); + void packetSocket.end(); }); packetSocket.once('end', done); @@ -251,7 +257,7 @@ describe('packet.socket', function () { }); packetSocket.once('connect', () => { - packetSocket.end(); + void packetSocket.end(); }); packetSocket.once('end', done); @@ -267,7 +273,7 @@ describe('packet.socket', function () { }); packetSocket.once('connect', () => { - packetSocket.end(); + void packetSocket.end(); }); packetSocket.once('end', done); @@ -275,6 +281,176 @@ describe('packet.socket', function () { server.listen(0); }); }); + + describe('#write()', function () { + it('should write packets with encoding and callback', function (done) { + const buffer = givenAPacketBuffer(); + const packet = new Packet(givenSomePacketProps()); + + when(packetMapper.fromDomain(anything())).thenReturn(buffer); + + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + server.once('connection', (socket) => { + socket.once('data', (data) => { + expect(Buffer.compare(data, buffer)).to.be.equal(0); + verify(packetMapper.fromDomain(packet)).once(); + socket.end(); + }); + }); + + packetSocket.once('connect', () => { + packetSocket.write(packet, 'utf8', done); + }); + + server.listen(0); + }); + + it('should write packets with callback', function (done) { + const buffer = givenAPacketBuffer(); + const packet = new Packet(givenSomePacketProps()); + + when(packetMapper.fromDomain(anything())).thenReturn(buffer); + + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + server.once('connection', (socket) => { + socket.once('data', (data) => { + expect(Buffer.compare(data, buffer)).to.be.equal(0); + verify(packetMapper.fromDomain(packet)).once(); + socket.end(); + }); + }); + + packetSocket.once('connect', () => { + packetSocket.write(packet, done); + }); + + server.listen(0); + }); + + it('should write packets as a promise', function (done) { + const buffer = givenAPacketBuffer(); + const packet = new Packet(givenSomePacketProps()); + + when(packetMapper.fromDomain(anything())).thenReturn(buffer); + + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + server.once('connection', (socket) => { + socket.once('data', (data) => { + expect(Buffer.compare(data, buffer)).to.be.equal(0); + verify(packetMapper.fromDomain(packet)).once(); + socket.end(); + }); + }); + + packetSocket.once('connect', async () => { + await packetSocket.write(packet); + done(); + }); + + server.listen(0); + }); + + it('should reject the write when it fails', function (done) { + const packet = new Packet(givenSomePacketProps()); + + packetSocket.once('error', () => { + // noop + }); + + packetSocket.write(packet).catch((error) => { + expect(error).to.be.an.instanceOf(DomainException); + expect((error as Error).message).to.be.equal('Socket is not connected'); + done(); + }); + }); + }); + + describe('#end()', function () { + it('should end socket with packet and callback', function (done) { + const buffer = givenAPacketBuffer(); + const packet = new Packet(givenSomePacketProps()); + + when(packetMapper.fromDomain(anything())).thenReturn(buffer); + + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + server.once('connection', (socket) => { + socket.once('data', (data) => { + expect(Buffer.compare(data, buffer)).to.be.equal(0); + verify(packetMapper.fromDomain(packet)).once(); + socket.end(); + }); + }); + + packetSocket.once('connect', () => { + packetSocket.end(packet, done); + }); + + server.listen(0); + }); + + it('should end socket with callback', function (done) { + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + packetSocket.once('connect', () => { + packetSocket.end(done); + }); + + server.listen(0); + }); + + it('should end socket with packet as a promise', function (done) { + const buffer = givenAPacketBuffer(); + const packet = new Packet(givenSomePacketProps()); + + when(packetMapper.fromDomain(anything())).thenReturn(buffer); + + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + server.once('connection', (socket) => { + socket.once('data', (data) => { + expect(Buffer.compare(data, buffer)).to.be.equal(0); + verify(packetMapper.fromDomain(packet)).once(); + socket.end(); + }); + }); + + packetSocket.once('connect', async () => { + await packetSocket.end(packet); + done(); + }); + + server.listen(0); + }); + + it('should end socket as a promise', function (done) { + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + packetSocket.once('connect', async () => { + await packetSocket.end(); + done(); + }); + + server.listen(0); + }); + }); }); function givenAPacketBuffer() { diff --git a/packages/transport-tcp/src/packet.socket.ts b/packages/transport-tcp/src/packet.socket.ts index 1864ad0a..99f2d6e9 100644 --- a/packages/transport-tcp/src/packet.socket.ts +++ b/packages/transport-tcp/src/packet.socket.ts @@ -1,6 +1,6 @@ import { Socket } from 'net'; import { Duplex } from 'stream'; -import { DomainException } from '@agnoc/toolkit'; +import { debug, DomainException } from '@agnoc/toolkit'; import type { PacketMapper } from './mappers/packet.mapper'; import type { Packet } from './value-objects/packet.value-object'; import type { SocketConnectOpts } from 'net'; @@ -8,7 +8,7 @@ import type { SocketConnectOpts } from 'net'; /** Events emitted by the {@link PacketSocket}. */ export interface PacketSocketEvents { /** Emits a {@link Packet} when `data` is received. */ - data: (packet: Packet) => void; + data: (packet: Packet) => void | Promise; connect: () => void; close: (hasError: boolean) => void; drain: () => void; @@ -22,6 +22,7 @@ export interface PacketSocketEvents { /** Socket that parses and serializes packets sent through a socket. */ export class PacketSocket extends Duplex { + private readonly debug = (msg: string, ...args: string[]) => debug(__filename).extend(this.toString())(msg, ...args); private socket?: Socket; private readingPaused = false; @@ -30,6 +31,7 @@ export class PacketSocket extends Duplex { if (socket) { this.wrapSocket(socket); + this.debug('new socket'); } } @@ -38,12 +40,12 @@ export class PacketSocket extends Duplex { connect(port: number, host: string): Promise; connect(path: string): Promise; connect(options: SocketConnectOpts): Promise; - connect(portOrPathOrOptions: number | string | SocketConnectOpts, host?: string): Promise { + async connect(portOrPathOrOptions: number | string | SocketConnectOpts, host?: string): Promise { const socket = new Socket(); this.wrapSocket(socket); - return new Promise((resolve) => { + await new Promise((resolve) => { if (typeof portOrPathOrOptions === 'number' && host) { return socket.connect(portOrPathOrOptions, host, resolve); } @@ -59,6 +61,60 @@ export class PacketSocket extends Duplex { return socket.connect(portOrPathOrOptions, resolve); }); + + this.debug('connected'); + } + + /** Writes a packet to the socket. */ + override write(packet: Packet, encoding: BufferEncoding, cb: (error: Error | null | undefined) => void): boolean; + override write(packet: Packet, cb: WriteCallback): boolean; + override write(packet: Packet): Promise; + override write( + packet: Packet, + encodingOrCb?: BufferEncoding | WriteCallback, + cb?: WriteCallback, + ): boolean | Promise { + if (cb) { + return super.write(packet, encodingOrCb as BufferEncoding, cb); + } + + if (encodingOrCb) { + return super.write(packet, encodingOrCb as WriteCallback); + } + + return new Promise((resolve, reject) => { + super.write(packet, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); + } + + /** Closes the socket. */ + override end(packet: Packet, cb: EndCallback): this; + override end(packet: Packet): Promise; + override end(cb: EndCallback): this; + override end(): Promise; + override end(packetOrCb?: Packet | EndCallback, cb?: EndCallback): this | Promise { + if (cb) { + return super.end(packetOrCb as Packet, cb); + } + + if (typeof packetOrCb === 'function') { + return super.end(packetOrCb); + } + + return new Promise((resolve) => { + if (packetOrCb) { + super.end(packetOrCb, resolve); + return; + } + + super.end(resolve); + }); } /** Returns the local address of the socket. */ @@ -86,6 +142,19 @@ export class PacketSocket extends Duplex { return Boolean(this.socket?.connecting); } + /** Returns whether the socket is connected. */ + get connected(): boolean { + return this.socket?.readyState === 'open'; + } + + /** Returns an string representation of the socket addresses. */ + override toString(): string { + const remoteAddress = `${this.remoteAddress ?? 'unknown'}:${this.remotePort ?? 0}`; + const localAddress = `${this.localAddress ?? 'unknown'}:${this.localPort ?? 0}`; + + return `${remoteAddress}::${localAddress}`; + } + private wrapSocket(socket: Socket): void { this.socket = socket; this.socket.on('close', (hadError) => this.emit('close', hadError)); @@ -137,6 +206,8 @@ export class PacketSocket extends Duplex { return; } + this.debug('received packet', packet.toString()); + const pushOk = this.push(packet); /* istanbul ignore if - unable to test */ @@ -151,7 +222,7 @@ export class PacketSocket extends Duplex { setImmediate(this.onReadable.bind(this)); } - override _write(packet: Packet, _: BufferEncoding, done: Callback): void { + override _write(packet: Packet, _: BufferEncoding, done: InternalCallback): void { if (!this.socket) { done(new DomainException('Socket is not connected')); return; @@ -159,27 +230,34 @@ export class PacketSocket extends Duplex { const buffer = this.packetMapper.fromDomain(packet); + this.debug('sending packet', packet.toString()); this.socket.write(buffer, done); } - override _final(done: Callback): void { + override _final(done: InternalCallback): void { if (!this.socket) { done(new DomainException('Socket is not connected')); return; } + this.debug('closing socket'); this.socket.end(done); } } -export type Callback = (error?: Error | null) => void; +export type InternalCallback = (error?: Error | null) => void; +export type WriteCallback = (error: Error | null | undefined) => void; +export type EndCallback = () => void; export declare interface PacketSocket extends Duplex { emit(event: U, ...args: Parameters): boolean; on(event: U, listener: PacketSocketEvents[U]): this; once(event: U, listener: PacketSocketEvents[U]): this; - write(packet: Packet, encoding?: BufferEncoding, cb?: (error: Error | null | undefined) => void): boolean; - write(packet: Packet, cb?: (error: Error | null | undefined) => void): boolean; - end(cb?: () => void): this; - end(packet: Packet, cb?: () => void): this; + write(packet: Packet, encoding: BufferEncoding, cb: WriteCallback): boolean; + write(packet: Packet, cb: WriteCallback): boolean; + write(packet: Packet): Promise; + end(packet: Packet, cb: EndCallback): this; + end(packet: Packet): Promise; + end(cb: EndCallback): this; + end(): Promise; } diff --git a/packages/transport-tcp/src/time-sync.server.test.ts b/packages/transport-tcp/src/time-sync.server.test.ts new file mode 100644 index 00000000..07a021ab --- /dev/null +++ b/packages/transport-tcp/src/time-sync.server.test.ts @@ -0,0 +1,72 @@ +import { ID } from '@agnoc/toolkit'; +import { + anything, + capture, + deepEqual, + greaterThanOrEqual, + imock, + instance, + verify, + when, +} from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { givenSomePacketProps } from './test-support'; +import { TimeSyncServer } from './time-sync.server'; +import { Packet } from './value-objects/packet.value-object'; +import type { PacketFactory } from './factories/packet.factory'; +import type { PacketServer } from './packet.server'; +import type { PacketSocket } from './packet.socket'; + +describe('TimeSyncServer', function () { + let packetServer: PacketServer; + let packetFactory: PacketFactory; + let packetSocket: PacketSocket; + let rtpPacketServer: TimeSyncServer; + + beforeEach(function () { + packetServer = imock(); + packetFactory = imock(); + packetSocket = imock(); + rtpPacketServer = new TimeSyncServer(instance(packetServer), instance(packetFactory)); + }); + + it('should handle new connections', async function () { + const packet = new Packet(givenSomePacketProps()); + const now = Math.floor(Date.now() / 1000); + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + + verify(packetServer.on('connection', anything())).once(); + + const [event, handler] = capture(packetServer.on<'connection'>).first(); + + expect(event).to.be.equal('connection'); + + await handler(instance(packetSocket)); + + verify( + packetFactory.create( + 'DEVICE_TIME_SYNC_RSP', + deepEqual({ result: 0, body: { time: greaterThanOrEqual(now) } }), + deepEqual({ userId: new ID(0), deviceId: new ID(0) }), + ), + ).once(); + verify(packetSocket.end(packet)).once(); + }); + + describe('#listen()', function () { + it('should listen on port 4050', async function () { + await rtpPacketServer.listen(); + + verify(packetServer.listen(4050)).once(); + }); + }); + + describe('#close()', function () { + it('should close the server', async function () { + await rtpPacketServer.close(); + + verify(packetServer.close()).once(); + }); + }); +}); diff --git a/packages/transport-tcp/src/time-sync.server.ts b/packages/transport-tcp/src/time-sync.server.ts new file mode 100644 index 00000000..88ff0b36 --- /dev/null +++ b/packages/transport-tcp/src/time-sync.server.ts @@ -0,0 +1,33 @@ +import { ID } from '@agnoc/toolkit'; +import type { PacketFactory } from './factories/packet.factory'; +import type { PacketServer } from './packet.server'; +import type { PacketSocket } from './packet.socket'; + +/** Device time synchronization server implementation. */ +export class TimeSyncServer { + constructor(private readonly packetServer: PacketServer, private readonly packetFactory: PacketFactory) { + this.addListeners(); + } + + /** Start listening for incoming connections. */ + async listen(): Promise { + return this.packetServer.listen(4050); + } + + /** Close the server. */ + async close(): Promise { + return this.packetServer.close(); + } + + private async handleConnection(socket: PacketSocket) { + const props = { userId: new ID(0), deviceId: new ID(0) }; + const payload = { result: 0, body: { time: Math.floor(Date.now() / 1000) } }; + const packet = this.packetFactory.create('DEVICE_TIME_SYNC_RSP', payload, props); + + return socket.end(packet); + } + + private addListeners() { + this.packetServer.on('connection', this.handleConnection.bind(this)); + } +} From 3a29e37b1020342d4b4d8424f4cf551ca6ccbf09 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sat, 18 Mar 2023 23:22:35 +0100 Subject: [PATCH 05/38] feat: add domain events --- .ncurc.json | 2 +- packages/adapter-tcp/package.json | 1 + .../adapter-tcp/src/connection.manager.ts | 65 ++++++++++++++++ .../src/event-handler.manager.test.ts | 11 ++- .../adapter-tcp/src/event-handler.manager.ts | 8 +- .../client-heartbeat.event-handler.ts | 9 --- .../client-login.event-handler.ts | 18 ----- ...ice-clean-map-data-report.event-handler.ts | 11 --- .../device-clean-map-report.event-handler.ts | 11 --- .../device-clean-task-report.event-handler.ts | 9 --- ...s-connected-event-handler.event-handler.ts | 19 +++++ ...e-is-locked-event-handler.event-handler.ts | 32 ++++++++ .../client-heartbeat.event-handler.ts | 10 +++ .../client-login.event-handler.ts | 26 +++++++ .../device-battery-update.event-handler.ts | 9 ++- ...ice-clean-map-data-report.event-handler.ts | 12 +++ .../device-clean-map-report.event-handler.ts | 12 +++ .../device-clean-task-report.event-handler.ts | 10 +++ .../device-locked.event-handler.ts | 20 +++++ .../device-offline.event-handler.ts | 15 ++++ .../device-register.event-handler.ts | 7 +- .../device-settings-update.event-handler.ts | 9 ++- .../device-version-update.event-handler.ts | 7 +- .../adapter-tcp/src/packet.event-bus.test.ts | 4 +- packages/adapter-tcp/src/packet.event-bus.ts | 6 +- .../adapter-tcp/src/packet.event-handler.ts | 6 +- packages/adapter-tcp/src/packet.message.ts | 2 +- .../src/time-sync.server.test.ts | 8 +- .../src/time-sync.server.ts | 4 +- .../domain/src/entities/device.entity.test.ts | 4 +- packages/domain/src/entities/device.entity.ts | 32 +++++++- .../event-handlers/domain.event-handler.ts | 11 +++ .../device-connected.domain-event.test.ts | 11 +++ .../events/device-connected.domain-event.ts | 8 ++ .../src/events/device-locked.domain-event.ts | 8 ++ packages/domain/src/events/index.ts | 9 +++ packages/domain/src/index.ts | 4 + .../repositories/device.repository.test.ts | 6 +- packages/eslint-config/typescript.js | 1 + packages/toolkit/package.json | 2 +- .../base-classes/aggregate-root.base.test.ts | 72 ++++++++++++++++++ .../src/base-classes/aggregate-root.base.ts | 33 ++++++++ .../base-classes/domain-event.base.test.ts | 21 +++++ .../src/base-classes/domain-event.base.ts | 16 ++++ .../toolkit/src/base-classes/entity.base.ts | 2 +- .../src/base-classes/event-bus.base.test.ts | 47 ++++++++++++ .../src/base-classes/event-bus.base.ts | 5 ++ .../src/base-classes}/event-handler.base.ts | 1 + .../src/base-classes/repository.base.test.ts | 76 +++++++++++++++++++ .../src/base-classes/repository.base.ts | 10 ++- .../src/event-buses/domain.event-bus.test.ts | 15 ++++ .../src/event-buses/domain.event-bus.ts | 6 ++ packages/toolkit/src/index.ts | 5 ++ packages/transport-tcp/package.json | 2 +- .../src/constants/payloads.constant.ts | 2 + packages/transport-tcp/src/index.ts | 1 - .../src/mappers/payload.mapper.test.ts | 4 +- .../src/mappers/payload.mapper.ts | 2 +- .../transport-tcp/src/packet.server.test.ts | 12 +-- packages/transport-tcp/src/packet.server.ts | 24 +++--- yarn.lock | 5 ++ 61 files changed, 684 insertions(+), 136 deletions(-) create mode 100644 packages/adapter-tcp/src/connection.manager.ts delete mode 100644 packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts delete mode 100644 packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts delete mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts delete mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts delete mode 100644 packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts rename packages/adapter-tcp/src/event-handlers/{ => packet-event-handlers}/device-battery-update.event-handler.ts (59%) create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts rename packages/adapter-tcp/src/event-handlers/{ => packet-event-handlers}/device-register.event-handler.ts (76%) rename packages/adapter-tcp/src/event-handlers/{ => packet-event-handlers}/device-settings-update.event-handler.ts (80%) rename packages/adapter-tcp/src/event-handlers/{ => packet-event-handlers}/device-version-update.event-handler.ts (65%) rename packages/{transport-tcp => adapter-tcp}/src/time-sync.server.test.ts (86%) rename packages/{transport-tcp => adapter-tcp}/src/time-sync.server.ts (84%) create mode 100644 packages/domain/src/event-handlers/domain.event-handler.ts create mode 100644 packages/domain/src/events/device-connected.domain-event.test.ts create mode 100644 packages/domain/src/events/device-connected.domain-event.ts create mode 100644 packages/domain/src/events/device-locked.domain-event.ts create mode 100644 packages/domain/src/events/index.ts create mode 100644 packages/toolkit/src/base-classes/aggregate-root.base.test.ts create mode 100644 packages/toolkit/src/base-classes/aggregate-root.base.ts create mode 100644 packages/toolkit/src/base-classes/domain-event.base.test.ts create mode 100644 packages/toolkit/src/base-classes/domain-event.base.ts create mode 100644 packages/toolkit/src/base-classes/event-bus.base.test.ts create mode 100644 packages/toolkit/src/base-classes/event-bus.base.ts rename packages/{adapter-tcp/src => toolkit/src/base-classes}/event-handler.base.ts (72%) create mode 100644 packages/toolkit/src/base-classes/repository.base.test.ts create mode 100644 packages/toolkit/src/event-buses/domain.event-bus.test.ts create mode 100644 packages/toolkit/src/event-buses/domain.event-bus.ts diff --git a/.ncurc.json b/.ncurc.json index f8d203c5..dada7064 100644 --- a/.ncurc.json +++ b/.ncurc.json @@ -1,3 +1,3 @@ { - "reject": ["chalk", "execa"] + "reject": ["chalk", "execa", "emittery"] } diff --git a/packages/adapter-tcp/package.json b/packages/adapter-tcp/package.json index da7da00c..0eb9419b 100644 --- a/packages/adapter-tcp/package.json +++ b/packages/adapter-tcp/package.json @@ -71,6 +71,7 @@ "@agnoc/toolkit": "^0.18.0-next.0", "debug": "^4.3.4", "tiny-typed-emitter": "^2.1.0", + "emittery": "^0.13.1", "tslib": "^2.5.0" }, "devDependencies": { diff --git a/packages/adapter-tcp/src/connection.manager.ts b/packages/adapter-tcp/src/connection.manager.ts new file mode 100644 index 00000000..397691b1 --- /dev/null +++ b/packages/adapter-tcp/src/connection.manager.ts @@ -0,0 +1,65 @@ +import { DomainException } from '@agnoc/toolkit'; +import { DeviceConnection } from './device.connection'; +import { PacketMessage } from './packet.message'; +import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; +import type { DeviceRepository, Device } from '@agnoc/domain'; +import type { ID } from '@agnoc/toolkit'; +import type { PacketServer, PacketFactory, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; + +export class ConnectionManager { + private readonly connections = new Set(); + + constructor( + private readonly servers: PacketServer[], + private readonly packetEventBus: PacketEventBus, + private readonly packetFactory: PacketFactory, + private readonly deviceRepository: DeviceRepository, + ) { + this.addListeners(); + } + + findConnectionsByDeviceId(deviceId: ID): DeviceConnection[] { + return [...this.connections].filter((connection) => connection.device?.id.equals(deviceId)); + } + + private addListeners() { + this.servers.forEach((server) => { + server.on('connection', (socket) => { + const connection = new DeviceConnection(this.packetFactory, socket); + + this.connections.add(connection); + + connection.on('data', async (packet: Packet) => { + const event = packet.payload.opcode.name as PayloadObjectName; + const packetMessage = new PacketMessage(connection, packet) as PacketEventBusEvents[PayloadObjectName]; + + // Update the device on the connection if the device id has changed. + if (!packet.deviceId.equals(connection.device?.id)) { + connection.device = await this.findDeviceById(packet.deviceId); + } + + const count = this.packetEventBus.listenerCount(event); + + if (count === 0) { + throw new DomainException(`No event handler found for packet event '${event}'`); + } + + await this.packetEventBus.emit(event, packetMessage); + }); + + connection.on('close', () => { + connection.removeAllListeners(); + this.connections.delete(connection); + }); + }); + }); + } + + private async findDeviceById(id: ID): Promise { + if (id.value === 0) { + return undefined; + } + + return this.deviceRepository.findOneById(id); + } +} diff --git a/packages/adapter-tcp/src/event-handler.manager.test.ts b/packages/adapter-tcp/src/event-handler.manager.test.ts index f1e3f6b3..405bb43b 100644 --- a/packages/adapter-tcp/src/event-handler.manager.test.ts +++ b/packages/adapter-tcp/src/event-handler.manager.test.ts @@ -1,11 +1,10 @@ import { anything, capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { EventHandlerManager } from './event-handler.manager'; -import type { EventHandler } from './event-handler.base'; -import type { EventEmitter } from 'stream'; +import type { EventBus, EventHandler } from '@agnoc/toolkit'; describe('EventHandlerManager', function () { - let eventBus: EventEmitter; + let eventBus: EventBus; let eventHandler: EventHandler; let eventHandlerManager: EventHandlerManager; @@ -23,18 +22,18 @@ describe('EventHandlerManager', function () { verify(eventBus.on('event', anything())).once(); }); - it('should call handle when event is emitted', function () { + it('should call handle when event is emitted', async function () { const data = { foo: 'bar' }; when(eventHandler.eventName).thenReturn('event'); eventHandlerManager.register(instance(eventHandler)); - const [eventName, callback] = capture(eventBus.on).first(); + const [eventName, callback] = capture(eventBus.on<'event'>).first(); expect(eventName).to.equal('event'); - callback(data); + await callback(data); verify(eventHandler.handle(data)).once(); }); diff --git a/packages/adapter-tcp/src/event-handler.manager.ts b/packages/adapter-tcp/src/event-handler.manager.ts index 71fbd413..053739ab 100644 --- a/packages/adapter-tcp/src/event-handler.manager.ts +++ b/packages/adapter-tcp/src/event-handler.manager.ts @@ -1,17 +1,15 @@ -import type { EventHandler } from './event-handler.base'; -import type { EventEmitter } from 'stream'; +import type { EventBus, EventHandler } from '@agnoc/toolkit'; /** Manages event handlers. */ export class EventHandlerManager { - private readonly eventHandlers: EventHandler[] = []; - constructor(private readonly eventBus: EventEmitter) {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(private readonly eventBus: EventBus) {} register(...eventHandlers: EventHandler[]): void { eventHandlers.forEach((eventHandler) => this.addEventHandler(eventHandler)); } private addEventHandler(eventHandler: EventHandler): void { - this.eventHandlers.push(eventHandler); this.eventBus.on(eventHandler.eventName, eventHandler.handle.bind(eventHandler)); } } diff --git a/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts deleted file mode 100644 index 935612d5..00000000 --- a/packages/adapter-tcp/src/event-handlers/client-heartbeat.event-handler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; - -export class ClientHeartbeatEventHandler implements PacketEventHandler { - eventName = 'CLIENT_HEARTBEAT_REQ' as const; - - async handle(message: PacketEventHandleParameter): Promise { - await message.respond('CLIENT_HEARTBEAT_RSP', {}); - } -} diff --git a/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts deleted file mode 100644 index 5841808d..00000000 --- a/packages/adapter-tcp/src/event-handlers/client-login.event-handler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; - -export class ClientLoginEventHandler implements PacketEventHandler { - eventName = 'CLIENT_ONLINE_REQ' as const; - - async handle(message: PacketEventHandleParameter): Promise { - if (!message.device) { - const data = { - result: 12002, - reason: `Device not registered(devsn: ${message.packet.payload.object.deviceSerialNumber})`, - }; - - return message.respond('CLIENT_ONLINE_RSP', data); - } - - await message.respond('CLIENT_ONLINE_RSP', { result: 0 }); - } -} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts deleted file mode 100644 index 9d6f9014..00000000 --- a/packages/adapter-tcp/src/event-handlers/device-clean-map-data-report.event-handler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; - -export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { - eventName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ' as const; - - async handle(message: PacketEventHandleParameter): Promise { - const data = message.packet.payload.object; - - await message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { result: 0, cleanId: data.cleanId }); - } -} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts deleted file mode 100644 index 4f94547f..00000000 --- a/packages/adapter-tcp/src/event-handlers/device-clean-map-report.event-handler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; - -export class DeviceCleanMapReportEventHandler implements PacketEventHandler { - eventName = 'DEVICE_EVENT_REPORT_CLEANMAP' as const; - - async handle(message: PacketEventHandleParameter): Promise { - const data = message.packet.payload.object; - - await message.respond('DEVICE_EVENT_REPORT_RSP', { result: 0, body: { cleanId: data.cleanId } }); - } -} diff --git a/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts deleted file mode 100644 index beb59187..00000000 --- a/packages/adapter-tcp/src/event-handlers/device-clean-task-report.event-handler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; - -export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { - eventName = 'DEVICE_EVENT_REPORT_CLEANTASK' as const; - - async handle(message: PacketEventHandleParameter): Promise { - await message.respond('UNK_11A4', { unk1: 0 }); - } -} diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts new file mode 100644 index 00000000..06906d71 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -0,0 +1,19 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { ConnectionManager } from '../../connection.manager'; +import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; + +export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { + readonly eventName = 'DeviceConnectedDomainEvent'; + + constructor(private readonly connectionManager: ConnectionManager) {} + + handle(event: DeviceConnectedDomainEvent): Promise { + const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); + + if (!connection) { + throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); + } + + return connection.send('DEVICE_CONTROL_LOCK_REQ', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts new file mode 100644 index 00000000..661396b0 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -0,0 +1,32 @@ +import { DeviceCapability } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { ConnectionManager } from '../../connection.manager'; +import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; + +export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { + readonly eventName = 'DeviceLockedDomainEvent'; + + constructor(private readonly connectionManager: ConnectionManager) {} + + async handle(event: DeviceLockedDomainEvent): Promise { + const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); + + if (!connection || !connection.device) { + throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); + } + + await connection.send('DEVICE_STATUS_GETTING_REQ', {}); + await connection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { unk1: 0, unk2: '' }); + + // TODO: move this to a get time service. + await connection.send('DEVICE_GETTIME_REQ', {}); + + // TODO: move this to a get map service. + await connection.send('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', { + mask: connection.device.system.supports(DeviceCapability.MAP_PLANS) ? 0x78ff : 0xff, + }); + + // TODO: move this to a get wlan service. + await connection.send('DEVICE_WLAN_INFO_GETTING_REQ', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts new file mode 100644 index 00000000..8164fb3e --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts @@ -0,0 +1,10 @@ +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class ClientHeartbeatEventHandler implements PacketEventHandler { + readonly eventName = 'CLIENT_HEARTBEAT_REQ'; + + async handle(message: PacketMessage<'CLIENT_HEARTBEAT_REQ'>): Promise { + await message.respond('CLIENT_HEARTBEAT_RSP', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts new file mode 100644 index 00000000..2949a314 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts @@ -0,0 +1,26 @@ +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; + +export class ClientLoginEventHandler implements PacketEventHandler { + readonly eventName = 'CLIENT_ONLINE_REQ'; + + constructor(private readonly deviceRepository: DeviceRepository) {} + + async handle(message: PacketMessage<'CLIENT_ONLINE_REQ'>): Promise { + if (!message.device) { + const data = { + result: 12002, + reason: `Device not registered(devsn: ${message.packet.payload.object.deviceSerialNumber})`, + }; + + return message.respond('CLIENT_ONLINE_RSP', data); + } + + await message.respond('CLIENT_ONLINE_RSP', { result: 0 }); + + message.device.setAsConnected(); + + await this.deviceRepository.saveOne(message.device); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts similarity index 59% rename from packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts rename to packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts index 353711ce..84f682ae 100644 --- a/packages/adapter-tcp/src/event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts @@ -1,13 +1,14 @@ import { DomainException } from '@agnoc/toolkit'; -import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; +import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { - eventName = 'PUSH_DEVICE_BATTERY_INFO_REQ' as const; + readonly eventName = 'PUSH_DEVICE_BATTERY_INFO_REQ'; constructor(private readonly deviceBatteryMapper: DeviceBatteryMapper) {} - async handle(message: PacketEventHandleParameter): Promise { + async handle(message: PacketMessage<'PUSH_DEVICE_BATTERY_INFO_REQ'>): Promise { if (!message.device) { throw new DomainException('Device not found'); } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts new file mode 100644 index 00000000..993b6d35 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -0,0 +1,12 @@ +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; + + async handle(message: PacketMessage<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): Promise { + const data = message.packet.payload.object; + + await message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { result: 0, cleanId: data.cleanId }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts new file mode 100644 index 00000000..ed11e4f0 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -0,0 +1,12 @@ +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceCleanMapReportEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_EVENT_REPORT_CLEANMAP'; + + async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANMAP'>): Promise { + const data = message.packet.payload.object; + + await message.respond('DEVICE_EVENT_REPORT_RSP', { result: 0, body: { cleanId: data.cleanId } }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts new file mode 100644 index 00000000..27febb0a --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -0,0 +1,10 @@ +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_EVENT_REPORT_CLEANTASK'; + + async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANTASK'>): Promise { + await message.respond('UNK_11A4', { unk1: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts new file mode 100644 index 00000000..140ca53e --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts @@ -0,0 +1,20 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; + +export class DeviceLockedEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_CONTROL_LOCK_RSP'; + + constructor(private readonly deviceRepository: DeviceRepository) {} + + async handle(message: PacketMessage<'DEVICE_CONTROL_LOCK_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + message.device.setAsLocked(); + + await this.deviceRepository.saveOne(message.device); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts new file mode 100644 index 00000000..2ffaa07a --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts @@ -0,0 +1,15 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceOfflineEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_OFFLINE_CMD'; + + async handle(message: PacketMessage<'DEVICE_OFFLINE_CMD'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + return message.connection.send('DEVICE_CONTROL_LOCK_REQ', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts similarity index 76% rename from packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts rename to packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts index 8a0d8084..ef6c05ee 100644 --- a/packages/adapter-tcp/src/event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts @@ -1,14 +1,15 @@ import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; import { ID } from '@agnoc/toolkit'; -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceRegisterEventHandler implements PacketEventHandler { - eventName = 'DEVICE_REGISTER_REQ' as const; + readonly eventName = 'DEVICE_REGISTER_REQ'; constructor(private readonly deviceRepository: DeviceRepository) {} - async handle(message: PacketEventHandleParameter): Promise { + async handle(message: PacketMessage<'DEVICE_REGISTER_REQ'>): Promise { const data = message.packet.payload.object; const device = new Device({ id: ID.generate(), diff --git a/packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts similarity index 80% rename from packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts rename to packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts index 76c00e9a..6ece287b 100644 --- a/packages/adapter-tcp/src/event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts @@ -1,14 +1,15 @@ import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { DeviceVoiceMapper } from '../mappers/device-voice.mapper'; -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; +import type { DeviceVoiceMapper } from '../../mappers/device-voice.mapper'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { - eventName = 'PUSH_DEVICE_AGENT_SETTING_REQ' as const; + readonly eventName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; constructor(private readonly deviceVoiceMapper: DeviceVoiceMapper) {} - async handle(message: PacketEventHandleParameter): Promise { + async handle(message: PacketMessage<'PUSH_DEVICE_AGENT_SETTING_REQ'>): Promise { if (!message.device) { throw new DomainException('Device not found'); } diff --git a/packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts similarity index 65% rename from packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts rename to packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts index 4e0df525..a5897efa 100644 --- a/packages/adapter-tcp/src/event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts @@ -1,11 +1,12 @@ import { DeviceVersion } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler, PacketEventHandleParameter } from '../packet.event-handler'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; export class DeviceVersionUpdateEventHandler implements PacketEventHandler { - eventName = 'DEVICE_VERSION_INFO_UPDATE_REQ' as const; + readonly eventName = 'DEVICE_VERSION_INFO_UPDATE_REQ'; - async handle(message: PacketEventHandleParameter): Promise { + async handle(message: PacketMessage<'DEVICE_VERSION_INFO_UPDATE_REQ'>): Promise { if (!message.device) { throw new DomainException('Device not found'); } diff --git a/packages/adapter-tcp/src/packet.event-bus.test.ts b/packages/adapter-tcp/src/packet.event-bus.test.ts index 845b756f..b7128d90 100644 --- a/packages/adapter-tcp/src/packet.event-bus.test.ts +++ b/packages/adapter-tcp/src/packet.event-bus.test.ts @@ -1,11 +1,11 @@ +import { EventBus } from '@agnoc/toolkit'; import { expect } from 'chai'; -import { TypedEmitter } from 'tiny-typed-emitter'; import { PacketEventBus } from './packet.event-bus'; describe('PacketEventBus', function () { it('should be created', function () { const packetEventBus = new PacketEventBus(); - expect(packetEventBus).to.be.instanceOf(TypedEmitter); + expect(packetEventBus).to.be.instanceOf(EventBus); }); }); diff --git a/packages/adapter-tcp/src/packet.event-bus.ts b/packages/adapter-tcp/src/packet.event-bus.ts index 2f4e8135..9d8dec82 100644 --- a/packages/adapter-tcp/src/packet.event-bus.ts +++ b/packages/adapter-tcp/src/packet.event-bus.ts @@ -1,11 +1,11 @@ -import { TypedEmitter } from 'tiny-typed-emitter'; +import { EventBus } from '@agnoc/toolkit'; import type { PacketMessage } from './packet.message'; import type { PayloadObjectName } from '@agnoc/transport-tcp'; /** Events for the packet event bus. */ export type PacketEventBusEvents = { - [Name in PayloadObjectName]: (message: PacketMessage) => void; + [Name in PayloadObjectName]: PacketMessage; }; /** Event bus for packets. */ -export class PacketEventBus extends TypedEmitter {} +export class PacketEventBus extends EventBus {} diff --git a/packages/adapter-tcp/src/packet.event-handler.ts b/packages/adapter-tcp/src/packet.event-handler.ts index 73119016..9187f155 100644 --- a/packages/adapter-tcp/src/packet.event-handler.ts +++ b/packages/adapter-tcp/src/packet.event-handler.ts @@ -1,14 +1,12 @@ -import type { EventHandler } from './event-handler.base'; import type { PacketMessage } from './packet.message'; +import type { EventHandler } from '@agnoc/toolkit'; import type { PayloadObjectName } from '@agnoc/transport-tcp'; -export type PacketEventHandleParameter = PacketMessage; - /** Base class for packet event handlers. */ export abstract class PacketEventHandler implements EventHandler { /** The name of the event to listen to. */ abstract eventName: PayloadObjectName; /** Handle the event. */ - abstract handle(message: PacketEventHandleParameter): void; + abstract handle(message: PacketMessage): void; } diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts index b94c1ee7..9e780e71 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/packet.message.ts @@ -3,7 +3,7 @@ import type { Device } from '@agnoc/domain'; import type { Packet, PayloadObjectFrom, PayloadObjectName } from '@agnoc/transport-tcp'; export class PacketMessage { - constructor(private readonly connection: DeviceConnection, readonly packet: Packet) {} + constructor(readonly connection: DeviceConnection, readonly packet: Packet) {} get device(): Device | undefined { return this.connection.device; diff --git a/packages/transport-tcp/src/time-sync.server.test.ts b/packages/adapter-tcp/src/time-sync.server.test.ts similarity index 86% rename from packages/transport-tcp/src/time-sync.server.test.ts rename to packages/adapter-tcp/src/time-sync.server.test.ts index 07a021ab..00a67c97 100644 --- a/packages/transport-tcp/src/time-sync.server.test.ts +++ b/packages/adapter-tcp/src/time-sync.server.test.ts @@ -1,4 +1,6 @@ import { ID } from '@agnoc/toolkit'; +import { Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { anything, capture, @@ -10,12 +12,8 @@ import { when, } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { givenSomePacketProps } from './test-support'; import { TimeSyncServer } from './time-sync.server'; -import { Packet } from './value-objects/packet.value-object'; -import type { PacketFactory } from './factories/packet.factory'; -import type { PacketServer } from './packet.server'; -import type { PacketSocket } from './packet.socket'; +import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; describe('TimeSyncServer', function () { let packetServer: PacketServer; diff --git a/packages/transport-tcp/src/time-sync.server.ts b/packages/adapter-tcp/src/time-sync.server.ts similarity index 84% rename from packages/transport-tcp/src/time-sync.server.ts rename to packages/adapter-tcp/src/time-sync.server.ts index 88ff0b36..78ca0be6 100644 --- a/packages/transport-tcp/src/time-sync.server.ts +++ b/packages/adapter-tcp/src/time-sync.server.ts @@ -1,7 +1,5 @@ import { ID } from '@agnoc/toolkit'; -import type { PacketFactory } from './factories/packet.factory'; -import type { PacketServer } from './packet.server'; -import type { PacketSocket } from './packet.socket'; +import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; /** Device time synchronization server implementation. */ export class TimeSyncServer { diff --git a/packages/domain/src/entities/device.entity.test.ts b/packages/domain/src/entities/device.entity.test.ts index 192148c3..c6960f4d 100644 --- a/packages/domain/src/entities/device.entity.test.ts +++ b/packages/domain/src/entities/device.entity.test.ts @@ -1,4 +1,4 @@ -import { ArgumentInvalidException, ArgumentNotProvidedException, Entity } from '@agnoc/toolkit'; +import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; import { expect } from 'chai'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; @@ -36,7 +36,7 @@ describe('Device', function () { }; const device = new Device(deviceProps); - expect(device).to.be.instanceOf(Entity); + expect(device).to.be.instanceOf(AggregateRoot); expect(device.id).to.be.equal(deviceProps.id); expect(device.userId).to.be.equal(deviceProps.userId); expect(device.system).to.be.equal(deviceProps.system); diff --git a/packages/domain/src/entities/device.entity.ts b/packages/domain/src/entities/device.entity.ts index b10d277c..5d124c30 100644 --- a/packages/domain/src/entities/device.entity.ts +++ b/packages/domain/src/entities/device.entity.ts @@ -1,10 +1,12 @@ -import { Entity, ID } from '@agnoc/toolkit'; +import { AggregateRoot, ID } from '@agnoc/toolkit'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; import { DeviceMode } from '../domain-primitives/device-mode.domain-primitive'; import { DeviceState } from '../domain-primitives/device-state.domain-primitive'; import { DeviceWaterLevel } from '../domain-primitives/device-water-level.domain-primitive'; +import { DeviceConnectedDomainEvent } from '../events/device-connected.domain-event'; +import { DeviceLockedDomainEvent } from '../events/device-locked.domain-event'; import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; @@ -23,6 +25,10 @@ export interface DeviceProps extends EntityProps { system: DeviceSystem; /** The device version. */ version: DeviceVersion; + /** Whether the device is connected. */ + isConnected?: boolean; + /** Whether the device is locked. */ + isLocked?: boolean; /** The device settings. */ config?: DeviceSettings; /** The device current clean. */ @@ -54,7 +60,7 @@ export interface DeviceProps extends EntityProps { } /** Describes a device. */ -export class Device extends Entity { +export class Device extends AggregateRoot { /** Returns the user id. */ get userId(): ID { return this.props.userId; @@ -65,6 +71,16 @@ export class Device extends Entity { return this.props.system; } + /** Returns whether the device is connected. */ + get isConnected(): boolean { + return this.props.isConnected ?? false; + } + + /** Returns whether the device is locked. */ + get isLocked(): boolean { + return this.props.isLocked ?? false; + } + /** Returns the device version. */ get version(): DeviceVersion { return this.props.version; @@ -140,6 +156,18 @@ export class Device extends Entity { return Boolean(this.props.hasWaitingMap); } + /** Sets the device as connected. */ + setAsConnected(): void { + this.props.isConnected = true; + this.addEvent(new DeviceConnectedDomainEvent({ aggregateId: this.id })); + } + + /** Sets the device as connected. */ + setAsLocked(): void { + this.props.isLocked = true; + this.addEvent(new DeviceLockedDomainEvent({ aggregateId: this.id })); + } + /** Updates the device system. */ updateSystem(system: DeviceSystem): void { this.validateDefinedProp({ system }, 'system'); diff --git a/packages/domain/src/event-handlers/domain.event-handler.ts b/packages/domain/src/event-handlers/domain.event-handler.ts new file mode 100644 index 00000000..07fcd318 --- /dev/null +++ b/packages/domain/src/event-handlers/domain.event-handler.ts @@ -0,0 +1,11 @@ +import type { DomainEventNames, DomainEvents } from '../events'; +import type { EventHandler } from '@agnoc/toolkit'; + +/** Base class for domain event handlers. */ +export abstract class DomainEventHandler implements EventHandler { + /** The name of the event to listen to. */ + abstract eventName: DomainEventNames; + + /** Handle the event. */ + abstract handle(event: DomainEvents[this['eventName']]): void; +} diff --git a/packages/domain/src/events/device-connected.domain-event.test.ts b/packages/domain/src/events/device-connected.domain-event.test.ts new file mode 100644 index 00000000..4b986079 --- /dev/null +++ b/packages/domain/src/events/device-connected.domain-event.test.ts @@ -0,0 +1,11 @@ +import { ID, DomainEvent } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceConnectedDomainEvent } from './device-connected.domain-event'; + +describe('DeviceConnectedDomainEvent', function () { + it('should be created', function () { + const deviceConnectedDomainEvent = new DeviceConnectedDomainEvent({ aggregateId: ID.generate() }); + + expect(deviceConnectedDomainEvent).to.be.instanceOf(DomainEvent); + }); +}); diff --git a/packages/domain/src/events/device-connected.domain-event.ts b/packages/domain/src/events/device-connected.domain-event.ts new file mode 100644 index 00000000..07c9c6d9 --- /dev/null +++ b/packages/domain/src/events/device-connected.domain-event.ts @@ -0,0 +1,8 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export class DeviceConnectedDomainEvent extends DomainEvent { + protected validate(_: DeviceConnectedDomainEvent): void { + // noop + } +} diff --git a/packages/domain/src/events/device-locked.domain-event.ts b/packages/domain/src/events/device-locked.domain-event.ts new file mode 100644 index 00000000..3cf6d7af --- /dev/null +++ b/packages/domain/src/events/device-locked.domain-event.ts @@ -0,0 +1,8 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export class DeviceLockedDomainEvent extends DomainEvent { + protected validate(_: DeviceLockedDomainEvent): void { + // noop + } +} diff --git a/packages/domain/src/events/index.ts b/packages/domain/src/events/index.ts new file mode 100644 index 00000000..41188396 --- /dev/null +++ b/packages/domain/src/events/index.ts @@ -0,0 +1,9 @@ +import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; +import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; + +export type DomainEvents = { + DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; + DeviceLockedDomainEvent: DeviceLockedDomainEvent; +}; + +export type DomainEventNames = keyof DomainEvents; diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 3a4bc83a..692d51ae 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -12,6 +12,10 @@ export * from './entities/device-order.entity'; export * from './entities/device.entity'; export * from './entities/room.entity'; export * from './entities/zone.entity'; +export * from './event-handlers/domain.event-handler'; +export * from './events'; +export * from './events/device-connected.domain-event'; +export * from './events/device-locked.domain-event'; export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; export * from './value-objects/device-consumable.value-object'; diff --git a/packages/domain/src/repositories/device.repository.test.ts b/packages/domain/src/repositories/device.repository.test.ts index dce75dfb..52680816 100644 --- a/packages/domain/src/repositories/device.repository.test.ts +++ b/packages/domain/src/repositories/device.repository.test.ts @@ -2,15 +2,17 @@ import { Repository } from '@agnoc/toolkit'; import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceRepository } from './device.repository'; -import type { Adapter } from '@agnoc/toolkit'; +import type { Adapter, DomainEventBus } from '@agnoc/toolkit'; describe('DeviceRepository', function () { + let domainEventBus: DomainEventBus; let adapter: Adapter; let repository: DeviceRepository; beforeEach(function () { + domainEventBus = imock(); adapter = imock(); - repository = new DeviceRepository(instance(adapter)); + repository = new DeviceRepository(instance(domainEventBus), instance(adapter)); }); it('should be a repository', function () { diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index b92c2d5e..e168d248 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -30,6 +30,7 @@ module.exports = { }, }, rules: { + 'object-shorthand': ['error', 'always'], 'tsdoc/syntax': 'warn', 'node/no-missing-import': 'off', 'node/no-extraneous-import': 'off', diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 07181889..4a653764 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "debug": "^4.3.4", - "tiny-typed-emitter": "^2.1.0", + "emittery": "^0.13.1", "tslib": "^2.5.0" }, "devDependencies": { diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts new file mode 100644 index 00000000..0d0ff751 --- /dev/null +++ b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts @@ -0,0 +1,72 @@ +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ID } from '../domain-primitives/id.domain-primitive'; +import { AggregateRoot } from './aggregate-root.base'; +import { DomainEvent } from './domain-event.base'; +import { Entity } from './entity.base'; +import type { DomainEventProps } from './domain-event.base'; +import type { EntityProps } from './entity.base'; +import type { DomainEventBus } from '../event-buses/domain.event-bus'; + +describe('AggregateRoot', function () { + let domainEventBus: DomainEventBus; + + beforeEach(function () { + domainEventBus = imock(); + }); + + it('should be created', function () { + const dummyAggregateRoot = new DummyAggregateRoot({ id: ID.generate() }); + + expect(dummyAggregateRoot).to.be.instanceOf(Entity); + }); + + it('should be able to add domain events', function () { + const dummyAggregateRoot = new DummyAggregateRoot({ id: ID.generate() }); + + dummyAggregateRoot.doSomething(); + + expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(1); + expect(dummyAggregateRoot.domainEvents[0]).to.be.instanceOf(DummyDomainEvent); + }); + + it('should be able to publish domain events', async function () { + const id = ID.generate(); + const dummyAggregateRoot = new DummyAggregateRoot({ id }); + + when(domainEventBus.emit(anything(), anything())).thenResolve(); + + dummyAggregateRoot.doSomething(); + + await dummyAggregateRoot.publishEvents(instance(domainEventBus)); + + expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(0); + + verify(domainEventBus.emit('DummyDomainEvent', deepEqual(new DummyDomainEvent({ aggregateId: id })))).once(); + }); + + it('should be able to clear domain events', function () { + const dummyAggregateRoot = new DummyAggregateRoot({ id: ID.generate() }); + + dummyAggregateRoot.doSomething(); + dummyAggregateRoot.clearEvents(); + + expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(0); + }); +}); + +class DummyDomainEvent extends DomainEvent { + protected validate(_: DomainEventProps): void { + // noop + } +} + +class DummyAggregateRoot extends AggregateRoot { + doSomething(): void { + this.addEvent(new DummyDomainEvent({ aggregateId: this.id })); + } + + protected validate(_: EntityProps): void { + // noop + } +} diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts new file mode 100644 index 00000000..5a28c3bf --- /dev/null +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -0,0 +1,33 @@ +import { debug } from '../utils/debug.util'; +import { Entity } from './entity.base'; +import type { DomainEvent } from './domain-event.base'; +import type { EntityProps } from './entity.base'; +import type { DomainEventBus } from '../event-buses/domain.event-bus'; + +export abstract class AggregateRoot extends Entity { + private readonly debug = debug(__filename).extend(`${this.constructor.name.toLowerCase()}:${this.id.value}`); + readonly #domainEvents = new Set(); + + get domainEvents(): DomainEvent[] { + return [...this.#domainEvents.values()]; + } + + clearEvents(): void { + this.#domainEvents.clear(); + } + + async publishEvents(domainEventBus: DomainEventBus): Promise { + await Promise.all( + this.domainEvents.map(async (event) => { + this.debug(`publishing domain event '${event.constructor.name}'`); + return domainEventBus.emit(event.constructor.name, event); + }), + ); + + this.clearEvents(); + } + + protected addEvent(domainEvent: DomainEvent): void { + this.#domainEvents.add(domainEvent); + } +} diff --git a/packages/toolkit/src/base-classes/domain-event.base.test.ts b/packages/toolkit/src/base-classes/domain-event.base.test.ts new file mode 100644 index 00000000..6fa66a7d --- /dev/null +++ b/packages/toolkit/src/base-classes/domain-event.base.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; +import { ID } from '../domain-primitives/id.domain-primitive'; +import { DomainEvent } from './domain-event.base'; +import { Validatable } from './validatable.base'; +import type { DomainEventProps } from './domain-event.base'; + +describe('DomainEvent', function () { + it('should be created', function () { + const aggregateId = ID.generate(); + const dummyDomainEvent = new DummyDomainEvent({ aggregateId }); + + expect(dummyDomainEvent).to.be.instanceOf(Validatable); + expect(dummyDomainEvent.aggregateId).to.be.instanceOf(ID); + }); +}); + +class DummyDomainEvent extends DomainEvent { + protected validate(_: DomainEventProps): void { + // noop + } +} diff --git a/packages/toolkit/src/base-classes/domain-event.base.ts b/packages/toolkit/src/base-classes/domain-event.base.ts new file mode 100644 index 00000000..17037cfa --- /dev/null +++ b/packages/toolkit/src/base-classes/domain-event.base.ts @@ -0,0 +1,16 @@ +import { Validatable } from './validatable.base'; +import type { ID } from '../domain-primitives/id.domain-primitive'; + +export interface DomainEventProps { + aggregateId: ID; +} + +export abstract class DomainEvent extends Validatable { + constructor(protected readonly props: T) { + super(props); + } + + get aggregateId(): ID { + return this.props.aggregateId; + } +} diff --git a/packages/toolkit/src/base-classes/entity.base.ts b/packages/toolkit/src/base-classes/entity.base.ts index 1a325cfe..57670801 100644 --- a/packages/toolkit/src/base-classes/entity.base.ts +++ b/packages/toolkit/src/base-classes/entity.base.ts @@ -7,7 +7,7 @@ export interface EntityProps { id: ID; } -export abstract class Entity extends Validatable { +export abstract class Entity extends Validatable { constructor(props: T) { super(props); this.validateId(props); diff --git a/packages/toolkit/src/base-classes/event-bus.base.test.ts b/packages/toolkit/src/base-classes/event-bus.base.test.ts new file mode 100644 index 00000000..4f3ddbd1 --- /dev/null +++ b/packages/toolkit/src/base-classes/event-bus.base.test.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import { EventBus } from './event-bus.base'; + +describe('EventBus', function () { + const eventName = 'event'; + let dummyEventBus: DummyEventBus; + + beforeEach(function () { + dummyEventBus = new DummyEventBus(); + }); + + it('should listen for events on the bus', function () { + dummyEventBus.on(eventName, (data) => { + expect(data).to.deep.equal({ foo: 'bar' }); + }); + + return dummyEventBus.emit(eventName, { foo: 'bar' }); + }); + + it('should throw an error when a listener for an event throws an error', function () { + dummyEventBus.on(eventName, () => { + // noop + }); + + dummyEventBus.on(eventName, () => { + throw new Error('event error'); + }); + + return expect(dummyEventBus.emit(eventName, { foo: 'bar' })).to.be.rejectedWith(Error, 'event error'); + }); + + it('should reject with an error when a listener for an event rejects with an error', function () { + const eventName = 'event'; + + dummyEventBus.on(eventName, () => { + // noop + }); + + dummyEventBus.on(eventName, () => { + return Promise.reject(new Error('event error')); + }); + + return expect(dummyEventBus.emit(eventName, { foo: 'bar' })).to.be.rejectedWith(Error, 'event error'); + }); +}); + +class DummyEventBus extends EventBus {} diff --git a/packages/toolkit/src/base-classes/event-bus.base.ts b/packages/toolkit/src/base-classes/event-bus.base.ts new file mode 100644 index 00000000..31682628 --- /dev/null +++ b/packages/toolkit/src/base-classes/event-bus.base.ts @@ -0,0 +1,5 @@ +import Emittery from 'emittery'; + +export type EventBusEvents = { [key: string]: unknown }; + +export abstract class EventBus extends Emittery {} diff --git a/packages/adapter-tcp/src/event-handler.base.ts b/packages/toolkit/src/base-classes/event-handler.base.ts similarity index 72% rename from packages/adapter-tcp/src/event-handler.base.ts rename to packages/toolkit/src/base-classes/event-handler.base.ts index 3c3ec80a..f5f4858a 100644 --- a/packages/adapter-tcp/src/event-handler.base.ts +++ b/packages/toolkit/src/base-classes/event-handler.base.ts @@ -1,5 +1,6 @@ /** Base class for event handlers. */ export abstract class EventHandler { + // eslint-disable-next-line @typescript-eslint/ban-types abstract eventName: string; abstract handle(...args: unknown[]): void; } diff --git a/packages/toolkit/src/base-classes/repository.base.test.ts b/packages/toolkit/src/base-classes/repository.base.test.ts new file mode 100644 index 00000000..26a0dfb9 --- /dev/null +++ b/packages/toolkit/src/base-classes/repository.base.test.ts @@ -0,0 +1,76 @@ +import { anything, imock, instance, spy, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ID } from '../domain-primitives/id.domain-primitive'; +import { AggregateRoot } from './aggregate-root.base'; +import { Repository } from './repository.base'; +import type { Adapter } from './adapter.base'; +import type { EntityProps } from './entity.base'; +import type { DomainEventBus } from '../event-buses/domain.event-bus'; + +describe('Repository', function () { + let domainEventBus: DomainEventBus; + let adapter: Adapter; + let dummyRepository: DummyRepository; + + beforeEach(function () { + domainEventBus = imock(); + adapter = imock(); + dummyRepository = new DummyRepository(instance(domainEventBus), instance(adapter)); + }); + + it('should find one by id', async function () { + const id = ID.generate(); + const entity = new DummyAggregateRoot({ id }); + + when(adapter.get(anything())).thenReturn(entity); + + const ret = await dummyRepository.findOneById(id); + + expect(ret).to.be.equal(entity); + + verify(adapter.get(id)).once(); + }); + + it('should find all', async function () { + const id = ID.generate(); + const entity = new DummyAggregateRoot({ id }); + + when(adapter.getAll()).thenReturn([entity]); + + const ret = await dummyRepository.findAll(); + + expect(ret).to.be.deep.equal([entity]); + + verify(adapter.getAll()).once(); + }); + + it('should save one', async function () { + const id = ID.generate(); + const entity = new DummyAggregateRoot({ id }); + const entitySpy = spy(entity); + + await dummyRepository.saveOne(entity); + + verify(adapter.set(id, entity)).once(); + verify(entitySpy.publishEvents(instance(domainEventBus))).once(); + }); + + it('should delete one', async function () { + const id = ID.generate(); + const entity = new DummyAggregateRoot({ id }); + const entitySpy = spy(entity); + + await dummyRepository.deleteOne(entity); + + verify(adapter.delete(id)).once(); + verify(entitySpy.publishEvents(instance(domainEventBus))).once(); + }); +}); + +class DummyAggregateRoot extends AggregateRoot { + protected validate(_: EntityProps): void { + // noop + } +} + +class DummyRepository extends Repository {} diff --git a/packages/toolkit/src/base-classes/repository.base.ts b/packages/toolkit/src/base-classes/repository.base.ts index e2a98e69..7d88d298 100644 --- a/packages/toolkit/src/base-classes/repository.base.ts +++ b/packages/toolkit/src/base-classes/repository.base.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/require-await */ import type { Adapter } from './adapter.base'; -import type { Entity, EntityProps } from './entity.base'; +import type { AggregateRoot } from './aggregate-root.base'; +import type { EntityProps } from './entity.base'; import type { ID } from '../domain-primitives/id.domain-primitive'; +import type { DomainEventBus } from '../event-buses/domain.event-bus'; -export abstract class Repository> { - constructor(private readonly adapter: Adapter) {} +export abstract class Repository> { + constructor(private readonly domainEventBus: DomainEventBus, private readonly adapter: Adapter) {} async findOneById(id: ID): Promise { return this.adapter.get(id) as T | undefined; @@ -16,9 +18,11 @@ export abstract class Repository> { async saveOne(entity: T): Promise { this.adapter.set(entity.id, entity); + await entity.publishEvents(this.domainEventBus); } async deleteOne(entity: T): Promise { this.adapter.delete(entity.id); + await entity.publishEvents(this.domainEventBus); } } diff --git a/packages/toolkit/src/event-buses/domain.event-bus.test.ts b/packages/toolkit/src/event-buses/domain.event-bus.test.ts new file mode 100644 index 00000000..c53f7619 --- /dev/null +++ b/packages/toolkit/src/event-buses/domain.event-bus.test.ts @@ -0,0 +1,15 @@ +import { expect } from 'chai'; +import { EventBus } from '../base-classes/event-bus.base'; +import { DomainEventBus } from './domain.event-bus'; + +describe('DomainEventBus', function () { + let domainEventBus: DomainEventBus; + + beforeEach(function () { + domainEventBus = new DomainEventBus(); + }); + + it('should be created', function () { + expect(domainEventBus).to.be.instanceOf(EventBus); + }); +}); diff --git a/packages/toolkit/src/event-buses/domain.event-bus.ts b/packages/toolkit/src/event-buses/domain.event-bus.ts new file mode 100644 index 00000000..bee79353 --- /dev/null +++ b/packages/toolkit/src/event-buses/domain.event-bus.ts @@ -0,0 +1,6 @@ +import { EventBus } from '../base-classes/event-bus.base'; +import type { DomainEvent } from '../base-classes/domain-event.base'; + +type DomainEvents = { [key: string]: DomainEvent }; + +export class DomainEventBus extends EventBus {} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index e63a3642..5dd962eb 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -1,7 +1,11 @@ export * from './adapters/memory.adapter'; export * from './base-classes/adapter.base'; +export * from './base-classes/aggregate-root.base'; +export * from './base-classes/domain-event.base'; export * from './base-classes/domain-primitive.base'; export * from './base-classes/entity.base'; +export * from './base-classes/event-bus.base'; +export * from './base-classes/event-handler.base'; export * from './base-classes/exception.base'; export * from './base-classes/factory.base'; export * from './base-classes/mapper.base'; @@ -10,6 +14,7 @@ export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; export * from './decorators/bind.decorator'; export * from './domain-primitives/id.domain-primitive'; +export * from './event-buses/domain.event-bus'; export * from './exceptions/argument-invalid.exception'; export * from './exceptions/argument-not-provided.exception'; export * from './exceptions/argument-out-of-range.exception'; diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json index 2fc46b08..f040c390 100644 --- a/packages/transport-tcp/package.json +++ b/packages/transport-tcp/package.json @@ -80,7 +80,7 @@ "@agnoc/schemas-tcp": "^0.16.0", "@agnoc/toolkit": "^0.18.0-next.0", "debug": "^4.3.4", - "tiny-typed-emitter": "^2.1.0", + "emittery": "^0.13.1", "tslib": "^2.5.0" }, "devDependencies": { diff --git a/packages/transport-tcp/src/constants/payloads.constant.ts b/packages/transport-tcp/src/constants/payloads.constant.ts index d59db162..90518013 100644 --- a/packages/transport-tcp/src/constants/payloads.constant.ts +++ b/packages/transport-tcp/src/constants/payloads.constant.ts @@ -55,6 +55,7 @@ import type { IDEVICE_MAPID_WORK_STATUS_PUSH_REQ, IDEVICE_MOP_FLOOR_CLEAN_REQ, IDEVICE_MOP_FLOOR_CLEAN_RSP, + IDEVICE_OFFLINE_CMD, IDEVICE_ORDERLIST_DELETE_REQ, IDEVICE_ORDERLIST_DELETE_RSP, IDEVICE_ORDERLIST_GETTING_REQ, @@ -157,6 +158,7 @@ interface PayloadObjectMap { DEVICE_MAPID_WORK_STATUS_PUSH_REQ: IDEVICE_MAPID_WORK_STATUS_PUSH_REQ; DEVICE_MOP_FLOOR_CLEAN_REQ: IDEVICE_MOP_FLOOR_CLEAN_REQ; DEVICE_MOP_FLOOR_CLEAN_RSP: IDEVICE_MOP_FLOOR_CLEAN_RSP; + DEVICE_OFFLINE_CMD: IDEVICE_OFFLINE_CMD; DEVICE_ORDERLIST_DELETE_REQ: IDEVICE_ORDERLIST_DELETE_REQ; DEVICE_ORDERLIST_DELETE_RSP: IDEVICE_ORDERLIST_DELETE_RSP; DEVICE_ORDERLIST_GETTING_REQ: IDEVICE_ORDERLIST_GETTING_REQ; diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts index 535ecfd2..835156b7 100644 --- a/packages/transport-tcp/src/index.ts +++ b/packages/transport-tcp/src/index.ts @@ -13,7 +13,6 @@ export * from './mappers/payload.mapper'; export * from './packet.server'; export * from './packet.socket'; export * from './services/payload-object-parser.service'; -export * from './time-sync.server'; export * from './utils/get-custom-decoders.util'; export * from './utils/get-protobuf-root.util'; export * from './value-objects/packet.value-object'; diff --git a/packages/transport-tcp/src/mappers/payload.mapper.test.ts b/packages/transport-tcp/src/mappers/payload.mapper.test.ts index f17a4e78..bc6a0313 100644 --- a/packages/transport-tcp/src/mappers/payload.mapper.test.ts +++ b/packages/transport-tcp/src/mappers/payload.mapper.test.ts @@ -54,7 +54,7 @@ describe('PayloadMapper', function () { it('should create a buffer from a payload', function () { const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); const object = { foo: 'bar' }; - const payload = new Payload({ opcode: opcode, object: object }); + const payload = new Payload({ opcode, object }); const buffer = Buffer.from('test'); when(payloadObjectParserService.getEncoder(anything())).thenReturn(instance(encoder)); @@ -70,7 +70,7 @@ describe('PayloadMapper', function () { it('should throw an error when the encoder does not exist', function () { const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); const object = { foo: 'bar' }; - const payload = new Payload({ opcode: opcode, object: object }); + const payload = new Payload({ opcode, object }); when(payloadObjectParserService.getEncoder(anything())).thenReturn(undefined); diff --git a/packages/transport-tcp/src/mappers/payload.mapper.ts b/packages/transport-tcp/src/mappers/payload.mapper.ts index 05b2e532..6f41963d 100644 --- a/packages/transport-tcp/src/mappers/payload.mapper.ts +++ b/packages/transport-tcp/src/mappers/payload.mapper.ts @@ -20,7 +20,7 @@ export class PayloadMapper implements Mapper { const object = decoder(buffer); return new Payload({ - opcode: opcode, + opcode, object, }); } diff --git a/packages/transport-tcp/src/packet.server.test.ts b/packages/transport-tcp/src/packet.server.test.ts index be311f02..a49b2840 100644 --- a/packages/transport-tcp/src/packet.server.test.ts +++ b/packages/transport-tcp/src/packet.server.test.ts @@ -29,7 +29,7 @@ describe('PacketServer', function () { it('should receive connections', function (done) { const socket = new Socket(); - packetServer.once('connection', (socket) => { + void packetServer.once('connection').then((socket) => { expect(socket).to.be.instanceof(PacketSocket); done(); }); @@ -43,12 +43,12 @@ describe('PacketServer', function () { }); it('should close the server', function (done) { - packetServer.once('close', () => { + void packetServer.once('close').then(() => { expect(packetServer.isListening).to.be.false; done(); }); - packetServer.once('listening', () => { + void packetServer.once('listening').then(() => { void packetServer.close(); }); @@ -65,7 +65,7 @@ describe('PacketServer', function () { describe('#listen()', function () { it('should listen by port', function (done) { - packetServer.once('listening', () => { + void packetServer.once('listening').then(() => { expect(packetServer.isListening).to.be.true; expect(packetServer.address).to.be.an.instanceof(Object); done(); @@ -75,7 +75,7 @@ describe('PacketServer', function () { }); it('should listen by host and port', function (done) { - packetServer.once('listening', () => { + void packetServer.once('listening').then(() => { expect(packetServer.isListening).to.be.true; expect(packetServer.address).to.be.an.instanceof(Object); done(); @@ -85,7 +85,7 @@ describe('PacketServer', function () { }); it('should listen by options', function (done) { - packetServer.once('listening', () => { + void packetServer.once('listening').then(() => { expect(packetServer.isListening).to.be.true; expect(packetServer.address).to.be.an.instanceof(Object); done(); diff --git a/packages/transport-tcp/src/packet.server.ts b/packages/transport-tcp/src/packet.server.ts index 275f71f3..d089759b 100644 --- a/packages/transport-tcp/src/packet.server.ts +++ b/packages/transport-tcp/src/packet.server.ts @@ -1,5 +1,5 @@ import { Server } from 'net'; -import { TypedEmitter } from 'tiny-typed-emitter'; +import Emittery from 'emittery'; import { PacketSocket } from './packet.socket'; import type { PacketMapper } from './mappers/packet.mapper'; import type { AddressInfo, ListenOptions, Socket, DropArgument } from 'net'; @@ -7,19 +7,19 @@ import type { AddressInfo, ListenOptions, Socket, DropArgument } from 'net'; /** Events emitted by the {@link PacketServer}. */ export interface PacketServerEvents { /** Emits a {@link PacketSocket} when a new connection is established. */ - connection: (socket: PacketSocket) => void | Promise; + connection: PacketSocket; /** Emits an error when an error occurs. */ - error: (err: Error) => void; + error: Error; /** Emits when the server has been bound after calling `server.listen`. */ - listening: () => void; + listening: undefined; /** Emits when the server closes. */ - close: () => void; + close: undefined; /** Emits when a packet is dropped due to a full socket buffer. */ - drop: (data?: DropArgument) => void; + drop: DropArgument | undefined; } /** Server that emits `PacketSockets`. */ -export class PacketServer extends TypedEmitter { +export class PacketServer extends Emittery { private server: Server; constructor(private readonly packetMapper: PacketMapper) { @@ -72,16 +72,16 @@ export class PacketServer extends TypedEmitter { private onConnection(socket: Socket): void { const client = new PacketSocket(this.packetMapper, socket); - this.emit('connection', client); + void this.emit('connection', client); } private addListeners(): void { this.server.on('connection', this.onConnection.bind(this)); - this.server.on('listening', () => this.emit('listening')); - this.server.on('close', () => this.emit('close')); + this.server.on('listening', () => void this.emit('listening')); + this.server.on('close', () => void this.emit('close')); /* istanbul ignore next - unable to test */ - this.server.on('error', (error) => this.emit('error', error)); + this.server.on('error', (error) => void this.emit('error', error)); /* istanbul ignore next - unable to test */ - this.server.on('drop', (data) => this.emit('drop', data)); + this.server.on('drop', (data) => void this.emit('drop', data)); } } diff --git a/yarn.lock b/yarn.lock index f85856e9..195f1164 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2769,6 +2769,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz#953bc9a4767f5ce8ec125f9a1ad8e00e8f67e479" integrity sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw== +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" From 466923caf544923359cf498b8dd723c1b20e86f3 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 19 Mar 2023 11:33:06 +0100 Subject: [PATCH 06/38] feat(adapter-tcp): add more packet event handlers --- .../adapter-tcp/src/connection.manager.ts | 14 ++ ...s-connected-event-handler.event-handler.ts | 4 +- .../client-login.event-handler.ts | 7 - .../device-battery-update.event-handler.ts | 2 + ...ice-clean-map-data-report.event-handler.ts | 7 + .../device-clean-map-report.event-handler.ts | 7 + .../device-clean-task-report.event-handler.ts | 7 + ...device-get-all-global-map.event-handler.ts | 15 ++ .../device-locked.event-handler.ts | 6 +- ...p-charger-position-update.event-handler.ts | 26 +++ .../device-map-update.event-handler.ts | 197 ++++++++++++++++++ ...ce-map-work-status-update.event-handler.ts | 64 ++++++ .../device-memory-map-info.event-handler.ts | 15 ++ .../device-offline.event-handler.ts | 2 - .../device-register.event-handler.ts | 2 + .../device-settings-update.event-handler.ts | 2 + .../device-time-update.event-handler.ts | 19 ++ .../device-upgrade-info.event-handler.ts | 17 ++ .../device-version-update.event-handler.ts | 2 + .../device-wlan-update.event-handler.ts | 28 +++ .../src/mappers/device-error.mapper.ts | 7 +- packages/eslint-config/typescript.js | 1 + 22 files changed, 434 insertions(+), 17 deletions(-) create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts diff --git a/packages/adapter-tcp/src/connection.manager.ts b/packages/adapter-tcp/src/connection.manager.ts index 397691b1..02d8c349 100644 --- a/packages/adapter-tcp/src/connection.manager.ts +++ b/packages/adapter-tcp/src/connection.manager.ts @@ -40,11 +40,25 @@ export class ConnectionManager { const count = this.packetEventBus.listenerCount(event); + // Throw an error if there is no event handler for the packet event. if (count === 0) { throw new DomainException(`No event handler found for packet event '${event}'`); } + // Emit the packet event. await this.packetEventBus.emit(event, packetMessage); + + // This is a hack to only mark the device as connected if there is more than one connection. + // Here we should check that the connections are from the same ip address. + if (connection.device && !connection.device.isConnected) { + const connections = this.findConnectionsByDeviceId(connection.device.id); + + if (connections.length > 1) { + connection.device.setAsConnected(); + + await this.deviceRepository.saveOne(connection.device); + } + } }); connection.on('close', () => { diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index 06906d71..d8a0800f 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -7,13 +7,13 @@ export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventH constructor(private readonly connectionManager: ConnectionManager) {} - handle(event: DeviceConnectedDomainEvent): Promise { + async handle(event: DeviceConnectedDomainEvent): Promise { const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); if (!connection) { throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); } - return connection.send('DEVICE_CONTROL_LOCK_REQ', {}); + await connection.send('DEVICE_CONTROL_LOCK_REQ', {}); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts index 2949a314..ba0477d3 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts @@ -1,12 +1,9 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; -import type { DeviceRepository } from '@agnoc/domain'; export class ClientLoginEventHandler implements PacketEventHandler { readonly eventName = 'CLIENT_ONLINE_REQ'; - constructor(private readonly deviceRepository: DeviceRepository) {} - async handle(message: PacketMessage<'CLIENT_ONLINE_REQ'>): Promise { if (!message.device) { const data = { @@ -18,9 +15,5 @@ export class ClientLoginEventHandler implements PacketEventHandler { } await message.respond('CLIENT_ONLINE_RSP', { result: 0 }); - - message.device.setAsConnected(); - - await this.deviceRepository.saveOne(message.device); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts index 84f682ae..328ac324 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts @@ -17,6 +17,8 @@ export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { message.device.updateBattery(this.deviceBatteryMapper.toDomain(data.battery.level)); + // TODO: save the entity and publish domain event + await message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { result: 0 }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts index 993b6d35..a7a4fb2f 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -1,3 +1,4 @@ +import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; @@ -5,8 +6,14 @@ export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler readonly eventName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; async handle(message: PacketMessage<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + const data = message.packet.payload.object; + // TODO: save device clean map data + await message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { result: 0, cleanId: data.cleanId }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts index ed11e4f0..8d39d860 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -1,3 +1,4 @@ +import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; @@ -5,8 +6,14 @@ export class DeviceCleanMapReportEventHandler implements PacketEventHandler { readonly eventName = 'DEVICE_EVENT_REPORT_CLEANMAP'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANMAP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + const data = message.packet.payload.object; + // TODO: save device clean map data + await message.respond('DEVICE_EVENT_REPORT_RSP', { result: 0, body: { cleanId: data.cleanId } }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts index 27febb0a..91e05319 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -1,3 +1,4 @@ +import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; @@ -5,6 +6,12 @@ export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { readonly eventName = 'DEVICE_EVENT_REPORT_CLEANTASK'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANTASK'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: save device clean task data + await message.respond('UNK_11A4', { unk1: 0 }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts new file mode 100644 index 00000000..93de0f5d --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts @@ -0,0 +1,15 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceGetAllGlobalMapEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; + + async handle(message: PacketMessage<'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: investigate the meaning of this packet. + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts index 140ca53e..e7451f91 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts @@ -13,8 +13,10 @@ export class DeviceLockedEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - message.device.setAsLocked(); + if (!message.device.isLocked) { + message.device.setAsLocked(); - await this.deviceRepository.saveOne(message.device); + await this.deviceRepository.saveOne(message.device); + } } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts new file mode 100644 index 00000000..2f0be65b --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -0,0 +1,26 @@ +import { MapPosition } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'; + + async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const data = message.packet.payload.object; + + message.device.map?.updateCharger( + new MapPosition({ + x: data.poseX, + y: data.poseY, + phi: data.posePhi, + }), + ); + + // TODO: save entity and publish domain event + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts new file mode 100644 index 00000000..db744e41 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts @@ -0,0 +1,197 @@ +import { + DeviceCleanWork, + CleanSize, + DeviceTime, + DeviceMap, + MapCoordinate, + MapPixel, + MapPosition, + Room, + Zone, +} from '@agnoc/domain'; +import { DomainException, ID, isPresent } from '@agnoc/toolkit'; +import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../../mappers/device-state.mapper'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceMapUpdateEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'; + + constructor( + private readonly deviceBatteryMapper: DeviceBatteryMapper, + private readonly deviceModeMapper: DeviceModeMapper, + private readonly deviceStateMapper: DeviceStateMapper, + private readonly deviceErrorMapper: DeviceErrorMapper, + private readonly deviceFanSpeedMapper: DeviceFanSpeedMapper, + ) {} + + async handle(message: PacketMessage<'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const { + statusInfo, + mapHeadInfo, + mapGrid, + historyHeadInfo, + robotPoseInfo, + robotChargeInfo, + cleanRoomList, + roomSegmentList, + wallListInfo, + spotInfo, + cleanPlanList, + currentPlanId, + } = message.packet.payload.object; + + if (statusInfo) { + const { + batteryPercent: battery, + faultType: type, + workingMode: workMode, + chargeState: chargeStatus, + cleanPreference, + faultCode, + } = statusInfo; + + message.device.updateCurrentClean( + new DeviceCleanWork({ + size: new CleanSize(statusInfo.cleanSize), + time: DeviceTime.fromMinutes(statusInfo.cleanTime), + }), + ); + message.device.updateBattery(this.deviceBatteryMapper.toDomain(battery)); + message.device.updateMode(this.deviceModeMapper.toDomain(workMode)); + message.device.updateState(this.deviceStateMapper.toDomain({ type, workMode, chargeStatus })); + message.device.updateError(this.deviceErrorMapper.toDomain(faultCode)); + message.device.updateFanSpeed(this.deviceFanSpeedMapper.toDomain(cleanPreference)); + } + + let map = message.device.map; + + if (mapHeadInfo && mapGrid) { + const props = { + id: new ID(mapHeadInfo.mapHeadId), + size: new MapPixel({ + x: mapHeadInfo.sizeX, + y: mapHeadInfo.sizeY, + }), + min: new MapCoordinate({ + x: mapHeadInfo.minX, + y: mapHeadInfo.minY, + }), + max: new MapCoordinate({ + x: mapHeadInfo.maxX, + y: mapHeadInfo.maxY, + }), + resolution: mapHeadInfo.resolution, + grid: mapGrid, + rooms: [], + restrictedZones: [], + robotPath: [], + }; + + map = map ? map.clone(props) : new DeviceMap(props); + + message.device.updateMap(map); + } + + if (map) { + if (historyHeadInfo) { + const currentIndex = map.robotPath.length; + + map.updateRobotPath( + map.robotPath.concat( + historyHeadInfo.pointList.slice(currentIndex).map(({ x, y }) => new MapCoordinate({ x, y })), + ), + ); + } + + if (robotPoseInfo) { + map.updateRobot( + new MapPosition({ + x: robotPoseInfo.poseX, + y: robotPoseInfo.poseY, + phi: robotPoseInfo.posePhi, + }), + ); + } + + if (robotChargeInfo) { + map.updateCharger( + new MapPosition({ + x: robotChargeInfo.poseX, + y: robotChargeInfo.poseY, + phi: robotChargeInfo.posePhi, + }), + ); + } + + if (spotInfo) { + map.updateCurrentSpot( + new MapPosition({ + x: spotInfo.poseX, + y: spotInfo.poseY, + phi: spotInfo.posePhi, + }), + ); + } + + if (wallListInfo) { + map.updateRestrictedZones( + wallListInfo.cleanAreaList.map((cleanArea) => { + return new Zone({ + id: new ID(cleanArea.cleanAreaId), + coordinates: cleanArea.coordinateList.map(({ x, y }) => { + return new MapCoordinate({ + x, + y, + }); + }), + }); + }), + ); + } + + if (cleanRoomList && roomSegmentList && cleanPlanList) { + const currentPlan = cleanPlanList.find((plan) => plan.planId === currentPlanId); + + map.updateRooms( + cleanRoomList + .map((cleanRoom) => { + const segment = roomSegmentList.find((roomSegment) => roomSegment.roomId === cleanRoom.roomId); + const roomInfo = currentPlan?.cleanRoomInfoList.find((r) => r.roomId === cleanRoom.roomId); + + if (!segment) { + return undefined; + } + + return new Room({ + id: new ID(cleanRoom.roomId), + name: cleanRoom.roomName, + isEnabled: Boolean(roomInfo?.enable), + center: new MapCoordinate({ + x: cleanRoom.roomX, + y: cleanRoom.roomY, + }), + pixels: segment?.roomPixelList.map((pixel) => { + return new MapPixel({ + x: pixel.x, + y: pixel.y, + }); + }), + }); + }) + .filter(isPresent), + ); + } + } + + // TODO: save entities and publish domain events + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts new file mode 100644 index 00000000..386fc1cb --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -0,0 +1,64 @@ +import { CleanSize, DeviceCleanWork, DeviceTime } from '@agnoc/domain'; +import { DomainException, isPresent } from '@agnoc/toolkit'; +import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../../mappers/device-state.mapper'; +import type { DeviceWaterLevelMapper } from '../../mappers/device-water-level.mapper'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'; + + constructor( + private readonly deviceStateMapper: DeviceStateMapper, + private readonly deviceModeMapper: DeviceModeMapper, + private readonly deviceErrorMapper: DeviceErrorMapper, + private readonly deviceBatteryMapper: DeviceBatteryMapper, + private readonly deviceFanSpeedMapper: DeviceFanSpeedMapper, + private readonly deviceWaterLevelMapper: DeviceWaterLevelMapper, + ) {} + + async handle(message: PacketMessage<'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const { + battery, + type, + workMode, + chargeStatus, + cleanPreference, + faultCode, + waterLevel, + mopType, + cleanSize, + cleanTime, + } = message.packet.payload.object; + + message.device.updateCurrentClean( + new DeviceCleanWork({ + size: new CleanSize(cleanSize), + time: DeviceTime.fromMinutes(cleanTime), + }), + ); + message.device.updateState(this.deviceStateMapper.toDomain({ type, workMode, chargeStatus })); + message.device.updateMode(this.deviceModeMapper.toDomain(workMode)); + message.device.updateError(this.deviceErrorMapper.toDomain(faultCode)); + message.device.updateBattery(this.deviceBatteryMapper.toDomain(battery)); + message.device.updateFanSpeed(this.deviceFanSpeedMapper.toDomain(cleanPreference)); + + if (isPresent(mopType)) { + message.device.updateHasMopAttached(mopType); + } + + if (isPresent(waterLevel)) { + message.device.updateWaterLevel(this.deviceWaterLevelMapper.toDomain(waterLevel)); + } + + // TODO: save entity and publish domain events + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts new file mode 100644 index 00000000..e7ce14db --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts @@ -0,0 +1,15 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceMemoryMapInfoEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; + + async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: save device memory map info + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts index 2ffaa07a..0bfda3f0 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts @@ -9,7 +9,5 @@ export class DeviceOfflineEventHandler implements PacketEventHandler { if (!message.device) { throw new DomainException('Device not found'); } - - return message.connection.send('DEVICE_CONTROL_LOCK_REQ', {}); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts index ef6c05ee..77f0db7a 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts @@ -18,6 +18,8 @@ export class DeviceRegisterEventHandler implements PacketEventHandler { version: new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion }), }); + // TODO: publish device created domain event + await this.deviceRepository.saveOne(device); const response = { result: 0, device: { id: device.id.value } }; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts index 6ece287b..5401371e 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts @@ -34,6 +34,8 @@ export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { message.device.updateConfig(deviceSettings); + // TODO: save entity and publish domain event + await message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { result: 0 }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts new file mode 100644 index 00000000..0fdae5b1 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts @@ -0,0 +1,19 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceTimeUpdateEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_GETTIME_RSP'; + + async handle(message: PacketMessage<'DEVICE_GETTIME_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: save device time + // { + // timestamp: object.body.deviceTime * 1000, + // offset: -1 * ((object.body.deviceTimezone || 0) / 60), + // }; + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts new file mode 100644 index 00000000..c2798d20 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts @@ -0,0 +1,17 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceUpgradeInfoEventHandler implements PacketEventHandler { + readonly eventName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; + + async handle(message: PacketMessage<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: save device upgrade info + + await message.connection.send('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { result: 0 }); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts index a5897efa..1d4ce738 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts @@ -15,6 +15,8 @@ export class DeviceVersionUpdateEventHandler implements PacketEventHandler { message.device.updateVersion(new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion })); + // TODO: save entity and publish domain event + await message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { result: 0 }); } } diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts new file mode 100644 index 00000000..7366b9c9 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts @@ -0,0 +1,28 @@ +import { DeviceWlan } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceWlanUpdateEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_WLAN_INFO_GETTING_RSP'; + + async handle(message: PacketMessage<'DEVICE_WLAN_INFO_GETTING_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + const data = message.packet.payload.object.body; + + message.device.updateWlan( + new DeviceWlan({ + ipv4: data.ipv4, + ssid: data.ssid, + port: data.port, + mask: data.mask, + mac: data.mac, + }), + ); + + // TODO: save entity and publish domain event + } +} diff --git a/packages/adapter-tcp/src/mappers/device-error.mapper.ts b/packages/adapter-tcp/src/mappers/device-error.mapper.ts index acd8f77f..30593c0c 100644 --- a/packages/adapter-tcp/src/mappers/device-error.mapper.ts +++ b/packages/adapter-tcp/src/mappers/device-error.mapper.ts @@ -2,7 +2,7 @@ import { DeviceError, DeviceErrorValue } from '@agnoc/domain'; import { DomainException, NotImplementedException } from '@agnoc/toolkit'; import type { Mapper } from '@agnoc/toolkit'; -const ROBOT_TO_DOMAIN = { +const ROBOT_TO_DOMAIN: Record = { 0: DeviceErrorValue.None, 2003: DeviceErrorValue.LowPowerPlanDis, 2100: DeviceErrorValue.BrokenGoHome, @@ -53,10 +53,9 @@ export class DeviceErrorMapper implements Mapper { throw new DomainException(`Unable to map error code '${error}' to domain value`); } - const value = ROBOT_TO_DOMAIN[error as keyof typeof ROBOT_TO_DOMAIN]; + const value = ROBOT_TO_DOMAIN[error]; - // @ts-expect-error unknown error - return new DeviceError({ value }); + return new DeviceError(value); } fromDomain(): never { diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index e168d248..af7c0c2e 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -36,6 +36,7 @@ module.exports = { 'node/no-extraneous-import': 'off', 'node/no-unsupported-features/es-syntax': 'off', 'security/detect-object-injection': 'off', + '@typescript-eslint/require-await': 'off', '@typescript-eslint/explicit-module-boundary-types': 'error', '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/consistent-type-exports': 'warn', From 866fa531c2b6eedadd606e63e7022a95cf88b77a Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 19 Mar 2023 19:52:48 +0100 Subject: [PATCH 07/38] feat(adapter-tcp): add commands --- .../adapter-tcp/src/connection.manager.ts | 116 ++++++++++----- packages/adapter-tcp/src/device.connection.ts | 73 +++++++-- .../locate-device.event-handler.ts | 19 +++ .../device-located.event-handler.ts | 15 ++ packages/adapter-tcp/src/packet.event-bus.ts | 2 + packages/adapter-tcp/src/packet.message.ts | 6 +- packages/adapter-tcp/src/tcp.adapter.ts | 138 ++++++++++++++++++ packages/domain/src/commands/commands.ts | 7 + .../src/commands/locate-device.command.ts | 16 ++ .../device-connected.domain-event.test.ts | 0 .../device-connected.domain-event.ts | 0 .../device-locked.domain-event.ts | 0 .../domain-events.ts} | 0 packages/domain/src/entities/device.entity.ts | 4 +- .../src/event-buses/command.event-bus.ts | 6 + .../src/event-buses/domain.event-bus.test.ts | 2 +- .../src/event-buses/domain.event-bus.ts | 6 + .../event-handlers/command.event-handler.ts | 7 + .../event-handlers/domain.event-handler.ts | 2 +- packages/domain/src/index.ts | 11 +- .../repositories/device.repository.test.ts | 8 +- packages/toolkit/package.json | 1 + .../base-classes/aggregate-root.base.test.ts | 12 +- .../src/base-classes/aggregate-root.base.ts | 6 +- .../toolkit/src/base-classes/command.base.ts | 10 ++ .../src/base-classes/event-bus.base.ts | 5 +- .../src/base-classes/repository.base.test.ts | 12 +- .../src/base-classes/repository.base.ts | 8 +- .../src/event-buses/domain.event-bus.ts | 6 - .../src/event-handler.registry.test.ts} | 8 +- .../src/event-handler.registry.ts} | 5 +- packages/toolkit/src/index.ts | 3 +- packages/transport-tcp/src/packet.server.ts | 20 ++- 33 files changed, 427 insertions(+), 107 deletions(-) create mode 100644 packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts create mode 100644 packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts create mode 100644 packages/adapter-tcp/src/tcp.adapter.ts create mode 100644 packages/domain/src/commands/commands.ts create mode 100644 packages/domain/src/commands/locate-device.command.ts rename packages/domain/src/{events => domain-events}/device-connected.domain-event.test.ts (100%) rename packages/domain/src/{events => domain-events}/device-connected.domain-event.ts (100%) rename packages/domain/src/{events => domain-events}/device-locked.domain-event.ts (100%) rename packages/domain/src/{events/index.ts => domain-events/domain-events.ts} (100%) create mode 100644 packages/domain/src/event-buses/command.event-bus.ts rename packages/{toolkit => domain}/src/event-buses/domain.event-bus.test.ts (85%) create mode 100644 packages/domain/src/event-buses/domain.event-bus.ts create mode 100644 packages/domain/src/event-handlers/command.event-handler.ts create mode 100644 packages/toolkit/src/base-classes/command.base.ts delete mode 100644 packages/toolkit/src/event-buses/domain.event-bus.ts rename packages/{adapter-tcp/src/event-handler.manager.test.ts => toolkit/src/event-handler.registry.test.ts} (80%) rename packages/{adapter-tcp/src/event-handler.manager.ts => toolkit/src/event-handler.registry.ts} (72%) diff --git a/packages/adapter-tcp/src/connection.manager.ts b/packages/adapter-tcp/src/connection.manager.ts index 02d8c349..43497170 100644 --- a/packages/adapter-tcp/src/connection.manager.ts +++ b/packages/adapter-tcp/src/connection.manager.ts @@ -7,68 +7,104 @@ import type { ID } from '@agnoc/toolkit'; import type { PacketServer, PacketFactory, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; export class ConnectionManager { - private readonly connections = new Set(); + private readonly servers = new Map>(); constructor( - private readonly servers: PacketServer[], private readonly packetEventBus: PacketEventBus, private readonly packetFactory: PacketFactory, private readonly deviceRepository: DeviceRepository, - ) { - this.addListeners(); - } + ) {} findConnectionsByDeviceId(deviceId: ID): DeviceConnection[] { - return [...this.connections].filter((connection) => connection.device?.id.equals(deviceId)); + const connections = [...this.servers.values()].flatMap((connections) => [...connections]); + + return connections.filter((connection) => connection.device?.id.equals(deviceId)); } - private addListeners() { - this.servers.forEach((server) => { - server.on('connection', (socket) => { - const connection = new DeviceConnection(this.packetFactory, socket); + addServers(...servers: PacketServer[]): void { + servers.forEach((server) => { + this.servers.set(server, new Set()); + this.addListeners(server); + }); + } - this.connections.add(connection); + private addListeners(server: PacketServer) { + server.on('connection', (socket) => { + const connection = new DeviceConnection(this.packetFactory, this.packetEventBus, socket); - connection.on('data', async (packet: Packet) => { - const event = packet.payload.opcode.name as PayloadObjectName; - const packetMessage = new PacketMessage(connection, packet) as PacketEventBusEvents[PayloadObjectName]; + this.servers.get(server)?.add(connection); - // Update the device on the connection if the device id has changed. - if (!packet.deviceId.equals(connection.device?.id)) { - connection.device = await this.findDeviceById(packet.deviceId); - } + connection.on('data', async (packet: Packet) => { + const packetMessage = new PacketMessage(connection, packet); - const count = this.packetEventBus.listenerCount(event); + // Update the device on the connection if the device id has changed. + await this.updateConnectionDevice(packet, connection); - // Throw an error if there is no event handler for the packet event. - if (count === 0) { - throw new DomainException(`No event handler found for packet event '${event}'`); - } + // Send the packet message to the packet event bus. + await this.emitPacketEvent(packetMessage); - // Emit the packet event. - await this.packetEventBus.emit(event, packetMessage); + // This is a hack to only mark the device as connected if there is more than one connection. + // Here we should check that the connections are from the same ip address. + await this.tryToSetDeviceAsConnected(connection); + }); - // This is a hack to only mark the device as connected if there is more than one connection. - // Here we should check that the connections are from the same ip address. - if (connection.device && !connection.device.isConnected) { - const connections = this.findConnectionsByDeviceId(connection.device.id); + connection.on('close', () => { + this.servers.get(server)?.delete(connection); + }); + }); - if (connections.length > 1) { - connection.device.setAsConnected(); + server.on('close', async () => { + const connections = this.servers.get(server); - await this.deviceRepository.saveOne(connection.device); - } - } - }); + if (connections) { + await Promise.all([...connections].map((connection) => connection.close())); - connection.on('close', () => { - connection.removeAllListeners(); - this.connections.delete(connection); - }); - }); + this.servers.delete(server); + } }); } + private async tryToSetDeviceAsConnected(connection: DeviceConnection) { + if (connection.device && !connection.device.isConnected) { + const connections = this.findConnectionsByDeviceId(connection.device.id); + + if (connections.length > 1) { + connection.device.setAsConnected(); + + await this.deviceRepository.saveOne(connection.device); + } + } + } + + private async emitPacketEvent(message: PacketMessage) { + const name = message.packet.payload.opcode.name as PayloadObjectName; + const sequence = message.packet.sequence.toString(); + + this.checkForPacketEventHandler(name); + + // Emit the packet event by the sequence string. + // This is used to wait for a response from a packet. + await this.packetEventBus.emit(sequence, message as PacketEventBusEvents[PayloadObjectName]); + + // Emit the packet event by the opcode name. + await this.packetEventBus.emit(name, message as PacketEventBusEvents[PayloadObjectName]); + } + + private checkForPacketEventHandler(event: PayloadObjectName) { + const count = this.packetEventBus.listenerCount(event); + + // Throw an error if there is no event handler for the packet event. + if (count === 0) { + throw new DomainException(`No event handler found for packet event '${event}'`); + } + } + + private async updateConnectionDevice(packet: Packet, connection: DeviceConnection) { + if (!packet.deviceId.equals(connection.device?.id)) { + connection.device = await this.findDeviceById(packet.deviceId); + } + } + private async findDeviceById(id: ID): Promise { if (id.value === 0) { return undefined; diff --git a/packages/adapter-tcp/src/device.connection.ts b/packages/adapter-tcp/src/device.connection.ts index bed95eec..02476bd7 100644 --- a/packages/adapter-tcp/src/device.connection.ts +++ b/packages/adapter-tcp/src/device.connection.ts @@ -1,19 +1,31 @@ import { Device } from '@agnoc/domain'; import { ArgumentInvalidException, DomainException, ID } from '@agnoc/toolkit'; import { PacketSocket } from '@agnoc/transport-tcp'; -import { TypedEmitter } from 'tiny-typed-emitter'; -import type { Packet, PacketFactory, PayloadObjectName, PayloadObjectFrom } from '@agnoc/transport-tcp'; +import Emittery from 'emittery'; +import type { PacketEventBus } from './packet.event-bus'; +import type { PacketMessage } from './packet.message'; +import type { + Packet, + PacketFactory, + PayloadObjectName, + PayloadObjectFrom, + CreatePacketProps, +} from '@agnoc/transport-tcp'; export interface DeviceConnectionEvents { - data: (packet: Packet) => void | Promise; - close: () => void; - error: (err: Error) => void; + data: Packet; + close: undefined; + error: Error; } -export class DeviceConnection extends TypedEmitter { +export class DeviceConnection extends Emittery { #device?: Device; - constructor(private readonly packetFactory: PacketFactory, private readonly socket: PacketSocket) { + constructor( + private readonly packetFactory: PacketFactory, + private readonly eventBus: PacketEventBus, + private readonly socket: PacketSocket, + ) { super(); this.validateSocket(); this.addListeners(); @@ -34,20 +46,52 @@ export class DeviceConnection extends TypedEmitter { } send(name: Name, object: PayloadObjectFrom): Promise { - const props = { deviceId: this.#device?.id ?? new ID(0), userId: this.#device?.userId ?? new ID(0) }; - const packet = this.packetFactory.create(name, object, props); + const packet = this.packetFactory.create(name, object, this.getPacketProps()); - return this.socket.write(packet); + return this.write(packet); } respond(name: Name, object: PayloadObjectFrom, packet: Packet): Promise { - return this.socket.write(this.packetFactory.create(name, object, packet)); + return this.write(this.packetFactory.create(name, object, packet)); + } + + sendAndWait(name: Name, object: PayloadObjectFrom): Promise { + const packet = this.packetFactory.create(name, object, this.getPacketProps()); + + return this.writeAndWait(packet); + } + + respondAndWait( + name: Name, + object: PayloadObjectFrom, + packet: Packet, + ): Promise { + return this.writeAndWait(this.packetFactory.create(name, object, packet)); } close(): Promise { return this.socket.end(); } + private getPacketProps(): CreatePacketProps { + return { deviceId: this.#device?.id ?? new ID(0), userId: this.#device?.userId ?? new ID(0) }; + } + + private writeAndWait(packet: Packet): Promise { + return new Promise((resolve, reject) => { + this.eventBus.once(packet.sequence.toString()).then(resolve, reject); + this.write(packet).catch(reject); + }); + } + + private async write(packet: Packet) { + if (!this.socket.connected) { + return; + } + + return this.socket.write(packet); + } + private validateSocket() { if (!(this.socket instanceof PacketSocket)) { throw new DomainException('Socket for Connection is not an instance of PacketSocket'); @@ -60,14 +104,13 @@ export class DeviceConnection extends TypedEmitter { private addListeners() { this.socket.on('data', (packet) => { - this.emit('data', packet); + void this.emit('data', packet); }); this.socket.on('error', (err) => { - this.emit('error', err); + void this.emit('error', err); }); this.socket.on('close', () => { - this.socket.removeAllListeners(); - this.emit('close'); + void this.emit('close'); }); } } diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts new file mode 100644 index 00000000..573541e8 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts @@ -0,0 +1,19 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { ConnectionManager } from '../../connection.manager'; +import type { CommandEventHandler, LocateDeviceCommand } from '@agnoc/domain'; + +export class LocateDeviceEventHandler implements CommandEventHandler { + readonly eventName = 'LocateDeviceCommand'; + + constructor(private readonly connectionManager: ConnectionManager) {} + + async handle(event: LocateDeviceCommand): Promise { + const [connection] = this.connectionManager.findConnectionsByDeviceId(event.deviceId); + + if (!connection || !connection.device) { + throw new DomainException(`Unable to find a connection for the device with id ${event.deviceId.value}`); + } + + await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); + } +} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts new file mode 100644 index 00000000..65e79dc7 --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts @@ -0,0 +1,15 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../../packet.event-handler'; +import type { PacketMessage } from '../../packet.message'; + +export class DeviceLocatedEventHandler implements PacketEventHandler { + readonly eventName = 'DEVICE_SEEK_LOCATION_RSP'; + + async handle(message: PacketMessage<'DEVICE_SEEK_LOCATION_RSP'>): Promise { + if (!message.device) { + throw new DomainException('Device not found'); + } + + // TODO: Should do something here? + } +} diff --git a/packages/adapter-tcp/src/packet.event-bus.ts b/packages/adapter-tcp/src/packet.event-bus.ts index 9d8dec82..d1a2109d 100644 --- a/packages/adapter-tcp/src/packet.event-bus.ts +++ b/packages/adapter-tcp/src/packet.event-bus.ts @@ -5,6 +5,8 @@ import type { PayloadObjectName } from '@agnoc/transport-tcp'; /** Events for the packet event bus. */ export type PacketEventBusEvents = { [Name in PayloadObjectName]: PacketMessage; +} & { + [key: string]: PacketMessage; }; /** Event bus for packets. */ diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts index 9e780e71..a5cffcce 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/packet.message.ts @@ -2,7 +2,7 @@ import type { DeviceConnection } from './device.connection'; import type { Device } from '@agnoc/domain'; import type { Packet, PayloadObjectFrom, PayloadObjectName } from '@agnoc/transport-tcp'; -export class PacketMessage { +export class PacketMessage { constructor(readonly connection: DeviceConnection, readonly packet: Packet) {} get device(): Device | undefined { @@ -12,4 +12,8 @@ export class PacketMessage { respond(name: Name, object: PayloadObjectFrom): Promise { return this.connection.respond(name, object, this.packet); } + + respondAndWait(name: Name, object: PayloadObjectFrom): Promise { + return this.connection.respondAndWait(name, object, this.packet); + } } diff --git a/packages/adapter-tcp/src/tcp.adapter.ts b/packages/adapter-tcp/src/tcp.adapter.ts new file mode 100644 index 00000000..3ec42621 --- /dev/null +++ b/packages/adapter-tcp/src/tcp.adapter.ts @@ -0,0 +1,138 @@ +import { EventHandlerRegistry } from '@agnoc/toolkit'; +import { + getCustomDecoders, + getProtobufRoot, + PacketMapper, + PacketServer, + PayloadMapper, + PayloadObjectParserService, + PacketFactory, +} from '@agnoc/transport-tcp'; +import { ConnectionManager } from './connection.manager'; +import { LocateDeviceEventHandler } from './event-handlers/command-event-handlers/locate-device.event-handler'; +import { LockDeviceWhenDeviceIsConnectedEventHandler } from './event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; +import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; +import { ClientHeartbeatEventHandler } from './event-handlers/packet-event-handlers/client-heartbeat.event-handler'; +import { ClientLoginEventHandler } from './event-handlers/packet-event-handlers/client-login.event-handler'; +import { DeviceBatteryUpdateEventHandler } from './event-handlers/packet-event-handlers/device-battery-update.event-handler'; +import { DeviceCleanMapDataReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler'; +import { DeviceCleanMapReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-map-report.event-handler'; +import { DeviceCleanTaskReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-task-report.event-handler'; +import { DeviceGetAllGlobalMapEventHandler } from './event-handlers/packet-event-handlers/device-get-all-global-map.event-handler'; +import { DeviceLocatedEventHandler } from './event-handlers/packet-event-handlers/device-located.event-handler'; +import { DeviceLockedEventHandler } from './event-handlers/packet-event-handlers/device-locked.event-handler'; +import { DeviceMapChargerPositionUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler'; +import { DeviceMapUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-update.event-handler'; +import { DeviceMapWorkStatusUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-work-status-update.event-handler'; +import { DeviceMemoryMapInfoEventHandler } from './event-handlers/packet-event-handlers/device-memory-map-info.event-handler'; +import { DeviceOfflineEventHandler } from './event-handlers/packet-event-handlers/device-offline.event-handler'; +import { DeviceRegisterEventHandler } from './event-handlers/packet-event-handlers/device-register.event-handler'; +import { DeviceSettingsUpdateEventHandler } from './event-handlers/packet-event-handlers/device-settings-update.event-handler'; +import { DeviceTimeUpdateEventHandler } from './event-handlers/packet-event-handlers/device-time-update.event-handler'; +import { DeviceUpgradeInfoEventHandler } from './event-handlers/packet-event-handlers/device-upgrade-info.event-handler'; +import { DeviceVersionUpdateEventHandler } from './event-handlers/packet-event-handlers/device-version-update.event-handler'; +import { DeviceWlanUpdateEventHandler } from './event-handlers/packet-event-handlers/device-wlan-update.event-handler'; +import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; +import { DeviceErrorMapper } from './mappers/device-error.mapper'; +import { DeviceFanSpeedMapper } from './mappers/device-fan-speed.mapper'; +import { DeviceModeMapper } from './mappers/device-mode.mapper'; +import { DeviceStateMapper } from './mappers/device-state.mapper'; +import { DeviceVoiceMapper } from './mappers/device-voice.mapper'; +import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; +import { PacketEventBus } from './packet.event-bus'; +import { TimeSyncServer } from './time-sync.server'; +import type { DeviceRepository } from '@agnoc/domain'; + +export class TCPAdapter { + private readonly timeSyncServer: TimeSyncServer; + private readonly cmdServer: PacketServer; + private readonly mapServer: PacketServer; + + constructor( + private readonly deviceRepository: DeviceRepository, + private readonly domainEventHandlerRegistry: EventHandlerRegistry, + private readonly commandEventHandlerRegistry: EventHandlerRegistry, + ) { + // Packet foundation + const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); + const packetMapper = new PacketMapper(payloadMapper); + const packetFactory = new PacketFactory(); + + // Servers + this.timeSyncServer = new TimeSyncServer(new PacketServer(packetMapper), packetFactory); + this.cmdServer = new PacketServer(packetMapper); + this.mapServer = new PacketServer(packetMapper); + + // Mappers + const deviceFanSpeedMapper = new DeviceFanSpeedMapper(); + const deviceWaterLevelMapper = new DeviceWaterLevelMapper(); + const deviceVoiceMapper = new DeviceVoiceMapper(); + const deviceStateMapper = new DeviceStateMapper(); + const deviceModeMapper = new DeviceModeMapper(); + const deviceErrorMapper = new DeviceErrorMapper(); + const deviceBatteryMapper = new DeviceBatteryMapper(); + + // Packet event bus + const packetEventBus = new PacketEventBus(); + const packetEventHandlerRegistry = new EventHandlerRegistry(packetEventBus); + + // Connection managers + const connectionManager = new ConnectionManager(packetEventBus, packetFactory, this.deviceRepository); + + connectionManager.addServers(this.cmdServer, this.mapServer); + + // Packet event handlers + packetEventHandlerRegistry.register( + new ClientHeartbeatEventHandler(), + new ClientLoginEventHandler(), + new DeviceBatteryUpdateEventHandler(deviceBatteryMapper), + new DeviceCleanMapDataReportEventHandler(), + new DeviceCleanMapReportEventHandler(), + new DeviceCleanTaskReportEventHandler(), + new DeviceGetAllGlobalMapEventHandler(), + new DeviceLocatedEventHandler(), + new DeviceLockedEventHandler(this.deviceRepository), + new DeviceMapChargerPositionUpdateEventHandler(), + new DeviceMapWorkStatusUpdateEventHandler( + deviceStateMapper, + deviceModeMapper, + deviceErrorMapper, + deviceBatteryMapper, + deviceFanSpeedMapper, + deviceWaterLevelMapper, + ), + new DeviceMemoryMapInfoEventHandler(), + new DeviceOfflineEventHandler(), + new DeviceRegisterEventHandler(this.deviceRepository), + new DeviceSettingsUpdateEventHandler(deviceVoiceMapper), + new DeviceTimeUpdateEventHandler(), + new DeviceUpgradeInfoEventHandler(), + new DeviceVersionUpdateEventHandler(), + new DeviceWlanUpdateEventHandler(), + new DeviceMapUpdateEventHandler( + deviceBatteryMapper, + deviceModeMapper, + deviceStateMapper, + deviceErrorMapper, + deviceFanSpeedMapper, + ), + ); + + // Domain event handlers + this.domainEventHandlerRegistry.register( + new LockDeviceWhenDeviceIsConnectedEventHandler(connectionManager), + new QueryDeviceInfoWhenDeviceIsLockedEventHandler(connectionManager), + ); + + // Command event handlers + this.commandEventHandlerRegistry.register(new LocateDeviceEventHandler(connectionManager)); + } + + async start(): Promise { + await Promise.all([this.cmdServer.listen(4010), this.mapServer.listen(4030), this.timeSyncServer.listen()]); + } + + async stop(): Promise { + await Promise.all([this.cmdServer.close(), this.mapServer.close(), this.timeSyncServer.close()]); + } +} diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts new file mode 100644 index 00000000..caa1063d --- /dev/null +++ b/packages/domain/src/commands/commands.ts @@ -0,0 +1,7 @@ +import type { LocateDeviceCommand } from './locate-device.command'; + +export type CommandEvents = { + LocateDeviceCommand: LocateDeviceCommand; +}; + +export type CommandEventNames = keyof CommandEvents; diff --git a/packages/domain/src/commands/locate-device.command.ts b/packages/domain/src/commands/locate-device.command.ts new file mode 100644 index 00000000..bbc6ab81 --- /dev/null +++ b/packages/domain/src/commands/locate-device.command.ts @@ -0,0 +1,16 @@ +import { Command } from '@agnoc/toolkit'; +import type { ID } from '@agnoc/toolkit'; + +export interface LocateDeviceCommandProps { + deviceId: ID; +} + +export class LocateDeviceCommand extends Command { + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(_: LocateDeviceCommandProps): void { + // noop + } +} diff --git a/packages/domain/src/events/device-connected.domain-event.test.ts b/packages/domain/src/domain-events/device-connected.domain-event.test.ts similarity index 100% rename from packages/domain/src/events/device-connected.domain-event.test.ts rename to packages/domain/src/domain-events/device-connected.domain-event.test.ts diff --git a/packages/domain/src/events/device-connected.domain-event.ts b/packages/domain/src/domain-events/device-connected.domain-event.ts similarity index 100% rename from packages/domain/src/events/device-connected.domain-event.ts rename to packages/domain/src/domain-events/device-connected.domain-event.ts diff --git a/packages/domain/src/events/device-locked.domain-event.ts b/packages/domain/src/domain-events/device-locked.domain-event.ts similarity index 100% rename from packages/domain/src/events/device-locked.domain-event.ts rename to packages/domain/src/domain-events/device-locked.domain-event.ts diff --git a/packages/domain/src/events/index.ts b/packages/domain/src/domain-events/domain-events.ts similarity index 100% rename from packages/domain/src/events/index.ts rename to packages/domain/src/domain-events/domain-events.ts diff --git a/packages/domain/src/entities/device.entity.ts b/packages/domain/src/entities/device.entity.ts index 5d124c30..581c6ddb 100644 --- a/packages/domain/src/entities/device.entity.ts +++ b/packages/domain/src/entities/device.entity.ts @@ -1,12 +1,12 @@ import { AggregateRoot, ID } from '@agnoc/toolkit'; +import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; import { DeviceMode } from '../domain-primitives/device-mode.domain-primitive'; import { DeviceState } from '../domain-primitives/device-state.domain-primitive'; import { DeviceWaterLevel } from '../domain-primitives/device-water-level.domain-primitive'; -import { DeviceConnectedDomainEvent } from '../events/device-connected.domain-event'; -import { DeviceLockedDomainEvent } from '../events/device-locked.domain-event'; import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; diff --git a/packages/domain/src/event-buses/command.event-bus.ts b/packages/domain/src/event-buses/command.event-bus.ts new file mode 100644 index 00000000..35e8cdd3 --- /dev/null +++ b/packages/domain/src/event-buses/command.event-bus.ts @@ -0,0 +1,6 @@ +import { EventBus } from '@agnoc/toolkit'; +import type { CommandEventNames, CommandEvents } from '../commands/commands'; + +type CommandEventBusEvents = { [Name in CommandEventNames]: CommandEvents[Name] }; + +export class CommandEventBus extends EventBus {} diff --git a/packages/toolkit/src/event-buses/domain.event-bus.test.ts b/packages/domain/src/event-buses/domain.event-bus.test.ts similarity index 85% rename from packages/toolkit/src/event-buses/domain.event-bus.test.ts rename to packages/domain/src/event-buses/domain.event-bus.test.ts index c53f7619..1bb4872a 100644 --- a/packages/toolkit/src/event-buses/domain.event-bus.test.ts +++ b/packages/domain/src/event-buses/domain.event-bus.test.ts @@ -1,5 +1,5 @@ +import { EventBus } from '@agnoc/toolkit'; import { expect } from 'chai'; -import { EventBus } from '../base-classes/event-bus.base'; import { DomainEventBus } from './domain.event-bus'; describe('DomainEventBus', function () { diff --git a/packages/domain/src/event-buses/domain.event-bus.ts b/packages/domain/src/event-buses/domain.event-bus.ts new file mode 100644 index 00000000..55658453 --- /dev/null +++ b/packages/domain/src/event-buses/domain.event-bus.ts @@ -0,0 +1,6 @@ +import { EventBus } from '@agnoc/toolkit'; +import type { DomainEventNames, DomainEvents } from '../domain-events/domain-events'; + +type DomainEventBusEvents = { [Name in DomainEventNames]: DomainEvents[Name] }; + +export class DomainEventBus extends EventBus {} diff --git a/packages/domain/src/event-handlers/command.event-handler.ts b/packages/domain/src/event-handlers/command.event-handler.ts new file mode 100644 index 00000000..857556e8 --- /dev/null +++ b/packages/domain/src/event-handlers/command.event-handler.ts @@ -0,0 +1,7 @@ +import type { CommandEventNames, CommandEvents } from '../commands/commands'; +import type { EventHandler } from '@agnoc/toolkit'; + +export abstract class CommandEventHandler implements EventHandler { + abstract eventName: CommandEventNames; + abstract handle(event: CommandEvents[this['eventName']]): void; +} diff --git a/packages/domain/src/event-handlers/domain.event-handler.ts b/packages/domain/src/event-handlers/domain.event-handler.ts index 07fcd318..c5f492df 100644 --- a/packages/domain/src/event-handlers/domain.event-handler.ts +++ b/packages/domain/src/event-handlers/domain.event-handler.ts @@ -1,4 +1,4 @@ -import type { DomainEventNames, DomainEvents } from '../events'; +import type { DomainEventNames, DomainEvents } from '../domain-events/domain-events'; import type { EventHandler } from '@agnoc/toolkit'; /** Base class for domain event handlers. */ diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 692d51ae..a65934c9 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -1,3 +1,8 @@ +export * from './commands/commands'; +export * from './commands/locate-device.command'; +export * from './domain-events/device-connected.domain-event'; +export * from './domain-events/device-locked.domain-event'; +export * from './domain-events/domain-events'; export * from './domain-primitives/clean-mode.domain-primitive'; export * from './domain-primitives/clean-size.domain-primitive'; export * from './domain-primitives/device-battery.domain-primitive'; @@ -12,10 +17,10 @@ export * from './entities/device-order.entity'; export * from './entities/device.entity'; export * from './entities/room.entity'; export * from './entities/zone.entity'; +export * from './event-buses/command.event-bus'; +export * from './event-buses/domain.event-bus'; +export * from './event-handlers/command.event-handler'; export * from './event-handlers/domain.event-handler'; -export * from './events'; -export * from './events/device-connected.domain-event'; -export * from './events/device-locked.domain-event'; export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; export * from './value-objects/device-consumable.value-object'; diff --git a/packages/domain/src/repositories/device.repository.test.ts b/packages/domain/src/repositories/device.repository.test.ts index 52680816..987328da 100644 --- a/packages/domain/src/repositories/device.repository.test.ts +++ b/packages/domain/src/repositories/device.repository.test.ts @@ -2,17 +2,17 @@ import { Repository } from '@agnoc/toolkit'; import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceRepository } from './device.repository'; -import type { Adapter, DomainEventBus } from '@agnoc/toolkit'; +import type { Adapter, EventBus } from '@agnoc/toolkit'; describe('DeviceRepository', function () { - let domainEventBus: DomainEventBus; + let eventBus: EventBus; let adapter: Adapter; let repository: DeviceRepository; beforeEach(function () { - domainEventBus = imock(); + eventBus = imock(); adapter = imock(); - repository = new DeviceRepository(instance(domainEventBus), instance(adapter)); + repository = new DeviceRepository(instance(eventBus), instance(adapter)); }); it('should be a repository', function () { diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 4a653764..c84529b8 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -70,6 +70,7 @@ "tslib": "^2.5.0" }, "devDependencies": { + "@johanblumenberg/ts-mockito": "^1.0.35", "chai": "^4.3.7" }, "engines": { diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts index 0d0ff751..333a7eff 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts @@ -6,13 +6,13 @@ import { DomainEvent } from './domain-event.base'; import { Entity } from './entity.base'; import type { DomainEventProps } from './domain-event.base'; import type { EntityProps } from './entity.base'; -import type { DomainEventBus } from '../event-buses/domain.event-bus'; +import type { EventBus } from './event-bus.base'; describe('AggregateRoot', function () { - let domainEventBus: DomainEventBus; + let eventBus: EventBus; beforeEach(function () { - domainEventBus = imock(); + eventBus = imock(); }); it('should be created', function () { @@ -34,15 +34,15 @@ describe('AggregateRoot', function () { const id = ID.generate(); const dummyAggregateRoot = new DummyAggregateRoot({ id }); - when(domainEventBus.emit(anything(), anything())).thenResolve(); + when(eventBus.emit(anything(), anything())).thenResolve(); dummyAggregateRoot.doSomething(); - await dummyAggregateRoot.publishEvents(instance(domainEventBus)); + await dummyAggregateRoot.publishEvents(instance(eventBus)); expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(0); - verify(domainEventBus.emit('DummyDomainEvent', deepEqual(new DummyDomainEvent({ aggregateId: id })))).once(); + verify(eventBus.emit('DummyDomainEvent', deepEqual(new DummyDomainEvent({ aggregateId: id })))).once(); }); it('should be able to clear domain events', function () { diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts index 5a28c3bf..61d69b2b 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -2,7 +2,7 @@ import { debug } from '../utils/debug.util'; import { Entity } from './entity.base'; import type { DomainEvent } from './domain-event.base'; import type { EntityProps } from './entity.base'; -import type { DomainEventBus } from '../event-buses/domain.event-bus'; +import type { EventBus } from './event-bus.base'; export abstract class AggregateRoot extends Entity { private readonly debug = debug(__filename).extend(`${this.constructor.name.toLowerCase()}:${this.id.value}`); @@ -16,11 +16,11 @@ export abstract class AggregateRoot extends this.#domainEvents.clear(); } - async publishEvents(domainEventBus: DomainEventBus): Promise { + async publishEvents(eventBus: EventBus): Promise { await Promise.all( this.domainEvents.map(async (event) => { this.debug(`publishing domain event '${event.constructor.name}'`); - return domainEventBus.emit(event.constructor.name, event); + return eventBus.emit(event.constructor.name, event); }), ); diff --git a/packages/toolkit/src/base-classes/command.base.ts b/packages/toolkit/src/base-classes/command.base.ts new file mode 100644 index 00000000..a92f79b9 --- /dev/null +++ b/packages/toolkit/src/base-classes/command.base.ts @@ -0,0 +1,10 @@ +import { Validatable } from './validatable.base'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CommandProps {} + +export abstract class Command extends Validatable { + constructor(protected readonly props: T) { + super(props); + } +} diff --git a/packages/toolkit/src/base-classes/event-bus.base.ts b/packages/toolkit/src/base-classes/event-bus.base.ts index 31682628..351f7023 100644 --- a/packages/toolkit/src/base-classes/event-bus.base.ts +++ b/packages/toolkit/src/base-classes/event-bus.base.ts @@ -1,5 +1,4 @@ import Emittery from 'emittery'; -export type EventBusEvents = { [key: string]: unknown }; - -export abstract class EventBus extends Emittery {} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export abstract class EventBus extends Emittery {} diff --git a/packages/toolkit/src/base-classes/repository.base.test.ts b/packages/toolkit/src/base-classes/repository.base.test.ts index 26a0dfb9..174712be 100644 --- a/packages/toolkit/src/base-classes/repository.base.test.ts +++ b/packages/toolkit/src/base-classes/repository.base.test.ts @@ -5,17 +5,17 @@ import { AggregateRoot } from './aggregate-root.base'; import { Repository } from './repository.base'; import type { Adapter } from './adapter.base'; import type { EntityProps } from './entity.base'; -import type { DomainEventBus } from '../event-buses/domain.event-bus'; +import type { EventBus } from './event-bus.base'; describe('Repository', function () { - let domainEventBus: DomainEventBus; + let eventBus: EventBus; let adapter: Adapter; let dummyRepository: DummyRepository; beforeEach(function () { - domainEventBus = imock(); + eventBus = imock(); adapter = imock(); - dummyRepository = new DummyRepository(instance(domainEventBus), instance(adapter)); + dummyRepository = new DummyRepository(instance(eventBus), instance(adapter)); }); it('should find one by id', async function () { @@ -52,7 +52,7 @@ describe('Repository', function () { await dummyRepository.saveOne(entity); verify(adapter.set(id, entity)).once(); - verify(entitySpy.publishEvents(instance(domainEventBus))).once(); + verify(entitySpy.publishEvents(instance(eventBus))).once(); }); it('should delete one', async function () { @@ -63,7 +63,7 @@ describe('Repository', function () { await dummyRepository.deleteOne(entity); verify(adapter.delete(id)).once(); - verify(entitySpy.publishEvents(instance(domainEventBus))).once(); + verify(entitySpy.publishEvents(instance(eventBus))).once(); }); }); diff --git a/packages/toolkit/src/base-classes/repository.base.ts b/packages/toolkit/src/base-classes/repository.base.ts index 7d88d298..9e42db80 100644 --- a/packages/toolkit/src/base-classes/repository.base.ts +++ b/packages/toolkit/src/base-classes/repository.base.ts @@ -2,11 +2,11 @@ import type { Adapter } from './adapter.base'; import type { AggregateRoot } from './aggregate-root.base'; import type { EntityProps } from './entity.base'; +import type { EventBus } from './event-bus.base'; import type { ID } from '../domain-primitives/id.domain-primitive'; -import type { DomainEventBus } from '../event-buses/domain.event-bus'; export abstract class Repository> { - constructor(private readonly domainEventBus: DomainEventBus, private readonly adapter: Adapter) {} + constructor(private readonly eventBus: EventBus, private readonly adapter: Adapter) {} async findOneById(id: ID): Promise { return this.adapter.get(id) as T | undefined; @@ -18,11 +18,11 @@ export abstract class Repository> { async saveOne(entity: T): Promise { this.adapter.set(entity.id, entity); - await entity.publishEvents(this.domainEventBus); + await entity.publishEvents(this.eventBus); } async deleteOne(entity: T): Promise { this.adapter.delete(entity.id); - await entity.publishEvents(this.domainEventBus); + await entity.publishEvents(this.eventBus); } } diff --git a/packages/toolkit/src/event-buses/domain.event-bus.ts b/packages/toolkit/src/event-buses/domain.event-bus.ts deleted file mode 100644 index bee79353..00000000 --- a/packages/toolkit/src/event-buses/domain.event-bus.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { EventBus } from '../base-classes/event-bus.base'; -import type { DomainEvent } from '../base-classes/domain-event.base'; - -type DomainEvents = { [key: string]: DomainEvent }; - -export class DomainEventBus extends EventBus {} diff --git a/packages/adapter-tcp/src/event-handler.manager.test.ts b/packages/toolkit/src/event-handler.registry.test.ts similarity index 80% rename from packages/adapter-tcp/src/event-handler.manager.test.ts rename to packages/toolkit/src/event-handler.registry.test.ts index 405bb43b..01077fb8 100644 --- a/packages/adapter-tcp/src/event-handler.manager.test.ts +++ b/packages/toolkit/src/event-handler.registry.test.ts @@ -1,17 +1,17 @@ import { anything, capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { EventHandlerManager } from './event-handler.manager'; +import { EventHandlerRegistry } from './event-handler.registry'; import type { EventBus, EventHandler } from '@agnoc/toolkit'; -describe('EventHandlerManager', function () { +describe('EventHandlerRegistry', function () { let eventBus: EventBus; let eventHandler: EventHandler; - let eventHandlerManager: EventHandlerManager; + let eventHandlerManager: EventHandlerRegistry; beforeEach(function () { eventBus = imock(); eventHandler = imock(); - eventHandlerManager = new EventHandlerManager(instance(eventBus)); + eventHandlerManager = new EventHandlerRegistry(instance(eventBus)); }); it('should listen for events on the bus', function () { diff --git a/packages/adapter-tcp/src/event-handler.manager.ts b/packages/toolkit/src/event-handler.registry.ts similarity index 72% rename from packages/adapter-tcp/src/event-handler.manager.ts rename to packages/toolkit/src/event-handler.registry.ts index 053739ab..f4624998 100644 --- a/packages/adapter-tcp/src/event-handler.manager.ts +++ b/packages/toolkit/src/event-handler.registry.ts @@ -1,7 +1,8 @@ -import type { EventBus, EventHandler } from '@agnoc/toolkit'; +import type { EventBus } from './base-classes/event-bus.base'; +import type { EventHandler } from './base-classes/event-handler.base'; /** Manages event handlers. */ -export class EventHandlerManager { +export class EventHandlerRegistry { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(private readonly eventBus: EventBus) {} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 5dd962eb..c879e725 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -1,6 +1,7 @@ export * from './adapters/memory.adapter'; export * from './base-classes/adapter.base'; export * from './base-classes/aggregate-root.base'; +export * from './base-classes/command.base'; export * from './base-classes/domain-event.base'; export * from './base-classes/domain-primitive.base'; export * from './base-classes/entity.base'; @@ -14,7 +15,7 @@ export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; export * from './decorators/bind.decorator'; export * from './domain-primitives/id.domain-primitive'; -export * from './event-buses/domain.event-bus'; +export * from './event-handler.registry'; export * from './exceptions/argument-invalid.exception'; export * from './exceptions/argument-not-provided.exception'; export * from './exceptions/argument-out-of-range.exception'; diff --git a/packages/transport-tcp/src/packet.server.ts b/packages/transport-tcp/src/packet.server.ts index d089759b..992bb8a1 100644 --- a/packages/transport-tcp/src/packet.server.ts +++ b/packages/transport-tcp/src/packet.server.ts @@ -20,11 +20,11 @@ export interface PacketServerEvents { /** Server that emits `PacketSockets`. */ export class PacketServer extends Emittery { - private server: Server; + private readonly sockets = new Set(); + private readonly server = new Server(); constructor(private readonly packetMapper: PacketMapper) { super(); - this.server = new Server(); this.addListeners(); } @@ -66,19 +66,29 @@ export class PacketServer extends Emittery { resolve(); }); + + this.sockets.forEach((socket) => void socket.end()); }); } private onConnection(socket: Socket): void { - const client = new PacketSocket(this.packetMapper, socket); + const packetSocket = new PacketSocket(this.packetMapper, socket); + + this.sockets.add(packetSocket); - void this.emit('connection', client); + packetSocket.on('close', () => { + this.sockets.delete(packetSocket); + }); + + void this.emit('connection', packetSocket); } private addListeners(): void { this.server.on('connection', this.onConnection.bind(this)); this.server.on('listening', () => void this.emit('listening')); - this.server.on('close', () => void this.emit('close')); + this.server.on('close', () => { + void this.emit('close'); + }); /* istanbul ignore next - unable to test */ this.server.on('error', (error) => void this.emit('error', error)); /* istanbul ignore next - unable to test */ From c9dc65f8227b5337e60f4f0e720eeecea7900c76 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Mon, 20 Mar 2023 12:34:08 +0100 Subject: [PATCH 08/38] test(adapter-tcp): add integration tests --- packages/adapter-tcp/package.json | 5 +- .../locate-device.event-handler.ts | 4 +- ...s-connected-event-handler.event-handler.ts | 4 +- ...e-is-locked-event-handler.event-handler.ts | 4 +- packages/adapter-tcp/src/index.ts | 1 + .../src/ntp-server.connection-handler.test.ts | 75 ++++++ .../src/ntp-server.connection-handler.ts | 36 +++ ...ts => packet-server.connection-handler.ts} | 2 +- packages/adapter-tcp/src/tcp.adapter.ts | 52 +++- .../adapter-tcp/src/time-sync.server.test.ts | 70 ------ packages/adapter-tcp/src/time-sync.server.ts | 31 --- .../test/integration/tcp.adapter.test.ts | 223 ++++++++++++++++++ .../toolkit/src/event-handler.registry.ts | 3 +- .../transport-tcp/src/packet.socket.test.ts | 19 ++ packages/transport-tcp/src/packet.socket.ts | 46 +++- 15 files changed, 445 insertions(+), 130 deletions(-) create mode 100644 packages/adapter-tcp/src/ntp-server.connection-handler.test.ts create mode 100644 packages/adapter-tcp/src/ntp-server.connection-handler.ts rename packages/adapter-tcp/src/{connection.manager.ts => packet-server.connection-handler.ts} (98%) delete mode 100644 packages/adapter-tcp/src/time-sync.server.test.ts delete mode 100644 packages/adapter-tcp/src/time-sync.server.ts create mode 100644 packages/adapter-tcp/test/integration/tcp.adapter.test.ts diff --git a/packages/adapter-tcp/package.json b/packages/adapter-tcp/package.json index 0eb9419b..5df0b35f 100644 --- a/packages/adapter-tcp/package.json +++ b/packages/adapter-tcp/package.json @@ -36,7 +36,7 @@ "build:lib": "tsc -b tsconfig.build.json", "clean": "rm -rf .build-cache lib coverage", "posttest": "test \"$(cat coverage/coverage-summary.json | json total.lines.total)\" -gt 0", - "test": "nyc mocha src" + "test": "nyc mocha src test" }, "nx": { "targets": { @@ -54,6 +54,9 @@ ] }, "test": { + "dependsOn": [ + "build" + ], "inputs": [ "{workspaceRoot}/.mocharc.yml", "{workspaceRoot}/nyc.config.js", diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts index 573541e8..655c1cc3 100644 --- a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts @@ -1,11 +1,11 @@ import { DomainException } from '@agnoc/toolkit'; -import type { ConnectionManager } from '../../connection.manager'; +import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; import type { CommandEventHandler, LocateDeviceCommand } from '@agnoc/domain'; export class LocateDeviceEventHandler implements CommandEventHandler { readonly eventName = 'LocateDeviceCommand'; - constructor(private readonly connectionManager: ConnectionManager) {} + constructor(private readonly connectionManager: PackerServerConnectionHandler) {} async handle(event: LocateDeviceCommand): Promise { const [connection] = this.connectionManager.findConnectionsByDeviceId(event.deviceId); diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index d8a0800f..4eda104b 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -1,11 +1,11 @@ import { DomainException } from '@agnoc/toolkit'; -import type { ConnectionManager } from '../../connection.manager'; +import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { readonly eventName = 'DeviceConnectedDomainEvent'; - constructor(private readonly connectionManager: ConnectionManager) {} + constructor(private readonly connectionManager: PackerServerConnectionHandler) {} async handle(event: DeviceConnectedDomainEvent): Promise { const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index 661396b0..4b49aedc 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -1,12 +1,12 @@ import { DeviceCapability } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { ConnectionManager } from '../../connection.manager'; +import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { readonly eventName = 'DeviceLockedDomainEvent'; - constructor(private readonly connectionManager: ConnectionManager) {} + constructor(private readonly connectionManager: PackerServerConnectionHandler) {} async handle(event: DeviceLockedDomainEvent): Promise { const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); diff --git a/packages/adapter-tcp/src/index.ts b/packages/adapter-tcp/src/index.ts index 707f9c1b..14d4180c 100644 --- a/packages/adapter-tcp/src/index.ts +++ b/packages/adapter-tcp/src/index.ts @@ -11,3 +11,4 @@ export * from './mappers/device-state.mapper'; export * from './mappers/device-voice.mapper'; export * from './mappers/device-water-level.mapper'; export * from './value-objects/message.value-object'; +export * from './tcp.adapter'; diff --git a/packages/adapter-tcp/src/ntp-server.connection-handler.test.ts b/packages/adapter-tcp/src/ntp-server.connection-handler.test.ts new file mode 100644 index 00000000..e9e59f61 --- /dev/null +++ b/packages/adapter-tcp/src/ntp-server.connection-handler.test.ts @@ -0,0 +1,75 @@ +import { ID } from '@agnoc/toolkit'; +import { Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { + anything, + capture, + deepEqual, + defer, + greaterThanOrEqual, + imock, + instance, + verify, + when, +} from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; +import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; + +describe('TimeSyncServer', function () { + let packetServer: PacketServer; + let packetFactory: PacketFactory; + let packetSocket: PacketSocket; + let rtpPacketServer: NTPServerConnectionHandler; + + beforeEach(function () { + packetServer = imock(); + packetFactory = imock(); + packetSocket = imock(); + rtpPacketServer = new NTPServerConnectionHandler(instance(packetFactory)); + }); + + describe('#register()', function () { + it('should register servers that handles connections', async function () { + const packet = new Packet(givenSomePacketProps()); + const now = Math.floor(Date.now() / 1000); + const onClose = defer(); + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + when(packetServer.once(anything())).thenReturn(onClose); + + rtpPacketServer.register(instance(packetServer)); + + verify(packetServer.on('connection', anything())).once(); + + const [event, handler] = capture(packetServer.on<'connection'>).first(); + + expect(event).to.be.equal('connection'); + + await handler(instance(packetSocket)); + + verify( + packetFactory.create( + 'DEVICE_TIME_SYNC_RSP', + deepEqual({ result: 0, body: { time: greaterThanOrEqual(now) } }), + deepEqual({ userId: new ID(0), deviceId: new ID(0) }), + ), + ).once(); + verify(packetSocket.end(packet)).once(); + }); + + it('should clean up listeners when the server is closed', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenReturn(onClose); + + rtpPacketServer.register(instance(packetServer)); + + const [, handler] = capture(packetServer.on<'connection'>).first(); + + await onClose.resolve(undefined); + + verify(packetServer.off('connection', handler)).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/ntp-server.connection-handler.ts b/packages/adapter-tcp/src/ntp-server.connection-handler.ts new file mode 100644 index 00000000..3c48aef8 --- /dev/null +++ b/packages/adapter-tcp/src/ntp-server.connection-handler.ts @@ -0,0 +1,36 @@ +import { ID } from '@agnoc/toolkit'; +import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; + +/** Handler for NTP server connections. */ +export class NTPServerConnectionHandler { + constructor(private readonly packetFactory: PacketFactory) {} + + /** + * Register a server to handle connections. + * + * This must be done before the server is started. + */ + register(...servers: PacketServer[]): void { + servers.forEach((server) => { + this.addListeners(server); + }); + } + + private async handleConnection(socket: PacketSocket) { + const props = { userId: new ID(0), deviceId: new ID(0) }; + const payload = { result: 0, body: { time: Math.floor(Date.now() / 1000) } }; + const packet = this.packetFactory.create('DEVICE_TIME_SYNC_RSP', payload, props); + + return socket.end(packet); + } + + private addListeners(packetServer: PacketServer) { + const onConnection = this.handleConnection.bind(this); + + packetServer.on('connection', onConnection); + + void packetServer.once('close').then(() => { + packetServer.off('connection', onConnection); + }); + } +} diff --git a/packages/adapter-tcp/src/connection.manager.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts similarity index 98% rename from packages/adapter-tcp/src/connection.manager.ts rename to packages/adapter-tcp/src/packet-server.connection-handler.ts index 43497170..8b7c693c 100644 --- a/packages/adapter-tcp/src/connection.manager.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -6,7 +6,7 @@ import type { DeviceRepository, Device } from '@agnoc/domain'; import type { ID } from '@agnoc/toolkit'; import type { PacketServer, PacketFactory, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; -export class ConnectionManager { +export class PackerServerConnectionHandler { private readonly servers = new Map>(); constructor( diff --git a/packages/adapter-tcp/src/tcp.adapter.ts b/packages/adapter-tcp/src/tcp.adapter.ts index 3ec42621..54082b56 100644 --- a/packages/adapter-tcp/src/tcp.adapter.ts +++ b/packages/adapter-tcp/src/tcp.adapter.ts @@ -8,7 +8,6 @@ import { PayloadObjectParserService, PacketFactory, } from '@agnoc/transport-tcp'; -import { ConnectionManager } from './connection.manager'; import { LocateDeviceEventHandler } from './event-handlers/command-event-handlers/locate-device.event-handler'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; @@ -39,14 +38,16 @@ import { DeviceModeMapper } from './mappers/device-mode.mapper'; import { DeviceStateMapper } from './mappers/device-state.mapper'; import { DeviceVoiceMapper } from './mappers/device-voice.mapper'; import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; +import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; +import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; -import { TimeSyncServer } from './time-sync.server'; import type { DeviceRepository } from '@agnoc/domain'; +import type { AddressInfo } from 'net'; export class TCPAdapter { - private readonly timeSyncServer: TimeSyncServer; private readonly cmdServer: PacketServer; private readonly mapServer: PacketServer; + private readonly ntpServer: PacketServer; constructor( private readonly deviceRepository: DeviceRepository, @@ -59,7 +60,7 @@ export class TCPAdapter { const packetFactory = new PacketFactory(); // Servers - this.timeSyncServer = new TimeSyncServer(new PacketServer(packetMapper), packetFactory); + this.ntpServer = new PacketServer(packetMapper); this.cmdServer = new PacketServer(packetMapper); this.mapServer = new PacketServer(packetMapper); @@ -77,10 +78,15 @@ export class TCPAdapter { const packetEventHandlerRegistry = new EventHandlerRegistry(packetEventBus); // Connection managers - const connectionManager = new ConnectionManager(packetEventBus, packetFactory, this.deviceRepository); + const connectionManager = new PackerServerConnectionHandler(packetEventBus, packetFactory, this.deviceRepository); connectionManager.addServers(this.cmdServer, this.mapServer); + // Time Sync server controller + const ntpServerConnectionHandler = new NTPServerConnectionHandler(packetFactory); + + ntpServerConnectionHandler.register(this.ntpServer); + // Packet event handlers packetEventHandlerRegistry.register( new ClientHeartbeatEventHandler(), @@ -128,11 +134,39 @@ export class TCPAdapter { this.commandEventHandlerRegistry.register(new LocateDeviceEventHandler(connectionManager)); } - async start(): Promise { - await Promise.all([this.cmdServer.listen(4010), this.mapServer.listen(4030), this.timeSyncServer.listen()]); + async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { + await Promise.all([ + this.cmdServer.listen(options.ports.cmd), + this.mapServer.listen(options.ports.map), + this.ntpServer.listen(options.ports.ntp), + ]); + + return { + ports: { + cmd: (this.cmdServer.address as AddressInfo).port, + map: (this.mapServer.address as AddressInfo).port, + ntp: (this.ntpServer.address as AddressInfo).port, + }, + }; } - async stop(): Promise { - await Promise.all([this.cmdServer.close(), this.mapServer.close(), this.timeSyncServer.close()]); + async close(): Promise { + await Promise.all([this.cmdServer.close(), this.mapServer.close(), this.ntpServer.close()]); } } + +const listenDefaultOptions: TCPAdapterListenOptions = { ports: { cmd: 4010, map: 4030, ntp: 4050 } }; + +export interface TCPAdapterListenOptions { + ports: ServerPorts; +} + +interface TCPAdapterListenReturn { + ports: ServerPorts; +} + +export interface ServerPorts { + cmd: number; + map: number; + ntp: number; +} diff --git a/packages/adapter-tcp/src/time-sync.server.test.ts b/packages/adapter-tcp/src/time-sync.server.test.ts deleted file mode 100644 index 00a67c97..00000000 --- a/packages/adapter-tcp/src/time-sync.server.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ID } from '@agnoc/toolkit'; -import { Packet } from '@agnoc/transport-tcp'; -import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; -import { - anything, - capture, - deepEqual, - greaterThanOrEqual, - imock, - instance, - verify, - when, -} from '@johanblumenberg/ts-mockito'; -import { expect } from 'chai'; -import { TimeSyncServer } from './time-sync.server'; -import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; - -describe('TimeSyncServer', function () { - let packetServer: PacketServer; - let packetFactory: PacketFactory; - let packetSocket: PacketSocket; - let rtpPacketServer: TimeSyncServer; - - beforeEach(function () { - packetServer = imock(); - packetFactory = imock(); - packetSocket = imock(); - rtpPacketServer = new TimeSyncServer(instance(packetServer), instance(packetFactory)); - }); - - it('should handle new connections', async function () { - const packet = new Packet(givenSomePacketProps()); - const now = Math.floor(Date.now() / 1000); - - when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); - - verify(packetServer.on('connection', anything())).once(); - - const [event, handler] = capture(packetServer.on<'connection'>).first(); - - expect(event).to.be.equal('connection'); - - await handler(instance(packetSocket)); - - verify( - packetFactory.create( - 'DEVICE_TIME_SYNC_RSP', - deepEqual({ result: 0, body: { time: greaterThanOrEqual(now) } }), - deepEqual({ userId: new ID(0), deviceId: new ID(0) }), - ), - ).once(); - verify(packetSocket.end(packet)).once(); - }); - - describe('#listen()', function () { - it('should listen on port 4050', async function () { - await rtpPacketServer.listen(); - - verify(packetServer.listen(4050)).once(); - }); - }); - - describe('#close()', function () { - it('should close the server', async function () { - await rtpPacketServer.close(); - - verify(packetServer.close()).once(); - }); - }); -}); diff --git a/packages/adapter-tcp/src/time-sync.server.ts b/packages/adapter-tcp/src/time-sync.server.ts deleted file mode 100644 index 78ca0be6..00000000 --- a/packages/adapter-tcp/src/time-sync.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ID } from '@agnoc/toolkit'; -import type { PacketServer, PacketFactory, PacketSocket } from '@agnoc/transport-tcp'; - -/** Device time synchronization server implementation. */ -export class TimeSyncServer { - constructor(private readonly packetServer: PacketServer, private readonly packetFactory: PacketFactory) { - this.addListeners(); - } - - /** Start listening for incoming connections. */ - async listen(): Promise { - return this.packetServer.listen(4050); - } - - /** Close the server. */ - async close(): Promise { - return this.packetServer.close(); - } - - private async handleConnection(socket: PacketSocket) { - const props = { userId: new ID(0), deviceId: new ID(0) }; - const payload = { result: 0, body: { time: Math.floor(Date.now() / 1000) } }; - const packet = this.packetFactory.create('DEVICE_TIME_SYNC_RSP', payload, props); - - return socket.end(packet); - } - - private addListeners() { - this.packetServer.on('connection', this.handleConnection.bind(this)); - } -} diff --git a/packages/adapter-tcp/test/integration/tcp.adapter.test.ts b/packages/adapter-tcp/test/integration/tcp.adapter.test.ts new file mode 100644 index 00000000..b42c78ed --- /dev/null +++ b/packages/adapter-tcp/test/integration/tcp.adapter.test.ts @@ -0,0 +1,223 @@ +import { once } from 'events'; +import { CommandEventBus, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { EventHandlerRegistry, ID, MemoryAdapter } from '@agnoc/toolkit'; +import { + getCustomDecoders, + getProtobufRoot, + PacketFactory, + PacketMapper, + PacketSocket, + PayloadMapper, + PayloadObjectParserService, +} from '@agnoc/transport-tcp'; +import { expect } from 'chai'; +import { TCPAdapter } from '@agnoc/adapter-tcp'; +import type { TCPAdapterListenOptions } from '@agnoc/adapter-tcp'; +import type { ICLIENT_ONLINE_REQ, IDEVICE_REGISTER_REQ } from '@agnoc/schemas-tcp'; +import type { CreatePacketProps, Packet } from '@agnoc/transport-tcp'; + +describe('TCPAdapter', function () { + let domainEventBus: DomainEventBus; + let commandEventBus: CommandEventBus; + let domainEventHandlerRegistry: EventHandlerRegistry; + let commandEventHandlerRegistry: EventHandlerRegistry; + let deviceRepository: DeviceRepository; + let tcpAdapter: TCPAdapter; + let packetSocket: PacketSocket; + let secondPacketSocket: PacketSocket; + let packetFactory: PacketFactory; + + beforeEach(function () { + // Server blocks + domainEventBus = new DomainEventBus(); + commandEventBus = new CommandEventBus(); + + domainEventHandlerRegistry = new EventHandlerRegistry(domainEventBus); + commandEventHandlerRegistry = new EventHandlerRegistry(commandEventBus); + deviceRepository = new DeviceRepository(domainEventBus, new MemoryAdapter()); + tcpAdapter = new TCPAdapter(deviceRepository, domainEventHandlerRegistry, commandEventHandlerRegistry); + + // Client blocks + const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); + const packetMapper = new PacketMapper(payloadMapper); + + packetSocket = new PacketSocket(packetMapper); + secondPacketSocket = new PacketSocket(packetMapper); + packetFactory = new PacketFactory(); + }); + + afterEach(async function () { + await tcpAdapter.close(); + }); + + describe('packet validation', function () { + it('should handle heartbeat packets on cmd server', async function () { + const createPacketProps = givenSomeCreatePacketProps(); + const sentPacket = packetFactory.create('CLIENT_HEARTBEAT_REQ', {}, createPacketProps); + const { ports } = await tcpAdapter.listen(givenSomeServerPorts()); + + await packetSocket.connect(ports.cmd); + + void packetSocket.write(sentPacket); + + const [receivedPacket] = (await once(packetSocket, 'data')) as Packet[]; + + expect(receivedPacket).to.exist; + expect(receivedPacket.ctype).to.be.equal(sentPacket.ctype); + expect(receivedPacket.sequence.equals(sentPacket.sequence)).to.be.true; + expect(receivedPacket.flow).to.be.equal(1); + expect(receivedPacket.deviceId.equals(sentPacket.deviceId)).to.be.true; + expect(receivedPacket.userId.equals(sentPacket.userId)).to.be.true; + expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); + expect(receivedPacket.payload.object).to.be.deep.equal({}); + }); + + it('should handle heartbeat packets on map server', async function () { + const createPacketProps = givenSomeCreatePacketProps(); + const sentPacket = packetFactory.create('CLIENT_HEARTBEAT_REQ', {}, createPacketProps); + const { ports } = await tcpAdapter.listen(givenSomeServerPorts()); + + await packetSocket.connect(ports.map); + + void packetSocket.write(sentPacket); + + const [receivedPacket] = (await once(packetSocket, 'data')) as Packet[]; + + expect(receivedPacket).to.exist; + expect(receivedPacket.ctype).to.be.equal(sentPacket.ctype); + expect(receivedPacket.sequence.equals(sentPacket.sequence)).to.be.true; + expect(receivedPacket.flow).to.be.equal(1); + expect(receivedPacket.deviceId.equals(sentPacket.deviceId)).to.be.true; + expect(receivedPacket.userId.equals(sentPacket.userId)).to.be.true; + expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); + expect(receivedPacket.payload.object).to.be.deep.equal({}); + }); + + it('should handle ntp connections', async function () { + const { ports } = await tcpAdapter.listen(givenSomeServerPorts()); + const now = Math.floor(Date.now() / 1000); + + await packetSocket.connect(ports.ntp); + + const [receivedPacket] = (await once(packetSocket, 'data')) as Packet<'DEVICE_TIME_SYNC_RSP'>[]; + + expect(receivedPacket).to.exist; + expect(receivedPacket.ctype).to.be.equal(2); + expect(receivedPacket.flow).to.be.equal(0); + expect(receivedPacket.deviceId.value).to.be.equal(0); + expect(receivedPacket.userId.value).to.be.equal(0); + expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_TIME_SYNC_RSP'); + expect(receivedPacket.payload.object.result).to.be.equal(0); + expect(receivedPacket.payload.object.body.time).to.be.greaterThanOrEqual(now); + }); + }); + + describe('flow validation', function () { + it('should handle a device registration', async function () { + const { ports } = await tcpAdapter.listen(givenSomeServerPorts()); + + await packetSocket.connect(ports.cmd); + + void packetSocket.write( + packetFactory.create('DEVICE_REGISTER_REQ', givenADeviceRegisterRequest(), givenSomeCreatePacketProps()), + ); + + const [receivedPacket] = (await once(packetSocket, 'data')) as Packet<'DEVICE_REGISTER_RSP'>[]; + + expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_REGISTER_RSP'); + + const device = await deviceRepository.findOneById(new ID(receivedPacket.payload.object.device.id)); + + expect(device).to.exist; + }); + + it('should handle a device connection', async function () { + const device = new Device(givenSomeDeviceProps()); + let receivedPacket: Packet; + + await deviceRepository.saveOne(device); + + const { ports } = await tcpAdapter.listen(givenSomeServerPorts()); + + await packetSocket.connect(ports.cmd); + + void packetSocket.write( + packetFactory.create('CLIENT_ONLINE_REQ', givenAClientOnlineRequest(), givenSomeCreatePacketProps(device)), + ); + + [receivedPacket] = (await once(packetSocket, 'data')) as Packet<'CLIENT_ONLINE_RSP'>[]; + + expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_ONLINE_RSP'); + + await secondPacketSocket.connect(ports.map); + + void secondPacketSocket.write( + packetFactory.create('CLIENT_HEARTBEAT_REQ', givenAClientOnlineRequest(), givenSomeCreatePacketProps(device)), + ); + + // The device already has two identified connections and + // the device should be marked as connected. + await domainEventBus.once('DeviceConnectedDomainEvent'); + + expect(device.isConnected).to.be.true; + + [receivedPacket] = (await once(secondPacketSocket, 'data')) as Packet<'CLIENT_HEARTBEAT_RSP'>[]; + + expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); + + expect(device.isConnected).to.be.true; + + [receivedPacket] = (await once(packetSocket, 'data')) as Packet<'DEVICE_CONTROL_LOCK_REQ'>[]; + + expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_CONTROL_LOCK_REQ'); + + void secondPacketSocket.write( + packetFactory.create('DEVICE_CONTROL_LOCK_RSP', { result: 0 }, givenSomeCreatePacketProps(device)), + ); + + // The device should be marked as locked. + await domainEventBus.once('DeviceLockedDomainEvent'); + + expect(device.isLocked).to.be.true; + }); + }); +}); + +function givenSomeServerPorts(): TCPAdapterListenOptions { + return { + ports: { + cmd: 0, + map: 0, + ntp: 0, + }, + }; +} + +function givenADeviceRegisterRequest(): IDEVICE_REGISTER_REQ { + return { + softwareVersion: 'S2.1.40.30.01R', + hardwareVersion: '1.0.1', + deviceSerialNumber: '1234567890', + deviceMac: '00:00:00:00:00:00', + deviceType: 9, + customerFirmwareId: 1003, + ctrlVersion: 'V4.0', + }; +} + +function givenAClientOnlineRequest(): ICLIENT_ONLINE_REQ { + return { + deviceSerialNumber: '1234567890', + unk1: false, + unk2: 0, + }; +} + +function givenSomeCreatePacketProps(device?: Device): CreatePacketProps { + // This properties should be inverted given that we are the client. + return { + deviceId: device?.userId ?? new ID(0), + userId: device?.id ?? new ID(0), + }; +} diff --git a/packages/toolkit/src/event-handler.registry.ts b/packages/toolkit/src/event-handler.registry.ts index f4624998..356861a3 100644 --- a/packages/toolkit/src/event-handler.registry.ts +++ b/packages/toolkit/src/event-handler.registry.ts @@ -3,8 +3,7 @@ import type { EventHandler } from './base-classes/event-handler.base'; /** Manages event handlers. */ export class EventHandlerRegistry { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(private readonly eventBus: EventBus) {} + constructor(private readonly eventBus: EventBus) {} register(...eventHandlers: EventHandler[]): void { eventHandlers.forEach((eventHandler) => this.addEventHandler(eventHandler)); diff --git a/packages/transport-tcp/src/packet.socket.test.ts b/packages/transport-tcp/src/packet.socket.test.ts index f4ef43da..295953a8 100644 --- a/packages/transport-tcp/src/packet.socket.test.ts +++ b/packages/transport-tcp/src/packet.socket.test.ts @@ -280,6 +280,25 @@ describe('PacketSocket', function () { server.listen(0); }); + + it('should throw an error when the socket is already connected', function (done) { + server.once('listening', () => { + void packetSocket.connect((server.address() as AddressInfo).port); + }); + + packetSocket.once('connect', async () => { + await expect(packetSocket.connect((server.address() as AddressInfo).port)).to.be.rejectedWith( + DomainException, + 'Socket is already connected', + ); + + await packetSocket.end(); + + done(); + }); + + server.listen(0); + }); }); describe('#write()', function () { diff --git a/packages/transport-tcp/src/packet.socket.ts b/packages/transport-tcp/src/packet.socket.ts index 99f2d6e9..4b614626 100644 --- a/packages/transport-tcp/src/packet.socket.ts +++ b/packages/transport-tcp/src/packet.socket.ts @@ -41,6 +41,10 @@ export class PacketSocket extends Duplex { connect(path: string): Promise; connect(options: SocketConnectOpts): Promise; async connect(portOrPathOrOptions: number | string | SocketConnectOpts, host?: string): Promise { + if (this.socket) { + throw new DomainException('Socket is already connected'); + } + const socket = new Socket(); this.wrapSocket(socket); @@ -156,18 +160,40 @@ export class PacketSocket extends Duplex { } private wrapSocket(socket: Socket): void { - this.socket = socket; - this.socket.on('close', (hadError) => this.emit('close', hadError)); - this.socket.on('connect', () => this.emit('connect')); - this.socket.on('end', () => this.emit('end')); - this.socket.on('error', (err) => this.emit('error', err)); - this.socket.on('lookup', (err, address, family, host) => this.emit('lookup', err, address, family, host)); // prettier-ignore - this.socket.on('ready', () => this.emit('ready')); - this.socket.on('readable', () => setImmediate(this.onReadable.bind(this))); + const onConnect: PacketSocketEvents['connect'] = () => this.emit('connect'); + const onEnd: PacketSocketEvents['end'] = () => this.emit('end'); + const onError: PacketSocketEvents['error'] = (err) => this.emit('error', err); + const onLookup: PacketSocketEvents['lookup'] = (err, address, family, host) => + this.emit('lookup', err, address, family, host); + const onReady: PacketSocketEvents['ready'] = () => this.emit('ready'); + const onReadable: PacketSocketEvents['readable'] = () => setImmediate(this.onReadable.bind(this)); /* istanbul ignore next - unable to test */ - this.socket.on('drain', () => this.emit('drain')); + const onDrain: PacketSocketEvents['drain'] = () => this.emit('drain'); /* istanbul ignore next - unable to test */ - this.socket.on('timeout', () => this.emit('timeout')); + const onTimeout: PacketSocketEvents['timeout'] = () => this.emit('timeout'); + const onClose: PacketSocketEvents['close'] = (hadError) => { + this.socket?.off('connect', onConnect); + this.socket?.off('end', onEnd); + this.socket?.off('error', onError); + this.socket?.off('lookup', onLookup); + this.socket?.off('ready', onReady); + this.socket?.off('readable', onReadable); + this.socket?.off('drain', onDrain); + this.socket?.off('timeout', onTimeout); + this.socket = undefined; + this.emit('close', hadError); + }; + + this.socket = socket; + this.socket.on('connect', onConnect); + this.socket.on('end', onEnd); + this.socket.on('error', onError); + this.socket.on('lookup', onLookup); + this.socket.on('ready', onReady); + this.socket.on('readable', onReadable); + this.socket.on('drain', onDrain); + this.socket.on('timeout', onTimeout); + this.socket.once('close', onClose); } private onReadable(): void { From 339541a9eeddc5d79422b5465bfbb55cbc52c514 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Mon, 20 Mar 2023 17:05:46 +0100 Subject: [PATCH 09/38] feat: add core package --- packages/adapter-tcp/package.json | 3 +- .../locate-device.event-handler.ts | 6 +- packages/adapter-tcp/src/index.ts | 2 +- .../src/packet-server.connection-handler.ts | 6 +- .../src/{tcp.adapter.ts => tcp.server.ts} | 3 +- ...tcp.adapter.test.ts => tcp.server.test.ts} | 6 +- packages/cli/package.json | 3 +- packages/cli/src/commands/read.command.ts | 1 + packages/core/.gitignore | 5 + packages/core/README.md | 3 + packages/core/package.json | 91 +++++++++++++++++++ packages/core/src/agnoc.server.ts | 57 ++++++++++++ packages/core/tsconfig.build.json | 15 +++ packages/domain/package.json | 1 + .../src/event-buses/command.event-bus.ts | 3 +- .../src/event-buses/domain.event-bus.ts | 3 +- packages/eslint-config/typescript.js | 9 ++ .../toolkit/src/base-classes/server.base.ts | 4 + packages/toolkit/src/index.ts | 1 + packages/transport-tcp/package.json | 1 + 20 files changed, 208 insertions(+), 15 deletions(-) rename packages/adapter-tcp/src/{tcp.adapter.ts => tcp.server.ts} (98%) rename packages/adapter-tcp/test/integration/{tcp.adapter.test.ts => tcp.server.test.ts} (97%) create mode 100644 packages/core/.gitignore create mode 100644 packages/core/README.md create mode 100644 packages/core/package.json create mode 100644 packages/core/src/agnoc.server.ts create mode 100644 packages/core/tsconfig.build.json create mode 100644 packages/toolkit/src/base-classes/server.base.ts diff --git a/packages/adapter-tcp/package.json b/packages/adapter-tcp/package.json index 5df0b35f..55b05280 100644 --- a/packages/adapter-tcp/package.json +++ b/packages/adapter-tcp/package.json @@ -71,6 +71,7 @@ "dependencies": { "@agnoc/domain": "^0.18.0-next.0", "@agnoc/schemas-tcp": "^0.16.0", + "@agnoc/transport-tcp": "^0.18.0-next.0", "@agnoc/toolkit": "^0.18.0-next.0", "debug": "^4.3.4", "tiny-typed-emitter": "^2.1.0", @@ -87,6 +88,6 @@ "typedoc": { "entryPoint": "./src/index.ts", "readmeFile": "./README.md", - "displayName": "adapter TCP" + "displayName": "Adapter TCP" } } diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts index 655c1cc3..f6762dfd 100644 --- a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts @@ -14,6 +14,10 @@ export class LocateDeviceEventHandler implements CommandEventHandler { throw new DomainException(`Unable to find a connection for the device with id ${event.deviceId.value}`); } - await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); + const response = await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); + + if (response.packet.payload.opcode.value !== 'DEVICE_SEEK_LOCATION_RSP') { + throw new DomainException(`Unexpected response from device: ${response.packet.payload.opcode.value}`); + } } } diff --git a/packages/adapter-tcp/src/index.ts b/packages/adapter-tcp/src/index.ts index 14d4180c..833bdc01 100644 --- a/packages/adapter-tcp/src/index.ts +++ b/packages/adapter-tcp/src/index.ts @@ -11,4 +11,4 @@ export * from './mappers/device-state.mapper'; export * from './mappers/device-voice.mapper'; export * from './mappers/device-water-level.mapper'; export * from './value-objects/message.value-object'; -export * from './tcp.adapter'; +export * from './tcp.server'; diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts index 8b7c693c..677a7bc3 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -40,12 +40,12 @@ export class PackerServerConnectionHandler { // Update the device on the connection if the device id has changed. await this.updateConnectionDevice(packet, connection); - // Send the packet message to the packet event bus. - await this.emitPacketEvent(packetMessage); - // This is a hack to only mark the device as connected if there is more than one connection. // Here we should check that the connections are from the same ip address. await this.tryToSetDeviceAsConnected(connection); + + // Send the packet message to the packet event bus. + await this.emitPacketEvent(packetMessage); }); connection.on('close', () => { diff --git a/packages/adapter-tcp/src/tcp.adapter.ts b/packages/adapter-tcp/src/tcp.server.ts similarity index 98% rename from packages/adapter-tcp/src/tcp.adapter.ts rename to packages/adapter-tcp/src/tcp.server.ts index 54082b56..66782f4f 100644 --- a/packages/adapter-tcp/src/tcp.adapter.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -42,9 +42,10 @@ import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; import type { DeviceRepository } from '@agnoc/domain'; +import type { Server } from '@agnoc/toolkit'; import type { AddressInfo } from 'net'; -export class TCPAdapter { +export class TCPServer implements Server { private readonly cmdServer: PacketServer; private readonly mapServer: PacketServer; private readonly ntpServer: PacketServer; diff --git a/packages/adapter-tcp/test/integration/tcp.adapter.test.ts b/packages/adapter-tcp/test/integration/tcp.server.test.ts similarity index 97% rename from packages/adapter-tcp/test/integration/tcp.adapter.test.ts rename to packages/adapter-tcp/test/integration/tcp.server.test.ts index b42c78ed..5ceee202 100644 --- a/packages/adapter-tcp/test/integration/tcp.adapter.test.ts +++ b/packages/adapter-tcp/test/integration/tcp.server.test.ts @@ -12,7 +12,7 @@ import { PayloadObjectParserService, } from '@agnoc/transport-tcp'; import { expect } from 'chai'; -import { TCPAdapter } from '@agnoc/adapter-tcp'; +import { TCPServer } from '@agnoc/adapter-tcp'; import type { TCPAdapterListenOptions } from '@agnoc/adapter-tcp'; import type { ICLIENT_ONLINE_REQ, IDEVICE_REGISTER_REQ } from '@agnoc/schemas-tcp'; import type { CreatePacketProps, Packet } from '@agnoc/transport-tcp'; @@ -23,7 +23,7 @@ describe('TCPAdapter', function () { let domainEventHandlerRegistry: EventHandlerRegistry; let commandEventHandlerRegistry: EventHandlerRegistry; let deviceRepository: DeviceRepository; - let tcpAdapter: TCPAdapter; + let tcpAdapter: TCPServer; let packetSocket: PacketSocket; let secondPacketSocket: PacketSocket; let packetFactory: PacketFactory; @@ -36,7 +36,7 @@ describe('TCPAdapter', function () { domainEventHandlerRegistry = new EventHandlerRegistry(domainEventBus); commandEventHandlerRegistry = new EventHandlerRegistry(commandEventBus); deviceRepository = new DeviceRepository(domainEventBus, new MemoryAdapter()); - tcpAdapter = new TCPAdapter(deviceRepository, domainEventHandlerRegistry, commandEventHandlerRegistry); + tcpAdapter = new TCPServer(deviceRepository, domainEventHandlerRegistry, commandEventHandlerRegistry); // Client blocks const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); diff --git a/packages/cli/package.json b/packages/cli/package.json index de4beeb3..1dd09fcb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -97,7 +97,8 @@ "@johanblumenberg/ts-mockito": "^1.0.35", "chai": "^4.3.7", "execa": "^5.1.1", - "mock-fs": "^5.2.0" + "mock-fs": "^5.2.0", + "pcap": "^3.1.0" }, "engines": { "node": ">=18.12" diff --git a/packages/cli/src/commands/read.command.ts b/packages/cli/src/commands/read.command.ts index 7650058c..feb86b13 100644 --- a/packages/cli/src/commands/read.command.ts +++ b/packages/cli/src/commands/read.command.ts @@ -20,6 +20,7 @@ export class ReadCommand implements Command { let pcap; try { + /* eslint import/no-extraneous-dependencies: ["error", {"optionalDependencies": true}] */ pcap = await import('pcap'); } catch (e) { /* istanbul ignore next */ diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 00000000..419a20a5 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1,5 @@ +*.tgz +.nyc_output/ +lib/ +coverage/ +node_modules/ diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..124c8f57 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,3 @@ +# @agnoc/core + +The core component of the library. diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..cad0bfb2 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,91 @@ +{ + "name": "@agnoc/core", + "version": "0.18.0-next.0", + "description": "Agnoc adapter TCP library", + "keywords": [ + "agnoc", + "adapter", + "tcp", + "conga", + "cecotec", + "driver", + "reverse", + "engineering" + ], + "homepage": "https://github.com/adrigzr/agnoc", + "bugs": { + "url": "https://github.com/adrigzr/agnoc/issues" + }, + "license": "MIT", + "author": "Adrián González Rus (https://github.com/adrigzr)", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/adrigzr/agnoc.git", + "directory": "packages/core" + }, + "scripts": { + "build": "npm-run-all build:lib", + "build:lib": "tsc -b tsconfig.build.json", + "clean": "rm -rf .build-cache lib coverage", + "posttest": "test \"$(cat coverage/coverage-summary.json | json total.lines.total)\" -gt 0", + "test": "nyc mocha src test" + }, + "nx": { + "targets": { + "build": { + "inputs": [ + "{workspaceRoot}/package.json", + "{workspaceRoot}/tsconfig.*", + "{projectRoot}/src/**/*", + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.*" + ], + "outputs": [ + "{projectRoot}/lib", + "{projectRoot}/.build-cache" + ] + }, + "test": { + "dependsOn": [ + "build" + ], + "inputs": [ + "{workspaceRoot}/.mocharc.yml", + "{workspaceRoot}/nyc.config.js", + "{projectRoot}/src/**/*" + ], + "outputs": [ + "{projectRoot}/coverage" + ] + } + } + }, + "dependencies": { + "@agnoc/domain": "^0.18.0-next.0", + "@agnoc/adapter-tcp": "^0.18.0-next.0", + "@agnoc/toolkit": "^0.18.0-next.0", + "debug": "^4.3.4", + "emittery": "^0.13.1", + "tslib": "^2.5.0" + }, + "devDependencies": { + "@johanblumenberg/ts-mockito": "^1.0.35", + "chai": "^4.3.7" + }, + "engines": { + "node": ">=18.12" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "Core" + } +} diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts new file mode 100644 index 00000000..31865203 --- /dev/null +++ b/packages/core/src/agnoc.server.ts @@ -0,0 +1,57 @@ +import { CommandEventBus, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { EventHandlerRegistry, MemoryAdapter } from '@agnoc/toolkit'; +import type { DomainEventNames, DomainEvents, CommandEventNames, CommandEvents } from '@agnoc/domain'; +import type { Server } from '@agnoc/toolkit'; + +export class AgnocServer implements Server { + private readonly domainEventBus: DomainEventBus; + private readonly domainEventHandlerRegistry: EventHandlerRegistry; + private readonly commandEventBus: CommandEventBus; + private readonly commandEventHandlerRegistry: EventHandlerRegistry; + private readonly deviceRepository: DeviceRepository; + private readonly adapters = new Set(); + + constructor() { + this.domainEventBus = new DomainEventBus(); + this.domainEventHandlerRegistry = new EventHandlerRegistry(this.domainEventBus); + this.commandEventBus = new CommandEventBus(); + this.commandEventHandlerRegistry = new EventHandlerRegistry(this.commandEventBus); + this.deviceRepository = new DeviceRepository(this.domainEventBus, new MemoryAdapter()); + } + + subscribe(eventName: Name, handler: SubscribeHandler): void { + this.domainEventBus.on(eventName, handler); + } + + trigger(eventName: Name, payload: CommandEvents[Name]): Promise { + return this.commandEventBus.emit(eventName, payload); + } + + buildAdapter(builder: AdapterFactory): void { + const adapter = builder({ + domainEventHandlerRegistry: this.domainEventHandlerRegistry, + commandEventHandlerRegistry: this.commandEventHandlerRegistry, + deviceRepository: this.deviceRepository, + }); + + this.adapters.add(adapter); + } + + async listen(): Promise { + await Promise.all([...this.adapters].map((adapter) => adapter.listen())); + } + + async close(): Promise { + await Promise.all([...this.adapters].map((adapter) => adapter.close())); + } +} + +type AdapterFactory = (container: Container) => Server; + +export type Container = { + domainEventHandlerRegistry: EventHandlerRegistry; + commandEventHandlerRegistry: EventHandlerRegistry; + deviceRepository: DeviceRepository; +}; + +export type SubscribeHandler = (event: DomainEvents[Name]) => Promise; diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 00000000..d9b2d985 --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": ".build-cache/tsconfig.build.tsbuildinfo" + }, + "include": ["src"], + "exclude": ["**/*.test.ts"], + "references": [ + { "path": "../toolkit/tsconfig.build.json" }, + { "path": "../domain/tsconfig.build.json" }, + { "path": "../adapter-tcp/tsconfig.build.json" } + ] +} diff --git a/packages/domain/package.json b/packages/domain/package.json index 8c80557f..d8b304d4 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -79,6 +79,7 @@ "tslib": "^2.5.0" }, "devDependencies": { + "@johanblumenberg/ts-mockito": "^1.0.35", "chai": "^4.3.7" }, "engines": { diff --git a/packages/domain/src/event-buses/command.event-bus.ts b/packages/domain/src/event-buses/command.event-bus.ts index 35e8cdd3..3a13082c 100644 --- a/packages/domain/src/event-buses/command.event-bus.ts +++ b/packages/domain/src/event-buses/command.event-bus.ts @@ -1,6 +1,5 @@ import { EventBus } from '@agnoc/toolkit'; import type { CommandEventNames, CommandEvents } from '../commands/commands'; -type CommandEventBusEvents = { [Name in CommandEventNames]: CommandEvents[Name] }; - +export type CommandEventBusEvents = { [Name in CommandEventNames]: CommandEvents[Name] }; export class CommandEventBus extends EventBus {} diff --git a/packages/domain/src/event-buses/domain.event-bus.ts b/packages/domain/src/event-buses/domain.event-bus.ts index 55658453..4d2754a6 100644 --- a/packages/domain/src/event-buses/domain.event-bus.ts +++ b/packages/domain/src/event-buses/domain.event-bus.ts @@ -1,6 +1,5 @@ import { EventBus } from '@agnoc/toolkit'; import type { DomainEventNames, DomainEvents } from '../domain-events/domain-events'; -type DomainEventBusEvents = { [Name in DomainEventNames]: DomainEvents[Name] }; - +export type DomainEventBusEvents = { [Name in DomainEventNames]: DomainEvents[Name] }; export class DomainEventBus extends EventBus {} diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index af7c0c2e..db402f26 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -57,6 +57,15 @@ module.exports = { forbid: ['packages/**/*', '@agnoc/*/src/**/*'], }, ], + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: ['**/*.test.ts', '**/test/**/*.ts'], + optionalDependencies: false, + peerDependencies: true, + }, + ], + 'import/no-unresolved': 'off', 'import/order': [ 'warn', { diff --git a/packages/toolkit/src/base-classes/server.base.ts b/packages/toolkit/src/base-classes/server.base.ts new file mode 100644 index 00000000..275869ab --- /dev/null +++ b/packages/toolkit/src/base-classes/server.base.ts @@ -0,0 +1,4 @@ +export abstract class Server { + abstract listen(): Promise; + abstract close(): Promise; +} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index c879e725..fa9c4d25 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -11,6 +11,7 @@ export * from './base-classes/exception.base'; export * from './base-classes/factory.base'; export * from './base-classes/mapper.base'; export * from './base-classes/repository.base'; +export * from './base-classes/server.base'; export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; export * from './decorators/bind.decorator'; diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json index f040c390..7b57914c 100644 --- a/packages/transport-tcp/package.json +++ b/packages/transport-tcp/package.json @@ -81,6 +81,7 @@ "@agnoc/toolkit": "^0.18.0-next.0", "debug": "^4.3.4", "emittery": "^0.13.1", + "protobufjs": "^7.2.2", "tslib": "^2.5.0" }, "devDependencies": { From 81875180fb9bfde70261df47c8b17555217346dd Mon Sep 17 00:00:00 2001 From: adrigzr Date: Mon, 20 Mar 2023 20:54:54 +0100 Subject: [PATCH 10/38] feat: add task bus for commands and queries --- .../locate-device.event-handler.ts | 6 +- ...s-connected-event-handler.event-handler.ts | 2 +- ...e-is-locked-event-handler.event-handler.ts | 2 +- .../client-heartbeat.event-handler.ts | 2 +- .../client-login.event-handler.ts | 2 +- .../device-battery-update.event-handler.ts | 2 +- ...ice-clean-map-data-report.event-handler.ts | 2 +- .../device-clean-map-report.event-handler.ts | 2 +- .../device-clean-task-report.event-handler.ts | 2 +- ...device-get-all-global-map.event-handler.ts | 2 +- .../device-located.event-handler.ts | 2 +- .../device-locked.event-handler.ts | 2 +- ...p-charger-position-update.event-handler.ts | 2 +- .../device-map-update.event-handler.ts | 2 +- ...ce-map-work-status-update.event-handler.ts | 2 +- .../device-memory-map-info.event-handler.ts | 2 +- .../device-offline.event-handler.ts | 2 +- .../device-register.event-handler.ts | 2 +- .../device-settings-update.event-handler.ts | 2 +- .../device-time-update.event-handler.ts | 2 +- .../device-upgrade-info.event-handler.ts | 2 +- .../device-version-update.event-handler.ts | 2 +- .../device-wlan-update.event-handler.ts | 2 +- .../src/packet-server.connection-handler.ts | 6 +- .../adapter-tcp/src/packet.event-handler.ts | 4 +- packages/adapter-tcp/src/tcp.server.ts | 8 +- .../test/integration/tcp.server.test.ts | 15 +-- packages/core/src/agnoc.server.ts | 24 ++-- packages/domain/src/commands/commands.ts | 4 +- .../src/commands/locate-device.command.ts | 8 +- .../src/event-buses/command.event-bus.ts | 7 +- .../event-handlers/command.event-handler.ts | 10 +- .../event-handlers/domain.event-handler.ts | 4 +- .../src/base-classes/command.base.test.ts | 25 +++++ .../toolkit/src/base-classes/command.base.ts | 11 +- .../src/base-classes/event-handler.base.ts | 2 +- .../src/base-classes/query.base.test.ts | 25 +++++ .../toolkit/src/base-classes/query.base.ts | 3 + .../src/base-classes/task-bus.base.test.ts | 104 ++++++++++++++++++ .../toolkit/src/base-classes/task-bus.base.ts | 56 ++++++++++ .../src/base-classes/task-handler.base.ts | 6 + .../src/base-classes/task.base.test.ts | 25 +++++ .../toolkit/src/base-classes/task.base.ts | 12 ++ .../src/event-handler.registry.test.ts | 8 +- .../toolkit/src/event-handler.registry.ts | 2 +- packages/toolkit/src/index.ts | 5 + .../toolkit/src/task-handler.registry.test.ts | 42 +++++++ packages/toolkit/src/task-handler.registry.ts | 19 ++++ 48 files changed, 400 insertions(+), 85 deletions(-) create mode 100644 packages/toolkit/src/base-classes/command.base.test.ts create mode 100644 packages/toolkit/src/base-classes/query.base.test.ts create mode 100644 packages/toolkit/src/base-classes/query.base.ts create mode 100644 packages/toolkit/src/base-classes/task-bus.base.test.ts create mode 100644 packages/toolkit/src/base-classes/task-bus.base.ts create mode 100644 packages/toolkit/src/base-classes/task-handler.base.ts create mode 100644 packages/toolkit/src/base-classes/task.base.test.ts create mode 100644 packages/toolkit/src/base-classes/task.base.ts create mode 100644 packages/toolkit/src/task-handler.registry.test.ts create mode 100644 packages/toolkit/src/task-handler.registry.ts diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts index f6762dfd..e235364a 100644 --- a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts @@ -1,9 +1,9 @@ import { DomainException } from '@agnoc/toolkit'; import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; -import type { CommandEventHandler, LocateDeviceCommand } from '@agnoc/domain'; +import type { CommandHandler, LocateDeviceCommand } from '@agnoc/domain'; -export class LocateDeviceEventHandler implements CommandEventHandler { - readonly eventName = 'LocateDeviceCommand'; +export class LocateDeviceEventHandler implements CommandHandler { + readonly forName = 'LocateDeviceCommand'; constructor(private readonly connectionManager: PackerServerConnectionHandler) {} diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index 4eda104b..aa2eb1ef 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -3,7 +3,7 @@ import type { PackerServerConnectionHandler } from '../../packet-server.connecti import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { - readonly eventName = 'DeviceConnectedDomainEvent'; + readonly forName = 'DeviceConnectedDomainEvent'; constructor(private readonly connectionManager: PackerServerConnectionHandler) {} diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index 4b49aedc..d46a09dc 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -4,7 +4,7 @@ import type { PackerServerConnectionHandler } from '../../packet-server.connecti import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { - readonly eventName = 'DeviceLockedDomainEvent'; + readonly forName = 'DeviceLockedDomainEvent'; constructor(private readonly connectionManager: PackerServerConnectionHandler) {} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts index 8164fb3e..4c4ce954 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts @@ -2,7 +2,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class ClientHeartbeatEventHandler implements PacketEventHandler { - readonly eventName = 'CLIENT_HEARTBEAT_REQ'; + readonly forName = 'CLIENT_HEARTBEAT_REQ'; async handle(message: PacketMessage<'CLIENT_HEARTBEAT_REQ'>): Promise { await message.respond('CLIENT_HEARTBEAT_RSP', {}); diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts index ba0477d3..4f212c42 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts @@ -2,7 +2,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class ClientLoginEventHandler implements PacketEventHandler { - readonly eventName = 'CLIENT_ONLINE_REQ'; + readonly forName = 'CLIENT_ONLINE_REQ'; async handle(message: PacketMessage<'CLIENT_ONLINE_REQ'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts index 328ac324..0f599707 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts @@ -4,7 +4,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'PUSH_DEVICE_BATTERY_INFO_REQ'; + readonly forName = 'PUSH_DEVICE_BATTERY_INFO_REQ'; constructor(private readonly deviceBatteryMapper: DeviceBatteryMapper) {} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts index a7a4fb2f..368cd9aa 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; + readonly forName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; async handle(message: PacketMessage<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts index 8d39d860..2502c96b 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceCleanMapReportEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_EVENT_REPORT_CLEANMAP'; + readonly forName = 'DEVICE_EVENT_REPORT_CLEANMAP'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANMAP'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts index 91e05319..0cfdc7a5 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_EVENT_REPORT_CLEANTASK'; + readonly forName = 'DEVICE_EVENT_REPORT_CLEANTASK'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANTASK'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts index 93de0f5d..af4133c8 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceGetAllGlobalMapEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; + readonly forName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; async handle(message: PacketMessage<'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts index 65e79dc7..dd5f8a83 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceLocatedEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_SEEK_LOCATION_RSP'; + readonly forName = 'DEVICE_SEEK_LOCATION_RSP'; async handle(message: PacketMessage<'DEVICE_SEEK_LOCATION_RSP'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts index e7451f91..4e0ff1c9 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts @@ -4,7 +4,7 @@ import type { PacketMessage } from '../../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceLockedEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_CONTROL_LOCK_RSP'; + readonly forName = 'DEVICE_CONTROL_LOCK_RSP'; constructor(private readonly deviceRepository: DeviceRepository) {} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts index 2f0be65b..87afef44 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -4,7 +4,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'; + readonly forName = 'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'; async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts index db744e41..378826a4 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts @@ -19,7 +19,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceMapUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'; + readonly forName = 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'; constructor( private readonly deviceBatteryMapper: DeviceBatteryMapper, diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts index 386fc1cb..61878bb0 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -10,7 +10,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'; + readonly forName = 'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'; constructor( private readonly deviceStateMapper: DeviceStateMapper, diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts index e7ce14db..fe70f1fb 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceMemoryMapInfoEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; + readonly forName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts index 0bfda3f0..a306f1eb 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceOfflineEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_OFFLINE_CMD'; + readonly forName = 'DEVICE_OFFLINE_CMD'; async handle(message: PacketMessage<'DEVICE_OFFLINE_CMD'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts index 77f0db7a..3cce8c51 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts @@ -5,7 +5,7 @@ import type { PacketMessage } from '../../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceRegisterEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_REGISTER_REQ'; + readonly forName = 'DEVICE_REGISTER_REQ'; constructor(private readonly deviceRepository: DeviceRepository) {} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts index 5401371e..047b03c7 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts @@ -5,7 +5,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; + readonly forName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; constructor(private readonly deviceVoiceMapper: DeviceVoiceMapper) {} diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts index 0fdae5b1..97b5b070 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceTimeUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_GETTIME_RSP'; + readonly forName = 'DEVICE_GETTIME_RSP'; async handle(message: PacketMessage<'DEVICE_GETTIME_RSP'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts index c2798d20..7042c17b 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts @@ -3,7 +3,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceUpgradeInfoEventHandler implements PacketEventHandler { - readonly eventName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; + readonly forName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; async handle(message: PacketMessage<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts index 1d4ce738..bf490ccf 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts @@ -4,7 +4,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceVersionUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_VERSION_INFO_UPDATE_REQ'; + readonly forName = 'DEVICE_VERSION_INFO_UPDATE_REQ'; async handle(message: PacketMessage<'DEVICE_VERSION_INFO_UPDATE_REQ'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts index 7366b9c9..74c50099 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts @@ -4,7 +4,7 @@ import type { PacketEventHandler } from '../../packet.event-handler'; import type { PacketMessage } from '../../packet.message'; export class DeviceWlanUpdateEventHandler implements PacketEventHandler { - readonly eventName = 'DEVICE_WLAN_INFO_GETTING_RSP'; + readonly forName = 'DEVICE_WLAN_INFO_GETTING_RSP'; async handle(message: PacketMessage<'DEVICE_WLAN_INFO_GETTING_RSP'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts index 677a7bc3..8b7c693c 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -40,12 +40,12 @@ export class PackerServerConnectionHandler { // Update the device on the connection if the device id has changed. await this.updateConnectionDevice(packet, connection); + // Send the packet message to the packet event bus. + await this.emitPacketEvent(packetMessage); + // This is a hack to only mark the device as connected if there is more than one connection. // Here we should check that the connections are from the same ip address. await this.tryToSetDeviceAsConnected(connection); - - // Send the packet message to the packet event bus. - await this.emitPacketEvent(packetMessage); }); connection.on('close', () => { diff --git a/packages/adapter-tcp/src/packet.event-handler.ts b/packages/adapter-tcp/src/packet.event-handler.ts index 9187f155..50974cf5 100644 --- a/packages/adapter-tcp/src/packet.event-handler.ts +++ b/packages/adapter-tcp/src/packet.event-handler.ts @@ -5,8 +5,8 @@ import type { PayloadObjectName } from '@agnoc/transport-tcp'; /** Base class for packet event handlers. */ export abstract class PacketEventHandler implements EventHandler { /** The name of the event to listen to. */ - abstract eventName: PayloadObjectName; + abstract forName: PayloadObjectName; /** Handle the event. */ - abstract handle(message: PacketMessage): void; + abstract handle(message: PacketMessage): void; } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 66782f4f..cb02d79c 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -41,8 +41,8 @@ import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; -import type { DeviceRepository } from '@agnoc/domain'; -import type { Server } from '@agnoc/toolkit'; +import type { Commands, DeviceRepository } from '@agnoc/domain'; +import type { Server, TaskHandlerRegistry } from '@agnoc/toolkit'; import type { AddressInfo } from 'net'; export class TCPServer implements Server { @@ -53,7 +53,7 @@ export class TCPServer implements Server { constructor( private readonly deviceRepository: DeviceRepository, private readonly domainEventHandlerRegistry: EventHandlerRegistry, - private readonly commandEventHandlerRegistry: EventHandlerRegistry, + private readonly commandHandlerRegistry: TaskHandlerRegistry, ) { // Packet foundation const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); @@ -132,7 +132,7 @@ export class TCPServer implements Server { ); // Command event handlers - this.commandEventHandlerRegistry.register(new LocateDeviceEventHandler(connectionManager)); + this.commandHandlerRegistry.register(new LocateDeviceEventHandler(connectionManager)); } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { diff --git a/packages/adapter-tcp/test/integration/tcp.server.test.ts b/packages/adapter-tcp/test/integration/tcp.server.test.ts index 5ceee202..280a3353 100644 --- a/packages/adapter-tcp/test/integration/tcp.server.test.ts +++ b/packages/adapter-tcp/test/integration/tcp.server.test.ts @@ -1,7 +1,7 @@ import { once } from 'events'; -import { CommandEventBus, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { CommandBus, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; -import { EventHandlerRegistry, ID, MemoryAdapter } from '@agnoc/toolkit'; +import { EventHandlerRegistry, ID, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; import { getCustomDecoders, getProtobufRoot, @@ -14,14 +14,15 @@ import { import { expect } from 'chai'; import { TCPServer } from '@agnoc/adapter-tcp'; import type { TCPAdapterListenOptions } from '@agnoc/adapter-tcp'; +import type { Commands } from '@agnoc/domain'; import type { ICLIENT_ONLINE_REQ, IDEVICE_REGISTER_REQ } from '@agnoc/schemas-tcp'; import type { CreatePacketProps, Packet } from '@agnoc/transport-tcp'; describe('TCPAdapter', function () { let domainEventBus: DomainEventBus; - let commandEventBus: CommandEventBus; + let commandBus: CommandBus; let domainEventHandlerRegistry: EventHandlerRegistry; - let commandEventHandlerRegistry: EventHandlerRegistry; + let commandHandlerRegistry: TaskHandlerRegistry; let deviceRepository: DeviceRepository; let tcpAdapter: TCPServer; let packetSocket: PacketSocket; @@ -31,12 +32,12 @@ describe('TCPAdapter', function () { beforeEach(function () { // Server blocks domainEventBus = new DomainEventBus(); - commandEventBus = new CommandEventBus(); + commandBus = new CommandBus(); domainEventHandlerRegistry = new EventHandlerRegistry(domainEventBus); - commandEventHandlerRegistry = new EventHandlerRegistry(commandEventBus); + commandHandlerRegistry = new TaskHandlerRegistry(commandBus); deviceRepository = new DeviceRepository(domainEventBus, new MemoryAdapter()); - tcpAdapter = new TCPServer(deviceRepository, domainEventHandlerRegistry, commandEventHandlerRegistry); + tcpAdapter = new TCPServer(deviceRepository, domainEventHandlerRegistry, commandHandlerRegistry); // Client blocks const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts index 31865203..d084f2c9 100644 --- a/packages/core/src/agnoc.server.ts +++ b/packages/core/src/agnoc.server.ts @@ -1,21 +1,21 @@ -import { CommandEventBus, DeviceRepository, DomainEventBus } from '@agnoc/domain'; -import { EventHandlerRegistry, MemoryAdapter } from '@agnoc/toolkit'; -import type { DomainEventNames, DomainEvents, CommandEventNames, CommandEvents } from '@agnoc/domain'; -import type { Server } from '@agnoc/toolkit'; +import { CommandBus, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { EventHandlerRegistry, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; +import type { DomainEventNames, DomainEvents, Commands } from '@agnoc/domain'; +import type { Server, TaskOutput } from '@agnoc/toolkit'; export class AgnocServer implements Server { private readonly domainEventBus: DomainEventBus; private readonly domainEventHandlerRegistry: EventHandlerRegistry; - private readonly commandEventBus: CommandEventBus; - private readonly commandEventHandlerRegistry: EventHandlerRegistry; + private readonly commandBus: CommandBus; + private readonly commandHandlerRegistry: TaskHandlerRegistry; private readonly deviceRepository: DeviceRepository; private readonly adapters = new Set(); constructor() { this.domainEventBus = new DomainEventBus(); this.domainEventHandlerRegistry = new EventHandlerRegistry(this.domainEventBus); - this.commandEventBus = new CommandEventBus(); - this.commandEventHandlerRegistry = new EventHandlerRegistry(this.commandEventBus); + this.commandBus = new CommandBus(); + this.commandHandlerRegistry = new TaskHandlerRegistry(this.commandBus); this.deviceRepository = new DeviceRepository(this.domainEventBus, new MemoryAdapter()); } @@ -23,14 +23,14 @@ export class AgnocServer implements Server { this.domainEventBus.on(eventName, handler); } - trigger(eventName: Name, payload: CommandEvents[Name]): Promise { - return this.commandEventBus.emit(eventName, payload); + trigger(command: Command): Promise> { + return this.commandBus.trigger(command); } buildAdapter(builder: AdapterFactory): void { const adapter = builder({ domainEventHandlerRegistry: this.domainEventHandlerRegistry, - commandEventHandlerRegistry: this.commandEventHandlerRegistry, + commandHandlerRegistry: this.commandHandlerRegistry, deviceRepository: this.deviceRepository, }); @@ -50,7 +50,7 @@ type AdapterFactory = (container: Container) => Server; export type Container = { domainEventHandlerRegistry: EventHandlerRegistry; - commandEventHandlerRegistry: EventHandlerRegistry; + commandHandlerRegistry: TaskHandlerRegistry; deviceRepository: DeviceRepository; }; diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index caa1063d..cfa2712f 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,7 +1,7 @@ import type { LocateDeviceCommand } from './locate-device.command'; -export type CommandEvents = { +export type Commands = { LocateDeviceCommand: LocateDeviceCommand; }; -export type CommandEventNames = keyof CommandEvents; +export type CommandNames = keyof Commands; diff --git a/packages/domain/src/commands/locate-device.command.ts b/packages/domain/src/commands/locate-device.command.ts index bbc6ab81..f9bd79b2 100644 --- a/packages/domain/src/commands/locate-device.command.ts +++ b/packages/domain/src/commands/locate-device.command.ts @@ -1,16 +1,16 @@ import { Command } from '@agnoc/toolkit'; import type { ID } from '@agnoc/toolkit'; -export interface LocateDeviceCommandProps { +export interface LocateDeviceCommandInput { deviceId: ID; } -export class LocateDeviceCommand extends Command { +export class LocateDeviceCommand extends Command { get deviceId(): ID { return this.props.deviceId; } - protected validate(_: LocateDeviceCommandProps): void { - // noop + protected validate(): void { + // TODO: validate input } } diff --git a/packages/domain/src/event-buses/command.event-bus.ts b/packages/domain/src/event-buses/command.event-bus.ts index 3a13082c..8419aae4 100644 --- a/packages/domain/src/event-buses/command.event-bus.ts +++ b/packages/domain/src/event-buses/command.event-bus.ts @@ -1,5 +1,4 @@ -import { EventBus } from '@agnoc/toolkit'; -import type { CommandEventNames, CommandEvents } from '../commands/commands'; +import { TaskBus } from '@agnoc/toolkit'; +import type { Commands } from '../commands/commands'; -export type CommandEventBusEvents = { [Name in CommandEventNames]: CommandEvents[Name] }; -export class CommandEventBus extends EventBus {} +export class CommandBus extends TaskBus {} diff --git a/packages/domain/src/event-handlers/command.event-handler.ts b/packages/domain/src/event-handlers/command.event-handler.ts index 857556e8..17c3d480 100644 --- a/packages/domain/src/event-handlers/command.event-handler.ts +++ b/packages/domain/src/event-handlers/command.event-handler.ts @@ -1,7 +1,7 @@ -import type { CommandEventNames, CommandEvents } from '../commands/commands'; -import type { EventHandler } from '@agnoc/toolkit'; +import type { CommandNames, Commands } from '../commands/commands'; +import type { TaskHandler } from '@agnoc/toolkit'; -export abstract class CommandEventHandler implements EventHandler { - abstract eventName: CommandEventNames; - abstract handle(event: CommandEvents[this['eventName']]): void; +export abstract class CommandHandler implements TaskHandler { + abstract forName: CommandNames; + abstract handle(event: Commands[this['forName']]): void; } diff --git a/packages/domain/src/event-handlers/domain.event-handler.ts b/packages/domain/src/event-handlers/domain.event-handler.ts index c5f492df..b37a4920 100644 --- a/packages/domain/src/event-handlers/domain.event-handler.ts +++ b/packages/domain/src/event-handlers/domain.event-handler.ts @@ -4,8 +4,8 @@ import type { EventHandler } from '@agnoc/toolkit'; /** Base class for domain event handlers. */ export abstract class DomainEventHandler implements EventHandler { /** The name of the event to listen to. */ - abstract eventName: DomainEventNames; + abstract forName: DomainEventNames; /** Handle the event. */ - abstract handle(event: DomainEvents[this['eventName']]): void; + abstract handle(event: DomainEvents[this['forName']]): void; } diff --git a/packages/toolkit/src/base-classes/command.base.test.ts b/packages/toolkit/src/base-classes/command.base.test.ts new file mode 100644 index 00000000..cf6c6230 --- /dev/null +++ b/packages/toolkit/src/base-classes/command.base.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command } from './command.base'; +import { Task } from './task.base'; + +describe('Command', function () { + it('should be created', function () { + const dummyCommand = new DummyCommand({ foo: 'bar' }); + + expect(dummyCommand).to.be.instanceOf(Task); + }); +}); + +interface DummyCommandInput { + foo: string; +} + +interface DummyCommandOutput { + bar: string; +} + +class DummyCommand extends Command { + protected validate(): void { + // noop + } +} diff --git a/packages/toolkit/src/base-classes/command.base.ts b/packages/toolkit/src/base-classes/command.base.ts index a92f79b9..186a3276 100644 --- a/packages/toolkit/src/base-classes/command.base.ts +++ b/packages/toolkit/src/base-classes/command.base.ts @@ -1,10 +1,3 @@ -import { Validatable } from './validatable.base'; +import { Task } from './task.base'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CommandProps {} - -export abstract class Command extends Validatable { - constructor(protected readonly props: T) { - super(props); - } -} +export abstract class Command extends Task {} diff --git a/packages/toolkit/src/base-classes/event-handler.base.ts b/packages/toolkit/src/base-classes/event-handler.base.ts index f5f4858a..e214a809 100644 --- a/packages/toolkit/src/base-classes/event-handler.base.ts +++ b/packages/toolkit/src/base-classes/event-handler.base.ts @@ -1,6 +1,6 @@ /** Base class for event handlers. */ export abstract class EventHandler { // eslint-disable-next-line @typescript-eslint/ban-types - abstract eventName: string; + abstract forName: string; abstract handle(...args: unknown[]): void; } diff --git a/packages/toolkit/src/base-classes/query.base.test.ts b/packages/toolkit/src/base-classes/query.base.test.ts new file mode 100644 index 00000000..63c4314a --- /dev/null +++ b/packages/toolkit/src/base-classes/query.base.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Query } from './query.base'; +import { Task } from './task.base'; + +describe('Query', function () { + it('should be created', function () { + const dummyQuery = new DummyQuery({ foo: 'bar' }); + + expect(dummyQuery).to.be.instanceOf(Task); + }); +}); + +interface DummyQueryInput { + foo: string; +} + +interface DummyQueryOutput { + bar: string; +} + +class DummyQuery extends Query { + protected validate(): void { + // noop + } +} diff --git a/packages/toolkit/src/base-classes/query.base.ts b/packages/toolkit/src/base-classes/query.base.ts new file mode 100644 index 00000000..62bcd5b9 --- /dev/null +++ b/packages/toolkit/src/base-classes/query.base.ts @@ -0,0 +1,3 @@ +import { Task } from './task.base'; + +export abstract class Query extends Task {} diff --git a/packages/toolkit/src/base-classes/task-bus.base.test.ts b/packages/toolkit/src/base-classes/task-bus.base.test.ts new file mode 100644 index 00000000..4a4ceb27 --- /dev/null +++ b/packages/toolkit/src/base-classes/task-bus.base.test.ts @@ -0,0 +1,104 @@ +import { expect } from 'chai'; +import { DomainException } from '../exceptions/domain.exception'; +import { TaskBus } from './task-bus.base'; +import { Task } from './task.base'; + +describe('TaskBus', function () { + let dummyTaskBus: DummyTaskBus; + + beforeEach(function () { + dummyTaskBus = new DummyTaskBus(); + }); + + it('should subscribe and trigger tasks', async function () { + dummyTaskBus.subscribe('IOTask', (task) => { + expect(task).to.be.instanceOf(IOTask); + + return { bar: 'bar' }; + }); + dummyTaskBus.subscribe('ITask', (task) => { + expect(task).to.be.instanceOf(ITask); + }); + + const ioTaskOutput = await dummyTaskBus.trigger(new IOTask({ foo: 'foo' })); + + expect(ioTaskOutput).to.be.deep.equal({ bar: 'bar' }); + + const iTaskOutput = await dummyTaskBus.trigger(new ITask({ wow: 'wow' })); + + expect(iTaskOutput).to.be.undefined; + }); + + it('should subscribe async tasks', async function () { + dummyTaskBus.subscribe('IOTask', async () => { + return { bar: 'bar' }; + }); + + const ioTaskOutput = await dummyTaskBus.trigger(new IOTask({ foo: 'foo' })); + + expect(ioTaskOutput).to.be.deep.equal({ bar: 'bar' }); + }); + + it('should throw an error when no handlers are registered', async function () { + await expect(dummyTaskBus.trigger(new IOTask({ foo: 'foo' }))).to.be.rejectedWith( + DomainException, + 'No handlers registered for IOTask', + ); + }); + + it('should throw an error when no handlers are fulfilled', async function () { + dummyTaskBus.subscribe('IOTask', () => { + throw new Error('foo'); + }); + + await expect(dummyTaskBus.trigger(new IOTask({ foo: 'foo' }))).to.be.rejectedWith( + DomainException, + 'No handlers fulfilled for IOTask', + ); + }); + + it('should throw an error when more than one handler is fulfilled', async function () { + dummyTaskBus.subscribe('IOTask', () => { + return { bar: 'bar' }; + }); + dummyTaskBus.subscribe('IOTask', () => { + return { bar: 'bar' }; + }); + + await expect(dummyTaskBus.trigger(new IOTask({ foo: 'foo' }))).to.be.rejectedWith( + DomainException, + 'Multiple handlers fulfilled for IOTask', + ); + }); +}); + +interface IOTaskInput { + foo: string; +} + +interface IOTaskOutput { + bar: string; +} + +class IOTask extends Task { + protected validate() { + // noop + } +} + +interface ITaskInput { + wow: string; +} + +class ITask extends Task { + protected validate() { + // noop + } +} + +type DummyTaskBusTasks = { + IOTask: IOTask; + ITask: ITask; +}; + +class DummyTaskBus extends TaskBus {} diff --git a/packages/toolkit/src/base-classes/task-bus.base.ts b/packages/toolkit/src/base-classes/task-bus.base.ts new file mode 100644 index 00000000..fa62984b --- /dev/null +++ b/packages/toolkit/src/base-classes/task-bus.base.ts @@ -0,0 +1,56 @@ +import { DomainException } from '../exceptions/domain.exception'; +import type { Task, TaskOutput } from './task.base'; + +export type TaskBusSubscribeHandler = ( + task: Events[Name], +) => TaskOutput | Promise>; + +// export type TaskBusTasks = Record>; + +export type TaskBusTasks = { + [key: string]: Task; +}; + +export abstract class TaskBus { + private readonly handlers = new Map>>(); + + subscribe(name: Name, handler: TaskBusSubscribeHandler): void { + if (!this.handlers.has(name)) { + this.handlers.set(name, new Set()); + } + + this.handlers.get(name)?.add(handler as TaskBusSubscribeHandler); + } + + async trigger(task: Task): Promise> { + const handlers = this.handlers.get(task.constructor.name as keyof Tasks); + + if (!handlers) { + throw new DomainException(`No handlers registered for ${task.constructor.name}`); + } + + const promises = [...handlers].map(async (handler) => handler(task)); + const outputs = await Promise.allSettled(promises); + const fulfilledOutputs = this.filterFulfilledPromises(outputs); + + if (fulfilledOutputs.length === 0) { + throw new DomainException(`No handlers fulfilled for ${task.constructor.name}`); + } + + if (fulfilledOutputs.length > 1) { + throw new DomainException(`Multiple handlers fulfilled for ${task.constructor.name}`); + } + + const output = fulfilledOutputs[0].value; + + task.validateOutput?.(output); + + return output; + } + + private filterFulfilledPromises( + outputs: PromiseSettledResult>[], + ) { + return outputs.filter((output) => output.status === 'fulfilled') as PromiseFulfilledResult>[]; + } +} diff --git a/packages/toolkit/src/base-classes/task-handler.base.ts b/packages/toolkit/src/base-classes/task-handler.base.ts new file mode 100644 index 00000000..24bd6071 --- /dev/null +++ b/packages/toolkit/src/base-classes/task-handler.base.ts @@ -0,0 +1,6 @@ +/** Base class for event handlers. */ +export abstract class TaskHandler { + // eslint-disable-next-line @typescript-eslint/ban-types + abstract forName: string; + abstract handle(...args: unknown[]): void; +} diff --git a/packages/toolkit/src/base-classes/task.base.test.ts b/packages/toolkit/src/base-classes/task.base.test.ts new file mode 100644 index 00000000..53e4d3d1 --- /dev/null +++ b/packages/toolkit/src/base-classes/task.base.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Task } from './task.base'; +import { Validatable } from './validatable.base'; + +describe('Task', function () { + it('should be created', function () { + const dummyTask = new DummyTask({ foo: 'bar' }); + + expect(dummyTask).to.be.instanceOf(Validatable); + }); +}); + +interface DummyTaskInput { + foo: string; +} + +interface DummyTaskOutput { + bar: string; +} + +class DummyTask extends Task { + protected validate(): void { + // noop + } +} diff --git a/packages/toolkit/src/base-classes/task.base.ts b/packages/toolkit/src/base-classes/task.base.ts new file mode 100644 index 00000000..e2aca548 --- /dev/null +++ b/packages/toolkit/src/base-classes/task.base.ts @@ -0,0 +1,12 @@ +import { Validatable } from './validatable.base'; + +export type TaskInput = T extends Task ? Input : never; +export type TaskOutput = T extends Task ? Output : never; + +export abstract class Task extends Validatable implements Task { + constructor(protected readonly props: Input) { + super(props); + } + + validateOutput?(output: Output): void; +} diff --git a/packages/toolkit/src/event-handler.registry.test.ts b/packages/toolkit/src/event-handler.registry.test.ts index 01077fb8..2b2489ac 100644 --- a/packages/toolkit/src/event-handler.registry.test.ts +++ b/packages/toolkit/src/event-handler.registry.test.ts @@ -15,7 +15,7 @@ describe('EventHandlerRegistry', function () { }); it('should listen for events on the bus', function () { - when(eventHandler.eventName).thenReturn('event'); + when(eventHandler.forName).thenReturn('event'); eventHandlerManager.register(instance(eventHandler)); @@ -25,13 +25,13 @@ describe('EventHandlerRegistry', function () { it('should call handle when event is emitted', async function () { const data = { foo: 'bar' }; - when(eventHandler.eventName).thenReturn('event'); + when(eventHandler.forName).thenReturn('event'); eventHandlerManager.register(instance(eventHandler)); - const [eventName, callback] = capture(eventBus.on<'event'>).first(); + const [forName, callback] = capture(eventBus.on<'event'>).first(); - expect(eventName).to.equal('event'); + expect(forName).to.equal('event'); await callback(data); diff --git a/packages/toolkit/src/event-handler.registry.ts b/packages/toolkit/src/event-handler.registry.ts index 356861a3..a1b3003b 100644 --- a/packages/toolkit/src/event-handler.registry.ts +++ b/packages/toolkit/src/event-handler.registry.ts @@ -10,6 +10,6 @@ export class EventHandlerRegistry { } private addEventHandler(eventHandler: EventHandler): void { - this.eventBus.on(eventHandler.eventName, eventHandler.handle.bind(eventHandler)); + this.eventBus.on(eventHandler.forName, eventHandler.handle.bind(eventHandler)); } } diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index fa9c4d25..3d880853 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -10,8 +10,12 @@ export * from './base-classes/event-handler.base'; export * from './base-classes/exception.base'; export * from './base-classes/factory.base'; export * from './base-classes/mapper.base'; +export * from './base-classes/query.base'; export * from './base-classes/repository.base'; export * from './base-classes/server.base'; +export * from './base-classes/task-bus.base'; +export * from './base-classes/task-handler.base'; +export * from './base-classes/task.base'; export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; export * from './decorators/bind.decorator'; @@ -24,6 +28,7 @@ export * from './exceptions/domain.exception'; export * from './exceptions/not-implemented.exception'; export * from './exceptions/timeout.exception'; export * from './streams/buffer-writer.stream'; +export * from './task-handler.registry'; export * from './types/constructor.type'; export * from './types/deep-partial.type'; export * from './types/object-literal.type'; diff --git a/packages/toolkit/src/task-handler.registry.test.ts b/packages/toolkit/src/task-handler.registry.test.ts new file mode 100644 index 00000000..16525451 --- /dev/null +++ b/packages/toolkit/src/task-handler.registry.test.ts @@ -0,0 +1,42 @@ +import { anything, capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { TaskHandlerRegistry } from './task-handler.registry'; +import type { TaskBus } from './base-classes/task-bus.base'; +import type { TaskHandler } from './base-classes/task-handler.base'; +import type { Task } from './base-classes/task.base'; + +describe('TaskHandlerRegistry', function () { + let task: Task; + let taskBus: TaskBus; + let taskHandler: TaskHandler; + let taskHandlerManager: TaskHandlerRegistry; + + beforeEach(function () { + task = imock(); + taskBus = imock(); + taskHandler = imock(); + taskHandlerManager = new TaskHandlerRegistry(instance(taskBus)); + }); + + it('should listen for tasks on the bus', function () { + when(taskHandler.forName).thenReturn('task'); + + taskHandlerManager.register(instance(taskHandler)); + + verify(taskBus.subscribe('task', anything())).once(); + }); + + it('should call handle when task is emitted', async function () { + when(taskHandler.forName).thenReturn('task'); + + taskHandlerManager.register(instance(taskHandler)); + + const [forName, callback] = capture(taskBus.subscribe<'task'>).first(); + + expect(forName).to.equal('task'); + + await callback(instance(task)); + + verify(taskHandler.handle(instance(task))).once(); + }); +}); diff --git a/packages/toolkit/src/task-handler.registry.ts b/packages/toolkit/src/task-handler.registry.ts new file mode 100644 index 00000000..50c74a2e --- /dev/null +++ b/packages/toolkit/src/task-handler.registry.ts @@ -0,0 +1,19 @@ +import type { EventHandler } from './base-classes/event-handler.base'; +import type { TaskBus, TaskBusSubscribeHandler, TaskBusTasks } from './base-classes/task-bus.base'; +import type { TaskHandler } from './base-classes/task-handler.base'; + +/** Manages event handlers. */ +export class TaskHandlerRegistry { + constructor(private readonly taskBus: TaskBus) {} + + register(...taskHandlers: TaskHandler[]): void { + taskHandlers.forEach((taskHandler) => this.addEventHandler(taskHandler)); + } + + private addEventHandler(eventHandler: EventHandler): void { + this.taskBus.subscribe( + eventHandler.forName, + eventHandler.handle.bind(eventHandler) as TaskBusSubscribeHandler, + ); + } +} From 09db58298e2dc80e2aa5c002a6c20ba93074590f Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 21 Mar 2023 11:31:01 +0100 Subject: [PATCH 11/38] test: raise coverage --- packages/adapter-tcp/package.json | 3 +- packages/adapter-tcp/src/tcp.server.test.ts | 32 +++++++ packages/core/src/agnoc.server.test.ts | 88 +++++++++++++++++++ .../device.aggregate-root.test.ts} | 48 +++++++++- .../device.aggregate-root.ts} | 6 +- .../commands/locate-device.command.test.ts | 34 +++++++ .../src/commands/locate-device.command.ts | 8 +- .../src/event-buses/command.event-bus.test.ts | 15 ++++ .../src/event-buses/domain.event-bus.ts | 19 +++- packages/domain/src/index.ts | 2 +- .../src/repositories/device.repository.ts | 2 +- packages/domain/src/test-support.ts | 2 +- .../src/base-classes/aggregate-root.base.ts | 5 +- .../src/base-classes/domain-event.base.ts | 2 + .../toolkit/src/base-classes/task-bus.base.ts | 21 +++-- packages/toolkit/src/index.ts | 1 + .../src/utils/to-dash-case.util.test.ts | 32 +++++++ .../toolkit/src/utils/to-dash-case.util.ts | 6 ++ 18 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 packages/adapter-tcp/src/tcp.server.test.ts create mode 100644 packages/core/src/agnoc.server.test.ts rename packages/domain/src/{entities/device.entity.test.ts => aggregate-roots/device.aggregate-root.test.ts} (88%) rename packages/domain/src/{entities/device.entity.ts => aggregate-roots/device.aggregate-root.ts} (97%) create mode 100644 packages/domain/src/commands/locate-device.command.test.ts create mode 100644 packages/domain/src/event-buses/command.event-bus.test.ts create mode 100644 packages/toolkit/src/utils/to-dash-case.util.test.ts create mode 100644 packages/toolkit/src/utils/to-dash-case.util.ts diff --git a/packages/adapter-tcp/package.json b/packages/adapter-tcp/package.json index 55b05280..399943b7 100644 --- a/packages/adapter-tcp/package.json +++ b/packages/adapter-tcp/package.json @@ -60,7 +60,8 @@ "inputs": [ "{workspaceRoot}/.mocharc.yml", "{workspaceRoot}/nyc.config.js", - "{projectRoot}/src/**/*" + "{projectRoot}/src/**/*", + "{projectRoot}/test/**/*" ], "outputs": [ "{projectRoot}/coverage" diff --git a/packages/adapter-tcp/src/tcp.server.test.ts b/packages/adapter-tcp/src/tcp.server.test.ts new file mode 100644 index 00000000..27104de6 --- /dev/null +++ b/packages/adapter-tcp/src/tcp.server.test.ts @@ -0,0 +1,32 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { TCPServer } from './tcp.server'; +import type { Commands, DeviceRepository } from '@agnoc/domain'; +import type { EventHandlerRegistry, TaskHandlerRegistry } from '@agnoc/toolkit'; + +describe('TCPServer', function () { + let domainEventHandlerRegistry: EventHandlerRegistry; + let commandHandlerRegistry: TaskHandlerRegistry; + let deviceRepository: DeviceRepository; + let tcpAdapter: TCPServer; + + beforeEach(function () { + domainEventHandlerRegistry = imock(); + commandHandlerRegistry = imock(); + deviceRepository = imock(); + tcpAdapter = new TCPServer( + instance(deviceRepository), + instance(domainEventHandlerRegistry), + instance(commandHandlerRegistry), + ); + }); + + it('should listen and close servers', async function () { + await tcpAdapter.listen(); + await tcpAdapter.close(); + }); + + it('should listen and close servers with custom ports', async function () { + await tcpAdapter.listen({ ports: { cmd: 0, map: 0, ntp: 0 } }); + await tcpAdapter.close(); + }); +}); diff --git a/packages/core/src/agnoc.server.test.ts b/packages/core/src/agnoc.server.test.ts new file mode 100644 index 00000000..318b159d --- /dev/null +++ b/packages/core/src/agnoc.server.test.ts @@ -0,0 +1,88 @@ +import { DeviceRepository, LocateDeviceCommand } from '@agnoc/domain'; +import { EventHandlerRegistry, ID, TaskHandlerRegistry } from '@agnoc/toolkit'; +import { capture, fnmock, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { AgnocServer } from './agnoc.server'; +import type { SubscribeHandler } from './agnoc.server'; +import type { Device, DomainEventBus, DeviceLockedDomainEvent } from '@agnoc/domain'; +import type { Server, TaskHandler } from '@agnoc/toolkit'; + +describe('AgnocServer', function () { + let server: Server; + let agnocServer: AgnocServer; + + beforeEach(function () { + server = imock(); + agnocServer = new AgnocServer(); + }); + + it('should provide a container to build an adapter', function () { + agnocServer.buildAdapter((container) => { + expect(container.deviceRepository).to.be.instanceOf(DeviceRepository); + expect(container.domainEventHandlerRegistry).to.be.instanceOf(EventHandlerRegistry); + expect(container.commandHandlerRegistry).to.be.instanceOf(TaskHandlerRegistry); + + return instance(server); + }); + }); + + it('should listen adapters', async function () { + agnocServer.buildAdapter(() => { + return instance(server); + }); + + await agnocServer.listen(); + + verify(server.listen()).once(); + }); + + it('should close adapters', async function () { + agnocServer.buildAdapter(() => { + return instance(server); + }); + + await agnocServer.close(); + + verify(server.close()).once(); + }); + + it('should subscribe to domain events', async function () { + const device: Device = imock(); + const event: DeviceLockedDomainEvent = imock(); + const handler: SubscribeHandler<'DeviceLockedDomainEvent'> = fnmock(); + + agnocServer.subscribe('DeviceLockedDomainEvent', instance(handler)); + + agnocServer.buildAdapter(({ deviceRepository }) => { + void deviceRepository.saveOne(instance(device)); + + return instance(server); + }); + + // Extract the eventBus from the `device.publishEvents` method. + // This is just a way to obtain the eventBus to manually publish events. + const args = capture(device.publishEvents).first(); + const eventBus = args[0] as DomainEventBus; + + await eventBus.emit('DeviceLockedDomainEvent', instance(event)); + + verify(handler(instance(event))).once(); + }); + + it('should trigger commands', async function () { + const taskHandler: TaskHandler = imock(); + const command = new LocateDeviceCommand({ deviceId: new ID(1) }); + + when(taskHandler.forName).thenReturn('LocateDeviceCommand'); + + agnocServer.buildAdapter(({ commandHandlerRegistry }) => { + commandHandlerRegistry.register(instance(taskHandler)); + + return instance(server); + }); + + await agnocServer.trigger(command); + + verify(taskHandler.handle(command)).once(); + }); +}); diff --git a/packages/domain/src/entities/device.entity.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts similarity index 88% rename from packages/domain/src/entities/device.entity.test.ts rename to packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index c6960f4d..71fd4f7a 100644 --- a/packages/domain/src/entities/device.entity.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -1,11 +1,15 @@ import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; import { expect } from 'chai'; +import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; import { DeviceMode, DeviceModeValue } from '../domain-primitives/device-mode.domain-primitive'; import { DeviceState, DeviceStateValue } from '../domain-primitives/device-state.domain-primitive'; import { DeviceWaterLevel, DeviceWaterLevelValue } from '../domain-primitives/device-water-level.domain-primitive'; +import { DeviceMap } from '../entities/device-map.entity'; +import { DeviceOrder } from '../entities/device-order.entity'; import { givenSomeDeviceCleanWorkProps, givenSomeDeviceConsumableProps, @@ -23,9 +27,7 @@ import { DeviceSettings } from '../value-objects/device-settings.value-object'; import { DeviceSystem } from '../value-objects/device-system.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; import { DeviceWlan } from '../value-objects/device-wlan.value-object'; -import { DeviceMap } from './device-map.entity'; -import { DeviceOrder } from './device-order.entity'; -import { Device } from './device.entity'; +import { Device } from './device.aggregate-root'; describe('Device', function () { it('should be created', function () { @@ -41,6 +43,8 @@ describe('Device', function () { expect(device.userId).to.be.equal(deviceProps.userId); expect(device.system).to.be.equal(deviceProps.system); expect(device.version).to.be.equal(deviceProps.version); + expect(device.isConnected).to.be.false; + expect(device.isLocked).to.be.false; }); it("should throw an error when 'userId' is not provided", function () { @@ -91,6 +95,22 @@ describe('Device', function () { ); }); + it("should throw an error when 'isConnected' is not a boolean", function () { + // @ts-expect-error - invalid property + expect(() => new Device({ ...givenSomeDeviceProps(), isConnected: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'isConnected' of Device is not a boolean`, + ); + }); + + it("should throw an error when 'isLocked' is not a boolean", function () { + // @ts-expect-error - invalid property + expect(() => new Device({ ...givenSomeDeviceProps(), isLocked: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'isLocked' of Device is not a boolean`, + ); + }); + it("should throw an error when 'config' is not a DeviceSettings", function () { // @ts-expect-error - invalid property expect(() => new Device({ ...givenSomeDeviceProps(), config: 'foo' })).to.throw( @@ -219,6 +239,28 @@ describe('Device', function () { ); }); + describe('#setAsConnected()', function () { + it('should update the device system', function () { + const device = new Device({ ...givenSomeDeviceProps(), isConnected: false }); + + device.setAsConnected(); + + expect(device.isConnected).to.be.true; + expect(device.domainEvents).to.deep.contain(new DeviceConnectedDomainEvent({ aggregateId: device.id })); + }); + }); + + describe('#setAsLocked()', function () { + it('should update the device system', function () { + const device = new Device({ ...givenSomeDeviceProps(), isLocked: false }); + + device.setAsLocked(); + + expect(device.isLocked).to.be.true; + expect(device.domainEvents).to.deep.contain(new DeviceLockedDomainEvent({ aggregateId: device.id })); + }); + }); + describe('#updateSystem()', function () { it('should update the device system', function () { const device = new Device(givenSomeDeviceProps()); diff --git a/packages/domain/src/entities/device.entity.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts similarity index 97% rename from packages/domain/src/entities/device.entity.ts rename to packages/domain/src/aggregate-roots/device.aggregate-root.ts index 581c6ddb..a98654a7 100644 --- a/packages/domain/src/entities/device.entity.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -7,14 +7,14 @@ import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-pri import { DeviceMode } from '../domain-primitives/device-mode.domain-primitive'; import { DeviceState } from '../domain-primitives/device-state.domain-primitive'; import { DeviceWaterLevel } from '../domain-primitives/device-water-level.domain-primitive'; +import { DeviceMap } from '../entities/device-map.entity'; +import { DeviceOrder } from '../entities/device-order.entity'; import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; import { DeviceSystem } from '../value-objects/device-system.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; import { DeviceWlan } from '../value-objects/device-wlan.value-object'; -import { DeviceMap } from './device-map.entity'; -import { DeviceOrder } from './device-order.entity'; import type { EntityProps } from '@agnoc/toolkit'; /** Describes the properties of a device. */ @@ -276,6 +276,8 @@ export class Device extends AggregateRoot { this.validateInstanceProp(props, 'userId', ID); this.validateInstanceProp(props, 'system', DeviceSystem); this.validateInstanceProp(props, 'version', DeviceVersion); + this.validateTypeProp(props, 'isConnected', 'boolean'); + this.validateTypeProp(props, 'isLocked', 'boolean'); this.validateInstanceProp(props, 'config', DeviceSettings); this.validateInstanceProp(props, 'currentClean', DeviceCleanWork); this.validateArrayProp(props, 'orders', DeviceOrder); diff --git a/packages/domain/src/commands/locate-device.command.test.ts b/packages/domain/src/commands/locate-device.command.test.ts new file mode 100644 index 00000000..64216581 --- /dev/null +++ b/packages/domain/src/commands/locate-device.command.test.ts @@ -0,0 +1,34 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { LocateDeviceCommand } from './locate-device.command'; +import type { LocateDeviceCommandInput } from './locate-device.command'; + +describe('LocateDeviceCommand', function () { + it('should be created', function () { + const input = givenALocateDeviceCommandInput(); + const command = new LocateDeviceCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new LocateDeviceCommand({ ...givenALocateDeviceCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for LocateDeviceCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new LocateDeviceCommand({ ...givenALocateDeviceCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of LocateDeviceCommand is not an instance of ID`, + ); + }); +}); + +function givenALocateDeviceCommandInput(): LocateDeviceCommandInput { + return { deviceId: ID.generate() }; +} diff --git a/packages/domain/src/commands/locate-device.command.ts b/packages/domain/src/commands/locate-device.command.ts index f9bd79b2..b6acec85 100644 --- a/packages/domain/src/commands/locate-device.command.ts +++ b/packages/domain/src/commands/locate-device.command.ts @@ -1,5 +1,4 @@ -import { Command } from '@agnoc/toolkit'; -import type { ID } from '@agnoc/toolkit'; +import { Command, ID } from '@agnoc/toolkit'; export interface LocateDeviceCommandInput { deviceId: ID; @@ -10,7 +9,8 @@ export class LocateDeviceCommand extends Command return this.props.deviceId; } - protected validate(): void { - // TODO: validate input + protected validate(props: LocateDeviceCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); } } diff --git a/packages/domain/src/event-buses/command.event-bus.test.ts b/packages/domain/src/event-buses/command.event-bus.test.ts new file mode 100644 index 00000000..b4a47fdc --- /dev/null +++ b/packages/domain/src/event-buses/command.event-bus.test.ts @@ -0,0 +1,15 @@ +import { TaskBus } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { CommandBus } from './command.event-bus'; + +describe('CommandBus', function () { + let commandBus: CommandBus; + + beforeEach(function () { + commandBus = new CommandBus(); + }); + + it('should be created', function () { + expect(commandBus).to.be.instanceOf(TaskBus); + }); +}); diff --git a/packages/domain/src/event-buses/domain.event-bus.ts b/packages/domain/src/event-buses/domain.event-bus.ts index 4d2754a6..d993dc3f 100644 --- a/packages/domain/src/event-buses/domain.event-bus.ts +++ b/packages/domain/src/event-buses/domain.event-bus.ts @@ -1,5 +1,20 @@ -import { EventBus } from '@agnoc/toolkit'; +import { EventBus, debug } from '@agnoc/toolkit'; import type { DomainEventNames, DomainEvents } from '../domain-events/domain-events'; export type DomainEventBusEvents = { [Name in DomainEventNames]: DomainEvents[Name] }; -export class DomainEventBus extends EventBus {} +export class DomainEventBus extends EventBus { + constructor() { + /* istanbul ignore next */ + super({ + debug: { + enabled: true, + name: DomainEventBus.name, + logger: (type, _, eventName, eventData) => { + debug(__filename).extend(type)( + `event '${eventName?.toString() ?? 'undefined'}' with data: ${JSON.stringify(eventData)}`, + ); + }, + }, + }); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index a65934c9..42624555 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -14,7 +14,7 @@ export * from './domain-primitives/device-water-level.domain-primitive'; export * from './domain-primitives/week-day.domain-primitive'; export * from './entities/device-map.entity'; export * from './entities/device-order.entity'; -export * from './entities/device.entity'; +export * from './aggregate-roots/device.aggregate-root'; export * from './entities/room.entity'; export * from './entities/zone.entity'; export * from './event-buses/command.event-bus'; diff --git a/packages/domain/src/repositories/device.repository.ts b/packages/domain/src/repositories/device.repository.ts index 504e2a99..12db51dd 100644 --- a/packages/domain/src/repositories/device.repository.ts +++ b/packages/domain/src/repositories/device.repository.ts @@ -1,4 +1,4 @@ import { Repository } from '@agnoc/toolkit'; -import type { Device } from '../entities/device.entity'; +import type { Device } from '../aggregate-roots/device.aggregate-root'; export class DeviceRepository extends Repository {} diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index b5b19074..5f4f5631 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -15,9 +15,9 @@ import { MapCoordinate } from './value-objects/map-coordinate.value-object'; import { MapPixel } from './value-objects/map-pixel.value-object'; import { QuietHoursSetting } from './value-objects/quiet-hours-setting.value-object'; import { VoiceSetting } from './value-objects/voice-setting.value-object'; +import type { DeviceProps } from './aggregate-roots/device.aggregate-root'; import type { DeviceMapProps } from './entities/device-map.entity'; import type { DeviceOrderProps } from './entities/device-order.entity'; -import type { DeviceProps } from './entities/device.entity'; import type { RoomProps } from './entities/room.entity'; import type { ZoneProps } from './entities/zone.entity'; import type { DeviceCleanWorkProps } from './value-objects/device-clean-work.value-object'; diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts index 61d69b2b..691abc5d 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -1,11 +1,12 @@ import { debug } from '../utils/debug.util'; +import { toDashCase } from '../utils/to-dash-case.util'; import { Entity } from './entity.base'; import type { DomainEvent } from './domain-event.base'; import type { EntityProps } from './entity.base'; import type { EventBus } from './event-bus.base'; export abstract class AggregateRoot extends Entity { - private readonly debug = debug(__filename).extend(`${this.constructor.name.toLowerCase()}:${this.id.value}`); + private readonly debug = debug(__filename).extend(toDashCase(this.constructor.name)).extend(this.id.toString()); readonly #domainEvents = new Set(); get domainEvents(): DomainEvent[] { @@ -19,7 +20,7 @@ export abstract class AggregateRoot extends async publishEvents(eventBus: EventBus): Promise { await Promise.all( this.domainEvents.map(async (event) => { - this.debug(`publishing domain event '${event.constructor.name}'`); + this.debug(`publishing domain event '${event.constructor.name}' with data: ${JSON.stringify(event)}`); return eventBus.emit(event.constructor.name, event); }), ); diff --git a/packages/toolkit/src/base-classes/domain-event.base.ts b/packages/toolkit/src/base-classes/domain-event.base.ts index 17037cfa..c5fd6e58 100644 --- a/packages/toolkit/src/base-classes/domain-event.base.ts +++ b/packages/toolkit/src/base-classes/domain-event.base.ts @@ -6,6 +6,8 @@ export interface DomainEventProps { } export abstract class DomainEvent extends Validatable { + readonly eventName = this.constructor.name; + constructor(protected readonly props: T) { super(props); } diff --git a/packages/toolkit/src/base-classes/task-bus.base.ts b/packages/toolkit/src/base-classes/task-bus.base.ts index fa62984b..315d4963 100644 --- a/packages/toolkit/src/base-classes/task-bus.base.ts +++ b/packages/toolkit/src/base-classes/task-bus.base.ts @@ -1,20 +1,23 @@ import { DomainException } from '../exceptions/domain.exception'; +import { debug } from '../utils/debug.util'; +import { toDashCase } from '../utils/to-dash-case.util'; import type { Task, TaskOutput } from './task.base'; export type TaskBusSubscribeHandler = ( task: Events[Name], ) => TaskOutput | Promise>; -// export type TaskBusTasks = Record>; - export type TaskBusTasks = { [key: string]: Task; }; export abstract class TaskBus { + private readonly debug = debug(__filename).extend(toDashCase(this.constructor.name)); private readonly handlers = new Map>>(); subscribe(name: Name, handler: TaskBusSubscribeHandler): void { + this.debug(`subscribing to task '${name.toString()}'`); + if (!this.handlers.has(name)) { this.handlers.set(name, new Set()); } @@ -23,10 +26,14 @@ export abstract class TaskBus { } async trigger(task: Task): Promise> { - const handlers = this.handlers.get(task.constructor.name as keyof Tasks); + const name = task.constructor.name as keyof Tasks & string; + + this.debug(`triggering task '${name}'`); + + const handlers = this.handlers.get(name as keyof Tasks); if (!handlers) { - throw new DomainException(`No handlers registered for ${task.constructor.name}`); + throw new DomainException(`No handlers registered for ${name}`); } const promises = [...handlers].map(async (handler) => handler(task)); @@ -34,17 +41,19 @@ export abstract class TaskBus { const fulfilledOutputs = this.filterFulfilledPromises(outputs); if (fulfilledOutputs.length === 0) { - throw new DomainException(`No handlers fulfilled for ${task.constructor.name}`); + throw new DomainException(`No handlers fulfilled for ${name}`); } if (fulfilledOutputs.length > 1) { - throw new DomainException(`Multiple handlers fulfilled for ${task.constructor.name}`); + throw new DomainException(`Multiple handlers fulfilled for ${name}`); } const output = fulfilledOutputs[0].value; task.validateOutput?.(output); + this.debug(`task '${name}' completed successfully with output: ${JSON.stringify(output)}`); + return output; } diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 3d880853..e986c5e6 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -43,5 +43,6 @@ export * from './utils/is-empty.util'; export * from './utils/is-object.util'; export * from './utils/is-present.util'; export * from './utils/stream.util'; +export * from './utils/to-dash-case.util'; export * from './utils/to-stream.util'; export * from './utils/wait-for.util'; diff --git a/packages/toolkit/src/utils/to-dash-case.util.test.ts b/packages/toolkit/src/utils/to-dash-case.util.test.ts new file mode 100644 index 00000000..6ccf5678 --- /dev/null +++ b/packages/toolkit/src/utils/to-dash-case.util.test.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import { toDashCase } from './to-dash-case.util'; + +describe('toDashCase', function () { + it('should convert camelCase to dash-case', function () { + expect(toDashCase('camelCase')).to.be.equal('camel-case'); + }); + + it('should convert PascalCase to dash-case', function () { + expect(toDashCase('PascalCase')).to.be.equal('pascal-case'); + }); + + it('should convert snake_case to dash-case', function () { + expect(toDashCase('snake_case')).to.be.equal('snake-case'); + }); + + it('should convert UPPER_CASE to dash-case', function () { + expect(toDashCase('UPPER_CASE')).to.be.equal('upper-case'); + }); + + it('should convert dot.case to dash-case', function () { + expect(toDashCase('dot.case')).to.be.equal('dot-case'); + }); + + it('should convert space case to dash-case', function () { + expect(toDashCase('space case')).to.be.equal('space-case'); + }); + + it('should convert Title Case to dash-case', function () { + expect(toDashCase('Title Case')).to.be.equal('title-case'); + }); +}); diff --git a/packages/toolkit/src/utils/to-dash-case.util.ts b/packages/toolkit/src/utils/to-dash-case.util.ts new file mode 100644 index 00000000..26531f61 --- /dev/null +++ b/packages/toolkit/src/utils/to-dash-case.util.ts @@ -0,0 +1,6 @@ +export function toDashCase(str: string): string { + return str + .replace(/[_. ]/g, '-') + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); +} From 39e10ebff77cd76c86cdbc441803bc64b0c3e684 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 21 Mar 2023 16:01:34 +0100 Subject: [PATCH 12/38] feat: add connection repository --- .../packet-connection.aggregate-root.ts} | 71 ++++------- .../locate-device.event-handler.ts | 18 ++- ...s-connected-event-handler.event-handler.ts | 18 ++- ...e-is-locked-event-handler.event-handler.ts | 20 ++- ...-connection-device-changed.domain-event.ts | 32 +++++ .../src/factories/connection.factory.ts | 13 ++ .../src/packet-server.connection-handler.ts | 51 +++----- packages/adapter-tcp/src/packet.message.ts | 4 +- packages/adapter-tcp/src/tcp.server.test.ts | 5 +- packages/adapter-tcp/src/tcp.server.ts | 22 +++- .../test/integration/tcp.server.test.ts | 34 ++--- packages/core/src/agnoc.server.test.ts | 3 +- packages/core/src/agnoc.server.ts | 6 +- .../connection.aggregate-root.test.ts | 116 ++++++++++++++++++ .../connection.aggregate-root.ts | 42 +++++++ ...ection-device-changed.domain-event.test.ts | 31 +++++ .../connection-device-changed.domain-event.ts | 21 ++++ .../domain/src/domain-events/domain-events.ts | 2 + packages/domain/src/index.ts | 5 +- .../connection.repository.test.ts | 52 ++++++++ .../src/repositories/connection.repository.ts | 15 +++ packages/domain/src/test-support.ts | 14 +++ packages/eslint-config/typescript.js | 1 + .../src/base-classes/repository.base.ts | 2 +- 24 files changed, 473 insertions(+), 125 deletions(-) rename packages/adapter-tcp/src/{device.connection.ts => aggregate-roots/packet-connection.aggregate-root.ts} (52%) create mode 100644 packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts create mode 100644 packages/adapter-tcp/src/factories/connection.factory.ts create mode 100644 packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts create mode 100644 packages/domain/src/aggregate-roots/connection.aggregate-root.ts create mode 100644 packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/connection-device-changed.domain-event.ts create mode 100644 packages/domain/src/repositories/connection.repository.test.ts create mode 100644 packages/domain/src/repositories/connection.repository.ts diff --git a/packages/adapter-tcp/src/device.connection.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts similarity index 52% rename from packages/adapter-tcp/src/device.connection.ts rename to packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts index 02476bd7..d817e1ee 100644 --- a/packages/adapter-tcp/src/device.connection.ts +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts @@ -1,9 +1,9 @@ -import { Device } from '@agnoc/domain'; -import { ArgumentInvalidException, DomainException, ID } from '@agnoc/toolkit'; +import { Connection } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; import { PacketSocket } from '@agnoc/transport-tcp'; -import Emittery from 'emittery'; -import type { PacketEventBus } from './packet.event-bus'; -import type { PacketMessage } from './packet.message'; +import type { PacketEventBus } from '../packet.event-bus'; +import type { PacketMessage } from '../packet.message'; +import type { ConnectionProps } from '@agnoc/domain'; import type { Packet, PacketFactory, @@ -12,37 +12,23 @@ import type { CreatePacketProps, } from '@agnoc/transport-tcp'; -export interface DeviceConnectionEvents { - data: Packet; - close: undefined; - error: Error; +export interface PacketConnectionProps extends ConnectionProps { + socket: PacketSocket; } -export class DeviceConnection extends Emittery { - #device?: Device; +export class PacketConnection extends Connection { + readonly connectionType = 'PACKET'; constructor( private readonly packetFactory: PacketFactory, private readonly eventBus: PacketEventBus, - private readonly socket: PacketSocket, + props: PacketConnectionProps, ) { - super(); - this.validateSocket(); - this.addListeners(); + super(props); } - get device(): Device | undefined { - return this.#device; - } - - set device(device: Device | undefined) { - if (device && !(device instanceof Device)) { - throw new ArgumentInvalidException( - `Value '${device as string} for property 'device' of Connection is not an instance of Device`, - ); - } - - this.#device = device; + get socket(): PacketSocket { + return this.props.socket; } send(name: Name, object: PayloadObjectFrom): Promise { @@ -73,8 +59,15 @@ export class DeviceConnection extends Emittery { return this.socket.end(); } + protected override validate(props: PacketConnectionProps): void { + super.validate(props); + + this.validateDefinedProp(props, 'socket'); + this.validateInstanceProp(props, 'socket', PacketSocket); + } + private getPacketProps(): CreatePacketProps { - return { deviceId: this.#device?.id ?? new ID(0), userId: this.#device?.userId ?? new ID(0) }; + return { deviceId: this.device?.id ?? new ID(0), userId: this.device?.userId ?? new ID(0) }; } private writeAndWait(packet: Packet): Promise { @@ -92,25 +85,7 @@ export class DeviceConnection extends Emittery { return this.socket.write(packet); } - private validateSocket() { - if (!(this.socket instanceof PacketSocket)) { - throw new DomainException('Socket for Connection is not an instance of PacketSocket'); - } - - if (!this.socket.connected) { - throw new DomainException('Socket for Connection is closed'); - } - } - - private addListeners() { - this.socket.on('data', (packet) => { - void this.emit('data', packet); - }); - this.socket.on('error', (err) => { - void this.emit('error', err); - }); - this.socket.on('close', () => { - void this.emit('close'); - }); + static isPacketConnection(connection: Connection): connection is PacketConnection { + return connection.connectionType === 'PACKET'; } } diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts index e235364a..4f2b615f 100644 --- a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts @@ -1,19 +1,27 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; -import type { CommandHandler, LocateDeviceCommand } from '@agnoc/domain'; +import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import type { CommandHandler, Connection, ConnectionRepository, LocateDeviceCommand } from '@agnoc/domain'; export class LocateDeviceEventHandler implements CommandHandler { readonly forName = 'LocateDeviceCommand'; - constructor(private readonly connectionManager: PackerServerConnectionHandler) {} + constructor(private readonly connectionRepository: ConnectionRepository) {} async handle(event: LocateDeviceCommand): Promise { - const [connection] = this.connectionManager.findConnectionsByDeviceId(event.deviceId); + const connections = await this.connectionRepository.findByDeviceId(event.deviceId); - if (!connection || !connection.device) { + if (connections.length === 0) { throw new DomainException(`Unable to find a connection for the device with id ${event.deviceId.value}`); } + const connection = connections.find((connection: Connection): connection is PacketConnection => + PacketConnection.isPacketConnection(connection), + ); + + if (!connection) { + return; + } + const response = await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); if (response.packet.payload.opcode.value !== 'DEVICE_SEEK_LOCATION_RSP') { diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index aa2eb1ef..76c411c9 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -1,19 +1,27 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; -import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; +import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import type { DomainEventHandler, DeviceConnectedDomainEvent, ConnectionRepository, Connection } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { readonly forName = 'DeviceConnectedDomainEvent'; - constructor(private readonly connectionManager: PackerServerConnectionHandler) {} + constructor(private readonly connectionRepository: ConnectionRepository) {} async handle(event: DeviceConnectedDomainEvent): Promise { - const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); + const connections = await this.connectionRepository.findByDeviceId(event.aggregateId); - if (!connection) { + if (connections.length === 0) { throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); } + const connection = connections.find((connection: Connection): connection is PacketConnection => + PacketConnection.isPacketConnection(connection), + ); + + if (!connection) { + return; + } + await connection.send('DEVICE_CONTROL_LOCK_REQ', {}); } } diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index d46a09dc..af2b38e5 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -1,20 +1,28 @@ import { DeviceCapability } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { PackerServerConnectionHandler } from '../../packet-server.connection-handler'; -import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; +import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceLockedDomainEvent, DomainEventHandler, Connection, ConnectionRepository } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { readonly forName = 'DeviceLockedDomainEvent'; - constructor(private readonly connectionManager: PackerServerConnectionHandler) {} + constructor(private readonly connectionRepository: ConnectionRepository) {} async handle(event: DeviceLockedDomainEvent): Promise { - const [connection] = this.connectionManager.findConnectionsByDeviceId(event.aggregateId); + const connections = await this.connectionRepository.findByDeviceId(event.aggregateId); - if (!connection || !connection.device) { + if (connections.length === 0) { throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); } + const connection = connections.find((connection: Connection): connection is PacketConnection => + PacketConnection.isPacketConnection(connection), + ); + + if (!connection) { + return; + } + await connection.send('DEVICE_STATUS_GETTING_REQ', {}); await connection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { unk1: 0, unk2: '' }); @@ -23,7 +31,7 @@ export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEven // TODO: move this to a get map service. await connection.send('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', { - mask: connection.device.system.supports(DeviceCapability.MAP_PLANS) ? 0x78ff : 0xff, + mask: connection.device?.system.supports(DeviceCapability.MAP_PLANS) ? 0x78ff : 0xff, }); // TODO: move this to a get wlan service. diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts new file mode 100644 index 00000000..ef44589c --- /dev/null +++ b/packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts @@ -0,0 +1,32 @@ +import type { + DomainEventHandler, + ConnectionRepository, + DeviceRepository, + ConnectionDeviceChangedDomainEvent, +} from '@agnoc/domain'; + +export class SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler implements DomainEventHandler { + readonly forName = 'ConnectionDeviceChangedDomainEvent'; + + constructor( + private readonly connectionRepository: ConnectionRepository, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: ConnectionDeviceChangedDomainEvent): Promise { + if (event.currentDeviceId) { + const connections = await this.connectionRepository.findByDeviceId(event.currentDeviceId); + const device = await this.deviceRepository.findOneById(event.currentDeviceId); + + // This is a hack to only mark the device as connected if there is more than one connection. + // Here we should check that the connections are from the same ip address. + if (connections.length > 1 && device && !device.isConnected) { + device.setAsConnected(); + + await this.deviceRepository.saveOne(device); + } + } + + // TODO: handle device disconnection + } +} diff --git a/packages/adapter-tcp/src/factories/connection.factory.ts b/packages/adapter-tcp/src/factories/connection.factory.ts new file mode 100644 index 00000000..431cdfe4 --- /dev/null +++ b/packages/adapter-tcp/src/factories/connection.factory.ts @@ -0,0 +1,13 @@ +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionProps } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketEventBus } from '../packet.event-bus'; +import type { Factory } from '@agnoc/toolkit'; +import type { PacketFactory } from '@agnoc/transport-tcp'; + +export class PacketConnectionFactory implements Factory { + constructor(private readonly packetEventBus: PacketEventBus, private readonly packetFactory: PacketFactory) {} + + create(props: PacketConnectionProps): PacketConnection { + return new PacketConnection(this.packetFactory, this.packetEventBus, props); + } +} diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts index 8b7c693c..e183361b 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -1,26 +1,21 @@ -import { DomainException } from '@agnoc/toolkit'; -import { DeviceConnection } from './device.connection'; +import { DomainException, ID } from '@agnoc/toolkit'; import { PacketMessage } from './packet.message'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFactory } from './factories/connection.factory'; import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; -import type { DeviceRepository, Device } from '@agnoc/domain'; -import type { ID } from '@agnoc/toolkit'; -import type { PacketServer, PacketFactory, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; +import type { DeviceRepository, Device, Connection, ConnectionRepository } from '@agnoc/domain'; +import type { PacketServer, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; export class PackerServerConnectionHandler { - private readonly servers = new Map>(); + private readonly servers = new Map>(); constructor( private readonly packetEventBus: PacketEventBus, - private readonly packetFactory: PacketFactory, private readonly deviceRepository: DeviceRepository, + private readonly connectionRepository: ConnectionRepository, + private readonly packetConnectionFactory: PacketConnectionFactory, ) {} - findConnectionsByDeviceId(deviceId: ID): DeviceConnection[] { - const connections = [...this.servers.values()].flatMap((connections) => [...connections]); - - return connections.filter((connection) => connection.device?.id.equals(deviceId)); - } - addServers(...servers: PacketServer[]): void { servers.forEach((server) => { this.servers.set(server, new Set()); @@ -30,11 +25,11 @@ export class PackerServerConnectionHandler { private addListeners(server: PacketServer) { server.on('connection', (socket) => { - const connection = new DeviceConnection(this.packetFactory, this.packetEventBus, socket); + const connection = this.packetConnectionFactory.create({ id: ID.generate(), socket }); this.servers.get(server)?.add(connection); - connection.on('data', async (packet: Packet) => { + connection.socket.on('data', async (packet: Packet) => { const packetMessage = new PacketMessage(connection, packet); // Update the device on the connection if the device id has changed. @@ -42,13 +37,9 @@ export class PackerServerConnectionHandler { // Send the packet message to the packet event bus. await this.emitPacketEvent(packetMessage); - - // This is a hack to only mark the device as connected if there is more than one connection. - // Here we should check that the connections are from the same ip address. - await this.tryToSetDeviceAsConnected(connection); }); - connection.on('close', () => { + connection.socket.on('close', () => { this.servers.get(server)?.delete(connection); }); }); @@ -64,18 +55,6 @@ export class PackerServerConnectionHandler { }); } - private async tryToSetDeviceAsConnected(connection: DeviceConnection) { - if (connection.device && !connection.device.isConnected) { - const connections = this.findConnectionsByDeviceId(connection.device.id); - - if (connections.length > 1) { - connection.device.setAsConnected(); - - await this.deviceRepository.saveOne(connection.device); - } - } - } - private async emitPacketEvent(message: PacketMessage) { const name = message.packet.payload.opcode.name as PayloadObjectName; const sequence = message.packet.sequence.toString(); @@ -99,9 +78,13 @@ export class PackerServerConnectionHandler { } } - private async updateConnectionDevice(packet: Packet, connection: DeviceConnection) { + private async updateConnectionDevice(packet: Packet, connection: Connection) { if (!packet.deviceId.equals(connection.device?.id)) { - connection.device = await this.findDeviceById(packet.deviceId); + const device = await this.findDeviceById(packet.deviceId); + + connection.setDevice(device); + + await this.connectionRepository.saveOne(connection); } } diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts index a5cffcce..b3e7d9fc 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/packet.message.ts @@ -1,9 +1,9 @@ -import type { DeviceConnection } from './device.connection'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; import type { Device } from '@agnoc/domain'; import type { Packet, PayloadObjectFrom, PayloadObjectName } from '@agnoc/transport-tcp'; export class PacketMessage { - constructor(readonly connection: DeviceConnection, readonly packet: Packet) {} + constructor(readonly connection: PacketConnection, readonly packet: Packet) {} get device(): Device | undefined { return this.connection.device; diff --git a/packages/adapter-tcp/src/tcp.server.test.ts b/packages/adapter-tcp/src/tcp.server.test.ts index 27104de6..49b782fa 100644 --- a/packages/adapter-tcp/src/tcp.server.test.ts +++ b/packages/adapter-tcp/src/tcp.server.test.ts @@ -1,20 +1,23 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { TCPServer } from './tcp.server'; -import type { Commands, DeviceRepository } from '@agnoc/domain'; +import type { Commands, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; import type { EventHandlerRegistry, TaskHandlerRegistry } from '@agnoc/toolkit'; describe('TCPServer', function () { let domainEventHandlerRegistry: EventHandlerRegistry; let commandHandlerRegistry: TaskHandlerRegistry; let deviceRepository: DeviceRepository; + let connectionRepository: ConnectionRepository; let tcpAdapter: TCPServer; beforeEach(function () { domainEventHandlerRegistry = imock(); commandHandlerRegistry = imock(); deviceRepository = imock(); + connectionRepository = imock(); tcpAdapter = new TCPServer( instance(deviceRepository), + instance(connectionRepository), instance(domainEventHandlerRegistry), instance(commandHandlerRegistry), ); diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index cb02d79c..0fe43ff8 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -11,6 +11,7 @@ import { import { LocateDeviceEventHandler } from './event-handlers/command-event-handlers/locate-device.event-handler'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; +import { SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler } from './event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event'; import { ClientHeartbeatEventHandler } from './event-handlers/packet-event-handlers/client-heartbeat.event-handler'; import { ClientLoginEventHandler } from './event-handlers/packet-event-handlers/client-login.event-handler'; import { DeviceBatteryUpdateEventHandler } from './event-handlers/packet-event-handlers/device-battery-update.event-handler'; @@ -31,6 +32,7 @@ import { DeviceTimeUpdateEventHandler } from './event-handlers/packet-event-hand import { DeviceUpgradeInfoEventHandler } from './event-handlers/packet-event-handlers/device-upgrade-info.event-handler'; import { DeviceVersionUpdateEventHandler } from './event-handlers/packet-event-handlers/device-version-update.event-handler'; import { DeviceWlanUpdateEventHandler } from './event-handlers/packet-event-handlers/device-wlan-update.event-handler'; +import { PacketConnectionFactory } from './factories/connection.factory'; import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; import { DeviceErrorMapper } from './mappers/device-error.mapper'; import { DeviceFanSpeedMapper } from './mappers/device-fan-speed.mapper'; @@ -41,7 +43,7 @@ import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; -import type { Commands, DeviceRepository } from '@agnoc/domain'; +import type { Commands, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; import type { Server, TaskHandlerRegistry } from '@agnoc/toolkit'; import type { AddressInfo } from 'net'; @@ -52,6 +54,7 @@ export class TCPServer implements Server { constructor( private readonly deviceRepository: DeviceRepository, + private readonly connectionRepository: ConnectionRepository, private readonly domainEventHandlerRegistry: EventHandlerRegistry, private readonly commandHandlerRegistry: TaskHandlerRegistry, ) { @@ -78,8 +81,14 @@ export class TCPServer implements Server { const packetEventBus = new PacketEventBus(); const packetEventHandlerRegistry = new EventHandlerRegistry(packetEventBus); - // Connection managers - const connectionManager = new PackerServerConnectionHandler(packetEventBus, packetFactory, this.deviceRepository); + // Connection + const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); + const connectionManager = new PackerServerConnectionHandler( + packetEventBus, + this.deviceRepository, + this.connectionRepository, + packetConnectionFactory, + ); connectionManager.addServers(this.cmdServer, this.mapServer); @@ -127,12 +136,13 @@ export class TCPServer implements Server { // Domain event handlers this.domainEventHandlerRegistry.register( - new LockDeviceWhenDeviceIsConnectedEventHandler(connectionManager), - new QueryDeviceInfoWhenDeviceIsLockedEventHandler(connectionManager), + new LockDeviceWhenDeviceIsConnectedEventHandler(connectionRepository), + new QueryDeviceInfoWhenDeviceIsLockedEventHandler(connectionRepository), + new SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler(connectionRepository, deviceRepository), ); // Command event handlers - this.commandHandlerRegistry.register(new LocateDeviceEventHandler(connectionManager)); + this.commandHandlerRegistry.register(new LocateDeviceEventHandler(connectionRepository)); } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { diff --git a/packages/adapter-tcp/test/integration/tcp.server.test.ts b/packages/adapter-tcp/test/integration/tcp.server.test.ts index 280a3353..fc9bc7cf 100644 --- a/packages/adapter-tcp/test/integration/tcp.server.test.ts +++ b/packages/adapter-tcp/test/integration/tcp.server.test.ts @@ -1,5 +1,6 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { once } from 'events'; -import { CommandBus, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { CommandBus, ConnectionRepository, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; import { EventHandlerRegistry, ID, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; import { @@ -18,12 +19,13 @@ import type { Commands } from '@agnoc/domain'; import type { ICLIENT_ONLINE_REQ, IDEVICE_REGISTER_REQ } from '@agnoc/schemas-tcp'; import type { CreatePacketProps, Packet } from '@agnoc/transport-tcp'; -describe('TCPAdapter', function () { +describe('Integration', function () { let domainEventBus: DomainEventBus; let commandBus: CommandBus; let domainEventHandlerRegistry: EventHandlerRegistry; let commandHandlerRegistry: TaskHandlerRegistry; let deviceRepository: DeviceRepository; + let connectionRepository: ConnectionRepository; let tcpAdapter: TCPServer; let packetSocket: PacketSocket; let secondPacketSocket: PacketSocket; @@ -37,7 +39,13 @@ describe('TCPAdapter', function () { domainEventHandlerRegistry = new EventHandlerRegistry(domainEventBus); commandHandlerRegistry = new TaskHandlerRegistry(commandBus); deviceRepository = new DeviceRepository(domainEventBus, new MemoryAdapter()); - tcpAdapter = new TCPServer(deviceRepository, domainEventHandlerRegistry, commandHandlerRegistry); + connectionRepository = new ConnectionRepository(domainEventBus, new MemoryAdapter()); + tcpAdapter = new TCPServer( + deviceRepository, + connectionRepository, + domainEventHandlerRegistry, + commandHandlerRegistry, + ); // Client blocks const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); @@ -136,6 +144,7 @@ describe('TCPAdapter', function () { it('should handle a device connection', async function () { const device = new Device(givenSomeDeviceProps()); let receivedPacket: Packet; + let secondReceivedPacket: Packet; await deviceRepository.saveOne(device); @@ -159,21 +168,18 @@ describe('TCPAdapter', function () { // The device already has two identified connections and // the device should be marked as connected. - await domainEventBus.once('DeviceConnectedDomainEvent'); + // eslint-disable-next-line prefer-const + [, [receivedPacket], [secondReceivedPacket]] = await Promise.all([ + domainEventBus.once('DeviceConnectedDomainEvent'), + once(packetSocket, 'data') as Promise[]>, + once(secondPacketSocket, 'data') as Promise[]>, + ]); expect(device.isConnected).to.be.true; - - [receivedPacket] = (await once(secondPacketSocket, 'data')) as Packet<'CLIENT_HEARTBEAT_RSP'>[]; - - expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); - - expect(device.isConnected).to.be.true; - - [receivedPacket] = (await once(packetSocket, 'data')) as Packet<'DEVICE_CONTROL_LOCK_REQ'>[]; - expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_CONTROL_LOCK_REQ'); + expect(secondReceivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); - void secondPacketSocket.write( + void packetSocket.write( packetFactory.create('DEVICE_CONTROL_LOCK_RSP', { result: 0 }, givenSomeCreatePacketProps(device)), ); diff --git a/packages/core/src/agnoc.server.test.ts b/packages/core/src/agnoc.server.test.ts index 318b159d..f874f660 100644 --- a/packages/core/src/agnoc.server.test.ts +++ b/packages/core/src/agnoc.server.test.ts @@ -1,4 +1,4 @@ -import { DeviceRepository, LocateDeviceCommand } from '@agnoc/domain'; +import { ConnectionRepository, DeviceRepository, LocateDeviceCommand } from '@agnoc/domain'; import { EventHandlerRegistry, ID, TaskHandlerRegistry } from '@agnoc/toolkit'; import { capture, fnmock, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; @@ -19,6 +19,7 @@ describe('AgnocServer', function () { it('should provide a container to build an adapter', function () { agnocServer.buildAdapter((container) => { expect(container.deviceRepository).to.be.instanceOf(DeviceRepository); + expect(container.connectionRepository).to.be.instanceOf(ConnectionRepository); expect(container.domainEventHandlerRegistry).to.be.instanceOf(EventHandlerRegistry); expect(container.commandHandlerRegistry).to.be.instanceOf(TaskHandlerRegistry); diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts index d084f2c9..359d3f86 100644 --- a/packages/core/src/agnoc.server.ts +++ b/packages/core/src/agnoc.server.ts @@ -1,4 +1,4 @@ -import { CommandBus, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { CommandBus, ConnectionRepository, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { EventHandlerRegistry, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; import type { DomainEventNames, DomainEvents, Commands } from '@agnoc/domain'; import type { Server, TaskOutput } from '@agnoc/toolkit'; @@ -9,6 +9,7 @@ export class AgnocServer implements Server { private readonly commandBus: CommandBus; private readonly commandHandlerRegistry: TaskHandlerRegistry; private readonly deviceRepository: DeviceRepository; + private readonly connectionRepository: ConnectionRepository; private readonly adapters = new Set(); constructor() { @@ -17,6 +18,7 @@ export class AgnocServer implements Server { this.commandBus = new CommandBus(); this.commandHandlerRegistry = new TaskHandlerRegistry(this.commandBus); this.deviceRepository = new DeviceRepository(this.domainEventBus, new MemoryAdapter()); + this.connectionRepository = new ConnectionRepository(this.domainEventBus, new MemoryAdapter()); } subscribe(eventName: Name, handler: SubscribeHandler): void { @@ -32,6 +34,7 @@ export class AgnocServer implements Server { domainEventHandlerRegistry: this.domainEventHandlerRegistry, commandHandlerRegistry: this.commandHandlerRegistry, deviceRepository: this.deviceRepository, + connectionRepository: this.connectionRepository, }); this.adapters.add(adapter); @@ -52,6 +55,7 @@ export type Container = { domainEventHandlerRegistry: EventHandlerRegistry; commandHandlerRegistry: TaskHandlerRegistry; deviceRepository: DeviceRepository; + connectionRepository: ConnectionRepository; }; export type SubscribeHandler = (event: DomainEvents[Name]) => Promise; diff --git a/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts new file mode 100644 index 00000000..b3bc053d --- /dev/null +++ b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts @@ -0,0 +1,116 @@ +import { AggregateRoot, ArgumentInvalidException } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { ConnectionDeviceChangedDomainEvent } from '../domain-events/connection-device-changed.domain-event'; +import { givenSomeDeviceProps, givenSomeConnectionProps } from '../test-support'; +import { Connection } from './connection.aggregate-root'; +import { Device } from './device.aggregate-root'; +import type { ConnectionProps } from './connection.aggregate-root'; + +describe('Connection', function () { + it('should be created', function () { + const props = givenSomeDeviceProps(); + const connection = new DummyConnection(props); + + expect(connection).to.be.instanceOf(AggregateRoot); + expect(connection.id).to.be.equal(props.id); + }); + + it('should be created with device', function () { + const device = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device }); + + expect(connection.device).to.be.equal(device); + }); + + it("should throw an error when 'deviceId' is not a Device", function () { + // @ts-expect-error - invalid property + expect(() => new DummyConnection({ ...givenSomeConnectionProps(), device: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'device' of DummyConnection is not an instance of Device`, + ); + }); + + describe('#setDevice', function () { + it('should set a device', function () { + const device = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device: undefined }); + + connection.setDevice(device); + + expect(connection.device).to.be.equal(device); + expect(connection.domainEvents).to.deep.contain( + new ConnectionDeviceChangedDomainEvent({ + aggregateId: connection.id, + previousDeviceId: undefined, + currentDeviceId: device.id, + }), + ); + }); + + it('should override a device', function () { + const deviceA = new Device(givenSomeDeviceProps()); + const deviceB = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device: deviceA }); + + connection.setDevice(deviceB); + + expect(connection.device).to.be.equal(deviceB); + expect(connection.domainEvents).to.deep.contain( + new ConnectionDeviceChangedDomainEvent({ + aggregateId: connection.id, + previousDeviceId: deviceA.id, + currentDeviceId: deviceB.id, + }), + ); + }); + + it('should unset a device', function () { + const device = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device }); + + connection.setDevice(undefined); + + expect(connection.device).to.be.equal(undefined); + expect(connection.domainEvents).to.deep.contain( + new ConnectionDeviceChangedDomainEvent({ + aggregateId: connection.id, + previousDeviceId: device.id, + currentDeviceId: undefined, + }), + ); + }); + + it('should do nothing when setting the same device', function () { + const device = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device }); + + connection.setDevice(device); + + expect(connection.device).to.be.equal(device); + expect(connection.domainEvents).to.be.empty; + }); + + it('should do nothing when setting nothing over nothing', function () { + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device: undefined }); + + connection.setDevice(undefined); + + expect(connection.device).to.be.equal(undefined); + expect(connection.domainEvents).to.be.empty; + }); + + it('should throw an error when setting not a Device', function () { + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device: undefined }); + + // @ts-expect-error - invalid property + expect(() => connection.setDevice('foo')).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'device' of DummyConnection is not an instance of Device`, + ); + }); + }); +}); + +class DummyConnection extends Connection { + connectionType = 'Dummy'; +} diff --git a/packages/domain/src/aggregate-roots/connection.aggregate-root.ts b/packages/domain/src/aggregate-roots/connection.aggregate-root.ts new file mode 100644 index 00000000..ffc6c2ea --- /dev/null +++ b/packages/domain/src/aggregate-roots/connection.aggregate-root.ts @@ -0,0 +1,42 @@ +import { AggregateRoot } from '@agnoc/toolkit'; +import { ConnectionDeviceChangedDomainEvent } from '../domain-events/connection-device-changed.domain-event'; +import { Device } from './device.aggregate-root'; +import type { EntityProps } from '@agnoc/toolkit'; + +export interface ConnectionProps extends EntityProps { + device?: Device; +} + +export abstract class Connection extends AggregateRoot { + abstract readonly connectionType: string; + + constructor(props: Props) { + super(props); + } + + get device(): Device | undefined { + return this.props.device; + } + + setDevice(device: Device | undefined): void { + if (this.device === device || this.device?.equals(device)) { + return; + } + + if (device) { + this.validateInstanceProp({ device }, 'device', Device); + } + + const previousDeviceId = this.device?.id; + const currentDeviceId = device?.id; + + this.props.device = device; + this.addEvent(new ConnectionDeviceChangedDomainEvent({ aggregateId: this.id, previousDeviceId, currentDeviceId })); + } + + protected validate(props: Props): void { + if (props.device) { + this.validateInstanceProp(props, 'device', Device); + } + } +} diff --git a/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts b/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts new file mode 100644 index 00000000..b3ecba11 --- /dev/null +++ b/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts @@ -0,0 +1,31 @@ +import { ID, DomainEvent } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeConnectionDeviceChangedDomainEventProps } from '../test-support'; +import { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; + +describe('ConnectionDeviceChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeConnectionDeviceChangedDomainEventProps(); + const event = new ConnectionDeviceChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousDeviceId).to.be.undefined; + expect(event.currentDeviceId).to.be.undefined; + }); + + it('should be created with previousDeviceId', function () { + const props = { ...givenSomeConnectionDeviceChangedDomainEventProps(), previousDeviceId: ID.generate() }; + const event = new ConnectionDeviceChangedDomainEvent(props); + + expect(event.previousDeviceId).to.be.equal(event.previousDeviceId); + expect(event.currentDeviceId).to.be.undefined; + }); + + it('should be created with currentDeviceId', function () { + const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: ID.generate() }); + + expect(event.previousDeviceId).to.be.undefined; + expect(event.previousDeviceId).to.be.equal(event.previousDeviceId); + }); +}); diff --git a/packages/domain/src/domain-events/connection-device-changed.domain-event.ts b/packages/domain/src/domain-events/connection-device-changed.domain-event.ts new file mode 100644 index 00000000..7dbf7ec8 --- /dev/null +++ b/packages/domain/src/domain-events/connection-device-changed.domain-event.ts @@ -0,0 +1,21 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps, ID } from '@agnoc/toolkit'; + +export interface ConnectionDeviceChangedDomainEventProps extends DomainEventProps { + previousDeviceId?: ID; + currentDeviceId?: ID; +} + +export class ConnectionDeviceChangedDomainEvent extends DomainEvent { + get previousDeviceId(): ID | undefined { + return this.props.previousDeviceId; + } + + get currentDeviceId(): ID | undefined { + return this.props.currentDeviceId; + } + + protected validate(): void { + // noop + } +} diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index 41188396..99dde318 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -1,9 +1,11 @@ +import type { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; export type DomainEvents = { DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; DeviceLockedDomainEvent: DeviceLockedDomainEvent; + ConnectionDeviceChangedDomainEvent: ConnectionDeviceChangedDomainEvent; }; export type DomainEventNames = keyof DomainEvents; diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 42624555..5b5d162a 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -1,5 +1,8 @@ +export * from './aggregate-roots/connection.aggregate-root'; +export * from './aggregate-roots/device.aggregate-root'; export * from './commands/commands'; export * from './commands/locate-device.command'; +export * from './domain-events/connection-device-changed.domain-event'; export * from './domain-events/device-connected.domain-event'; export * from './domain-events/device-locked.domain-event'; export * from './domain-events/domain-events'; @@ -14,13 +17,13 @@ export * from './domain-primitives/device-water-level.domain-primitive'; export * from './domain-primitives/week-day.domain-primitive'; export * from './entities/device-map.entity'; export * from './entities/device-order.entity'; -export * from './aggregate-roots/device.aggregate-root'; export * from './entities/room.entity'; export * from './entities/zone.entity'; export * from './event-buses/command.event-bus'; export * from './event-buses/domain.event-bus'; export * from './event-handlers/command.event-handler'; export * from './event-handlers/domain.event-handler'; +export * from './repositories/connection.repository'; export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; export * from './value-objects/device-consumable.value-object'; diff --git a/packages/domain/src/repositories/connection.repository.test.ts b/packages/domain/src/repositories/connection.repository.test.ts new file mode 100644 index 00000000..25d85be1 --- /dev/null +++ b/packages/domain/src/repositories/connection.repository.test.ts @@ -0,0 +1,52 @@ +import { ID, Repository } from '@agnoc/toolkit'; +import { imock, instance, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { Connection } from '../aggregate-roots/connection.aggregate-root'; +import { Device } from '../aggregate-roots/device.aggregate-root'; +import { givenSomeConnectionProps, givenSomeDeviceProps } from '../test-support'; +import { ConnectionRepository } from './connection.repository'; +import type { ConnectionProps } from '../aggregate-roots/connection.aggregate-root'; +import type { Adapter, EventBus } from '@agnoc/toolkit'; + +describe('ConnectionRepository', function () { + let eventBus: EventBus; + let adapter: Adapter; + let repository: ConnectionRepository; + + beforeEach(function () { + eventBus = imock(); + adapter = imock(); + repository = new ConnectionRepository(instance(eventBus), instance(adapter)); + }); + + it('should be a repository', function () { + expect(repository).to.be.an.instanceOf(Repository); + }); + + describe('#findByDeviceId', function () { + it('should find a connection by device id', async function () { + const connections = [ + givenAConnectionWithDeviceId(new ID(1)), + givenAConnectionWithDeviceId(new ID(2)), + givenAConnectionWithDeviceId(new ID(3)), + ]; + + when(adapter.getAll()).thenReturn(connections); + + const ret = await repository.findByDeviceId(new ID(1)); + + expect(ret).to.contain(connections[0]); + }); + }); +}); + +class DummyConnection extends Connection { + connectionType = 'Dummy'; +} + +function givenAConnectionWithDeviceId(deviceId: ID) { + return new DummyConnection({ + ...givenSomeConnectionProps(), + device: new Device({ ...givenSomeDeviceProps(), id: deviceId }), + }); +} diff --git a/packages/domain/src/repositories/connection.repository.ts b/packages/domain/src/repositories/connection.repository.ts new file mode 100644 index 00000000..0205cbcb --- /dev/null +++ b/packages/domain/src/repositories/connection.repository.ts @@ -0,0 +1,15 @@ +import { Repository } from '@agnoc/toolkit'; +import type { Connection } from '../aggregate-roots/connection.aggregate-root'; +import type { ID } from '@agnoc/toolkit'; + +export interface ConnectionRepositoryPorts { + findByDeviceId(deviceId: ID): Promise; +} + +export class ConnectionRepository extends Repository implements ConnectionRepositoryPorts { + async findByDeviceId(deviceId: ID): Promise { + const connections = this.adapter.getAll() as Connection[]; + + return connections.filter((connection) => connection.device?.id.equals(deviceId)); + } +} diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index 5f4f5631..47f0ba5f 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -15,7 +15,9 @@ import { MapCoordinate } from './value-objects/map-coordinate.value-object'; import { MapPixel } from './value-objects/map-pixel.value-object'; import { QuietHoursSetting } from './value-objects/quiet-hours-setting.value-object'; import { VoiceSetting } from './value-objects/voice-setting.value-object'; +import type { ConnectionProps } from './aggregate-roots/connection.aggregate-root'; import type { DeviceProps } from './aggregate-roots/device.aggregate-root'; +import type { ConnectionDeviceChangedDomainEventProps } from './domain-events/connection-device-changed.domain-event'; import type { DeviceMapProps } from './entities/device-map.entity'; import type { DeviceOrderProps } from './entities/device-order.entity'; import type { RoomProps } from './entities/room.entity'; @@ -189,3 +191,15 @@ export function givenSomeDeviceProps(): DeviceProps { version: new DeviceVersion(givenSomeDeviceVersionProps()), }; } + +export function givenSomeConnectionProps(): ConnectionProps { + return { + id: ID.generate(), + }; +} + +export function givenSomeConnectionDeviceChangedDomainEventProps(): ConnectionDeviceChangedDomainEventProps { + return { + aggregateId: ID.generate(), + }; +} diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index db402f26..057174f4 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -63,6 +63,7 @@ module.exports = { devDependencies: ['**/*.test.ts', '**/test/**/*.ts'], optionalDependencies: false, peerDependencies: true, + includeInternal: true, }, ], 'import/no-unresolved': 'off', diff --git a/packages/toolkit/src/base-classes/repository.base.ts b/packages/toolkit/src/base-classes/repository.base.ts index 9e42db80..d13b534f 100644 --- a/packages/toolkit/src/base-classes/repository.base.ts +++ b/packages/toolkit/src/base-classes/repository.base.ts @@ -6,7 +6,7 @@ import type { EventBus } from './event-bus.base'; import type { ID } from '../domain-primitives/id.domain-primitive'; export abstract class Repository> { - constructor(private readonly eventBus: EventBus, private readonly adapter: Adapter) {} + constructor(private readonly eventBus: EventBus, protected readonly adapter: Adapter) {} async findOneById(id: ID): Promise { return this.adapter.get(id) as T | undefined; From ab8e28f8235d621c6b0c210aa6e18332bedd0e3b Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 08:04:40 +0100 Subject: [PATCH 13/38] feat(core): add `host` option to `listen` method --- packages/adapter-tcp/src/tcp.server.test.ts | 2 +- packages/adapter-tcp/src/tcp.server.ts | 14 +++++++++----- packages/core/src/agnoc.server.test.ts | 6 ++++-- packages/core/src/agnoc.server.ts | 8 ++++++-- packages/core/src/index.ts | 1 + packages/toolkit/src/base-classes/server.base.ts | 2 +- 6 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/index.ts diff --git a/packages/adapter-tcp/src/tcp.server.test.ts b/packages/adapter-tcp/src/tcp.server.test.ts index 49b782fa..3bde6fb9 100644 --- a/packages/adapter-tcp/src/tcp.server.test.ts +++ b/packages/adapter-tcp/src/tcp.server.test.ts @@ -29,7 +29,7 @@ describe('TCPServer', function () { }); it('should listen and close servers with custom ports', async function () { - await tcpAdapter.listen({ ports: { cmd: 0, map: 0, ntp: 0 } }); + await tcpAdapter.listen({ host: '127.0.0.1', ports: { cmd: 0, map: 0, ntp: 0 } }); await tcpAdapter.close(); }); }); diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 0fe43ff8..c3010806 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -146,10 +146,13 @@ export class TCPServer implements Server { } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { + const host = options.host; + const ports = options.ports ?? listenDefaultOptions.ports; + await Promise.all([ - this.cmdServer.listen(options.ports.cmd), - this.mapServer.listen(options.ports.map), - this.ntpServer.listen(options.ports.ntp), + this.cmdServer.listen({ host, port: ports.cmd }), + this.mapServer.listen({ host, port: ports.map }), + this.ntpServer.listen({ host, port: ports.ntp }), ]); return { @@ -166,10 +169,11 @@ export class TCPServer implements Server { } } -const listenDefaultOptions: TCPAdapterListenOptions = { ports: { cmd: 4010, map: 4030, ntp: 4050 } }; +const listenDefaultOptions = { ports: { cmd: 4010, map: 4030, ntp: 4050 } } satisfies TCPAdapterListenOptions; export interface TCPAdapterListenOptions { - ports: ServerPorts; + host?: string; + ports?: ServerPorts; } interface TCPAdapterListenReturn { diff --git a/packages/core/src/agnoc.server.test.ts b/packages/core/src/agnoc.server.test.ts index f874f660..a1946b7d 100644 --- a/packages/core/src/agnoc.server.test.ts +++ b/packages/core/src/agnoc.server.test.ts @@ -28,13 +28,15 @@ describe('AgnocServer', function () { }); it('should listen adapters', async function () { + const options = { host: '127.0.0.1' }; + agnocServer.buildAdapter(() => { return instance(server); }); - await agnocServer.listen(); + await agnocServer.listen(options); - verify(server.listen()).once(); + verify(server.listen(options)).once(); }); it('should close adapters', async function () { diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts index 359d3f86..16759f93 100644 --- a/packages/core/src/agnoc.server.ts +++ b/packages/core/src/agnoc.server.ts @@ -40,8 +40,8 @@ export class AgnocServer implements Server { this.adapters.add(adapter); } - async listen(): Promise { - await Promise.all([...this.adapters].map((adapter) => adapter.listen())); + async listen(options?: AgnocServerListenOptions): Promise { + await Promise.all([...this.adapters].map((adapter) => adapter.listen(options))); } async close(): Promise { @@ -49,6 +49,10 @@ export class AgnocServer implements Server { } } +export interface AgnocServerListenOptions { + host?: string; +} + type AdapterFactory = (container: Container) => Server; export type Container = { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..cb67867d --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1 @@ +export * from './agnoc.server'; diff --git a/packages/toolkit/src/base-classes/server.base.ts b/packages/toolkit/src/base-classes/server.base.ts index 275869ab..de40bfa6 100644 --- a/packages/toolkit/src/base-classes/server.base.ts +++ b/packages/toolkit/src/base-classes/server.base.ts @@ -1,4 +1,4 @@ export abstract class Server { - abstract listen(): Promise; + abstract listen(options?: unknown): Promise; abstract close(): Promise; } From 4925e5e55a0e813ff708646b20fdb0c57fdde0bb Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 08:07:49 +0100 Subject: [PATCH 14/38] refactor(adapter-tcp): relocate handlers --- .../locate-device.event-handler.ts | 2 +- ...s-connected-event-handler.event-handler.ts | 2 +- ...e-is-locked-event-handler.event-handler.ts | 2 +- ...-connection-device-changed.domain-event.ts | 0 .../client-heartbeat.event-handler.ts | 4 +- .../client-login.event-handler.ts | 4 +- .../device-battery-update.event-handler.ts | 6 +-- ...ice-clean-map-data-report.event-handler.ts | 4 +- .../device-clean-map-report.event-handler.ts | 4 +- .../device-clean-task-report.event-handler.ts | 4 +- ...device-get-all-global-map.event-handler.ts | 4 +- .../device-located.event-handler.ts | 4 +- .../device-locked.event-handler.ts | 4 +- ...p-charger-position-update.event-handler.ts | 4 +- .../device-map-update.event-handler.ts | 14 +++--- ...ce-map-work-status-update.event-handler.ts | 16 +++---- .../device-memory-map-info.event-handler.ts | 4 +- .../device-offline.event-handler.ts | 4 +- .../device-register.event-handler.ts | 4 +- .../device-settings-update.event-handler.ts | 6 +-- .../device-time-update.event-handler.ts | 4 +- .../device-upgrade-info.event-handler.ts | 4 +- .../device-version-update.event-handler.ts | 4 +- .../device-wlan-update.event-handler.ts | 4 +- packages/adapter-tcp/src/tcp.server.ts | 48 +++++++++---------- 25 files changed, 80 insertions(+), 80 deletions(-) rename packages/adapter-tcp/src/{event-handlers/command-event-handlers => command-handlers}/locate-device.event-handler.ts (92%) rename packages/adapter-tcp/src/{event-handlers => }/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts (91%) rename packages/adapter-tcp/src/{event-handlers => }/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts (94%) rename packages/adapter-tcp/src/{event-handlers => }/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts (100%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/client-heartbeat.event-handler.ts (66%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/client-login.event-handler.ts (79%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-battery-update.event-handler.ts (77%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-clean-map-data-report.event-handler.ts (81%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-clean-map-report.event-handler.ts (81%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-clean-task-report.event-handler.ts (78%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-get-all-global-map.event-handler.ts (76%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-located.event-handler.ts (75%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-locked.event-handler.ts (82%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-map-charger-position-update.event-handler.ts (84%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-map-update.event-handler.ts (91%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-map-work-status-update.event-handler.ts (76%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-memory-map-info.event-handler.ts (76%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-offline.event-handler.ts (72%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-register.event-handler.ts (88%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-settings-update.event-handler.ts (88%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-time-update.event-handler.ts (79%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-upgrade-info.event-handler.ts (79%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-version-update.event-handler.ts (84%) rename packages/adapter-tcp/src/{event-handlers => }/packet-event-handlers/device-wlan-update.event-handler.ts (84%) diff --git a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts similarity index 92% rename from packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts rename to packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts index 4f2b615f..afbe06de 100644 --- a/packages/adapter-tcp/src/event-handlers/command-event-handlers/locate-device.event-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts @@ -1,5 +1,5 @@ import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { CommandHandler, Connection, ConnectionRepository, LocateDeviceCommand } from '@agnoc/domain'; export class LocateDeviceEventHandler implements CommandHandler { diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts similarity index 91% rename from packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts rename to packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index 76c411c9..073bd642 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -1,5 +1,5 @@ import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { DomainEventHandler, DeviceConnectedDomainEvent, ConnectionRepository, Connection } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts similarity index 94% rename from packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts rename to packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index af2b38e5..aba3580d 100644 --- a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -1,6 +1,6 @@ import { DeviceCapability } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../../aggregate-roots/packet-connection.aggregate-root'; +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { DeviceLockedDomainEvent, DomainEventHandler, Connection, ConnectionRepository } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { diff --git a/packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts similarity index 100% rename from packages/adapter-tcp/src/event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts rename to packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts similarity index 66% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts index 4c4ce954..882649d3 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-heartbeat.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class ClientHeartbeatEventHandler implements PacketEventHandler { readonly forName = 'CLIENT_HEARTBEAT_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts similarity index 79% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts index 4f212c42..f5a89dd4 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/client-login.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class ClientLoginEventHandler implements PacketEventHandler { readonly forName = 'CLIENT_ONLINE_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts similarity index 77% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts index 0f599707..848282a9 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts @@ -1,7 +1,7 @@ import { DomainException } from '@agnoc/toolkit'; -import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_BATTERY_INFO_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts similarity index 81% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts index 368cd9aa..b5ff40bc 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts similarity index 81% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts index 2502c96b..cf54020a 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceCleanMapReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANMAP'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts similarity index 78% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts index 0cfdc7a5..4d210675 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-clean-task-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANTASK'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts similarity index 76% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts index af4133c8..9ff0006f 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-get-all-global-map.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceGetAllGlobalMapEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts similarity index 75% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts index dd5f8a83..833b40a5 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-located.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceLocatedEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_SEEK_LOCATION_RSP'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts similarity index 82% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts index 4e0ff1c9..059dc6e9 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-locked.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceLockedEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts similarity index 84% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts index 87afef44..d42d02df 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -1,7 +1,7 @@ import { MapPosition } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts similarity index 91% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index 378826a4..c165d7e1 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -10,13 +10,13 @@ import { Zone, } from '@agnoc/domain'; import { DomainException, ID, isPresent } from '@agnoc/toolkit'; -import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; -import type { DeviceErrorMapper } from '../../mappers/device-error.mapper'; -import type { DeviceFanSpeedMapper } from '../../mappers/device-fan-speed.mapper'; -import type { DeviceModeMapper } from '../../mappers/device-mode.mapper'; -import type { DeviceStateMapper } from '../../mappers/device-state.mapper'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../mappers/device-state.mapper'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceMapUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts similarity index 76% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index 61878bb0..ba256f7f 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -1,13 +1,13 @@ import { CleanSize, DeviceCleanWork, DeviceTime } from '@agnoc/domain'; import { DomainException, isPresent } from '@agnoc/toolkit'; -import type { DeviceBatteryMapper } from '../../mappers/device-battery.mapper'; -import type { DeviceErrorMapper } from '../../mappers/device-error.mapper'; -import type { DeviceFanSpeedMapper } from '../../mappers/device-fan-speed.mapper'; -import type { DeviceModeMapper } from '../../mappers/device-mode.mapper'; -import type { DeviceStateMapper } from '../../mappers/device-state.mapper'; -import type { DeviceWaterLevelMapper } from '../../mappers/device-water-level.mapper'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../mappers/device-state.mapper'; +import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts similarity index 76% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts index fe70f1fb..6fdad56d 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-memory-map-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceMemoryMapInfoEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts similarity index 72% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts index a306f1eb..c21de12c 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-offline.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceOfflineEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_OFFLINE_CMD'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts similarity index 88% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts index 3cce8c51..c7d25287 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts @@ -1,7 +1,7 @@ import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; import { ID } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceRegisterEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts similarity index 88% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index 047b03c7..2dd0e749 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -1,8 +1,8 @@ import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { DeviceVoiceMapper } from '../../mappers/device-voice.mapper'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { DeviceVoiceMapper } from '../mappers/device-voice.mapper'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts similarity index 79% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts index 97b5b070..d35d9def 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-time-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceTimeUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GETTIME_RSP'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts similarity index 79% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts index 7042c17b..907ad545 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-upgrade-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceUpgradeInfoEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts similarity index 84% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts index bf490ccf..370dd53e 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts @@ -1,7 +1,7 @@ import { DeviceVersion } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceVersionUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_VERSION_INFO_UPDATE_REQ'; diff --git a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts similarity index 84% rename from packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts index 74c50099..96b705f0 100644 --- a/packages/adapter-tcp/src/event-handlers/packet-event-handlers/device-wlan-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts @@ -1,7 +1,7 @@ import { DeviceWlan } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../../packet.event-handler'; -import type { PacketMessage } from '../../packet.message'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; export class DeviceWlanUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_WLAN_INFO_GETTING_RSP'; diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index c3010806..d28885c7 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -8,30 +8,10 @@ import { PayloadObjectParserService, PacketFactory, } from '@agnoc/transport-tcp'; -import { LocateDeviceEventHandler } from './event-handlers/command-event-handlers/locate-device.event-handler'; -import { LockDeviceWhenDeviceIsConnectedEventHandler } from './event-handlers/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; -import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './event-handlers/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; -import { SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler } from './event-handlers/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event'; -import { ClientHeartbeatEventHandler } from './event-handlers/packet-event-handlers/client-heartbeat.event-handler'; -import { ClientLoginEventHandler } from './event-handlers/packet-event-handlers/client-login.event-handler'; -import { DeviceBatteryUpdateEventHandler } from './event-handlers/packet-event-handlers/device-battery-update.event-handler'; -import { DeviceCleanMapDataReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-map-data-report.event-handler'; -import { DeviceCleanMapReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-map-report.event-handler'; -import { DeviceCleanTaskReportEventHandler } from './event-handlers/packet-event-handlers/device-clean-task-report.event-handler'; -import { DeviceGetAllGlobalMapEventHandler } from './event-handlers/packet-event-handlers/device-get-all-global-map.event-handler'; -import { DeviceLocatedEventHandler } from './event-handlers/packet-event-handlers/device-located.event-handler'; -import { DeviceLockedEventHandler } from './event-handlers/packet-event-handlers/device-locked.event-handler'; -import { DeviceMapChargerPositionUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-charger-position-update.event-handler'; -import { DeviceMapUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-update.event-handler'; -import { DeviceMapWorkStatusUpdateEventHandler } from './event-handlers/packet-event-handlers/device-map-work-status-update.event-handler'; -import { DeviceMemoryMapInfoEventHandler } from './event-handlers/packet-event-handlers/device-memory-map-info.event-handler'; -import { DeviceOfflineEventHandler } from './event-handlers/packet-event-handlers/device-offline.event-handler'; -import { DeviceRegisterEventHandler } from './event-handlers/packet-event-handlers/device-register.event-handler'; -import { DeviceSettingsUpdateEventHandler } from './event-handlers/packet-event-handlers/device-settings-update.event-handler'; -import { DeviceTimeUpdateEventHandler } from './event-handlers/packet-event-handlers/device-time-update.event-handler'; -import { DeviceUpgradeInfoEventHandler } from './event-handlers/packet-event-handlers/device-upgrade-info.event-handler'; -import { DeviceVersionUpdateEventHandler } from './event-handlers/packet-event-handlers/device-version-update.event-handler'; -import { DeviceWlanUpdateEventHandler } from './event-handlers/packet-event-handlers/device-wlan-update.event-handler'; +import { LocateDeviceEventHandler } from './command-handlers/locate-device.event-handler'; +import { LockDeviceWhenDeviceIsConnectedEventHandler } from './domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; +import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; +import { SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler } from './domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event'; import { PacketConnectionFactory } from './factories/connection.factory'; import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; import { DeviceErrorMapper } from './mappers/device-error.mapper'; @@ -41,6 +21,26 @@ import { DeviceStateMapper } from './mappers/device-state.mapper'; import { DeviceVoiceMapper } from './mappers/device-voice.mapper'; import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; +import { ClientHeartbeatEventHandler } from './packet-event-handlers/client-heartbeat.event-handler'; +import { ClientLoginEventHandler } from './packet-event-handlers/client-login.event-handler'; +import { DeviceBatteryUpdateEventHandler } from './packet-event-handlers/device-battery-update.event-handler'; +import { DeviceCleanMapDataReportEventHandler } from './packet-event-handlers/device-clean-map-data-report.event-handler'; +import { DeviceCleanMapReportEventHandler } from './packet-event-handlers/device-clean-map-report.event-handler'; +import { DeviceCleanTaskReportEventHandler } from './packet-event-handlers/device-clean-task-report.event-handler'; +import { DeviceGetAllGlobalMapEventHandler } from './packet-event-handlers/device-get-all-global-map.event-handler'; +import { DeviceLocatedEventHandler } from './packet-event-handlers/device-located.event-handler'; +import { DeviceLockedEventHandler } from './packet-event-handlers/device-locked.event-handler'; +import { DeviceMapChargerPositionUpdateEventHandler } from './packet-event-handlers/device-map-charger-position-update.event-handler'; +import { DeviceMapUpdateEventHandler } from './packet-event-handlers/device-map-update.event-handler'; +import { DeviceMapWorkStatusUpdateEventHandler } from './packet-event-handlers/device-map-work-status-update.event-handler'; +import { DeviceMemoryMapInfoEventHandler } from './packet-event-handlers/device-memory-map-info.event-handler'; +import { DeviceOfflineEventHandler } from './packet-event-handlers/device-offline.event-handler'; +import { DeviceRegisterEventHandler } from './packet-event-handlers/device-register.event-handler'; +import { DeviceSettingsUpdateEventHandler } from './packet-event-handlers/device-settings-update.event-handler'; +import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-time-update.event-handler'; +import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-upgrade-info.event-handler'; +import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; +import { DeviceWlanUpdateEventHandler } from './packet-event-handlers/device-wlan-update.event-handler'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; import type { Commands, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; From 23ff3243aeeb23abc6709ac8bdf51926c5a66804 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 08:35:56 +0100 Subject: [PATCH 15/38] refactor: remove legacy code --- packages/adapter-tcp/package.json | 2 - .../src/emitters/cloud-server.emitter.ts | 167 --- .../src/emitters/connection.emitter.ts | 140 -- .../src/emitters/multiplexer.emitter.ts | 96 -- .../adapter-tcp/src/emitters/robot.emitter.ts | 1208 ----------------- packages/adapter-tcp/src/index.ts | 5 - .../src/value-objects/message.value-object.ts | 59 - packages/core/package.json | 3 - packages/transport-tcp/package.json | 1 - 9 files changed, 1681 deletions(-) delete mode 100644 packages/adapter-tcp/src/emitters/cloud-server.emitter.ts delete mode 100644 packages/adapter-tcp/src/emitters/connection.emitter.ts delete mode 100644 packages/adapter-tcp/src/emitters/multiplexer.emitter.ts delete mode 100644 packages/adapter-tcp/src/emitters/robot.emitter.ts delete mode 100644 packages/adapter-tcp/src/value-objects/message.value-object.ts diff --git a/packages/adapter-tcp/package.json b/packages/adapter-tcp/package.json index 399943b7..bf1e7b57 100644 --- a/packages/adapter-tcp/package.json +++ b/packages/adapter-tcp/package.json @@ -74,8 +74,6 @@ "@agnoc/schemas-tcp": "^0.16.0", "@agnoc/transport-tcp": "^0.18.0-next.0", "@agnoc/toolkit": "^0.18.0-next.0", - "debug": "^4.3.4", - "tiny-typed-emitter": "^2.1.0", "emittery": "^0.13.1", "tslib": "^2.5.0" }, diff --git a/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts b/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts deleted file mode 100644 index 8120fe74..00000000 --- a/packages/adapter-tcp/src/emitters/cloud-server.emitter.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; -import { ID, bind } from '@agnoc/toolkit'; -import { PacketServer } from '@agnoc/transport-tcp'; -import { TypedEmitter } from 'tiny-typed-emitter'; -import { Message } from '../value-objects/message.value-object'; -import { Connection } from './connection.emitter'; -import { Multiplexer } from './multiplexer.emitter'; -import { Robot } from './robot.emitter'; -import type { MessageHandlers } from '../value-objects/message.value-object'; -import type { PacketMapper, PayloadObjectName, PacketSocket } from '@agnoc/transport-tcp'; - -interface Servers { - cmd: PacketServer; - map: PacketServer; - rtc: PacketServer; -} - -export interface CloudServerEvents { - addRobot: (robot: Robot) => void; - error: (err: Error) => void; -} - -export class CloudServer extends TypedEmitter { - private robots = new Map() as Map; - private servers: Servers; - private handlers: MessageHandlers = { - CLIENT_ONLINE_REQ: this.handleClientLogin, - DEVICE_REGISTER_REQ: this.handleClientRegister, - } as const; - - constructor(private readonly packetMapper: PacketMapper) { - super(); - this.servers = { - cmd: new PacketServer(this.packetMapper), - map: new PacketServer(this.packetMapper), - rtc: new PacketServer(this.packetMapper), - }; - this.addListeners(); - } - - getRobots(): Robot[] { - return [...this.robots.values()]; - } - - async listen(host?: string): Promise { - await Promise.all([ - this.servers.cmd.listen({ host, port: 4010 }), - this.servers.map.listen({ host, port: 4030 }), - this.servers.rtc.listen({ host, port: 4050 }), - ]); - } - - async close(): Promise { - this.getRobots().map((robot) => { - void robot.disconnect(); - }); - - await Promise.all([this.servers.cmd.close(), this.servers.map.close(), this.servers.rtc.close()]); - } - - @bind - private handleClientLogin(message: Message<'CLIENT_ONLINE_REQ'>): void { - const robot = this.robots.get(message.packet.deviceId.value); - - if (!robot) { - const object = message.packet.payload.object; - - void message.respond('CLIENT_ONLINE_RSP', { - result: 12002, - reason: `Device not registered(devsn: ${object.deviceSerialNumber})`, - }); - } else { - void message.respond('CLIENT_ONLINE_RSP', { - result: 0, - }); - } - } - - @bind - private handleClientRegister(message: Message<'DEVICE_REGISTER_REQ'>): void { - const props = message.packet.payload.object; - const multiplexer = new Multiplexer(); - const device = new Device({ - id: ID.generate(), - userId: ID.generate(), - system: new DeviceSystem({ type: props.deviceType, serialNumber: props.deviceSerialNumber }), - version: new DeviceVersion({ - software: props.softwareVersion, - hardware: props.hardwareVersion, - }), - }); - const robot = new Robot({ device, multiplexer }); - - multiplexer.on('error', (err) => this.emit('error', err)); - - robot.addConnection(message.connection); - - this.robots.set(device.id.value, robot); - - this.emit('addRobot', robot); - - void message.respond('DEVICE_REGISTER_RSP', { - result: 0, - device: { - id: device.id.value, - }, - }); - } - - @bind - handleMessage(message: Message): void { - const handler = this.handlers[message.opname]; - - if (handler) { - return handler(message); - } - - const robot = this.robots.get(message.packet.deviceId.value); - - if (robot) { - robot.addConnection(message.connection); - robot.handleMessage(message); - return; - } - - void message.respond('COMMON_ERROR_REPLY', { - result: 1, - opcode: message.packet.payload.opcode.code, - error: 'Device not registered', - }); - } - - @bind - private handleConnection(socket: PacketSocket): void { - const connection = new Connection(socket); - - connection.on('data', (packet) => { - const message = new Message({ connection, packet }); - - this.handleMessage(message); - }); - } - - @bind - private handleRTPConnection(socket: PacketSocket) { - const connection = new Connection(socket); - - void connection.send({ - opname: 'DEVICE_TIME_SYNC_RSP', - object: { - result: 0, - body: { - time: Math.floor(Date.now() / 1000), - }, - }, - }); - - void connection.close(); - } - - private addListeners(): void { - this.servers.cmd.on('connection', this.handleConnection); - this.servers.map.on('connection', this.handleConnection); - this.servers.rtc.on('connection', this.handleRTPConnection); - } -} diff --git a/packages/adapter-tcp/src/emitters/connection.emitter.ts b/packages/adapter-tcp/src/emitters/connection.emitter.ts deleted file mode 100644 index 7acc83cd..00000000 --- a/packages/adapter-tcp/src/emitters/connection.emitter.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { - debug, - bind, - DomainException, - isPresent, - ArgumentNotProvidedException, - ArgumentInvalidException, - ID, -} from '@agnoc/toolkit'; -import { Packet, PacketSocket, PacketSequence, OPCode, Payload } from '@agnoc/transport-tcp'; -import { TypedEmitter } from 'tiny-typed-emitter'; -import type { Device } from '@agnoc/domain'; -import type { PayloadObjectName, PayloadObjectFrom } from '@agnoc/transport-tcp'; -import type { Debugger } from 'debug'; - -export interface ConnectionSendProps { - opname: Name; - device?: Device; - object: PayloadObjectFrom; -} - -export interface ConnectionRespondProps { - packet: Packet; - opname: Name; - object: PayloadObjectFrom; -} - -export type ConnectionEvents = { - [key in Name]: (packet: Packet) => void; -} & { - data: (packet: Packet) => void; - close: () => void; - error: (err: Error) => void; -}; - -export class Connection extends TypedEmitter> { - private socket: PacketSocket; - private debug: Debugger; - - constructor(socket: PacketSocket) { - super(); - this.validate(socket); - this.socket = socket; - this.addListeners(); - this.debug = debug(__filename).extend(this.toString()); - this.debug(`new connection`); - } - - private addListeners(): void { - this.socket.on('data', this.handlePacket); - this.socket.on('error', this.handleError); - this.socket.on('close', this.handleClose); - } - - send({ opname, device, object }: ConnectionSendProps): Promise { - const packet = new Packet({ - ctype: 2, - flow: 0, - // This swap is intended. - userId: device?.id ?? new ID(0), - deviceId: device?.userId ?? new ID(0), - sequence: PacketSequence.generate(), - payload: new Payload({ opcode: OPCode.fromName(opname), object }), - }); - - this.debug(`sending packet ${packet.toString()}`); - - return this.socket.write(packet); - } - - respond({ packet, opname, object }: ConnectionRespondProps): Promise { - const response = new Packet({ - ctype: packet.ctype, - flow: packet.flow + 1, - // This swap is intended. - userId: packet.deviceId, - deviceId: packet.userId, - sequence: packet.sequence, - payload: new Payload({ opcode: OPCode.fromName(opname), object }), - }); - - this.debug(`responding to packet with ${response.toString()}`); - - return this.socket.write(response); - } - - close(): Promise { - this.debug('closing socket...'); - - return this.socket.end(); - } - - @bind - private handlePacket(packet: Packet): void { - this.validatePacket(packet); - - const opname = packet.payload.opcode.name as PayloadObjectName; - - if (!opname) { - throw new DomainException(`Unable to handle unknown packet ${packet.payload.opcode.toString()}`); - } - - this.debug(`received packet ${packet.toString()}`); - this.emit(opname, packet); - this.emit('data', packet); - } - - @bind - private handleError(err: Error): void { - this.emit('error', err); - } - - @bind - private handleClose(): void { - this.emit('close'); - } - - override toString(): string { - return `${this.socket.remoteAddress || 'unknown'}:${this.socket.remotePort || 0}::${ - this.socket.localAddress || 'unknown' - }:${this.socket.localPort || 0}`; - } - - protected validatePacket(packet: Packet): void { - if (!(packet instanceof Packet)) { - throw new DomainException('Connection socket emitted non-packet data'); - } - } - - protected validate(socket: PacketSocket): void { - if (!isPresent(socket)) { - throw new ArgumentNotProvidedException('Missing property in connection constructor'); - } - - if (!(socket instanceof PacketSocket)) { - throw new ArgumentInvalidException('Invalid socket in connection constructor'); - } - } -} diff --git a/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts b/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts deleted file mode 100644 index c446e901..00000000 --- a/packages/adapter-tcp/src/emitters/multiplexer.emitter.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { DomainException, debug, bind } from '@agnoc/toolkit'; -import { TypedEmitter } from 'tiny-typed-emitter'; -import type { Connection } from './connection.emitter'; -import type { Device } from '@agnoc/domain'; -import type { PayloadObjectName, Packet, PayloadObjectFrom } from '@agnoc/transport-tcp'; - -export type MultiplexerEvents = { - [key in Name]: (packet: Packet) => void; -} & { - data: (packet: Packet) => void; - error: (err: Error) => void; -}; - -export interface MultiplexerSendProps { - opname: Name; - device: Device; - object: PayloadObjectFrom; -} - -export class Multiplexer extends TypedEmitter> { - private connections: Connection[] = []; - private debug = debug(__filename); - - get hasConnections(): boolean { - return this.connections.length > 0; - } - - get connectionCount(): number { - return this.connections.length; - } - - addConnection(connection: Connection): boolean { - if (!this.connections.includes(connection)) { - connection.on('data', this.handlePacket); - connection.on('error', (err) => this.handleError(connection, err)); - connection.on('close', () => this.handleClose(connection)); - - this.connections = [...this.connections, connection]; - this.debug('added connection'); - - return true; - } - - return false; - } - - send(props: MultiplexerSendProps): Promise { - const connection = this.connections[0]; - - if (!connection) { - const error = new DomainException(`No valid connection found to send packet ${props.opname}`); - - this.emit('error', error); - - throw error; - } - - return connection.send(props); - } - - async close(): Promise { - this.debug('closing connections...'); - - await Promise.all([this.connections.map((connection) => connection.close())]); - } - - @bind - private handlePacket(packet: Packet): void { - const opname = packet.payload.opcode.name as PayloadObjectName; - - if (!opname) { - throw new DomainException(`Unable to handle unknown packet ${packet.payload.opcode.toString()}`); - } - - this.emit(opname, packet); - this.emit('data', packet); - } - - @bind - private handleError(connection: Connection, err: Error): void { - this.emit('error', err); - this.handleClose(connection); - } - - @bind - private handleClose(connection: Connection): void { - // TODO: fix this. - // Remove all listeners to prevent mem leaks. - // But ensure error handling works... - // connection.removeAllListeners(); - - this.connections = this.connections.filter((conn) => conn !== connection); - this.debug('removed connection'); - } -} diff --git a/packages/adapter-tcp/src/emitters/robot.emitter.ts b/packages/adapter-tcp/src/emitters/robot.emitter.ts deleted file mode 100644 index 4112af87..00000000 --- a/packages/adapter-tcp/src/emitters/robot.emitter.ts +++ /dev/null @@ -1,1208 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { - DeviceMode, - DeviceCapability, - DeviceConsumable, - DeviceWlan, - MapPosition, - MapCoordinate, - QuietHoursSetting, - DeviceTime, - Room, - DeviceVersion, - DeviceSettings, - DeviceCleanWork, - MapPixel, - DeviceMap, - Zone, - DeviceModeValue, - DeviceConsumableType, - DeviceStateValue, - CleanSize, - DeviceSetting, -} from '@agnoc/domain'; -import { - ArgumentInvalidException, - waitFor, - DomainException, - ID, - writeByte, - writeFloat, - isPresent, - debug, - bind, - BufferWriter, -} from '@agnoc/toolkit'; -import { TypedEmitter } from 'tiny-typed-emitter'; -import { CleanModeMapper } from '../mappers/clean-mode.mapper'; -import { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; -import { DeviceErrorMapper } from '../mappers/device-error.mapper'; -import { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; -import { DeviceModeMapper } from '../mappers/device-mode.mapper'; -import { DeviceOrderMapper } from '../mappers/device-order.mapper'; -import { DeviceStateMapper } from '../mappers/device-state.mapper'; -import { DeviceVoiceMapper } from '../mappers/device-voice.mapper'; -import { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; -import { WeekDayListMapper } from '../mappers/week-day-list.mapper'; -import type { Connection } from './connection.emitter'; -import type { Multiplexer } from './multiplexer.emitter'; -import type { Message, MessageHandlers } from '../value-objects/message.value-object'; -import type { - Device, - DeviceFanSpeed, - DeviceWaterLevel, - DeviceOrder, - VoiceSetting, - DeviceSettingsProps, -} from '@agnoc/domain'; -import type { PayloadObjectName, PayloadObjectFrom, Packet } from '@agnoc/transport-tcp'; -import type { Debugger } from 'debug'; - -export interface RobotProps { - device: Device; - multiplexer: Multiplexer; -} - -export interface DeviceTimestamp { - timestamp: number; - offset: number; -} - -export interface RobotEvents { - updateDevice: () => void; - updateMap: () => void; - updateRobotPosition: () => void; - updateChargerPosition: () => void; -} - -export enum MANUAL_MODE { - 'forward' = 1, - 'left' = 2, - 'right' = 3, - 'backward' = 4, - 'stop' = 5, - 'init' = 10, -} - -export type ManualMode = (typeof MANUAL_MODE)[keyof typeof MANUAL_MODE]; - -const MODE_CHANGE_TIMEOUT = 5000; -const RECV_TIMEOUT = 5000; - -const CONSUMABLE_TYPE_RESET = { - [DeviceConsumableType.MainBrush]: 1, - [DeviceConsumableType.SideBrush]: 2, - [DeviceConsumableType.Filter]: 3, - [DeviceConsumableType.Dishcloth]: 4, -}; - -const CTRL_VALUE = { - STOP: 0, - START: 1, - PAUSE: 2, -}; - -// TODO: move to constructor -const deviceFanSpeedMapper = new DeviceFanSpeedMapper(); -const deviceWaterLevelMapper = new DeviceWaterLevelMapper(); -const cleanModeMapper = new CleanModeMapper(); -const weekDayListMapper = new WeekDayListMapper(); -const deviceOrderMapper = new DeviceOrderMapper( - deviceFanSpeedMapper, - deviceWaterLevelMapper, - cleanModeMapper, - weekDayListMapper, -); -const deviceVoiceMapper = new DeviceVoiceMapper(); -const deviceStateMapper = new DeviceStateMapper(); -const deviceModeMapper = new DeviceModeMapper(); -const deviceErrorMapper = new DeviceErrorMapper(); -const deviceBatteryMapper = new DeviceBatteryMapper(); - -export class Robot extends TypedEmitter { - public readonly device: Device; - private readonly multiplexer: Multiplexer; - private debug: Debugger; - private handlers: MessageHandlers = { - CLIENT_HEARTBEAT_REQ: this.handleClientHeartbeat, - DEVICE_MAPID_GET_GLOBAL_INFO_RSP: this.handleMapUpdate, - DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO: this.handleUpdateChargePosition, - DEVICE_MAPID_PUSH_MAP_INFO: this.handleMapUpdate, - DEVICE_MAPID_PUSH_POSITION_INFO: this.handleUpdateRobotPosition, - DEVICE_MAPID_WORK_STATUS_PUSH_REQ: this.handleDeviceStatus, - DEVICE_VERSION_INFO_UPDATE_REQ: this.handleDeviceVersionInfoUpdate, - PUSH_DEVICE_AGENT_SETTING_REQ: this.handleDeviceAgentSetting, - PUSH_DEVICE_BATTERY_INFO_REQ: this.handleDeviceBatteryInfo, - PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ: this.handleDevicePackageUpgrade, - DEVICE_MAPID_PUSH_HAS_WAITING_BE_SAVED: this.handleWaitingMap, - DEVICE_WORKSTATUS_REPORT_REQ: this.handleWorkstatusReport, - DEVICE_EVENT_REPORT_CLEANTASK: this.handleReportCleantask, - DEVICE_EVENT_REPORT_CLEANMAP: this.handleReportCleanmap, - DEVICE_CLEANMAP_BINDATA_REPORT_REQ: this.handleBinDataReport, - DEVICE_EVENT_REPORT_REQ: this.handleEventReport, - DEVICE_SETTIME_REQ: this.handleSetTime, - }; - - constructor({ device, multiplexer }: RobotProps) { - super(); - this.device = device; - this.multiplexer = multiplexer; - this.debug = debug(__filename).extend(this.device.id.toString()); - this.debug('new robot'); - } - - get isConnected(): boolean { - return this.multiplexer.hasConnections; - } - - async start(): Promise { - if (this.device.hasMopAttached && this.device.mode?.value !== DeviceModeValue.Mop) { - await this.setMode(new DeviceMode(DeviceModeValue.Mop)); - } else if (!this.device.hasMopAttached && this.device.mode?.value === DeviceModeValue.Mop) { - await this.setMode(new DeviceMode(DeviceModeValue.None)); - } - - if (this.device.mode?.value === DeviceModeValue.Zone) { - await this.sendRecv('DEVICE_AREA_CLEAN_REQ', 'DEVICE_AREA_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.START, - }); - } else if (this.device.mode?.value === DeviceModeValue.Mop) { - await this.sendRecv('DEVICE_MOP_FLOOR_CLEAN_REQ', 'DEVICE_MOP_FLOOR_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.START, - }); - } else if (this.device.mode?.value === DeviceModeValue.Spot && this.device.map?.currentSpot) { - await this.sendRecv('DEVICE_MAPID_SET_NAVIGATION_REQ', 'DEVICE_MAPID_SET_NAVIGATION_RSP', { - mapHeadId: this.device.map.id.value, - poseX: this.device.map.currentSpot.x, - poseY: this.device.map.currentSpot.y, - posePhi: this.device.map.currentSpot.phi, - ctrlValue: CTRL_VALUE.START, - }); - } else { - if ( - this.device.system.supports(DeviceCapability.MAP_PLANS) && - this.device.state?.value === DeviceStateValue.Docked && - this.device.map - ) { - const { id, restrictedZones } = this.device.map; - - await this.sendRecv('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', 'DEVICE_MAPID_SET_PLAN_PARAMS_RSP', { - mapHeadId: id.value, - // FIXME: this will change user map name. - mapName: 'Default', - planId: 2, - // FIXME: this will change user plan name. - planName: 'Default', - roomList: this.device.map.rooms.map((room) => ({ - roomId: room.id.value, - roomName: room.name, - enable: true, - })), - areaInfo: { - mapHeadId: id.value, - planId: 2, - cleanAreaLength: restrictedZones.length, - cleanAreaList: restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: zone.coordinates.length, - coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), - })), - }, - }); - } - - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.START, - cleanType: 2, - }); - } - } - - async pause(): Promise { - if (this.device.mode?.value === DeviceModeValue.Zone) { - await this.sendRecv('DEVICE_AREA_CLEAN_REQ', 'DEVICE_AREA_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.PAUSE, - }); - } else if (this.device.mode?.value === DeviceModeValue.Mop) { - await this.sendRecv('DEVICE_MOP_FLOOR_CLEAN_REQ', 'DEVICE_MOP_FLOOR_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.PAUSE, - }); - } else if (this.device.mode?.value === DeviceModeValue.Spot && this.device.map?.currentSpot) { - await this.sendRecv('DEVICE_MAPID_SET_NAVIGATION_REQ', 'DEVICE_MAPID_SET_NAVIGATION_RSP', { - mapHeadId: this.device.map.id.value, - poseX: this.device.map.currentSpot.x, - poseY: this.device.map.currentSpot.y, - posePhi: this.device.map.currentSpot.phi, - ctrlValue: CTRL_VALUE.PAUSE, - }); - } else { - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.PAUSE, - cleanType: 2, - }); - } - } - - async stop(): Promise { - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.STOP, - cleanType: 2, - }); - - if (this.device.system.supports(DeviceCapability.MAP_PLANS) && this.device.map) { - const { id, restrictedZones } = this.device.map; - - await this.sendRecv('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', 'DEVICE_MAPID_SET_PLAN_PARAMS_RSP', { - mapHeadId: id.value, - // FIXME: this will change user map name. - mapName: 'Default', - planId: 2, - // FIXME: this will change user plan name. - planName: 'Default', - roomList: this.device.map.rooms.map((room) => ({ - roomId: room.id.value, - roomName: room.name, - enable: true, - })), - areaInfo: { - mapHeadId: id.value, - planId: 2, - cleanAreaLength: restrictedZones.length, - cleanAreaList: restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: zone.coordinates.length, - coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), - })), - }, - }); - - await this.updateMap(); - } - } - - async home(): Promise { - await this.sendRecv('DEVICE_CHARGE_REQ', 'DEVICE_CHARGE_RSP', { - enable: 1, - }); - } - - async locate(): Promise { - await this.sendRecv('DEVICE_SEEK_LOCATION_REQ', 'DEVICE_SEEK_LOCATION_RSP', {}); - } - - async setMode(mode: DeviceMode): Promise { - if (mode.value === DeviceModeValue.None) { - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.STOP, - cleanType: 2, - }); - } else if (mode.value === DeviceModeValue.Spot) { - let mask = 0x78ff | 0x200; - - if (!this.device.system.supports(DeviceCapability.MAP_PLANS)) { - mask = 0xff | 0x200; - } - - await this.sendRecv('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP', { mask }); - } else if (mode.value === DeviceModeValue.Zone) { - await this.sendRecv('DEVICE_AREA_CLEAN_REQ', 'DEVICE_AREA_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.STOP, - }); - - let mask = 0x78ff | 0x100; - - if (!this.device.system.supports(DeviceCapability.MAP_PLANS)) { - mask = 0xff | 0x100; - } - - await this.sendRecv('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP', { mask }); - } else if (mode.value === DeviceModeValue.Mop) { - await this.sendRecv('DEVICE_MAPID_INTO_MODEIDLE_INFO_REQ', 'DEVICE_MAPID_INTO_MODEIDLE_INFO_RSP', { - mode: 7, - }); - } else { - throw new ArgumentInvalidException('Unknown device mode'); - } - - await waitFor(() => this.device.mode?.value === mode.value, { - timeout: MODE_CHANGE_TIMEOUT, - }).catch(() => { - throw new DomainException(`Unable to change robot mode to ${mode.value}`); - }); - } - - async setFanSpeed(fanSpeed: DeviceFanSpeed): Promise { - await this.sendRecv('DEVICE_SET_CLEAN_PREFERENCE_REQ', 'DEVICE_SET_CLEAN_PREFERENCE_RSP', { - mode: deviceFanSpeedMapper.fromDomain(fanSpeed), - }); - } - - async setWaterLevel(waterLevel: DeviceWaterLevel): Promise { - await this.sendRecv('DEVICE_SET_CLEAN_PREFERENCE_REQ', 'DEVICE_SET_CLEAN_PREFERENCE_RSP', { - mode: deviceWaterLevelMapper.fromDomain(waterLevel), - }); - } - - async getTime(): Promise { - const packet = await this.sendRecv('DEVICE_GETTIME_REQ', 'DEVICE_GETTIME_RSP', {}); - const object = packet.payload.object; - - return { - timestamp: object.body.deviceTime * 1000, - offset: -1 * ((object.body.deviceTimezone || 0) / 60), - }; - } - - async getConsumables(): Promise { - if (!this.device.system.supports(DeviceCapability.CONSUMABLES)) { - return []; - } - - const packet = await this.sendRecv( - 'DEVICE_MAPID_GET_CONSUMABLES_PARAM_REQ', - 'DEVICE_MAPID_GET_CONSUMABLES_PARAM_RSP', - {}, - ); - const object = packet.payload.object; - const consumables = [ - new DeviceConsumable({ - type: DeviceConsumableType.MainBrush, - minutesUsed: object.mainBrushTime, - }), - new DeviceConsumable({ - type: DeviceConsumableType.SideBrush, - minutesUsed: object.sideBrushTime, - }), - new DeviceConsumable({ - type: DeviceConsumableType.Filter, - minutesUsed: object.filterTime, - }), - new DeviceConsumable({ - type: DeviceConsumableType.Dishcloth, - minutesUsed: object.dishclothTime, - }), - ]; - - this.device.updateConsumables(consumables); - - return consumables; - } - - async resetConsumable(consumable: DeviceConsumableType): Promise { - if (!(consumable in CONSUMABLE_TYPE_RESET)) { - throw new ArgumentInvalidException('Invalid consumable'); - } - - const itemId = CONSUMABLE_TYPE_RESET[consumable]; - - await this.sendRecv('DEVICE_MAPID_SET_CONSUMABLES_PARAM_REQ', 'DEVICE_MAPID_SET_CONSUMABLES_PARAM_RSP', { itemId }); - } - - async updateMap(): Promise { - let mask = 0x78ff; - - if (!this.device.system.supports(DeviceCapability.MAP_PLANS)) { - mask = 0xff; - } - - await this.sendRecv('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP', { mask }); - } - - async getWlan(): Promise { - const packet = await this.sendRecv('DEVICE_WLAN_INFO_GETTING_REQ', 'DEVICE_WLAN_INFO_GETTING_RSP', {}); - const object = packet.payload.object; - - this.device.updateWlan(new DeviceWlan(object.body)); - this.emit('updateDevice'); - - return this.device.wlan as DeviceWlan; - } - - async enterManualMode(): Promise { - // TODO: make a stop if robot is not stopped - await this.sendRecv('DEVICE_MANUAL_CTRL_REQ', 'DEVICE_MANUAL_CTRL_RSP', { - command: MANUAL_MODE.init, - }); - } - - async leaveManualMode(): Promise { - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.STOP, - cleanType: 2, - }); - } - - async setManualMode(command: ManualMode): Promise { - await this.sendRecv('DEVICE_MANUAL_CTRL_REQ', 'DEVICE_MANUAL_CTRL_RSP', { - command, - }); - } - - async getOrders(): Promise { - const packet = await this.sendRecv('DEVICE_ORDERLIST_GETTING_REQ', 'DEVICE_ORDERLIST_GETTING_RSP', {}); - const object = packet.payload.object; - const orders = object.orderList?.map(deviceOrderMapper.toDomain) || []; - - this.device.updateOrders(orders); - - return orders; - } - - async setOrder(order: DeviceOrder): Promise { - const orderList = deviceOrderMapper.fromDomain(order); - - await this.sendRecv('DEVICE_ORDERLIST_SETTING_REQ', 'DEVICE_ORDERLIST_SETTING_RSP', orderList); - } - - async deleteOrder(order: DeviceOrder): Promise { - await this.sendRecv('DEVICE_ORDERLIST_DELETE_REQ', 'DEVICE_ORDERLIST_DELETE_RSP', { - orderId: order.id.value, - mode: 1, - }); - } - - async cleanPosition(position: MapPosition): Promise { - if (!this.device.map) { - throw new DomainException('Unable to set robot position: map not loaded'); - } - - if (this.device.mode?.value !== DeviceModeValue.Spot) { - await this.setMode(new DeviceMode(DeviceModeValue.Spot)); - } - - await this.sendRecv('DEVICE_MAPID_SET_NAVIGATION_REQ', 'DEVICE_MAPID_SET_NAVIGATION_RSP', { - mapHeadId: this.device.map.id.value, - poseX: position.x, - poseY: position.y, - posePhi: position.phi, - ctrlValue: CTRL_VALUE.START, - }); - } - - /** - * A ┌───┐ D - * │ │ - * B └───┘ C - */ - async cleanAreas(areas: MapCoordinate[][]): Promise { - if (!this.device.map) { - return; - } - - if (this.device.mode?.value !== DeviceModeValue.Zone) { - await this.setMode(new DeviceMode(DeviceModeValue.Zone)); - } - - const req = { - mapHeadId: this.device.map.id.value, - planId: 0, - cleanAreaLength: areas.length, - cleanAreaList: areas.map((coords) => { - return { - cleanAreaId: ID.generate().value, - type: 0, - coordinateLength: coords.length, - coordinateList: coords, - }; - }), - }; - - await this.sendRecv('DEVICE_MAPID_SET_AREA_CLEAN_INFO_REQ', 'DEVICE_MAPID_SET_AREA_CLEAN_INFO_RSP', req); - await this.sendRecv('DEVICE_AREA_CLEAN_REQ', 'DEVICE_AREA_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.START, - }); - } - - /** - * A ┌───┐ D - * │ │ - * B └───┘ C - */ - async setRestrictedZones(areas: MapCoordinate[][]): Promise { - if (!this.device.map) { - return; - } - - if (!areas.length) { - areas.push([]); - } - - if (!this.device.system.supports(DeviceCapability.MAP_PLANS)) { - await this.sendRecv('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP', { - mask: 0xff | 0x400, - }); - } - - const cleanAreaList = [ - ...areas.map((coords) => ({ - cleanAreaId: ID.generate().value, - type: 0, - coordinateLength: coords.length, - coordinateList: coords, - })), - ...this.device.map.restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: 0, - coordinateList: [], - })), - ]; - - await this.sendRecv('DEVICE_MAPID_SET_AREA_RESTRICTED_INFO_REQ', 'DEVICE_MAPID_SET_AREA_RESTRICTED_INFO_RSP', { - mapHeadId: this.device.map.id.value, - planId: 0, - cleanAreaLength: cleanAreaList.length, - cleanAreaList, - }); - } - - async getQuietHours(): Promise { - const packet = await this.sendRecv('USER_GET_DEVICE_QUIETHOURS_REQ', 'USER_GET_DEVICE_QUIETHOURS_RSP', {}); - const object = packet.payload.object; - const quietHours = new QuietHoursSetting({ - isEnabled: object.isOpen, - beginTime: DeviceTime.fromMinutes(object.beginTime), - endTime: DeviceTime.fromMinutes(object.endTime), - }); - - this.device.updateConfig(this.device.config?.clone({ quietHours })); - - return quietHours; - } - - async setQuietHours(quietHours: QuietHoursSetting): Promise { - await this.sendRecv('USER_SET_DEVICE_QUIETHOURS_REQ', 'USER_SET_DEVICE_QUIETHOURS_RSP', { - isOpen: quietHours.isEnabled, - beginTime: quietHours.beginTime.toMinutes(), - endTime: quietHours.endTime.toMinutes(), - }); - - this.device.updateConfig(this.device.config?.clone({ quietHours })); - } - - async setCarpetMode(enable: boolean): Promise { - await this.sendRecv('USER_SET_DEVICE_CLEANPREFERENCE_REQ', 'USER_SET_DEVICE_CLEANPREFERENCE_RSP', { - carpetTurbo: enable, - }); - - this.device.updateConfig(this.device.config?.clone({ carpetMode: new DeviceSetting({ isEnabled: enable }) })); - this.emit('updateDevice'); - } - - async setHistoryMap(enable: boolean): Promise { - await this.sendRecv('USER_SET_DEVICE_CLEANPREFERENCE_REQ', 'USER_SET_DEVICE_CLEANPREFERENCE_RSP', { - historyMap: enable, - }); - - this.device.updateConfig(this.device.config?.clone({ historyMap: new DeviceSetting({ isEnabled: enable }) })); - this.emit('updateDevice'); - } - - async setVoice(voice: VoiceSetting): Promise { - const robotVoice = deviceVoiceMapper.fromDomain(voice); - - await this.sendRecv('USER_SET_DEVICE_CTRL_SETTING_REQ', 'USER_SET_DEVICE_CTRL_SETTING_RSP', { - voiceMode: robotVoice.isEnabled, - volume: robotVoice.volume, - }); - - this.device.updateConfig(this.device.config?.clone({ voice })); - this.emit('updateDevice'); - } - - async saveWaitingMap(save: boolean): Promise { - await this.sendRecv('DEVICE_MAPID_SET_SAVEWAITINGMAP_INFO_REQ', 'DEVICE_MAPID_SET_SAVEWAITINGMAP_INFO_RSP', { - mode: Number(save), - }); - - this.device.updateHasWaitingMap(false); - } - - async updateRoom(room: Room): Promise { - if (!this.device.map) { - return; - } - - const { id, restrictedZones } = this.device.map; - - await this.sendRecv('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', 'DEVICE_MAPID_SET_PLAN_PARAMS_RSP', { - mapHeadId: id.value, - // FIXME: this will change user map name. - mapName: 'Default', - planId: 2, - // FIXME: this will change user plan name. - planName: 'Default', - roomList: this.device.map.rooms.map((r) => { - r = room.equals(r) ? room : r; - - return { - roomId: r.id.value, - roomName: r.name, - enable: true, - }; - }), - areaInfo: { - mapHeadId: id.value, - planId: 2, - cleanAreaLength: restrictedZones.length, - cleanAreaList: restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: zone.coordinates.length, - coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), - })), - }, - }); - } - - async joinRooms(rooms: Room[]): Promise { - if (!this.device.map) { - return; - } - - const stream = new BufferWriter(); - - writeByte(stream, 1); - writeByte(stream, rooms.length); - - rooms.forEach((room) => { - writeByte(stream, room.id.value); - }); - - const data = stream.buffer; - - await this.sendRecv('DEVICE_MAPID_SET_ARRANGEROOM_INFO_REQ', 'DEVICE_MAPID_SET_ARRANGEROOM_INFO_RSP', { - mapHeadId: this.device.map.id.value, - type: 0, - dataLen: data.length, - data, - roomId: 0, - }); - } - - async splitRoom(room: Room, pointA: MapCoordinate, pointB: MapCoordinate): Promise { - if (!this.device.map) { - return; - } - - const stream = new BufferWriter(); - - writeByte(stream, 1); - writeByte(stream, 1); - writeByte(stream, 2); - writeFloat(stream, pointA.x); - writeFloat(stream, pointA.y); - writeFloat(stream, pointB.x); - writeFloat(stream, pointB.y); - - const data = stream.buffer; - - await this.sendRecv('DEVICE_MAPID_SET_ARRANGEROOM_INFO_REQ', 'DEVICE_MAPID_SET_ARRANGEROOM_INFO_RSP', { - mapHeadId: this.device.map.id.value, - type: 1, - dataLen: data.length, - data, - roomId: room.id.value, - }); - } - - async cleanRooms(rooms: Room[]): Promise { - if (!this.device.map) { - return; - } - - const { id, restrictedZones } = this.device.map; - - await this.sendRecv('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', 'DEVICE_MAPID_SET_PLAN_PARAMS_RSP', { - mapHeadId: id.value, - // FIXME: this will change user map name. - mapName: 'Default', - planId: 2, - // FIXME: this will change user plan name. - planName: 'Default', - roomList: this.device.map.rooms.map((room) => ({ - roomId: room.id.value, - roomName: room.name, - enable: Boolean(rooms.find((r) => room.equals(r))), - })), - areaInfo: { - mapHeadId: id.value, - planId: 2, - cleanAreaLength: restrictedZones.length, - cleanAreaList: restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: zone.coordinates.length, - coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), - })), - }, - }); - - await this.sendRecv('DEVICE_MAPID_SELECT_MAP_PLAN_REQ', 'DEVICE_MAPID_SELECT_MAP_PLAN_RSP', { - mapHeadId: id.value, - planId: 2, - mode: 1, - }); - - await this.sendRecv('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP', { mask: 0x78ff }); - - await this.sendRecv('DEVICE_AUTO_CLEAN_REQ', 'DEVICE_AUTO_CLEAN_RSP', { - ctrlValue: CTRL_VALUE.START, - cleanType: 2, - }); - } - - async resetMap(): Promise { - await this.sendRecv('DEVICE_MAPID_SET_HISTORY_MAP_ENABLE_REQ', 'DEVICE_MAPID_SET_HISTORY_MAP_ENABLE_RSP', {}); - } - - async controlLock(): Promise { - await this.sendRecv('DEVICE_CONTROL_LOCK_REQ', 'DEVICE_CONTROL_LOCK_RSP', {}); - } - - async handshake(): Promise { - await this.controlLock(); - await this.send('DEVICE_STATUS_GETTING_REQ', {}); - await this.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { - unk1: 0, - unk2: '', - }); - - void this.getTime(); - void this.updateMap(); - void this.getWlan(); - } - - @bind - handleDeviceVersionInfoUpdate(message: Message<'DEVICE_VERSION_INFO_UPDATE_REQ'>): void { - const object = message.packet.payload.object; - - this.device.updateVersion( - new DeviceVersion({ - software: object.softwareVersion, - hardware: object.hardwareVersion, - }), - ); - this.emit('updateDevice'); - - void message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { - result: 0, - }); - } - - @bind - handleDeviceAgentSetting(message: Message<'PUSH_DEVICE_AGENT_SETTING_REQ'>): void { - const object = message.packet.payload.object; - const props: DeviceSettingsProps = { - voice: deviceVoiceMapper.toDomain({ - isEnabled: object.voice.voiceMode, - volume: object.voice.volume, - }), - quietHours: new QuietHoursSetting({ - isEnabled: object.quietHours.isOpen, - beginTime: DeviceTime.fromMinutes(object.quietHours.beginTime), - endTime: DeviceTime.fromMinutes(object.quietHours.endTime), - }), - ecoMode: new DeviceSetting({ isEnabled: object.cleanPreference.ecoMode ?? false }), - repeatClean: new DeviceSetting({ isEnabled: object.cleanPreference.repeatClean ?? false }), - brokenClean: new DeviceSetting({ isEnabled: object.cleanPreference.cleanBroken ?? false }), - carpetMode: new DeviceSetting({ isEnabled: object.cleanPreference.carpetTurbo ?? false }), - historyMap: new DeviceSetting({ isEnabled: object.cleanPreference.historyMap ?? false }), - }; - - this.device.updateConfig(new DeviceSettings(props)); - - void message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { - result: 0, - }); - } - - @bind - handleClientHeartbeat(message: Message<'CLIENT_HEARTBEAT_REQ'>): void { - void message.respond('CLIENT_HEARTBEAT_RSP', {}); - } - - @bind - handleDevicePackageUpgrade(message: Message<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>): void { - void message.respond('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { - result: 0, - }); - } - - @bind - handleDeviceStatus(message: Message<'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'>): void { - const object = message.packet.payload.object; - const { battery, type, workMode, chargeStatus, cleanPreference, faultCode, waterLevel, mopType } = object; - - this.device.updateCurrentClean( - new DeviceCleanWork({ - size: new CleanSize(object.cleanSize), - time: DeviceTime.fromMinutes(object.cleanTime), - }), - ); - this.device.updateState(deviceStateMapper.toDomain({ type, workMode, chargeStatus })); - this.device.updateMode(deviceModeMapper.toDomain(workMode)); - this.device.updateError(deviceErrorMapper.toDomain(faultCode)); - this.device.updateBattery(deviceBatteryMapper.toDomain(battery)); - this.device.updateFanSpeed(deviceFanSpeedMapper.toDomain(cleanPreference)); - - if (isPresent(mopType)) { - this.device.updateHasMopAttached(mopType); - } - - if (isPresent(waterLevel)) { - this.device.updateWaterLevel(deviceWaterLevelMapper.toDomain(waterLevel)); - } - - this.emit('updateDevice'); - } - - @bind - handleMapUpdate(message: Message<'DEVICE_MAPID_PUSH_MAP_INFO' | 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'>): void { - const object = message.packet.payload.object; - const { - statusInfo, - mapHeadInfo, - mapGrid, - historyHeadInfo, - robotPoseInfo, - robotChargeInfo, - cleanRoomList, - roomSegmentList, - wallListInfo, - spotInfo, - cleanPlanList, - currentPlanId, - } = object; - - if (statusInfo) { - const { - batteryPercent: battery, - faultType: type, - workingMode: workMode, - chargeState: chargeStatus, - cleanPreference, - faultCode, - } = statusInfo; - - this.device.updateCurrentClean( - new DeviceCleanWork({ - size: new CleanSize(statusInfo.cleanSize), - time: DeviceTime.fromMinutes(statusInfo.cleanTime), - }), - ); - this.device.updateBattery(deviceBatteryMapper.toDomain(battery)); - this.device.updateMode(deviceModeMapper.toDomain(workMode)); - this.device.updateState(deviceStateMapper.toDomain({ type, workMode, chargeStatus })); - this.device.updateError(deviceErrorMapper.toDomain(faultCode)); - this.device.updateFanSpeed(deviceFanSpeedMapper.toDomain(cleanPreference)); - this.emit('updateDevice'); - } - - let map = this.device.map; - - if (mapHeadInfo && mapGrid) { - const props = { - id: new ID(mapHeadInfo.mapHeadId), - size: new MapPixel({ - x: mapHeadInfo.sizeX, - y: mapHeadInfo.sizeY, - }), - min: new MapCoordinate({ - x: mapHeadInfo.minX, - y: mapHeadInfo.minY, - }), - max: new MapCoordinate({ - x: mapHeadInfo.maxX, - y: mapHeadInfo.maxY, - }), - resolution: mapHeadInfo.resolution, - grid: mapGrid, - rooms: [], - restrictedZones: [], - robotPath: [], - }; - - map = map ? map.clone(props) : new DeviceMap(props); - - this.device.updateMap(map); - } - - if (map) { - if (historyHeadInfo) { - const currentIndex = map.robotPath.length; - - map.updateRobotPath( - map.robotPath.concat( - historyHeadInfo.pointList.slice(currentIndex).map(({ x, y }) => new MapCoordinate({ x, y })), - ), - ); - } - - if (robotPoseInfo) { - map.updateRobot( - new MapPosition({ - x: robotPoseInfo.poseX, - y: robotPoseInfo.poseY, - phi: robotPoseInfo.posePhi, - }), - ); - } - - if (robotChargeInfo) { - map.updateCharger( - new MapPosition({ - x: robotChargeInfo.poseX, - y: robotChargeInfo.poseY, - phi: robotChargeInfo.posePhi, - }), - ); - } - - if (spotInfo) { - map.updateCurrentSpot( - new MapPosition({ - x: spotInfo.poseX, - y: spotInfo.poseY, - phi: spotInfo.posePhi, - }), - ); - } - - if (wallListInfo) { - map.updateRestrictedZones( - wallListInfo.cleanAreaList.map((cleanArea) => { - return new Zone({ - id: new ID(cleanArea.cleanAreaId), - coordinates: cleanArea.coordinateList.map(({ x, y }) => { - return new MapCoordinate({ - x, - y, - }); - }), - }); - }), - ); - } - - if (cleanRoomList && roomSegmentList && cleanPlanList) { - const currentPlan = cleanPlanList.find((plan) => plan.planId === currentPlanId); - - map.updateRooms( - cleanRoomList - .map((cleanRoom) => { - const segment = roomSegmentList.find((roomSegment) => roomSegment.roomId === cleanRoom.roomId); - const roomInfo = currentPlan?.cleanRoomInfoList.find((r) => r.roomId === cleanRoom.roomId); - - if (!segment) { - return undefined; - } - - return new Room({ - id: new ID(cleanRoom.roomId), - name: cleanRoom.roomName, - isEnabled: Boolean(roomInfo?.enable), - center: new MapCoordinate({ - x: cleanRoom.roomX, - y: cleanRoom.roomY, - }), - pixels: segment?.roomPixelList.map((pixel) => { - return new MapPixel({ - x: pixel.x, - y: pixel.y, - }); - }), - }); - }) - .filter(isPresent), - ); - } - } - - this.emit('updateMap'); - } - - @bind - handleUpdateRobotPosition(message: Message<'DEVICE_MAPID_PUSH_POSITION_INFO'>): void { - if (!this.device.map) { - return; - } - - const object = message.packet.payload.object; - - if (object.update) { - this.device.map.updateRobot( - new MapPosition({ - x: object.poseX, - y: object.poseY, - phi: object.posePhi, - }), - ); - - this.emit('updateRobotPosition'); - } - } - - @bind - handleUpdateChargePosition(message: Message<'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'>): void { - if (!this.device.map) { - return; - } - - const object = message.packet.payload.object; - - this.device.map.updateCharger( - new MapPosition({ - x: object.poseX, - y: object.poseY, - phi: object.posePhi, - }), - ); - this.emit('updateChargerPosition'); - } - - @bind - handleDeviceBatteryInfo(message: Message<'PUSH_DEVICE_BATTERY_INFO_REQ'>): void { - void message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { - result: 0, - }); - - const object = message.packet.payload.object; - - this.device.updateBattery(deviceBatteryMapper.toDomain(object.battery.level)); - - this.emit('updateDevice'); - } - - @bind - handleWaitingMap(): void { - this.device.updateHasWaitingMap(true); - this.emit('updateDevice'); - } - - @bind - handleWorkstatusReport(message: Message<'DEVICE_WORKSTATUS_REPORT_REQ'>): void { - void message.respond('DEVICE_WORKSTATUS_REPORT_RSP', { - result: 0, - }); - } - - @bind - handleReportCleantask(message: Message<'DEVICE_EVENT_REPORT_CLEANTASK'>): void { - void message.respond('UNK_11A4', { unk1: 0 }); - } - - @bind - handleReportCleanmap(message: Message<'DEVICE_EVENT_REPORT_CLEANMAP'>): void { - const object = message.packet.payload.object; - void message.respond('DEVICE_EVENT_REPORT_RSP', { - result: 0, - body: { - cleanId: object.cleanId, - }, - }); - } - - @bind - handleBinDataReport(message: Message<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): void { - const object = message.packet.payload.object; - void message.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', { - result: 0, - cleanId: object.cleanId, - }); - } - - @bind - handleEventReport(message: Message<'DEVICE_EVENT_REPORT_REQ'>): void { - void message.respond('UNK_11A7', { unk1: 0 }); - } - - @bind - handleSetTime(message: Message<'DEVICE_SETTIME_REQ'>): void { - const date = new Date(); - - void message.respond('DEVICE_SETTIME_RSP', { - deviceTime: Math.floor(date.getTime() / 1000), - deviceTimezone: -1 * (date.getTimezoneOffset() * 60), - }); - } - - addConnection(connection: Connection): void { - const added = this.multiplexer.addConnection(connection); - - if (added && this.multiplexer.connectionCount === 2) { - void this.handshake(); - } - } - - handleMessage(message: Message): void { - const handler = this.handlers[message.opname]; - - if (message.packet.userId.value !== 0 && message.packet.userId.value !== this.device.userId.value) { - void message.respond('COMMON_ERROR_REPLY', { - result: 11001, - error: 'Target user is offline', - opcode: message.packet.payload.opcode.code, - }); - return; - } - - if (handler) { - handler(message); - } else { - this.debug(`unhandled opcode ${message.opname}`); - } - } - - override toString(): string { - return [`device: ${this.device.toString()}`].join(' '); - } - - disconnect(): Promise { - this.debug('disconnecting...'); - - return this.multiplexer.close(); - } - - async send(opname: Name, object: PayloadObjectFrom): Promise { - try { - await this.multiplexer.send({ - opname, - device: this.device, - object, - }); - } catch (err) { - throw new DomainException(`There was an error sending opcode '${opname}'`, {}, { cause: err }); - } - } - - recv(opname: Name): Promise> { - return new Promise((resolve, reject) => { - const done = (packet: Packet) => { - clearTimeout(timer); - resolve(packet as Packet); - }; - - const fail = () => { - this.multiplexer.off(opname, done); - reject(new DomainException(`Timeout waiting for response from opcode '${opname}'`)); - }; - - const timer = setTimeout(fail, RECV_TIMEOUT); - - this.multiplexer.once(opname, done); - }); - } - - async sendRecv( - sendOPName: SendName, - recvOPName: RecvName, - sendObject: PayloadObjectFrom, - ): Promise> { - await this.send(sendOPName, sendObject); - - return this.recv(recvOPName); - } -} diff --git a/packages/adapter-tcp/src/index.ts b/packages/adapter-tcp/src/index.ts index 833bdc01..e397dcd6 100644 --- a/packages/adapter-tcp/src/index.ts +++ b/packages/adapter-tcp/src/index.ts @@ -1,7 +1,3 @@ -export * from './emitters/cloud-server.emitter'; -export * from './emitters/connection.emitter'; -export * from './emitters/multiplexer.emitter'; -export * from './emitters/robot.emitter'; export * from './mappers/device-battery.mapper'; export * from './mappers/device-error.mapper'; export * from './mappers/device-fan-speed.mapper'; @@ -10,5 +6,4 @@ export * from './mappers/device-order.mapper'; export * from './mappers/device-state.mapper'; export * from './mappers/device-voice.mapper'; export * from './mappers/device-water-level.mapper'; -export * from './value-objects/message.value-object'; export * from './tcp.server'; diff --git a/packages/adapter-tcp/src/value-objects/message.value-object.ts b/packages/adapter-tcp/src/value-objects/message.value-object.ts deleted file mode 100644 index a6e1fc86..00000000 --- a/packages/adapter-tcp/src/value-objects/message.value-object.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { ValueObject, isPresent, ArgumentNotProvidedException, ArgumentInvalidException } from '@agnoc/toolkit'; -import { Packet } from '@agnoc/transport-tcp'; -import { Connection } from '../emitters/connection.emitter'; -import type { PayloadObjectName, PayloadObjectFrom } from '@agnoc/transport-tcp'; - -export interface MessageProps { - connection: Connection; - packet: Packet; -} - -export type MessageHandler = (message: Message) => void; -export type MessageHandlers = Partial<{ - [Name in PayloadObjectName]: MessageHandler; -}>; - -export class Message extends ValueObject> { - constructor(props: MessageProps) { - super(props); - } - - get connection(): Connection { - return this.props.connection; - } - - get packet(): Packet { - return this.props.packet; - } - - get opname(): Name { - return this.packet.payload.opcode.name as Name; - } - - respond(opname: RName, object: PayloadObjectFrom): Promise { - return this.connection.respond({ packet: this.packet, opname, object }); - } - - public override toString(): string { - return '[Message]'; - } - - protected validate(props: MessageProps): void { - if (![props.connection, props.packet].every(isPresent)) { - throw new ArgumentNotProvidedException('Missing property in message constructor'); - } - - if (!(props.connection instanceof Connection)) { - throw new ArgumentInvalidException('Invalid connection in message constructor'); - } - - if (!(props.packet instanceof Packet)) { - throw new ArgumentInvalidException('Invalid packet in message constructor'); - } - - if (!isPresent(props.packet.payload.opcode.name)) { - throw new ArgumentInvalidException('Unknown packet in message constructor'); - } - } -} diff --git a/packages/core/package.json b/packages/core/package.json index cad0bfb2..6b760dc3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,10 +70,7 @@ }, "dependencies": { "@agnoc/domain": "^0.18.0-next.0", - "@agnoc/adapter-tcp": "^0.18.0-next.0", "@agnoc/toolkit": "^0.18.0-next.0", - "debug": "^4.3.4", - "emittery": "^0.13.1", "tslib": "^2.5.0" }, "devDependencies": { diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json index 7b57914c..f36d30e5 100644 --- a/packages/transport-tcp/package.json +++ b/packages/transport-tcp/package.json @@ -79,7 +79,6 @@ "dependencies": { "@agnoc/schemas-tcp": "^0.16.0", "@agnoc/toolkit": "^0.18.0-next.0", - "debug": "^4.3.4", "emittery": "^0.13.1", "protobufjs": "^7.2.2", "tslib": "^2.5.0" From 49eaaa3b635db0e39d5cba6da0131f824d26809e Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 10:33:29 +0100 Subject: [PATCH 16/38] feat: add `FindDeviceQuery` --- packages/adapter-tcp/src/tcp.server.test.ts | 4 +-- packages/adapter-tcp/src/tcp.server.ts | 6 ++-- .../test/integration/tcp.server.test.ts | 10 +++---- packages/core/src/agnoc.server.test.ts | 4 +-- packages/core/src/agnoc.server.ts | 28 ++++++++++++------- packages/core/src/index.ts | 1 + .../find-device.query-handler.ts | 18 ++++++++++++ ...test.ts => command-query.task-bus.test.ts} | 6 ++-- .../src/event-buses/command-query.task-bus.ts | 8 ++++++ .../src/event-buses/command.event-bus.ts | 4 --- ...ent-handler.ts => command.task-handler.ts} | 0 .../src/event-handlers/query.task-handler.ts | 7 +++++ packages/domain/src/index.ts | 7 +++-- .../domain/src/queries/find-device.query.ts | 26 +++++++++++++++++ packages/domain/src/queries/queries.ts | 7 +++++ 15 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 packages/core/src/query-handlers/find-device.query-handler.ts rename packages/domain/src/event-buses/{command.event-bus.test.ts => command-query.task-bus.test.ts} (64%) create mode 100644 packages/domain/src/event-buses/command-query.task-bus.ts delete mode 100644 packages/domain/src/event-buses/command.event-bus.ts rename packages/domain/src/event-handlers/{command.event-handler.ts => command.task-handler.ts} (100%) create mode 100644 packages/domain/src/event-handlers/query.task-handler.ts create mode 100644 packages/domain/src/queries/find-device.query.ts create mode 100644 packages/domain/src/queries/queries.ts diff --git a/packages/adapter-tcp/src/tcp.server.test.ts b/packages/adapter-tcp/src/tcp.server.test.ts index 3bde6fb9..5ef401c8 100644 --- a/packages/adapter-tcp/src/tcp.server.test.ts +++ b/packages/adapter-tcp/src/tcp.server.test.ts @@ -1,11 +1,11 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { TCPServer } from './tcp.server'; -import type { Commands, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; +import type { CommandsOrQueries, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; import type { EventHandlerRegistry, TaskHandlerRegistry } from '@agnoc/toolkit'; describe('TCPServer', function () { let domainEventHandlerRegistry: EventHandlerRegistry; - let commandHandlerRegistry: TaskHandlerRegistry; + let commandHandlerRegistry: TaskHandlerRegistry; let deviceRepository: DeviceRepository; let connectionRepository: ConnectionRepository; let tcpAdapter: TCPServer; diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index d28885c7..faafdbbd 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -43,7 +43,7 @@ import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device- import { DeviceWlanUpdateEventHandler } from './packet-event-handlers/device-wlan-update.event-handler'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; -import type { Commands, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; +import type { CommandsOrQueries, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; import type { Server, TaskHandlerRegistry } from '@agnoc/toolkit'; import type { AddressInfo } from 'net'; @@ -56,7 +56,7 @@ export class TCPServer implements Server { private readonly deviceRepository: DeviceRepository, private readonly connectionRepository: ConnectionRepository, private readonly domainEventHandlerRegistry: EventHandlerRegistry, - private readonly commandHandlerRegistry: TaskHandlerRegistry, + private readonly commandQueryHandlerRegistry: TaskHandlerRegistry, ) { // Packet foundation const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); @@ -142,7 +142,7 @@ export class TCPServer implements Server { ); // Command event handlers - this.commandHandlerRegistry.register(new LocateDeviceEventHandler(connectionRepository)); + this.commandQueryHandlerRegistry.register(new LocateDeviceEventHandler(connectionRepository)); } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { diff --git a/packages/adapter-tcp/test/integration/tcp.server.test.ts b/packages/adapter-tcp/test/integration/tcp.server.test.ts index fc9bc7cf..8e397c95 100644 --- a/packages/adapter-tcp/test/integration/tcp.server.test.ts +++ b/packages/adapter-tcp/test/integration/tcp.server.test.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { once } from 'events'; -import { CommandBus, ConnectionRepository, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { CommandQueryBus, ConnectionRepository, Device, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; import { EventHandlerRegistry, ID, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; import { @@ -15,15 +15,15 @@ import { import { expect } from 'chai'; import { TCPServer } from '@agnoc/adapter-tcp'; import type { TCPAdapterListenOptions } from '@agnoc/adapter-tcp'; -import type { Commands } from '@agnoc/domain'; +import type { CommandsOrQueries } from '@agnoc/domain'; import type { ICLIENT_ONLINE_REQ, IDEVICE_REGISTER_REQ } from '@agnoc/schemas-tcp'; import type { CreatePacketProps, Packet } from '@agnoc/transport-tcp'; describe('Integration', function () { let domainEventBus: DomainEventBus; - let commandBus: CommandBus; + let commandBus: CommandQueryBus; let domainEventHandlerRegistry: EventHandlerRegistry; - let commandHandlerRegistry: TaskHandlerRegistry; + let commandHandlerRegistry: TaskHandlerRegistry; let deviceRepository: DeviceRepository; let connectionRepository: ConnectionRepository; let tcpAdapter: TCPServer; @@ -34,7 +34,7 @@ describe('Integration', function () { beforeEach(function () { // Server blocks domainEventBus = new DomainEventBus(); - commandBus = new CommandBus(); + commandBus = new CommandQueryBus(); domainEventHandlerRegistry = new EventHandlerRegistry(domainEventBus); commandHandlerRegistry = new TaskHandlerRegistry(commandBus); diff --git a/packages/core/src/agnoc.server.test.ts b/packages/core/src/agnoc.server.test.ts index a1946b7d..f3b31d11 100644 --- a/packages/core/src/agnoc.server.test.ts +++ b/packages/core/src/agnoc.server.test.ts @@ -21,7 +21,7 @@ describe('AgnocServer', function () { expect(container.deviceRepository).to.be.instanceOf(DeviceRepository); expect(container.connectionRepository).to.be.instanceOf(ConnectionRepository); expect(container.domainEventHandlerRegistry).to.be.instanceOf(EventHandlerRegistry); - expect(container.commandHandlerRegistry).to.be.instanceOf(TaskHandlerRegistry); + expect(container.commandQueryHandlerRegistry).to.be.instanceOf(TaskHandlerRegistry); return instance(server); }); @@ -78,7 +78,7 @@ describe('AgnocServer', function () { when(taskHandler.forName).thenReturn('LocateDeviceCommand'); - agnocServer.buildAdapter(({ commandHandlerRegistry }) => { + agnocServer.buildAdapter(({ commandQueryHandlerRegistry: commandHandlerRegistry }) => { commandHandlerRegistry.register(instance(taskHandler)); return instance(server); diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts index 16759f93..a7653c0d 100644 --- a/packages/core/src/agnoc.server.ts +++ b/packages/core/src/agnoc.server.ts @@ -1,13 +1,14 @@ -import { CommandBus, ConnectionRepository, DeviceRepository, DomainEventBus } from '@agnoc/domain'; +import { CommandQueryBus, ConnectionRepository, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { EventHandlerRegistry, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; -import type { DomainEventNames, DomainEvents, Commands } from '@agnoc/domain'; +import { FindDeviceQueryHandler } from './query-handlers/find-device.query-handler'; +import type { DomainEventNames, DomainEvents, CommandsOrQueries } from '@agnoc/domain'; import type { Server, TaskOutput } from '@agnoc/toolkit'; export class AgnocServer implements Server { private readonly domainEventBus: DomainEventBus; private readonly domainEventHandlerRegistry: EventHandlerRegistry; - private readonly commandBus: CommandBus; - private readonly commandHandlerRegistry: TaskHandlerRegistry; + private readonly commandQueryBus: CommandQueryBus; + private readonly commandQueryHandlerRegistry: TaskHandlerRegistry; private readonly deviceRepository: DeviceRepository; private readonly connectionRepository: ConnectionRepository; private readonly adapters = new Set(); @@ -15,24 +16,27 @@ export class AgnocServer implements Server { constructor() { this.domainEventBus = new DomainEventBus(); this.domainEventHandlerRegistry = new EventHandlerRegistry(this.domainEventBus); - this.commandBus = new CommandBus(); - this.commandHandlerRegistry = new TaskHandlerRegistry(this.commandBus); + this.commandQueryBus = new CommandQueryBus(); + this.commandQueryHandlerRegistry = new TaskHandlerRegistry(this.commandQueryBus); this.deviceRepository = new DeviceRepository(this.domainEventBus, new MemoryAdapter()); this.connectionRepository = new ConnectionRepository(this.domainEventBus, new MemoryAdapter()); + this.registerHandlers(); } subscribe(eventName: Name, handler: SubscribeHandler): void { this.domainEventBus.on(eventName, handler); } - trigger(command: Command): Promise> { - return this.commandBus.trigger(command); + trigger( + command: CommandOrQuery, + ): Promise> { + return this.commandQueryBus.trigger(command); } buildAdapter(builder: AdapterFactory): void { const adapter = builder({ domainEventHandlerRegistry: this.domainEventHandlerRegistry, - commandHandlerRegistry: this.commandHandlerRegistry, + commandQueryHandlerRegistry: this.commandQueryHandlerRegistry, deviceRepository: this.deviceRepository, connectionRepository: this.connectionRepository, }); @@ -47,6 +51,10 @@ export class AgnocServer implements Server { async close(): Promise { await Promise.all([...this.adapters].map((adapter) => adapter.close())); } + + private registerHandlers(): void { + this.commandQueryHandlerRegistry.register(new FindDeviceQueryHandler(this.deviceRepository)); + } } export interface AgnocServerListenOptions { @@ -57,7 +65,7 @@ type AdapterFactory = (container: Container) => Server; export type Container = { domainEventHandlerRegistry: EventHandlerRegistry; - commandHandlerRegistry: TaskHandlerRegistry; + commandQueryHandlerRegistry: TaskHandlerRegistry; deviceRepository: DeviceRepository; connectionRepository: ConnectionRepository; }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cb67867d..3fa0af59 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1 +1,2 @@ export * from './agnoc.server'; +export * from './query-handlers/find-device.query-handler'; diff --git a/packages/core/src/query-handlers/find-device.query-handler.ts b/packages/core/src/query-handlers/find-device.query-handler.ts new file mode 100644 index 00000000..34dfd80d --- /dev/null +++ b/packages/core/src/query-handlers/find-device.query-handler.ts @@ -0,0 +1,18 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { DeviceRepository, FindDeviceQuery, FindDeviceQueryOutput, QueryHandler } from '@agnoc/domain'; + +export class FindDeviceQueryHandler implements QueryHandler { + readonly forName = 'FindDeviceQuery'; + + constructor(private readonly deviceRepository: DeviceRepository) {} + + async handle(event: FindDeviceQuery): Promise { + const device = await this.deviceRepository.findOneById(event.deviceId); + + if (!device) { + throw new DomainException(`Unable to find a device with id ${event.deviceId.value}`); + } + + return { device }; + } +} diff --git a/packages/domain/src/event-buses/command.event-bus.test.ts b/packages/domain/src/event-buses/command-query.task-bus.test.ts similarity index 64% rename from packages/domain/src/event-buses/command.event-bus.test.ts rename to packages/domain/src/event-buses/command-query.task-bus.test.ts index b4a47fdc..c73eaf3b 100644 --- a/packages/domain/src/event-buses/command.event-bus.test.ts +++ b/packages/domain/src/event-buses/command-query.task-bus.test.ts @@ -1,12 +1,12 @@ import { TaskBus } from '@agnoc/toolkit'; import { expect } from 'chai'; -import { CommandBus } from './command.event-bus'; +import { CommandQueryBus } from './command-query.task-bus'; describe('CommandBus', function () { - let commandBus: CommandBus; + let commandBus: CommandQueryBus; beforeEach(function () { - commandBus = new CommandBus(); + commandBus = new CommandQueryBus(); }); it('should be created', function () { diff --git a/packages/domain/src/event-buses/command-query.task-bus.ts b/packages/domain/src/event-buses/command-query.task-bus.ts new file mode 100644 index 00000000..235c6132 --- /dev/null +++ b/packages/domain/src/event-buses/command-query.task-bus.ts @@ -0,0 +1,8 @@ +import { TaskBus } from '@agnoc/toolkit'; +import type { Commands } from '../commands/commands'; +import type { Queries } from '../queries/queries'; + +export type CommandsOrQueries = Commands & Queries; +export type CommandOrQueryNames = keyof CommandsOrQueries; + +export class CommandQueryBus extends TaskBus {} diff --git a/packages/domain/src/event-buses/command.event-bus.ts b/packages/domain/src/event-buses/command.event-bus.ts deleted file mode 100644 index 8419aae4..00000000 --- a/packages/domain/src/event-buses/command.event-bus.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { TaskBus } from '@agnoc/toolkit'; -import type { Commands } from '../commands/commands'; - -export class CommandBus extends TaskBus {} diff --git a/packages/domain/src/event-handlers/command.event-handler.ts b/packages/domain/src/event-handlers/command.task-handler.ts similarity index 100% rename from packages/domain/src/event-handlers/command.event-handler.ts rename to packages/domain/src/event-handlers/command.task-handler.ts diff --git a/packages/domain/src/event-handlers/query.task-handler.ts b/packages/domain/src/event-handlers/query.task-handler.ts new file mode 100644 index 00000000..d306da8a --- /dev/null +++ b/packages/domain/src/event-handlers/query.task-handler.ts @@ -0,0 +1,7 @@ +import type { Queries, QueryNames } from '../queries/queries'; +import type { TaskHandler } from '@agnoc/toolkit'; + +export abstract class QueryHandler implements TaskHandler { + abstract forName: QueryNames; + abstract handle(event: Queries[this['forName']]): void; +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 5b5d162a..8ef919c8 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -19,9 +19,9 @@ export * from './entities/device-map.entity'; export * from './entities/device-order.entity'; export * from './entities/room.entity'; export * from './entities/zone.entity'; -export * from './event-buses/command.event-bus'; +export * from './event-buses/command-query.task-bus'; export * from './event-buses/domain.event-bus'; -export * from './event-handlers/command.event-handler'; +export * from './event-handlers/command.task-handler'; export * from './event-handlers/domain.event-handler'; export * from './repositories/connection.repository'; export * from './repositories/device.repository'; @@ -38,3 +38,6 @@ export * from './value-objects/map-pixel.value-object'; export * from './value-objects/map-position.value-object'; export * from './value-objects/quiet-hours-setting.value-object'; export * from './value-objects/voice-setting.value-object'; +export * from './queries/find-device.query'; +export * from './queries/queries'; +export * from './event-handlers/query.task-handler'; diff --git a/packages/domain/src/queries/find-device.query.ts b/packages/domain/src/queries/find-device.query.ts new file mode 100644 index 00000000..1b49261d --- /dev/null +++ b/packages/domain/src/queries/find-device.query.ts @@ -0,0 +1,26 @@ +import { ID, Query } from '@agnoc/toolkit'; +import { Device } from '../aggregate-roots/device.aggregate-root'; + +export interface FindDeviceQueryInput { + deviceId: ID; +} + +export interface FindDeviceQueryOutput { + device: Device; +} + +export class FindDeviceQuery extends Query { + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: FindDeviceQueryInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } + + override validateOutput(output: FindDeviceQueryOutput): void { + this.validateDefinedProp(output, 'device'); + this.validateInstanceProp(output, 'device', Device); + } +} diff --git a/packages/domain/src/queries/queries.ts b/packages/domain/src/queries/queries.ts new file mode 100644 index 00000000..65bf1406 --- /dev/null +++ b/packages/domain/src/queries/queries.ts @@ -0,0 +1,7 @@ +import type { FindDeviceQuery } from './find-device.query'; + +export type Queries = { + FindDeviceQuery: FindDeviceQuery; +}; + +export type QueryNames = keyof Queries; From c3e657e215c2dce572c72447444d9c787352f728 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 10:46:45 +0100 Subject: [PATCH 17/38] refactor(transport-tcp): rename `object` to `data` in `Payload` --- .../packet-connection.aggregate-root.ts | 18 +++------ .../client-login.event-handler.ts | 2 +- .../device-battery-update.event-handler.ts | 2 +- ...ice-clean-map-data-report.event-handler.ts | 2 +- .../device-clean-map-report.event-handler.ts | 2 +- ...p-charger-position-update.event-handler.ts | 2 +- .../device-map-update.event-handler.ts | 2 +- ...ce-map-work-status-update.event-handler.ts | 2 +- .../device-register.event-handler.ts | 2 +- .../device-settings-update.event-handler.ts | 2 +- .../device-version-update.event-handler.ts | 2 +- .../device-wlan-update.event-handler.ts | 2 +- .../src/packet-server.connection-handler.ts | 12 +++--- packages/adapter-tcp/src/packet.event-bus.ts | 4 +- .../adapter-tcp/src/packet.event-handler.ts | 4 +- packages/adapter-tcp/src/packet.message.ts | 8 ++-- packages/adapter-tcp/src/tcp.server.ts | 4 +- .../test/integration/tcp.server.test.ts | 14 +++---- packages/cli/src/cli.ts | 4 +- .../cli/src/commands/decode.command.test.ts | 8 ++-- .../cli/src/commands/encode.command.test.ts | 4 +- .../cli/src/commands/read.command.test.ts | 8 ++-- .../packet-encode-transform.stream.test.ts | 2 +- .../streams/packet-encode-transform.stream.ts | 8 ++-- packages/cli/src/test-support.ts | 2 +- .../src/constants/payloads.constant.ts | 8 ++-- .../src/factories/packet.factory.test.ts | 4 +- .../src/factories/packet.factory.ts | 16 ++++---- packages/transport-tcp/src/index.ts | 2 +- .../src/mappers/packet.mapper.test.ts | 2 +- .../src/mappers/packet.mapper.ts | 4 +- .../src/mappers/payload.mapper.test.ts | 24 ++++++------ .../src/mappers/payload.mapper.ts | 24 ++++++------ ...ts => payload-data-parser.service.test.ts} | 38 +++++++++---------- ...vice.ts => payload-data-parser.service.ts} | 20 +++++----- packages/transport-tcp/src/test-support.ts | 2 +- .../src/utils/get-custom-decoders.util.ts | 2 +- .../value-objects/packet.value-object.test.ts | 4 +- .../src/value-objects/packet.value-object.ts | 6 +-- .../payload.value-object.test.ts | 22 +++++------ .../src/value-objects/payload.value-object.ts | 22 +++++------ 41 files changed, 158 insertions(+), 164 deletions(-) rename packages/transport-tcp/src/services/{payload-object-parser.service.test.ts => payload-data-parser.service.test.ts} (82%) rename packages/transport-tcp/src/services/{payload-object-parser.service.ts => payload-data-parser.service.ts} (63%) diff --git a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts index d817e1ee..f474091f 100644 --- a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts @@ -4,13 +4,7 @@ import { PacketSocket } from '@agnoc/transport-tcp'; import type { PacketEventBus } from '../packet.event-bus'; import type { PacketMessage } from '../packet.message'; import type { ConnectionProps } from '@agnoc/domain'; -import type { - Packet, - PacketFactory, - PayloadObjectName, - PayloadObjectFrom, - CreatePacketProps, -} from '@agnoc/transport-tcp'; +import type { Packet, PacketFactory, PayloadDataName, PayloadDataFrom, CreatePacketProps } from '@agnoc/transport-tcp'; export interface PacketConnectionProps extends ConnectionProps { socket: PacketSocket; @@ -31,25 +25,25 @@ export class PacketConnection extends Connection { return this.props.socket; } - send(name: Name, object: PayloadObjectFrom): Promise { + send(name: Name, object: PayloadDataFrom): Promise { const packet = this.packetFactory.create(name, object, this.getPacketProps()); return this.write(packet); } - respond(name: Name, object: PayloadObjectFrom, packet: Packet): Promise { + respond(name: Name, object: PayloadDataFrom, packet: Packet): Promise { return this.write(this.packetFactory.create(name, object, packet)); } - sendAndWait(name: Name, object: PayloadObjectFrom): Promise { + sendAndWait(name: Name, object: PayloadDataFrom): Promise { const packet = this.packetFactory.create(name, object, this.getPacketProps()); return this.writeAndWait(packet); } - respondAndWait( + respondAndWait( name: Name, - object: PayloadObjectFrom, + object: PayloadDataFrom, packet: Packet, ): Promise { return this.writeAndWait(this.packetFactory.create(name, object, packet)); diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts index f5a89dd4..511b98b1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts @@ -8,7 +8,7 @@ export class ClientLoginEventHandler implements PacketEventHandler { if (!message.device) { const data = { result: 12002, - reason: `Device not registered(devsn: ${message.packet.payload.object.deviceSerialNumber})`, + reason: `Device not registered(devsn: ${message.packet.payload.data.deviceSerialNumber})`, }; return message.respond('CLIENT_ONLINE_RSP', data); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts index 848282a9..8f35f416 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts @@ -13,7 +13,7 @@ export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; message.device.updateBattery(this.deviceBatteryMapper.toDomain(data.battery.level)); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts index b5ff40bc..e1abd193 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -10,7 +10,7 @@ export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; // TODO: save device clean map data diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts index cf54020a..a8b0098c 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -10,7 +10,7 @@ export class DeviceCleanMapReportEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; // TODO: save device clean map data diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts index d42d02df..4488a8f6 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -11,7 +11,7 @@ export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHa throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; message.device.map?.updateCharger( new MapPosition({ diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index c165d7e1..21a5d52b 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -47,7 +47,7 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { spotInfo, cleanPlanList, currentPlanId, - } = message.packet.payload.object; + } = message.packet.payload.data; if (statusInfo) { const { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index ba256f7f..84d320cd 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -37,7 +37,7 @@ export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler mopType, cleanSize, cleanTime, - } = message.packet.payload.object; + } = message.packet.payload.data; message.device.updateCurrentClean( new DeviceCleanWork({ diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts index c7d25287..ba998bd0 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts @@ -10,7 +10,7 @@ export class DeviceRegisterEventHandler implements PacketEventHandler { constructor(private readonly deviceRepository: DeviceRepository) {} async handle(message: PacketMessage<'DEVICE_REGISTER_REQ'>): Promise { - const data = message.packet.payload.object; + const data = message.packet.payload.data; const device = new Device({ id: ID.generate(), userId: ID.generate(), diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index 2dd0e749..92c03ae9 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -14,7 +14,7 @@ export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; const deviceSettings = new DeviceSettings({ voice: this.deviceVoiceMapper.toDomain({ isEnabled: data.voice.voiceMode, diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts index 370dd53e..19eeef52 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts @@ -11,7 +11,7 @@ export class DeviceVersionUpdateEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - const data = message.packet.payload.object; + const data = message.packet.payload.data; message.device.updateVersion(new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion })); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts index 96b705f0..b4cccff1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts @@ -11,7 +11,7 @@ export class DeviceWlanUpdateEventHandler implements PacketEventHandler { throw new DomainException('Device not found'); } - const data = message.packet.payload.object.body; + const data = message.packet.payload.data.body; message.device.updateWlan( new DeviceWlan({ diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts index e183361b..72ed16b4 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -4,7 +4,7 @@ import type { PacketConnection } from './aggregate-roots/packet-connection.aggre import type { PacketConnectionFactory } from './factories/connection.factory'; import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; import type { DeviceRepository, Device, Connection, ConnectionRepository } from '@agnoc/domain'; -import type { PacketServer, Packet, PayloadObjectName } from '@agnoc/transport-tcp'; +import type { PacketServer, Packet, PayloadDataName } from '@agnoc/transport-tcp'; export class PackerServerConnectionHandler { private readonly servers = new Map>(); @@ -55,21 +55,21 @@ export class PackerServerConnectionHandler { }); } - private async emitPacketEvent(message: PacketMessage) { - const name = message.packet.payload.opcode.name as PayloadObjectName; + private async emitPacketEvent(message: PacketMessage) { + const name = message.packet.payload.opcode.name as PayloadDataName; const sequence = message.packet.sequence.toString(); this.checkForPacketEventHandler(name); // Emit the packet event by the sequence string. // This is used to wait for a response from a packet. - await this.packetEventBus.emit(sequence, message as PacketEventBusEvents[PayloadObjectName]); + await this.packetEventBus.emit(sequence, message as PacketEventBusEvents[PayloadDataName]); // Emit the packet event by the opcode name. - await this.packetEventBus.emit(name, message as PacketEventBusEvents[PayloadObjectName]); + await this.packetEventBus.emit(name, message as PacketEventBusEvents[PayloadDataName]); } - private checkForPacketEventHandler(event: PayloadObjectName) { + private checkForPacketEventHandler(event: PayloadDataName) { const count = this.packetEventBus.listenerCount(event); // Throw an error if there is no event handler for the packet event. diff --git a/packages/adapter-tcp/src/packet.event-bus.ts b/packages/adapter-tcp/src/packet.event-bus.ts index d1a2109d..f5387f2d 100644 --- a/packages/adapter-tcp/src/packet.event-bus.ts +++ b/packages/adapter-tcp/src/packet.event-bus.ts @@ -1,10 +1,10 @@ import { EventBus } from '@agnoc/toolkit'; import type { PacketMessage } from './packet.message'; -import type { PayloadObjectName } from '@agnoc/transport-tcp'; +import type { PayloadDataName } from '@agnoc/transport-tcp'; /** Events for the packet event bus. */ export type PacketEventBusEvents = { - [Name in PayloadObjectName]: PacketMessage; + [Name in PayloadDataName]: PacketMessage; } & { [key: string]: PacketMessage; }; diff --git a/packages/adapter-tcp/src/packet.event-handler.ts b/packages/adapter-tcp/src/packet.event-handler.ts index 50974cf5..4d8bbbbe 100644 --- a/packages/adapter-tcp/src/packet.event-handler.ts +++ b/packages/adapter-tcp/src/packet.event-handler.ts @@ -1,11 +1,11 @@ import type { PacketMessage } from './packet.message'; import type { EventHandler } from '@agnoc/toolkit'; -import type { PayloadObjectName } from '@agnoc/transport-tcp'; +import type { PayloadDataName } from '@agnoc/transport-tcp'; /** Base class for packet event handlers. */ export abstract class PacketEventHandler implements EventHandler { /** The name of the event to listen to. */ - abstract forName: PayloadObjectName; + abstract forName: PayloadDataName; /** Handle the event. */ abstract handle(message: PacketMessage): void; diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts index b3e7d9fc..8bdda9bf 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/packet.message.ts @@ -1,19 +1,19 @@ import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; import type { Device } from '@agnoc/domain'; -import type { Packet, PayloadObjectFrom, PayloadObjectName } from '@agnoc/transport-tcp'; +import type { Packet, PayloadDataFrom, PayloadDataName } from '@agnoc/transport-tcp'; -export class PacketMessage { +export class PacketMessage { constructor(readonly connection: PacketConnection, readonly packet: Packet) {} get device(): Device | undefined { return this.connection.device; } - respond(name: Name, object: PayloadObjectFrom): Promise { + respond(name: Name, object: PayloadDataFrom): Promise { return this.connection.respond(name, object, this.packet); } - respondAndWait(name: Name, object: PayloadObjectFrom): Promise { + respondAndWait(name: Name, object: PayloadDataFrom): Promise { return this.connection.respondAndWait(name, object, this.packet); } } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index faafdbbd..ae68c898 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -5,7 +5,7 @@ import { PacketMapper, PacketServer, PayloadMapper, - PayloadObjectParserService, + PayloadDataParserService, PacketFactory, } from '@agnoc/transport-tcp'; import { LocateDeviceEventHandler } from './command-handlers/locate-device.event-handler'; @@ -59,7 +59,7 @@ export class TCPServer implements Server { private readonly commandQueryHandlerRegistry: TaskHandlerRegistry, ) { // Packet foundation - const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); + const payloadMapper = new PayloadMapper(new PayloadDataParserService(getProtobufRoot(), getCustomDecoders())); const packetMapper = new PacketMapper(payloadMapper); const packetFactory = new PacketFactory(); diff --git a/packages/adapter-tcp/test/integration/tcp.server.test.ts b/packages/adapter-tcp/test/integration/tcp.server.test.ts index 8e397c95..c5f3d057 100644 --- a/packages/adapter-tcp/test/integration/tcp.server.test.ts +++ b/packages/adapter-tcp/test/integration/tcp.server.test.ts @@ -10,7 +10,7 @@ import { PacketMapper, PacketSocket, PayloadMapper, - PayloadObjectParserService, + PayloadDataParserService, } from '@agnoc/transport-tcp'; import { expect } from 'chai'; import { TCPServer } from '@agnoc/adapter-tcp'; @@ -48,7 +48,7 @@ describe('Integration', function () { ); // Client blocks - const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); + const payloadMapper = new PayloadMapper(new PayloadDataParserService(getProtobufRoot(), getCustomDecoders())); const packetMapper = new PacketMapper(payloadMapper); packetSocket = new PacketSocket(packetMapper); @@ -79,7 +79,7 @@ describe('Integration', function () { expect(receivedPacket.deviceId.equals(sentPacket.deviceId)).to.be.true; expect(receivedPacket.userId.equals(sentPacket.userId)).to.be.true; expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); - expect(receivedPacket.payload.object).to.be.deep.equal({}); + expect(receivedPacket.payload.data).to.be.deep.equal({}); }); it('should handle heartbeat packets on map server', async function () { @@ -100,7 +100,7 @@ describe('Integration', function () { expect(receivedPacket.deviceId.equals(sentPacket.deviceId)).to.be.true; expect(receivedPacket.userId.equals(sentPacket.userId)).to.be.true; expect(receivedPacket.payload.opcode.value).to.be.equal('CLIENT_HEARTBEAT_RSP'); - expect(receivedPacket.payload.object).to.be.deep.equal({}); + expect(receivedPacket.payload.data).to.be.deep.equal({}); }); it('should handle ntp connections', async function () { @@ -117,8 +117,8 @@ describe('Integration', function () { expect(receivedPacket.deviceId.value).to.be.equal(0); expect(receivedPacket.userId.value).to.be.equal(0); expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_TIME_SYNC_RSP'); - expect(receivedPacket.payload.object.result).to.be.equal(0); - expect(receivedPacket.payload.object.body.time).to.be.greaterThanOrEqual(now); + expect(receivedPacket.payload.data.result).to.be.equal(0); + expect(receivedPacket.payload.data.body.time).to.be.greaterThanOrEqual(now); }); }); @@ -136,7 +136,7 @@ describe('Integration', function () { expect(receivedPacket.payload.opcode.value).to.be.equal('DEVICE_REGISTER_RSP'); - const device = await deviceRepository.findOneById(new ID(receivedPacket.payload.object.device.id)); + const device = await deviceRepository.findOneById(new ID(receivedPacket.payload.data.device.id)); expect(device).to.exist; }); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 5cec9e8a..dd344d82 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ import { - PayloadObjectParserService, + PayloadDataParserService, getProtobufRoot, PacketMapper, getCustomDecoders, @@ -33,7 +33,7 @@ export function main(): void { stderr: process.stderr, }; - const payloadMapper = new PayloadMapper(new PayloadObjectParserService(getProtobufRoot(), getCustomDecoders())); + const payloadMapper = new PayloadMapper(new PayloadDataParserService(getProtobufRoot(), getCustomDecoders())); const packetMapper = new PacketMapper(payloadMapper); const decodeCommand = new DecodeCommand(stdio, packetMapper); const readCommand = new ReadCommand(stdio, packetMapper); diff --git a/packages/cli/src/commands/decode.command.test.ts b/packages/cli/src/commands/decode.command.test.ts index f4519a74..cd11297e 100644 --- a/packages/cli/src/commands/decode.command.test.ts +++ b/packages/cli/src/commands/decode.command.test.ts @@ -47,7 +47,7 @@ describe('DecodeCommand', function () { const [ret] = await readStream(stdio.stdout, 'utf8'); expect(ret).to.equal( - '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","object":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', + '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","data":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', ); verify(packetMapper.toDomain(deepEqual(buffer))).once(); @@ -73,7 +73,7 @@ describe('DecodeCommand', function () { sequence: 'fb3dd1ebc0e6c58f', payload: { opcode: 'DEVICE_GETTIME_RSP', - object: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }, }, ]); @@ -91,7 +91,7 @@ describe('DecodeCommand', function () { const [ret] = await readStream(stdio.stdout, 'utf8'); expect(ret).to.equal( - '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","object":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', + '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","data":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', ); verify(packetMapper.toDomain(deepEqual(buffer))).once(); @@ -115,7 +115,7 @@ describe('DecodeCommand', function () { sequence: 'fb3dd1ebc0e6c58f', payload: { opcode: 'DEVICE_GETTIME_RSP', - object: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }, }, ]); diff --git a/packages/cli/src/commands/encode.command.test.ts b/packages/cli/src/commands/encode.command.test.ts index f4077d24..04985700 100644 --- a/packages/cli/src/commands/encode.command.test.ts +++ b/packages/cli/src/commands/encode.command.test.ts @@ -61,7 +61,7 @@ describe('EncodeCommand', function () { sequence: PacketSequence.fromString(jsonPacket.sequence), payload: new Payload({ opcode: OPCode.fromName(jsonPacket.payload.opcode), - object: jsonPacket.payload.object, + data: jsonPacket.payload.data, }), }), ), @@ -91,7 +91,7 @@ describe('EncodeCommand', function () { sequence: PacketSequence.fromString(jsonPacket.sequence), payload: new Payload({ opcode: OPCode.fromName(jsonPacket.payload.opcode), - object: jsonPacket.payload.object, + data: jsonPacket.payload.data, }), }), ), diff --git a/packages/cli/src/commands/read.command.test.ts b/packages/cli/src/commands/read.command.test.ts index 4dfd208b..1561b781 100644 --- a/packages/cli/src/commands/read.command.test.ts +++ b/packages/cli/src/commands/read.command.test.ts @@ -35,10 +35,10 @@ describe('ReadCommand', function () { const [ret1, ret2] = await readStream(stdio.stdout, 'utf8'); expect(ret1).to.equal( - '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","object":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', + '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","data":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', ); expect(ret2).to.equal( - '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","object":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', + '[fb3dd1ebc0e6c58f] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","data":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}\n', ); verify(packetMapper.toDomain(anything())).twice(); @@ -63,7 +63,7 @@ describe('ReadCommand', function () { sequence: 'fb3dd1ebc0e6c58f', payload: { opcode: 'DEVICE_GETTIME_RSP', - object: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }, }, { @@ -74,7 +74,7 @@ describe('ReadCommand', function () { sequence: 'fb3dd1ebc0e6c58f', payload: { opcode: 'DEVICE_GETTIME_RSP', - object: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: +0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }, }, ]); diff --git a/packages/cli/src/streams/packet-encode-transform.stream.test.ts b/packages/cli/src/streams/packet-encode-transform.stream.test.ts index ab3d15c4..18e98cb9 100644 --- a/packages/cli/src/streams/packet-encode-transform.stream.test.ts +++ b/packages/cli/src/streams/packet-encode-transform.stream.test.ts @@ -39,7 +39,7 @@ describe('PacketEncodeTransform', function () { sequence: PacketSequence.fromString(jsonPacket.sequence), payload: new Payload({ opcode: OPCode.fromName(jsonPacket.payload.opcode), - object: jsonPacket.payload.object, + data: jsonPacket.payload.data, }), }), ), diff --git a/packages/cli/src/streams/packet-encode-transform.stream.ts b/packages/cli/src/streams/packet-encode-transform.stream.ts index 08976960..acf0407e 100644 --- a/packages/cli/src/streams/packet-encode-transform.stream.ts +++ b/packages/cli/src/streams/packet-encode-transform.stream.ts @@ -1,7 +1,7 @@ import { Transform } from 'stream'; import { ID } from '@agnoc/toolkit'; import { OPCode, Packet, PacketSequence, Payload } from '@agnoc/transport-tcp'; -import type { PayloadObjectName, PacketProps, JSONPayload, PacketMapper } from '@agnoc/transport-tcp'; +import type { PayloadDataName, PacketProps, JSONPayload, PacketMapper } from '@agnoc/transport-tcp'; import type { TransformCallback } from 'stream'; export class PacketEncodeTransform extends Transform { @@ -16,21 +16,21 @@ export class PacketEncodeTransform extends Transform { done(); } - buildPacketFromJSON(serialized: JSONPacket): Packet { + buildPacketFromJSON(serialized: JSONPacket): Packet { const props: PacketProps = { ctype: serialized.ctype, flow: serialized.flow, deviceId: new ID(serialized.deviceId), userId: new ID(serialized.userId), sequence: PacketSequence.fromString(serialized.sequence), - payload: new Payload({ opcode: OPCode.fromName(serialized.payload.opcode), object: serialized.payload.object }), + payload: new Payload({ opcode: OPCode.fromName(serialized.payload.opcode), data: serialized.payload.data }), }; return new Packet(props); } } -export interface JSONPacket { +export interface JSONPacket { ctype: number; flow: number; deviceId: number; diff --git a/packages/cli/src/test-support.ts b/packages/cli/src/test-support.ts index 3e7b7943..98d638d2 100644 --- a/packages/cli/src/test-support.ts +++ b/packages/cli/src/test-support.ts @@ -9,7 +9,7 @@ export function givenAJSONPacket(): JSONPacket<'DEVICE_GETTIME_RSP'> { sequence: '7a479a0fbb978c12', payload: { opcode: 'DEVICE_GETTIME_RSP', - object: { + data: { result: 1, body: { deviceTime: 2, diff --git a/packages/transport-tcp/src/constants/payloads.constant.ts b/packages/transport-tcp/src/constants/payloads.constant.ts index 90518013..26d383bc 100644 --- a/packages/transport-tcp/src/constants/payloads.constant.ts +++ b/packages/transport-tcp/src/constants/payloads.constant.ts @@ -99,7 +99,7 @@ import type { IUSER_SET_DEVICE_QUIETHOURS_RSP, } from '@agnoc/schemas-tcp'; -interface PayloadObjectMap { +interface PayloadDataMap { CLIENT_HEARTBEAT_REQ: ICLIENT_HEARTBEAT_REQ; CLIENT_HEARTBEAT_RSP: ICLIENT_HEARTBEAT_RSP; CLIENT_IDLE_TIMEOUT: ICLIENT_IDLE_TIMEOUT; @@ -202,6 +202,6 @@ interface PayloadObjectMap { USER_SET_DEVICE_QUIETHOURS_RSP: IUSER_SET_DEVICE_QUIETHOURS_RSP; } -export type PayloadObject = PayloadObjectMap[PayloadObjectName]; -export type PayloadObjectName = keyof PayloadObjectMap & OPNameLiteral; -export type PayloadObjectFrom = PayloadObjectMap[Name]; +export type PayloadData = PayloadDataMap[PayloadDataName]; +export type PayloadDataName = keyof PayloadDataMap & OPNameLiteral; +export type PayloadDataFrom = PayloadDataMap[Name]; diff --git a/packages/transport-tcp/src/factories/packet.factory.test.ts b/packages/transport-tcp/src/factories/packet.factory.test.ts index 21a33ace..c541430f 100644 --- a/packages/transport-tcp/src/factories/packet.factory.test.ts +++ b/packages/transport-tcp/src/factories/packet.factory.test.ts @@ -24,7 +24,7 @@ describe('PacketFactory', function () { expect(packet.deviceId).to.equal(props.userId); expect(packet.sequence).to.be.instanceOf(PacketSequence); expect(packet.payload.opcode.name).to.equal('DEVICE_GETTIME_RSP'); - expect(packet.payload.object).to.equal(object); + expect(packet.payload.data).to.equal(object); }); it('should create a packet from another packet', function () { @@ -40,6 +40,6 @@ describe('PacketFactory', function () { expect(packet.deviceId).to.equal(sourcePacketProps.userId); expect(packet.sequence).to.be.equal(sourcePacketProps.sequence); expect(packet.payload.opcode.name).to.equal('DEVICE_GETTIME_RSP'); - expect(packet.payload.object).to.equal(object); + expect(packet.payload.data).to.equal(object); }); }); diff --git a/packages/transport-tcp/src/factories/packet.factory.ts b/packages/transport-tcp/src/factories/packet.factory.ts index 45f74d7a..9879a79a 100644 --- a/packages/transport-tcp/src/factories/packet.factory.ts +++ b/packages/transport-tcp/src/factories/packet.factory.ts @@ -2,7 +2,7 @@ import { OPCode } from '../domain-primitives/opcode.domain-primitive'; import { PacketSequence } from '../domain-primitives/packet-sequence.domain-primitive'; import { Packet } from '../value-objects/packet.value-object'; import { Payload } from '../value-objects/payload.value-object'; -import type { PayloadObjectFrom, PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataFrom, PayloadDataName } from '../constants/payloads.constant'; import type { Factory, ID } from '@agnoc/toolkit'; /** The props to create a packet. */ @@ -19,15 +19,15 @@ export class PacketFactory implements Factory { * To use this method you must provider a `deviceId` and `userId` in the `props` object, * or a `Packet` object in the `packet` argument to copy the packet props from. */ - create( + create( name: Name, - object: PayloadObjectFrom, + object: PayloadDataFrom, props: CreatePacketProps, ): Packet; - create(name: Name, object: PayloadObjectFrom, packet: Packet): Packet; - create( + create(name: Name, object: PayloadDataFrom, packet: Packet): Packet; + create( name: Name, - object: PayloadObjectFrom, + object: PayloadDataFrom, propsOrPacket: CreatePacketProps | Packet, ): Packet { if (propsOrPacket instanceof Packet) { @@ -38,7 +38,7 @@ export class PacketFactory implements Factory { userId: propsOrPacket.deviceId, deviceId: propsOrPacket.userId, sequence: propsOrPacket.sequence, - payload: new Payload({ opcode: OPCode.fromName(name), object }), + payload: new Payload({ opcode: OPCode.fromName(name), data: object }), }); } @@ -49,7 +49,7 @@ export class PacketFactory implements Factory { userId: propsOrPacket.deviceId, deviceId: propsOrPacket.userId, sequence: PacketSequence.generate(), - payload: new Payload({ opcode: OPCode.fromName(name), object }), + payload: new Payload({ opcode: OPCode.fromName(name), data: object }), }); } } diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts index 835156b7..f159992b 100644 --- a/packages/transport-tcp/src/index.ts +++ b/packages/transport-tcp/src/index.ts @@ -12,7 +12,7 @@ export * from './mappers/packet.mapper'; export * from './mappers/payload.mapper'; export * from './packet.server'; export * from './packet.socket'; -export * from './services/payload-object-parser.service'; +export * from './services/payload-data-parser.service'; export * from './utils/get-custom-decoders.util'; export * from './utils/get-protobuf-root.util'; export * from './value-objects/packet.value-object'; diff --git a/packages/transport-tcp/src/mappers/packet.mapper.test.ts b/packages/transport-tcp/src/mappers/packet.mapper.test.ts index 118c741f..eca05bda 100644 --- a/packages/transport-tcp/src/mappers/packet.mapper.test.ts +++ b/packages/transport-tcp/src/mappers/packet.mapper.test.ts @@ -84,7 +84,7 @@ describe('PacketMapper', function () { sequence: PacketSequence.fromString('7a479a0fbb978c12'), payload: new Payload({ opcode: OPCode.fromName('DEVICE_GETTIME_RSP'), - object: { result: 1, body: { deviceTime: 1 } }, + data: { result: 1, body: { deviceTime: 1 } }, }), }); diff --git a/packages/transport-tcp/src/mappers/packet.mapper.ts b/packages/transport-tcp/src/mappers/packet.mapper.ts index 39853b3f..38797039 100644 --- a/packages/transport-tcp/src/mappers/packet.mapper.ts +++ b/packages/transport-tcp/src/mappers/packet.mapper.ts @@ -16,7 +16,7 @@ import { OPCode } from '../domain-primitives/opcode.domain-primitive'; import { PacketSequence } from '../domain-primitives/packet-sequence.domain-primitive'; import { Packet } from '../value-objects/packet.value-object'; import type { PayloadMapper } from './payload.mapper'; -import type { PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataName } from '../constants/payloads.constant'; import type { Mapper } from '@agnoc/toolkit'; /** Mapper for converting packets to and from buffers. */ @@ -24,7 +24,7 @@ export class PacketMapper implements Mapper { constructor(private readonly payloadMapper: PayloadMapper) {} /** Converts a buffer to a packet. */ - toDomain(data: Buffer): Packet { + toDomain(data: Buffer): Packet { const stream = toStream(data); const size = readWord(stream); diff --git a/packages/transport-tcp/src/mappers/payload.mapper.test.ts b/packages/transport-tcp/src/mappers/payload.mapper.test.ts index bc6a0313..91e9a63e 100644 --- a/packages/transport-tcp/src/mappers/payload.mapper.test.ts +++ b/packages/transport-tcp/src/mappers/payload.mapper.test.ts @@ -4,19 +4,19 @@ import { expect } from 'chai'; import { OPCode } from '../domain-primitives/opcode.domain-primitive'; import { Payload } from '../value-objects/payload.value-object'; import { PayloadMapper } from './payload.mapper'; -import type { Decoder, Encoder, PayloadObjectParserService } from '../services/payload-object-parser.service'; +import type { Decoder, Encoder, PayloadDataParserService } from '../services/payload-data-parser.service'; describe('PayloadMapper', function () { let decoder: Decoder; let encoder: Encoder; - let payloadObjectParserService: PayloadObjectParserService; + let payloadDataParserService: PayloadDataParserService; let payloadMapper: PayloadMapper; beforeEach(function () { decoder = fnmock(); encoder = fnmock(); - payloadObjectParserService = imock(); - payloadMapper = new PayloadMapper(instance(payloadObjectParserService)); + payloadDataParserService = imock(); + payloadMapper = new PayloadMapper(instance(payloadDataParserService)); }); describe('#toDomain()', function () { @@ -25,14 +25,14 @@ describe('PayloadMapper', function () { const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); const object = { foo: 'bar' }; - when(payloadObjectParserService.getDecoder(anything())).thenReturn(instance(decoder)); + when(payloadDataParserService.getDecoder(anything())).thenReturn(instance(decoder)); when(decoder(anything())).thenReturn(object); const payload = payloadMapper.toDomain(buffer, opcode); expect(payload).to.be.instanceOf(Payload); expect(payload.opcode).to.equal(opcode); - expect(payload.object).to.equal(object); + expect(payload.data).to.equal(object); verify(decoder(buffer)).once(); }); @@ -41,7 +41,7 @@ describe('PayloadMapper', function () { const buffer = Buffer.from('test'); const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); - when(payloadObjectParserService.getDecoder(anything())).thenReturn(undefined); + when(payloadDataParserService.getDecoder(anything())).thenReturn(undefined); expect(() => payloadMapper.toDomain(buffer, opcode)).to.throw( ArgumentInvalidException, @@ -54,10 +54,10 @@ describe('PayloadMapper', function () { it('should create a buffer from a payload', function () { const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); const object = { foo: 'bar' }; - const payload = new Payload({ opcode, object }); + const payload = new Payload({ opcode, data: object }); const buffer = Buffer.from('test'); - when(payloadObjectParserService.getEncoder(anything())).thenReturn(instance(encoder)); + when(payloadDataParserService.getEncoder(anything())).thenReturn(instance(encoder)); when(encoder(anything())).thenReturn(buffer); const ret = payloadMapper.fromDomain(payload); @@ -70,13 +70,13 @@ describe('PayloadMapper', function () { it('should throw an error when the encoder does not exist', function () { const opcode = OPCode.fromName('CLIENT_HEARTBEAT_REQ'); const object = { foo: 'bar' }; - const payload = new Payload({ opcode, object }); + const payload = new Payload({ opcode, data: object }); - when(payloadObjectParserService.getEncoder(anything())).thenReturn(undefined); + when(payloadDataParserService.getEncoder(anything())).thenReturn(undefined); expect(() => payloadMapper.fromDomain(payload)).to.throw( ArgumentInvalidException, - `Encoder not found for opcode 'CLIENT_HEARTBEAT_REQ' while creating payload from object: {"foo":"bar"}`, + `Encoder not found for opcode 'CLIENT_HEARTBEAT_REQ' while creating payload from data: {"foo":"bar"}`, ); }); }); diff --git a/packages/transport-tcp/src/mappers/payload.mapper.ts b/packages/transport-tcp/src/mappers/payload.mapper.ts index 6f41963d..ba8a6402 100644 --- a/packages/transport-tcp/src/mappers/payload.mapper.ts +++ b/packages/transport-tcp/src/mappers/payload.mapper.ts @@ -1,15 +1,15 @@ import { ArgumentInvalidException } from '@agnoc/toolkit'; import { Payload } from '../value-objects/payload.value-object'; -import type { PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataName } from '../constants/payloads.constant'; import type { OPCode } from '../domain-primitives/opcode.domain-primitive'; -import type { PayloadObjectParserService } from '../services/payload-object-parser.service'; +import type { PayloadDataParserService } from '../services/payload-data-parser.service'; import type { Mapper } from '@agnoc/toolkit'; export class PayloadMapper implements Mapper { - constructor(private readonly payloadObjectParserService: PayloadObjectParserService) {} + constructor(private readonly payloadDataParserService: PayloadDataParserService) {} - toDomain(buffer: Buffer, opcode: OPCode): Payload { - const decoder = this.payloadObjectParserService.getDecoder(opcode.name as Name); + toDomain(buffer: Buffer, opcode: OPCode): Payload { + const decoder = this.payloadDataParserService.getDecoder(opcode.name as Name); if (!decoder) { throw new ArgumentInvalidException( @@ -17,25 +17,25 @@ export class PayloadMapper implements Mapper { ); } - const object = decoder(buffer); + const data = decoder(buffer); return new Payload({ opcode, - object, + data, }); } - fromDomain(payload: Payload): Buffer { - const encoder = this.payloadObjectParserService.getEncoder(payload.opcode.name as Name); + fromDomain(payload: Payload): Buffer { + const encoder = this.payloadDataParserService.getEncoder(payload.opcode.name as Name); if (!encoder) { throw new ArgumentInvalidException( - `Encoder not found for opcode '${payload.opcode.name}' while creating payload from object: ${JSON.stringify( - payload.object, + `Encoder not found for opcode '${payload.opcode.name}' while creating payload from data: ${JSON.stringify( + payload.data, )}`, ); } - return encoder(payload.object); + return encoder(payload.data); } } diff --git a/packages/transport-tcp/src/services/payload-object-parser.service.test.ts b/packages/transport-tcp/src/services/payload-data-parser.service.test.ts similarity index 82% rename from packages/transport-tcp/src/services/payload-object-parser.service.test.ts rename to packages/transport-tcp/src/services/payload-data-parser.service.test.ts index 42b7cee9..c7f821f0 100644 --- a/packages/transport-tcp/src/services/payload-object-parser.service.test.ts +++ b/packages/transport-tcp/src/services/payload-data-parser.service.test.ts @@ -1,19 +1,19 @@ import { ArgumentInvalidException } from '@agnoc/toolkit'; import { anything, fnmock, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { PayloadObjectParserService } from './payload-object-parser.service'; -import type { Decoder, DecoderMap, Encoder, EncoderMap } from './payload-object-parser.service'; -import type { PayloadObject } from '../constants/payloads.constant'; +import { PayloadDataParserService } from './payload-data-parser.service'; +import type { Decoder, DecoderMap, Encoder, EncoderMap } from './payload-data-parser.service'; +import type { PayloadData } from '../constants/payloads.constant'; import type { MapInfo } from '../decoders/map.interface'; import type { Root, Type, Message, Writer } from 'protobufjs/light'; -describe('PayloadObjectParserService', function () { - let service: PayloadObjectParserService; +describe('PayloadDataParserService', function () { + let service: PayloadDataParserService; let protoRoot: Root; let protoType: Type; let protoMessage: Message; let protoWriter: Writer; - let payloadObject: PayloadObject; + let payloadData: PayloadData; let buffer: Buffer; let customDecoders: Partial; let customDecoder: Decoder; @@ -25,26 +25,26 @@ describe('PayloadObjectParserService', function () { protoType = imock(); protoMessage = imock(); protoWriter = imock(); - payloadObject = instance(imock()); + payloadData = instance(imock()); buffer = Buffer.from('example'); customDecoder = fnmock(); customDecoders = { DEVICE_MAPID_GET_GLOBAL_INFO_RSP: instance(customDecoder) }; customEncoder = fnmock(); customEncoders = { DEVICE_MAPID_GET_GLOBAL_INFO_RSP: instance(customEncoder) }; - service = new PayloadObjectParserService(instance(protoRoot), customDecoders, customEncoders); + service = new PayloadDataParserService(instance(protoRoot), customDecoders, customEncoders); }); describe('#getDecoder()', function () { it('should return a decoder function from the protobuf schema', function () { when(protoRoot.get(anything())).thenReturn(instance(protoType)); when(protoType.decode(anything())).thenReturn(instance(protoMessage)); - when(protoType.toObject(anything())).thenReturn(payloadObject); + when(protoType.toObject(anything())).thenReturn(payloadData); when(customDecoder(anything())).thenReturn({}); const fn = service.getDecoder('CLIENT_HEARTBEAT_REQ'); const ret = fn?.(buffer); - expect(ret).to.equal(payloadObject); + expect(ret).to.equal(payloadData); verify(protoRoot.get('CLIENT_HEARTBEAT_REQ')).once(); verify(protoType.decode(buffer)).once(); @@ -55,12 +55,12 @@ describe('PayloadObjectParserService', function () { when(protoRoot.get(anything())).thenReturn(null); when(protoType.decode(anything())).thenReturn(instance(protoMessage)); when(protoType.toObject(anything())).thenReturn({}); - when(customDecoder(anything())).thenReturn(payloadObject); + when(customDecoder(anything())).thenReturn(payloadData); const fn = service.getDecoder('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'); const ret = fn?.(buffer); - expect(ret).to.equal(payloadObject); + expect(ret).to.equal(payloadData); verify(protoRoot.get('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); verify(protoType.decode(anything())).never(); @@ -78,14 +78,14 @@ describe('PayloadObjectParserService', function () { when(customEncoder(anything())).thenReturn(Buffer.alloc(0)); const fn = service.getEncoder('CLIENT_HEARTBEAT_REQ'); - const ret = fn?.(payloadObject); + const ret = fn?.(payloadData); expect(ret).to.exist; expect(Buffer.compare(ret as Buffer, buffer)).to.equal(0); verify(protoRoot.get('CLIENT_HEARTBEAT_REQ')).once(); - verify(protoType.verify(payloadObject)).once(); - verify(protoType.create(payloadObject)).once(); + verify(protoType.verify(payloadData)).once(); + verify(protoType.create(payloadData)).once(); verify(customEncoder(anything())).never(); }); @@ -98,13 +98,13 @@ describe('PayloadObjectParserService', function () { when(customEncoder(anything())).thenReturn(Buffer.alloc(0)); const fn = service.getEncoder('CLIENT_HEARTBEAT_REQ'); - expect(() => fn?.(payloadObject)).to.throw( + expect(() => fn?.(payloadData)).to.throw( ArgumentInvalidException, `Cannot encode a payload for opcode 'CLIENT_HEARTBEAT_REQ' for object 'null': Verify error`, ); verify(protoRoot.get('CLIENT_HEARTBEAT_REQ')).once(); - verify(protoType.verify(payloadObject)).once(); + verify(protoType.verify(payloadData)).once(); verify(protoType.create(anything())).never(); verify(customEncoder(anything())).never(); }); @@ -118,7 +118,7 @@ describe('PayloadObjectParserService', function () { when(customEncoder(anything())).thenReturn(buffer); const fn = service.getEncoder('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'); - const ret = fn?.(payloadObject as MapInfo); + const ret = fn?.(payloadData as MapInfo); expect(ret).to.exist; expect(Buffer.compare(ret as Buffer, buffer)).to.equal(0); @@ -126,7 +126,7 @@ describe('PayloadObjectParserService', function () { verify(protoRoot.get('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); verify(protoType.verify(anything())).never(); verify(protoType.create(anything())).never(); - verify(customEncoder(payloadObject)).once(); + verify(customEncoder(payloadData)).once(); }); }); }); diff --git a/packages/transport-tcp/src/services/payload-object-parser.service.ts b/packages/transport-tcp/src/services/payload-data-parser.service.ts similarity index 63% rename from packages/transport-tcp/src/services/payload-object-parser.service.ts rename to packages/transport-tcp/src/services/payload-data-parser.service.ts index 06014243..baab9788 100644 --- a/packages/transport-tcp/src/services/payload-object-parser.service.ts +++ b/packages/transport-tcp/src/services/payload-data-parser.service.ts @@ -1,15 +1,15 @@ import { ArgumentInvalidException } from '@agnoc/toolkit'; -import type { PayloadObjectFrom, PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataFrom, PayloadDataName } from '../constants/payloads.constant'; import type { Root, Type } from 'protobufjs/light'; -export type Decoder = (buffer: Buffer) => PayloadObjectFrom; -export type Encoder = (object: PayloadObjectFrom) => Buffer; +export type Decoder = (buffer: Buffer) => PayloadDataFrom; +export type Encoder = (object: PayloadDataFrom) => Buffer; -export type EncoderMap = Record>; -export type DecoderMap = Record>; +export type EncoderMap = Record>; +export type DecoderMap = Record>; /** Service to encde and decode payload objects. */ -export class PayloadObjectParserService { +export class PayloadDataParserService { constructor( private readonly protoRoot: Root, private readonly decoders?: Partial, @@ -17,7 +17,7 @@ export class PayloadObjectParserService { ) {} /** Returns a decoder for a payload object. */ - getDecoder(name: Name): Decoder | undefined { + getDecoder(name: Name): Decoder | undefined { const schema = this.protoRoot.get(name) as Type | null; if (schema) { @@ -28,7 +28,7 @@ export class PayloadObjectParserService { } /** Returns an encoder for a payload object. */ - getEncoder(name: Name): Encoder | undefined { + getEncoder(name: Name): Encoder | undefined { const schema = this.protoRoot.get(name) as Type | null; if (schema) { @@ -38,7 +38,7 @@ export class PayloadObjectParserService { return this.encoders?.[name] as Encoder | undefined; } - private buildSchemaDecoder(name: PayloadObjectName, schema: Type): Decoder { + private buildSchemaDecoder(name: PayloadDataName, schema: Type): Decoder { const decoder = (buffer: Buffer) => { const message = schema.decode(buffer); @@ -50,7 +50,7 @@ export class PayloadObjectParserService { return decoder as Decoder; } - private buildSchemaEncoder(name: PayloadObjectName, schema: Type): Encoder { + private buildSchemaEncoder(name: PayloadDataName, schema: Type): Encoder { const encoder = (object: Buffer) => { const err = schema.verify(object); diff --git a/packages/transport-tcp/src/test-support.ts b/packages/transport-tcp/src/test-support.ts index 77f42c78..208dd7c0 100644 --- a/packages/transport-tcp/src/test-support.ts +++ b/packages/transport-tcp/src/test-support.ts @@ -8,7 +8,7 @@ import type { PayloadProps } from './value-objects/payload.value-object'; export function givenSomePayloadProps(): PayloadProps<'DEVICE_GETTIME_RSP'> { return { opcode: OPCode.fromName('DEVICE_GETTIME_RSP'), - object: { result: 0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: 0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }; } diff --git a/packages/transport-tcp/src/utils/get-custom-decoders.util.ts b/packages/transport-tcp/src/utils/get-custom-decoders.util.ts index d1d2837f..523f92a6 100644 --- a/packages/transport-tcp/src/utils/get-custom-decoders.util.ts +++ b/packages/transport-tcp/src/utils/get-custom-decoders.util.ts @@ -2,7 +2,7 @@ import { decodeArea } from '../decoders/area.decoder'; import { decodeChargePosition } from '../decoders/charge-position.decoder'; import { decodeMap } from '../decoders/map.decoder'; import { decodeRobotPosition } from '../decoders/robot-position.decoder'; -import type { DecoderMap } from '../services/payload-object-parser.service'; +import type { DecoderMap } from '../services/payload-data-parser.service'; const decoders: Partial = { DEVICE_MAPID_GET_GLOBAL_INFO_RSP: decodeMap, diff --git a/packages/transport-tcp/src/value-objects/packet.value-object.test.ts b/packages/transport-tcp/src/value-objects/packet.value-object.test.ts index cd232b82..e8626702 100644 --- a/packages/transport-tcp/src/value-objects/packet.value-object.test.ts +++ b/packages/transport-tcp/src/value-objects/packet.value-object.test.ts @@ -126,12 +126,12 @@ describe('Packet', function () { sequence: new PacketSequence(5n), payload: new Payload({ opcode: OPCode.fromName('DEVICE_GETTIME_RSP'), - object: { result: 0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, + data: { result: 0, body: { deviceTime: 1606129555, deviceTimezone: 3600 } }, }), }); expect(packet.toString()).to.be.equal( - '[5] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","object":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}', + '[5] [ctype: 2] [flow: 1] [userId: 4] [deviceId: 3] {"opcode":"DEVICE_GETTIME_RSP","data":{"result":0,"body":{"deviceTime":1606129555,"deviceTimezone":3600}}}', ); }); }); diff --git a/packages/transport-tcp/src/value-objects/packet.value-object.ts b/packages/transport-tcp/src/value-objects/packet.value-object.ts index af0eb03f..37f3eb03 100644 --- a/packages/transport-tcp/src/value-objects/packet.value-object.ts +++ b/packages/transport-tcp/src/value-objects/packet.value-object.ts @@ -1,10 +1,10 @@ import { ValueObject, ID } from '@agnoc/toolkit'; import { PacketSequence } from '../domain-primitives/packet-sequence.domain-primitive'; import { Payload } from './payload.value-object'; -import type { PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataName } from '../constants/payloads.constant'; /** Describes the properties of a packet. */ -export interface PacketProps { +export interface PacketProps { /** The packet type. */ ctype: number; /** The packet flow. */ @@ -20,7 +20,7 @@ export interface PacketProps { } /** Describes a packet. */ -export class Packet extends ValueObject> { +export class Packet extends ValueObject> { /** Returns the packet type. */ get ctype(): number { return this.props.ctype; diff --git a/packages/transport-tcp/src/value-objects/payload.value-object.test.ts b/packages/transport-tcp/src/value-objects/payload.value-object.test.ts index 1c72c48e..bfca9899 100644 --- a/packages/transport-tcp/src/value-objects/payload.value-object.test.ts +++ b/packages/transport-tcp/src/value-objects/payload.value-object.test.ts @@ -11,7 +11,7 @@ describe('Payload', function () { expect(payload).to.be.instanceOf(ValueObject); expect(payload.opcode).to.be.equal(payloadProps.opcode); - expect(payload.object).to.be.equal(payloadProps.object); + expect(payload.data).to.be.equal(payloadProps.data); }); it("should throw an error when 'opcode' is not provided", function () { @@ -24,9 +24,9 @@ describe('Payload', function () { it("should throw an error when 'object' is not provided", function () { // @ts-expect-error - missing property - expect(() => new Payload({ ...givenSomePayloadProps(), object: undefined })).to.throw( + expect(() => new Payload({ ...givenSomePayloadProps(), data: undefined })).to.throw( ArgumentNotProvidedException, - `Property 'object' for Payload not provided`, + `Property 'data' for Payload not provided`, ); }); @@ -38,11 +38,11 @@ describe('Payload', function () { ); }); - it('should throw an error when "object" is not an object', function () { + it('should throw an error when "data" is not an object', function () { // @ts-expect-error - invalid property - expect(() => new Payload({ ...givenSomePayloadProps(), object: 'foo' })).to.throw( + expect(() => new Payload({ ...givenSomePayloadProps(), data: 'foo' })).to.throw( ArgumentInvalidException, - `Value 'foo' for property 'object' of Payload is not an instance of Object`, + `Value 'foo' for property 'data' of Payload is not an instance of Object`, ); }); @@ -50,26 +50,26 @@ describe('Payload', function () { it('should return a string representation of the Payload', function () { const payload = new Payload<'DEVICE_MAPID_PUSH_MAP_INFO'>({ opcode: OPCode.fromName('DEVICE_MAPID_PUSH_MAP_INFO'), - object: { mask: 0, mapGrid: Buffer.from('example') }, + data: { mask: 0, mapGrid: Buffer.from('example') }, }); expect(payload.toString()).to.be.equal( - '{"opcode":"DEVICE_MAPID_PUSH_MAP_INFO","object":{"mask":0,"mapGrid":"[Buffer]"}}', + '{"opcode":"DEVICE_MAPID_PUSH_MAP_INFO","data":{"mask":0,"mapGrid":"[Buffer]"}}', ); }); }); describe('#toJSON()', function () { it('should return a JSON representation of the Payload', function () { - const object = { mask: 0, mapGrid: Buffer.from('example') }; + const data = { mask: 0, mapGrid: Buffer.from('example') }; const payload = new Payload<'DEVICE_MAPID_PUSH_MAP_INFO'>({ opcode: OPCode.fromName('DEVICE_MAPID_PUSH_MAP_INFO'), - object, + data, }); expect(payload.toJSON()).to.be.deep.equal({ opcode: 'DEVICE_MAPID_PUSH_MAP_INFO', - object, + data, }); }); }); diff --git a/packages/transport-tcp/src/value-objects/payload.value-object.ts b/packages/transport-tcp/src/value-objects/payload.value-object.ts index 413ef350..5ee1ed58 100644 --- a/packages/transport-tcp/src/value-objects/payload.value-object.ts +++ b/packages/transport-tcp/src/value-objects/payload.value-object.ts @@ -1,33 +1,33 @@ import { isObject, ValueObject } from '@agnoc/toolkit'; import { OPCode } from '../domain-primitives/opcode.domain-primitive'; -import type { PayloadObjectFrom, PayloadObjectName } from '../constants/payloads.constant'; +import type { PayloadDataFrom, PayloadDataName } from '../constants/payloads.constant'; /** Describes the properties of a payload. */ -export interface PayloadProps { +export interface PayloadProps { /** The opcode of the payload. */ opcode: OPCode; /** The object representation of the payload. */ - object: PayloadObjectFrom; + data: PayloadDataFrom; } /** Describes the JSON representation of a payload. */ -export interface JSONPayload { +export interface JSONPayload { /** The name of the opcode for the payload. */ opcode: Name; /** The object representation of the payload. */ - object: PayloadObjectFrom; + data: PayloadDataFrom; } /** Describes a payload. */ -export class Payload extends ValueObject> { +export class Payload extends ValueObject> { /** Returns the opcode of the payload. */ get opcode(): OPCode { return this.props.opcode; } /** Returns the object representation of the payload. */ - get object(): PayloadObjectFrom { - return this.props.object; + get data(): PayloadDataFrom { + return this.props.data; } /** Returns the string representation of the payload with some properties filtered. */ @@ -37,18 +37,18 @@ export class Payload extends /** Returns the JSON representation of the payload. */ override toJSON(): JSONPayload { - return { opcode: this.props.opcode.name as Name, object: this.props.object }; + return { opcode: this.props.opcode.name as Name, data: this.props.data }; } protected validate(props: PayloadProps): void { - const keys: (keyof PayloadProps)[] = ['opcode', 'object']; + const keys: (keyof PayloadProps)[] = ['opcode', 'data']; keys.forEach((prop) => { this.validateDefinedProp(props, prop); }); this.validateInstanceProp(props, 'opcode', OPCode); - this.validateInstanceProp(props, 'object', Object); + this.validateInstanceProp(props, 'data', Object); } } From ae0811ce4bf98149a8af1fae9e924ba239600c45 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Thu, 23 Mar 2023 17:46:41 +0100 Subject: [PATCH 18/38] test: raise coverage --- package.json | 16 +- .../packet-connection.aggregate-root.test.ts | 316 +++++ .../packet-connection.aggregate-root.ts | 45 +- .../locate-device.command-handler.test.ts | 51 + .../locate-device.command-handler.ts | 21 + .../locate-device.event-handler.ts | 31 - .../connection-device-updater.service.test.ts | 64 + .../src/connection-device-updater.service.ts | 29 + ...nected-event-handler.event-handler.test.ts | 45 + ...s-connected-event-handler.event-handler.ts | 17 +- ...locked-event-handler.event-handler.test.ts | 70 + ...e-is-locked-event-handler.event-handler.ts | 17 +- ...ction-device-changed.event-handler.test.ts | 100 ++ ...onnection-device-changed.event-handler.ts} | 9 +- .../src/factories/connection.factory.test.ts | 32 + packages/adapter-tcp/src/index.ts | 2 +- .../src/mappers/clean-mode.mapper.test.ts | 29 + .../src/mappers/device-battery.mapper.test.ts | 43 + .../src/mappers/device-error.mapper.test.ts | 31 + .../src/mappers/device-error.mapper.ts | 2 +- .../mappers/device-fan-speed.mapper.test.ts | 29 + .../src/mappers/device-mode.mapper.test.ts | 52 + .../src/mappers/device-mode.mapper.ts | 2 +- .../src/mappers/device-order.mapper.test.ts | 179 +++ .../src/mappers/device-state.mapper.test.ts | 76 ++ .../src/mappers/device-state.mapper.ts | 2 +- .../mappers/device-water-level.mapper.test.ts | 29 + .../src/mappers/voice-setting.mapper.test.ts | 39 + ...oice.mapper.ts => voice-setting.mapper.ts} | 2 +- .../src/mappers/week-day-list.mapper.test.ts | 10 +- .../packet-connection-finder.service.test.ts | 53 + .../src/packet-connection-finder.service.ts | 20 + .../client-heartbeat.event-handler.test.ts | 26 + .../client-login.event-handler.test.ts | 53 + ...evice-battery-update.event-handler.test.ts | 49 + .../device-battery-update.event-handler.ts | 5 +- .../device-settings-update.event-handler.ts | 4 +- .../packet-event-publisher.service.test.ts | 51 + .../src/packet-event-publisher.service.ts | 31 + .../packet-server.connection-handler.test.ts | 152 +++ .../src/packet-server.connection-handler.ts | 95 +- .../adapter-tcp/src/packet.message.test.ts | 119 ++ packages/adapter-tcp/src/packet.message.ts | 19 + packages/adapter-tcp/src/tcp.server.test.ts | 7 +- packages/adapter-tcp/src/tcp.server.ts | 26 +- .../find-device.query-handler.test.ts | 48 + .../src/queries/find-device-query.test.ts | 60 + packages/eslint-config/package.json | 8 +- packages/schemas-tcp/src/index.proto | 2 +- .../transport-tcp/src/packet.socket.test.ts | 24 +- packages/transport-tcp/src/packet.socket.ts | 3 +- tsconfig.json | 1 + yarn.lock | 1202 +++++++++-------- 53 files changed, 2726 insertions(+), 722 deletions(-) create mode 100644 packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts delete mode 100644 packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts create mode 100644 packages/adapter-tcp/src/connection-device-updater.service.test.ts create mode 100644 packages/adapter-tcp/src/connection-device-updater.service.ts create mode 100644 packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts rename packages/adapter-tcp/src/domain-event-handlers/{set-device-connected-when-connection-device-changed.domain-event.ts => set-device-connected-when-connection-device-changed.event-handler.ts} (67%) create mode 100644 packages/adapter-tcp/src/factories/connection.factory.test.ts create mode 100644 packages/adapter-tcp/src/mappers/clean-mode.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-battery.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-error.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-fan-speed.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-mode.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-order.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-state.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/device-water-level.mapper.test.ts create mode 100644 packages/adapter-tcp/src/mappers/voice-setting.mapper.test.ts rename packages/adapter-tcp/src/mappers/{device-voice.mapper.ts => voice-setting.mapper.ts} (91%) create mode 100644 packages/adapter-tcp/src/packet-connection-finder.service.test.ts create mode 100644 packages/adapter-tcp/src/packet-connection-finder.service.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-publisher.service.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-publisher.service.ts create mode 100644 packages/adapter-tcp/src/packet-server.connection-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet.message.test.ts create mode 100644 packages/core/src/query-handlers/find-device.query-handler.test.ts create mode 100644 packages/domain/src/queries/find-device-query.test.ts diff --git a/package.json b/package.json index 0b45e952..fedf52e5 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "posttest": "sh ./scripts/merge-coverage.sh", "prepare": "husky install", "test": "npm-run-all 'test:*'", - "test:packages": "lerna run test", + "test:packages": "lerna run test --stream --concurrency=1", "version": "npm run lint:code:fix && npm run lint:style:fix" }, "config": { @@ -28,7 +28,7 @@ } }, "devDependencies": { - "@commitlint/cli": "^17.4.4", + "@commitlint/cli": "^17.5.0", "@commitlint/config-conventional": "^17.4.4", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@johanblumenberg/ts-mockito": "^1.0.35", @@ -38,23 +38,23 @@ "@types/debug": "^4.1.7", "@types/mocha": "^10.0.1", "@types/mock-fs": "^4.13.1", - "@types/node": "^18.14.2", + "@types/node": "^18.15.5", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", "commitizen": "^4.3.0", "cz-conventional-changelog": "^3.3.0", "husky": "^8.0.3", "json": "^11.0.0", - "lerna": "^6.5.1", - "lint-staged": ">=13.1.2", + "lerna": "^6.6.0", + "lint-staged": ">=13.2.0", "mocha": "^10.2.0", - "npm-check-updates": "^16.7.9", + "npm-check-updates": "^16.8.0", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", - "prettier": "2.8.4", + "prettier": "2.8.6", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2", - "typedoc": "^0.23.25", + "typedoc": "^0.23.28", "typedoc-plugin-resolve-crossmodule-references": "^0.3.3", "typescript": "^4.9.5", "yarn-deduplicate": "^6.0.1" diff --git a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts new file mode 100644 index 00000000..12beb90d --- /dev/null +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts @@ -0,0 +1,316 @@ +import { Socket, Server } from 'net'; +import { Connection, Device } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { ArgumentInvalidException, ArgumentNotProvidedException, DomainException, ID } from '@agnoc/toolkit'; +import { Packet, PacketSocket } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, defer, imock, instance, spy, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketConnection } from './packet-connection.aggregate-root'; +import type { PacketEventBus } from '../packet.event-bus'; +import type { PacketMessage } from '../packet.message'; +import type { PacketFactory, PacketMapper } from '@agnoc/transport-tcp'; +import type { AddressInfo } from 'net'; + +describe('PacketConnection', function () { + let packetFactory: PacketFactory; + let eventBus: PacketEventBus; + let packetMapper: PacketMapper; + let packetSocket: PacketSocket; + let packetSocketSpy: PacketSocket; + let packetMessage: PacketMessage; + let socket: Socket; + let server: Server; + + beforeEach(function () { + server = new Server(); + socket = new Socket(); + packetFactory = imock(); + eventBus = imock(); + packetMapper = imock(); + packetMessage = imock(); + packetSocket = new PacketSocket(instance(packetMapper), socket); + packetSocketSpy = spy(packetSocket); + }); + + it('should be created', function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + + expect(connection).to.be.instanceOf(Connection); + expect(connection.id).to.be.equal(props.id); + expect(connection.socket).to.be.equal(props.socket); + expect(connection.connectionType).to.be.equal('PACKET'); + expect(PacketConnection.isPacketConnection(connection)).to.be.true; + }); + + it("should throw an error when 'socket' is not provided", function () { + expect( + // @ts-expect-error - invalid property + () => new PacketConnection(instance(packetFactory), instance(eventBus), { id: ID.generate(), socket: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'socket' for PacketConnection not provided`); + }); + + it("should throw an error when 'socket' is not a PacketSocket", function () { + expect( + // @ts-expect-error - invalid property + () => new PacketConnection(instance(packetFactory), instance(eventBus), { id: ID.generate(), socket: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'socket' of PacketConnection is not an instance of PacketSocket`, + ); + }); + + describe('#send()', function () { + it('should do nothing', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + + await expect(connection.send('DEVICE_GETTIME_RSP', data)).to.be.rejectedWith( + DomainException, + 'Unable to send packet through a closed connection', + ); + + verify(packetFactory.create(anything(), anything(), anything())).never(); + verify(packetSocketSpy.write(anything())).never(); + }); + }); + + describe('#respond()', function () { + it('should do nothing', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const anotherPacket = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(anotherPacket); + + await expect(connection.respond('DEVICE_GETTIME_RSP', data, packet)).to.be.rejectedWith( + DomainException, + 'Unable to send packet through a closed connection', + ); + + verify(packetFactory.create(anything(), anything(), anything())).never(); + verify(packetSocketSpy.write(anything())).never(); + }); + }); + + describe('#sendAndWait()', function () { + it('should do nothing', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + + await expect(connection.sendAndWait('DEVICE_GETTIME_RSP', data)).to.be.rejectedWith( + DomainException, + 'Unable to send packet through a closed connection', + ); + + verify(packetFactory.create(anything(), anything(), anything())).never(); + verify(packetSocketSpy.write(anything())).never(); + }); + }); + + describe('#respondAndWait()', function () { + it('should do nothing', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + + await expect(connection.respondAndWait('DEVICE_GETTIME_RSP', data, packet)).to.be.rejectedWith( + DomainException, + 'Unable to send packet through a closed connection', + ); + + verify(packetFactory.create(anything(), anything(), anything())).never(); + verify(packetSocketSpy.write(anything())).never(); + }); + }); + + describe('#close()', function () { + it('should do nothing', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + + await connection.close(); + + verify(packetSocketSpy.end()).never(); + }); + }); + + describe('with socket connected', function () { + beforeEach(function (done) { + server.listen(0, () => { + socket.connect((server.address() as AddressInfo).port, done); + }); + }); + + afterEach(function (done) { + if (socket.readyState === 'open') { + socket.end(); + } + + if (server.listening) { + server.close(done); + } else { + done(); + } + }); + + describe('#send()', function () { + it('should send a packet', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + + await connection.send('DEVICE_GETTIME_RSP', data); + + verify( + packetFactory.create('DEVICE_GETTIME_RSP', data, deepEqual({ deviceId: new ID(0), userId: new ID(0) })), + ).once(); + verify(packetSocketSpy.write(packet)).once(); + }); + + it('should send a packet with a device attached', async function () { + const device = new Device(givenSomeDeviceProps()); + const props = { id: ID.generate(), socket: packetSocket, device }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + + await connection.send('DEVICE_GETTIME_RSP', data); + + verify( + packetFactory.create('DEVICE_GETTIME_RSP', data, deepEqual({ deviceId: device.id, userId: device.userId })), + ).once(); + verify(packetSocketSpy.write(packet)).once(); + }); + }); + + describe('#respond()', function () { + it('should respond with a packet', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const anotherPacket = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(anotherPacket); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + + await connection.respond('DEVICE_GETTIME_RSP', data, packet); + + verify(packetFactory.create('DEVICE_GETTIME_RSP', data, packet)).once(); + verify(packetSocketSpy.write(anotherPacket)).once(); + }); + }); + + describe('#sendAndWait()', function () { + it('should send a packet and wait for the response', function (done) { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + const eventPromise = defer(); + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + when(eventBus.once(anything())).thenReturn(eventPromise); + + void connection.sendAndWait('DEVICE_GETTIME_RSP', data).then((ret) => { + expect(ret).to.be.equal(instance(packetMessage)); + + verify( + packetFactory.create('DEVICE_GETTIME_RSP', data, deepEqual({ deviceId: new ID(0), userId: new ID(0) })), + ).once(); + verify(packetSocketSpy.write(packet)).once(); + verify(eventBus.once(packet.sequence.toString())).once(); + done(); + }); + + void eventPromise.resolve(instance(packetMessage)); + }); + + it('should send a packet with a device attached and wait for the response', function (done) { + const device = new Device(givenSomeDeviceProps()); + const props = { id: ID.generate(), socket: packetSocket, device }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + const eventPromise = defer(); + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(packet); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + when(eventBus.once(anything())).thenReturn(eventPromise); + + void connection.sendAndWait('DEVICE_GETTIME_RSP', data).then((ret) => { + expect(ret).to.be.equal(instance(packetMessage)); + + verify( + packetFactory.create('DEVICE_GETTIME_RSP', data, deepEqual({ deviceId: device.id, userId: device.userId })), + ).once(); + verify(packetSocketSpy.write(packet)).once(); + verify(eventBus.once(packet.sequence.toString())).once(); + done(); + }); + + void eventPromise.resolve(instance(packetMessage)); + }); + }); + + describe('#respondAndWait()', function () { + it('should respond to a packet and wait for the response', function (done) { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + const packet = new Packet(givenSomePacketProps()); + const anotherPacket = new Packet(givenSomePacketProps()); + const data = { result: 0, body: { deviceTime: 1 } }; + const eventPromise = defer(); + + when(packetFactory.create(anything(), anything(), anything())).thenReturn(anotherPacket); + when(packetMapper.fromDomain(anything())).thenReturn(Buffer.alloc(0)); + when(eventBus.once(anything())).thenReturn(eventPromise); + + void connection.respondAndWait('DEVICE_GETTIME_RSP', data, packet).then((ret) => { + expect(ret).to.be.equal(instance(packetMessage)); + + verify(packetFactory.create('DEVICE_GETTIME_RSP', data, packet)).once(); + verify(packetSocketSpy.write(anotherPacket)).once(); + verify(eventBus.once(packet.sequence.toString())).once(); + done(); + }); + + void eventPromise.resolve(instance(packetMessage)); + }); + }); + + describe('#close()', function () { + it('should close the socket', async function () { + const props = { id: ID.generate(), socket: packetSocket }; + const connection = new PacketConnection(instance(packetFactory), instance(eventBus), props); + + await connection.close(); + + verify(packetSocketSpy.end()).once(); + }); + }); + }); +}); diff --git a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts index f474091f..2678a165 100644 --- a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts @@ -1,5 +1,5 @@ import { Connection } from '@agnoc/domain'; -import { ID } from '@agnoc/toolkit'; +import { DomainException, ID } from '@agnoc/toolkit'; import { PacketSocket } from '@agnoc/transport-tcp'; import type { PacketEventBus } from '../packet.event-bus'; import type { PacketMessage } from '../packet.message'; @@ -25,37 +25,48 @@ export class PacketConnection extends Connection { return this.props.socket; } - send(name: Name, object: PayloadDataFrom): Promise { - const packet = this.packetFactory.create(name, object, this.getPacketProps()); + async send(name: Name, data: PayloadDataFrom): Promise { + this.validateConnectedSocket(); - return this.write(packet); + const packet = this.packetFactory.create(name, data, this.getPacketProps()); + + return this.socket.write(packet); } - respond(name: Name, object: PayloadDataFrom, packet: Packet): Promise { - return this.write(this.packetFactory.create(name, object, packet)); + async respond(name: Name, data: PayloadDataFrom, packet: Packet): Promise { + this.validateConnectedSocket(); + + return this.socket.write(this.packetFactory.create(name, data, packet)); } - sendAndWait(name: Name, object: PayloadDataFrom): Promise { - const packet = this.packetFactory.create(name, object, this.getPacketProps()); + async sendAndWait(name: Name, data: PayloadDataFrom): Promise { + this.validateConnectedSocket(); + + const packet = this.packetFactory.create(name, data, this.getPacketProps()); return this.writeAndWait(packet); } - respondAndWait( + async respondAndWait( name: Name, - object: PayloadDataFrom, + data: PayloadDataFrom, packet: Packet, ): Promise { - return this.writeAndWait(this.packetFactory.create(name, object, packet)); + this.validateConnectedSocket(); + + return this.writeAndWait(this.packetFactory.create(name, data, packet)); } - close(): Promise { + async close(): Promise { + if (!this.socket.connected) { + return; + } + return this.socket.end(); } protected override validate(props: PacketConnectionProps): void { super.validate(props); - this.validateDefinedProp(props, 'socket'); this.validateInstanceProp(props, 'socket', PacketSocket); } @@ -67,16 +78,14 @@ export class PacketConnection extends Connection { private writeAndWait(packet: Packet): Promise { return new Promise((resolve, reject) => { this.eventBus.once(packet.sequence.toString()).then(resolve, reject); - this.write(packet).catch(reject); + this.socket.write(packet).catch(reject); }); } - private async write(packet: Packet) { + private validateConnectedSocket(): void { if (!this.socket.connected) { - return; + throw new DomainException('Unable to send packet through a closed connection'); } - - return this.socket.write(packet); } static isPacketConnection(connection: Connection): connection is PacketConnection { diff --git a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts new file mode 100644 index 00000000..24b3e72a --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts @@ -0,0 +1,51 @@ +import { LocateDeviceCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { LocateDeviceCommandHandler } from './locate-device.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketMessage } from '../packet.message'; + +describe('LocateDeviceCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: LocateDeviceCommandHandler; + let packetConnection: PacketConnection; + let packetMessage: PacketMessage; + + beforeEach(function () { + packetConnectionFinderService = imock(); + commandHandler = new LocateDeviceCommandHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('LocateDeviceCommand'); + }); + + describe('#handle()', function () { + it('should send locate device command', async function () { + const command = new LocateDeviceCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', deepEqual({}))).once(); + verify(packetMessage.assertPayloadName('DEVICE_SEEK_LOCATION_RSP')).once(); + }); + + it('should do nothing when no connection is found', async function () { + const command = new LocateDeviceCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts new file mode 100644 index 00000000..945a489e --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts @@ -0,0 +1,21 @@ +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketMessage } from '../packet.message'; +import type { CommandHandler, LocateDeviceCommand } from '@agnoc/domain'; + +export class LocateDeviceCommandHandler implements CommandHandler { + readonly forName = 'LocateDeviceCommand'; + + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + + async handle(event: LocateDeviceCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + const response: PacketMessage = await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); + + response.assertPayloadName('DEVICE_SEEK_LOCATION_RSP'); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts b/packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts deleted file mode 100644 index afbe06de..00000000 --- a/packages/adapter-tcp/src/command-handlers/locate-device.event-handler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { CommandHandler, Connection, ConnectionRepository, LocateDeviceCommand } from '@agnoc/domain'; - -export class LocateDeviceEventHandler implements CommandHandler { - readonly forName = 'LocateDeviceCommand'; - - constructor(private readonly connectionRepository: ConnectionRepository) {} - - async handle(event: LocateDeviceCommand): Promise { - const connections = await this.connectionRepository.findByDeviceId(event.deviceId); - - if (connections.length === 0) { - throw new DomainException(`Unable to find a connection for the device with id ${event.deviceId.value}`); - } - - const connection = connections.find((connection: Connection): connection is PacketConnection => - PacketConnection.isPacketConnection(connection), - ); - - if (!connection) { - return; - } - - const response = await connection.sendAndWait('DEVICE_SEEK_LOCATION_REQ', {}); - - if (response.packet.payload.opcode.value !== 'DEVICE_SEEK_LOCATION_RSP') { - throw new DomainException(`Unexpected response from device: ${response.packet.payload.opcode.value}`); - } - } -} diff --git a/packages/adapter-tcp/src/connection-device-updater.service.test.ts b/packages/adapter-tcp/src/connection-device-updater.service.test.ts new file mode 100644 index 00000000..3b123685 --- /dev/null +++ b/packages/adapter-tcp/src/connection-device-updater.service.test.ts @@ -0,0 +1,64 @@ +import { Device } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository, DeviceRepository } from '@agnoc/domain'; + +describe('ConnectionDeviceUpdaterService', function () { + let connectionRepository: ConnectionRepository; + let deviceRepository: DeviceRepository; + let service: ConnectionDeviceUpdaterService; + let connection: PacketConnection; + + beforeEach(function () { + connectionRepository = imock(); + deviceRepository = imock(); + connection = imock(); + service = new ConnectionDeviceUpdaterService(instance(connectionRepository), instance(deviceRepository)); + }); + + describe('#updateFromPacket()', function () { + it('should update device in connection from packet when ids does not match', async function () { + const device = new Device(givenSomeDeviceProps()); + const packet = new Packet({ ...givenSomePacketProps(), deviceId: new ID(1) }); + + when(connection.device).thenReturn(new Device({ ...givenSomeDeviceProps(), id: new ID(2) })); + when(deviceRepository.findOneById(anything())).thenResolve(device); + + await service.updateFromPacket(packet, instance(connection)); + + verify(connection.setDevice(device)).once(); + verify(connectionRepository.saveOne(instance(connection))).once(); + }); + + it('should not update device in connection from packet when ids match', async function () { + const device = new Device(givenSomeDeviceProps()); + const packet = new Packet({ ...givenSomePacketProps(), deviceId: new ID(1) }); + + when(connection.device).thenReturn(new Device({ ...givenSomeDeviceProps(), id: new ID(1) })); + when(deviceRepository.findOneById(anything())).thenResolve(device); + + await service.updateFromPacket(packet, instance(connection)); + + verify(connection.setDevice(anything())).never(); + verify(connectionRepository.saveOne(anything())).never(); + }); + + it('should update device in connection with nothing when packet device id is zero', async function () { + const device = new Device(givenSomeDeviceProps()); + const packet = new Packet({ ...givenSomePacketProps(), deviceId: new ID(0) }); + + when(connection.device).thenReturn(new Device({ ...givenSomeDeviceProps(), id: new ID(1) })); + when(deviceRepository.findOneById(anything())).thenResolve(device); + + await service.updateFromPacket(packet, instance(connection)); + + verify(connection.setDevice(undefined)).once(); + verify(connectionRepository.saveOne(instance(connection))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/connection-device-updater.service.ts b/packages/adapter-tcp/src/connection-device-updater.service.ts new file mode 100644 index 00000000..0005c561 --- /dev/null +++ b/packages/adapter-tcp/src/connection-device-updater.service.ts @@ -0,0 +1,29 @@ +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository, DeviceRepository, Device } from '@agnoc/domain'; +import type { ID } from '@agnoc/toolkit'; +import type { Packet } from '@agnoc/transport-tcp'; + +export class ConnectionDeviceUpdaterService { + constructor( + private readonly connectionRepository: ConnectionRepository, + private readonly deviceRepository: DeviceRepository, + ) {} + + async updateFromPacket(packet: Packet, connection: PacketConnection): Promise { + if (!packet.deviceId.equals(connection.device?.id)) { + const device = await this.findDeviceById(packet.deviceId); + + connection.setDevice(device); + + await this.connectionRepository.saveOne(connection); + } + } + + private async findDeviceById(id: ID): Promise { + if (id.value === 0) { + return undefined; + } + + return this.deviceRepository.findOneById(id); + } +} diff --git a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts new file mode 100644 index 00000000..3e5ea18c --- /dev/null +++ b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts @@ -0,0 +1,45 @@ +import { DeviceConnectedDomainEvent } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { LockDeviceWhenDeviceIsConnectedEventHandler } from './lock-device-when-device-is-connected-event-handler.event-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; + +describe('LockDeviceWhenDeviceIsConnectedEventHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let eventHandler: LockDeviceWhenDeviceIsConnectedEventHandler; + let packetConnection: PacketConnection; + + beforeEach(function () { + packetConnectionFinderService = imock(); + eventHandler = new LockDeviceWhenDeviceIsConnectedEventHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DeviceConnectedDomainEvent'); + }); + + describe('#handle()', function () { + it('should lock the device', async function () { + const event = new DeviceConnectedDomainEvent({ aggregateId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + + await eventHandler.handle(event); + + verify(packetConnection.send('DEVICE_CONTROL_LOCK_REQ', deepEqual({}))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const event = new DeviceConnectedDomainEvent({ aggregateId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await eventHandler.handle(event); + + verify(packetConnection.send(anything(), anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index 073bd642..8986f5ec 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -1,22 +1,13 @@ -import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { DomainEventHandler, DeviceConnectedDomainEvent, ConnectionRepository, Connection } from '@agnoc/domain'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { readonly forName = 'DeviceConnectedDomainEvent'; - constructor(private readonly connectionRepository: ConnectionRepository) {} + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} async handle(event: DeviceConnectedDomainEvent): Promise { - const connections = await this.connectionRepository.findByDeviceId(event.aggregateId); - - if (connections.length === 0) { - throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); - } - - const connection = connections.find((connection: Connection): connection is PacketConnection => - PacketConnection.isPacketConnection(connection), - ); + const connection = await this.packetConnectionFinderService.findByDeviceId(event.aggregateId); if (!connection) { return; diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts new file mode 100644 index 00000000..8ecb3c0c --- /dev/null +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts @@ -0,0 +1,70 @@ +import { Device, DeviceLockedDomainEvent, DeviceSystem } from '@agnoc/domain'; +import { givenSomeDeviceProps, givenSomeDeviceSystemProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, anything, verify, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './query-device-info-when-device-is-locked-event-handler.event-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; + +describe('QueryDeviceInfoWhenDeviceIsLockedEventHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let eventHandler: QueryDeviceInfoWhenDeviceIsLockedEventHandler; + let packetConnection: PacketConnection; + + beforeEach(function () { + packetConnectionFinderService = imock(); + eventHandler = new QueryDeviceInfoWhenDeviceIsLockedEventHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DeviceLockedDomainEvent'); + }); + + describe('#handle()', function () { + it('should query device info for type C3490', async function () { + const system = new DeviceSystem({ ...givenSomeDeviceSystemProps(), type: 9 }); + const device = new Device({ ...givenSomeDeviceProps(), system }); + const event = new DeviceLockedDomainEvent({ aggregateId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(device); + + await eventHandler.handle(event); + + verify(packetConnection.send('DEVICE_STATUS_GETTING_REQ', deepEqual({}))).once(); + verify(packetConnection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', deepEqual({ unk1: 0, unk2: '' }))).once(); + verify(packetConnection.send('DEVICE_GETTIME_REQ', deepEqual({}))).once(); + verify(packetConnection.send('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x78ff }))).once(); + verify(packetConnection.send('DEVICE_WLAN_INFO_GETTING_REQ', deepEqual({}))).once(); + }); + + it('should query device info for type C3090', async function () { + const system = new DeviceSystem({ ...givenSomeDeviceSystemProps(), type: 3 }); + const device = new Device({ ...givenSomeDeviceProps(), system }); + const event = new DeviceLockedDomainEvent({ aggregateId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(device); + + await eventHandler.handle(event); + + verify(packetConnection.send('DEVICE_STATUS_GETTING_REQ', deepEqual({}))).once(); + verify(packetConnection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', deepEqual({ unk1: 0, unk2: '' }))).once(); + verify(packetConnection.send('DEVICE_GETTIME_REQ', deepEqual({}))).once(); + verify(packetConnection.send('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0xff }))).once(); + verify(packetConnection.send('DEVICE_WLAN_INFO_GETTING_REQ', deepEqual({}))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const event = new DeviceLockedDomainEvent({ aggregateId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await eventHandler.handle(event); + + verify(packetConnection.send(anything(), anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index aba3580d..af1b684f 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -1,23 +1,14 @@ import { DeviceCapability } from '@agnoc/domain'; -import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { DeviceLockedDomainEvent, DomainEventHandler, Connection, ConnectionRepository } from '@agnoc/domain'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { readonly forName = 'DeviceLockedDomainEvent'; - constructor(private readonly connectionRepository: ConnectionRepository) {} + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} async handle(event: DeviceLockedDomainEvent): Promise { - const connections = await this.connectionRepository.findByDeviceId(event.aggregateId); - - if (connections.length === 0) { - throw new DomainException(`Unable to find a connection for the device with id ${event.aggregateId.value}`); - } - - const connection = connections.find((connection: Connection): connection is PacketConnection => - PacketConnection.isPacketConnection(connection), - ); + const connection = await this.packetConnectionFinderService.findByDeviceId(event.aggregateId); if (!connection) { return; diff --git a/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts new file mode 100644 index 00000000..8c9d5b6d --- /dev/null +++ b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts @@ -0,0 +1,100 @@ +import { ConnectionDeviceChangedDomainEvent } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler } from './set-device-connected-when-connection-device-changed.event-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository, DeviceRepository, Device } from '@agnoc/domain'; + +describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function () { + let connectionRepository: ConnectionRepository; + let deviceRepository: DeviceRepository; + let eventHandler: SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler; + let packetConnection: PacketConnection; + let device: Device; + + beforeEach(function () { + connectionRepository = imock(); + deviceRepository = imock(); + eventHandler = new SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler( + instance(connectionRepository), + instance(deviceRepository), + ); + packetConnection = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('ConnectionDeviceChangedDomainEvent'); + }); + + describe('#handle()', function () { + it('should set device as connected when there are more than one connection', async function () { + const currentDeviceId = new ID(2); + const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); + + when(connectionRepository.findByDeviceId(anything())).thenResolve([ + instance(packetConnection), + instance(packetConnection), + ]); + when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); + when(packetConnection.connectionType).thenReturn('PACKET'); + when(device.isConnected).thenReturn(false); + + await eventHandler.handle(event); + + verify(connectionRepository.findByDeviceId(currentDeviceId)).once(); + verify(deviceRepository.findOneById(currentDeviceId)).once(); + verify(device.setAsConnected()).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when there is no current device id', async function () { + const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1) }); + + await eventHandler.handle(event); + + verify(connectionRepository.findByDeviceId(anything())).never(); + verify(deviceRepository.findOneById(anything())).never(); + verify(device.setAsConnected()).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + + it('should do nothing when there is only one connection', async function () { + const currentDeviceId = new ID(2); + const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); + + when(connectionRepository.findByDeviceId(anything())).thenResolve([instance(packetConnection)]); + when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); + when(packetConnection.connectionType).thenReturn('PACKET'); + when(device.isConnected).thenReturn(false); + + await eventHandler.handle(event); + + verify(connectionRepository.findByDeviceId(currentDeviceId)).once(); + verify(deviceRepository.findOneById(currentDeviceId)).once(); + verify(device.setAsConnected()).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + + it('should do nothing when connections are not packet connections', async function () { + const currentDeviceId = new ID(2); + const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); + + when(connectionRepository.findByDeviceId(anything())).thenResolve([ + instance(packetConnection), + instance(packetConnection), + ]); + when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); + when(packetConnection.connectionType).thenReturn('OTHER'); + when(device.isConnected).thenReturn(false); + + await eventHandler.handle(event); + + verify(connectionRepository.findByDeviceId(currentDeviceId)).once(); + verify(deviceRepository.findOneById(currentDeviceId)).once(); + verify(device.setAsConnected()).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.ts similarity index 67% rename from packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts rename to packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.ts index ef44589c..9326b4f3 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.ts @@ -1,11 +1,13 @@ +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { DomainEventHandler, ConnectionRepository, DeviceRepository, ConnectionDeviceChangedDomainEvent, + Connection, } from '@agnoc/domain'; -export class SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler implements DomainEventHandler { +export class SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler implements DomainEventHandler { readonly forName = 'ConnectionDeviceChangedDomainEvent'; constructor( @@ -17,10 +19,13 @@ export class SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler imp if (event.currentDeviceId) { const connections = await this.connectionRepository.findByDeviceId(event.currentDeviceId); const device = await this.deviceRepository.findOneById(event.currentDeviceId); + const packetConnections = connections.filter((connection: Connection): connection is PacketConnection => + PacketConnection.isPacketConnection(connection), + ); // This is a hack to only mark the device as connected if there is more than one connection. // Here we should check that the connections are from the same ip address. - if (connections.length > 1 && device && !device.isConnected) { + if (packetConnections.length > 1 && device && !device.isConnected) { device.setAsConnected(); await this.deviceRepository.saveOne(device); diff --git a/packages/adapter-tcp/src/factories/connection.factory.test.ts b/packages/adapter-tcp/src/factories/connection.factory.test.ts new file mode 100644 index 00000000..248b0b81 --- /dev/null +++ b/packages/adapter-tcp/src/factories/connection.factory.test.ts @@ -0,0 +1,32 @@ +import { Connection } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { PacketSocket } from '@agnoc/transport-tcp'; +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketConnectionFactory } from './connection.factory'; +import type { PacketEventBus } from '../packet.event-bus'; +import type { PacketFactory, PacketMapper } from '@agnoc/transport-tcp'; + +describe('PacketConnectionFactory', function () { + let packetEventBus: PacketEventBus; + let packetFactory: PacketFactory; + let packetMapper: PacketMapper; + let connectionFactory: PacketConnectionFactory; + + beforeEach(function () { + packetEventBus = imock(); + packetFactory = imock(); + packetMapper = imock(); + connectionFactory = new PacketConnectionFactory(instance(packetEventBus), instance(packetFactory)); + }); + + describe('#create()', function () { + it('should return a PacketConnection', function () { + const id = ID.generate(); + const connection = connectionFactory.create({ id, socket: new PacketSocket(packetMapper) }); + + expect(connection).to.be.instanceOf(Connection); + expect(connection.id.equals(id)).to.be.true; + }); + }); +}); diff --git a/packages/adapter-tcp/src/index.ts b/packages/adapter-tcp/src/index.ts index e397dcd6..bac62133 100644 --- a/packages/adapter-tcp/src/index.ts +++ b/packages/adapter-tcp/src/index.ts @@ -4,6 +4,6 @@ export * from './mappers/device-fan-speed.mapper'; export * from './mappers/device-mode.mapper'; export * from './mappers/device-order.mapper'; export * from './mappers/device-state.mapper'; -export * from './mappers/device-voice.mapper'; +export * from './mappers/voice-setting.mapper'; export * from './mappers/device-water-level.mapper'; export * from './tcp.server'; diff --git a/packages/adapter-tcp/src/mappers/clean-mode.mapper.test.ts b/packages/adapter-tcp/src/mappers/clean-mode.mapper.test.ts new file mode 100644 index 00000000..dcc8db5d --- /dev/null +++ b/packages/adapter-tcp/src/mappers/clean-mode.mapper.test.ts @@ -0,0 +1,29 @@ +import { CleanMode, CleanModeValue } from '@agnoc/domain'; +import { expect } from 'chai'; +import { CleanModeMapper } from './clean-mode.mapper'; + +describe('CleanModeMapper', function () { + let mapper: CleanModeMapper; + + beforeEach(function () { + mapper = new CleanModeMapper(); + }); + + describe('#toDomain()', function () { + it('should return a CleanMode', function () { + const cleanMode = mapper.toDomain(1); + + expect(cleanMode).to.be.instanceOf(CleanMode); + expect(cleanMode.value).to.be.equal(CleanModeValue.Auto); + }); + }); + + describe('#fromDomain()', function () { + it('should return a number', function () { + const cleanMode = new CleanMode(CleanModeValue.Auto); + const cleanModeValue = mapper.fromDomain(cleanMode); + + expect(cleanModeValue).to.be.equal(1); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-battery.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-battery.mapper.test.ts new file mode 100644 index 00000000..a4cfb1ba --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-battery.mapper.test.ts @@ -0,0 +1,43 @@ +import { DeviceBattery } from '@agnoc/domain'; +import { expect } from 'chai'; +import { DeviceBatteryMapper } from './device-battery.mapper'; + +describe('DeviceBatteryMapper', function () { + let mapper: DeviceBatteryMapper; + + beforeEach(function () { + mapper = new DeviceBatteryMapper(); + }); + + describe('#toDomain()', function () { + it('should return a DeviceBattery', function () { + const deviceBattery = mapper.toDomain(150); + + expect(deviceBattery).to.be.instanceOf(DeviceBattery); + expect(deviceBattery.value).to.be.equal(50); + }); + + it('should return a DeviceBattery with minimum value when below minimum', function () { + const deviceBattery = mapper.toDomain(-50); + + expect(deviceBattery).to.be.instanceOf(DeviceBattery); + expect(deviceBattery.value).to.be.equal(0); + }); + + it('should return a DeviceBattery with maximum value when above maximum', function () { + const deviceBattery = mapper.toDomain(250); + + expect(deviceBattery).to.be.instanceOf(DeviceBattery); + expect(deviceBattery.value).to.be.equal(100); + }); + }); + + describe('#fromDomain()', function () { + it('should return a number', function () { + const deviceBattery = new DeviceBattery(50); + const deviceBatteryValue = mapper.fromDomain(deviceBattery); + + expect(deviceBatteryValue).to.be.equal(150); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-error.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-error.mapper.test.ts new file mode 100644 index 00000000..d11f46f4 --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-error.mapper.test.ts @@ -0,0 +1,31 @@ +import { DeviceError, DeviceErrorValue } from '@agnoc/domain'; +import { DomainException, NotImplementedException } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceErrorMapper } from './device-error.mapper'; + +describe('DeviceErrorMapper', function () { + let mapper: DeviceErrorMapper; + + beforeEach(function () { + mapper = new DeviceErrorMapper(); + }); + + describe('#toDomain()', function () { + it('should return a DeviceError', function () { + const deviceError = mapper.toDomain(0); + + expect(deviceError).to.be.instanceOf(DeviceError); + expect(deviceError.value).to.be.equal(DeviceErrorValue.None); + }); + + it('should throw an error when device mode is unknown', function () { + expect(() => mapper.toDomain(999)).to.throw(DomainException, `Unable to map error code '999' to domain value`); + }); + }); + + describe('#fromDomain()', function () { + it('should throw an error', function () { + expect(() => mapper.fromDomain()).to.throw(NotImplementedException, 'DeviceErrorMapper.fromDomain'); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-error.mapper.ts b/packages/adapter-tcp/src/mappers/device-error.mapper.ts index 30593c0c..895dbaa8 100644 --- a/packages/adapter-tcp/src/mappers/device-error.mapper.ts +++ b/packages/adapter-tcp/src/mappers/device-error.mapper.ts @@ -59,6 +59,6 @@ export class DeviceErrorMapper implements Mapper { } fromDomain(): never { - throw new NotImplementedException('DeviceErrorMapper.toRobot'); + throw new NotImplementedException('DeviceErrorMapper.fromDomain'); } } diff --git a/packages/adapter-tcp/src/mappers/device-fan-speed.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-fan-speed.mapper.test.ts new file mode 100644 index 00000000..751e7284 --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-fan-speed.mapper.test.ts @@ -0,0 +1,29 @@ +import { DeviceFanSpeed, DeviceFanSpeedValue } from '@agnoc/domain'; +import { expect } from 'chai'; +import { DeviceFanSpeedMapper } from './device-fan-speed.mapper'; + +describe('DeviceFanSpeedMapper', function () { + let mapper: DeviceFanSpeedMapper; + + beforeEach(function () { + mapper = new DeviceFanSpeedMapper(); + }); + + describe('#toDomain()', function () { + it('should return a DeviceFanSpeed', function () { + const deviceFanSpeed = mapper.toDomain(0); + + expect(deviceFanSpeed).to.be.instanceOf(DeviceFanSpeed); + expect(deviceFanSpeed.value).to.be.equal(DeviceFanSpeedValue.Off); + }); + }); + + describe('#fromDomain()', function () { + it('should return a number', function () { + const deviceFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Off); + const deviceFanSpeedValue = mapper.fromDomain(deviceFanSpeed); + + expect(deviceFanSpeedValue).to.be.equal(0); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-mode.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-mode.mapper.test.ts new file mode 100644 index 00000000..a34e965e --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-mode.mapper.test.ts @@ -0,0 +1,52 @@ +import { DeviceMode, DeviceModeValue } from '@agnoc/domain'; +import { DomainException, NotImplementedException } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceModeMapper } from './device-mode.mapper'; + +describe('DeviceModeMapper', function () { + let mapper: DeviceModeMapper; + + beforeEach(function () { + mapper = new DeviceModeMapper(); + }); + + describe('#toDomain()', function () { + it('should return a none device mode', function () { + const deviceMode = mapper.toDomain(0); + + expect(deviceMode).to.be.instanceOf(DeviceMode); + expect(deviceMode.value).to.be.equal(DeviceModeValue.None); + }); + + it('should return a zone device mode', function () { + const deviceMode = mapper.toDomain(30); + + expect(deviceMode).to.be.instanceOf(DeviceMode); + expect(deviceMode.value).to.be.equal(DeviceModeValue.Zone); + }); + + it('should return an spot device mode', function () { + const deviceMode = mapper.toDomain(7); + + expect(deviceMode).to.be.instanceOf(DeviceMode); + expect(deviceMode.value).to.be.equal(DeviceModeValue.Spot); + }); + + it('should return a mop device mode', function () { + const deviceMode = mapper.toDomain(36); + + expect(deviceMode).to.be.instanceOf(DeviceMode); + expect(deviceMode.value).to.be.equal(DeviceModeValue.Mop); + }); + + it('should throw an error when device mode is unknown', function () { + expect(() => mapper.toDomain(999)).to.throw(DomainException, 'Unable to map device mode from mode 999'); + }); + }); + + describe('#fromDomain()', function () { + it('should throw an error', function () { + expect(() => mapper.fromDomain()).to.throw(NotImplementedException, 'DeviceModeMapper.fromDomain'); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-mode.mapper.ts b/packages/adapter-tcp/src/mappers/device-mode.mapper.ts index 282dd560..3a55478d 100644 --- a/packages/adapter-tcp/src/mappers/device-mode.mapper.ts +++ b/packages/adapter-tcp/src/mappers/device-mode.mapper.ts @@ -28,6 +28,6 @@ export class DeviceModeMapper implements Mapper { } fromDomain(): number { - throw new NotImplementedException('DeviceModeMapper.toRobot'); + throw new NotImplementedException(`${this.constructor.name}.fromDomain`); } } diff --git a/packages/adapter-tcp/src/mappers/device-order.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-order.mapper.test.ts new file mode 100644 index 00000000..d64d74d0 --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-order.mapper.test.ts @@ -0,0 +1,179 @@ +import { + DeviceFanSpeed, + DeviceFanSpeedValue, + DeviceWaterLevel, + DeviceWaterLevelValue, + CleanMode, + CleanModeValue, + WeekDay, + WeekDayValue, + DeviceOrder, + DeviceTime, +} from '@agnoc/domain'; +import { ArgumentNotProvidedException, ID } from '@agnoc/toolkit'; +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceOrderMapper } from './device-order.mapper'; +import type { CleanModeMapper } from './clean-mode.mapper'; +import type { DeviceFanSpeedMapper } from './device-fan-speed.mapper'; +import type { DeviceWaterLevelMapper } from './device-water-level.mapper'; +import type { WeekDayListMapper } from './week-day-list.mapper'; + +describe('DeviceOrderMapper', function () { + let deviceFanSpeedMapper: DeviceFanSpeedMapper; + let deviceWaterLevelMapper: DeviceWaterLevelMapper; + let cleanModeMapper: CleanModeMapper; + let weekDayListMapper: WeekDayListMapper; + let mapper: DeviceOrderMapper; + + beforeEach(function () { + deviceFanSpeedMapper = imock(); + deviceWaterLevelMapper = imock(); + cleanModeMapper = imock(); + weekDayListMapper = imock(); + mapper = new DeviceOrderMapper( + instance(deviceFanSpeedMapper), + instance(deviceWaterLevelMapper), + instance(cleanModeMapper), + instance(weekDayListMapper), + ); + }); + + describe('#toDomain()', function () { + it('should return a DeviceOrder', function () { + when(deviceFanSpeedMapper.toDomain(anything())).thenReturn(new DeviceFanSpeed(DeviceFanSpeedValue.Off)); + when(deviceWaterLevelMapper.toDomain(anything())).thenReturn(new DeviceWaterLevel(DeviceWaterLevelValue.Off)); + when(cleanModeMapper.toDomain(anything())).thenReturn(new CleanMode(CleanModeValue.Auto)); + when(weekDayListMapper.toDomain(anything())).thenReturn([new WeekDay(WeekDayValue.Monday)]); + + const deviceOrder = mapper.toDomain({ + orderId: 1, + enable: true, + repeat: true, + weekDay: 1, + dayTime: 90, + cleanInfo: { + mapHeadId: 2, + planId: 3, + cleanMode: 4, + windPower: 5, + waterLevel: 6, + twiceClean: true, + }, + }); + + expect(deviceOrder).to.be.instanceOf(DeviceOrder); + expect(deviceOrder.id.equals(new ID(1))).to.be.true; + expect(deviceOrder.mapId.equals(new ID(2))).to.be.true; + expect(deviceOrder.planId.equals(new ID(3))).to.be.true; + expect(deviceOrder.isEnabled).to.be.true; + expect(deviceOrder.isRepeatable).to.be.true; + expect(deviceOrder.isDeepClean).to.be.true; + expect(deviceOrder.weekDays).to.deep.contain(new WeekDay(WeekDayValue.Monday)); + expect(deviceOrder.time.equals(new DeviceTime({ hours: 1, minutes: 30 }))).to.be.true; + expect(deviceOrder.cleanMode.equals(new CleanMode(CleanModeValue.Auto))).to.be.true; + expect(deviceOrder.fanSpeed.equals(new DeviceFanSpeed(DeviceFanSpeedValue.Off))).to.be.true; + expect(deviceOrder.waterLevel.equals(new DeviceWaterLevel(DeviceWaterLevelValue.Off))).to.be.true; + + verify(deviceFanSpeedMapper.toDomain(5)).once(); + verify(deviceWaterLevelMapper.toDomain(6)).once(); + verify(cleanModeMapper.toDomain(4)).once(); + verify(weekDayListMapper.toDomain(1)).once(); + }); + + it('should return a DeviceOrder when order list has no water level', function () { + when(deviceFanSpeedMapper.toDomain(anything())).thenReturn(new DeviceFanSpeed(DeviceFanSpeedValue.Off)); + when(cleanModeMapper.toDomain(anything())).thenReturn(new CleanMode(CleanModeValue.Auto)); + when(weekDayListMapper.toDomain(anything())).thenReturn([new WeekDay(WeekDayValue.Monday)]); + + const deviceOrder = mapper.toDomain({ + orderId: 1, + enable: true, + repeat: true, + weekDay: 1, + dayTime: 90, + cleanInfo: { + mapHeadId: 2, + planId: 3, + cleanMode: 4, + windPower: 5, + twiceClean: true, + }, + }); + + expect(deviceOrder).to.be.instanceOf(DeviceOrder); + expect(deviceOrder.id.equals(new ID(1))).to.be.true; + expect(deviceOrder.mapId.equals(new ID(2))).to.be.true; + expect(deviceOrder.planId.equals(new ID(3))).to.be.true; + expect(deviceOrder.isEnabled).to.be.true; + expect(deviceOrder.isRepeatable).to.be.true; + expect(deviceOrder.isDeepClean).to.be.true; + expect(deviceOrder.weekDays).to.deep.contain(new WeekDay(WeekDayValue.Monday)); + expect(deviceOrder.time.equals(new DeviceTime({ hours: 1, minutes: 30 }))).to.be.true; + expect(deviceOrder.cleanMode.equals(new CleanMode(CleanModeValue.Auto))).to.be.true; + expect(deviceOrder.fanSpeed.equals(new DeviceFanSpeed(DeviceFanSpeedValue.Off))).to.be.true; + expect(deviceOrder.waterLevel.equals(new DeviceWaterLevel(DeviceWaterLevelValue.Off))).to.be.true; + + verify(deviceFanSpeedMapper.toDomain(5)).once(); + verify(deviceWaterLevelMapper.toDomain(anything())).never(); + verify(cleanModeMapper.toDomain(4)).once(); + verify(weekDayListMapper.toDomain(1)).once(); + }); + + it("should throw an error when 'cleanInfo' is not defined", function () { + expect(() => + mapper.toDomain({ + orderId: 1, + enable: true, + repeat: true, + weekDay: 1, + dayTime: 90, + }), + ).to.throw(ArgumentNotProvidedException, 'Unable to read clean info from order list'); + }); + }); + + describe('#fromDomain()', function () { + it('should return an order list', function () { + const deviceOrder = new DeviceOrder({ + id: new ID(1), + mapId: new ID(2), + planId: new ID(3), + isEnabled: true, + isRepeatable: true, + isDeepClean: true, + weekDays: [new WeekDay(WeekDayValue.Monday)], + time: new DeviceTime({ hours: 1, minutes: 30 }), + cleanMode: new CleanMode(CleanModeValue.Auto), + fanSpeed: new DeviceFanSpeed(DeviceFanSpeedValue.Off), + waterLevel: new DeviceWaterLevel(DeviceWaterLevelValue.Off), + }); + + when(deviceFanSpeedMapper.fromDomain(anything())).thenReturn(5); + when(deviceWaterLevelMapper.fromDomain(anything())).thenReturn(6); + when(cleanModeMapper.fromDomain(anything())).thenReturn(4); + when(weekDayListMapper.fromDomain(anything())).thenReturn(1); + + const orderList = mapper.fromDomain(deviceOrder); + + expect(orderList.orderId).to.be.equal(1); + expect(orderList.enable).to.be.true; + expect(orderList.repeat).to.be.true; + expect(orderList.weekDay).to.be.equal(1); + expect(orderList.dayTime).to.be.equal(90); + expect(orderList.cleanInfo).to.be.deep.equal({ + mapHeadId: 2, + planId: 3, + cleanMode: 4, + windPower: 5, + waterLevel: 6, + twiceClean: true, + }); + + verify(deviceFanSpeedMapper.fromDomain(deviceOrder.fanSpeed)).once(); + verify(deviceWaterLevelMapper.fromDomain(deviceOrder.waterLevel)).once(); + verify(cleanModeMapper.fromDomain(deviceOrder.cleanMode)).once(); + verify(weekDayListMapper.fromDomain(deviceOrder.weekDays)).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-state.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-state.mapper.test.ts new file mode 100644 index 00000000..4e67af52 --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-state.mapper.test.ts @@ -0,0 +1,76 @@ +import { DeviceState, DeviceStateValue } from '@agnoc/domain'; +import { DomainException, NotImplementedException } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceStateMapper } from './device-state.mapper'; + +describe('DeviceStateMapper', function () { + let mapper: DeviceStateMapper; + + beforeEach(function () { + mapper = new DeviceStateMapper(); + }); + + describe('#toDomain()', function () { + it('should return an error device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: true, type: 1, workMode: 0 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Error); + }); + + it('should return a manual control device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: false, type: 0, workMode: 2 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.ManualControl); + }); + + it('should return a returning device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: false, type: 0, workMode: 5 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Returning); + }); + + it('should return a paused device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: false, type: 0, workMode: 4 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Paused); + }); + + it('should return a cleaning device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: false, type: 0, workMode: 1 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Cleaning); + }); + + it('should return a docked device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: true, type: 0, workMode: 0 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Docked); + }); + + it('should return a idle device state', function () { + const deviceState = mapper.toDomain({ chargeStatus: false, type: 0, workMode: 0 }); + + expect(deviceState).to.be.instanceOf(DeviceState); + expect(deviceState.value).to.be.equal(DeviceStateValue.Idle); + }); + + it('should throw an error when device state is unknown', function () { + expect(() => mapper.toDomain({ chargeStatus: false, type: 0, workMode: 100 })).to.throw( + DomainException, + 'Unable to map device state from data: {"chargeStatus":false,"type":0,"workMode":100}', + ); + }); + }); + + describe('#fromDomain()', function () { + it('should throw an error', function () { + expect(() => mapper.fromDomain()).to.throw(NotImplementedException, 'DeviceStateMapper.fromDomain'); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-state.mapper.ts b/packages/adapter-tcp/src/mappers/device-state.mapper.ts index 3a5956f9..e1819f5b 100644 --- a/packages/adapter-tcp/src/mappers/device-state.mapper.ts +++ b/packages/adapter-tcp/src/mappers/device-state.mapper.ts @@ -81,6 +81,6 @@ export class DeviceStateMapper implements Mapper { } fromDomain(): RobotState { - throw new NotImplementedException('DeviceStateMapper.toRobot'); + throw new NotImplementedException(`${this.constructor.name}.fromDomain`); } } diff --git a/packages/adapter-tcp/src/mappers/device-water-level.mapper.test.ts b/packages/adapter-tcp/src/mappers/device-water-level.mapper.test.ts new file mode 100644 index 00000000..1d43344f --- /dev/null +++ b/packages/adapter-tcp/src/mappers/device-water-level.mapper.test.ts @@ -0,0 +1,29 @@ +import { DeviceWaterLevel, DeviceWaterLevelValue } from '@agnoc/domain'; +import { expect } from 'chai'; +import { DeviceWaterLevelMapper } from './device-water-level.mapper'; + +describe('DeviceWaterLevelMapper', function () { + let mapper: DeviceWaterLevelMapper; + + beforeEach(function () { + mapper = new DeviceWaterLevelMapper(); + }); + + describe('#toDomain()', function () { + it('should return a DeviceWaterLevel', function () { + const deviceWaterLevel = mapper.toDomain(10); + + expect(deviceWaterLevel).to.be.instanceOf(DeviceWaterLevel); + expect(deviceWaterLevel.value).to.be.equal(DeviceWaterLevelValue.Off); + }); + }); + + describe('#fromDomain()', function () { + it('should return a number', function () { + const deviceWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Off); + const deviceWaterLevelValue = mapper.fromDomain(deviceWaterLevel); + + expect(deviceWaterLevelValue).to.be.equal(10); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/voice-setting.mapper.test.ts b/packages/adapter-tcp/src/mappers/voice-setting.mapper.test.ts new file mode 100644 index 00000000..d0194d8f --- /dev/null +++ b/packages/adapter-tcp/src/mappers/voice-setting.mapper.test.ts @@ -0,0 +1,39 @@ +import { VoiceSetting } from '@agnoc/domain'; +import { expect } from 'chai'; +import { VoiceSettingMapper } from './voice-setting.mapper'; + +describe('VoiceSettingMapper', function () { + let mapper: VoiceSettingMapper; + + beforeEach(function () { + mapper = new VoiceSettingMapper(); + }); + + describe('#toDomain()', function () { + it('should return a VoiceSetting', function () { + const voiceSetting = mapper.toDomain({ isEnabled: true, volume: 6 }); + + expect(voiceSetting).to.be.instanceOf(VoiceSetting); + expect(voiceSetting.isEnabled).to.be.equal(true); + expect(voiceSetting.volume).to.be.equal(50); + }); + + it('should return a VoiceSetting with defaults', function () { + const voiceSetting = mapper.toDomain({ isEnabled: undefined, volume: undefined }); + + expect(voiceSetting).to.be.instanceOf(VoiceSetting); + expect(voiceSetting.isEnabled).to.be.equal(false); + expect(voiceSetting.volume).to.be.equal(0); + }); + }); + + describe('#fromDomain()', function () { + it('should return a number', function () { + const voiceSetting = new VoiceSetting({ isEnabled: true, volume: 50 }); + const voiceSettingValue = mapper.fromDomain(voiceSetting); + + expect(voiceSettingValue.isEnabled).to.be.equal(true); + expect(voiceSettingValue.volume).to.be.equal(6); + }); + }); +}); diff --git a/packages/adapter-tcp/src/mappers/device-voice.mapper.ts b/packages/adapter-tcp/src/mappers/voice-setting.mapper.ts similarity index 91% rename from packages/adapter-tcp/src/mappers/device-voice.mapper.ts rename to packages/adapter-tcp/src/mappers/voice-setting.mapper.ts index 7b114115..3a17ae7f 100644 --- a/packages/adapter-tcp/src/mappers/device-voice.mapper.ts +++ b/packages/adapter-tcp/src/mappers/voice-setting.mapper.ts @@ -19,7 +19,7 @@ export interface RobotVoice { volume: number | null | undefined; } -export class DeviceVoiceMapper implements Mapper { +export class VoiceSettingMapper implements Mapper { toDomain({ isEnabled, volume }: RobotVoice): VoiceSetting { return new VoiceSetting({ isEnabled: isEnabled ?? false, diff --git a/packages/adapter-tcp/src/mappers/week-day-list.mapper.test.ts b/packages/adapter-tcp/src/mappers/week-day-list.mapper.test.ts index 6519d093..a6b3e714 100644 --- a/packages/adapter-tcp/src/mappers/week-day-list.mapper.test.ts +++ b/packages/adapter-tcp/src/mappers/week-day-list.mapper.test.ts @@ -3,10 +3,16 @@ import { expect } from 'chai'; import { WeekDayDomainToRobotMap, WeekDayListMapper } from './week-day-list.mapper'; describe('WeekDayListMapper', function () { + let mapper: WeekDayListMapper; + + beforeEach(function () { + mapper = new WeekDayListMapper(); + }); + describe('#toDomain()', function () { it('should return an array of WeekDay', function () { const weekDay = WeekDayDomainToRobotMap.Monday | WeekDayDomainToRobotMap.Wednesday; - const weekDayList = new WeekDayListMapper().toDomain(weekDay); + const weekDayList = mapper.toDomain(weekDay); expect(weekDayList.length).to.be.equal(2); expect(weekDayList[0]).to.be.instanceOf(WeekDay); @@ -19,7 +25,7 @@ describe('WeekDayListMapper', function () { describe('#fromDomain()', function () { it('should return a number', function () { const weekDayList = [new WeekDay(WeekDayValue.Monday), new WeekDay(WeekDayValue.Wednesday)]; - const weekDay = new WeekDayListMapper().fromDomain(weekDayList); + const weekDay = mapper.fromDomain(weekDayList); expect(weekDay).to.be.equal(WeekDayDomainToRobotMap.Monday | WeekDayDomainToRobotMap.Wednesday); }); diff --git a/packages/adapter-tcp/src/packet-connection-finder.service.test.ts b/packages/adapter-tcp/src/packet-connection-finder.service.test.ts new file mode 100644 index 00000000..c0413a9c --- /dev/null +++ b/packages/adapter-tcp/src/packet-connection-finder.service.test.ts @@ -0,0 +1,53 @@ +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketConnectionFinderService } from './packet-connection-finder.service'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository } from '@agnoc/domain'; + +describe('PacketConnectionFinderService', function () { + let connectionRepository: ConnectionRepository; + let service: PacketConnectionFinderService; + let packetConnection: PacketConnection; + + beforeEach(function () { + connectionRepository = imock(); + service = new PacketConnectionFinderService(instance(connectionRepository)); + packetConnection = imock(); + }); + + describe('#findByDeviceId()', function () { + it('should find a connection by device id when it is a packet connection', async function () { + const deviceId = new ID(1); + + when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(packetConnection)]); + when(packetConnection.connectionType).thenReturn('PACKET'); + + const ret = await service.findByDeviceId(deviceId); + + expect(ret).to.equal(instance(packetConnection)); + }); + + it('should not find anything when is it not a packet connection', async function () { + const deviceId = new ID(1); + + when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(packetConnection)]); + when(packetConnection.connectionType).thenReturn('OTHER'); + + const ret = await service.findByDeviceId(deviceId); + + expect(ret).to.equal(undefined); + }); + + it('should throw an error when no connection is found', async function () { + const deviceId = new ID(1); + + when(connectionRepository.findByDeviceId(deviceId)).thenResolve([]); + + await expect(service.findByDeviceId(deviceId)).to.be.rejectedWith( + DomainException, + 'Unable to find a connection for the device with id 1', + ); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-connection-finder.service.ts b/packages/adapter-tcp/src/packet-connection-finder.service.ts new file mode 100644 index 00000000..1bbe641e --- /dev/null +++ b/packages/adapter-tcp/src/packet-connection-finder.service.ts @@ -0,0 +1,20 @@ +import { DomainException } from '@agnoc/toolkit'; +import { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository, Connection } from '@agnoc/domain'; +import type { ID } from '@agnoc/toolkit'; + +export class PacketConnectionFinderService { + constructor(private readonly connectionRepository: ConnectionRepository) {} + + async findByDeviceId(deviceId: ID): Promise { + const connections = await this.connectionRepository.findByDeviceId(deviceId); + + if (connections.length === 0) { + throw new DomainException(`Unable to find a connection for the device with id ${deviceId.value}`); + } + + return connections.find((connection: Connection): connection is PacketConnection => + PacketConnection.isPacketConnection(connection), + ); + } +} diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts new file mode 100644 index 00000000..90f721f8 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts @@ -0,0 +1,26 @@ +import { deepEqual, imock, instance, verify } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ClientHeartbeatEventHandler } from './client-heartbeat.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('ClientHeartbeatEventHandler', function () { + let eventHandler: ClientHeartbeatEventHandler; + let packetMessage: PacketMessage<'CLIENT_HEARTBEAT_REQ'>; + + beforeEach(function () { + eventHandler = new ClientHeartbeatEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('CLIENT_HEARTBEAT_REQ'); + }); + + describe('#handle()', function () { + it('should respond to the message', async function () { + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('CLIENT_HEARTBEAT_RSP', deepEqual({}))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts new file mode 100644 index 00000000..d5dada18 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts @@ -0,0 +1,53 @@ +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ClientLoginEventHandler } from './client-login.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { Device } from '@agnoc/domain'; + +describe('ClientLoginEventHandler', function () { + let eventHandler: ClientLoginEventHandler; + let packetMessage: PacketMessage<'CLIENT_ONLINE_REQ'>; + let device: Device; + + beforeEach(function () { + eventHandler = new ClientLoginEventHandler(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('CLIENT_ONLINE_REQ'); + }); + + describe('#handle()', function () { + it('should respond when it has device', async function () { + when(packetMessage.device).thenReturn(device); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('CLIENT_ONLINE_RSP', deepEqual({ result: 0 }))).once(); + }); + + it('should respond when it does not has device', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('CLIENT_ONLINE_REQ'), + data: { deviceSerialNumber: '1234567890', unk1: true, unk2: 1 }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.device).thenReturn(undefined); + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + verify( + packetMessage.respond( + 'CLIENT_ONLINE_RSP', + deepEqual({ result: 12002, reason: 'Device not registered(devsn: 1234567890)' }), + ), + ).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts new file mode 100644 index 00000000..c212e2da --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts @@ -0,0 +1,49 @@ +import { DeviceBattery } from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceBatteryUpdateEventHandler } from './device-battery-update.event-handler'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { PacketMessage } from '../packet.message'; +import type { Device } from '@agnoc/domain'; + +describe('DeviceBatteryUpdateEventHandler', function () { + let deviceBatteryMapper: DeviceBatteryMapper; + let eventHandler: DeviceBatteryUpdateEventHandler; + let packetMessage: PacketMessage<'PUSH_DEVICE_BATTERY_INFO_REQ'>; + let device: Device; + + beforeEach(function () { + deviceBatteryMapper = imock(); + eventHandler = new DeviceBatteryUpdateEventHandler(instance(deviceBatteryMapper)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('PUSH_DEVICE_BATTERY_INFO_REQ'); + }); + + describe('#handle()', function () { + it('should update the device battery', async function () { + const deviceBattery = new DeviceBattery(5); + const payload = new Payload({ + opcode: OPCode.fromName('PUSH_DEVICE_BATTERY_INFO_REQ'), + data: { battery: { level: 1 } }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceBatteryMapper.toDomain(anything())).thenReturn(deviceBattery); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceBatteryMapper.toDomain(1)).once(); + verify(device.updateBattery(deviceBattery)).once(); + verify(packetMessage.respond('PUSH_DEVICE_BATTERY_INFO_RSP', deepEqual({ result: 0 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts index 8f35f416..7252c2b4 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -9,9 +8,7 @@ export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { constructor(private readonly deviceBatteryMapper: DeviceBatteryMapper) {} async handle(message: PacketMessage<'PUSH_DEVICE_BATTERY_INFO_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const data = message.packet.payload.data; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index 92c03ae9..aa92aeb7 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -1,13 +1,13 @@ import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; -import type { DeviceVoiceMapper } from '../mappers/device-voice.mapper'; +import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; - constructor(private readonly deviceVoiceMapper: DeviceVoiceMapper) {} + constructor(private readonly deviceVoiceMapper: VoiceSettingMapper) {} async handle(message: PacketMessage<'PUSH_DEVICE_AGENT_SETTING_REQ'>): Promise { if (!message.device) { diff --git a/packages/adapter-tcp/src/packet-event-publisher.service.test.ts b/packages/adapter-tcp/src/packet-event-publisher.service.test.ts new file mode 100644 index 00000000..59f9c43f --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-publisher.service.test.ts @@ -0,0 +1,51 @@ +import { DomainException } from '@agnoc/toolkit'; +import { Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { imock, instance, anything, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketEventPublisherService } from './packet-event-publisher.service'; +import { PacketMessage } from './packet.message'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketEventBus } from './packet.event-bus'; + +describe('PacketEventPublisherService', function () { + let packetEventBus: PacketEventBus; + let packetConnection: PacketConnection; + let service: PacketEventPublisherService; + + beforeEach(function () { + packetEventBus = imock(); + packetConnection = imock(); + service = new PacketEventPublisherService(instance(packetEventBus)); + }); + + describe('#publishPacketMessage()', function () { + it('should emit packets through the packet event bus when has handlers for the packet', async function () { + const packet = new Packet(givenSomePacketProps()); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + when(packetEventBus.listenerCount(anything())).thenReturn(1); + + await service.publishPacketMessage(packetMessage); + + verify(packetEventBus.listenerCount(packet.payload.opcode.name)).once(); + verify(packetEventBus.emit(packet.sequence.toString(), packetMessage)).once(); + verify(packetEventBus.emit(packet.payload.opcode.name, packetMessage)).once(); + }); + + it('should throw an error when there is no handlers for the packet', async function () { + const packet = new Packet(givenSomePacketProps()); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + when(packetEventBus.listenerCount(anything())).thenReturn(0); + + await expect(service.publishPacketMessage(packetMessage)).to.be.rejectedWith( + DomainException, + `No event handler found for packet event 'DEVICE_GETTIME_RSP'`, + ); + + verify(packetEventBus.listenerCount(packet.payload.opcode.name)).once(); + verify(packetEventBus.emit(anything(), anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-publisher.service.ts b/packages/adapter-tcp/src/packet-event-publisher.service.ts new file mode 100644 index 00000000..01b19079 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-publisher.service.ts @@ -0,0 +1,31 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; +import type { PacketMessage } from './packet.message'; +import type { PayloadDataName } from '@agnoc/transport-tcp'; + +export class PacketEventPublisherService { + constructor(private readonly packetEventBus: PacketEventBus) {} + + async publishPacketMessage(packetMessage: PacketMessage): Promise { + const name = packetMessage.packet.payload.opcode.name as PayloadDataName; + const sequence = packetMessage.packet.sequence.toString(); + + this.checkForPacketEventHandler(name); + + // Emit the packet event by the sequence string. + // This is used to wait for a response from a packet. + await this.packetEventBus.emit(sequence, packetMessage as PacketEventBusEvents[PayloadDataName]); + + // Emit the packet event by the opcode name. + await this.packetEventBus.emit(name, packetMessage as PacketEventBusEvents[PayloadDataName]); + } + + private checkForPacketEventHandler(event: PayloadDataName) { + const count = this.packetEventBus.listenerCount(event); + + // Throw an error if there is no event handler for the packet event. + if (count === 0) { + throw new DomainException(`No event handler found for packet event '${event}'`); + } + } +} diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.test.ts b/packages/adapter-tcp/src/packet-server.connection-handler.test.ts new file mode 100644 index 00000000..90c3402e --- /dev/null +++ b/packages/adapter-tcp/src/packet-server.connection-handler.test.ts @@ -0,0 +1,152 @@ +import { anything, capture, defer, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PackerServerConnectionHandler } from './packet-server.connection-handler'; +import { PacketMessage } from './packet.message'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; +import type { PacketConnectionFactory } from './factories/connection.factory'; +import type { PacketEventPublisherService } from './packet-event-publisher.service'; +import type { ConnectionRepository } from '@agnoc/domain'; +import type { Packet, PacketServer, PacketSocket } from '@agnoc/transport-tcp'; + +describe('PackerServerConnectionHandler', function () { + let connectionRepository: ConnectionRepository; + let packetConnectionFactory: PacketConnectionFactory; + let updateConnectionDeviceService: ConnectionDeviceUpdaterService; + let packetEventPublisherService: PacketEventPublisherService; + let handler: PackerServerConnectionHandler; + let packetServer: PacketServer; + let packetSocket: PacketSocket; + let packetConnection: PacketConnection; + let packet: Packet; + + beforeEach(function () { + connectionRepository = imock(); + packetConnectionFactory = imock(); + updateConnectionDeviceService = imock(); + packetEventPublisherService = imock(); + handler = new PackerServerConnectionHandler( + instance(connectionRepository), + instance(packetConnectionFactory), + instance(updateConnectionDeviceService), + instance(packetEventPublisherService), + ); + packetServer = imock(); + packetSocket = imock(); + packetConnection = imock(); + packet = imock(); + }); + + describe('#addServers', function () { + it('should register a server and listen for connections', function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + verify(packetServer.on('connection', anything())).once(); + verify(packetServer.once('close')).once(); + }); + + it('should handle connections', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); + when(packetConnection.socket).thenReturn(instance(packetSocket)); + + const [, onConnection] = capture<'connection', (socket: PacketSocket) => Promise>(packetServer.on).first(); + + await onConnection(instance(packetSocket)); + + verify(connectionRepository.saveOne(instance(packetConnection))).once(); + verify(packetSocket.on('data', anything())).once(); + verify(packetSocket.once('close', anything())).once(); + }); + + it('should close connections on server close', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); + when(packetConnection.socket).thenReturn(instance(packetSocket)); + + const [, onConnection] = capture<'connection', (socket: PacketSocket) => Promise>(packetServer.on).first(); + + await onConnection(instance(packetSocket)); + await onClose.resolve(undefined); + + verify(packetServer.off('connection', onConnection)).once(); + verify(packetConnection.close()).once(); + }); + + it('should server close without connections', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + await onClose.resolve(undefined); + + verify(packetConnection.close()).never(); + }); + + it('should handle connection data', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); + when(packetConnection.socket).thenReturn(instance(packetSocket)); + + const [, onConnection] = capture<'connection', (socket: PacketSocket) => Promise>(packetServer.on).first(); + + await onConnection(instance(packetSocket)); + + const [, onData] = capture<'data', (packet: Packet) => Promise>(packetSocket.on).first(); + + await onData(instance(packet)); + + verify(updateConnectionDeviceService.updateFromPacket(instance(packet), instance(packetConnection))).once(); + + const [packetMessage] = capture(packetEventPublisherService.publishPacketMessage).first(); + + expect(packetMessage).to.be.instanceOf(PacketMessage); + expect(packetMessage.packet).to.equal(instance(packet)); + expect(packetMessage.connection).to.equal(instance(packetConnection)); + }); + + it('should handle connection close', async function () { + const onClose = defer(); + + when(packetServer.once(anything())).thenResolve(onClose); + + handler.addServers(instance(packetServer)); + + when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); + when(packetConnection.socket).thenReturn(instance(packetSocket)); + + const [, onConnection] = capture<'connection', (socket: PacketSocket) => Promise>(packetServer.on).first(); + + await onConnection(instance(packetSocket)); + + const [, onData] = capture<'data', (packet: Packet) => Promise>(packetSocket.on).first(); + const [, onConnectionClose] = capture<'close', () => Promise>(packetSocket.once).first(); + + await onConnectionClose(); + + verify(packetSocket.off('data', onData)).once(); + verify(connectionRepository.deleteOne(instance(packetConnection))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/packet-server.connection-handler.ts index 72ed16b4..e5439e18 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/packet-server.connection-handler.ts @@ -1,19 +1,20 @@ -import { DomainException, ID } from '@agnoc/toolkit'; +import { ID } from '@agnoc/toolkit'; import { PacketMessage } from './packet.message'; import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; import type { PacketConnectionFactory } from './factories/connection.factory'; -import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; -import type { DeviceRepository, Device, Connection, ConnectionRepository } from '@agnoc/domain'; -import type { PacketServer, Packet, PayloadDataName } from '@agnoc/transport-tcp'; +import type { PacketEventPublisherService } from './packet-event-publisher.service'; +import type { ConnectionRepository } from '@agnoc/domain'; +import type { PacketServer, Packet, PacketSocket } from '@agnoc/transport-tcp'; export class PackerServerConnectionHandler { private readonly servers = new Map>(); constructor( - private readonly packetEventBus: PacketEventBus, - private readonly deviceRepository: DeviceRepository, private readonly connectionRepository: ConnectionRepository, private readonly packetConnectionFactory: PacketConnectionFactory, + private readonly updateConnectionDeviceService: ConnectionDeviceUpdaterService, + private readonly packetEventPublisherService: PacketEventPublisherService, ) {} addServers(...servers: PacketServer[]): void { @@ -24,75 +25,51 @@ export class PackerServerConnectionHandler { } private addListeners(server: PacketServer) { - server.on('connection', (socket) => { - const connection = this.packetConnectionFactory.create({ id: ID.generate(), socket }); + const onConnection = (socket: PacketSocket) => this.handleServerConnection(server, socket); - this.servers.get(server)?.add(connection); + server.on('connection', onConnection); - connection.socket.on('data', async (packet: Packet) => { - const packetMessage = new PacketMessage(connection, packet); + void server.once('close').then(() => { + server.off('connection', onConnection); - // Update the device on the connection if the device id has changed. - await this.updateConnectionDevice(packet, connection); - - // Send the packet message to the packet event bus. - await this.emitPacketEvent(packetMessage); - }); - - connection.socket.on('close', () => { - this.servers.get(server)?.delete(connection); - }); - }); - - server.on('close', async () => { - const connections = this.servers.get(server); - - if (connections) { - await Promise.all([...connections].map((connection) => connection.close())); - - this.servers.delete(server); - } + return this.handleServerClose(server); }); } - private async emitPacketEvent(message: PacketMessage) { - const name = message.packet.payload.opcode.name as PayloadDataName; - const sequence = message.packet.sequence.toString(); - - this.checkForPacketEventHandler(name); + private async handleServerClose(server: PacketServer) { + const connections = this.servers.get(server) as Set; - // Emit the packet event by the sequence string. - // This is used to wait for a response from a packet. - await this.packetEventBus.emit(sequence, message as PacketEventBusEvents[PayloadDataName]); + this.servers.delete(server); - // Emit the packet event by the opcode name. - await this.packetEventBus.emit(name, message as PacketEventBusEvents[PayloadDataName]); + await Promise.all([...connections].map((connection) => connection.close())); } - private checkForPacketEventHandler(event: PayloadDataName) { - const count = this.packetEventBus.listenerCount(event); + private async handleServerConnection(server: PacketServer, socket: PacketSocket) { + const connection = this.packetConnectionFactory.create({ id: ID.generate(), socket }); - // Throw an error if there is no event handler for the packet event. - if (count === 0) { - throw new DomainException(`No event handler found for packet event '${event}'`); - } - } + this.servers.get(server)?.add(connection); - private async updateConnectionDevice(packet: Packet, connection: Connection) { - if (!packet.deviceId.equals(connection.device?.id)) { - const device = await this.findDeviceById(packet.deviceId); + // Should this be done before or after registering the listeners? + await this.connectionRepository.saveOne(connection); - connection.setDevice(device); + const onData = (packet: Packet) => this.handleConnectionData(connection, packet); + const onClose = () => { + connection.socket.off('data', onData); + this.servers.get(server)?.delete(connection); + return this.connectionRepository.deleteOne(connection); + }; - await this.connectionRepository.saveOne(connection); - } + connection.socket.on('data', onData); + connection.socket.once('close', onClose); } - private async findDeviceById(id: ID): Promise { - if (id.value === 0) { - return undefined; - } + private async handleConnectionData(connection: PacketConnection, packet: Packet) { + const packetMessage = new PacketMessage(connection, packet); + + // Update the device on the connection if the device id has changed. + await this.updateConnectionDeviceService.updateFromPacket(packet, connection); - return this.deviceRepository.findOneById(id); + // Send the packet message to the packet event bus. + await this.packetEventPublisherService.publishPacketMessage(packetMessage); } } diff --git a/packages/adapter-tcp/src/packet.message.test.ts b/packages/adapter-tcp/src/packet.message.test.ts new file mode 100644 index 00000000..0e17bbd9 --- /dev/null +++ b/packages/adapter-tcp/src/packet.message.test.ts @@ -0,0 +1,119 @@ +import { Device } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { DomainException } from '@agnoc/toolkit'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps, givenSomePayloadProps } from '@agnoc/transport-tcp/test-support'; +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PacketMessage } from './packet.message'; +import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; + +describe('PacketMessage', function () { + let packetConnection: PacketConnection; + let packet: Packet; + let packetMessage: PacketMessage; + + beforeEach(function () { + packetConnection = imock(); + packet = imock(); + packetMessage = new PacketMessage(instance(packetConnection), instance(packet)); + }); + + it('should be created', function () { + const device = new Device(givenSomeDeviceProps()); + + when(packetConnection.device).thenReturn(device); + + expect(packetMessage).to.exist; + expect(packetMessage.device).to.be.equal(device); + }); + + describe('#respond()', function () { + it('should respond to the connection', async function () { + const data = { result: 0, body: { deviceTime: 1 } }; + + when(packetConnection.respond(anything(), anything(), anything())).thenResolve(undefined); + + await packetMessage.respond('DEVICE_GETTIME_RSP', data); + + verify(packetConnection.respond('DEVICE_GETTIME_RSP', data, instance(packet))).once(); + }); + }); + + describe('#respondAndWait()', function () { + it('should respond to the connection and wait for the response', async function () { + const data = { result: 0, body: { deviceTime: 1 } }; + const anotherPacketMessage = new PacketMessage(instance(packetConnection), instance(packet)); + + when(packetConnection.respondAndWait(anything(), anything(), anything())).thenResolve(anotherPacketMessage); + + const ret = await packetMessage.respondAndWait('DEVICE_GETTIME_RSP', data); + + expect(ret).to.be.equal(anotherPacketMessage); + + verify(packetConnection.respondAndWait('DEVICE_GETTIME_RSP', data, instance(packet))).once(); + }); + }); + + describe('#hasPayloadName()', function () { + it('should return true when the payload name is the same', function () { + const opcode = OPCode.fromName('DEVICE_GETTIME_RSP'); + const payload = new Payload({ ...givenSomePayloadProps(), opcode }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + expect(packetMessage.hasPayloadName('DEVICE_GETTIME_RSP')).to.be.true; + }); + + it('should return false when the payload name is not the same', function () { + const opcode = OPCode.fromName('DEVICE_GETTIME_RSP'); + const payload = new Payload({ ...givenSomePayloadProps(), opcode }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + expect(packetMessage.hasPayloadName('DEVICE_GETTIME_REQ')).to.be.false; + }); + }); + + describe('#assertPayloadName()', function () { + it('should throw an error when the payload name is not the same', function () { + const opcode = OPCode.fromName('DEVICE_GETTIME_RSP'); + const payload = new Payload({ ...givenSomePayloadProps(), opcode }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + expect(() => packetMessage.assertPayloadName('DEVICE_GETTIME_REQ')).to.throw( + DomainException, + `Unexpected packet with payload name 'DEVICE_GETTIME_RSP', expecting 'DEVICE_GETTIME_REQ'`, + ); + }); + + it('should not throw an error when the payload name is the same', function () { + const opcode = OPCode.fromName('DEVICE_GETTIME_RSP'); + const payload = new Payload({ ...givenSomePayloadProps(), opcode }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + const packetMessage = new PacketMessage(instance(packetConnection), packet); + + expect(() => packetMessage.assertPayloadName('DEVICE_GETTIME_RSP')).to.not.throw(DomainException); + }); + }); + + describe('#assertDevice()', function () { + it('should throw an error when the connection does not has a device set', function () { + when(packetConnection.device).thenReturn(undefined); + + expect(() => packetMessage.assertDevice()).to.throw( + DomainException, + 'Connection does not have a reference to a device', + ); + }); + + it('should not throw an error when the connection has a device set', function () { + const device = new Device(givenSomeDeviceProps()); + + when(packetConnection.device).thenReturn(device); + + expect(() => packetMessage.assertDevice()).to.not.throw(DomainException); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/packet.message.ts index 8bdda9bf..e7b3a033 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/packet.message.ts @@ -1,3 +1,4 @@ +import { DomainException } from '@agnoc/toolkit'; import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; import type { Device } from '@agnoc/domain'; import type { Packet, PayloadDataFrom, PayloadDataName } from '@agnoc/transport-tcp'; @@ -16,4 +17,22 @@ export class PacketMessage { respondAndWait(name: Name, object: PayloadDataFrom): Promise { return this.connection.respondAndWait(name, object, this.packet); } + + hasPayloadName(name: Name): this is PacketMessage { + return this.packet.payload.opcode.value === (name as string); + } + + assertPayloadName(name: Name): asserts this is PacketMessage { + if (!this.hasPayloadName(name)) { + throw new DomainException( + `Unexpected packet with payload name '${this.packet.payload.opcode.value}', expecting '${name}'`, + ); + } + } + + assertDevice(): asserts this is PacketMessage & { device: Device } { + if (!this.device) { + throw new DomainException('Connection does not have a reference to a device'); + } + } } diff --git a/packages/adapter-tcp/src/tcp.server.test.ts b/packages/adapter-tcp/src/tcp.server.test.ts index 5ef401c8..1b230d81 100644 --- a/packages/adapter-tcp/src/tcp.server.test.ts +++ b/packages/adapter-tcp/src/tcp.server.test.ts @@ -29,7 +29,12 @@ describe('TCPServer', function () { }); it('should listen and close servers with custom ports', async function () { - await tcpAdapter.listen({ host: '127.0.0.1', ports: { cmd: 0, map: 0, ntp: 0 } }); + await tcpAdapter.listen({ ports: { cmd: 0, map: 0, ntp: 0 } }); + await tcpAdapter.close(); + }); + + it('should listen and close servers with custom host', async function () { + await tcpAdapter.listen({ host: '127.0.0.1' }); await tcpAdapter.close(); }); }); diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index ae68c898..e1792bd9 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -8,19 +8,21 @@ import { PayloadDataParserService, PacketFactory, } from '@agnoc/transport-tcp'; -import { LocateDeviceEventHandler } from './command-handlers/locate-device.event-handler'; +import { LocateDeviceCommandHandler } from './command-handlers/locate-device.command-handler'; +import { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; -import { SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler } from './domain-event-handlers/set-device-connected-when-connection-device-changed.domain-event'; +import { SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler } from './domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler'; import { PacketConnectionFactory } from './factories/connection.factory'; import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; import { DeviceErrorMapper } from './mappers/device-error.mapper'; import { DeviceFanSpeedMapper } from './mappers/device-fan-speed.mapper'; import { DeviceModeMapper } from './mappers/device-mode.mapper'; import { DeviceStateMapper } from './mappers/device-state.mapper'; -import { DeviceVoiceMapper } from './mappers/device-voice.mapper'; import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; +import { VoiceSettingMapper } from './mappers/voice-setting.mapper'; import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; +import { PacketConnectionFinderService } from './packet-connection-finder.service'; import { ClientHeartbeatEventHandler } from './packet-event-handlers/client-heartbeat.event-handler'; import { ClientLoginEventHandler } from './packet-event-handlers/client-login.event-handler'; import { DeviceBatteryUpdateEventHandler } from './packet-event-handlers/device-battery-update.event-handler'; @@ -41,6 +43,7 @@ import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-tim import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-upgrade-info.event-handler'; import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; import { DeviceWlanUpdateEventHandler } from './packet-event-handlers/device-wlan-update.event-handler'; +import { PacketEventPublisherService } from './packet-event-publisher.service'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; import type { CommandsOrQueries, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; @@ -71,7 +74,7 @@ export class TCPServer implements Server { // Mappers const deviceFanSpeedMapper = new DeviceFanSpeedMapper(); const deviceWaterLevelMapper = new DeviceWaterLevelMapper(); - const deviceVoiceMapper = new DeviceVoiceMapper(); + const deviceVoiceMapper = new VoiceSettingMapper(); const deviceStateMapper = new DeviceStateMapper(); const deviceModeMapper = new DeviceModeMapper(); const deviceErrorMapper = new DeviceErrorMapper(); @@ -83,11 +86,14 @@ export class TCPServer implements Server { // Connection const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); + const connectionDeviceUpdaterService = new ConnectionDeviceUpdaterService(connectionRepository, deviceRepository); + const packetEventPublisherService = new PacketEventPublisherService(packetEventBus); + const packetConnectionFinderService = new PacketConnectionFinderService(connectionRepository); const connectionManager = new PackerServerConnectionHandler( - packetEventBus, - this.deviceRepository, this.connectionRepository, packetConnectionFactory, + connectionDeviceUpdaterService, + packetEventPublisherService, ); connectionManager.addServers(this.cmdServer, this.mapServer); @@ -136,13 +142,13 @@ export class TCPServer implements Server { // Domain event handlers this.domainEventHandlerRegistry.register( - new LockDeviceWhenDeviceIsConnectedEventHandler(connectionRepository), - new QueryDeviceInfoWhenDeviceIsLockedEventHandler(connectionRepository), - new SetDeviceAsConnectedWhenConnectionDeviceAddedDomainEventHandler(connectionRepository, deviceRepository), + new LockDeviceWhenDeviceIsConnectedEventHandler(packetConnectionFinderService), + new QueryDeviceInfoWhenDeviceIsLockedEventHandler(packetConnectionFinderService), + new SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler(connectionRepository, deviceRepository), ); // Command event handlers - this.commandQueryHandlerRegistry.register(new LocateDeviceEventHandler(connectionRepository)); + this.commandQueryHandlerRegistry.register(new LocateDeviceCommandHandler(packetConnectionFinderService)); } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { diff --git a/packages/core/src/query-handlers/find-device.query-handler.test.ts b/packages/core/src/query-handlers/find-device.query-handler.test.ts new file mode 100644 index 00000000..5b27052c --- /dev/null +++ b/packages/core/src/query-handlers/find-device.query-handler.test.ts @@ -0,0 +1,48 @@ +import { Device, FindDeviceQuery } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { FindDeviceQueryHandler } from './find-device.query-handler'; +import type { DeviceRepository } from '@agnoc/domain'; + +describe('FindDeviceQueryHandler', function () { + let deviceRepository: DeviceRepository; + let queryHandler: FindDeviceQueryHandler; + + beforeEach(function () { + deviceRepository = imock(); + queryHandler = new FindDeviceQueryHandler(instance(deviceRepository)); + }); + + it('should define the name', function () { + expect(queryHandler.forName).to.be.equal('FindDeviceQuery'); + }); + + describe('#handle()', function () { + it('should find a device', async function () { + const deviceId = new ID(1); + const device = new Device(givenSomeDeviceProps()); + const query = new FindDeviceQuery({ deviceId }); + + when(deviceRepository.findOneById(anything())).thenResolve(device); + + const result = await queryHandler.handle(query); + + expect(result).to.be.deep.equal({ device }); + + verify(deviceRepository.findOneById(deviceId)).once(); + }); + + it("throw an error when 'device' is not found", async function () { + const deviceId = new ID(1); + const query = new FindDeviceQuery({ deviceId }); + + when(deviceRepository.findOneById(anything())).thenResolve(undefined); + + await expect(queryHandler.handle(query)).to.be.rejectedWith(`Unable to find a device with id ${deviceId.value}`); + + verify(deviceRepository.findOneById(deviceId)).once(); + }); + }); +}); diff --git a/packages/domain/src/queries/find-device-query.test.ts b/packages/domain/src/queries/find-device-query.test.ts new file mode 100644 index 00000000..6c127fe4 --- /dev/null +++ b/packages/domain/src/queries/find-device-query.test.ts @@ -0,0 +1,60 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, ID, Query } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { Device } from '../aggregate-roots/device.aggregate-root'; +import { givenSomeDeviceProps } from '../test-support'; +import { FindDeviceQuery } from './find-device.query'; +import type { FindDeviceQueryInput, FindDeviceQueryOutput } from './find-device.query'; + +describe('FindDeviceQuery', function () { + it('should be created', function () { + const input = givenAFindDeviceQueryInput(); + const query = new FindDeviceQuery(input); + + expect(query).to.be.instanceOf(Query); + expect(query.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new FindDeviceQuery({ ...givenAFindDeviceQueryInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for FindDeviceQuery not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new FindDeviceQuery({ ...givenAFindDeviceQueryInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of FindDeviceQuery is not an instance of ID`, + ); + }); + + it("should throw an error when 'device' is not provided", function () { + const query = new FindDeviceQuery(givenAFindDeviceQueryInput()); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAFindDeviceQueryOutput(), device: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'device' for FindDeviceQuery not provided`, + ); + }); + + it("should throw an error when 'device' is not a Device", function () { + const query = new FindDeviceQuery(givenAFindDeviceQueryInput()); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAFindDeviceQueryOutput(), device: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'device' of FindDeviceQuery is not an instance of Device`, + ); + }); +}); + +function givenAFindDeviceQueryInput(): FindDeviceQueryInput { + return { deviceId: ID.generate() }; +} + +function givenAFindDeviceQueryOutput(): FindDeviceQueryOutput { + return { device: new Device(givenSomeDeviceProps()) }; +} diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 593b76bc..427e0669 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -10,10 +10,10 @@ "./typescript": "./typescript.js" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", - "eslint": "^8.35.0", - "eslint-config-prettier": "^8.6.0", + "@typescript-eslint/eslint-plugin": "^5.56.0", + "@typescript-eslint/parser": "^5.56.0", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", "eslint-import-resolver-typescript": "^3.5.3", "eslint-plugin-chai-friendly": "^0.7.2", "eslint-plugin-import": "^2.27.5", diff --git a/packages/schemas-tcp/src/index.proto b/packages/schemas-tcp/src/index.proto index dfa8a226..e5bff3de 100644 --- a/packages/schemas-tcp/src/index.proto +++ b/packages/schemas-tcp/src/index.proto @@ -295,7 +295,7 @@ message DEVICE_ORDERLIST_SETTING_REQ { required uint32 planId = 2; required uint32 cleanMode = 3; required uint32 windPower = 4; - required uint32 waterLevel = 5; + optional uint32 waterLevel = 5; required bool twiceClean = 6; } diff --git a/packages/transport-tcp/src/packet.socket.test.ts b/packages/transport-tcp/src/packet.socket.test.ts index 295953a8..7fadf0b1 100644 --- a/packages/transport-tcp/src/packet.socket.test.ts +++ b/packages/transport-tcp/src/packet.socket.test.ts @@ -1,4 +1,4 @@ -import { Server } from 'net'; +import { Server, Socket } from 'net'; import { Duplex } from 'stream'; import { setTimeout } from 'timers/promises'; import { DomainException } from '@agnoc/toolkit'; @@ -209,7 +209,7 @@ describe('PacketSocket', function () { server.listen(0); }); - it('should wrap an existing socket', function (done) { + it('should wrap an existing connected socket', function (done) { const buffer = givenAPacketBuffer(); const packet = new Packet(givenSomePacketProps()); @@ -223,6 +223,14 @@ describe('PacketSocket', function () { server.once('connection', (socket) => { const packetSocketServer = new PacketSocket(instance(packetMapper), socket); + expect(packetSocketServer.localAddress).to.be.a('string'); + expect(packetSocketServer.localPort).to.be.a('number'); + expect(packetSocketServer.remoteAddress).to.be.a('string'); + expect(packetSocketServer.remotePort).to.be.a('number'); + expect(packetSocketServer.toString()).to.be.not.equal('unknown:0::unknown:0'); + expect(packetSocketServer.connecting).to.be.false; + expect(packetSocketServer.connected).to.be.true; + void packetSocketServer.end(packet); }); @@ -234,6 +242,18 @@ describe('PacketSocket', function () { server.listen(0); }); + it('should wrap an existing socket', function () { + packetSocket = new PacketSocket(instance(packetMapper), new Socket()); + + expect(packetSocket.localAddress).to.be.undefined; + expect(packetSocket.localPort).to.be.undefined; + expect(packetSocket.remoteAddress).to.be.undefined; + expect(packetSocket.remotePort).to.be.undefined; + expect(packetSocket.toString()).to.be.equal('unknown:0::unknown:0'); + expect(packetSocket.connecting).to.be.false; + expect(packetSocket.connected).to.be.false; + }); + describe('#connect()', function () { it('should connect to a server by options', function (done) { server.once('listening', () => { diff --git a/packages/transport-tcp/src/packet.socket.ts b/packages/transport-tcp/src/packet.socket.ts index 4b614626..f82bc4e2 100644 --- a/packages/transport-tcp/src/packet.socket.ts +++ b/packages/transport-tcp/src/packet.socket.ts @@ -148,7 +148,7 @@ export class PacketSocket extends Duplex { /** Returns whether the socket is connected. */ get connected(): boolean { - return this.socket?.readyState === 'open'; + return !this.socket?.pending && this.socket?.readyState === 'open'; } /** Returns an string representation of the socket addresses. */ @@ -279,6 +279,7 @@ export declare interface PacketSocket extends Duplex { emit(event: U, ...args: Parameters): boolean; on(event: U, listener: PacketSocketEvents[U]): this; once(event: U, listener: PacketSocketEvents[U]): this; + off(event: U, listener: PacketSocketEvents[U]): this; write(packet: Packet, encoding: BufferEncoding, cb: WriteCallback): boolean; write(packet: Packet, cb: WriteCallback): boolean; write(packet: Packet): Promise; diff --git a/tsconfig.json b/tsconfig.json index 819e004b..6a6b5e91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "include": ["packages/*/src", "packages/*/test", "packages/*/types"], "typedocOptions": { "entryPoints": [ + "packages/core", "packages/cli", "packages/adapter-tcp", "packages/transport-tcp", diff --git a/yarn.lock b/yarn.lock index 195f1164..1da752eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -191,14 +191,19 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@commitlint/cli@^17.4.4": - version "17.4.4" - resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-17.4.4.tgz#36df08bfa31dbb9a2b6b1d7187a31e578f001a06" - integrity sha512-HwKlD7CPVMVGTAeFZylVNy14Vm5POVY0WxPkZr7EXLC/os0LH/obs6z4HRvJtH/nHCMYBvUBQhGwnufKfTjd5g== +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@commitlint/cli@^17.5.0": + version "17.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-17.5.0.tgz#045bf46fc38bb4246da30b462d8db66f48c28f9a" + integrity sha512-yNW3+M7UM1ioK28LKTrryIVB5qGpXlEv8+rJQiWPMZNayy9/1XR5+lL8qBTNlgopYtZWWnIm5RETcAN29ZTL/A== dependencies: "@commitlint/format" "^17.4.4" "@commitlint/lint" "^17.4.4" - "@commitlint/load" "^17.4.4" + "@commitlint/load" "^17.5.0" "@commitlint/read" "^17.4.4" "@commitlint/types" "^17.4.4" execa "^5.0.0" @@ -265,10 +270,10 @@ "@commitlint/rules" "^17.4.4" "@commitlint/types" "^17.4.4" -"@commitlint/load@>6.1.1", "@commitlint/load@^17.4.4": - version "17.4.4" - resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-17.4.4.tgz#13fcb553572f265339801cde6dd10ee5eea07f5e" - integrity sha512-z6uFIQ7wfKX5FGBe1AkOF4l/ShOQsaa1ml/nLMkbW7R/xF8galGS7Zh0yHvzVp/srtfS0brC+0bUfQfmpMPFVQ== +"@commitlint/load@>6.1.1", "@commitlint/load@^17.5.0": + version "17.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-17.5.0.tgz#be45dbbb50aaf5eb7e8e940e1e0d6171d1426bab" + integrity sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q== dependencies: "@commitlint/config-validator" "^17.4.4" "@commitlint/execute-rule" "^17.4.0" @@ -283,7 +288,7 @@ lodash.uniq "^4.5.0" resolve-from "^5.0.0" ts-node "^10.8.1" - typescript "^4.6.4" + typescript "^4.6.4 || ^5.0.0" "@commitlint/message@^17.4.2": version "17.4.2" @@ -359,14 +364,26 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@eslint/eslintrc@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff" - integrity sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A== +"@eslint-community/eslint-utils@^4.2.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a" + integrity sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.1.tgz#087cb8d9d757bb22e9c9946c9c0c2bf8806830f1" + integrity sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw== + +"@eslint/eslintrc@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d" + integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" + espree "^9.5.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -374,10 +391,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.35.0": - version "8.35.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.35.0.tgz#b7569632b0b788a0ca0e438235154e45d42813a7" - integrity sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw== +"@eslint/js@8.36.0": + version "8.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" + integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== "@gar/promisify@^1.1.3": version "1.1.3" @@ -436,6 +453,13 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + "@johanblumenberg/ts-mockito@^1.0.35": version "1.0.35" resolved "https://registry.yarnpkg.com/@johanblumenberg/ts-mockito/-/ts-mockito-1.0.35.tgz#045fde7d479c2d0ed0156ea8e5ffb6eddbbd14b8" @@ -498,21 +522,21 @@ dependencies: lodash "^4.17.21" -"@lerna/child-process@6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-6.5.1.tgz#da9161ba00e8d67fa7241a709703e5cc5e4a5e5e" - integrity sha512-QfyleXSD9slh4qM54wDaqKVPvtUH1NJMgsFc9BabqSHO1Ttpandv1EAvTCN9Lu73RbCX3LJpn+BfJmnjHbjCyw== +"@lerna/child-process@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-6.6.0.tgz#6bd2effce7a2dd3a06b67db1c2cb6cc999859519" + integrity sha512-Y1elVHYKBLvdseKu74ud+pWqQi0I81AeWBKLRs0ehnD2EZsmy/f5ILkRcNU9aUp4GPF1TMUPAq2/+0By97gb1g== dependencies: chalk "^4.1.0" execa "^5.0.0" strong-log-transformer "^2.1.0" -"@lerna/create@6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-6.5.1.tgz#326b5d26c247bfc9e2d8728aa1f69419840cec8c" - integrity sha512-ejERJnfA36jEuKrfM+94feLiyf2/hF2NoG923N0rE4rsmvRFPr1XLVPvAKleXW+Gdi/t1p410lJ7NKaLRMYCYw== +"@lerna/create@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-6.6.0.tgz#7cec9d9c16c00e7d6359e76e75a12d2dc3ac987b" + integrity sha512-F1Q4bB9QzePSnIPd2ZY6a+3l/aZiYYmN1J0ednr8O+xMOyZsumX29c4XvnS7rgovz4aFqi+XdXiL35vmvrSfjA== dependencies: - "@lerna/child-process" "6.5.1" + "@lerna/child-process" "6.6.0" dedent "^0.7.0" fs-extra "^9.1.0" init-package-json "^3.0.2" @@ -526,6 +550,75 @@ validate-npm-package-name "^4.0.0" yargs-parser "20.2.4" +"@lerna/legacy-package-management@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@lerna/legacy-package-management/-/legacy-package-management-6.6.0.tgz#099a918cf1df82aa47ae21172db25643c5b0e4f5" + integrity sha512-vGkQv7nuZNhjs1+Arm1m+CKyBblig4oSv/PakyNnqTnRNryIgdXqYFn8DGGkUsA3qXLlouY41Sz5xEY/7hjNEA== + dependencies: + "@npmcli/arborist" "6.2.3" + "@npmcli/run-script" "4.1.7" + "@nrwl/devkit" ">=15.5.2 < 16" + "@octokit/rest" "19.0.3" + byte-size "7.0.0" + chalk "4.1.0" + clone-deep "4.0.1" + cmd-shim "5.0.0" + columnify "1.6.0" + config-chain "1.1.12" + conventional-changelog-core "4.2.4" + conventional-recommended-bump "6.1.0" + cosmiconfig "7.0.0" + dedent "0.7.0" + dot-prop "6.0.1" + execa "5.0.0" + file-url "3.0.0" + find-up "5.0.0" + fs-extra "9.1.0" + get-port "5.1.1" + get-stream "6.0.0" + git-url-parse "13.1.0" + glob-parent "5.1.2" + globby "11.1.0" + graceful-fs "4.2.10" + has-unicode "2.0.1" + inquirer "8.2.4" + is-ci "2.0.0" + is-stream "2.0.0" + libnpmpublish "6.0.4" + load-json-file "6.2.0" + make-dir "3.1.0" + minimatch "3.0.5" + multimatch "5.0.0" + node-fetch "2.6.7" + npm-package-arg "8.1.1" + npm-packlist "5.1.1" + npm-registry-fetch "14.0.3" + npmlog "6.0.2" + p-map "4.0.0" + p-map-series "2.1.0" + p-queue "6.6.2" + p-waterfall "2.1.1" + pacote "13.6.2" + path-exists "4.0.0" + pify "5.0.0" + pretty-format "29.4.3" + read-cmd-shim "3.0.0" + read-package-json "5.0.1" + resolve-from "5.0.0" + semver "7.3.8" + signal-exit "3.0.7" + slash "3.0.0" + ssri "9.0.1" + strong-log-transformer "2.1.0" + tar "6.1.11" + temp-dir "1.0.0" + tempy "1.0.0" + upath "2.0.1" + uuid "8.3.2" + write-file-atomic "4.0.1" + write-pkg "4.0.0" + yargs "16.2.0" + "@microsoft/tsdoc-config@0.16.2": version "0.16.2" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" @@ -562,44 +655,43 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@npmcli/arborist@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-5.3.0.tgz#321d9424677bfc08569e98a5ac445ee781f32053" - integrity sha512-+rZ9zgL1lnbl8Xbb1NQdMjveOMwj4lIYfcDtyJHHi5x4X8jtR6m8SXooJMZy5vmFVZ8w7A2Bnd/oX9eTuU8w5A== +"@npmcli/arborist@6.2.3": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-6.2.3.tgz#31f8aed2588341864d3811151d929c01308f8e71" + integrity sha512-lpGOC2ilSJXcc2zfW9QtukcCTcMbl3fVI0z4wvFB2AFIl0C+Q6Wv7ccrpdrQa8rvJ1ZVuc6qkX7HVTyKlzGqKA== dependencies: "@isaacs/string-locale-compare" "^1.1.0" - "@npmcli/installed-package-contents" "^1.0.7" - "@npmcli/map-workspaces" "^2.0.3" - "@npmcli/metavuln-calculator" "^3.0.1" - "@npmcli/move-file" "^2.0.0" - "@npmcli/name-from-folder" "^1.0.1" - "@npmcli/node-gyp" "^2.0.0" - "@npmcli/package-json" "^2.0.0" - "@npmcli/run-script" "^4.1.3" - bin-links "^3.0.0" - cacache "^16.0.6" + "@npmcli/fs" "^3.1.0" + "@npmcli/installed-package-contents" "^2.0.0" + "@npmcli/map-workspaces" "^3.0.2" + "@npmcli/metavuln-calculator" "^5.0.0" + "@npmcli/name-from-folder" "^2.0.0" + "@npmcli/node-gyp" "^3.0.0" + "@npmcli/package-json" "^3.0.0" + "@npmcli/query" "^3.0.0" + "@npmcli/run-script" "^6.0.0" + bin-links "^4.0.1" + cacache "^17.0.4" common-ancestor-path "^1.0.1" - json-parse-even-better-errors "^2.3.1" + hosted-git-info "^6.1.1" + json-parse-even-better-errors "^3.0.0" json-stringify-nice "^1.1.4" - mkdirp "^1.0.4" - mkdirp-infer-owner "^2.0.0" - nopt "^5.0.0" - npm-install-checks "^5.0.0" - npm-package-arg "^9.0.0" - npm-pick-manifest "^7.0.0" - npm-registry-fetch "^13.0.0" - npmlog "^6.0.2" - pacote "^13.6.1" - parse-conflict-json "^2.0.1" - proc-log "^2.0.0" + minimatch "^6.1.6" + nopt "^7.0.0" + npm-install-checks "^6.0.0" + npm-package-arg "^10.1.0" + npm-pick-manifest "^8.0.1" + npm-registry-fetch "^14.0.3" + npmlog "^7.0.1" + pacote "^15.0.8" + parse-conflict-json "^3.0.0" + proc-log "^3.0.0" promise-all-reject-late "^1.0.0" promise-call-limit "^1.0.1" - read-package-json-fast "^2.0.2" - readdir-scoped-modules "^1.1.0" - rimraf "^3.0.2" + read-package-json-fast "^3.0.2" semver "^7.3.7" - ssri "^9.0.0" - treeverse "^2.0.0" + ssri "^10.0.1" + treeverse "^3.0.0" walk-up-path "^1.0.0" "@npmcli/fs@^2.1.0": @@ -655,32 +747,32 @@ npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" -"@npmcli/installed-package-contents@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-2.0.1.tgz#3cad3141c95613426820128757a3549bef1b346b" - integrity sha512-GIykAFdOVK31Q1/zAtT5MbxqQL2vyl9mvFJv+OGu01zxbhL3p0xc8gJjdNGX1mWmUT43aEKVO2L6V/2j4TOsAA== +"@npmcli/installed-package-contents@^2.0.0", "@npmcli/installed-package-contents@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz#bfd817eccd9e8df200919e73f57f9e3d9e4f9e33" + integrity sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ== dependencies: npm-bundled "^3.0.0" npm-normalize-package-bin "^3.0.0" -"@npmcli/map-workspaces@^2.0.3": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-2.0.4.tgz#9e5e8ab655215a262aefabf139782b894e0504fc" - integrity sha512-bMo0aAfwhVwqoVM5UzX1DJnlvVvzDCHae821jv48L1EsrYwfOZChlqWYXEtto/+BkBXetPbEWgau++/brh4oVg== +"@npmcli/map-workspaces@^3.0.2": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-3.0.3.tgz#476944b63cd1f65bf83c6fdc7f4ca7be56906b1f" + integrity sha512-HlCvFuTzw4UNoKyZdqiNrln+qMF71QJkxy2dsusV8QQdoa89e2TF4dATCzBxbl4zzRzdDoWWyP5ADVrNAH9cRQ== dependencies: - "@npmcli/name-from-folder" "^1.0.1" - glob "^8.0.1" - minimatch "^5.0.1" - read-package-json-fast "^2.0.3" + "@npmcli/name-from-folder" "^2.0.0" + glob "^9.3.1" + minimatch "^7.4.2" + read-package-json-fast "^3.0.0" -"@npmcli/metavuln-calculator@^3.0.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-3.1.1.tgz#9359bd72b400f8353f6a28a25c8457b562602622" - integrity sha512-n69ygIaqAedecLeVH3KnO39M6ZHiJ2dEv5A7DGvcqCB8q17BGUgW8QaanIkbWUo2aYGZqJaOORTLAlIvKjNDKA== +"@npmcli/metavuln-calculator@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-5.0.0.tgz#917c3be49ebed0b424b07f38060b929127e4c499" + integrity sha512-BBFQx4M12wiEuVwCgtX/Depx0B/+NHMwDWOlXT41/Pdy5W/1Fenk+hibUlMSrFWwASbX+fY90UbILAEIYH02/A== dependencies: - cacache "^16.0.0" - json-parse-even-better-errors "^2.3.1" - pacote "^13.0.3" + cacache "^17.0.0" + json-parse-even-better-errors "^3.0.0" + pacote "^15.0.0" semver "^7.3.5" "@npmcli/move-file@^2.0.0": @@ -691,10 +783,10 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@npmcli/name-from-folder@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz#77ecd0a4fcb772ba6fe927e2e2e155fbec2e6b1a" - integrity sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA== +"@npmcli/name-from-folder@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz#c44d3a7c6d5c184bb6036f4d5995eee298945815" + integrity sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg== "@npmcli/node-gyp@^2.0.0": version "2.0.0" @@ -706,12 +798,12 @@ resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz#101b2d0490ef1aa20ed460e4c0813f0db560545a" integrity sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA== -"@npmcli/package-json@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-2.0.0.tgz#3bbcf4677e21055adbe673d9f08c9f9cde942e4a" - integrity sha512-42jnZ6yl16GzjWSH7vtrmWyJDGVa/LXPdpN2rcUWolFjc9ON2N3uz0qdBbQACfmhuJZ2lbKYtmK5qx68ZPLHMA== +"@npmcli/package-json@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-3.0.0.tgz#c9219a197e1be8dbf43c4ef8767a72277c0533b6" + integrity sha512-NnuPuM97xfiCpbTEJYtEuKz6CFbpUHtaT0+5via5pQeI25omvQDFbp1GcGJ/c4zvL/WX0qbde6YiLgfZbWFgvg== dependencies: - json-parse-even-better-errors "^2.3.1" + json-parse-even-better-errors "^3.0.0" "@npmcli/promise-spawn@^3.0.0": version "3.0.0" @@ -727,7 +819,14 @@ dependencies: which "^3.0.0" -"@npmcli/run-script@4.1.7": +"@npmcli/query@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/query/-/query-3.0.0.tgz#51a0dfb85811e04f244171f164b6bc83b36113a7" + integrity sha512-MFNDSJNgsLZIEBVZ0Q9w9K7o07j5N4o4yjtdz2uEpuCZlXGMuPENiRaFYk0vRqAA64qVuUQwC05g27fRtfUgnA== + dependencies: + postcss-selector-parser "^6.0.10" + +"@npmcli/run-script@4.1.7", "@npmcli/run-script@^4.1.0": version "4.1.7" resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-4.1.7.tgz#b1a2f57568eb738e45e9ea3123fb054b400a86f7" integrity sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw== @@ -738,17 +837,6 @@ read-package-json-fast "^2.0.3" which "^2.0.2" -"@npmcli/run-script@^4.1.0", "@npmcli/run-script@^4.1.3": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-4.2.1.tgz#c07c5c71bc1c70a5f2a06b0d4da976641609b946" - integrity sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg== - dependencies: - "@npmcli/node-gyp" "^2.0.0" - "@npmcli/promise-spawn" "^3.0.0" - node-gyp "^9.0.0" - read-package-json-fast "^2.0.3" - which "^2.0.2" - "@npmcli/run-script@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-6.0.0.tgz#f89e322c729e26ae29db6cc8cc76559074aac208" @@ -842,7 +930,7 @@ debug "^4.1.1" semver "^7.3.8" -"@oclif/config@1.18.6": +"@oclif/config@1.18.6", "@oclif/config@^1.18.2": version "1.18.6" resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.18.6.tgz#37367026b3110a2f04875509b1920a8ee4489f21" integrity sha512-OWhCpdu4QqggOPX1YPZ4XVmLLRX+lhGjXV6RNA7sogOwLqlEmSslnN/lhR5dkhcWZbKWBQH29YCrB3LDPRu/IA== @@ -854,18 +942,6 @@ is-wsl "^2.1.1" tslib "^2.3.1" -"@oclif/config@^1.18.2": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.18.8.tgz#efaccbd0381f90a98fa69c9131e14c5a91fc0659" - integrity sha512-FetS52+emaZQui0roFSdbBP8ddBkIezEoH2NcjLJRjqkMGdE9Z1V+jsISVqTYXk2KJ1gAI0CHDXFjJlNBYbJBg== - dependencies: - "@oclif/errors" "^1.3.6" - "@oclif/parser" "^3.8.10" - debug "^4.3.4" - globby "^11.1.0" - is-wsl "^2.1.1" - tslib "^2.5.0" - "@oclif/errors@1.3.6", "@oclif/errors@^1.3.5", "@oclif/errors@^1.3.6": version "1.3.6" resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.6.tgz#e8fe1fc12346cb77c4f274e26891964f5175f75d" @@ -1137,6 +1213,11 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@sinclair/typebox@^0.25.16": + version "0.25.24" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== + "@sindresorhus/is@^5.2.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.3.0.tgz#0ec9264cf54a527671d990eb874e030b55b70dcc" @@ -1258,10 +1339,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.14.2": - version "18.14.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" - integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.15.5": + version "18.15.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.5.tgz#3af577099a99c61479149b716183e70b5239324a" + integrity sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew== "@types/node@^10.0.0": version "10.17.60" @@ -1283,88 +1364,88 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== -"@typescript-eslint/eslint-plugin@^5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz#24b8b4a952f3c615fe070e3c461dd852b5056734" - integrity sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw== +"@typescript-eslint/eslint-plugin@^5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.56.0.tgz#e4fbb4d6dd8dab3e733485c1a44a02189ae75364" + integrity sha512-ZNW37Ccl3oMZkzxrYDUX4o7cnuPgU+YrcaYXzsRtLB16I1FR5SHMqga3zGsaSliZADCWo2v8qHWqAYIj8nWCCg== dependencies: - "@typescript-eslint/scope-manager" "5.53.0" - "@typescript-eslint/type-utils" "5.53.0" - "@typescript-eslint/utils" "5.53.0" + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.56.0" + "@typescript-eslint/type-utils" "5.56.0" + "@typescript-eslint/utils" "5.56.0" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" - regexpp "^3.2.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.53.0.tgz#a1f2b9ae73b83181098747e96683f1b249ecab52" - integrity sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ== +"@typescript-eslint/parser@^5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.56.0.tgz#42eafb44b639ef1dbd54a3dbe628c446ca753ea6" + integrity sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg== dependencies: - "@typescript-eslint/scope-manager" "5.53.0" - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/typescript-estree" "5.53.0" + "@typescript-eslint/scope-manager" "5.56.0" + "@typescript-eslint/types" "5.56.0" + "@typescript-eslint/typescript-estree" "5.56.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef" - integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== +"@typescript-eslint/scope-manager@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.56.0.tgz#62b4055088903b5254fa20403010e1c16d6ab725" + integrity sha512-jGYKyt+iBakD0SA5Ww8vFqGpoV2asSjwt60Gl6YcO8ksQ8s2HlUEyHBMSa38bdLopYqGf7EYQMUIGdT/Luw+sw== dependencies: - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/visitor-keys" "5.53.0" + "@typescript-eslint/types" "5.56.0" + "@typescript-eslint/visitor-keys" "5.56.0" -"@typescript-eslint/type-utils@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz#41665449935ba9b4e6a1ba6e2a3f4b2c31d6cf97" - integrity sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw== +"@typescript-eslint/type-utils@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.56.0.tgz#e6f004a072f09c42e263dc50e98c70b41a509685" + integrity sha512-8WxgOgJjWRy6m4xg9KoSHPzBNZeQbGlQOH7l2QEhQID/+YseaFxg5J/DLwWSsi9Axj4e/cCiKx7PVzOq38tY4A== dependencies: - "@typescript-eslint/typescript-estree" "5.53.0" - "@typescript-eslint/utils" "5.53.0" + "@typescript-eslint/typescript-estree" "5.56.0" + "@typescript-eslint/utils" "5.56.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" - integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== +"@typescript-eslint/types@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.56.0.tgz#b03f0bfd6fa2afff4e67c5795930aff398cbd834" + integrity sha512-JyAzbTJcIyhuUhogmiu+t79AkdnqgPUEsxMTMc/dCZczGMJQh1MK2wgrju++yMN6AWroVAy2jxyPcPr3SWCq5w== -"@typescript-eslint/typescript-estree@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" - integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== +"@typescript-eslint/typescript-estree@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.56.0.tgz#48342aa2344649a03321e74cab9ccecb9af086c3" + integrity sha512-41CH/GncsLXOJi0jb74SnC7jVPWeVJ0pxQj8bOjH1h2O26jXN3YHKDT1ejkVz5YeTEQPeLCCRY0U2r68tfNOcg== dependencies: - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/visitor-keys" "5.53.0" + "@typescript-eslint/types" "5.56.0" + "@typescript-eslint/visitor-keys" "5.56.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.53.0.tgz#e55eaad9d6fffa120575ffaa530c7e802f13bce8" - integrity sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g== +"@typescript-eslint/utils@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.56.0.tgz#db64705409b9a15546053fb4deb2888b37df1f41" + integrity sha512-XhZDVdLnUJNtbzaJeDSCIYaM+Tgr59gZGbFuELgF7m0IY03PlciidS7UQNKLE0+WpUTn1GlycEr6Ivb/afjbhA== dependencies: + "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.53.0" - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/typescript-estree" "5.53.0" + "@typescript-eslint/scope-manager" "5.56.0" + "@typescript-eslint/types" "5.56.0" + "@typescript-eslint/typescript-estree" "5.56.0" eslint-scope "^5.1.1" - eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" - integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== +"@typescript-eslint/visitor-keys@5.56.0": + version "5.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.56.0.tgz#f19eb297d972417eb13cb69b35b3213e13cc214f" + integrity sha512-1mFdED7u5bZpX6Xxf5N9U2c18sb+8EvU3tyOIj6LQZ5OOvnmj8BVeNNP603OFPm5KkS1a7IvCIcwrdHXaEMG/Q== dependencies: - "@typescript-eslint/types" "5.53.0" + "@typescript-eslint/types" "5.56.0" eslint-visitor-keys "^3.3.0" "@yarnpkg/lockfile@^1.1.0": @@ -1395,11 +1476,23 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" -abbrev@1, abbrev@^1.0.0: +abbrev@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1471,16 +1564,11 @@ ansi-align@^3.0.1: dependencies: string-width "^4.1.0" -ansi-colors@4.1.1: +ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-escapes@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -1522,6 +1610,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.2.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^6.0.0, ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -1565,6 +1658,14 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" +are-we-there-yet@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-4.0.0.tgz#3ff397dc14f08b52dd8b2a64d3cee154ab8760d2" + integrity sha512-nSXlV+u3vtVjRgihdTzbfWYzxPWGo424zPgQbHD0ZqIla3jqYAewDcvee0Ua2hjS5IfTAmjGlx1Jf0PKwjZDEw== + dependencies: + delegates "^1.0.0" + readable-stream "^4.1.0" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -1648,11 +1749,6 @@ arrify@^2.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -1707,17 +1803,15 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -bin-links@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-3.0.3.tgz#3842711ef3db2cd9f16a5f404a996a12db355a6e" - integrity sha512-zKdnMPWEdh4F5INR07/eBrodC7QrF5JKvqskjz/ZZRXg5YSAZIbn8zGhbhUrElzHBZ2fvEQdOU59RHcTG3GiwA== +bin-links@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-4.0.1.tgz#afeb0549e642f61ff889b58ea2f8dca78fb9d8d3" + integrity sha512-bmFEM39CyX336ZGGRsGPlc6jZHriIoHacOQcTt72MktIjpPhZoP4te2jOyUXF3BLILmJ8aNLncoPVeIIFlrDeA== dependencies: - cmd-shim "^5.0.0" - mkdirp-infer-owner "^2.0.0" - npm-normalize-package-bin "^2.0.0" - read-cmd-shim "^3.0.0" - rimraf "^3.0.0" - write-file-atomic "^4.0.0" + cmd-shim "^6.0.0" + npm-normalize-package-bin "^3.0.0" + read-cmd-shim "^4.0.0" + write-file-atomic "^5.0.0" binary-extensions@^2.0.0: version "2.2.0" @@ -1802,6 +1896,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -1819,7 +1921,7 @@ byte-size@7.0.0: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" integrity sha512-NNiBxKgxybMBtWdmvx7ZITJi4ZG+CYUgwOSZTfqB1qogkRHrhbQE/R2r5Fh94X+InN5MCYz6SvB/ejHMj/HbsQ== -cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: +cacache@^16.0.0, cacache@^16.1.0: version "16.1.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== @@ -1843,14 +1945,14 @@ cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: tar "^6.1.11" unique-filename "^2.0.0" -cacache@^17.0.0: - version "17.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.0.4.tgz#5023ed892ba8843e3b7361c26d0ada37e146290c" - integrity sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA== +cacache@^17.0.0, cacache@^17.0.4: + version "17.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.0.5.tgz#6dbec26c11f1f6a2b558bc11ed3316577c339ebc" + integrity sha512-Y/PRQevNSsjAPWykl9aeGz8Pr+OI6BYM9fYDNMvOkuUiG9IhG4LEmaYrZZZvioMUEQ+cBCxT0v8wrnCURccyKA== dependencies: "@npmcli/fs" "^3.1.0" fs-minipass "^3.0.0" - glob "^8.0.1" + glob "^9.3.1" lru-cache "^7.7.1" minipass "^4.0.0" minipass-collect "^1.0.2" @@ -1980,6 +2082,11 @@ chalk@4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@5.2.0, chalk@^5.0.1, chalk@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" + integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== + chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1997,11 +2104,6 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.1, chalk@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" - integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== - chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -2073,22 +2175,19 @@ cli-progress@^3.4.0: dependencies: string-width "^4.2.3" -cli-spinners@2.6.1: +cli-spinners@2.6.1, cli-spinners@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== -cli-spinners@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" - integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== - -cli-table@^0.3.11: - version "0.3.11" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.11.tgz#ac69cdecbe81dccdba4889b9a18b7da312a9d3ee" - integrity sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ== +cli-table3@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== dependencies: - colors "1.0.3" + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" cli-truncate@^2.1.0: version "2.1.0" @@ -2184,13 +2283,18 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -cmd-shim@5.0.0, cmd-shim@^5.0.0: +cmd-shim@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-5.0.0.tgz#8d0aaa1a6b0708630694c4dbde070ed94c707724" integrity sha512-qkCtZ59BidfEwHltnJwkyVZn+XQojdAySM1D1gSeh11Z4pW1Kpolkyo53L5noc0nrxmIvyFwTmJRo4xs7FFLPw== dependencies: mkdirp-infer-owner "^2.0.0" +cmd-shim@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d" + integrity sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2225,11 +2329,6 @@ colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw== - columnify@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" @@ -2328,7 +2427,7 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -config-chain@1.1.12: +config-chain@1.1.12, config-chain@^1.1.11: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -2336,14 +2435,6 @@ config-chain@1.1.12: ini "^1.3.4" proto-list "~1.2.1" -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - configstore@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" @@ -2360,7 +2451,7 @@ console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -conventional-changelog-angular@5.0.12: +conventional-changelog-angular@5.0.12, conventional-changelog-angular@^5.0.11: version "5.0.12" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== @@ -2368,14 +2459,6 @@ conventional-changelog-angular@5.0.12: compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-angular@^5.0.11: - version "5.0.13" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" - integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - conventional-changelog-conventionalcommits@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-5.0.0.tgz#41bdce54eb65a848a4a3ffdca93e92fa22b64a86" @@ -2525,6 +2608,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -2532,6 +2620,11 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cz-conventional-changelog@3.3.0, cz-conventional-changelog@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz#9246947c90404149b3fe2cf7ee91acad3b7d22d2" @@ -2570,11 +2663,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debuglog@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" - integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== - decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -2654,6 +2742,20 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +del@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" + integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2689,14 +2791,6 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g== -dezalgo@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" - integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== - dependencies: - asap "^2.0.0" - wrappy "1" - diff@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" @@ -2946,10 +3040,10 @@ escodegen@^1.13.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" - integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== +eslint-config-prettier@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== eslint-import-resolver-node@^0.3.7: version "0.3.7" @@ -3101,13 +3195,15 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.35.0: - version "8.35.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323" - integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== +eslint@^8.36.0: + version "8.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" + integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== dependencies: - "@eslint/eslintrc" "^2.0.0" - "@eslint/js" "8.35.0" + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.1" + "@eslint/js" "8.36.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -3118,9 +3214,8 @@ eslint@^8.35.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.1.1" - eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.4.0" + espree "^9.5.0" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3142,15 +3237,14 @@ eslint@^8.35.0: minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - regexpp "^3.2.0" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.0.0, espree@^9.4.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== +espree@^9.0.0, espree@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113" + integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" @@ -3190,11 +3284,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" @@ -3225,14 +3329,14 @@ execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20" - integrity sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA== +execa@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" + integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== dependencies: cross-spawn "^7.0.3" get-stream "^6.0.1" - human-signals "^3.0.1" + human-signals "^4.3.0" is-stream "^3.0.0" merge-stream "^2.0.0" npm-run-path "^5.1.0" @@ -3329,6 +3433,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-url@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/file-url/-/file-url-3.0.0.tgz#247a586a746ce9f7a8ed05560290968afc262a77" + integrity sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA== + filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -3558,6 +3667,20 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" +gauge@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-5.0.0.tgz#e270ca9d97dae84abf64e5277ef1ebddc7dd1e2f" + integrity sha512-0s5T5eciEG7Q3ugkxAkFtaDhrrhXsCRivA5y8C9WMHWuI8UlMOJg7+Iwf7Mccii+Dfs3H5jHepU0joPVyQU0Lw== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -3740,6 +3863,16 @@ glob@^8.0.0, glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +glob@^9.2.0, glob@^9.3.1: + version "9.3.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.2.tgz#8528522e003819e63d11c979b30896e0eaf52eda" + integrity sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -3798,7 +3931,7 @@ globalyzer@0.1.0: resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== -globby@11.1.0, globby@^11.0.4, globby@^11.1.0: +globby@11.1.0, globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -3979,7 +4112,7 @@ hosted-git-info@^5.0.0, hosted-git-info@^5.1.0: dependencies: lru-cache "^7.5.1" -hosted-git-info@^6.0.0: +hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== @@ -4026,10 +4159,10 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -human-signals@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" - integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== humanize-ms@^1.2.1: version "1.2.1" @@ -4062,7 +4195,7 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -4145,10 +4278,10 @@ ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ini@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.1.tgz#c76ec81007875bc44d544ff7a11a55d12294102d" - integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ== +ini@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.0.0.tgz#35b4b0ba3bb9a3feb8c50dbf92fb1671efda88eb" + integrity sha512-t0ikzf5qkSFqRl1e6ejKBe+Tk2bsQd8ivEkcisyGXsku2t8NvXZ1Y3RRz5vxrDgOrTBOi13CvGsVoI5wVpd7xg== init-package-json@3.0.2, init-package-json@^3.0.2: version "3.0.2" @@ -4163,6 +4296,27 @@ init-package-json@3.0.2, init-package-json@^3.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^4.0.0" +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + inquirer@8.2.5, inquirer@^8.2.4: version "8.2.5" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" @@ -4339,6 +4493,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -4388,16 +4547,11 @@ is-ssh@^1.4.0: dependencies: protocols "^2.0.1" -is-stream@2.0.0: +is-stream@2.0.0, is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - is-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" @@ -4727,10 +4881,10 @@ just-diff-apply@^5.2.0: resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" integrity sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw== -just-diff@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.2.0.tgz#60dca55891cf24cd4a094e33504660692348a241" - integrity sha512-6ufhP9SHjb7jibNFrNxyFZ6od3g+An6Ai9mhGRvcYe8UJlH0prseN64M+6ZBBUoKYHZsitDP42gAJ8+eVWr3lw== +just-diff@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.0.tgz#77ebd7ee7fbb22f887dbe1dc376e5f781ea0c9c5" + integrity sha512-MbEkhMEa9p7bVD/U29cTM6zUHcLPWwSZjnSquOu++6GyKfoc3aGNdsp2uLQ3Zq0ocg60MzTxwB9qjqwgoQu3+g== keyv@^4.5.2: version "4.5.2" @@ -4763,14 +4917,15 @@ latest-version@^7.0.0: dependencies: package-json "^8.1.0" -lerna@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.5.1.tgz#eb89698e5b2891f5681f39d980f63d0519fc464f" - integrity sha512-Va1bysubwWdoWZ1ncKcoTGBXNAu/10/TwELb550TTivXmEWjCCdek4eX0BNLTEYKxu3tpV2UEeqVisUiWGn4WA== +lerna@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.6.0.tgz#385dcf9a88ddd9e62112f03bcfc2ce9e75e64980" + integrity sha512-E1uxpcKVkXPL7IZd8jXmoGlES878M7ConEpkX1o+KTs99ir9c3StIOnrhULYHP3VacUyjtxAPyPCCSg6rIipaw== dependencies: - "@lerna/child-process" "6.5.1" - "@lerna/create" "6.5.1" - "@npmcli/arborist" "5.3.0" + "@lerna/child-process" "6.6.0" + "@lerna/create" "6.6.0" + "@lerna/legacy-package-management" "6.6.0" + "@npmcli/arborist" "6.2.3" "@npmcli/run-script" "4.1.7" "@nrwl/devkit" ">=15.5.2 < 16" "@octokit/plugin-enterprise-rest" "6.0.1" @@ -4812,7 +4967,7 @@ lerna@^6.5.1: node-fetch "2.6.7" npm-package-arg "8.1.1" npm-packlist "5.1.1" - npm-registry-fetch "13.3.0" + npm-registry-fetch "^14.0.3" npmlog "^6.0.2" nx ">=15.5.2 < 16" p-map "4.0.0" @@ -4821,14 +4976,14 @@ lerna@^6.5.1: p-queue "6.6.2" p-reduce "2.1.0" p-waterfall "2.1.1" - pacote "13.6.1" + pacote "13.6.2" path-exists "4.0.0" pify "5.0.0" read-cmd-shim "3.0.0" read-package-json "5.0.1" resolve-from "5.0.0" rimraf "^3.0.2" - semver "7.3.4" + semver "^7.3.8" signal-exit "3.0.7" slash "3.0.0" ssri "9.0.1" @@ -4882,10 +5037,10 @@ libnpmpublish@6.0.4: semver "^7.3.7" ssri "^9.0.0" -lilconfig@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" - integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== +lilconfig@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== lines-and-columns@^1.1.6: version "1.2.4" @@ -4904,29 +5059,29 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -lint-staged@>=13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.2.tgz#443636a0cfd834d5518d57d228130dc04c83d6fb" - integrity sha512-K9b4FPbWkpnupvK3WXZLbgu9pchUJ6N7TtVZjbaPsoizkqFUDkUReUL25xdrCljJs7uLUF3tZ7nVPeo/6lp+6w== +lint-staged@>=13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.2.0.tgz#b7abaf79c91cd36d824f17b23a4ce5209206126a" + integrity sha512-GbyK5iWinax5Dfw5obm2g2ccUiZXNGtAS4mCbJ0Lv4rq6iEtfBSjOYdcbOtAIFtM114t0vdpViDDetjVTSd8Vw== dependencies: + chalk "5.2.0" cli-truncate "^3.1.0" - colorette "^2.0.19" - commander "^9.4.1" + commander "^10.0.0" debug "^4.3.4" - execa "^6.1.0" - lilconfig "2.0.6" - listr2 "^5.0.5" + execa "^7.0.0" + lilconfig "2.1.0" + listr2 "^5.0.7" micromatch "^4.0.5" normalize-path "^3.0.0" - object-inspect "^1.12.2" + object-inspect "^1.12.3" pidtree "^0.6.0" string-argv "^0.3.1" - yaml "^2.1.3" + yaml "^2.2.1" -listr2@^5.0.5: - version "5.0.7" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.7.tgz#de69ccc4caf6bea7da03c74f7a2ffecf3904bd53" - integrity sha512-MD+qXHPmtivrHIDRwPYdfNkrzqDiuaKU/rfBcec3WMyMF3xylQj3jMq344OtvQxz7zaCFViRAeqlr2AFhPvXHw== +listr2@^5.0.7: + version "5.0.8" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.8.tgz#a9379ffeb4bd83a68931a65fb223a11510d6ba23" + integrity sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA== dependencies: cli-truncate "^2.1.0" colorette "^2.0.19" @@ -5103,10 +5258,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.17.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.17.0.tgz#00c7ba5919e5ea7c69ff94ddabbf32cb09ab805c" - integrity sha512-zSxlVVwOabhVyTi6E8gYv2cr6bXK+8ifYz5/uyJb9feXX6NACVDwY4p5Ut3WC3Ivo/QhpARHU3iujx2xGAYHbQ== +lru-cache@^7.14.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== lunr@^2.3.9: version "2.3.9" @@ -5301,7 +5456,7 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@5.0.1: +minimatch@5.0.1, minimatch@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== @@ -5315,24 +5470,17 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^6.1.0, minimatch@^6.1.6, minimatch@^6.2.0: +minimatch@^6.1.0, minimatch@^6.1.6: version "6.2.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-6.2.0.tgz#2b70fd13294178c69c04dfc05aebdb97a4e79e42" integrity sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg== dependencies: brace-expansion "^2.0.1" -minimatch@^7.1.3: - version "7.2.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.2.0.tgz#741ff59370007ebb8faff0a9b20cdb44357803e4" - integrity sha512-rMRHmwySzopAQjmWW6TkAKCEDKNaY/HuV/c2YkWWuWnfkTwApt0V4hnYzzPnZ/5Gcd2+8MPncSyuOGPl3xPvcg== +minimatch@^7.1.3, minimatch@^7.4.1, minimatch@^7.4.2, minimatch@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.3.tgz#012cbf110a65134bb354ae9773b55256cdb045a2" + integrity sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A== dependencies: brace-expansion "^2.0.1" @@ -5345,16 +5493,11 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@1.2.7: +minimist@1.2.7, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -5420,10 +5563,10 @@ minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: dependencies: yallist "^4.0.0" -minipass@^4.0.0: - version "4.2.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.4.tgz#7d0d97434b6a19f59c5c3221698b48bbf3b2cd06" - integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ== +minipass@^4.0.0, minipass@^4.0.2, minipass@^4.2.4: + version "4.2.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb" + integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q== minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" @@ -5555,20 +5698,13 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-fetch@2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.7: - version "2.6.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== - dependencies: - whatwg-url "^5.0.0" - node-gyp-build@^4.3.0: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -5610,13 +5746,6 @@ node-wifi@^2.0.16: command-line-args "^5.2.0" command-line-usage "^6.1.1" -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - nopt@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" @@ -5624,6 +5753,13 @@ nopt@^6.0.0: dependencies: abbrev "^1.0.0" +nopt@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.1.0.tgz#91f6a3366182176e72ecab93a09c19b63b485f28" + integrity sha512-ZFPLe9Iu0tnx7oWhFxAo4s7QTn8+NNDDxYNaKLjE7Dp0tbakQ3M1QhQzsnzXHQBTUO3K9BmwaxnyO8Ayn2I95Q== + dependencies: + abbrev "^2.0.0" + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -5681,13 +5817,6 @@ npm-bundled@^1.1.1, npm-bundled@^1.1.2: dependencies: npm-normalize-package-bin "^1.0.1" -npm-bundled@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-2.0.1.tgz#94113f7eb342cd7a67de1e789f896b04d2c600f4" - integrity sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw== - dependencies: - npm-normalize-package-bin "^2.0.0" - npm-bundled@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-3.0.0.tgz#7e8e2f8bb26b794265028491be60321a25a39db7" @@ -5695,13 +5824,13 @@ npm-bundled@^3.0.0: dependencies: npm-normalize-package-bin "^3.0.0" -npm-check-updates@^16.7.9: - version "16.7.9" - resolved "https://registry.yarnpkg.com/npm-check-updates/-/npm-check-updates-16.7.9.tgz#b30e21eb27a9b9f590f96ef9eb0f128a09aaf216" - integrity sha512-2/T9PB0W1uABB1N11jdnPauqNLXMhPUyC0DBeeGlUktcGl/412f40iqFsnNGCqIAag/C37SjLNiW2/yuD7Salw== +npm-check-updates@^16.8.0: + version "16.8.0" + resolved "https://registry.yarnpkg.com/npm-check-updates/-/npm-check-updates-16.8.0.tgz#43884cc479f03d274c9ca9e3150e6fb64412377e" + integrity sha512-2xpFtUPa2OyswC8kpIpxRqQcP/D0uqaBgfiOQAtvGlnDhmHr3QwR2gWjjWtS6xnNvgyyVly/4edY0Uu24c46ew== dependencies: chalk "^5.2.0" - cli-table "^0.3.11" + cli-table3 "^0.6.3" commander "^10.0.0" fast-memoize "^2.5.2" find-up "5.0.0" @@ -5709,19 +5838,19 @@ npm-check-updates@^16.7.9: get-stdin "^8.0.0" globby "^11.0.4" hosted-git-info "^5.1.0" - ini "^3.0.1" + ini "^4.0.0" json-parse-helpfulerror "^1.0.3" jsonlines "^0.1.1" lodash "^4.17.21" - minimatch "^6.2.0" + minimatch "^7.4.3" p-map "^4.0.0" - pacote "15.1.0" + pacote "15.1.1" parse-github-url "^1.0.2" progress "^2.0.3" prompts-ncu "^2.5.1" rc-config-loader "^4.1.2" remote-git-tags "^3.0.0" - rimraf "^4.1.2" + rimraf "^4.4.1" semver "^7.3.8" semver-utils "^1.1.4" source-map-support "^0.5.21" @@ -5769,7 +5898,7 @@ npm-package-arg@8.1.1: semver "^7.0.0" validate-npm-package-name "^3.0.0" -npm-package-arg@^10.0.0: +npm-package-arg@^10.0.0, npm-package-arg@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== @@ -5789,7 +5918,7 @@ npm-package-arg@^9.0.0, npm-package-arg@^9.0.1: semver "^7.3.5" validate-npm-package-name "^4.0.0" -npm-packlist@5.1.1: +npm-packlist@5.1.1, npm-packlist@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-5.1.1.tgz#79bcaf22a26b6c30aa4dd66b976d69cc286800e0" integrity sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw== @@ -5799,16 +5928,6 @@ npm-packlist@5.1.1: npm-bundled "^1.1.2" npm-normalize-package-bin "^1.0.1" -npm-packlist@^5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-5.1.3.tgz#69d253e6fd664b9058b85005905012e00e69274b" - integrity sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg== - dependencies: - glob "^8.0.1" - ignore-walk "^5.0.1" - npm-bundled "^2.0.0" - npm-normalize-package-bin "^2.0.0" - npm-packlist@^7.0.0: version "7.0.4" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-7.0.4.tgz#033bf74110eb74daf2910dc75144411999c5ff32" @@ -5826,7 +5945,7 @@ npm-pick-manifest@^7.0.0: npm-package-arg "^9.0.0" semver "^7.3.5" -npm-pick-manifest@^8.0.0: +npm-pick-manifest@^8.0.0, npm-pick-manifest@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz#c6acd97d1ad4c5dbb80eac7b386b03ffeb289e5f" integrity sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA== @@ -5836,18 +5955,18 @@ npm-pick-manifest@^8.0.0: npm-package-arg "^10.0.0" semver "^7.3.5" -npm-registry-fetch@13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz#0ce10fa4a699a1e70685ecf41bbfb4150d74231b" - integrity sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg== +npm-registry-fetch@14.0.3, npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3: + version "14.0.3" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-14.0.3.tgz#8545e321c2b36d2c6fe6e009e77e9f0e527f547b" + integrity sha512-YaeRbVNpnWvsGOjX2wk5s85XJ7l1qQBGAp724h8e2CZFFhMSuw9enom7K1mWVUtvXO1uUSFIAPofQK0pPN0ZcA== dependencies: - make-fetch-happen "^10.0.6" - minipass "^3.1.6" - minipass-fetch "^2.0.3" + make-fetch-happen "^11.0.0" + minipass "^4.0.0" + minipass-fetch "^3.0.0" minipass-json-stream "^1.0.1" minizlib "^2.1.2" - npm-package-arg "^9.0.1" - proc-log "^2.0.0" + npm-package-arg "^10.0.0" + proc-log "^3.0.0" npm-registry-fetch@^13.0.0, npm-registry-fetch@^13.0.1: version "13.3.1" @@ -5862,19 +5981,6 @@ npm-registry-fetch@^13.0.0, npm-registry-fetch@^13.0.1: npm-package-arg "^9.0.1" proc-log "^2.0.0" -npm-registry-fetch@^14.0.0: - version "14.0.3" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-14.0.3.tgz#8545e321c2b36d2c6fe6e009e77e9f0e527f547b" - integrity sha512-YaeRbVNpnWvsGOjX2wk5s85XJ7l1qQBGAp724h8e2CZFFhMSuw9enom7K1mWVUtvXO1uUSFIAPofQK0pPN0ZcA== - dependencies: - make-fetch-happen "^11.0.0" - minipass "^4.0.0" - minipass-fetch "^3.0.0" - minipass-json-stream "^1.0.1" - minizlib "^2.1.2" - npm-package-arg "^10.0.0" - proc-log "^3.0.0" - npm-run-all@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" @@ -5904,7 +6010,7 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" -npmlog@^6.0.0, npmlog@^6.0.2: +npmlog@6.0.2, npmlog@^6.0.0, npmlog@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== @@ -5914,6 +6020,16 @@ npmlog@^6.0.0, npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" +npmlog@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-7.0.1.tgz#7372151a01ccb095c47d8bf1d0771a4ff1f53ac8" + integrity sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg== + dependencies: + are-we-there-yet "^4.0.0" + console-control-strings "^1.1.0" + gauge "^5.0.0" + set-blocking "^2.0.0" + nx@15.7.2, "nx@>=15.5.2 < 16": version "15.7.2" resolved "https://registry.yarnpkg.com/nx/-/nx-15.7.2.tgz#048f8968420f5d56a1f464a83c8c3e84dfc95bf4" @@ -5998,7 +6114,7 @@ nyc@^15.1.0: test-exclude "^6.0.0" yargs "^15.0.2" -object-inspect@^1.12.2, object-inspect@^1.9.0: +object-inspect@^1.12.2, object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== @@ -6239,10 +6355,10 @@ package-json@^8.1.0: registry-url "^6.0.0" semver "^7.3.7" -pacote@13.6.1: - version "13.6.1" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.1.tgz#ac6cbd9032b4c16e5c1e0c60138dfe44e4cc589d" - integrity sha512-L+2BI1ougAPsFjXRyBhcKmfT016NscRFLv6Pz5EiNf1CCFJFU0pSKKQwsZTyAQB+sTuUL4TyFyp6J1Ork3dOqw== +pacote@13.6.2, pacote@^13.6.1: + version "13.6.2" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.2.tgz#0d444ba3618ab3e5cd330b451c22967bbd0ca48a" + integrity sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg== dependencies: "@npmcli/git" "^3.0.0" "@npmcli/installed-package-contents" "^1.0.7" @@ -6266,10 +6382,10 @@ pacote@13.6.1: ssri "^9.0.0" tar "^6.1.11" -pacote@15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.0.tgz#2e0b12a4f55ffd801a8134a1ae28ef361dc3f243" - integrity sha512-FFcjtIl+BQNfeliSm7MZz5cpdohvUV1yjGnqgVM4UnVF7JslRY0ImXAygdaCDV0jjUADEWu4y5xsDV8brtrTLg== +pacote@15.1.1, pacote@^15.0.0, pacote@^15.0.8: + version "15.1.1" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.1.tgz#94d8c6e0605e04d427610b3aacb0357073978348" + integrity sha512-eeqEe77QrA6auZxNHIp+1TzHQ0HBKf5V6c8zcaYZ134EJe1lCi+fjXATkNiEEfbG+e50nu02GLvUtmZcGOYabQ== dependencies: "@npmcli/git" "^4.0.0" "@npmcli/installed-package-contents" "^2.0.1" @@ -6290,33 +6406,6 @@ pacote@15.1.0: ssri "^10.0.0" tar "^6.1.11" -pacote@^13.0.3, pacote@^13.6.1: - version "13.6.2" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.2.tgz#0d444ba3618ab3e5cd330b451c22967bbd0ca48a" - integrity sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg== - dependencies: - "@npmcli/git" "^3.0.0" - "@npmcli/installed-package-contents" "^1.0.7" - "@npmcli/promise-spawn" "^3.0.0" - "@npmcli/run-script" "^4.1.0" - cacache "^16.0.0" - chownr "^2.0.0" - fs-minipass "^2.1.0" - infer-owner "^1.0.4" - minipass "^3.1.6" - mkdirp "^1.0.4" - npm-package-arg "^9.0.0" - npm-packlist "^5.1.0" - npm-pick-manifest "^7.0.0" - npm-registry-fetch "^13.0.1" - proc-log "^2.0.0" - promise-retry "^2.0.1" - read-package-json "^5.0.0" - read-package-json-fast "^2.0.3" - rimraf "^3.0.2" - ssri "^9.0.0" - tar "^6.1.11" - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6324,13 +6413,13 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-conflict-json@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-2.0.2.tgz#3d05bc8ffe07d39600dc6436c6aefe382033d323" - integrity sha512-jDbRGb00TAPFsKWCpZZOT93SxVP9nONOSgES3AevqRq/CHvavEBvKAjxX9p5Y5F0RZLxH9Ufd9+RwtCsa+lFDA== +parse-conflict-json@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz#67dc55312781e62aa2ddb91452c7606d1969960c" + integrity sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw== dependencies: - json-parse-even-better-errors "^2.3.1" - just-diff "^5.0.1" + json-parse-even-better-errors "^3.0.0" + just-diff "^6.0.0" just-diff-apply "^5.2.0" parse-github-url@^1.0.2: @@ -6418,6 +6507,14 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.6.1: + version "1.6.3" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.3.tgz#4eba7183d64ef88b63c7d330bddc3ba279dc6c40" + integrity sha512-RAmB+n30SlN+HnNx6EbcpoDy9nwdpcGPnEKrJnu6GZoDWBdIjo1UQMVtW2ybtC7LC2oKLcMq8y5g8WnKLiod9g== + dependencies: + lru-cache "^7.14.1" + minipass "^4.0.2" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6490,6 +6587,14 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +postcss-selector-parser@^6.0.10: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -6507,10 +6612,19 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@2.8.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== +prettier@2.8.6: + version "2.8.6" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.6.tgz#5c174b29befd507f14b83e3c19f83fdc0e974b71" + integrity sha512-mtuzdiBbHwPEgl7NxWlqOkithPyp4VN93V7VeHVWBF+ad3I5avc0RVDT4oImXQy9H/AqxA2NSQH8pSxHW6FYbQ== + +pretty-format@29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.3.tgz#25500ada21a53c9e8423205cf0337056b201244c" + integrity sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA== + dependencies: + "@jest/schemas" "^29.4.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" proc-log@^2.0.0, proc-log@^2.0.1: version "2.0.1" @@ -6534,6 +6648,11 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -6690,17 +6809,22 @@ rc@1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + read-cmd-shim@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-3.0.0.tgz#62b8c638225c61e6cc607f8f4b779f3b8238f155" integrity sha512-KQDVjGqhZk92PPNRj9ZEXEuqg8bUobSKRw+q0YQ3TKI5xkce7bUJobL4Z/OtiEbAAv70yEpYIXp4iQ9L8oPVog== -read-cmd-shim@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-3.0.1.tgz#868c235ec59d1de2db69e11aec885bc095aea087" - integrity sha512-kEmDUoYf/CDy8yZbLTmhB1X9kkjf9Q80PCNsDMb7ufrGd6zZSQA1+UyjrO+pZm5K/S4OXCWJeiIt1JA8kAsa6g== +read-cmd-shim@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz#640a08b473a49043e394ae0c7a34dd822c73b9bb" + integrity sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q== -read-package-json-fast@^2.0.2, read-package-json-fast@^2.0.3: +read-package-json-fast@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== @@ -6708,7 +6832,7 @@ read-package-json-fast@^2.0.2, read-package-json-fast@^2.0.3: json-parse-even-better-errors "^2.3.0" npm-normalize-package-bin "^1.0.1" -read-package-json-fast@^3.0.0: +read-package-json-fast@^3.0.0, read-package-json-fast@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== @@ -6716,7 +6840,7 @@ read-package-json-fast@^3.0.0: json-parse-even-better-errors "^3.0.0" npm-normalize-package-bin "^3.0.0" -read-package-json@5.0.1: +read-package-json@5.0.1, read-package-json@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-5.0.1.tgz#1ed685d95ce258954596b13e2e0e76c7d0ab4c26" integrity sha512-MALHuNgYWdGW3gKzuNMuYtcSSZbGQm94fAp16xt8VsYTLBjUSc55bLMKe6gzpWue0Tfi6CBgwCSdDAqutGDhMg== @@ -6726,16 +6850,6 @@ read-package-json@5.0.1: normalize-package-data "^4.0.0" npm-normalize-package-bin "^1.0.1" -read-package-json@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-5.0.2.tgz#b8779ccfd169f523b67208a89cc912e3f663f3fa" - integrity sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q== - dependencies: - glob "^8.0.1" - json-parse-even-better-errors "^2.3.1" - normalize-package-data "^4.0.0" - npm-normalize-package-bin "^2.0.0" - read-package-json@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-6.0.0.tgz#6a741841ad72a40e77a82b9c3c8c10e865bbc519" @@ -6798,6 +6912,16 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba" + integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -6811,16 +6935,6 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdir-scoped-modules@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" - integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== - dependencies: - debuglog "^1.0.1" - dezalgo "^1.0.0" - graceful-fs "^4.1.2" - once "^1.3.0" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -6862,7 +6976,7 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.0.0, regexpp@^3.2.0: +regexpp@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -7006,10 +7120,12 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" - integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== +rimraf@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" + integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== + dependencies: + glob "^9.2.0" run-async@^2.4.0: version "2.4.1" @@ -7030,12 +7146,7 @@ rxjs@^7.5.5, rxjs@^7.8.0: dependencies: tslib "^2.1.0" -safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -7331,7 +7442,7 @@ ssri@9.0.1, ssri@^9.0.0: dependencies: minipass "^3.1.1" -ssri@^10.0.0: +ssri@^10.0.0, ssri@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.1.tgz#c61f85894bbc6929fc3746f05e31cf5b44c030d5" integrity sha512-WVy6di9DlPOeBWEjMScpNipeSX2jIZBGEn5Uuo8Q7aIuFEuDX0pw8RxcOjlD1TWP4obi24ki7m/13+nFpcbXrw== @@ -7388,14 +7499,7 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: +string_decoder@^1.1.1, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== @@ -7535,7 +7639,7 @@ tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@6.1.11: +tar@6.1.11, tar@^6.1.11, tar@^6.1.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -7547,23 +7651,27 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.11, tar@^6.1.2: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - temp-dir@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ== +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-1.0.0.tgz#4f192b3ee3328a2684d0e3fc5c491425395aab65" + integrity sha512-eLXG5B1G0mRPHmgH2WydPl5v4jH35qEn3y/rA/aahKhIa91Pn119SsU7n7v/433gtT9ONzC8ISvNHIh2JSTm0w== + dependencies: + del "^6.0.0" + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -7611,11 +7719,6 @@ tiny-glob@^0.2.9: globalyzer "0.1.0" globrex "^0.1.2" -tiny-typed-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" - integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7647,10 +7750,10 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -treeverse@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca" - integrity sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A== +treeverse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-3.0.0.tgz#dd82de9eb602115c6ebd77a574aae67003cb48c8" + integrity sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ== trim-newlines@^3.0.0: version "3.0.1" @@ -7739,6 +7842,11 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -7805,17 +7913,17 @@ typedoc-plugin-resolve-crossmodule-references@^0.3.3: resolved "https://registry.yarnpkg.com/typedoc-plugin-resolve-crossmodule-references/-/typedoc-plugin-resolve-crossmodule-references-0.3.3.tgz#812a6e7083f46031c69fe0021bb3913b10bd68cb" integrity sha512-ZWWBy2WR8z9a6iXYGlyB3KrpV+JDdZv1mndYU6Eh6mInrfMCrQJi3Y5K9ihMBfuaBGB//le1nEmQLgzU3IO+dw== -typedoc@^0.23.25: - version "0.23.26" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.26.tgz#ae082683698bad68757d8fe619242a56d6b5bf36" - integrity sha512-5m4KwR5tOLnk0OtMaRn9IdbeRM32uPemN9kur7YK9wFqx8U0CYrvO9aVq6ysdZSV1c824BTm+BuQl2Ze/k1HtA== +typedoc@^0.23.28: + version "0.23.28" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.28.tgz#3ce9c36ef1c273fa849d2dea18651855100d3ccd" + integrity sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w== dependencies: lunr "^2.3.9" marked "^4.2.12" minimatch "^7.1.3" shiki "^0.14.1" -"typescript@^3 || ^4", typescript@^4.6.4, typescript@^4.9.5: +"typescript@^3 || ^4", "typescript@^4.6.4 || ^5.0.0", typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -7883,6 +7991,13 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unique-string@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" @@ -7910,7 +8025,7 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -upath@^2.0.1: +upath@2.0.1, upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== @@ -7950,7 +8065,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -8186,10 +8301,10 @@ write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-file-atomic@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== +write-file-atomic@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0" + integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w== dependencies: imurmurhash "^0.1.4" signal-exit "^3.0.7" @@ -8255,12 +8370,12 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.1.3, yaml@^2.2.1: +yaml@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4" integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw== -yargs-parser@20.2.4: +yargs-parser@20.2.4, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== @@ -8278,11 +8393,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2, yargs-parser@^20.2.3: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" From a9cb5eaf22d597ccf71197afa485abf0944df129 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Fri, 24 Mar 2023 18:29:58 +0100 Subject: [PATCH 19/38] feat: add device battery changed domain event --- package.json | 2 +- ...evice-battery-update.event-handler.test.ts | 7 +- .../device-battery-update.event-handler.ts | 8 +- .../device-map-update.event-handler.ts | 4 +- ...ce-map-work-status-update.event-handler.ts | 4 +- .../device-register.event-handler.ts | 5 +- packages/adapter-tcp/src/tcp.server.ts | 4 +- .../device.aggregate-root.test.ts | 55 +++++++++++-- .../aggregate-roots/device.aggregate-root.ts | 38 ++++++--- ...ection-device-changed.domain-event.test.ts | 34 ++++++-- .../connection-device-changed.domain-event.ts | 14 +++- ...evice-battery-changed.domain-event.test.ts | 81 +++++++++++++++++++ .../device-battery-changed.domain-event.ts | 25 ++++++ .../device-connected.domain-event.ts | 2 +- .../device-locked.domain-event.ts | 2 +- .../domain/src/domain-events/domain-events.ts | 2 + .../src/event-buses/domain.event-bus.ts | 19 +---- packages/domain/src/index.ts | 7 +- packages/domain/src/test-support.ts | 11 +-- .../transport-tcp/src/packet.socket.test.ts | 4 + 20 files changed, 260 insertions(+), 68 deletions(-) create mode 100644 packages/domain/src/domain-events/device-battery-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-battery-changed.domain-event.ts diff --git a/package.json b/package.json index fedf52e5..fe1e457e 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "posttest": "sh ./scripts/merge-coverage.sh", "prepare": "husky install", "test": "npm-run-all 'test:*'", - "test:packages": "lerna run test --stream --concurrency=1", + "test:packages": "lerna run test", "version": "npm run lint:code:fix && npm run lint:style:fix" }, "config": { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts index c212e2da..bc705090 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts @@ -6,17 +6,19 @@ import { expect } from 'chai'; import { DeviceBatteryUpdateEventHandler } from './device-battery-update.event-handler'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { PacketMessage } from '../packet.message'; -import type { Device } from '@agnoc/domain'; +import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceBatteryUpdateEventHandler', function () { let deviceBatteryMapper: DeviceBatteryMapper; + let deviceRepository: DeviceRepository; let eventHandler: DeviceBatteryUpdateEventHandler; let packetMessage: PacketMessage<'PUSH_DEVICE_BATTERY_INFO_REQ'>; let device: Device; beforeEach(function () { deviceBatteryMapper = imock(); - eventHandler = new DeviceBatteryUpdateEventHandler(instance(deviceBatteryMapper)); + deviceRepository = imock(); + eventHandler = new DeviceBatteryUpdateEventHandler(instance(deviceBatteryMapper), instance(deviceRepository)); packetMessage = imock(); device = imock(); }); @@ -43,6 +45,7 @@ describe('DeviceBatteryUpdateEventHandler', function () { verify(packetMessage.assertDevice()).once(); verify(deviceBatteryMapper.toDomain(1)).once(); verify(device.updateBattery(deviceBattery)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); verify(packetMessage.respond('PUSH_DEVICE_BATTERY_INFO_RSP', deepEqual({ result: 0 }))).once(); }); }); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts index 7252c2b4..826d59bb 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts @@ -1,11 +1,15 @@ import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_BATTERY_INFO_REQ'; - constructor(private readonly deviceBatteryMapper: DeviceBatteryMapper) {} + constructor( + private readonly deviceBatteryMapper: DeviceBatteryMapper, + private readonly deviceRepository: DeviceRepository, + ) {} async handle(message: PacketMessage<'PUSH_DEVICE_BATTERY_INFO_REQ'>): Promise { message.assertDevice(); @@ -14,7 +18,7 @@ export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { message.device.updateBattery(this.deviceBatteryMapper.toDomain(data.battery.level)); - // TODO: save the entity and publish domain event + await this.deviceRepository.saveOne(message.device); await message.respond('PUSH_DEVICE_BATTERY_INFO_RSP', { result: 0 }); } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index 21a5d52b..241726c6 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -17,6 +17,7 @@ import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; import type { DeviceStateMapper } from '../mappers/device-state.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'; @@ -27,6 +28,7 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { private readonly deviceStateMapper: DeviceStateMapper, private readonly deviceErrorMapper: DeviceErrorMapper, private readonly deviceFanSpeedMapper: DeviceFanSpeedMapper, + private readonly deviceRepository: DeviceRepository, ) {} async handle(message: PacketMessage<'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'>): Promise { @@ -192,6 +194,6 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { } } - // TODO: save entities and publish domain events + await this.deviceRepository.saveOne(message.device); } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index 84d320cd..4b791206 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -8,6 +8,7 @@ import type { DeviceStateMapper } from '../mappers/device-state.mapper'; import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'; @@ -19,6 +20,7 @@ export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler private readonly deviceBatteryMapper: DeviceBatteryMapper, private readonly deviceFanSpeedMapper: DeviceFanSpeedMapper, private readonly deviceWaterLevelMapper: DeviceWaterLevelMapper, + private readonly deviceRepository: DeviceRepository, ) {} async handle(message: PacketMessage<'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'>): Promise { @@ -59,6 +61,6 @@ export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler message.device.updateWaterLevel(this.deviceWaterLevelMapper.toDomain(waterLevel)); } - // TODO: save entity and publish domain events + await this.deviceRepository.saveOne(message.device); } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts index ba998bd0..2a26bd6f 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts @@ -1,4 +1,4 @@ -import { Device, DeviceSystem, DeviceVersion } from '@agnoc/domain'; +import { Device, DeviceBattery, DeviceBatteryMaxValue, DeviceSystem, DeviceVersion } from '@agnoc/domain'; import { ID } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -16,6 +16,9 @@ export class DeviceRegisterEventHandler implements PacketEventHandler { userId: ID.generate(), system: new DeviceSystem({ type: data.deviceType, serialNumber: data.deviceSerialNumber }), version: new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion }), + battery: new DeviceBattery(DeviceBatteryMaxValue), + isConnected: false, + isLocked: false, }); // TODO: publish device created domain event diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index e1792bd9..edca2285 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -107,7 +107,7 @@ export class TCPServer implements Server { packetEventHandlerRegistry.register( new ClientHeartbeatEventHandler(), new ClientLoginEventHandler(), - new DeviceBatteryUpdateEventHandler(deviceBatteryMapper), + new DeviceBatteryUpdateEventHandler(deviceBatteryMapper, deviceRepository), new DeviceCleanMapDataReportEventHandler(), new DeviceCleanMapReportEventHandler(), new DeviceCleanTaskReportEventHandler(), @@ -122,6 +122,7 @@ export class TCPServer implements Server { deviceBatteryMapper, deviceFanSpeedMapper, deviceWaterLevelMapper, + deviceRepository, ), new DeviceMemoryMapInfoEventHandler(), new DeviceOfflineEventHandler(), @@ -137,6 +138,7 @@ export class TCPServer implements Server { deviceStateMapper, deviceErrorMapper, deviceFanSpeedMapper, + deviceRepository, ), ); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 71fd4f7a..71db9b3b 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -1,5 +1,6 @@ import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; import { expect } from 'chai'; +import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; @@ -43,8 +44,9 @@ describe('Device', function () { expect(device.userId).to.be.equal(deviceProps.userId); expect(device.system).to.be.equal(deviceProps.system); expect(device.version).to.be.equal(deviceProps.version); - expect(device.isConnected).to.be.false; - expect(device.isLocked).to.be.false; + expect(device.battery).to.be.equal(deviceProps.battery); + expect(device.isConnected).to.be.equal(deviceProps.isConnected); + expect(device.isLocked).to.be.equal(deviceProps.isLocked); }); it("should throw an error when 'userId' is not provided", function () { @@ -71,6 +73,30 @@ describe('Device', function () { ); }); + it("should throw an error when 'battery' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new Device({ ...givenSomeDeviceProps(), battery: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'battery' for Device not provided`, + ); + }); + + it("should throw an error when 'isConnected' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new Device({ ...givenSomeDeviceProps(), isConnected: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'isConnected' for Device not provided`, + ); + }); + + it("should throw an error when 'isLocked' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new Device({ ...givenSomeDeviceProps(), isLocked: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'isLocked' for Device not provided`, + ); + }); + it("should throw an error when 'userId' is not an ID", function () { // @ts-expect-error - invalid property expect(() => new Device({ ...givenSomeDeviceProps(), userId: 'foo' })).to.throw( @@ -351,12 +377,29 @@ describe('Device', function () { describe('#updateBattery()', function () { it('should update the device battery', function () { - const device = new Device(givenSomeDeviceProps()); - const battery = new DeviceBattery(50); + const previousBattery = new DeviceBattery(60); + const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); + const currentBattery = new DeviceBattery(50); + + device.updateBattery(currentBattery); + + expect(device.battery).to.be.equal(currentBattery); + expect(device.domainEvents).to.deep.contain( + new DeviceBatteryChangedDomainEvent({ aggregateId: device.id, previousBattery, currentBattery }), + ); + }); + + it('should not update the device battery when value is equal', function () { + const previousBattery = new DeviceBattery(50); + const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); + const currentBattery = new DeviceBattery(50); - device.updateBattery(battery); + device.updateBattery(currentBattery); - expect(device.battery).to.be.equal(battery); + expect(device.battery).to.be.equal(previousBattery); + expect(device.domainEvents).to.not.deep.contain( + new DeviceBatteryChangedDomainEvent({ aggregateId: device.id, previousBattery, currentBattery }), + ); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index a98654a7..cc325fc9 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -1,4 +1,5 @@ import { AggregateRoot, ID } from '@agnoc/toolkit'; +import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; @@ -25,10 +26,12 @@ export interface DeviceProps extends EntityProps { system: DeviceSystem; /** The device version. */ version: DeviceVersion; + /** The device battery. */ + battery: DeviceBattery; /** Whether the device is connected. */ - isConnected?: boolean; + isConnected: boolean; /** Whether the device is locked. */ - isLocked?: boolean; + isLocked: boolean; /** The device settings. */ config?: DeviceSettings; /** The device current clean. */ @@ -41,8 +44,6 @@ export interface DeviceProps extends EntityProps { map?: DeviceMap; /** The device wlan. */ wlan?: DeviceWlan; - /** The device battery. */ - battery?: DeviceBattery; /** The device state. */ state?: DeviceState; /** The device mode. */ @@ -71,6 +72,11 @@ export class Device extends AggregateRoot { return this.props.system; } + /** Returns the device battery. */ + get battery(): DeviceBattery { + return this.props.battery; + } + /** Returns whether the device is connected. */ get isConnected(): boolean { return this.props.isConnected ?? false; @@ -116,11 +122,6 @@ export class Device extends AggregateRoot { return this.props.wlan; } - /** Returns the device battery. */ - get battery(): DeviceBattery | undefined { - return this.props.battery; - } - /** Returns the device state. */ get state(): DeviceState | undefined { return this.props.state; @@ -158,14 +159,14 @@ export class Device extends AggregateRoot { /** Sets the device as connected. */ setAsConnected(): void { - this.props.isConnected = true; this.addEvent(new DeviceConnectedDomainEvent({ aggregateId: this.id })); + this.props.isConnected = true; } /** Sets the device as connected. */ setAsLocked(): void { - this.props.isLocked = true; this.addEvent(new DeviceLockedDomainEvent({ aggregateId: this.id })); + this.props.isLocked = true; } /** Updates the device system. */ @@ -219,8 +220,19 @@ export class Device extends AggregateRoot { } /** Updates the device battery. */ - updateBattery(battery?: DeviceBattery): void { + updateBattery(battery: DeviceBattery): void { + if (this.battery.equals(battery)) { + return; + } + this.validateInstanceProp({ battery }, 'battery', DeviceBattery); + this.addEvent( + new DeviceBatteryChangedDomainEvent({ + aggregateId: this.id, + previousBattery: this.props.battery, + currentBattery: battery, + }), + ); this.props.battery = battery; } @@ -267,7 +279,7 @@ export class Device extends AggregateRoot { } protected validate(props: DeviceProps): void { - const keys: (keyof DeviceProps)[] = ['userId', 'system', 'version']; + const keys: (keyof DeviceProps)[] = ['userId', 'system', 'version', 'battery', 'isConnected', 'isLocked']; keys.forEach((prop) => { this.validateDefinedProp(props, prop); diff --git a/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts b/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts index b3ecba11..f2c3eeae 100644 --- a/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts +++ b/packages/domain/src/domain-events/connection-device-changed.domain-event.test.ts @@ -1,11 +1,10 @@ -import { ID, DomainEvent } from '@agnoc/toolkit'; +import { ID, DomainEvent, ArgumentInvalidException } from '@agnoc/toolkit'; import { expect } from 'chai'; -import { givenSomeConnectionDeviceChangedDomainEventProps } from '../test-support'; import { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; describe('ConnectionDeviceChangedDomainEvent', function () { it('should be created', function () { - const props = givenSomeConnectionDeviceChangedDomainEventProps(); + const props = { aggregateId: ID.generate() }; const event = new ConnectionDeviceChangedDomainEvent(props); expect(event).to.be.instanceOf(DomainEvent); @@ -15,17 +14,38 @@ describe('ConnectionDeviceChangedDomainEvent', function () { }); it('should be created with previousDeviceId', function () { - const props = { ...givenSomeConnectionDeviceChangedDomainEventProps(), previousDeviceId: ID.generate() }; + const props = { aggregateId: ID.generate(), previousDeviceId: ID.generate() }; const event = new ConnectionDeviceChangedDomainEvent(props); - expect(event.previousDeviceId).to.be.equal(event.previousDeviceId); + expect(event.previousDeviceId).to.be.equal(props.previousDeviceId); expect(event.currentDeviceId).to.be.undefined; }); it('should be created with currentDeviceId', function () { - const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: ID.generate() }); + const props = { aggregateId: ID.generate(), currentDeviceId: ID.generate() }; + const event = new ConnectionDeviceChangedDomainEvent(props); expect(event.previousDeviceId).to.be.undefined; - expect(event.previousDeviceId).to.be.equal(event.previousDeviceId); + expect(event.currentDeviceId).to.be.equal(props.currentDeviceId); + }); + + it("should thrown an error when 'previousDeviceId' is not an instance of ID", function () { + expect( + // @ts-expect-error - invalid property + () => new ConnectionDeviceChangedDomainEvent({ aggregateId: ID.generate(), previousDeviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousDeviceId' of ConnectionDeviceChangedDomainEvent is not an instance of ID", + ); + }); + + it("should thrown an error when 'currentDeviceId' is not an instance of ID", function () { + expect( + // @ts-expect-error - invalid property + () => new ConnectionDeviceChangedDomainEvent({ aggregateId: ID.generate(), currentDeviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentDeviceId' of ConnectionDeviceChangedDomainEvent is not an instance of ID", + ); }); }); diff --git a/packages/domain/src/domain-events/connection-device-changed.domain-event.ts b/packages/domain/src/domain-events/connection-device-changed.domain-event.ts index 7dbf7ec8..cda68e96 100644 --- a/packages/domain/src/domain-events/connection-device-changed.domain-event.ts +++ b/packages/domain/src/domain-events/connection-device-changed.domain-event.ts @@ -1,5 +1,5 @@ -import { DomainEvent } from '@agnoc/toolkit'; -import type { DomainEventProps, ID } from '@agnoc/toolkit'; +import { DomainEvent, ID } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; export interface ConnectionDeviceChangedDomainEventProps extends DomainEventProps { previousDeviceId?: ID; @@ -15,7 +15,13 @@ export class ConnectionDeviceChangedDomainEvent extends DomainEvent + new DeviceBatteryChangedDomainEvent({ + ...givenSomeDeviceBatteryChangedDomainEventProps(), + // @ts-expect-error - missing property + previousBattery: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'previousBattery' for DeviceBatteryChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'currentBattery' is not provided", function () { + expect( + () => + new DeviceBatteryChangedDomainEvent({ + ...givenSomeDeviceBatteryChangedDomainEventProps(), + // @ts-expect-error - missing property + currentBattery: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentBattery' for DeviceBatteryChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousBattery' is not an instance of ID", function () { + expect( + () => + new DeviceBatteryChangedDomainEvent({ + ...givenSomeDeviceBatteryChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousBattery: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousBattery' of DeviceBatteryChangedDomainEvent is not an instance of DeviceBattery", + ); + }); + + it("should thrown an error when 'currentBattery' is not an instance of ID", function () { + expect( + () => + new DeviceBatteryChangedDomainEvent({ + ...givenSomeDeviceBatteryChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentBattery: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentBattery' of DeviceBatteryChangedDomainEvent is not an instance of DeviceBattery", + ); + }); +}); + +function givenSomeDeviceBatteryChangedDomainEventProps(): DeviceBatteryChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousBattery: new DeviceBattery(50), + currentBattery: new DeviceBattery(100), + }; +} diff --git a/packages/domain/src/domain-events/device-battery-changed.domain-event.ts b/packages/domain/src/domain-events/device-battery-changed.domain-event.ts new file mode 100644 index 00000000..791e2c5b --- /dev/null +++ b/packages/domain/src/domain-events/device-battery-changed.domain-event.ts @@ -0,0 +1,25 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceBatteryChangedDomainEventProps extends DomainEventProps { + previousBattery: DeviceBattery; + currentBattery: DeviceBattery; +} + +export class DeviceBatteryChangedDomainEvent extends DomainEvent { + get previousBattery(): DeviceBattery | undefined { + return this.props.previousBattery; + } + + get currentBattery(): DeviceBattery | undefined { + return this.props.currentBattery; + } + + protected validate(props: DeviceBatteryChangedDomainEventProps): void { + this.validateDefinedProp(props, 'previousBattery'); + this.validateInstanceProp(props, 'previousBattery', DeviceBattery); + this.validateDefinedProp(props, 'currentBattery'); + this.validateInstanceProp(props, 'currentBattery', DeviceBattery); + } +} diff --git a/packages/domain/src/domain-events/device-connected.domain-event.ts b/packages/domain/src/domain-events/device-connected.domain-event.ts index 07c9c6d9..ceae3c0a 100644 --- a/packages/domain/src/domain-events/device-connected.domain-event.ts +++ b/packages/domain/src/domain-events/device-connected.domain-event.ts @@ -2,7 +2,7 @@ import { DomainEvent } from '@agnoc/toolkit'; import type { DomainEventProps } from '@agnoc/toolkit'; export class DeviceConnectedDomainEvent extends DomainEvent { - protected validate(_: DeviceConnectedDomainEvent): void { + protected validate(_: DomainEventProps): void { // noop } } diff --git a/packages/domain/src/domain-events/device-locked.domain-event.ts b/packages/domain/src/domain-events/device-locked.domain-event.ts index 3cf6d7af..fea0583d 100644 --- a/packages/domain/src/domain-events/device-locked.domain-event.ts +++ b/packages/domain/src/domain-events/device-locked.domain-event.ts @@ -2,7 +2,7 @@ import { DomainEvent } from '@agnoc/toolkit'; import type { DomainEventProps } from '@agnoc/toolkit'; export class DeviceLockedDomainEvent extends DomainEvent { - protected validate(_: DeviceLockedDomainEvent): void { + protected validate(_: DomainEventProps): void { // noop } } diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index 99dde318..b6f5fa49 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -1,4 +1,5 @@ import type { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; +import type { DeviceBatteryChangedDomainEvent } from './device-battery-changed.domain-event'; import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; @@ -6,6 +7,7 @@ export type DomainEvents = { DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; DeviceLockedDomainEvent: DeviceLockedDomainEvent; ConnectionDeviceChangedDomainEvent: ConnectionDeviceChangedDomainEvent; + DeviceBatteryChangedDomainEvent: DeviceBatteryChangedDomainEvent; }; export type DomainEventNames = keyof DomainEvents; diff --git a/packages/domain/src/event-buses/domain.event-bus.ts b/packages/domain/src/event-buses/domain.event-bus.ts index d993dc3f..4d2754a6 100644 --- a/packages/domain/src/event-buses/domain.event-bus.ts +++ b/packages/domain/src/event-buses/domain.event-bus.ts @@ -1,20 +1,5 @@ -import { EventBus, debug } from '@agnoc/toolkit'; +import { EventBus } from '@agnoc/toolkit'; import type { DomainEventNames, DomainEvents } from '../domain-events/domain-events'; export type DomainEventBusEvents = { [Name in DomainEventNames]: DomainEvents[Name] }; -export class DomainEventBus extends EventBus { - constructor() { - /* istanbul ignore next */ - super({ - debug: { - enabled: true, - name: DomainEventBus.name, - logger: (type, _, eventName, eventData) => { - debug(__filename).extend(type)( - `event '${eventName?.toString() ?? 'undefined'}' with data: ${JSON.stringify(eventData)}`, - ); - }, - }, - }); - } -} +export class DomainEventBus extends EventBus {} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 8ef919c8..0426cda6 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -3,6 +3,7 @@ export * from './aggregate-roots/device.aggregate-root'; export * from './commands/commands'; export * from './commands/locate-device.command'; export * from './domain-events/connection-device-changed.domain-event'; +export * from './domain-events/device-battery-changed.domain-event'; export * from './domain-events/device-connected.domain-event'; export * from './domain-events/device-locked.domain-event'; export * from './domain-events/domain-events'; @@ -23,6 +24,9 @@ export * from './event-buses/command-query.task-bus'; export * from './event-buses/domain.event-bus'; export * from './event-handlers/command.task-handler'; export * from './event-handlers/domain.event-handler'; +export * from './event-handlers/query.task-handler'; +export * from './queries/find-device.query'; +export * from './queries/queries'; export * from './repositories/connection.repository'; export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; @@ -38,6 +42,3 @@ export * from './value-objects/map-pixel.value-object'; export * from './value-objects/map-position.value-object'; export * from './value-objects/quiet-hours-setting.value-object'; export * from './value-objects/voice-setting.value-object'; -export * from './queries/find-device.query'; -export * from './queries/queries'; -export * from './event-handlers/query.task-handler'; diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index 47f0ba5f..5590806a 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -1,6 +1,7 @@ import { ID } from '@agnoc/toolkit'; import { CleanMode, CleanModeValue } from './domain-primitives/clean-mode.domain-primitive'; import { CleanSize } from './domain-primitives/clean-size.domain-primitive'; +import { DeviceBattery } from './domain-primitives/device-battery.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from './domain-primitives/device-fan-speed.domain-primitive'; import { DeviceWaterLevel, DeviceWaterLevelValue } from './domain-primitives/device-water-level.domain-primitive'; import { WeekDay, WeekDayValue } from './domain-primitives/week-day.domain-primitive'; @@ -17,7 +18,6 @@ import { QuietHoursSetting } from './value-objects/quiet-hours-setting.value-obj import { VoiceSetting } from './value-objects/voice-setting.value-object'; import type { ConnectionProps } from './aggregate-roots/connection.aggregate-root'; import type { DeviceProps } from './aggregate-roots/device.aggregate-root'; -import type { ConnectionDeviceChangedDomainEventProps } from './domain-events/connection-device-changed.domain-event'; import type { DeviceMapProps } from './entities/device-map.entity'; import type { DeviceOrderProps } from './entities/device-order.entity'; import type { RoomProps } from './entities/room.entity'; @@ -189,6 +189,9 @@ export function givenSomeDeviceProps(): DeviceProps { userId: ID.generate(), system: new DeviceSystem(givenSomeDeviceSystemProps()), version: new DeviceVersion(givenSomeDeviceVersionProps()), + battery: new DeviceBattery(100), + isConnected: false, + isLocked: false, }; } @@ -197,9 +200,3 @@ export function givenSomeConnectionProps(): ConnectionProps { id: ID.generate(), }; } - -export function givenSomeConnectionDeviceChangedDomainEventProps(): ConnectionDeviceChangedDomainEventProps { - return { - aggregateId: ID.generate(), - }; -} diff --git a/packages/transport-tcp/src/packet.socket.test.ts b/packages/transport-tcp/src/packet.socket.test.ts index 7fadf0b1..2081ff95 100644 --- a/packages/transport-tcp/src/packet.socket.test.ts +++ b/packages/transport-tcp/src/packet.socket.test.ts @@ -22,6 +22,10 @@ describe('PacketSocket', function () { }); afterEach(function (done) { + if (packetSocket.connected) { + void packetSocket.end(); + } + if (server.listening) { server.close(done); } else { From 858b6f336866988c4dd2dd619d1e9be2ff1220f1 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Fri, 24 Mar 2023 19:35:00 +0100 Subject: [PATCH 20/38] feat: add more domain events --- .../device-located.event-handler.test.ts | 24 +++++ .../device-located.event-handler.ts | 7 +- .../device-offline.event-handler.test.ts | 24 +++++ .../device-offline.event-handler.ts | 7 +- .../device-register.event-handler.test.ts | 63 ++++++++++++++ .../device-register.event-handler.ts | 2 - .../device-time-update.event-handler.test.ts | 24 +++++ .../device-time-update.event-handler.ts | 13 +-- .../device-wlan-update.event-handler.test.ts | 63 ++++++++++++++ .../device-wlan-update.event-handler.ts | 10 +-- packages/adapter-tcp/src/tcp.server.ts | 2 +- .../connection.aggregate-root.test.ts | 42 ++++----- .../device.aggregate-root.test.ts | 87 +++++++++++++++---- .../aggregate-roots/device.aggregate-root.ts | 34 +++++++- .../device-battery-changed.domain-event.ts | 4 +- .../device-created.domain-event.test.ts | 11 +++ .../device-created.domain-event.ts | 8 ++ .../device-locked.domain-event.test.ts | 11 +++ .../device-wlan-changed.domain-event.test.ts | 75 ++++++++++++++++ .../device-wlan-changed.domain-event.ts | 27 ++++++ packages/domain/src/index.ts | 2 + .../base-classes/aggregate-root.base.test.ts | 7 +- .../src/base-classes/aggregate-root.base.ts | 9 +- .../base-classes/domain-event.base.test.ts | 3 + .../src/base-classes/domain-event.base.ts | 11 ++- .../src/base-classes/task.base.test.ts | 5 ++ .../toolkit/src/base-classes/task.base.ts | 12 +++ 27 files changed, 508 insertions(+), 79 deletions(-) create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts create mode 100644 packages/domain/src/domain-events/device-created.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-created.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-locked.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-wlan-changed.domain-event.ts diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts new file mode 100644 index 00000000..31c2b4e6 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts @@ -0,0 +1,24 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceLocatedEventHandler } from './device-located.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceLocatedEventHandler', function () { + let eventHandler: DeviceLocatedEventHandler; + let packetMessage: PacketMessage<'DEVICE_SEEK_LOCATION_RSP'>; + + beforeEach(function () { + eventHandler = new DeviceLocatedEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_SEEK_LOCATION_RSP'); + }); + + describe('#handle()', function () { + it('should do nothing', async function () { + await eventHandler.handle(instance(packetMessage)); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts index 833b40a5..6b12e9a3 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts @@ -1,15 +1,10 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceLocatedEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_SEEK_LOCATION_RSP'; - async handle(message: PacketMessage<'DEVICE_SEEK_LOCATION_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - + async handle(_: PacketMessage<'DEVICE_SEEK_LOCATION_RSP'>): Promise { // TODO: Should do something here? } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts new file mode 100644 index 00000000..f966f8ad --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts @@ -0,0 +1,24 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceOfflineEventHandler } from './device-offline.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceOfflineEventHandler', function () { + let eventHandler: DeviceOfflineEventHandler; + let packetMessage: PacketMessage<'DEVICE_OFFLINE_CMD'>; + + beforeEach(function () { + eventHandler = new DeviceOfflineEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_OFFLINE_CMD'); + }); + + describe('#handle()', function () { + it('should do nothing', async function () { + await eventHandler.handle(instance(packetMessage)); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts index c21de12c..2e0b94da 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts @@ -1,13 +1,10 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceOfflineEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_OFFLINE_CMD'; - async handle(message: PacketMessage<'DEVICE_OFFLINE_CMD'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + async handle(_: PacketMessage<'DEVICE_OFFLINE_CMD'>): Promise { + // TODO: Should do something here? } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts new file mode 100644 index 00000000..005db065 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts @@ -0,0 +1,63 @@ +import { Device } from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceRegisterEventHandler } from './device-register.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; + +describe('DeviceRegisterEventHandler', function () { + let eventHandler: DeviceRegisterEventHandler; + let packetMessage: PacketMessage<'DEVICE_REGISTER_REQ'>; + let deviceRepository: DeviceRepository; + + beforeEach(function () { + deviceRepository = imock(); + eventHandler = new DeviceRegisterEventHandler(instance(deviceRepository)); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_REGISTER_REQ'); + }); + + describe('#handle()', function () { + it('should create a device', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_REGISTER_REQ'), + data: { + deviceType: 9, + deviceSerialNumber: '1234567890', + softwareVersion: '1.0.0', + hardwareVersion: '1.0.0', + deviceMac: '00:00:00:00:00:00', + customerFirmwareId: 1, + ctrlVersion: '1.0.0', + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + const [device] = capture(deviceRepository.saveOne).first(); + + expect(device).to.be.instanceOf(Device); + expect(device.id).to.exist; + expect(device.userId).to.exist; + expect(device.system.type).to.be.equal(9); + expect(device.system.serialNumber).to.be.equal('1234567890'); + expect(device.version.software).to.be.equal('1.0.0'); + expect(device.version.hardware).to.be.equal('1.0.0'); + expect(device.battery.value).to.be.equal(100); + expect(device.isConnected).to.be.false; + expect(device.isLocked).to.be.false; + + verify( + packetMessage.respond('DEVICE_REGISTER_RSP', deepEqual({ result: 0, device: { id: device.id.value } })), + ).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts index 2a26bd6f..f7377910 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts @@ -21,8 +21,6 @@ export class DeviceRegisterEventHandler implements PacketEventHandler { isLocked: false, }); - // TODO: publish device created domain event - await this.deviceRepository.saveOne(device); const response = { result: 0, device: { id: device.id.value } }; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts new file mode 100644 index 00000000..8a874c4c --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts @@ -0,0 +1,24 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceTimeUpdateEventHandler } from './device-time-update.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceTimeUpdateEventHandler', function () { + let eventHandler: DeviceTimeUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_GETTIME_RSP'>; + + beforeEach(function () { + eventHandler = new DeviceTimeUpdateEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_GETTIME_RSP'); + }); + + describe('#handle()', function () { + it('should do nothing', async function () { + await eventHandler.handle(instance(packetMessage)); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts index d35d9def..4f191fa1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts @@ -1,19 +1,10 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceTimeUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GETTIME_RSP'; - async handle(message: PacketMessage<'DEVICE_GETTIME_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - - // TODO: save device time - // { - // timestamp: object.body.deviceTime * 1000, - // offset: -1 * ((object.body.deviceTimezone || 0) / 60), - // }; + async handle(_: PacketMessage<'DEVICE_GETTIME_RSP'>): Promise { + // TODO: Should do something here? } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts new file mode 100644 index 00000000..ae9607b7 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts @@ -0,0 +1,63 @@ +import { DeviceWlan } from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceWlanUpdateEventHandler } from './device-wlan-update.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceWlanUpdateEventHandler', function () { + let deviceRepository: DeviceRepository; + let eventHandler: DeviceWlanUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_WLAN_INFO_GETTING_RSP'>; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + eventHandler = new DeviceWlanUpdateEventHandler(instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_WLAN_INFO_GETTING_RSP'); + }); + + describe('#handle()', function () { + it('should update the device wlan', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_WLAN_INFO_GETTING_RSP'), + data: { + result: 0, + body: { + ipv4: '127.0.0.1', + ssid: 'ssid', + port: 1234, + mask: '255.255.255.255', + mac: '00:00:00:00:00:00', + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + const [deviceWlan] = capture(device.updateWlan).first(); + + expect(deviceWlan).to.be.instanceOf(DeviceWlan); + expect(deviceWlan.ipv4).to.be.equal('127.0.0.1'); + expect(deviceWlan.ssid).to.be.equal('ssid'); + expect(deviceWlan.port).to.be.equal(1234); + expect(deviceWlan.mask).to.be.equal('255.255.255.255'); + expect(deviceWlan.mac).to.be.equal('00:00:00:00:00:00'); + + verify(packetMessage.assertDevice()).once(); + verify(device.updateWlan(deviceWlan)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts index b4cccff1..90d13980 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts @@ -1,15 +1,15 @@ import { DeviceWlan } from '@agnoc/domain'; -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceWlanUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_WLAN_INFO_GETTING_RSP'; + constructor(private readonly deviceRepository: DeviceRepository) {} + async handle(message: PacketMessage<'DEVICE_WLAN_INFO_GETTING_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const data = message.packet.payload.data.body; @@ -23,6 +23,6 @@ export class DeviceWlanUpdateEventHandler implements PacketEventHandler { }), ); - // TODO: save entity and publish domain event + await this.deviceRepository.saveOne(message.device); } } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index edca2285..953df990 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -131,7 +131,7 @@ export class TCPServer implements Server { new DeviceTimeUpdateEventHandler(), new DeviceUpgradeInfoEventHandler(), new DeviceVersionUpdateEventHandler(), - new DeviceWlanUpdateEventHandler(), + new DeviceWlanUpdateEventHandler(deviceRepository), new DeviceMapUpdateEventHandler( deviceBatteryMapper, deviceModeMapper, diff --git a/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts index b3bc053d..398336fb 100644 --- a/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts @@ -38,13 +38,13 @@ describe('Connection', function () { connection.setDevice(device); expect(connection.device).to.be.equal(device); - expect(connection.domainEvents).to.deep.contain( - new ConnectionDeviceChangedDomainEvent({ - aggregateId: connection.id, - previousDeviceId: undefined, - currentDeviceId: device.id, - }), - ); + + const event = connection.domainEvents[0] as ConnectionDeviceChangedDomainEvent; + + expect(event).to.be.instanceOf(ConnectionDeviceChangedDomainEvent); + expect(event.aggregateId).to.be.equal(connection.id); + expect(event.previousDeviceId).to.be.equal(undefined); + expect(event.currentDeviceId).to.be.equal(device.id); }); it('should override a device', function () { @@ -55,13 +55,13 @@ describe('Connection', function () { connection.setDevice(deviceB); expect(connection.device).to.be.equal(deviceB); - expect(connection.domainEvents).to.deep.contain( - new ConnectionDeviceChangedDomainEvent({ - aggregateId: connection.id, - previousDeviceId: deviceA.id, - currentDeviceId: deviceB.id, - }), - ); + + const event = connection.domainEvents[0] as ConnectionDeviceChangedDomainEvent; + + expect(event).to.be.instanceOf(ConnectionDeviceChangedDomainEvent); + expect(event.aggregateId).to.be.equal(connection.id); + expect(event.previousDeviceId).to.be.equal(deviceA.id); + expect(event.currentDeviceId).to.be.equal(deviceB.id); }); it('should unset a device', function () { @@ -71,13 +71,13 @@ describe('Connection', function () { connection.setDevice(undefined); expect(connection.device).to.be.equal(undefined); - expect(connection.domainEvents).to.deep.contain( - new ConnectionDeviceChangedDomainEvent({ - aggregateId: connection.id, - previousDeviceId: device.id, - currentDeviceId: undefined, - }), - ); + + const event = connection.domainEvents[0] as ConnectionDeviceChangedDomainEvent; + + expect(event).to.be.instanceOf(ConnectionDeviceChangedDomainEvent); + expect(event.aggregateId).to.be.equal(connection.id); + expect(event.previousDeviceId).to.be.equal(device.id); + expect(event.currentDeviceId).to.be.equal(undefined); }); it('should do nothing when setting the same device', function () { diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 71db9b3b..97324760 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -2,7 +2,9 @@ import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } import { expect } from 'chai'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceWlanChangedDomainEvent } from '../domain-events/device-wlan-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -47,6 +49,11 @@ describe('Device', function () { expect(device.battery).to.be.equal(deviceProps.battery); expect(device.isConnected).to.be.equal(deviceProps.isConnected); expect(device.isLocked).to.be.equal(deviceProps.isLocked); + + const event = device.domainEvents[0] as DeviceCreatedDomainEvent; + + expect(event).to.be.instanceOf(DeviceCreatedDomainEvent); + expect(event.aggregateId).to.be.equal(device.id); }); it("should throw an error when 'userId' is not provided", function () { @@ -266,24 +273,51 @@ describe('Device', function () { }); describe('#setAsConnected()', function () { - it('should update the device system', function () { + it('should set the device as connected', function () { const device = new Device({ ...givenSomeDeviceProps(), isConnected: false }); device.setAsConnected(); expect(device.isConnected).to.be.true; - expect(device.domainEvents).to.deep.contain(new DeviceConnectedDomainEvent({ aggregateId: device.id })); + + const event = device.domainEvents[1] as DeviceConnectedDomainEvent; + + expect(event).to.be.instanceOf(DeviceConnectedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + }); + + it('should not set the device as connected when is connected', function () { + const device = new Device({ ...givenSomeDeviceProps(), isConnected: true }); + + device.setAsConnected(); + + expect(device.isConnected).to.be.true; + + expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceConnectedDomainEvent); }); }); describe('#setAsLocked()', function () { - it('should update the device system', function () { + it('should set the device as locked', function () { const device = new Device({ ...givenSomeDeviceProps(), isLocked: false }); device.setAsLocked(); expect(device.isLocked).to.be.true; - expect(device.domainEvents).to.deep.contain(new DeviceLockedDomainEvent({ aggregateId: device.id })); + + const event = device.domainEvents[1] as DeviceLockedDomainEvent; + + expect(event).to.be.instanceOf(DeviceLockedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + }); + + it('should not set the device as locked', function () { + const device = new Device({ ...givenSomeDeviceProps(), isLocked: true }); + + device.setAsLocked(); + + expect(device.isLocked).to.be.true; + expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceLockedDomainEvent); }); }); @@ -366,40 +400,61 @@ describe('Device', function () { describe('#updateWlan()', function () { it('should update the device wlan', function () { - const device = new Device(givenSomeDeviceProps()); - const wlan = new DeviceWlan(givenSomeDeviceWlanProps()); + const previousWlan = new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: 1 }); + const currentWlan = new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: 2 }); + const device = new Device({ ...givenSomeDeviceProps(), wlan: previousWlan }); - device.updateWlan(wlan); + device.updateWlan(currentWlan); - expect(device.wlan).to.be.equal(wlan); + expect(device.wlan).to.be.equal(currentWlan); + + const event = device.domainEvents[1] as DeviceWlanChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceWlanChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousWlan).to.be.equal(previousWlan); + expect(event.currentWlan).to.be.equal(currentWlan); + }); + + it('should not update the device wlan when value is equal', function () { + const previousWlan = new DeviceWlan(givenSomeDeviceWlanProps()); + const currentWlan = new DeviceWlan(givenSomeDeviceWlanProps()); + const device = new Device({ ...givenSomeDeviceProps(), wlan: previousWlan }); + + device.updateWlan(currentWlan); + + expect(device.wlan).to.be.equal(previousWlan); + expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceWlanChangedDomainEvent); }); }); describe('#updateBattery()', function () { it('should update the device battery', function () { const previousBattery = new DeviceBattery(60); - const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); const currentBattery = new DeviceBattery(50); + const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); device.updateBattery(currentBattery); expect(device.battery).to.be.equal(currentBattery); - expect(device.domainEvents).to.deep.contain( - new DeviceBatteryChangedDomainEvent({ aggregateId: device.id, previousBattery, currentBattery }), - ); + + const event = device.domainEvents[1] as DeviceBatteryChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceBatteryChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousBattery).to.be.equal(previousBattery); + expect(event.currentBattery).to.be.equal(currentBattery); }); it('should not update the device battery when value is equal', function () { const previousBattery = new DeviceBattery(50); - const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); const currentBattery = new DeviceBattery(50); + const device = new Device({ ...givenSomeDeviceProps(), battery: previousBattery }); device.updateBattery(currentBattery); expect(device.battery).to.be.equal(previousBattery); - expect(device.domainEvents).to.not.deep.contain( - new DeviceBatteryChangedDomainEvent({ aggregateId: device.id, previousBattery, currentBattery }), - ); + expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceBatteryChangedDomainEvent); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index cc325fc9..f43fb0a7 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -1,7 +1,9 @@ import { AggregateRoot, ID } from '@agnoc/toolkit'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceWlanChangedDomainEvent } from '../domain-events/device-wlan-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -62,6 +64,11 @@ export interface DeviceProps extends EntityProps { /** Describes a device. */ export class Device extends AggregateRoot { + constructor(props: DeviceProps) { + super(props); + this.addEvent(new DeviceCreatedDomainEvent({ aggregateId: this.id })); + } + /** Returns the user id. */ get userId(): ID { return this.props.userId; @@ -79,12 +86,12 @@ export class Device extends AggregateRoot { /** Returns whether the device is connected. */ get isConnected(): boolean { - return this.props.isConnected ?? false; + return this.props.isConnected; } /** Returns whether the device is locked. */ get isLocked(): boolean { - return this.props.isLocked ?? false; + return this.props.isLocked; } /** Returns the device version. */ @@ -159,12 +166,20 @@ export class Device extends AggregateRoot { /** Sets the device as connected. */ setAsConnected(): void { + if (this.isConnected) { + return; + } + this.addEvent(new DeviceConnectedDomainEvent({ aggregateId: this.id })); this.props.isConnected = true; } /** Sets the device as connected. */ setAsLocked(): void { + if (this.isLocked) { + return; + } + this.addEvent(new DeviceLockedDomainEvent({ aggregateId: this.id })); this.props.isLocked = true; } @@ -214,14 +229,25 @@ export class Device extends AggregateRoot { } /** Updates the device wlan. */ - updateWlan(wlan?: DeviceWlan): void { + updateWlan(wlan: DeviceWlan): void { + if (wlan.equals(this.wlan)) { + return; + } + this.validateInstanceProp({ wlan }, 'wlan', DeviceWlan); + this.addEvent( + new DeviceWlanChangedDomainEvent({ + aggregateId: this.id, + previousWlan: this.props.wlan, + currentWlan: wlan, + }), + ); this.props.wlan = wlan; } /** Updates the device battery. */ updateBattery(battery: DeviceBattery): void { - if (this.battery.equals(battery)) { + if (battery.equals(this.battery)) { return; } diff --git a/packages/domain/src/domain-events/device-battery-changed.domain-event.ts b/packages/domain/src/domain-events/device-battery-changed.domain-event.ts index 791e2c5b..2e9db42a 100644 --- a/packages/domain/src/domain-events/device-battery-changed.domain-event.ts +++ b/packages/domain/src/domain-events/device-battery-changed.domain-event.ts @@ -8,11 +8,11 @@ export interface DeviceBatteryChangedDomainEventProps extends DomainEventProps { } export class DeviceBatteryChangedDomainEvent extends DomainEvent { - get previousBattery(): DeviceBattery | undefined { + get previousBattery(): DeviceBattery { return this.props.previousBattery; } - get currentBattery(): DeviceBattery | undefined { + get currentBattery(): DeviceBattery { return this.props.currentBattery; } diff --git a/packages/domain/src/domain-events/device-created.domain-event.test.ts b/packages/domain/src/domain-events/device-created.domain-event.test.ts new file mode 100644 index 00000000..734d197b --- /dev/null +++ b/packages/domain/src/domain-events/device-created.domain-event.test.ts @@ -0,0 +1,11 @@ +import { ID, DomainEvent } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceCreatedDomainEvent } from './device-created.domain-event'; + +describe('DeviceCreatedDomainEvent', function () { + it('should be created', function () { + const deviceCreatedDomainEvent = new DeviceCreatedDomainEvent({ aggregateId: ID.generate() }); + + expect(deviceCreatedDomainEvent).to.be.instanceOf(DomainEvent); + }); +}); diff --git a/packages/domain/src/domain-events/device-created.domain-event.ts b/packages/domain/src/domain-events/device-created.domain-event.ts new file mode 100644 index 00000000..051a46f9 --- /dev/null +++ b/packages/domain/src/domain-events/device-created.domain-event.ts @@ -0,0 +1,8 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export class DeviceCreatedDomainEvent extends DomainEvent { + protected validate(_: DomainEventProps): void { + // noop + } +} diff --git a/packages/domain/src/domain-events/device-locked.domain-event.test.ts b/packages/domain/src/domain-events/device-locked.domain-event.test.ts new file mode 100644 index 00000000..9747e0c1 --- /dev/null +++ b/packages/domain/src/domain-events/device-locked.domain-event.test.ts @@ -0,0 +1,11 @@ +import { ID, DomainEvent } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceLockedDomainEvent } from './device-locked.domain-event'; + +describe('DeviceLockedDomainEvent', function () { + it('should be created', function () { + const deviceLockedDomainEvent = new DeviceLockedDomainEvent({ aggregateId: ID.generate() }); + + expect(deviceLockedDomainEvent).to.be.instanceOf(DomainEvent); + }); +}); diff --git a/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts new file mode 100644 index 00000000..71c8261d --- /dev/null +++ b/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts @@ -0,0 +1,75 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceWlanProps } from '../test-support'; +import { DeviceWlan } from '../value-objects/device-wlan.value-object'; +import { DeviceWlanChangedDomainEvent } from './device-wlan-changed.domain-event'; +import type { DeviceWlanChangedDomainEventProps } from './device-wlan-changed.domain-event'; + +describe('DeviceWlanChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceWlanChangedDomainEventProps(); + const event = new DeviceWlanChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousWlan).to.be.undefined; + expect(event.currentWlan).to.be.equal(props.currentWlan); + }); + + it("should be created with 'previousWlan'", function () { + const props = { + ...givenSomeDeviceWlanChangedDomainEventProps(), + previousWlan: new DeviceWlan(givenSomeDeviceWlanProps()), + }; + const event = new DeviceWlanChangedDomainEvent(props); + + expect(event.previousWlan).to.be.equal(props.previousWlan); + }); + + it("should thrown an error when 'currentWlan' is not provided", function () { + expect( + () => + new DeviceWlanChangedDomainEvent({ + ...givenSomeDeviceWlanChangedDomainEventProps(), + // @ts-expect-error - missing property + currentWlan: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'currentWlan' for DeviceWlanChangedDomainEvent not provided`); + }); + + it("should thrown an error when 'previousWlan' is not an instance of DeviceWlan", function () { + expect( + () => + new DeviceWlanChangedDomainEvent({ + ...givenSomeDeviceWlanChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousWlan: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousWlan' of DeviceWlanChangedDomainEvent is not an instance of DeviceWlan", + ); + }); + + it("should thrown an error when 'currentWlan' is not an instance of DeviceWlan", function () { + expect( + () => + new DeviceWlanChangedDomainEvent({ + ...givenSomeDeviceWlanChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentWlan: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentWlan' of DeviceWlanChangedDomainEvent is not an instance of DeviceWlan", + ); + }); +}); + +function givenSomeDeviceWlanChangedDomainEventProps(): DeviceWlanChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousWlan: undefined, + currentWlan: new DeviceWlan(givenSomeDeviceWlanProps()), + }; +} diff --git a/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts b/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts new file mode 100644 index 00000000..2c8629ef --- /dev/null +++ b/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceWlan } from '../value-objects/device-wlan.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceWlanChangedDomainEventProps extends DomainEventProps { + previousWlan?: DeviceWlan; + currentWlan: DeviceWlan; +} + +export class DeviceWlanChangedDomainEvent extends DomainEvent { + get previousWlan(): DeviceWlan | undefined { + return this.props.previousWlan; + } + + get currentWlan(): DeviceWlan { + return this.props.currentWlan; + } + + protected validate(props: DeviceWlanChangedDomainEventProps): void { + if (props.previousWlan) { + this.validateInstanceProp(props, 'previousWlan', DeviceWlan); + } + + this.validateDefinedProp(props, 'currentWlan'); + this.validateInstanceProp(props, 'currentWlan', DeviceWlan); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 0426cda6..235d68aa 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -5,7 +5,9 @@ export * from './commands/locate-device.command'; export * from './domain-events/connection-device-changed.domain-event'; export * from './domain-events/device-battery-changed.domain-event'; export * from './domain-events/device-connected.domain-event'; +export * from './domain-events/device-created.domain-event'; export * from './domain-events/device-locked.domain-event'; +export * from './domain-events/device-wlan-changed.domain-event'; export * from './domain-events/domain-events'; export * from './domain-primitives/clean-mode.domain-primitive'; export * from './domain-primitives/clean-size.domain-primitive'; diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts index 333a7eff..b7cf6ef6 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts @@ -1,4 +1,4 @@ -import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { anything, capture, imock, instance, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { ID } from '../domain-primitives/id.domain-primitive'; import { AggregateRoot } from './aggregate-root.base'; @@ -42,7 +42,10 @@ describe('AggregateRoot', function () { expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(0); - verify(eventBus.emit('DummyDomainEvent', deepEqual(new DummyDomainEvent({ aggregateId: id })))).once(); + const [, event] = capture(eventBus.emit).first(); + + expect(event).to.be.instanceOf(DummyDomainEvent); + expect(event.aggregateId).to.be.equal(id); }); it('should be able to clear domain events', function () { diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts index 691abc5d..6aebab4f 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -19,9 +19,11 @@ export abstract class AggregateRoot extends async publishEvents(eventBus: EventBus): Promise { await Promise.all( - this.domainEvents.map(async (event) => { - this.debug(`publishing domain event '${event.constructor.name}' with data: ${JSON.stringify(event)}`); - return eventBus.emit(event.constructor.name, event); + this.domainEvents.map(async (domainEvent) => { + this.debug( + `publishing domain event '${domainEvent.constructor.name}' with data: ${JSON.stringify(domainEvent)}`, + ); + return eventBus.emit(domainEvent.constructor.name, domainEvent); }), ); @@ -29,6 +31,7 @@ export abstract class AggregateRoot extends } protected addEvent(domainEvent: DomainEvent): void { + this.debug(`adding domain event '${domainEvent.constructor.name}' with data: ${JSON.stringify(domainEvent)}`); this.#domainEvents.add(domainEvent); } } diff --git a/packages/toolkit/src/base-classes/domain-event.base.test.ts b/packages/toolkit/src/base-classes/domain-event.base.test.ts index 6fa66a7d..4a1f0250 100644 --- a/packages/toolkit/src/base-classes/domain-event.base.test.ts +++ b/packages/toolkit/src/base-classes/domain-event.base.test.ts @@ -6,11 +6,14 @@ import type { DomainEventProps } from './domain-event.base'; describe('DomainEvent', function () { it('should be created', function () { + const now = Date.now(); const aggregateId = ID.generate(); const dummyDomainEvent = new DummyDomainEvent({ aggregateId }); expect(dummyDomainEvent).to.be.instanceOf(Validatable); + expect(dummyDomainEvent.id).to.be.instanceOf(ID); expect(dummyDomainEvent.aggregateId).to.be.instanceOf(ID); + expect(dummyDomainEvent.metadata.timestamp).to.be.greaterThanOrEqual(now); }); }); diff --git a/packages/toolkit/src/base-classes/domain-event.base.ts b/packages/toolkit/src/base-classes/domain-event.base.ts index c5fd6e58..c78e560f 100644 --- a/packages/toolkit/src/base-classes/domain-event.base.ts +++ b/packages/toolkit/src/base-classes/domain-event.base.ts @@ -1,15 +1,24 @@ +import { ID } from '../domain-primitives/id.domain-primitive'; import { Validatable } from './validatable.base'; -import type { ID } from '../domain-primitives/id.domain-primitive'; export interface DomainEventProps { aggregateId: ID; } +export interface DomainEventMetadata { + timestamp: number; +} + export abstract class DomainEvent extends Validatable { + readonly id = ID.generate(); readonly eventName = this.constructor.name; + readonly metadata: DomainEventMetadata; constructor(protected readonly props: T) { super(props); + this.metadata = { + timestamp: Date.now(), + }; } get aggregateId(): ID { diff --git a/packages/toolkit/src/base-classes/task.base.test.ts b/packages/toolkit/src/base-classes/task.base.test.ts index 53e4d3d1..a4769899 100644 --- a/packages/toolkit/src/base-classes/task.base.test.ts +++ b/packages/toolkit/src/base-classes/task.base.test.ts @@ -1,12 +1,17 @@ import { expect } from 'chai'; +import { ID } from '../domain-primitives/id.domain-primitive'; import { Task } from './task.base'; import { Validatable } from './validatable.base'; describe('Task', function () { it('should be created', function () { + const now = Date.now(); const dummyTask = new DummyTask({ foo: 'bar' }); expect(dummyTask).to.be.instanceOf(Validatable); + expect(dummyTask.id).to.be.instanceOf(ID); + expect(dummyTask.taskName).to.be.equal('DummyTask'); + expect(dummyTask.metadata.timestamp).to.be.greaterThanOrEqual(now); }); }); diff --git a/packages/toolkit/src/base-classes/task.base.ts b/packages/toolkit/src/base-classes/task.base.ts index e2aca548..f0c639a4 100644 --- a/packages/toolkit/src/base-classes/task.base.ts +++ b/packages/toolkit/src/base-classes/task.base.ts @@ -1,11 +1,23 @@ +import { ID } from '../domain-primitives/id.domain-primitive'; import { Validatable } from './validatable.base'; export type TaskInput = T extends Task ? Input : never; export type TaskOutput = T extends Task ? Output : never; +export interface TaskMetadata { + timestamp: number; +} + export abstract class Task extends Validatable implements Task { + readonly id = ID.generate(); + readonly taskName = this.constructor.name; + readonly metadata: TaskMetadata; + constructor(protected readonly props: Input) { super(props); + this.metadata = { + timestamp: Date.now(), + }; } validateOutput?(output: Output): void; From 7ae8f49dfb06440bfaa9974b0e241be5315a15b3 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sat, 25 Mar 2023 11:23:18 +0100 Subject: [PATCH 21/38] refactor(domain): rename `DeviceWlan` to `DeviceNetwork` --- ...e-is-locked-event-handler.event-handler.ts | 2 +- ...vice-network-update.event-handler.test.ts} | 28 +++--- ...=> device-network-update.event-handler.ts} | 8 +- packages/adapter-tcp/src/tcp.server.ts | 4 +- .../device.aggregate-root.test.ts | 48 ++++----- .../aggregate-roots/device.aggregate-root.ts | 32 +++--- ...evice-network-changed.domain-event.test.ts | 78 +++++++++++++++ .../device-network-changed.domain-event.ts | 27 +++++ .../device-wlan-changed.domain-event.test.ts | 75 -------------- .../device-wlan-changed.domain-event.ts | 27 ----- packages/domain/src/index.ts | 4 +- packages/domain/src/test-support.ts | 4 +- .../device-network.value-object.test.ts | 98 +++++++++++++++++++ ...ject.ts => device-network.value-object.ts} | 12 +-- .../device-wlan.value-object.test.ts | 98 ------------------- 15 files changed, 274 insertions(+), 271 deletions(-) rename packages/adapter-tcp/src/packet-event-handlers/{device-wlan-update.event-handler.test.ts => device-network-update.event-handler.test.ts} (63%) rename packages/adapter-tcp/src/packet-event-handlers/{device-wlan-update.event-handler.ts => device-network-update.event-handler.ts} (78%) create mode 100644 packages/domain/src/domain-events/device-network-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-network-changed.domain-event.ts delete mode 100644 packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts delete mode 100644 packages/domain/src/domain-events/device-wlan-changed.domain-event.ts create mode 100644 packages/domain/src/value-objects/device-network.value-object.test.ts rename packages/domain/src/value-objects/{device-wlan.value-object.ts => device-network.value-object.ts} (74%) delete mode 100644 packages/domain/src/value-objects/device-wlan.value-object.test.ts diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index af1b684f..043aff18 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -25,7 +25,7 @@ export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEven mask: connection.device?.system.supports(DeviceCapability.MAP_PLANS) ? 0x78ff : 0xff, }); - // TODO: move this to a get wlan service. + // TODO: move this to a get network service. await connection.send('DEVICE_WLAN_INFO_GETTING_REQ', {}); } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts similarity index 63% rename from packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts index ae9607b7..18257b1f 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts @@ -1,21 +1,21 @@ -import { DeviceWlan } from '@agnoc/domain'; +import { DeviceNetwork } from '@agnoc/domain'; import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; -import { DeviceWlanUpdateEventHandler } from './device-wlan-update.event-handler'; +import { DeviceNetworkUpdateEventHandler } from './device-network-update.event-handler'; import type { PacketMessage } from '../packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; -describe('DeviceWlanUpdateEventHandler', function () { +describe('DeviceNetworkUpdateEventHandler', function () { let deviceRepository: DeviceRepository; - let eventHandler: DeviceWlanUpdateEventHandler; + let eventHandler: DeviceNetworkUpdateEventHandler; let packetMessage: PacketMessage<'DEVICE_WLAN_INFO_GETTING_RSP'>; let device: Device; beforeEach(function () { deviceRepository = imock(); - eventHandler = new DeviceWlanUpdateEventHandler(instance(deviceRepository)); + eventHandler = new DeviceNetworkUpdateEventHandler(instance(deviceRepository)); packetMessage = imock(); device = imock(); }); @@ -25,7 +25,7 @@ describe('DeviceWlanUpdateEventHandler', function () { }); describe('#handle()', function () { - it('should update the device wlan', async function () { + it('should update the device network', async function () { const payload = new Payload({ opcode: OPCode.fromName('DEVICE_WLAN_INFO_GETTING_RSP'), data: { @@ -46,17 +46,17 @@ describe('DeviceWlanUpdateEventHandler', function () { await eventHandler.handle(instance(packetMessage)); - const [deviceWlan] = capture(device.updateWlan).first(); + const [deviceNetwork] = capture(device.updateNetwork).first(); - expect(deviceWlan).to.be.instanceOf(DeviceWlan); - expect(deviceWlan.ipv4).to.be.equal('127.0.0.1'); - expect(deviceWlan.ssid).to.be.equal('ssid'); - expect(deviceWlan.port).to.be.equal(1234); - expect(deviceWlan.mask).to.be.equal('255.255.255.255'); - expect(deviceWlan.mac).to.be.equal('00:00:00:00:00:00'); + expect(deviceNetwork).to.be.instanceOf(DeviceNetwork); + expect(deviceNetwork.ipv4).to.be.equal('127.0.0.1'); + expect(deviceNetwork.ssid).to.be.equal('ssid'); + expect(deviceNetwork.port).to.be.equal(1234); + expect(deviceNetwork.mask).to.be.equal('255.255.255.255'); + expect(deviceNetwork.mac).to.be.equal('00:00:00:00:00:00'); verify(packetMessage.assertDevice()).once(); - verify(device.updateWlan(deviceWlan)).once(); + verify(device.updateNetwork(deviceNetwork)).once(); verify(deviceRepository.saveOne(instance(device))).once(); }); }); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts similarity index 78% rename from packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts rename to packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts index 90d13980..f5f5356a 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-wlan-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts @@ -1,9 +1,9 @@ -import { DeviceWlan } from '@agnoc/domain'; +import { DeviceNetwork } from '@agnoc/domain'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; -export class DeviceWlanUpdateEventHandler implements PacketEventHandler { +export class DeviceNetworkUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_WLAN_INFO_GETTING_RSP'; constructor(private readonly deviceRepository: DeviceRepository) {} @@ -13,8 +13,8 @@ export class DeviceWlanUpdateEventHandler implements PacketEventHandler { const data = message.packet.payload.data.body; - message.device.updateWlan( - new DeviceWlan({ + message.device.updateNetwork( + new DeviceNetwork({ ipv4: data.ipv4, ssid: data.ssid, port: data.port, diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 953df990..70cc9c7a 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -36,13 +36,13 @@ import { DeviceMapChargerPositionUpdateEventHandler } from './packet-event-handl import { DeviceMapUpdateEventHandler } from './packet-event-handlers/device-map-update.event-handler'; import { DeviceMapWorkStatusUpdateEventHandler } from './packet-event-handlers/device-map-work-status-update.event-handler'; import { DeviceMemoryMapInfoEventHandler } from './packet-event-handlers/device-memory-map-info.event-handler'; +import { DeviceNetworkUpdateEventHandler } from './packet-event-handlers/device-network-update.event-handler'; import { DeviceOfflineEventHandler } from './packet-event-handlers/device-offline.event-handler'; import { DeviceRegisterEventHandler } from './packet-event-handlers/device-register.event-handler'; import { DeviceSettingsUpdateEventHandler } from './packet-event-handlers/device-settings-update.event-handler'; import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-time-update.event-handler'; import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-upgrade-info.event-handler'; import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; -import { DeviceWlanUpdateEventHandler } from './packet-event-handlers/device-wlan-update.event-handler'; import { PacketEventPublisherService } from './packet-event-publisher.service'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; import { PacketEventBus } from './packet.event-bus'; @@ -131,7 +131,7 @@ export class TCPServer implements Server { new DeviceTimeUpdateEventHandler(), new DeviceUpgradeInfoEventHandler(), new DeviceVersionUpdateEventHandler(), - new DeviceWlanUpdateEventHandler(deviceRepository), + new DeviceNetworkUpdateEventHandler(deviceRepository), new DeviceMapUpdateEventHandler( deviceBatteryMapper, deviceModeMapper, diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 97324760..63a9382f 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -4,7 +4,7 @@ import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; -import { DeviceWlanChangedDomainEvent } from '../domain-events/device-wlan-changed.domain-event'; +import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -22,14 +22,14 @@ import { givenSomeDeviceSettingsProps, givenSomeDeviceSystemProps, givenSomeDeviceVersionProps, - givenSomeDeviceWlanProps, + givenSomeDeviceNetworkProps, } from '../test-support'; import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; +import { DeviceNetwork } from '../value-objects/device-network.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; import { DeviceSystem } from '../value-objects/device-system.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; -import { DeviceWlan } from '../value-objects/device-wlan.value-object'; import { Device } from './device.aggregate-root'; describe('Device', function () { @@ -200,11 +200,11 @@ describe('Device', function () { ); }); - it("should throw an error when 'wlan' is not a DeviceWlan", function () { + it("should throw an error when 'network' is not a DeviceNetwork", function () { // @ts-expect-error - invalid property - expect(() => new Device({ ...givenSomeDeviceProps(), wlan: 'foo' })).to.throw( + expect(() => new Device({ ...givenSomeDeviceProps(), network: 'foo' })).to.throw( ArgumentInvalidException, - `Value 'foo' for property 'wlan' of Device is not an instance of DeviceWlan`, + `Value 'foo' for property 'network' of Device is not an instance of DeviceNetwork`, ); }); @@ -398,33 +398,33 @@ describe('Device', function () { }); }); - describe('#updateWlan()', function () { - it('should update the device wlan', function () { - const previousWlan = new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: 1 }); - const currentWlan = new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: 2 }); - const device = new Device({ ...givenSomeDeviceProps(), wlan: previousWlan }); + describe('#updateNetwork()', function () { + it('should update the device network', function () { + const previousNetwork = new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), port: 1 }); + const currentNetwork = new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), port: 2 }); + const device = new Device({ ...givenSomeDeviceProps(), network: previousNetwork }); - device.updateWlan(currentWlan); + device.updateNetwork(currentNetwork); - expect(device.wlan).to.be.equal(currentWlan); + expect(device.network).to.be.equal(currentNetwork); - const event = device.domainEvents[1] as DeviceWlanChangedDomainEvent; + const event = device.domainEvents[1] as DeviceNetworkChangedDomainEvent; - expect(event).to.be.instanceOf(DeviceWlanChangedDomainEvent); + expect(event).to.be.instanceOf(DeviceNetworkChangedDomainEvent); expect(event.aggregateId).to.equal(device.id); - expect(event.previousWlan).to.be.equal(previousWlan); - expect(event.currentWlan).to.be.equal(currentWlan); + expect(event.previousNetwork).to.be.equal(previousNetwork); + expect(event.currentNetwork).to.be.equal(currentNetwork); }); - it('should not update the device wlan when value is equal', function () { - const previousWlan = new DeviceWlan(givenSomeDeviceWlanProps()); - const currentWlan = new DeviceWlan(givenSomeDeviceWlanProps()); - const device = new Device({ ...givenSomeDeviceProps(), wlan: previousWlan }); + it('should not update the device network when value is equal', function () { + const previousNetwork = new DeviceNetwork(givenSomeDeviceNetworkProps()); + const currentNetwork = new DeviceNetwork(givenSomeDeviceNetworkProps()); + const device = new Device({ ...givenSomeDeviceProps(), network: previousNetwork }); - device.updateWlan(currentWlan); + device.updateNetwork(currentNetwork); - expect(device.wlan).to.be.equal(previousWlan); - expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceWlanChangedDomainEvent); + expect(device.network).to.be.equal(previousNetwork); + expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceNetworkChangedDomainEvent); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index f43fb0a7..9fc527e3 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -3,7 +3,7 @@ import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; -import { DeviceWlanChangedDomainEvent } from '../domain-events/device-wlan-changed.domain-event'; +import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -14,10 +14,10 @@ import { DeviceMap } from '../entities/device-map.entity'; import { DeviceOrder } from '../entities/device-order.entity'; import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; +import { DeviceNetwork } from '../value-objects/device-network.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; import { DeviceSystem } from '../value-objects/device-system.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; -import { DeviceWlan } from '../value-objects/device-wlan.value-object'; import type { EntityProps } from '@agnoc/toolkit'; /** Describes the properties of a device. */ @@ -44,8 +44,8 @@ export interface DeviceProps extends EntityProps { consumables?: DeviceConsumable[]; /** The device map. */ map?: DeviceMap; - /** The device wlan. */ - wlan?: DeviceWlan; + /** The device network. */ + network?: DeviceNetwork; /** The device state. */ state?: DeviceState; /** The device mode. */ @@ -124,9 +124,9 @@ export class Device extends AggregateRoot { return this.props.map; } - /** Returns the device wlan. */ - get wlan(): DeviceWlan | undefined { - return this.props.wlan; + /** Returns the device network. */ + get network(): DeviceNetwork | undefined { + return this.props.network; } /** Returns the device state. */ @@ -228,21 +228,21 @@ export class Device extends AggregateRoot { this.props.map = map; } - /** Updates the device wlan. */ - updateWlan(wlan: DeviceWlan): void { - if (wlan.equals(this.wlan)) { + /** Updates the device network. */ + updateNetwork(network: DeviceNetwork): void { + if (network.equals(this.network)) { return; } - this.validateInstanceProp({ wlan }, 'wlan', DeviceWlan); + this.validateInstanceProp({ network }, 'network', DeviceNetwork); this.addEvent( - new DeviceWlanChangedDomainEvent({ + new DeviceNetworkChangedDomainEvent({ aggregateId: this.id, - previousWlan: this.props.wlan, - currentWlan: wlan, + previousNetwork: this.props.network, + currentNetwork: network, }), ); - this.props.wlan = wlan; + this.props.network = network; } /** Updates the device battery. */ @@ -321,7 +321,7 @@ export class Device extends AggregateRoot { this.validateArrayProp(props, 'orders', DeviceOrder); this.validateArrayProp(props, 'consumables', DeviceConsumable); this.validateInstanceProp(props, 'map', DeviceMap); - this.validateInstanceProp(props, 'wlan', DeviceWlan); + this.validateInstanceProp(props, 'network', DeviceNetwork); this.validateInstanceProp(props, 'battery', DeviceBattery); this.validateInstanceProp(props, 'state', DeviceState); this.validateInstanceProp(props, 'mode', DeviceMode); diff --git a/packages/domain/src/domain-events/device-network-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-network-changed.domain-event.test.ts new file mode 100644 index 00000000..896693b0 --- /dev/null +++ b/packages/domain/src/domain-events/device-network-changed.domain-event.test.ts @@ -0,0 +1,78 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceNetworkProps } from '../test-support'; +import { DeviceNetwork } from '../value-objects/device-network.value-object'; +import { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; +import type { DeviceNetworkChangedDomainEventProps } from './device-network-changed.domain-event'; + +describe('DeviceNetworkChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceNetworkChangedDomainEventProps(); + const event = new DeviceNetworkChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousNetwork).to.be.undefined; + expect(event.currentNetwork).to.be.equal(props.currentNetwork); + }); + + it("should be created with 'previousNetwork'", function () { + const props = { + ...givenSomeDeviceNetworkChangedDomainEventProps(), + previousNetwork: new DeviceNetwork(givenSomeDeviceNetworkProps()), + }; + const event = new DeviceNetworkChangedDomainEvent(props); + + expect(event.previousNetwork).to.be.equal(props.previousNetwork); + }); + + it("should thrown an error when 'currentNetwork' is not provided", function () { + expect( + () => + new DeviceNetworkChangedDomainEvent({ + ...givenSomeDeviceNetworkChangedDomainEventProps(), + // @ts-expect-error - missing property + currentNetwork: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentNetwork' for DeviceNetworkChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousNetwork' is not an instance of DeviceNetwork", function () { + expect( + () => + new DeviceNetworkChangedDomainEvent({ + ...givenSomeDeviceNetworkChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousNetwork: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousNetwork' of DeviceNetworkChangedDomainEvent is not an instance of DeviceNetwork", + ); + }); + + it("should thrown an error when 'currentNetwork' is not an instance of DeviceNetwork", function () { + expect( + () => + new DeviceNetworkChangedDomainEvent({ + ...givenSomeDeviceNetworkChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentNetwork: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentNetwork' of DeviceNetworkChangedDomainEvent is not an instance of DeviceNetwork", + ); + }); +}); + +function givenSomeDeviceNetworkChangedDomainEventProps(): DeviceNetworkChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousNetwork: undefined, + currentNetwork: new DeviceNetwork(givenSomeDeviceNetworkProps()), + }; +} diff --git a/packages/domain/src/domain-events/device-network-changed.domain-event.ts b/packages/domain/src/domain-events/device-network-changed.domain-event.ts new file mode 100644 index 00000000..f64d3ed8 --- /dev/null +++ b/packages/domain/src/domain-events/device-network-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceNetwork } from '../value-objects/device-network.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceNetworkChangedDomainEventProps extends DomainEventProps { + previousNetwork?: DeviceNetwork; + currentNetwork: DeviceNetwork; +} + +export class DeviceNetworkChangedDomainEvent extends DomainEvent { + get previousNetwork(): DeviceNetwork | undefined { + return this.props.previousNetwork; + } + + get currentNetwork(): DeviceNetwork { + return this.props.currentNetwork; + } + + protected validate(props: DeviceNetworkChangedDomainEventProps): void { + if (props.previousNetwork) { + this.validateInstanceProp(props, 'previousNetwork', DeviceNetwork); + } + + this.validateDefinedProp(props, 'currentNetwork'); + this.validateInstanceProp(props, 'currentNetwork', DeviceNetwork); + } +} diff --git a/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts deleted file mode 100644 index 71c8261d..00000000 --- a/packages/domain/src/domain-events/device-wlan-changed.domain-event.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; -import { expect } from 'chai'; -import { givenSomeDeviceWlanProps } from '../test-support'; -import { DeviceWlan } from '../value-objects/device-wlan.value-object'; -import { DeviceWlanChangedDomainEvent } from './device-wlan-changed.domain-event'; -import type { DeviceWlanChangedDomainEventProps } from './device-wlan-changed.domain-event'; - -describe('DeviceWlanChangedDomainEvent', function () { - it('should be created', function () { - const props = givenSomeDeviceWlanChangedDomainEventProps(); - const event = new DeviceWlanChangedDomainEvent(props); - - expect(event).to.be.instanceOf(DomainEvent); - expect(event.aggregateId).to.be.equal(props.aggregateId); - expect(event.previousWlan).to.be.undefined; - expect(event.currentWlan).to.be.equal(props.currentWlan); - }); - - it("should be created with 'previousWlan'", function () { - const props = { - ...givenSomeDeviceWlanChangedDomainEventProps(), - previousWlan: new DeviceWlan(givenSomeDeviceWlanProps()), - }; - const event = new DeviceWlanChangedDomainEvent(props); - - expect(event.previousWlan).to.be.equal(props.previousWlan); - }); - - it("should thrown an error when 'currentWlan' is not provided", function () { - expect( - () => - new DeviceWlanChangedDomainEvent({ - ...givenSomeDeviceWlanChangedDomainEventProps(), - // @ts-expect-error - missing property - currentWlan: undefined, - }), - ).to.throw(ArgumentNotProvidedException, `Property 'currentWlan' for DeviceWlanChangedDomainEvent not provided`); - }); - - it("should thrown an error when 'previousWlan' is not an instance of DeviceWlan", function () { - expect( - () => - new DeviceWlanChangedDomainEvent({ - ...givenSomeDeviceWlanChangedDomainEventProps(), - // @ts-expect-error - invalid property - previousWlan: 'foo', - }), - ).to.throw( - ArgumentInvalidException, - "Value 'foo' for property 'previousWlan' of DeviceWlanChangedDomainEvent is not an instance of DeviceWlan", - ); - }); - - it("should thrown an error when 'currentWlan' is not an instance of DeviceWlan", function () { - expect( - () => - new DeviceWlanChangedDomainEvent({ - ...givenSomeDeviceWlanChangedDomainEventProps(), - // @ts-expect-error - invalid property - currentWlan: 'foo', - }), - ).to.throw( - ArgumentInvalidException, - "Value 'foo' for property 'currentWlan' of DeviceWlanChangedDomainEvent is not an instance of DeviceWlan", - ); - }); -}); - -function givenSomeDeviceWlanChangedDomainEventProps(): DeviceWlanChangedDomainEventProps { - return { - aggregateId: ID.generate(), - previousWlan: undefined, - currentWlan: new DeviceWlan(givenSomeDeviceWlanProps()), - }; -} diff --git a/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts b/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts deleted file mode 100644 index 2c8629ef..00000000 --- a/packages/domain/src/domain-events/device-wlan-changed.domain-event.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DomainEvent } from '@agnoc/toolkit'; -import { DeviceWlan } from '../value-objects/device-wlan.value-object'; -import type { DomainEventProps } from '@agnoc/toolkit'; - -export interface DeviceWlanChangedDomainEventProps extends DomainEventProps { - previousWlan?: DeviceWlan; - currentWlan: DeviceWlan; -} - -export class DeviceWlanChangedDomainEvent extends DomainEvent { - get previousWlan(): DeviceWlan | undefined { - return this.props.previousWlan; - } - - get currentWlan(): DeviceWlan { - return this.props.currentWlan; - } - - protected validate(props: DeviceWlanChangedDomainEventProps): void { - if (props.previousWlan) { - this.validateInstanceProp(props, 'previousWlan', DeviceWlan); - } - - this.validateDefinedProp(props, 'currentWlan'); - this.validateInstanceProp(props, 'currentWlan', DeviceWlan); - } -} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 235d68aa..7d3405a5 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -7,7 +7,7 @@ export * from './domain-events/device-battery-changed.domain-event'; export * from './domain-events/device-connected.domain-event'; export * from './domain-events/device-created.domain-event'; export * from './domain-events/device-locked.domain-event'; -export * from './domain-events/device-wlan-changed.domain-event'; +export * from './domain-events/device-network-changed.domain-event'; export * from './domain-events/domain-events'; export * from './domain-primitives/clean-mode.domain-primitive'; export * from './domain-primitives/clean-size.domain-primitive'; @@ -38,7 +38,7 @@ export * from './value-objects/device-settings.value-object'; export * from './value-objects/device-system.value-object'; export * from './value-objects/device-time.value-object'; export * from './value-objects/device-version.value-object'; -export * from './value-objects/device-wlan.value-object'; +export * from './value-objects/device-network.value-object'; export * from './value-objects/map-coordinate.value-object'; export * from './value-objects/map-pixel.value-object'; export * from './value-objects/map-position.value-object'; diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index 5590806a..686068c3 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -24,12 +24,12 @@ import type { RoomProps } from './entities/room.entity'; import type { ZoneProps } from './entities/zone.entity'; import type { DeviceCleanWorkProps } from './value-objects/device-clean-work.value-object'; import type { DeviceConsumableProps } from './value-objects/device-consumable.value-object'; +import type { DeviceNetworkProps } from './value-objects/device-network.value-object'; import type { DeviceSettingProps } from './value-objects/device-setting.value-object'; import type { DeviceSettingsProps } from './value-objects/device-settings.value-object'; import type { DeviceSystemProps } from './value-objects/device-system.value-object'; import type { DeviceTimeProps } from './value-objects/device-time.value-object'; import type { DeviceVersionProps } from './value-objects/device-version.value-object'; -import type { DeviceWlanProps } from './value-objects/device-wlan.value-object'; import type { MapCoordinateProps } from './value-objects/map-coordinate.value-object'; import type { MapPixelProps } from './value-objects/map-pixel.value-object'; import type { MapPositionProps } from './value-objects/map-position.value-object'; @@ -119,7 +119,7 @@ export function givenSomeDeviceVersionProps(): DeviceVersionProps { }; } -export function givenSomeDeviceWlanProps(): DeviceWlanProps { +export function givenSomeDeviceNetworkProps(): DeviceNetworkProps { return { ipv4: '127.0.0.1', ssid: 'ssid', diff --git a/packages/domain/src/value-objects/device-network.value-object.test.ts b/packages/domain/src/value-objects/device-network.value-object.test.ts new file mode 100644 index 00000000..aee22c2b --- /dev/null +++ b/packages/domain/src/value-objects/device-network.value-object.test.ts @@ -0,0 +1,98 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, ValueObject } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceNetworkProps } from '../test-support'; +import { DeviceNetwork } from './device-network.value-object'; + +describe('DeviceNetwork', function () { + it('should be created', function () { + const deviceNetworkProps = givenSomeDeviceNetworkProps(); + const deviceNetwork = new DeviceNetwork(deviceNetworkProps); + + expect(deviceNetwork).to.be.instanceOf(ValueObject); + expect(deviceNetwork.ipv4).to.be.equal(deviceNetworkProps.ipv4); + expect(deviceNetwork.ssid).to.be.equal(deviceNetworkProps.ssid); + expect(deviceNetwork.port).to.be.equal(deviceNetworkProps.port); + expect(deviceNetwork.mask).to.be.equal(deviceNetworkProps.mask); + expect(deviceNetwork.mac).to.be.equal(deviceNetworkProps.mac); + }); + + it("should throw an error when 'ipv4' property is not provided", function () { + // @ts-expect-error - missing property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), ipv4: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'ipv4' for DeviceNetwork not provided`, + ); + }); + + it("should throw an error when 'ipv4' property is invalid", function () { + // @ts-expect-error - invalid property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), ipv4: 1 })).to.throw( + ArgumentInvalidException, + `Value '1' for property 'ipv4' of DeviceNetwork is not a string`, + ); + }); + + it("should throw an error when 'ssid' property is not provided", function () { + // @ts-expect-error - missing property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), ssid: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'ssid' for DeviceNetwork not provided`, + ); + }); + + it("should throw an error when 'ssid' property is invalid", function () { + // @ts-expect-error - invalid property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), ssid: 1 })).to.throw( + ArgumentInvalidException, + `Value '1' for property 'ssid' of DeviceNetwork is not a string`, + ); + }); + + it("should throw an error when 'port' property is not provided", function () { + // @ts-expect-error - missing property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), port: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'port' for DeviceNetwork not provided`, + ); + }); + + it("should throw an error when 'port' property is invalid", function () { + // @ts-expect-error - invalid property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), port: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'port' of DeviceNetwork is not a number`, + ); + }); + + it("should throw an error when 'mask' property is not provided", function () { + // @ts-expect-error - missing property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), mask: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'mask' for DeviceNetwork not provided`, + ); + }); + + it("should throw an error when 'mask' property is invalid", function () { + // @ts-expect-error - invalid property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), mask: 1 })).to.throw( + ArgumentInvalidException, + `Value '1' for property 'mask' of DeviceNetwork is not a string`, + ); + }); + + it("should throw an error when 'mac' property is not provided", function () { + // @ts-expect-error - missing property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), mac: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'mac' for DeviceNetwork not provided`, + ); + }); + + it("should throw an error when 'mac' property is invalid", function () { + // @ts-expect-error - invalid property + expect(() => new DeviceNetwork({ ...givenSomeDeviceNetworkProps(), mac: 1 })).to.throw( + ArgumentInvalidException, + `Value '1' for property 'mac' of DeviceNetwork is not a string`, + ); + }); +}); diff --git a/packages/domain/src/value-objects/device-wlan.value-object.ts b/packages/domain/src/value-objects/device-network.value-object.ts similarity index 74% rename from packages/domain/src/value-objects/device-wlan.value-object.ts rename to packages/domain/src/value-objects/device-network.value-object.ts index c808ecab..772e3b9c 100644 --- a/packages/domain/src/value-objects/device-wlan.value-object.ts +++ b/packages/domain/src/value-objects/device-network.value-object.ts @@ -1,7 +1,7 @@ import { ValueObject } from '@agnoc/toolkit'; -/** The properties of the device wlan. */ -export interface DeviceWlanProps { +/** The properties of the device network. */ +export interface DeviceNetworkProps { /** The ipv4 address. */ ipv4: string; /** The ssid. */ @@ -14,8 +14,8 @@ export interface DeviceWlanProps { mac: string; } -/** Describe the wlan of a device. */ -export class DeviceWlan extends ValueObject { +/** Describe the network of a device. */ +export class DeviceNetwork extends ValueObject { /** Returns the ipv4 address. */ get ipv4(): string { return this.props.ipv4; @@ -41,8 +41,8 @@ export class DeviceWlan extends ValueObject { return this.props.mac; } - protected validate(props: DeviceWlanProps): void { - const keys: (keyof DeviceWlanProps)[] = ['ipv4', 'ssid', 'port', 'mask', 'mac']; + protected validate(props: DeviceNetworkProps): void { + const keys: (keyof DeviceNetworkProps)[] = ['ipv4', 'ssid', 'port', 'mask', 'mac']; keys.forEach((prop) => { this.validateDefinedProp(props, prop); diff --git a/packages/domain/src/value-objects/device-wlan.value-object.test.ts b/packages/domain/src/value-objects/device-wlan.value-object.test.ts deleted file mode 100644 index 3d5d55a3..00000000 --- a/packages/domain/src/value-objects/device-wlan.value-object.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ArgumentInvalidException, ArgumentNotProvidedException, ValueObject } from '@agnoc/toolkit'; -import { expect } from 'chai'; -import { givenSomeDeviceWlanProps } from '../test-support'; -import { DeviceWlan } from './device-wlan.value-object'; - -describe('DeviceWlan', function () { - it('should be created', function () { - const deviceWlanProps = givenSomeDeviceWlanProps(); - const deviceWlan = new DeviceWlan(deviceWlanProps); - - expect(deviceWlan).to.be.instanceOf(ValueObject); - expect(deviceWlan.ipv4).to.be.equal(deviceWlanProps.ipv4); - expect(deviceWlan.ssid).to.be.equal(deviceWlanProps.ssid); - expect(deviceWlan.port).to.be.equal(deviceWlanProps.port); - expect(deviceWlan.mask).to.be.equal(deviceWlanProps.mask); - expect(deviceWlan.mac).to.be.equal(deviceWlanProps.mac); - }); - - it("should throw an error when 'ipv4' property is not provided", function () { - // @ts-expect-error - missing property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), ipv4: undefined })).to.throw( - ArgumentNotProvidedException, - `Property 'ipv4' for DeviceWlan not provided`, - ); - }); - - it("should throw an error when 'ipv4' property is invalid", function () { - // @ts-expect-error - invalid property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), ipv4: 1 })).to.throw( - ArgumentInvalidException, - `Value '1' for property 'ipv4' of DeviceWlan is not a string`, - ); - }); - - it("should throw an error when 'ssid' property is not provided", function () { - // @ts-expect-error - missing property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), ssid: undefined })).to.throw( - ArgumentNotProvidedException, - `Property 'ssid' for DeviceWlan not provided`, - ); - }); - - it("should throw an error when 'ssid' property is invalid", function () { - // @ts-expect-error - invalid property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), ssid: 1 })).to.throw( - ArgumentInvalidException, - `Value '1' for property 'ssid' of DeviceWlan is not a string`, - ); - }); - - it("should throw an error when 'port' property is not provided", function () { - // @ts-expect-error - missing property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: undefined })).to.throw( - ArgumentNotProvidedException, - `Property 'port' for DeviceWlan not provided`, - ); - }); - - it("should throw an error when 'port' property is invalid", function () { - // @ts-expect-error - invalid property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), port: 'foo' })).to.throw( - ArgumentInvalidException, - `Value 'foo' for property 'port' of DeviceWlan is not a number`, - ); - }); - - it("should throw an error when 'mask' property is not provided", function () { - // @ts-expect-error - missing property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), mask: undefined })).to.throw( - ArgumentNotProvidedException, - `Property 'mask' for DeviceWlan not provided`, - ); - }); - - it("should throw an error when 'mask' property is invalid", function () { - // @ts-expect-error - invalid property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), mask: 1 })).to.throw( - ArgumentInvalidException, - `Value '1' for property 'mask' of DeviceWlan is not a string`, - ); - }); - - it("should throw an error when 'mac' property is not provided", function () { - // @ts-expect-error - missing property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), mac: undefined })).to.throw( - ArgumentNotProvidedException, - `Property 'mac' for DeviceWlan not provided`, - ); - }); - - it("should throw an error when 'mac' property is invalid", function () { - // @ts-expect-error - invalid property - expect(() => new DeviceWlan({ ...givenSomeDeviceWlanProps(), mac: 1 })).to.throw( - ArgumentInvalidException, - `Value '1' for property 'mac' of DeviceWlan is not a string`, - ); - }); -}); From e9617b3891a8311a55953e2a31336d7536346ab5 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 26 Mar 2023 12:29:13 +0200 Subject: [PATCH 22/38] feat(domain): add `DeviceVersionChangedDomainEvent` --- ...evice-version-update.event-handler.test.ts | 51 ++++++++++++ .../device-version-update.event-handler.ts | 10 +-- packages/adapter-tcp/src/tcp.server.ts | 2 +- .../device.aggregate-root.test.ts | 41 ++++++---- .../aggregate-roots/device.aggregate-root.ts | 21 +++-- ...evice-version-changed.domain-event.test.ts | 81 +++++++++++++++++++ .../device-version-changed.domain-event.ts | 25 ++++++ 7 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts create mode 100644 packages/domain/src/domain-events/device-version-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-version-changed.domain-event.ts diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts new file mode 100644 index 00000000..84046432 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts @@ -0,0 +1,51 @@ +import { DeviceVersion } from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceVersionUpdateEventHandler } from './device-version-update.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceVersionUpdateEventHandler', function () { + let deviceRepository: DeviceRepository; + let eventHandler: DeviceVersionUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_VERSION_INFO_UPDATE_REQ'>; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + eventHandler = new DeviceVersionUpdateEventHandler(instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_VERSION_INFO_UPDATE_REQ'); + }); + + describe('#handle()', function () { + it('should update the device version', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_VERSION_INFO_UPDATE_REQ'), + data: { softwareVersion: '1.0.0', hardwareVersion: '1.0.0' }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + const [deviceVersion] = capture(device.updateVersion).first(); + + expect(deviceVersion).to.be.instanceOf(DeviceVersion); + expect(deviceVersion.software).to.be.equal('1.0.0'); + expect(deviceVersion.hardware).to.be.equal('1.0.0'); + + verify(packetMessage.assertDevice()).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + verify(packetMessage.respond('DEVICE_VERSION_INFO_UPDATE_RSP', deepEqual({ result: 0 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts index 19eeef52..c0a5fdfb 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts @@ -1,21 +1,21 @@ import { DeviceVersion } from '@agnoc/domain'; -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceVersionUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_VERSION_INFO_UPDATE_REQ'; + constructor(private readonly deviceRepository: DeviceRepository) {} + async handle(message: PacketMessage<'DEVICE_VERSION_INFO_UPDATE_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const data = message.packet.payload.data; message.device.updateVersion(new DeviceVersion({ software: data.softwareVersion, hardware: data.hardwareVersion })); - // TODO: save entity and publish domain event + await this.deviceRepository.saveOne(message.device); await message.respond('DEVICE_VERSION_INFO_UPDATE_RSP', { result: 0 }); } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 70cc9c7a..4b506dfb 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -130,7 +130,7 @@ export class TCPServer implements Server { new DeviceSettingsUpdateEventHandler(deviceVoiceMapper), new DeviceTimeUpdateEventHandler(), new DeviceUpgradeInfoEventHandler(), - new DeviceVersionUpdateEventHandler(), + new DeviceVersionUpdateEventHandler(deviceRepository), new DeviceNetworkUpdateEventHandler(deviceRepository), new DeviceMapUpdateEventHandler( deviceBatteryMapper, diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 63a9382f..9163cc24 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -5,6 +5,7 @@ import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.do import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -20,7 +21,6 @@ import { givenSomeDeviceOrderProps, givenSomeDeviceProps, givenSomeDeviceSettingsProps, - givenSomeDeviceSystemProps, givenSomeDeviceVersionProps, givenSomeDeviceNetworkProps, } from '../test-support'; @@ -28,7 +28,6 @@ import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; import { DeviceNetwork } from '../value-objects/device-network.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; -import { DeviceSystem } from '../value-objects/device-system.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; import { Device } from './device.aggregate-root'; @@ -321,25 +320,33 @@ describe('Device', function () { }); }); - describe('#updateSystem()', function () { - it('should update the device system', function () { - const device = new Device(givenSomeDeviceProps()); - const system = new DeviceSystem(givenSomeDeviceSystemProps()); + describe('#updateVersion()', function () { + it('should update the device version', function () { + const previousVersion = new DeviceVersion({ ...givenSomeDeviceVersionProps(), software: '1.0.0' }); + const currentVersion = new DeviceVersion({ ...givenSomeDeviceVersionProps(), software: '1.0.1' }); + const device = new Device({ ...givenSomeDeviceProps(), version: previousVersion }); + + device.updateVersion(currentVersion); + + expect(device.version).to.be.equal(currentVersion); - device.updateSystem(system); + const event = device.domainEvents[1] as DeviceVersionChangedDomainEvent; - expect(device.system).to.be.equal(system); + expect(event).to.be.instanceOf(DeviceVersionChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousVersion).to.be.equal(previousVersion); + expect(event.currentVersion).to.be.equal(currentVersion); }); - }); - describe('#updateVersion()', function () { - it('should update the device version', function () { - const device = new Device(givenSomeDeviceProps()); - const version = new DeviceVersion(givenSomeDeviceVersionProps()); + it('should not update the device version when value is equal', function () { + const previousVersion = new DeviceVersion(givenSomeDeviceVersionProps()); + const currentVersion = new DeviceVersion(givenSomeDeviceVersionProps()); + const device = new Device({ ...givenSomeDeviceProps(), version: previousVersion }); - device.updateVersion(version); + device.updateVersion(currentVersion); - expect(device.version).to.be.equal(version); + expect(device.version).to.be.equal(previousVersion); + expect(device.domainEvents[1]).to.not.exist; }); }); @@ -424,7 +431,7 @@ describe('Device', function () { device.updateNetwork(currentNetwork); expect(device.network).to.be.equal(previousNetwork); - expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceNetworkChangedDomainEvent); + expect(device.domainEvents[1]).to.not.exist; }); }); @@ -454,7 +461,7 @@ describe('Device', function () { device.updateBattery(currentBattery); expect(device.battery).to.be.equal(previousBattery); - expect(device.domainEvents[1]).to.not.be.instanceOf(DeviceBatteryChangedDomainEvent); + expect(device.domainEvents[1]).to.not.exist; }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index 9fc527e3..c2ce137c 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -4,6 +4,7 @@ import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.do import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -184,17 +185,21 @@ export class Device extends AggregateRoot { this.props.isLocked = true; } - /** Updates the device system. */ - updateSystem(system: DeviceSystem): void { - this.validateDefinedProp({ system }, 'system'); - this.validateInstanceProp({ system }, 'system', DeviceSystem); - this.props.system = system; - } - /** Updates the device version. */ updateVersion(version: DeviceVersion): void { + if (version.equals(this.version)) { + return; + } + this.validateDefinedProp({ version }, 'version'); this.validateInstanceProp({ version }, 'version', DeviceVersion); + this.addEvent( + new DeviceVersionChangedDomainEvent({ + aggregateId: this.id, + previousVersion: this.version, + currentVersion: version, + }), + ); this.props.version = version; } @@ -234,6 +239,7 @@ export class Device extends AggregateRoot { return; } + this.validateDefinedProp({ network }, 'network'); this.validateInstanceProp({ network }, 'network', DeviceNetwork); this.addEvent( new DeviceNetworkChangedDomainEvent({ @@ -251,6 +257,7 @@ export class Device extends AggregateRoot { return; } + this.validateDefinedProp({ battery }, 'battery'); this.validateInstanceProp({ battery }, 'battery', DeviceBattery); this.addEvent( new DeviceBatteryChangedDomainEvent({ diff --git a/packages/domain/src/domain-events/device-version-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-version-changed.domain-event.test.ts new file mode 100644 index 00000000..cb68d24e --- /dev/null +++ b/packages/domain/src/domain-events/device-version-changed.domain-event.test.ts @@ -0,0 +1,81 @@ +import { DomainEvent, ID, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceVersion } from '../value-objects/device-version.value-object'; +import { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; +import type { DeviceVersionChangedDomainEventProps } from './device-version-changed.domain-event'; + +describe('DeviceVersionChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceVersionChangedDomainEventProps(); + const event = new DeviceVersionChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousVersion).to.be.equal(props.previousVersion); + expect(event.currentVersion).to.be.equal(props.currentVersion); + }); + + it("should thrown an error when 'previousVersion' is not provided", function () { + expect( + () => + new DeviceVersionChangedDomainEvent({ + ...givenSomeDeviceVersionChangedDomainEventProps(), + // @ts-expect-error - missing property + previousVersion: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'previousVersion' for DeviceVersionChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'currentVersion' is not provided", function () { + expect( + () => + new DeviceVersionChangedDomainEvent({ + ...givenSomeDeviceVersionChangedDomainEventProps(), + // @ts-expect-error - missing property + currentVersion: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentVersion' for DeviceVersionChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousVersion' is not an instance of ID", function () { + expect( + () => + new DeviceVersionChangedDomainEvent({ + ...givenSomeDeviceVersionChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousVersion: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousVersion' of DeviceVersionChangedDomainEvent is not an instance of DeviceVersion", + ); + }); + + it("should thrown an error when 'currentVersion' is not an instance of ID", function () { + expect( + () => + new DeviceVersionChangedDomainEvent({ + ...givenSomeDeviceVersionChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentVersion: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentVersion' of DeviceVersionChangedDomainEvent is not an instance of DeviceVersion", + ); + }); +}); + +function givenSomeDeviceVersionChangedDomainEventProps(): DeviceVersionChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousVersion: new DeviceVersion({ software: '1.0.0', hardware: '1.0.0' }), + currentVersion: new DeviceVersion({ software: '1.0.1', hardware: '1.0.1' }), + }; +} diff --git a/packages/domain/src/domain-events/device-version-changed.domain-event.ts b/packages/domain/src/domain-events/device-version-changed.domain-event.ts new file mode 100644 index 00000000..7d46b192 --- /dev/null +++ b/packages/domain/src/domain-events/device-version-changed.domain-event.ts @@ -0,0 +1,25 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceVersion } from '../value-objects/device-version.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceVersionChangedDomainEventProps extends DomainEventProps { + previousVersion: DeviceVersion; + currentVersion: DeviceVersion; +} + +export class DeviceVersionChangedDomainEvent extends DomainEvent { + get previousVersion(): DeviceVersion { + return this.props.previousVersion; + } + + get currentVersion(): DeviceVersion { + return this.props.currentVersion; + } + + protected validate(props: DeviceVersionChangedDomainEventProps): void { + this.validateDefinedProp(props, 'previousVersion'); + this.validateInstanceProp(props, 'previousVersion', DeviceVersion); + this.validateDefinedProp(props, 'currentVersion'); + this.validateInstanceProp(props, 'currentVersion', DeviceVersion); + } +} From d92ee6a03832e38a62a6758a3e399793669b3698 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 26 Mar 2023 12:56:44 +0200 Subject: [PATCH 23/38] feat(domain): add `DeviceSettingsChangedDomainEvent` --- ...vice-settings-update.event-handler.test.ts | 113 ++++++++++++++++++ .../device-settings-update.event-handler.ts | 20 ++-- packages/adapter-tcp/src/tcp.server.ts | 21 ++-- .../device.aggregate-root.test.ts | 45 +++++-- .../aggregate-roots/device.aggregate-root.ts | 27 +++-- ...vice-settings-changed.domain-event.test.ts | 78 ++++++++++++ .../device-settings-changed.domain-event.ts | 27 +++++ 7 files changed, 299 insertions(+), 32 deletions(-) create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts create mode 100644 packages/domain/src/domain-events/device-settings-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-settings-changed.domain-event.ts diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts new file mode 100644 index 00000000..268abaa7 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts @@ -0,0 +1,113 @@ +import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting, VoiceSetting } from '@agnoc/domain'; +import { givenSomeVoiceSettingProps } from '@agnoc/domain/test-support'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceSettingsUpdateEventHandler } from './device-settings-update.event-handler'; +import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceSettingsUpdateEventHandler', function () { + let voiceSettingMapper: VoiceSettingMapper; + let deviceRepository: DeviceRepository; + let eventHandler: DeviceSettingsUpdateEventHandler; + let packetMessage: PacketMessage<'PUSH_DEVICE_AGENT_SETTING_REQ'>; + let device: Device; + + beforeEach(function () { + voiceSettingMapper = imock(); + deviceRepository = imock(); + eventHandler = new DeviceSettingsUpdateEventHandler(instance(voiceSettingMapper), instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('PUSH_DEVICE_AGENT_SETTING_REQ'); + }); + + describe('#handle()', function () { + it('should update the device settings', async function () { + const voiceSetting = new VoiceSetting(givenSomeVoiceSettingProps()); + const payload = new Payload({ + opcode: OPCode.fromName('PUSH_DEVICE_AGENT_SETTING_REQ'), + data: { + deviceId: 1, + voice: { + voiceMode: true, + volume: 2, + }, + quietHours: { + isOpen: true, + beginTime: 0, + endTime: 180, + }, + cleanPreference: { + ecoMode: true, + repeatClean: true, + cleanBroken: true, + carpetTurbo: true, + historyMap: true, + }, + ota: { + forceupgrade: false, + newVersion: false, + otaPackageVersion: '1.0.0', + packageSize: '0', + remoteUrl: '', + systemVersion: '1.0.0', + }, + taskList: { + enable: true, + dayTime: 0, + orderId: 0, + repeat: false, + weekDay: 0, + cleanInfo: { + mapHeadId: 0, + cleanMode: 0, + planId: 0, + twiceClean: false, + windPower: 0, + waterLevel: 0, + }, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(voiceSettingMapper.toDomain(anything())).thenReturn(voiceSetting); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + const [deviceSettings] = capture(device.updateSettings).first(); + + expect(deviceSettings).to.be.instanceOf(DeviceSettings); + expect(deviceSettings.voice).to.equal(voiceSetting); + expect( + deviceSettings.quietHours.equals( + new QuietHoursSetting({ + isEnabled: true, + beginTime: new DeviceTime({ hours: 0, minutes: 0 }), + endTime: new DeviceTime({ hours: 3, minutes: 0 }), + }), + ), + ).to.be.true; + expect(deviceSettings.ecoMode.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; + expect(deviceSettings.repeatClean.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; + expect(deviceSettings.brokenClean.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; + expect(deviceSettings.carpetMode.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; + expect(deviceSettings.historyMap.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; + + verify(packetMessage.assertDevice()).once(); + verify(voiceSettingMapper.toDomain(deepEqual({ isEnabled: true, volume: 2 }))).once(); + verify(device.updateSettings(deviceSettings)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + verify(packetMessage.respond('PUSH_DEVICE_AGENT_SETTING_RSP', deepEqual({ result: 0 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index aa92aeb7..33dbfdb5 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -1,18 +1,19 @@ import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; -import { DomainException } from '@agnoc/toolkit'; import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; - constructor(private readonly deviceVoiceMapper: VoiceSettingMapper) {} + constructor( + private readonly deviceVoiceMapper: VoiceSettingMapper, + private readonly deviceRepository: DeviceRepository, + ) {} async handle(message: PacketMessage<'PUSH_DEVICE_AGENT_SETTING_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const data = message.packet.payload.data; const deviceSettings = new DeviceSettings({ @@ -32,9 +33,14 @@ export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { historyMap: new DeviceSetting({ isEnabled: data.cleanPreference.historyMap ?? false }), }); - message.device.updateConfig(deviceSettings); + // TODO: Fields below are unused + // - data.deviceId + // - data.ota + // - data.taskList - // TODO: save entity and publish domain event + message.device.updateSettings(deviceSettings); + + await this.deviceRepository.saveOne(message.device); await message.respond('PUSH_DEVICE_AGENT_SETTING_RSP', { result: 0 }); } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 4b506dfb..b85fc399 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -86,9 +86,12 @@ export class TCPServer implements Server { // Connection const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); - const connectionDeviceUpdaterService = new ConnectionDeviceUpdaterService(connectionRepository, deviceRepository); + const connectionDeviceUpdaterService = new ConnectionDeviceUpdaterService( + this.connectionRepository, + deviceRepository, + ); const packetEventPublisherService = new PacketEventPublisherService(packetEventBus); - const packetConnectionFinderService = new PacketConnectionFinderService(connectionRepository); + const packetConnectionFinderService = new PacketConnectionFinderService(this.connectionRepository); const connectionManager = new PackerServerConnectionHandler( this.connectionRepository, packetConnectionFactory, @@ -107,7 +110,7 @@ export class TCPServer implements Server { packetEventHandlerRegistry.register( new ClientHeartbeatEventHandler(), new ClientLoginEventHandler(), - new DeviceBatteryUpdateEventHandler(deviceBatteryMapper, deviceRepository), + new DeviceBatteryUpdateEventHandler(deviceBatteryMapper, this.deviceRepository), new DeviceCleanMapDataReportEventHandler(), new DeviceCleanMapReportEventHandler(), new DeviceCleanTaskReportEventHandler(), @@ -122,23 +125,23 @@ export class TCPServer implements Server { deviceBatteryMapper, deviceFanSpeedMapper, deviceWaterLevelMapper, - deviceRepository, + this.deviceRepository, ), new DeviceMemoryMapInfoEventHandler(), new DeviceOfflineEventHandler(), new DeviceRegisterEventHandler(this.deviceRepository), - new DeviceSettingsUpdateEventHandler(deviceVoiceMapper), + new DeviceSettingsUpdateEventHandler(deviceVoiceMapper, this.deviceRepository), new DeviceTimeUpdateEventHandler(), new DeviceUpgradeInfoEventHandler(), - new DeviceVersionUpdateEventHandler(deviceRepository), - new DeviceNetworkUpdateEventHandler(deviceRepository), + new DeviceVersionUpdateEventHandler(this.deviceRepository), + new DeviceNetworkUpdateEventHandler(this.deviceRepository), new DeviceMapUpdateEventHandler( deviceBatteryMapper, deviceModeMapper, deviceStateMapper, deviceErrorMapper, deviceFanSpeedMapper, - deviceRepository, + this.deviceRepository, ), ); @@ -146,7 +149,7 @@ export class TCPServer implements Server { this.domainEventHandlerRegistry.register( new LockDeviceWhenDeviceIsConnectedEventHandler(packetConnectionFinderService), new QueryDeviceInfoWhenDeviceIsLockedEventHandler(packetConnectionFinderService), - new SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler(connectionRepository, deviceRepository), + new SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler(this.connectionRepository, this.deviceRepository), ); // Command event handlers diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 9163cc24..696c9f36 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -5,6 +5,7 @@ import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.do import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; @@ -27,6 +28,7 @@ import { import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; import { DeviceNetwork } from '../value-objects/device-network.value-object'; +import { DeviceSetting } from '../value-objects/device-setting.value-object'; import { DeviceSettings } from '../value-objects/device-settings.value-object'; import { DeviceVersion } from '../value-objects/device-version.value-object'; import { Device } from './device.aggregate-root'; @@ -143,11 +145,11 @@ describe('Device', function () { ); }); - it("should throw an error when 'config' is not a DeviceSettings", function () { + it("should throw an error when 'settings' is not a DeviceSettings", function () { // @ts-expect-error - invalid property - expect(() => new Device({ ...givenSomeDeviceProps(), config: 'foo' })).to.throw( + expect(() => new Device({ ...givenSomeDeviceProps(), settings: 'foo' })).to.throw( ArgumentInvalidException, - `Value 'foo' for property 'config' of Device is not an instance of DeviceSettings`, + `Value 'foo' for property 'settings' of Device is not an instance of DeviceSettings`, ); }); @@ -350,14 +352,39 @@ describe('Device', function () { }); }); - describe('#updateConfig()', function () { - it('should update the device config', function () { - const device = new Device(givenSomeDeviceProps()); - const config = new DeviceSettings(givenSomeDeviceSettingsProps()); + describe('#updateSettings()', function () { + it('should update the device settings', function () { + const previousSettings = new DeviceSettings({ + ...givenSomeDeviceSettingsProps(), + ecoMode: new DeviceSetting({ isEnabled: false }), + }); + const currentSettings = new DeviceSettings({ + ...givenSomeDeviceSettingsProps(), + ecoMode: new DeviceSetting({ isEnabled: true }), + }); + const device = new Device({ ...givenSomeDeviceProps(), settings: previousSettings }); + + device.updateSettings(currentSettings); + + expect(device.settings).to.be.equal(currentSettings); - device.updateConfig(config); + const event = device.domainEvents[1] as DeviceSettingsChangedDomainEvent; - expect(device.config).to.be.equal(config); + expect(event).to.be.instanceOf(DeviceSettingsChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousSettings).to.be.equal(previousSettings); + expect(event.currentSettings).to.be.equal(currentSettings); + }); + + it('should not update the device settings when value is equal', function () { + const previousSettings = new DeviceSettings(givenSomeDeviceSettingsProps()); + const currentSettings = new DeviceSettings(givenSomeDeviceSettingsProps()); + const device = new Device({ ...givenSomeDeviceProps(), settings: previousSettings }); + + device.updateSettings(currentSettings); + + expect(device.settings).to.be.equal(previousSettings); + expect(device.domainEvents[1]).to.not.exist; }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index c2ce137c..8a36f90c 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -4,6 +4,7 @@ import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.do import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; @@ -36,7 +37,7 @@ export interface DeviceProps extends EntityProps { /** Whether the device is locked. */ isLocked: boolean; /** The device settings. */ - config?: DeviceSettings; + settings?: DeviceSettings; /** The device current clean. */ currentClean?: DeviceCleanWork; /** The device orders. */ @@ -101,8 +102,8 @@ export class Device extends AggregateRoot { } /** Returns the device settings. */ - get config(): DeviceSettings | undefined { - return this.props.config; + get settings(): DeviceSettings | undefined { + return this.props.settings; } /** Returns the device current clean. */ @@ -204,9 +205,21 @@ export class Device extends AggregateRoot { } /** Updates the device settings. */ - updateConfig(config?: DeviceSettings): void { - this.validateInstanceProp({ config }, 'config', DeviceSettings); - this.props.config = config; + updateSettings(settings: DeviceSettings): void { + if (settings.equals(this.settings)) { + return; + } + + this.validateDefinedProp({ settings }, 'settings'); + this.validateInstanceProp({ settings }, 'settings', DeviceSettings); + this.addEvent( + new DeviceSettingsChangedDomainEvent({ + aggregateId: this.id, + previousSettings: this.settings, + currentSettings: settings, + }), + ); + this.props.settings = settings; } /** Updates the device current clean. */ @@ -323,7 +336,7 @@ export class Device extends AggregateRoot { this.validateInstanceProp(props, 'version', DeviceVersion); this.validateTypeProp(props, 'isConnected', 'boolean'); this.validateTypeProp(props, 'isLocked', 'boolean'); - this.validateInstanceProp(props, 'config', DeviceSettings); + this.validateInstanceProp(props, 'settings', DeviceSettings); this.validateInstanceProp(props, 'currentClean', DeviceCleanWork); this.validateArrayProp(props, 'orders', DeviceOrder); this.validateArrayProp(props, 'consumables', DeviceConsumable); diff --git a/packages/domain/src/domain-events/device-settings-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-settings-changed.domain-event.test.ts new file mode 100644 index 00000000..7dc61ab4 --- /dev/null +++ b/packages/domain/src/domain-events/device-settings-changed.domain-event.test.ts @@ -0,0 +1,78 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceSettingsProps } from '../test-support'; +import { DeviceSettings } from '../value-objects/device-settings.value-object'; +import { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; +import type { DeviceSettingsChangedDomainEventProps } from './device-settings-changed.domain-event'; + +describe('DeviceSettingsChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceSettingsChangedDomainEventProps(); + const event = new DeviceSettingsChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousSettings).to.be.undefined; + expect(event.currentSettings).to.be.equal(props.currentSettings); + }); + + it("should be created with 'previousSettings'", function () { + const props = { + ...givenSomeDeviceSettingsChangedDomainEventProps(), + previousSettings: new DeviceSettings(givenSomeDeviceSettingsProps()), + }; + const event = new DeviceSettingsChangedDomainEvent(props); + + expect(event.previousSettings).to.be.equal(props.previousSettings); + }); + + it("should thrown an error when 'currentSettings' is not provided", function () { + expect( + () => + new DeviceSettingsChangedDomainEvent({ + ...givenSomeDeviceSettingsChangedDomainEventProps(), + // @ts-expect-error - missing property + currentSettings: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentSettings' for DeviceSettingsChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousSettings' is not an instance of DeviceSettings", function () { + expect( + () => + new DeviceSettingsChangedDomainEvent({ + ...givenSomeDeviceSettingsChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousSettings: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousSettings' of DeviceSettingsChangedDomainEvent is not an instance of DeviceSettings", + ); + }); + + it("should thrown an error when 'currentSettings' is not an instance of DeviceSettings", function () { + expect( + () => + new DeviceSettingsChangedDomainEvent({ + ...givenSomeDeviceSettingsChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentSettings: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentSettings' of DeviceSettingsChangedDomainEvent is not an instance of DeviceSettings", + ); + }); +}); + +function givenSomeDeviceSettingsChangedDomainEventProps(): DeviceSettingsChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousSettings: undefined, + currentSettings: new DeviceSettings(givenSomeDeviceSettingsProps()), + }; +} diff --git a/packages/domain/src/domain-events/device-settings-changed.domain-event.ts b/packages/domain/src/domain-events/device-settings-changed.domain-event.ts new file mode 100644 index 00000000..46c895c9 --- /dev/null +++ b/packages/domain/src/domain-events/device-settings-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceSettings } from '../value-objects/device-settings.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceSettingsChangedDomainEventProps extends DomainEventProps { + previousSettings?: DeviceSettings; + currentSettings: DeviceSettings; +} + +export class DeviceSettingsChangedDomainEvent extends DomainEvent { + get previousSettings(): DeviceSettings | undefined { + return this.props.previousSettings; + } + + get currentSettings(): DeviceSettings { + return this.props.currentSettings; + } + + protected validate(props: DeviceSettingsChangedDomainEventProps): void { + if (props.previousSettings) { + this.validateInstanceProp(props, 'previousSettings', DeviceSettings); + } + + this.validateDefinedProp(props, 'currentSettings'); + this.validateInstanceProp(props, 'currentSettings', DeviceSettings); + } +} From c6b740021c298283436bddbd26d2fb8d08512541 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 26 Mar 2023 13:11:02 +0200 Subject: [PATCH 24/38] test(adapter-tcp): raise coverage --- .../device-locked.event-handler.test.ts | 47 +++++++++++++++++++ .../device-locked.event-handler.ts | 5 +- .../device-upgrade-info.event-handler.test.ts | 43 +++++++++++++++++ .../device-upgrade-info.event-handler.ts | 9 +--- 4 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts new file mode 100644 index 00000000..d94f1119 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts @@ -0,0 +1,47 @@ +import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceLockedEventHandler } from './device-locked.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceLockedEventHandler', function () { + let deviceRepository: DeviceRepository; + let eventHandler: DeviceLockedEventHandler; + let packetMessage: PacketMessage<'DEVICE_CONTROL_LOCK_RSP'>; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + eventHandler = new DeviceLockedEventHandler(instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_CONTROL_LOCK_RSP'); + }); + + describe('#handle()', function () { + it('should lock the device when is not locked', async function () { + when(packetMessage.device).thenReturn(instance(device)); + when(device.isLocked).thenReturn(false); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(device.setAsLocked()).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when device is already locked', async function () { + when(packetMessage.device).thenReturn(instance(device)); + when(device.isLocked).thenReturn(true); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(device.setAsLocked()).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts index 059dc6e9..4a5b9824 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; import type { DeviceRepository } from '@agnoc/domain'; @@ -9,9 +8,7 @@ export class DeviceLockedEventHandler implements PacketEventHandler { constructor(private readonly deviceRepository: DeviceRepository) {} async handle(message: PacketMessage<'DEVICE_CONTROL_LOCK_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); if (!message.device.isLocked) { message.device.setAsLocked(); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts new file mode 100644 index 00000000..29d00ec9 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts @@ -0,0 +1,43 @@ +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceUpgradeInfoEventHandler } from './device-upgrade-info.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceUpgradeInfoEventHandler', function () { + let eventHandler: DeviceUpgradeInfoEventHandler; + let packetMessage: PacketMessage<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>; + + beforeEach(function () { + eventHandler = new DeviceUpgradeInfoEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'); + }); + + describe('#handle()', function () { + it('should update the device battery', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'), + data: { + forceupgrade: false, + newVersion: false, + otaPackageVersion: '1.0.0', + packageSize: '0', + remoteUrl: '', + systemVersion: '1.0.0', + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', deepEqual({ result: 0 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts index 907ad545..6b7749cf 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -6,12 +5,8 @@ export class DeviceUpgradeInfoEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; async handle(message: PacketMessage<'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + // TODO: save device ota info - // TODO: save device upgrade info - - await message.connection.send('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { result: 0 }); + await message.respond('PUSH_DEVICE_PACKAGE_UPGRADE_INFO_RSP', { result: 0 }); } } From 7d49c08d80a9ba20191c8f98a3296b79a6c9e31e Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sun, 26 Mar 2023 19:45:51 +0200 Subject: [PATCH 25/38] feat(domain): add `DeviceCleanWorkChangedDomainEvent` --- ...lean-map-data-report.event-handler.test.ts | 36 +++++++++ ...ice-clean-map-data-report.event-handler.ts | 5 -- ...ice-clean-map-report.event-handler.test.ts | 36 +++++++++ .../device-clean-map-report.event-handler.ts | 5 -- ...ce-clean-task-report.event-handler.test.ts | 48 ++++++++++++ .../device-clean-task-report.event-handler.ts | 5 -- ...e-get-all-global-map.event-handler.test.ts | 24 ++++++ ...device-get-all-global-map.event-handler.ts | 7 +- .../device-map-update.event-handler.ts | 2 +- ...ce-map-work-status-update.event-handler.ts | 2 +- ...vice-memory-map-info.event-handler.test.ts | 24 ++++++ .../device-memory-map-info.event-handler.ts | 7 +- .../device.aggregate-root.test.ts | 39 +++++++--- .../aggregate-roots/device.aggregate-root.ts | 27 +++++-- ...ce-clean-work-changed.domain-event.test.ts | 78 +++++++++++++++++++ .../device-clean-work-changed.domain-event.ts | 27 +++++++ .../domain/src/domain-events/domain-events.ts | 14 +++- 17 files changed, 339 insertions(+), 47 deletions(-) create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts create mode 100644 packages/domain/src/domain-events/device-clean-work-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-clean-work-changed.domain-event.ts diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts new file mode 100644 index 00000000..92cce37d --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts @@ -0,0 +1,36 @@ +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceCleanMapDataReportEventHandler } from './device-clean-map-data-report.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceCleanMapDataReportEventHandler', function () { + let eventHandler: DeviceCleanMapDataReportEventHandler; + let packetMessage: PacketMessage<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>; + + beforeEach(function () { + eventHandler = new DeviceCleanMapDataReportEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_CLEANMAP_BINDATA_REPORT_REQ'); + }); + + describe('#handle()', function () { + it('should update the device version', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_CLEANMAP_BINDATA_REPORT_REQ'), + data: { cleanId: 1 }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('DEVICE_CLEANMAP_BINDATA_REPORT_RSP', deepEqual({ result: 0, cleanId: 1 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts index e1abd193..ac203bfa 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -6,10 +5,6 @@ export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler readonly forName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; async handle(message: PacketMessage<'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - const data = message.packet.payload.data; // TODO: save device clean map data diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts new file mode 100644 index 00000000..39135a55 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts @@ -0,0 +1,36 @@ +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceCleanMapReportEventHandler } from './device-clean-map-report.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceCleanMapReportEventHandler', function () { + let eventHandler: DeviceCleanMapReportEventHandler; + let packetMessage: PacketMessage<'DEVICE_EVENT_REPORT_CLEANMAP'>; + + beforeEach(function () { + eventHandler = new DeviceCleanMapReportEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_EVENT_REPORT_CLEANMAP'); + }); + + describe('#handle()', function () { + it('should update the device version', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_EVENT_REPORT_CLEANMAP'), + data: { cleanId: 1 }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('DEVICE_EVENT_REPORT_RSP', deepEqual({ result: 0, body: { cleanId: 1 } }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts index a8b0098c..2b09a0ca 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -6,10 +5,6 @@ export class DeviceCleanMapReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANMAP'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANMAP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - const data = message.packet.payload.data; // TODO: save device clean map data diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts new file mode 100644 index 00000000..5ddbf52e --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts @@ -0,0 +1,48 @@ +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceCleanTaskReportEventHandler } from './device-clean-task-report.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceCleanTaskReportEventHandler', function () { + let eventHandler: DeviceCleanTaskReportEventHandler; + let packetMessage: PacketMessage<'DEVICE_EVENT_REPORT_CLEANTASK'>; + + beforeEach(function () { + eventHandler = new DeviceCleanTaskReportEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_EVENT_REPORT_CLEANTASK'); + }); + + describe('#handle()', function () { + it('should update the device version', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_EVENT_REPORT_CLEANTASK'), + data: { + cleanId: 1, + startTime: 0, + endTime: 180, + unk4: 0, + unk5: 0, + unk6: 0, + unk7: 0, + unk8: { unk1Unk1: 0, unk1Unk2: 0, unk1Unk6: 0 }, + mapHeadId: 2, + mapName: 'map', + planName: 'plan', + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.respond('UNK_11A4', deepEqual({ unk1: 0 }))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts index 4d210675..26f9575d 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -1,4 +1,3 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; @@ -6,10 +5,6 @@ export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANTASK'; async handle(message: PacketMessage<'DEVICE_EVENT_REPORT_CLEANTASK'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - // TODO: save device clean task data await message.respond('UNK_11A4', { unk1: 0 }); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts new file mode 100644 index 00000000..8c3dc730 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts @@ -0,0 +1,24 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceGetAllGlobalMapEventHandler } from './device-get-all-global-map.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceGetAllGlobalMapEventHandler', function () { + let eventHandler: DeviceGetAllGlobalMapEventHandler; + let packetMessage: PacketMessage<'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'>; + + beforeEach(function () { + eventHandler = new DeviceGetAllGlobalMapEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'); + }); + + describe('#handle()', function () { + it('should do nothing', async function () { + await eventHandler.handle(instance(packetMessage)); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts index 9ff0006f..cf8c68c1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts @@ -1,15 +1,10 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceGetAllGlobalMapEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; - async handle(message: PacketMessage<'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - + async handle(_: PacketMessage<'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'>): Promise { // TODO: investigate the meaning of this packet. } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index 241726c6..5f18da28 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -61,7 +61,7 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { faultCode, } = statusInfo; - message.device.updateCurrentClean( + message.device.updateCurrentCleanWork( new DeviceCleanWork({ size: new CleanSize(statusInfo.cleanSize), time: DeviceTime.fromMinutes(statusInfo.cleanTime), diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index 4b791206..dc0307a1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -41,7 +41,7 @@ export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler cleanTime, } = message.packet.payload.data; - message.device.updateCurrentClean( + message.device.updateCurrentCleanWork( new DeviceCleanWork({ size: new CleanSize(cleanSize), time: DeviceTime.fromMinutes(cleanTime), diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts new file mode 100644 index 00000000..48d5994c --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts @@ -0,0 +1,24 @@ +import { imock, instance } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceMemoryMapInfoEventHandler } from './device-memory-map-info.event-handler'; +import type { PacketMessage } from '../packet.message'; + +describe('DeviceMemoryMapInfoEventHandler', function () { + let eventHandler: DeviceMemoryMapInfoEventHandler; + let packetMessage: PacketMessage<'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'>; + + beforeEach(function () { + eventHandler = new DeviceMemoryMapInfoEventHandler(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'); + }); + + describe('#handle()', function () { + it('should do nothing', async function () { + await eventHandler.handle(instance(packetMessage)); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts index 6fdad56d..60407bb7 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts @@ -1,15 +1,10 @@ -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; export class DeviceMemoryMapInfoEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; - async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } - + async handle(_: PacketMessage<'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'>): Promise { // TODO: save device memory map info } } diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 696c9f36..d8beff04 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -1,12 +1,14 @@ import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; import { expect } from 'chai'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; +import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; +import { CleanSize } from '../domain-primitives/clean-size.domain-primitive'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -153,11 +155,11 @@ describe('Device', function () { ); }); - it("should throw an error when 'currentClean' is not a DeviceCleanWork", function () { + it("should throw an error when 'currentCleanWork' is not a DeviceCleanWork", function () { // @ts-expect-error - invalid property - expect(() => new Device({ ...givenSomeDeviceProps(), currentClean: 'foo' })).to.throw( + expect(() => new Device({ ...givenSomeDeviceProps(), currentCleanWork: 'foo' })).to.throw( ArgumentInvalidException, - `Value 'foo' for property 'currentClean' of Device is not an instance of DeviceCleanWork`, + `Value 'foo' for property 'currentCleanWork' of Device is not an instance of DeviceCleanWork`, ); }); @@ -388,14 +390,33 @@ describe('Device', function () { }); }); - describe('#updateCurrentClean()', function () { - it('should update the device current clean', function () { - const device = new Device(givenSomeDeviceProps()); - const currentClean = new DeviceCleanWork(givenSomeDeviceCleanWorkProps()); + describe('#updateCurrentCleanWork()', function () { + it('should update the device cleanWork', function () { + const previousCleanWork = new DeviceCleanWork({ ...givenSomeDeviceCleanWorkProps(), size: new CleanSize(1) }); + const currentCleanWork = new DeviceCleanWork({ ...givenSomeDeviceCleanWorkProps(), size: new CleanSize(2) }); + const device = new Device({ ...givenSomeDeviceProps(), currentCleanWork: previousCleanWork }); + + device.updateCurrentCleanWork(currentCleanWork); + + expect(device.currentCleanWork).to.be.equal(currentCleanWork); - device.updateCurrentClean(currentClean); + const event = device.domainEvents[1] as DeviceCleanWorkChangedDomainEvent; - expect(device.currentClean).to.be.equal(currentClean); + expect(event).to.be.instanceOf(DeviceCleanWorkChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousCleanWork).to.be.equal(previousCleanWork); + expect(event.currentCleanWork).to.be.equal(currentCleanWork); + }); + + it('should not update the device cleanWork when value is equal', function () { + const previousCleanWork = new DeviceCleanWork(givenSomeDeviceCleanWorkProps()); + const currentCleanWork = new DeviceCleanWork(givenSomeDeviceCleanWorkProps()); + const device = new Device({ ...givenSomeDeviceProps(), currentCleanWork: previousCleanWork }); + + device.updateCurrentCleanWork(currentCleanWork); + + expect(device.currentCleanWork).to.be.equal(previousCleanWork); + expect(device.domainEvents[1]).to.not.exist; }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index 8a36f90c..bff31786 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -1,5 +1,6 @@ import { AggregateRoot, ID } from '@agnoc/toolkit'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; +import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; @@ -39,7 +40,7 @@ export interface DeviceProps extends EntityProps { /** The device settings. */ settings?: DeviceSettings; /** The device current clean. */ - currentClean?: DeviceCleanWork; + currentCleanWork?: DeviceCleanWork; /** The device orders. */ orders?: DeviceOrder[]; /** The device consumables. */ @@ -107,8 +108,8 @@ export class Device extends AggregateRoot { } /** Returns the device current clean. */ - get currentClean(): DeviceCleanWork | undefined { - return this.props.currentClean; + get currentCleanWork(): DeviceCleanWork | undefined { + return this.props.currentCleanWork; } /** Returns the device orders. */ @@ -223,9 +224,21 @@ export class Device extends AggregateRoot { } /** Updates the device current clean. */ - updateCurrentClean(currentClean?: DeviceCleanWork): void { - this.validateInstanceProp({ currentClean }, 'currentClean', DeviceCleanWork); - this.props.currentClean = currentClean; + updateCurrentCleanWork(currentCleanWork: DeviceCleanWork): void { + if (currentCleanWork.equals(this.currentCleanWork)) { + return; + } + + this.validateDefinedProp({ currentCleanWork }, 'currentCleanWork'); + this.validateInstanceProp({ currentCleanWork }, 'currentCleanWork', DeviceCleanWork); + this.addEvent( + new DeviceCleanWorkChangedDomainEvent({ + aggregateId: this.id, + previousCleanWork: this.currentCleanWork, + currentCleanWork, + }), + ); + this.props.currentCleanWork = currentCleanWork; } /** Updates the device orders. */ @@ -337,7 +350,7 @@ export class Device extends AggregateRoot { this.validateTypeProp(props, 'isConnected', 'boolean'); this.validateTypeProp(props, 'isLocked', 'boolean'); this.validateInstanceProp(props, 'settings', DeviceSettings); - this.validateInstanceProp(props, 'currentClean', DeviceCleanWork); + this.validateInstanceProp(props, 'currentCleanWork', DeviceCleanWork); this.validateArrayProp(props, 'orders', DeviceOrder); this.validateArrayProp(props, 'consumables', DeviceConsumable); this.validateInstanceProp(props, 'map', DeviceMap); diff --git a/packages/domain/src/domain-events/device-clean-work-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-clean-work-changed.domain-event.test.ts new file mode 100644 index 00000000..2dbe13b4 --- /dev/null +++ b/packages/domain/src/domain-events/device-clean-work-changed.domain-event.test.ts @@ -0,0 +1,78 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceCleanWorkProps } from '../test-support'; +import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; +import { DeviceCleanWorkChangedDomainEvent } from './device-clean-work-changed.domain-event'; +import type { DeviceCleanWorkChangedDomainEventProps } from './device-clean-work-changed.domain-event'; + +describe('DeviceCleanWorkChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceCleanWorkChangedDomainEventProps(); + const event = new DeviceCleanWorkChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousCleanWork).to.be.undefined; + expect(event.currentCleanWork).to.be.equal(props.currentCleanWork); + }); + + it("should be created with 'previousCleanWork'", function () { + const props = { + ...givenSomeDeviceCleanWorkChangedDomainEventProps(), + previousCleanWork: new DeviceCleanWork(givenSomeDeviceCleanWorkProps()), + }; + const event = new DeviceCleanWorkChangedDomainEvent(props); + + expect(event.previousCleanWork).to.be.equal(props.previousCleanWork); + }); + + it("should thrown an error when 'currentCleanWork' is not provided", function () { + expect( + () => + new DeviceCleanWorkChangedDomainEvent({ + ...givenSomeDeviceCleanWorkChangedDomainEventProps(), + // @ts-expect-error - missing property + currentCleanWork: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentCleanWork' for DeviceCleanWorkChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousCleanWork' is not an instance of DeviceCleanWork", function () { + expect( + () => + new DeviceCleanWorkChangedDomainEvent({ + ...givenSomeDeviceCleanWorkChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousCleanWork: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousCleanWork' of DeviceCleanWorkChangedDomainEvent is not an instance of DeviceCleanWork", + ); + }); + + it("should thrown an error when 'currentCleanWork' is not an instance of DeviceCleanWork", function () { + expect( + () => + new DeviceCleanWorkChangedDomainEvent({ + ...givenSomeDeviceCleanWorkChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentCleanWork: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentCleanWork' of DeviceCleanWorkChangedDomainEvent is not an instance of DeviceCleanWork", + ); + }); +}); + +function givenSomeDeviceCleanWorkChangedDomainEventProps(): DeviceCleanWorkChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousCleanWork: undefined, + currentCleanWork: new DeviceCleanWork(givenSomeDeviceCleanWorkProps()), + }; +} diff --git a/packages/domain/src/domain-events/device-clean-work-changed.domain-event.ts b/packages/domain/src/domain-events/device-clean-work-changed.domain-event.ts new file mode 100644 index 00000000..fde2e719 --- /dev/null +++ b/packages/domain/src/domain-events/device-clean-work-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceCleanWork } from '../value-objects/device-clean-work.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceCleanWorkChangedDomainEventProps extends DomainEventProps { + previousCleanWork?: DeviceCleanWork; + currentCleanWork: DeviceCleanWork; +} + +export class DeviceCleanWorkChangedDomainEvent extends DomainEvent { + get previousCleanWork(): DeviceCleanWork | undefined { + return this.props.previousCleanWork; + } + + get currentCleanWork(): DeviceCleanWork { + return this.props.currentCleanWork; + } + + protected validate(props: DeviceCleanWorkChangedDomainEventProps): void { + if (props.previousCleanWork) { + this.validateInstanceProp(props, 'previousCleanWork', DeviceCleanWork); + } + + this.validateDefinedProp(props, 'currentCleanWork'); + this.validateInstanceProp(props, 'currentCleanWork', DeviceCleanWork); + } +} diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index b6f5fa49..76300bf3 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -1,13 +1,23 @@ import type { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; import type { DeviceBatteryChangedDomainEvent } from './device-battery-changed.domain-event'; +import type { DeviceCleanWorkChangedDomainEvent } from './device-clean-work-changed.domain-event'; import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; +import type { DeviceCreatedDomainEvent } from './device-created.domain-event'; import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; +import type { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; +import type { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; +import type { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; export type DomainEvents = { - DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; - DeviceLockedDomainEvent: DeviceLockedDomainEvent; ConnectionDeviceChangedDomainEvent: ConnectionDeviceChangedDomainEvent; DeviceBatteryChangedDomainEvent: DeviceBatteryChangedDomainEvent; + DeviceCleanWorkChangedDomainEvent: DeviceCleanWorkChangedDomainEvent; + DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; + DeviceCreatedDomainEvent: DeviceCreatedDomainEvent; + DeviceLockedDomainEvent: DeviceLockedDomainEvent; + DeviceNetworkChangedDomainEvent: DeviceNetworkChangedDomainEvent; + DeviceSettingsChangedDomainEvent: DeviceSettingsChangedDomainEvent; + DeviceVersionChangedDomainEvent: DeviceVersionChangedDomainEvent; }; export type DomainEventNames = keyof DomainEvents; From 23c6d936503332fc36b1a730a9141f552acf2a85 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Mon, 27 Mar 2023 17:59:50 +0200 Subject: [PATCH 26/38] feat(domain): add more domain events --- .../device.aggregate-root.test.ts | 188 +++++++++++++++--- .../aggregate-roots/device.aggregate-root.ts | 111 ++++++++++- .../device-error-changed.domain-event.test.ts | 74 +++++++ .../device-error-changed.domain-event.ts | 27 +++ ...ice-fan-speed-changed.domain-event.test.ts | 77 +++++++ .../device-fan-speed-changed.domain-event.ts | 27 +++ .../device-map-pending.domain-event.test.ts | 47 +++++ .../device-map-pending.domain-event.ts | 17 ++ .../device-mode-changed.domain-event.test.ts | 74 +++++++ .../device-mode-changed.domain-event.ts | 27 +++ .../device-mop-attached.domain-event.test.ts | 47 +++++ .../device-mop-attached.domain-event.ts | 17 ++ .../device-state-changed.domain-event.test.ts | 74 +++++++ .../device-state-changed.domain-event.ts | 27 +++ ...e-water-level-changed.domain-event.test.ts | 77 +++++++ ...device-water-level-changed.domain-event.ts | 27 +++ .../domain/src/domain-events/domain-events.ts | 14 ++ 17 files changed, 913 insertions(+), 39 deletions(-) create mode 100644 packages/domain/src/domain-events/device-error-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-error-changed.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-fan-speed-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-fan-speed-changed.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-map-pending.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-map-pending.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-mode-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-mode-changed.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-mop-attached.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-mop-attached.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-state-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-state-changed.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-water-level-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-water-level-changed.domain-event.ts diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index d8beff04..ce4032e8 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -4,10 +4,17 @@ import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; +import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; +import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceMapPendingDomainEvent } from '../domain-events/device-map-pending.domain-event'; +import { DeviceModeChangedDomainEvent } from '../domain-events/device-mode-changed.domain-event'; +import { DeviceMopAttachedDomainEvent } from '../domain-events/device-mop-attached.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; +import { DeviceStateChangedDomainEvent } from '../domain-events/device-state-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; +import { DeviceWaterLevelChangedDomainEvent } from '../domain-events/device-water-level-changed.domain-event'; import { CleanSize } from '../domain-primitives/clean-size.domain-primitive'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; @@ -515,76 +522,201 @@ describe('Device', function () { describe('#updateState()', function () { it('should update the device state', function () { - const device = new Device(givenSomeDeviceProps()); - const state = new DeviceState(DeviceStateValue.Idle); + const previousState = new DeviceState(DeviceStateValue.Idle); + const currentState = new DeviceState(DeviceStateValue.Docked); + const device = new Device({ ...givenSomeDeviceProps(), state: previousState }); + + device.updateState(currentState); + + expect(device.state).to.be.equal(currentState); + + const event = device.domainEvents[1] as DeviceStateChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceStateChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousState).to.be.equal(previousState); + expect(event.currentState).to.be.equal(currentState); + }); + + it('should not update the device state when value is equal', function () { + const previousState = new DeviceState(DeviceStateValue.Idle); + const currentState = new DeviceState(DeviceStateValue.Idle); + const device = new Device({ ...givenSomeDeviceProps(), state: previousState }); - device.updateState(state); + device.updateState(currentState); - expect(device.state).to.be.equal(state); + expect(device.state).to.be.equal(previousState); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateMode()', function () { it('should update the device mode', function () { - const device = new Device(givenSomeDeviceProps()); - const mode = new DeviceMode(DeviceModeValue.Spot); + const previousMode = new DeviceMode(DeviceModeValue.None); + const currentMode = new DeviceMode(DeviceModeValue.Mop); + const device = new Device({ ...givenSomeDeviceProps(), mode: previousMode }); + + device.updateMode(currentMode); + + expect(device.mode).to.be.equal(currentMode); - device.updateMode(mode); + const event = device.domainEvents[1] as DeviceModeChangedDomainEvent; - expect(device.mode).to.be.equal(mode); + expect(event).to.be.instanceOf(DeviceModeChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousMode).to.be.equal(previousMode); + expect(event.currentMode).to.be.equal(currentMode); + }); + + it('should not update the device mode when value is equal', function () { + const previousMode = new DeviceMode(DeviceModeValue.None); + const currentMode = new DeviceMode(DeviceModeValue.None); + const device = new Device({ ...givenSomeDeviceProps(), mode: previousMode }); + + device.updateMode(currentMode); + + expect(device.mode).to.be.equal(previousMode); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateError()', function () { it('should update the device error', function () { - const device = new Device(givenSomeDeviceProps()); - const error = new DeviceError(DeviceErrorValue.None); + const previousError = new DeviceError(DeviceErrorValue.None); + const currentError = new DeviceError(DeviceErrorValue.WheelUp); + const device = new Device({ ...givenSomeDeviceProps(), error: previousError }); + + device.updateError(currentError); - device.updateError(error); + expect(device.error).to.be.equal(currentError); - expect(device.error).to.be.equal(error); + const event = device.domainEvents[1] as DeviceErrorChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceErrorChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousError).to.be.equal(previousError); + expect(event.currentError).to.be.equal(currentError); + }); + + it('should not update the device error when value is equal', function () { + const previousError = new DeviceError(DeviceErrorValue.None); + const currentError = new DeviceError(DeviceErrorValue.None); + const device = new Device({ ...givenSomeDeviceProps(), error: previousError }); + + device.updateError(currentError); + + expect(device.error).to.be.equal(previousError); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateFanSpeed()', function () { - it('should update the device fan speed', function () { - const device = new Device(givenSomeDeviceProps()); - const fanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + it('should update the device fanSpeed', function () { + const previousFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Off); + const currentFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const device = new Device({ ...givenSomeDeviceProps(), fanSpeed: previousFanSpeed }); - device.updateFanSpeed(fanSpeed); + device.updateFanSpeed(currentFanSpeed); - expect(device.fanSpeed).to.be.equal(fanSpeed); + expect(device.fanSpeed).to.be.equal(currentFanSpeed); + + const event = device.domainEvents[1] as DeviceFanSpeedChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceFanSpeedChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousFanSpeed).to.be.equal(previousFanSpeed); + expect(event.currentFanSpeed).to.be.equal(currentFanSpeed); + }); + + it('should not update the device fanSpeed when value is equal', function () { + const previousFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Off); + const currentFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Off); + const device = new Device({ ...givenSomeDeviceProps(), fanSpeed: previousFanSpeed }); + + device.updateFanSpeed(currentFanSpeed); + + expect(device.fanSpeed).to.be.equal(previousFanSpeed); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateWaterLevel()', function () { - it('should update the device water level', function () { - const device = new Device(givenSomeDeviceProps()); - const waterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Low); + it('should update the device waterLevel', function () { + const previousWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Off); + const currentWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Low); + const device = new Device({ ...givenSomeDeviceProps(), waterLevel: previousWaterLevel }); + + device.updateWaterLevel(currentWaterLevel); + + expect(device.waterLevel).to.be.equal(currentWaterLevel); - device.updateWaterLevel(waterLevel); + const event = device.domainEvents[1] as DeviceWaterLevelChangedDomainEvent; - expect(device.waterLevel).to.be.equal(waterLevel); + expect(event).to.be.instanceOf(DeviceWaterLevelChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousWaterLevel).to.be.equal(previousWaterLevel); + expect(event.currentWaterLevel).to.be.equal(currentWaterLevel); + }); + + it('should not update the device waterLevel when value is equal', function () { + const previousWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Off); + const currentWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Off); + const device = new Device({ ...givenSomeDeviceProps(), waterLevel: previousWaterLevel }); + + device.updateWaterLevel(currentWaterLevel); + + expect(device.waterLevel).to.be.equal(previousWaterLevel); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateHasMopAttached()', function () { - it('should update the device has mop attached', function () { + it('should update the device hasMopAttached', function () { + const device = new Device({ ...givenSomeDeviceProps(), hasMopAttached: false }); + + device.updateHasMopAttached(true); + + expect(device.hasMopAttached).to.be.equal(true); + + const event = device.domainEvents[1] as DeviceMopAttachedDomainEvent; + + expect(event).to.be.instanceOf(DeviceMopAttachedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.isAttached).to.be.equal(true); + }); + + it('should not update the device hasMopAttached when value is equal', function () { const device = new Device({ ...givenSomeDeviceProps(), hasMopAttached: true }); - device.updateHasMopAttached(false); + device.updateHasMopAttached(true); - expect(device.hasMopAttached).to.be.equal(false); + expect(device.hasMopAttached).to.be.equal(true); + expect(device.domainEvents[1]).to.not.exist; }); }); describe('#updateHasWaitingMap()', function () { - it('should update the device has waiting map', function () { + it('should update the device hasWaitingMap', function () { + const device = new Device({ ...givenSomeDeviceProps(), hasWaitingMap: false }); + + device.updateHasWaitingMap(true); + + expect(device.hasWaitingMap).to.be.equal(true); + + const event = device.domainEvents[1] as DeviceMapPendingDomainEvent; + + expect(event).to.be.instanceOf(DeviceMapPendingDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.isPending).to.be.equal(true); + }); + + it('should not update the device hasWaitingMap when value is equal', function () { const device = new Device({ ...givenSomeDeviceProps(), hasWaitingMap: true }); - device.updateHasWaitingMap(false); + device.updateHasWaitingMap(true); - expect(device.hasWaitingMap).to.be.equal(false); + expect(device.hasWaitingMap).to.be.equal(true); + expect(device.domainEvents[1]).to.not.exist; }); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index bff31786..87b1aa51 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -3,10 +3,17 @@ import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; +import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; +import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceMapPendingDomainEvent } from '../domain-events/device-map-pending.domain-event'; +import { DeviceModeChangedDomainEvent } from '../domain-events/device-mode-changed.domain-event'; +import { DeviceMopAttachedDomainEvent } from '../domain-events/device-mop-attached.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; +import { DeviceStateChangedDomainEvent } from '../domain-events/device-state-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; +import { DeviceWaterLevelChangedDomainEvent } from '../domain-events/device-water-level-changed.domain-event'; import { DeviceBattery } from '../domain-primitives/device-battery.domain-primitive'; import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; @@ -296,45 +303,127 @@ export class Device extends AggregateRoot { } /** Updates the device state. */ - updateState(state?: DeviceState): void { + updateState(state: DeviceState): void { + if (state.equals(this.state)) { + return; + } + + this.validateDefinedProp({ state }, 'state'); this.validateInstanceProp({ state }, 'state', DeviceState); + this.addEvent( + new DeviceStateChangedDomainEvent({ + aggregateId: this.id, + previousState: this.state, + currentState: state, + }), + ); this.props.state = state; } /** Updates the device mode. */ - updateMode(mode?: DeviceMode): void { + updateMode(mode: DeviceMode): void { + if (mode.equals(this.mode)) { + return; + } + + this.validateDefinedProp({ mode }, 'mode'); this.validateInstanceProp({ mode }, 'mode', DeviceMode); + this.addEvent( + new DeviceModeChangedDomainEvent({ + aggregateId: this.id, + previousMode: this.mode, + currentMode: mode, + }), + ); this.props.mode = mode; } /** Updates the device error. */ - updateError(error?: DeviceError): void { + updateError(error: DeviceError): void { + if (error.equals(this.error)) { + return; + } + + this.validateDefinedProp({ error }, 'error'); this.validateInstanceProp({ error }, 'error', DeviceError); + this.addEvent( + new DeviceErrorChangedDomainEvent({ + aggregateId: this.id, + previousError: this.error, + currentError: error, + }), + ); this.props.error = error; } /** Updates the device fan speed. */ - updateFanSpeed(fanSpeed?: DeviceFanSpeed): void { + updateFanSpeed(fanSpeed: DeviceFanSpeed): void { + if (fanSpeed.equals(this.fanSpeed)) { + return; + } + + this.validateDefinedProp({ fanSpeed }, 'fanSpeed'); this.validateInstanceProp({ fanSpeed }, 'fanSpeed', DeviceFanSpeed); + this.addEvent( + new DeviceFanSpeedChangedDomainEvent({ + aggregateId: this.id, + previousFanSpeed: this.fanSpeed, + currentFanSpeed: fanSpeed, + }), + ); this.props.fanSpeed = fanSpeed; } /** Updates the device water level. */ - updateWaterLevel(waterLevel?: DeviceWaterLevel): void { + updateWaterLevel(waterLevel: DeviceWaterLevel): void { + if (waterLevel.equals(this.waterLevel)) { + return; + } + + this.validateDefinedProp({ waterLevel }, 'waterLevel'); this.validateInstanceProp({ waterLevel }, 'waterLevel', DeviceWaterLevel); + this.addEvent( + new DeviceWaterLevelChangedDomainEvent({ + aggregateId: this.id, + previousWaterLevel: this.waterLevel, + currentWaterLevel: waterLevel, + }), + ); this.props.waterLevel = waterLevel; } /** Updates whether the device has a mop attached. */ - updateHasMopAttached(value: boolean): void { - this.validateTypeProp({ hasMopAttached: value }, 'hasMopAttached', 'boolean'); - this.props.hasMopAttached = value; + updateHasMopAttached(hasMopAttached: boolean): void { + if (hasMopAttached === this.hasMopAttached) { + return; + } + + this.validateDefinedProp({ hasMopAttached }, 'hasMopAttached'); + this.validateTypeProp({ hasMopAttached }, 'hasMopAttached', 'boolean'); + this.addEvent( + new DeviceMopAttachedDomainEvent({ + aggregateId: this.id, + isAttached: hasMopAttached, + }), + ); + this.props.hasMopAttached = hasMopAttached; } /** Updates whether the device has a waiting map. */ - updateHasWaitingMap(value: boolean): void { - this.validateTypeProp({ hasWaitingMap: value }, 'hasWaitingMap', 'boolean'); - this.props.hasWaitingMap = value; + updateHasWaitingMap(hasWaitingMap: boolean): void { + if (hasWaitingMap === this.hasWaitingMap) { + return; + } + + this.validateDefinedProp({ hasWaitingMap }, 'hasWaitingMap'); + this.validateTypeProp({ hasWaitingMap }, 'hasWaitingMap', 'boolean'); + this.addEvent( + new DeviceMapPendingDomainEvent({ + aggregateId: this.id, + isPending: hasWaitingMap, + }), + ); + this.props.hasWaitingMap = hasWaitingMap; } protected validate(props: DeviceProps): void { diff --git a/packages/domain/src/domain-events/device-error-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-error-changed.domain-event.test.ts new file mode 100644 index 00000000..16ffb864 --- /dev/null +++ b/packages/domain/src/domain-events/device-error-changed.domain-event.test.ts @@ -0,0 +1,74 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceError, DeviceErrorValue } from '../domain-primitives/device-error.domain-primitive'; +import { DeviceErrorChangedDomainEvent } from './device-error-changed.domain-event'; +import type { DeviceErrorChangedDomainEventProps } from './device-error-changed.domain-event'; + +describe('DeviceErrorChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceErrorChangedDomainEventProps(); + const event = new DeviceErrorChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousError).to.be.undefined; + expect(event.currentError).to.be.equal(props.currentError); + }); + + it("should be created with 'previousError'", function () { + const props = { + ...givenSomeDeviceErrorChangedDomainEventProps(), + previousError: new DeviceError(DeviceErrorValue.None), + }; + const event = new DeviceErrorChangedDomainEvent(props); + + expect(event.previousError).to.be.equal(props.previousError); + }); + + it("should thrown an error when 'currentError' is not provided", function () { + expect( + () => + new DeviceErrorChangedDomainEvent({ + ...givenSomeDeviceErrorChangedDomainEventProps(), + // @ts-expect-error - missing property + currentError: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'currentError' for DeviceErrorChangedDomainEvent not provided`); + }); + + it("should thrown an error when 'previousError' is not an instance of DeviceError", function () { + expect( + () => + new DeviceErrorChangedDomainEvent({ + ...givenSomeDeviceErrorChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousError: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousError' of DeviceErrorChangedDomainEvent is not an instance of DeviceError", + ); + }); + + it("should thrown an error when 'currentError' is not an instance of DeviceError", function () { + expect( + () => + new DeviceErrorChangedDomainEvent({ + ...givenSomeDeviceErrorChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentError: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentError' of DeviceErrorChangedDomainEvent is not an instance of DeviceError", + ); + }); +}); + +function givenSomeDeviceErrorChangedDomainEventProps(): DeviceErrorChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousError: undefined, + currentError: new DeviceError(DeviceErrorValue.None), + }; +} diff --git a/packages/domain/src/domain-events/device-error-changed.domain-event.ts b/packages/domain/src/domain-events/device-error-changed.domain-event.ts new file mode 100644 index 00000000..0a8bab3e --- /dev/null +++ b/packages/domain/src/domain-events/device-error-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceError } from '../domain-primitives/device-error.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceErrorChangedDomainEventProps extends DomainEventProps { + previousError?: DeviceError; + currentError: DeviceError; +} + +export class DeviceErrorChangedDomainEvent extends DomainEvent { + get previousError(): DeviceError | undefined { + return this.props.previousError; + } + + get currentError(): DeviceError { + return this.props.currentError; + } + + protected validate(props: DeviceErrorChangedDomainEventProps): void { + if (props.previousError) { + this.validateInstanceProp(props, 'previousError', DeviceError); + } + + this.validateDefinedProp(props, 'currentError'); + this.validateInstanceProp(props, 'currentError', DeviceError); + } +} diff --git a/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.test.ts new file mode 100644 index 00000000..708bc0cd --- /dev/null +++ b/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.test.ts @@ -0,0 +1,77 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; +import { DeviceFanSpeedChangedDomainEvent } from './device-fan-speed-changed.domain-event'; +import type { DeviceFanSpeedChangedDomainEventProps } from './device-fan-speed-changed.domain-event'; + +describe('DeviceFanSpeedChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceFanSpeedChangedDomainEventProps(); + const event = new DeviceFanSpeedChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousFanSpeed).to.be.undefined; + expect(event.currentFanSpeed).to.be.equal(props.currentFanSpeed); + }); + + it("should be created with 'previousFanSpeed'", function () { + const props = { + ...givenSomeDeviceFanSpeedChangedDomainEventProps(), + previousFanSpeed: new DeviceFanSpeed(DeviceFanSpeedValue.Low), + }; + const event = new DeviceFanSpeedChangedDomainEvent(props); + + expect(event.previousFanSpeed).to.be.equal(props.previousFanSpeed); + }); + + it("should thrown an error when 'currentFanSpeed' is not provided", function () { + expect( + () => + new DeviceFanSpeedChangedDomainEvent({ + ...givenSomeDeviceFanSpeedChangedDomainEventProps(), + // @ts-expect-error - missing property + currentFanSpeed: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentFanSpeed' for DeviceFanSpeedChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousFanSpeed' is not an instance of DeviceFanSpeed", function () { + expect( + () => + new DeviceFanSpeedChangedDomainEvent({ + ...givenSomeDeviceFanSpeedChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousFanSpeed: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousFanSpeed' of DeviceFanSpeedChangedDomainEvent is not an instance of DeviceFanSpeed", + ); + }); + + it("should thrown an error when 'currentFanSpeed' is not an instance of DeviceFanSpeed", function () { + expect( + () => + new DeviceFanSpeedChangedDomainEvent({ + ...givenSomeDeviceFanSpeedChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentFanSpeed: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentFanSpeed' of DeviceFanSpeedChangedDomainEvent is not an instance of DeviceFanSpeed", + ); + }); +}); + +function givenSomeDeviceFanSpeedChangedDomainEventProps(): DeviceFanSpeedChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousFanSpeed: undefined, + currentFanSpeed: new DeviceFanSpeed(DeviceFanSpeedValue.Low), + }; +} diff --git a/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.ts b/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.ts new file mode 100644 index 00000000..4656d7a4 --- /dev/null +++ b/packages/domain/src/domain-events/device-fan-speed-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceFanSpeedChangedDomainEventProps extends DomainEventProps { + previousFanSpeed?: DeviceFanSpeed; + currentFanSpeed: DeviceFanSpeed; +} + +export class DeviceFanSpeedChangedDomainEvent extends DomainEvent { + get previousFanSpeed(): DeviceFanSpeed | undefined { + return this.props.previousFanSpeed; + } + + get currentFanSpeed(): DeviceFanSpeed { + return this.props.currentFanSpeed; + } + + protected validate(props: DeviceFanSpeedChangedDomainEventProps): void { + if (props.previousFanSpeed) { + this.validateInstanceProp(props, 'previousFanSpeed', DeviceFanSpeed); + } + + this.validateDefinedProp(props, 'currentFanSpeed'); + this.validateInstanceProp(props, 'currentFanSpeed', DeviceFanSpeed); + } +} diff --git a/packages/domain/src/domain-events/device-map-pending.domain-event.test.ts b/packages/domain/src/domain-events/device-map-pending.domain-event.test.ts new file mode 100644 index 00000000..0f0e8bd3 --- /dev/null +++ b/packages/domain/src/domain-events/device-map-pending.domain-event.test.ts @@ -0,0 +1,47 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceMapPendingDomainEvent } from './device-map-pending.domain-event'; +import type { DeviceMapPendingDomainEventProps } from './device-map-pending.domain-event'; + +describe('DeviceMapPendingDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceMapPendingDomainEventProps(); + const event = new DeviceMapPendingDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.isPending).to.be.true; + }); + + it("should thrown an error when 'isPending' is not provided", function () { + expect( + () => + new DeviceMapPendingDomainEvent({ + ...givenSomeDeviceMapPendingDomainEventProps(), + // @ts-expect-error - missing property + isPending: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'isPending' for DeviceMapPendingDomainEvent not provided`); + }); + + it("should thrown an error when 'isPending' is not a boolean", function () { + expect( + () => + new DeviceMapPendingDomainEvent({ + ...givenSomeDeviceMapPendingDomainEventProps(), + // @ts-expect-error - invalid property + isPending: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'isPending' of DeviceMapPendingDomainEvent is not a boolean", + ); + }); +}); + +function givenSomeDeviceMapPendingDomainEventProps(): DeviceMapPendingDomainEventProps { + return { + aggregateId: ID.generate(), + isPending: true, + }; +} diff --git a/packages/domain/src/domain-events/device-map-pending.domain-event.ts b/packages/domain/src/domain-events/device-map-pending.domain-event.ts new file mode 100644 index 00000000..352fb584 --- /dev/null +++ b/packages/domain/src/domain-events/device-map-pending.domain-event.ts @@ -0,0 +1,17 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceMapPendingDomainEventProps extends DomainEventProps { + isPending: boolean; +} + +export class DeviceMapPendingDomainEvent extends DomainEvent { + get isPending(): boolean { + return this.props.isPending; + } + + protected validate(props: DeviceMapPendingDomainEventProps): void { + this.validateDefinedProp(props, 'isPending'); + this.validateTypeProp(props, 'isPending', 'boolean'); + } +} diff --git a/packages/domain/src/domain-events/device-mode-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-mode-changed.domain-event.test.ts new file mode 100644 index 00000000..6d751ffb --- /dev/null +++ b/packages/domain/src/domain-events/device-mode-changed.domain-event.test.ts @@ -0,0 +1,74 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceMode, DeviceModeValue } from '../domain-primitives/device-mode.domain-primitive'; +import { DeviceModeChangedDomainEvent } from './device-mode-changed.domain-event'; +import type { DeviceModeChangedDomainEventProps } from './device-mode-changed.domain-event'; + +describe('DeviceModeChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceModeChangedDomainEventProps(); + const event = new DeviceModeChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousMode).to.be.undefined; + expect(event.currentMode).to.be.equal(props.currentMode); + }); + + it("should be created with 'previousMode'", function () { + const props = { + ...givenSomeDeviceModeChangedDomainEventProps(), + previousMode: new DeviceMode(DeviceModeValue.None), + }; + const event = new DeviceModeChangedDomainEvent(props); + + expect(event.previousMode).to.be.equal(props.previousMode); + }); + + it("should thrown an error when 'currentMode' is not provided", function () { + expect( + () => + new DeviceModeChangedDomainEvent({ + ...givenSomeDeviceModeChangedDomainEventProps(), + // @ts-expect-error - missing property + currentMode: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'currentMode' for DeviceModeChangedDomainEvent not provided`); + }); + + it("should thrown an error when 'previousMode' is not an instance of DeviceMode", function () { + expect( + () => + new DeviceModeChangedDomainEvent({ + ...givenSomeDeviceModeChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousMode: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousMode' of DeviceModeChangedDomainEvent is not an instance of DeviceMode", + ); + }); + + it("should thrown an error when 'currentMode' is not an instance of DeviceMode", function () { + expect( + () => + new DeviceModeChangedDomainEvent({ + ...givenSomeDeviceModeChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentMode: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentMode' of DeviceModeChangedDomainEvent is not an instance of DeviceMode", + ); + }); +}); + +function givenSomeDeviceModeChangedDomainEventProps(): DeviceModeChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousMode: undefined, + currentMode: new DeviceMode(DeviceModeValue.None), + }; +} diff --git a/packages/domain/src/domain-events/device-mode-changed.domain-event.ts b/packages/domain/src/domain-events/device-mode-changed.domain-event.ts new file mode 100644 index 00000000..435f0cac --- /dev/null +++ b/packages/domain/src/domain-events/device-mode-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceMode } from '../domain-primitives/device-mode.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceModeChangedDomainEventProps extends DomainEventProps { + previousMode?: DeviceMode; + currentMode: DeviceMode; +} + +export class DeviceModeChangedDomainEvent extends DomainEvent { + get previousMode(): DeviceMode | undefined { + return this.props.previousMode; + } + + get currentMode(): DeviceMode { + return this.props.currentMode; + } + + protected validate(props: DeviceModeChangedDomainEventProps): void { + if (props.previousMode) { + this.validateInstanceProp(props, 'previousMode', DeviceMode); + } + + this.validateDefinedProp(props, 'currentMode'); + this.validateInstanceProp(props, 'currentMode', DeviceMode); + } +} diff --git a/packages/domain/src/domain-events/device-mop-attached.domain-event.test.ts b/packages/domain/src/domain-events/device-mop-attached.domain-event.test.ts new file mode 100644 index 00000000..d24fe5e3 --- /dev/null +++ b/packages/domain/src/domain-events/device-mop-attached.domain-event.test.ts @@ -0,0 +1,47 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceMopAttachedDomainEvent } from './device-mop-attached.domain-event'; +import type { DeviceMopAttachedDomainEventProps } from './device-mop-attached.domain-event'; + +describe('DeviceMopAttachedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceMopAttachedDomainEventProps(); + const event = new DeviceMopAttachedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.isAttached).to.be.true; + }); + + it("should thrown an error when 'isAttached' is not provided", function () { + expect( + () => + new DeviceMopAttachedDomainEvent({ + ...givenSomeDeviceMopAttachedDomainEventProps(), + // @ts-expect-error - missing property + isAttached: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'isAttached' for DeviceMopAttachedDomainEvent not provided`); + }); + + it("should thrown an error when 'isAttached' is not a boolean", function () { + expect( + () => + new DeviceMopAttachedDomainEvent({ + ...givenSomeDeviceMopAttachedDomainEventProps(), + // @ts-expect-error - invalid property + isAttached: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'isAttached' of DeviceMopAttachedDomainEvent is not a boolean", + ); + }); +}); + +function givenSomeDeviceMopAttachedDomainEventProps(): DeviceMopAttachedDomainEventProps { + return { + aggregateId: ID.generate(), + isAttached: true, + }; +} diff --git a/packages/domain/src/domain-events/device-mop-attached.domain-event.ts b/packages/domain/src/domain-events/device-mop-attached.domain-event.ts new file mode 100644 index 00000000..efc7167a --- /dev/null +++ b/packages/domain/src/domain-events/device-mop-attached.domain-event.ts @@ -0,0 +1,17 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceMopAttachedDomainEventProps extends DomainEventProps { + isAttached: boolean; +} + +export class DeviceMopAttachedDomainEvent extends DomainEvent { + get isAttached(): boolean { + return this.props.isAttached; + } + + protected validate(props: DeviceMopAttachedDomainEventProps): void { + this.validateDefinedProp(props, 'isAttached'); + this.validateTypeProp(props, 'isAttached', 'boolean'); + } +} diff --git a/packages/domain/src/domain-events/device-state-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-state-changed.domain-event.test.ts new file mode 100644 index 00000000..54734842 --- /dev/null +++ b/packages/domain/src/domain-events/device-state-changed.domain-event.test.ts @@ -0,0 +1,74 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceState, DeviceStateValue } from '../domain-primitives/device-state.domain-primitive'; +import { DeviceStateChangedDomainEvent } from './device-state-changed.domain-event'; +import type { DeviceStateChangedDomainEventProps } from './device-state-changed.domain-event'; + +describe('DeviceStateChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceStateChangedDomainEventProps(); + const event = new DeviceStateChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousState).to.be.undefined; + expect(event.currentState).to.be.equal(props.currentState); + }); + + it("should be created with 'previousState'", function () { + const props = { + ...givenSomeDeviceStateChangedDomainEventProps(), + previousState: new DeviceState(DeviceStateValue.Idle), + }; + const event = new DeviceStateChangedDomainEvent(props); + + expect(event.previousState).to.be.equal(props.previousState); + }); + + it("should thrown an error when 'currentState' is not provided", function () { + expect( + () => + new DeviceStateChangedDomainEvent({ + ...givenSomeDeviceStateChangedDomainEventProps(), + // @ts-expect-error - missing property + currentState: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'currentState' for DeviceStateChangedDomainEvent not provided`); + }); + + it("should thrown an error when 'previousState' is not an instance of DeviceState", function () { + expect( + () => + new DeviceStateChangedDomainEvent({ + ...givenSomeDeviceStateChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousState: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousState' of DeviceStateChangedDomainEvent is not an instance of DeviceState", + ); + }); + + it("should thrown an error when 'currentState' is not an instance of DeviceState", function () { + expect( + () => + new DeviceStateChangedDomainEvent({ + ...givenSomeDeviceStateChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentState: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentState' of DeviceStateChangedDomainEvent is not an instance of DeviceState", + ); + }); +}); + +function givenSomeDeviceStateChangedDomainEventProps(): DeviceStateChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousState: undefined, + currentState: new DeviceState(DeviceStateValue.Idle), + }; +} diff --git a/packages/domain/src/domain-events/device-state-changed.domain-event.ts b/packages/domain/src/domain-events/device-state-changed.domain-event.ts new file mode 100644 index 00000000..522878be --- /dev/null +++ b/packages/domain/src/domain-events/device-state-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceState } from '../domain-primitives/device-state.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceStateChangedDomainEventProps extends DomainEventProps { + previousState?: DeviceState; + currentState: DeviceState; +} + +export class DeviceStateChangedDomainEvent extends DomainEvent { + get previousState(): DeviceState | undefined { + return this.props.previousState; + } + + get currentState(): DeviceState { + return this.props.currentState; + } + + protected validate(props: DeviceStateChangedDomainEventProps): void { + if (props.previousState) { + this.validateInstanceProp(props, 'previousState', DeviceState); + } + + this.validateDefinedProp(props, 'currentState'); + this.validateInstanceProp(props, 'currentState', DeviceState); + } +} diff --git a/packages/domain/src/domain-events/device-water-level-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-water-level-changed.domain-event.test.ts new file mode 100644 index 00000000..b2e95597 --- /dev/null +++ b/packages/domain/src/domain-events/device-water-level-changed.domain-event.test.ts @@ -0,0 +1,77 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceWaterLevel, DeviceWaterLevelValue } from '../domain-primitives/device-water-level.domain-primitive'; +import { DeviceWaterLevelChangedDomainEvent } from './device-water-level-changed.domain-event'; +import type { DeviceWaterLevelChangedDomainEventProps } from './device-water-level-changed.domain-event'; + +describe('DeviceWaterLevelChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceWaterLevelChangedDomainEventProps(); + const event = new DeviceWaterLevelChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousWaterLevel).to.be.undefined; + expect(event.currentWaterLevel).to.be.equal(props.currentWaterLevel); + }); + + it("should be created with 'previousWaterLevel'", function () { + const props = { + ...givenSomeDeviceWaterLevelChangedDomainEventProps(), + previousWaterLevel: new DeviceWaterLevel(DeviceWaterLevelValue.Low), + }; + const event = new DeviceWaterLevelChangedDomainEvent(props); + + expect(event.previousWaterLevel).to.be.equal(props.previousWaterLevel); + }); + + it("should thrown an error when 'currentWaterLevel' is not provided", function () { + expect( + () => + new DeviceWaterLevelChangedDomainEvent({ + ...givenSomeDeviceWaterLevelChangedDomainEventProps(), + // @ts-expect-error - missing property + currentWaterLevel: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentWaterLevel' for DeviceWaterLevelChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousWaterLevel' is not an instance of DeviceWaterLevel", function () { + expect( + () => + new DeviceWaterLevelChangedDomainEvent({ + ...givenSomeDeviceWaterLevelChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousWaterLevel: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousWaterLevel' of DeviceWaterLevelChangedDomainEvent is not an instance of DeviceWaterLevel", + ); + }); + + it("should thrown an error when 'currentWaterLevel' is not an instance of DeviceWaterLevel", function () { + expect( + () => + new DeviceWaterLevelChangedDomainEvent({ + ...givenSomeDeviceWaterLevelChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentWaterLevel: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentWaterLevel' of DeviceWaterLevelChangedDomainEvent is not an instance of DeviceWaterLevel", + ); + }); +}); + +function givenSomeDeviceWaterLevelChangedDomainEventProps(): DeviceWaterLevelChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousWaterLevel: undefined, + currentWaterLevel: new DeviceWaterLevel(DeviceWaterLevelValue.Low), + }; +} diff --git a/packages/domain/src/domain-events/device-water-level-changed.domain-event.ts b/packages/domain/src/domain-events/device-water-level-changed.domain-event.ts new file mode 100644 index 00000000..006d8e97 --- /dev/null +++ b/packages/domain/src/domain-events/device-water-level-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceWaterLevel } from '../domain-primitives/device-water-level.domain-primitive'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceWaterLevelChangedDomainEventProps extends DomainEventProps { + previousWaterLevel?: DeviceWaterLevel; + currentWaterLevel: DeviceWaterLevel; +} + +export class DeviceWaterLevelChangedDomainEvent extends DomainEvent { + get previousWaterLevel(): DeviceWaterLevel | undefined { + return this.props.previousWaterLevel; + } + + get currentWaterLevel(): DeviceWaterLevel { + return this.props.currentWaterLevel; + } + + protected validate(props: DeviceWaterLevelChangedDomainEventProps): void { + if (props.previousWaterLevel) { + this.validateInstanceProp(props, 'previousWaterLevel', DeviceWaterLevel); + } + + this.validateDefinedProp(props, 'currentWaterLevel'); + this.validateInstanceProp(props, 'currentWaterLevel', DeviceWaterLevel); + } +} diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index 76300bf3..9254a708 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -3,10 +3,17 @@ import type { DeviceBatteryChangedDomainEvent } from './device-battery-changed.d import type { DeviceCleanWorkChangedDomainEvent } from './device-clean-work-changed.domain-event'; import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; import type { DeviceCreatedDomainEvent } from './device-created.domain-event'; +import type { DeviceErrorChangedDomainEvent } from './device-error-changed.domain-event'; +import type { DeviceFanSpeedChangedDomainEvent } from './device-fan-speed-changed.domain-event'; import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; +import type { DeviceMapPendingDomainEvent } from './device-map-pending.domain-event'; +import type { DeviceModeChangedDomainEvent } from './device-mode-changed.domain-event'; +import type { DeviceMopAttachedDomainEvent } from './device-mop-attached.domain-event'; import type { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; import type { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; +import type { DeviceStateChangedDomainEvent } from './device-state-changed.domain-event'; import type { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; +import type { DeviceWaterLevelChangedDomainEvent } from './device-water-level-changed.domain-event'; export type DomainEvents = { ConnectionDeviceChangedDomainEvent: ConnectionDeviceChangedDomainEvent; @@ -14,10 +21,17 @@ export type DomainEvents = { DeviceCleanWorkChangedDomainEvent: DeviceCleanWorkChangedDomainEvent; DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; DeviceCreatedDomainEvent: DeviceCreatedDomainEvent; + DeviceErrorChangedDomainEvent: DeviceErrorChangedDomainEvent; + DeviceFanSpeedChangedDomainEvent: DeviceFanSpeedChangedDomainEvent; DeviceLockedDomainEvent: DeviceLockedDomainEvent; + DeviceMapPendingDomainEvent: DeviceMapPendingDomainEvent; + DeviceModeChangedDomainEvent: DeviceModeChangedDomainEvent; + DeviceMopAttachedDomainEvent: DeviceMopAttachedDomainEvent; DeviceNetworkChangedDomainEvent: DeviceNetworkChangedDomainEvent; DeviceSettingsChangedDomainEvent: DeviceSettingsChangedDomainEvent; + DeviceStateChangedDomainEvent: DeviceStateChangedDomainEvent; DeviceVersionChangedDomainEvent: DeviceVersionChangedDomainEvent; + DeviceWaterLevelChangedDomainEvent: DeviceWaterLevelChangedDomainEvent; }; export type DomainEventNames = keyof DomainEvents; From f1c5614d5d7f3f6dddc254841cf828813e70a6c3 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 28 Mar 2023 08:41:13 +0200 Subject: [PATCH 27/38] feat(adapter-tcp): raise coverage --- ...device-quiet-hours.command-handler.test.ts | 71 +++ .../set-device-quiet-hours.command-handler.ts | 25 + ...locked-event-handler.event-handler.test.ts | 2 + ...e-is-locked-event-handler.event-handler.ts | 1 + ...ce-clean-task-report.event-handler.test.ts | 4 +- ...rger-position-update.event-handler.test.ts | 78 +++ ...p-charger-position-update.event-handler.ts | 30 +- .../device-map-update.event-handler.test.ts | 469 ++++++++++++++++++ .../device-map-update.event-handler.ts | 12 +- ...p-work-status-update.event-handler.test.ts | 168 +++++++ ...ce-map-work-status-update.event-handler.ts | 6 +- ...ce-order-list-update.event-handler.test.ts | 76 +++ .../device-order-list-update.event-handler.ts | 27 + ...vice-settings-update.event-handler.test.ts | 72 ++- .../device-settings-update.event-handler.ts | 4 +- packages/adapter-tcp/src/tcp.server.ts | 15 +- .../device.aggregate-root.test.ts | 48 +- .../aggregate-roots/device.aggregate-root.ts | 33 +- packages/domain/src/commands/commands.ts | 4 + .../set-device-quiet-hours.command.test.ts | 55 ++ .../set-device-quiet-hours.command.ts | 24 + .../commands/set-device-voice.command.test.ts | 55 ++ .../src/commands/set-device-voice.command.ts | 24 + .../device-map-changed.domain-event.test.ts | 75 +++ .../device-map-changed.domain-event.ts | 27 + ...device-orders-changed.domain-event.test.ts | 106 ++++ .../device-orders-changed.domain-event.ts | 27 + .../domain/src/domain-events/domain-events.ts | 4 + .../device-state.domain-primitive.ts | 16 +- packages/domain/src/index.ts | 16 +- packages/schemas-tcp/src/index.proto | 27 +- .../base-classes/aggregate-root.base.test.ts | 24 +- .../src/base-classes/aggregate-root.base.ts | 8 +- packages/toolkit/src/index.ts | 1 + .../utils/symmetric-difference.util.test.ts | 13 + .../src/utils/symmetric-difference.util.ts | 10 + .../payload.value-object.test.ts | 13 +- .../src/value-objects/payload.value-object.ts | 4 + 38 files changed, 1603 insertions(+), 71 deletions(-) create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts create mode 100644 packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts create mode 100644 packages/domain/src/commands/set-device-quiet-hours.command.test.ts create mode 100644 packages/domain/src/commands/set-device-quiet-hours.command.ts create mode 100644 packages/domain/src/commands/set-device-voice.command.test.ts create mode 100644 packages/domain/src/commands/set-device-voice.command.ts create mode 100644 packages/domain/src/domain-events/device-map-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-map-changed.domain-event.ts create mode 100644 packages/domain/src/domain-events/device-orders-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-orders-changed.domain-event.ts create mode 100644 packages/toolkit/src/utils/symmetric-difference.util.test.ts create mode 100644 packages/toolkit/src/utils/symmetric-difference.util.ts diff --git a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts new file mode 100644 index 00000000..a26c2090 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts @@ -0,0 +1,71 @@ +import { DeviceTime, QuietHoursSetting, SetDeviceQuietHoursCommand } from '@agnoc/domain'; +import { givenSomeQuietHoursSettingProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetDeviceQuietHoursCommandHandler } from './set-device-quiet-hours.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketMessage } from '../packet.message'; + +describe('SetDeviceQuietHoursCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetDeviceQuietHoursCommandHandler; + let packetConnection: PacketConnection; + let packetMessage: PacketMessage; + + beforeEach(function () { + packetConnectionFinderService = imock(); + commandHandler = new SetDeviceQuietHoursCommandHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetDeviceQuietHoursCommand'); + }); + + describe('#handle()', function () { + it('should send locate device command', async function () { + const command = new SetDeviceQuietHoursCommand({ + deviceId: new ID(1), + quietHours: new QuietHoursSetting({ + isEnabled: true, + beginTime: DeviceTime.fromMinutes(0), + endTime: DeviceTime.fromMinutes(180), + }), + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await commandHandler.handle(command); + + verify( + packetConnection.sendAndWait( + 'USER_SET_DEVICE_QUIETHOURS_REQ', + deepEqual({ + isOpen: true, + beginTime: 0, + endTime: 180, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('USER_SET_DEVICE_QUIETHOURS_RSP')).once(); + }); + + it('should do nothing when no connection is found', async function () { + const command = new SetDeviceQuietHoursCommand({ + deviceId: new ID(1), + quietHours: new QuietHoursSetting(givenSomeQuietHoursSettingProps()), + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts new file mode 100644 index 00000000..0db68bc6 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts @@ -0,0 +1,25 @@ +import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketMessage } from '../packet.message'; +import type { CommandHandler, SetDeviceQuietHoursCommand } from '@agnoc/domain'; + +export class SetDeviceQuietHoursCommandHandler implements CommandHandler { + readonly forName = 'SetDeviceQuietHoursCommand'; + + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + + async handle(event: SetDeviceQuietHoursCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + const response: PacketMessage = await connection.sendAndWait('USER_SET_DEVICE_QUIETHOURS_REQ', { + isOpen: event.quietHours.isEnabled, + beginTime: event.quietHours.beginTime.toMinutes(), + endTime: event.quietHours.endTime.toMinutes(), + }); + + response.assertPayloadName('USER_SET_DEVICE_QUIETHOURS_RSP'); + } +} diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts index 8ecb3c0c..ea86dcca 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts @@ -33,6 +33,7 @@ describe('QueryDeviceInfoWhenDeviceIsLockedEventHandler', function () { await eventHandler.handle(event); + verify(packetConnection.send('DEVICE_ORDERLIST_GETTING_REQ', deepEqual({}))).once(); verify(packetConnection.send('DEVICE_STATUS_GETTING_REQ', deepEqual({}))).once(); verify(packetConnection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', deepEqual({ unk1: 0, unk2: '' }))).once(); verify(packetConnection.send('DEVICE_GETTIME_REQ', deepEqual({}))).once(); @@ -50,6 +51,7 @@ describe('QueryDeviceInfoWhenDeviceIsLockedEventHandler', function () { await eventHandler.handle(event); + verify(packetConnection.send('DEVICE_ORDERLIST_GETTING_REQ', deepEqual({}))).once(); verify(packetConnection.send('DEVICE_STATUS_GETTING_REQ', deepEqual({}))).once(); verify(packetConnection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', deepEqual({ unk1: 0, unk2: '' }))).once(); verify(packetConnection.send('DEVICE_GETTIME_REQ', deepEqual({}))).once(); diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index 043aff18..5d4d29b9 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -14,6 +14,7 @@ export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEven return; } + await connection.send('DEVICE_ORDERLIST_GETTING_REQ', {}); await connection.send('DEVICE_STATUS_GETTING_REQ', {}); await connection.send('DEVICE_GET_ALL_GLOBAL_MAP_INFO_REQ', { unk1: 0, unk2: '' }); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts index 5ddbf52e..4e6ef67e 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts @@ -26,8 +26,8 @@ describe('DeviceCleanTaskReportEventHandler', function () { cleanId: 1, startTime: 0, endTime: 180, - unk4: 0, - unk5: 0, + cleanTime: 0, + cleanSize: 0, unk6: 0, unk7: 0, unk8: { unk1Unk1: 0, unk1Unk2: 0, unk1Unk6: 0 }, diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts new file mode 100644 index 00000000..076ad9d3 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts @@ -0,0 +1,78 @@ +import { MapPosition } from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceMapChargerPositionUpdateEventHandler } from './device-map-charger-position-update.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository, DeviceMap } from '@agnoc/domain'; + +describe('DeviceMapChargerPositionUpdateEventHandler', function () { + let deviceRepository: DeviceRepository; + let eventHandler: DeviceMapChargerPositionUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'>; + let device: Device; + let deviceMap: DeviceMap; + + beforeEach(function () { + deviceRepository = imock(); + eventHandler = new DeviceMapChargerPositionUpdateEventHandler(instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + deviceMap = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'); + }); + + describe('#handle()', function () { + it('should update the device network', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'), + data: { + poseId: 0, + poseX: 1, + poseY: 2, + posePhi: 3, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceMap.updateCharger(deepEqual(new MapPosition({ x: 1, y: 2, phi: 3 })))).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when device has no map', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'), + data: { + poseId: 0, + poseX: 1, + poseY: 2, + posePhi: 3, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(undefined); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceMap.updateCharger(anything())).never(); + verify(device.updateMap(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts index 4488a8f6..91684d04 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -1,26 +1,30 @@ import { MapPosition } from '@agnoc/domain'; -import { DomainException } from '@agnoc/toolkit'; import type { PacketEventHandler } from '../packet.event-handler'; import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'; + constructor(private readonly deviceRepository: DeviceRepository) {} + async handle(message: PacketMessage<'DEVICE_MAPID_PUSH_CHARGE_POSITION_INFO'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); - const data = message.packet.payload.data; + if (message.device.map) { + const data = message.packet.payload.data; - message.device.map?.updateCharger( - new MapPosition({ - x: data.poseX, - y: data.poseY, - phi: data.posePhi, - }), - ); + message.device.map.updateCharger( + new MapPosition({ + x: data.poseX, + y: data.poseY, + phi: data.posePhi, + }), + ); - // TODO: save entity and publish domain event + message.device.updateMap(message.device.map); + + await this.deviceRepository.saveOne(message.device); + } } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts new file mode 100644 index 00000000..e98c0b2a --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts @@ -0,0 +1,469 @@ +import { + CleanSize, + DeviceBattery, + DeviceCleanWork, + DeviceError, + DeviceErrorValue, + DeviceFanSpeed, + DeviceFanSpeedValue, + DeviceMode, + DeviceModeValue, + DeviceState, + DeviceStateValue, + DeviceTime, + MapCoordinate, + MapPixel, + MapPosition, + DeviceMap, + Zone, + Room, +} from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceMapUpdateEventHandler } from './device-map-update.event-handler'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../mappers/device-state.mapper'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceMapUpdateEventHandler', function () { + let deviceBatteryMapper: DeviceBatteryMapper; + let deviceModeMapper: DeviceModeMapper; + let deviceStateMapper: DeviceStateMapper; + let deviceErrorMapper: DeviceErrorMapper; + let deviceFanSpeedMapper: DeviceFanSpeedMapper; + let deviceRepository: DeviceRepository; + let eventHandler: DeviceMapUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'>; + let device: Device; + let deviceMap: DeviceMap; + + beforeEach(function () { + deviceBatteryMapper = imock(); + deviceModeMapper = imock(); + deviceStateMapper = imock(); + deviceErrorMapper = imock(); + deviceFanSpeedMapper = imock(); + deviceRepository = imock(); + eventHandler = new DeviceMapUpdateEventHandler( + instance(deviceBatteryMapper), + instance(deviceModeMapper), + instance(deviceStateMapper), + instance(deviceErrorMapper), + instance(deviceFanSpeedMapper), + instance(deviceRepository), + ); + packetMessage = imock(); + device = imock(); + deviceMap = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'); + }); + + describe('#handle()', function () { + it('should do nothing when there is no data', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { mask: 0 }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(undefined); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceBatteryMapper.toDomain(anything())).never(); + verify(deviceModeMapper.toDomain(anything())).never(); + verify(deviceStateMapper.toDomain(anything())).never(); + verify(deviceErrorMapper.toDomain(anything())).never(); + verify(deviceFanSpeedMapper.toDomain(anything())).never(); + verify(device.updateMap(anything())).never(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update the status info', async function () { + const deviceBattery = new DeviceBattery(5); + const deviceState = new DeviceState(DeviceStateValue.Docked); + const deviceMode = new DeviceMode(DeviceModeValue.None); + const deviceError = new DeviceError(DeviceErrorValue.None); + const deviceFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + statusInfo: { + mapHeadId: 1, + hasHistoryMap: true, + workingMode: 0, + batteryPercent: 100, + chargeState: false, + faultType: 3, + faultCode: 2103, + cleanPreference: 1, + repeatClean: false, + cleanTime: 0, + cleanSize: 0, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceBatteryMapper.toDomain(anything())).thenReturn(deviceBattery); + when(deviceStateMapper.toDomain(anything())).thenReturn(deviceState); + when(deviceModeMapper.toDomain(anything())).thenReturn(deviceMode); + when(deviceErrorMapper.toDomain(anything())).thenReturn(deviceError); + when(deviceFanSpeedMapper.toDomain(anything())).thenReturn(deviceFanSpeed); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(undefined); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceStateMapper.toDomain(deepEqual({ type: 3, workMode: 0, chargeStatus: false }))).once(); + verify(deviceModeMapper.toDomain(0)).once(); + verify(deviceErrorMapper.toDomain(2103)).once(); + verify(deviceBatteryMapper.toDomain(100)).once(); + verify(deviceFanSpeedMapper.toDomain(1)).once(); + verify( + device.updateCurrentCleanWork( + deepEqual(new DeviceCleanWork({ size: new CleanSize(0), time: DeviceTime.fromMinutes(0) })), + ), + ).once(); + verify(device.updateState(deviceState)).once(); + verify(device.updateMode(deviceMode)).once(); + verify(device.updateError(deviceError)).once(); + verify(device.updateBattery(deviceBattery)).once(); + verify(device.updateFanSpeed(deviceFanSpeed)).once(); + verify(device.updateMap(anything())).never(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do create a map', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + mapHeadInfo: { + mapHeadId: 1, + mapValid: 1, + mapType: 1, + sizeX: 800, + sizeY: 800, + minX: -20, + minY: -20, + maxX: 20, + maxY: 20, + resolution: 0.05, + }, + mapGrid: Buffer.alloc(0), + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(undefined); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify( + device.updateMap( + deepEqual( + new DeviceMap({ + id: new ID(1), + size: new MapPixel({ x: 800, y: 800 }), + min: new MapCoordinate({ x: -20, y: -20 }), + max: new MapCoordinate({ x: 20, y: 20 }), + resolution: 0.05, + grid: Buffer.alloc(0), + rooms: [], + restrictedZones: [], + robotPath: [], + }), + ), + ), + ).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update a map', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + mapHeadInfo: { + mapHeadId: 1, + mapValid: 1, + mapType: 1, + sizeX: 800, + sizeY: 800, + minX: -20, + minY: -20, + maxX: 20, + maxY: 20, + resolution: 0.05, + }, + mapGrid: Buffer.alloc(0), + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + when(deviceMap.clone(anything())).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify( + deviceMap.clone( + deepEqual({ + id: new ID(1), + size: new MapPixel({ x: 800, y: 800 }), + min: new MapCoordinate({ x: -20, y: -20 }), + max: new MapCoordinate({ x: 20, y: 20 }), + resolution: 0.05, + grid: Buffer.alloc(0), + rooms: [], + restrictedZones: [], + robotPath: [], + }), + ), + ).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map robot path', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + historyHeadInfo: { + mapHeadId: 1, + poseId: 1, + pointList: [ + { flag: 0, x: 0, y: 0 }, + { flag: 1, x: 1, y: 1 }, + ], + pointNumber: 0, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + when(deviceMap.robotPath).thenReturn([]); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify( + deviceMap.updateRobotPath(deepEqual([new MapCoordinate({ x: 0, y: 0 }), new MapCoordinate({ x: 1, y: 1 })])), + ).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map robot position', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + robotPoseInfo: { + mapHeadId: 1, + poseId: 0, + update: 1, + poseX: 0, + poseY: 1, + posePhi: 2, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceMap.updateRobot(deepEqual(new MapPosition({ x: 0, y: 1, phi: 2 })))).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map charger position', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + robotChargeInfo: { + mapHeadId: 1, + poseX: 0, + poseY: 1, + posePhi: 2, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceMap.updateCharger(deepEqual(new MapPosition({ x: 0, y: 1, phi: 2 })))).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map spot position', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + spotInfo: { + mapHeadId: 1, + ctrlValue: 0, + poseX: 0, + poseY: 1, + posePhi: 2, + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceMap.updateCurrentSpot(deepEqual(new MapPosition({ x: 0, y: 1, phi: 2 })))).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map restricted zones', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + wallListInfo: { + mapHeadId: 1, + cleanPlanId: 2, + cleanAreaList: [ + { + cleanAreaId: 3, + cleanPlanId: 2, + coordinateList: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + ], + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify( + deviceMap.updateRestrictedZones( + deepEqual([ + new Zone({ + id: new ID(3), + coordinates: [new MapCoordinate({ x: 0, y: 0 }), new MapCoordinate({ x: 1, y: 1 })], + }), + ]), + ), + ).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do update map rooms', async function () { + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'), + data: { + mask: 0, + currentPlanId: 2, + cleanPlanList: [ + { + planId: 2, + planName: 'plan1', + mapHeadId: 1, + currentPlanId: 1, + areaInfoList: [], + cleanRoomInfoList: [{ roomId: 3, enable: 1 }], + }, + ], + cleanRoomList: [ + { roomId: 3, roomName: 'room1', roomState: 0, roomX: 0, roomY: 0 }, + { roomId: 4, roomName: 'room2', roomState: 0, roomX: 0, roomY: 0 }, + ], + roomSegmentList: [ + { + roomId: 3, + roomPixelList: [ + { mask: 0, x: 1, y: 1 }, + { mask: 1, x: 2, y: 2 }, + ], + }, + ], + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + when(device.map).thenReturn(instance(deviceMap)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify( + deviceMap.updateRooms( + deepEqual([ + new Room({ + id: new ID(3), + name: 'room1', + isEnabled: true, + center: new MapCoordinate({ x: 0, y: 0 }), + pixels: [new MapPixel({ x: 1, y: 1 }), new MapPixel({ x: 2, y: 2 })], + }), + ]), + ), + ).once(); + verify(device.updateMap(instance(deviceMap))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index 5f18da28..abe9c58b 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -9,7 +9,7 @@ import { Room, Zone, } from '@agnoc/domain'; -import { DomainException, ID, isPresent } from '@agnoc/toolkit'; +import { ID, isPresent } from '@agnoc/toolkit'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; @@ -32,9 +32,7 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { ) {} async handle(message: PacketMessage<'DEVICE_MAPID_GET_GLOBAL_INFO_RSP'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const { statusInfo, @@ -99,8 +97,6 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { }; map = map ? map.clone(props) : new DeviceMap(props); - - message.device.updateMap(map); } if (map) { @@ -194,6 +190,10 @@ export class DeviceMapUpdateEventHandler implements PacketEventHandler { } } + if (map) { + message.device.updateMap(map); + } + await this.deviceRepository.saveOne(message.device); } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts new file mode 100644 index 00000000..4ef6c92c --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts @@ -0,0 +1,168 @@ +import { + CleanSize, + DeviceBattery, + DeviceCleanWork, + DeviceError, + DeviceErrorValue, + DeviceFanSpeed, + DeviceFanSpeedValue, + DeviceMode, + DeviceModeValue, + DeviceState, + DeviceStateValue, + DeviceTime, + DeviceWaterLevel, + DeviceWaterLevelValue, +} from '@agnoc/domain'; +import { OPCode, Packet, Payload } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceMapWorkStatusUpdateEventHandler } from './device-map-work-status-update.event-handler'; +import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; +import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; +import type { DeviceStateMapper } from '../mappers/device-state.mapper'; +import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceMapWorkStatusUpdateEventHandler', function () { + let deviceStateMapper: DeviceStateMapper; + let deviceModeMapper: DeviceModeMapper; + let deviceErrorMapper: DeviceErrorMapper; + let deviceBatteryMapper: DeviceBatteryMapper; + let deviceFanSpeedMapper: DeviceFanSpeedMapper; + let deviceWaterLevelMapper: DeviceWaterLevelMapper; + let deviceRepository: DeviceRepository; + let eventHandler: DeviceMapWorkStatusUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'>; + let device: Device; + + beforeEach(function () { + deviceStateMapper = imock(); + deviceModeMapper = imock(); + deviceErrorMapper = imock(); + deviceBatteryMapper = imock(); + deviceFanSpeedMapper = imock(); + deviceWaterLevelMapper = imock(); + deviceRepository = imock(); + eventHandler = new DeviceMapWorkStatusUpdateEventHandler( + instance(deviceStateMapper), + instance(deviceModeMapper), + instance(deviceErrorMapper), + instance(deviceBatteryMapper), + instance(deviceFanSpeedMapper), + instance(deviceWaterLevelMapper), + instance(deviceRepository), + ); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_MAPID_WORK_STATUS_PUSH_REQ'); + }); + + describe('#handle()', function () { + it('should update the device map work status', async function () { + const deviceState = new DeviceState(DeviceStateValue.Docked); + const deviceMode = new DeviceMode(DeviceModeValue.None); + const deviceError = new DeviceError(DeviceErrorValue.None); + const deviceBattery = new DeviceBattery(5); + const deviceFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_WORK_STATUS_PUSH_REQ'), + data: { + mapHeadId: 1, + areaCleanFlag: true, + workMode: 0, + battery: 200, + chargeStatus: true, + type: 3, + faultCode: 2105, + cleanPreference: 3, + repeatClean: false, + cleanTime: 0, + cleanSize: 0, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceStateMapper.toDomain(anything())).thenReturn(deviceState); + when(deviceModeMapper.toDomain(anything())).thenReturn(deviceMode); + when(deviceErrorMapper.toDomain(anything())).thenReturn(deviceError); + when(deviceBatteryMapper.toDomain(anything())).thenReturn(deviceBattery); + when(deviceFanSpeedMapper.toDomain(anything())).thenReturn(deviceFanSpeed); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceStateMapper.toDomain(deepEqual({ type: 3, workMode: 0, chargeStatus: true }))).once(); + verify(deviceModeMapper.toDomain(0)).once(); + verify(deviceErrorMapper.toDomain(2105)).once(); + verify(deviceBatteryMapper.toDomain(200)).once(); + verify(deviceFanSpeedMapper.toDomain(3)).once(); + verify( + device.updateCurrentCleanWork( + deepEqual(new DeviceCleanWork({ size: new CleanSize(0), time: DeviceTime.fromMinutes(0) })), + ), + ).once(); + verify(device.updateState(deviceState)).once(); + verify(device.updateMode(deviceMode)).once(); + verify(device.updateError(deviceError)).once(); + verify(device.updateBattery(deviceBattery)).once(); + verify(device.updateFanSpeed(deviceFanSpeed)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should update optional values of device map work status', async function () { + const deviceState = new DeviceState(DeviceStateValue.Docked); + const deviceMode = new DeviceMode(DeviceModeValue.None); + const deviceError = new DeviceError(DeviceErrorValue.None); + const deviceBattery = new DeviceBattery(5); + const deviceFanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const deviceWaterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Low); + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_WORK_STATUS_PUSH_REQ'), + data: { + mapHeadId: 1, + areaCleanFlag: true, + workMode: 0, + battery: 200, + chargeStatus: true, + type: 3, + faultCode: 2105, + cleanPreference: 3, + repeatClean: false, + cleanTime: 0, + cleanSize: 0, + waterLevel: 12, + dustBoxType: 3, + mopType: false, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceStateMapper.toDomain(anything())).thenReturn(deviceState); + when(deviceModeMapper.toDomain(anything())).thenReturn(deviceMode); + when(deviceErrorMapper.toDomain(anything())).thenReturn(deviceError); + when(deviceBatteryMapper.toDomain(anything())).thenReturn(deviceBattery); + when(deviceFanSpeedMapper.toDomain(anything())).thenReturn(deviceFanSpeed); + when(deviceWaterLevelMapper.toDomain(anything())).thenReturn(deviceWaterLevel); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceWaterLevelMapper.toDomain(12)).once(); + verify(device.updateHasMopAttached(false)).once(); + verify(device.updateWaterLevel(deviceWaterLevel)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index dc0307a1..994c9e51 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -1,5 +1,5 @@ import { CleanSize, DeviceCleanWork, DeviceTime } from '@agnoc/domain'; -import { DomainException, isPresent } from '@agnoc/toolkit'; +import { isPresent } from '@agnoc/toolkit'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; @@ -24,9 +24,7 @@ export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler ) {} async handle(message: PacketMessage<'DEVICE_MAPID_WORK_STATUS_PUSH_REQ'>): Promise { - if (!message.device) { - throw new DomainException('Device not found'); - } + message.assertDevice(); const { battery, diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts new file mode 100644 index 00000000..55678e2f --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts @@ -0,0 +1,76 @@ +import { DeviceOrder } from '@agnoc/domain'; +import { givenSomeDeviceOrderProps } from '@agnoc/domain/test-support'; +import { Payload, OPCode, Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceOrderListUpdateEventHandler } from './device-order-list-update.event-handler'; +import type { DeviceOrderMapper } from '../mappers/device-order.mapper'; +import type { PacketMessage } from '../packet.message'; +import type { Device, DeviceRepository } from '@agnoc/domain'; + +describe('DeviceOrderListUpdateEventHandler', function () { + let deviceOrderMapper: DeviceOrderMapper; + let deviceRepository: DeviceRepository; + let eventHandler: DeviceOrderListUpdateEventHandler; + let packetMessage: PacketMessage<'DEVICE_ORDERLIST_GETTING_RSP'>; + let device: Device; + + beforeEach(function () { + deviceOrderMapper = imock(); + deviceRepository = imock(); + eventHandler = new DeviceOrderListUpdateEventHandler(instance(deviceOrderMapper), instance(deviceRepository)); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(eventHandler.forName).to.be.equal('DEVICE_ORDERLIST_GETTING_RSP'); + }); + + describe('#handle()', function () { + it('should not update the device orders when it is empty', async function () { + const deviceOrder = new DeviceOrder(givenSomeDeviceOrderProps()); + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_ORDERLIST_GETTING_RSP'), + data: { result: 0 }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceOrderMapper.toDomain(anything())).thenReturn(deviceOrder); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceOrderMapper.toDomain(anything())).never(); + verify(device.updateOrders(anything())).never(); + verify(deviceRepository.saveOne(instance(device))).never(); + }); + + it('should update the device orders when it has orders', async function () { + const deviceOrder = new DeviceOrder(givenSomeDeviceOrderProps()); + const order = { orderId: 1, enable: true, repeat: false, weekDay: 1, dayTime: 180 }; + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_ORDERLIST_GETTING_RSP'), + data: { + result: 0, + orderList: [order], + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(deviceOrderMapper.toDomain(anything())).thenReturn(deviceOrder); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + verify(packetMessage.assertDevice()).once(); + verify(deviceOrderMapper.toDomain(order)).once(); + verify(device.updateOrders(deepEqual([deviceOrder]))).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts new file mode 100644 index 00000000..2ca7cc00 --- /dev/null +++ b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts @@ -0,0 +1,27 @@ +import type { DeviceOrderMapper } from '../mappers/device-order.mapper'; +import type { PacketEventHandler } from '../packet.event-handler'; +import type { PacketMessage } from '../packet.message'; +import type { DeviceRepository } from '@agnoc/domain'; + +export class DeviceOrderListUpdateEventHandler implements PacketEventHandler { + readonly forName = 'DEVICE_ORDERLIST_GETTING_RSP'; + + constructor( + private readonly deviceOrderMapper: DeviceOrderMapper, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(message: PacketMessage<'DEVICE_ORDERLIST_GETTING_RSP'>): Promise { + message.assertDevice(); + + const data = message.packet.payload.data; + + if (data.orderList) { + const deviceOrders = data.orderList?.map((order) => this.deviceOrderMapper.toDomain(order)); + + message.device.updateOrders(deviceOrders); + + await this.deviceRepository.saveOne(message.device); + } + } +} diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts index 268abaa7..8043c1ad 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts @@ -30,6 +30,66 @@ describe('DeviceSettingsUpdateEventHandler', function () { describe('#handle()', function () { it('should update the device settings', async function () { + const voiceSetting = new VoiceSetting(givenSomeVoiceSettingProps()); + const payload = new Payload({ + opcode: OPCode.fromName('PUSH_DEVICE_AGENT_SETTING_REQ'), + data: { + deviceId: 1, + voice: { + voiceMode: true, + volume: 2, + }, + quietHours: { + isOpen: true, + beginTime: 0, + endTime: 180, + }, + cleanPreference: {}, + ota: { + forceupgrade: false, + newVersion: false, + otaPackageVersion: '1.0.0', + packageSize: '0', + remoteUrl: '', + systemVersion: '1.0.0', + }, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + + when(voiceSettingMapper.toDomain(anything())).thenReturn(voiceSetting); + when(packetMessage.packet).thenReturn(packet); + when(packetMessage.device).thenReturn(instance(device)); + + await eventHandler.handle(instance(packetMessage)); + + const [deviceSettings] = capture(device.updateSettings).first(); + + expect(deviceSettings).to.be.instanceOf(DeviceSettings); + expect(deviceSettings.voice).to.equal(voiceSetting); + expect( + deviceSettings.quietHours.equals( + new QuietHoursSetting({ + isEnabled: true, + beginTime: new DeviceTime({ hours: 0, minutes: 0 }), + endTime: new DeviceTime({ hours: 3, minutes: 0 }), + }), + ), + ).to.be.true; + expect(deviceSettings.ecoMode.equals(new DeviceSetting({ isEnabled: false }))).to.be.true; + expect(deviceSettings.repeatClean.equals(new DeviceSetting({ isEnabled: false }))).to.be.true; + expect(deviceSettings.brokenClean.equals(new DeviceSetting({ isEnabled: false }))).to.be.true; + expect(deviceSettings.carpetMode.equals(new DeviceSetting({ isEnabled: false }))).to.be.true; + expect(deviceSettings.historyMap.equals(new DeviceSetting({ isEnabled: false }))).to.be.true; + + verify(packetMessage.assertDevice()).once(); + verify(voiceSettingMapper.toDomain(deepEqual({ isEnabled: true, volume: 2 }))).once(); + verify(device.updateSettings(deviceSettings)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + verify(packetMessage.respond('PUSH_DEVICE_AGENT_SETTING_RSP', deepEqual({ result: 0 }))).once(); + }); + + it('should update the optional values of device settings', async function () { const voiceSetting = new VoiceSetting(givenSomeVoiceSettingProps()); const payload = new Payload({ opcode: OPCode.fromName('PUSH_DEVICE_AGENT_SETTING_REQ'), @@ -87,16 +147,6 @@ describe('DeviceSettingsUpdateEventHandler', function () { const [deviceSettings] = capture(device.updateSettings).first(); expect(deviceSettings).to.be.instanceOf(DeviceSettings); - expect(deviceSettings.voice).to.equal(voiceSetting); - expect( - deviceSettings.quietHours.equals( - new QuietHoursSetting({ - isEnabled: true, - beginTime: new DeviceTime({ hours: 0, minutes: 0 }), - endTime: new DeviceTime({ hours: 3, minutes: 0 }), - }), - ), - ).to.be.true; expect(deviceSettings.ecoMode.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; expect(deviceSettings.repeatClean.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; expect(deviceSettings.brokenClean.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; @@ -104,10 +154,8 @@ describe('DeviceSettingsUpdateEventHandler', function () { expect(deviceSettings.historyMap.equals(new DeviceSetting({ isEnabled: true }))).to.be.true; verify(packetMessage.assertDevice()).once(); - verify(voiceSettingMapper.toDomain(deepEqual({ isEnabled: true, volume: 2 }))).once(); verify(device.updateSettings(deviceSettings)).once(); verify(deviceRepository.saveOne(instance(device))).once(); - verify(packetMessage.respond('PUSH_DEVICE_AGENT_SETTING_RSP', deepEqual({ result: 0 }))).once(); }); }); }); diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index 33dbfdb5..9a11725f 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -8,7 +8,7 @@ export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_AGENT_SETTING_REQ'; constructor( - private readonly deviceVoiceMapper: VoiceSettingMapper, + private readonly voiceSettingMapper: VoiceSettingMapper, private readonly deviceRepository: DeviceRepository, ) {} @@ -17,7 +17,7 @@ export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { const data = message.packet.payload.data; const deviceSettings = new DeviceSettings({ - voice: this.deviceVoiceMapper.toDomain({ + voice: this.voiceSettingMapper.toDomain({ isEnabled: data.voice.voiceMode, volume: data.voice.volume, }), diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index b85fc399..35e59322 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -14,13 +14,16 @@ import { LockDeviceWhenDeviceIsConnectedEventHandler } from './domain-event-hand import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; import { SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler } from './domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler'; import { PacketConnectionFactory } from './factories/connection.factory'; +import { CleanModeMapper } from './mappers/clean-mode.mapper'; import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; import { DeviceErrorMapper } from './mappers/device-error.mapper'; import { DeviceFanSpeedMapper } from './mappers/device-fan-speed.mapper'; import { DeviceModeMapper } from './mappers/device-mode.mapper'; +import { DeviceOrderMapper } from './mappers/device-order.mapper'; import { DeviceStateMapper } from './mappers/device-state.mapper'; import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; import { VoiceSettingMapper } from './mappers/voice-setting.mapper'; +import { WeekDayListMapper } from './mappers/week-day-list.mapper'; import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; import { PacketConnectionFinderService } from './packet-connection-finder.service'; import { ClientHeartbeatEventHandler } from './packet-event-handlers/client-heartbeat.event-handler'; @@ -38,6 +41,7 @@ import { DeviceMapWorkStatusUpdateEventHandler } from './packet-event-handlers/d import { DeviceMemoryMapInfoEventHandler } from './packet-event-handlers/device-memory-map-info.event-handler'; import { DeviceNetworkUpdateEventHandler } from './packet-event-handlers/device-network-update.event-handler'; import { DeviceOfflineEventHandler } from './packet-event-handlers/device-offline.event-handler'; +import { DeviceOrderListUpdateEventHandler } from './packet-event-handlers/device-order-list-update.event-handler'; import { DeviceRegisterEventHandler } from './packet-event-handlers/device-register.event-handler'; import { DeviceSettingsUpdateEventHandler } from './packet-event-handlers/device-settings-update.event-handler'; import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-time-update.event-handler'; @@ -79,6 +83,14 @@ export class TCPServer implements Server { const deviceModeMapper = new DeviceModeMapper(); const deviceErrorMapper = new DeviceErrorMapper(); const deviceBatteryMapper = new DeviceBatteryMapper(); + const cleanModeMapper = new CleanModeMapper(); + const weekDayListMapper = new WeekDayListMapper(); + const deviceOrderMapper = new DeviceOrderMapper( + deviceFanSpeedMapper, + deviceWaterLevelMapper, + cleanModeMapper, + weekDayListMapper, + ); // Packet event bus const packetEventBus = new PacketEventBus(); @@ -117,7 +129,7 @@ export class TCPServer implements Server { new DeviceGetAllGlobalMapEventHandler(), new DeviceLocatedEventHandler(), new DeviceLockedEventHandler(this.deviceRepository), - new DeviceMapChargerPositionUpdateEventHandler(), + new DeviceMapChargerPositionUpdateEventHandler(this.deviceRepository), new DeviceMapWorkStatusUpdateEventHandler( deviceStateMapper, deviceModeMapper, @@ -129,6 +141,7 @@ export class TCPServer implements Server { ), new DeviceMemoryMapInfoEventHandler(), new DeviceOfflineEventHandler(), + new DeviceOrderListUpdateEventHandler(deviceOrderMapper, this.deviceRepository), new DeviceRegisterEventHandler(this.deviceRepository), new DeviceSettingsUpdateEventHandler(deviceVoiceMapper, this.deviceRepository), new DeviceTimeUpdateEventHandler(), diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index ce4032e8..43e1f10d 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -1,4 +1,4 @@ -import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException } from '@agnoc/toolkit'; +import { AggregateRoot, ArgumentInvalidException, ArgumentNotProvidedException, ID } from '@agnoc/toolkit'; import { expect } from 'chai'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; @@ -7,10 +7,12 @@ import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceMapChangedDomainEvent } from '../domain-events/device-map-changed.domain-event'; import { DeviceMapPendingDomainEvent } from '../domain-events/device-map-pending.domain-event'; import { DeviceModeChangedDomainEvent } from '../domain-events/device-mode-changed.domain-event'; import { DeviceMopAttachedDomainEvent } from '../domain-events/device-mop-attached.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceOrdersChangedDomainEvent } from '../domain-events/device-orders-changed.domain-event'; import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; import { DeviceStateChangedDomainEvent } from '../domain-events/device-state-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; @@ -428,13 +430,31 @@ describe('Device', function () { }); describe('#updateOrders()', function () { - it('should update the device orders', function () { - const device = new Device(givenSomeDeviceProps()); - const orders = [new DeviceOrder(givenSomeDeviceOrderProps())]; + it('should update the device order', function () { + const currentOrders = [new DeviceOrder({ ...givenSomeDeviceOrderProps(), id: new ID(2) })]; + const device = new Device({ ...givenSomeDeviceProps(), orders: undefined }); + + device.updateOrders(currentOrders); - device.updateOrders(orders); + expect(device.orders).to.be.equal(currentOrders); - expect(device.orders).to.be.equal(orders); + const event = device.domainEvents[1] as DeviceOrdersChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceOrdersChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousOrders).to.be.equal(undefined); + expect(event.currentOrders).to.be.equal(currentOrders); + }); + + it('should not update the device order when value is equal', function () { + const previousOrders = [new DeviceOrder({ ...givenSomeDeviceOrderProps(), id: new ID(1) })]; + const currentOrders = [new DeviceOrder({ ...givenSomeDeviceOrderProps(), id: new ID(1) })]; + const device = new Device({ ...givenSomeDeviceProps(), orders: previousOrders }); + + device.updateOrders(currentOrders); + + expect(device.orders).to.be.equal(previousOrders); + expect(device.domainEvents[1]).to.not.exist; }); }); @@ -451,12 +471,20 @@ describe('Device', function () { describe('#updateMap()', function () { it('should update the device map', function () { - const device = new Device(givenSomeDeviceProps()); - const map = new DeviceMap(givenSomeDeviceMapProps()); + const previousMap = new DeviceMap({ ...givenSomeDeviceMapProps(), id: new ID(1) }); + const currentMap = new DeviceMap({ ...givenSomeDeviceMapProps(), id: new ID(2) }); + const device = new Device({ ...givenSomeDeviceProps(), map: previousMap }); - device.updateMap(map); + device.updateMap(currentMap); - expect(device.map).to.be.equal(map); + expect(device.map).to.be.equal(currentMap); + + const event = device.domainEvents[1] as DeviceMapChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceMapChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousMap).to.be.equal(previousMap); + expect(event.currentMap).to.be.equal(currentMap); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index 87b1aa51..66f3cadb 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -1,4 +1,4 @@ -import { AggregateRoot, ID } from '@agnoc/toolkit'; +import { AggregateRoot, ID, symmetricDifference } from '@agnoc/toolkit'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; @@ -6,10 +6,12 @@ import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; import { DeviceLockedDomainEvent } from '../domain-events/device-locked.domain-event'; +import { DeviceMapChangedDomainEvent } from '../domain-events/device-map-changed.domain-event'; import { DeviceMapPendingDomainEvent } from '../domain-events/device-map-pending.domain-event'; import { DeviceModeChangedDomainEvent } from '../domain-events/device-mode-changed.domain-event'; import { DeviceMopAttachedDomainEvent } from '../domain-events/device-mop-attached.domain-event'; import { DeviceNetworkChangedDomainEvent } from '../domain-events/device-network-changed.domain-event'; +import { DeviceOrdersChangedDomainEvent } from '../domain-events/device-orders-changed.domain-event'; import { DeviceSettingsChangedDomainEvent } from '../domain-events/device-settings-changed.domain-event'; import { DeviceStateChangedDomainEvent } from '../domain-events/device-state-changed.domain-event'; import { DeviceVersionChangedDomainEvent } from '../domain-events/device-version-changed.domain-event'; @@ -249,8 +251,24 @@ export class Device extends AggregateRoot { } /** Updates the device orders. */ - updateOrders(orders?: DeviceOrder[]): void { + updateOrders(orders: DeviceOrder[]): void { + const previousOrderIds = this.orders?.map((order) => order.id.value) ?? []; + const currentOrderIds = orders.map((order) => order.id.value); + const diff = symmetricDifference(previousOrderIds, currentOrderIds); + + if (diff.length === 0) { + return; + } + + this.validateDefinedProp({ orders }, 'orders'); this.validateArrayProp({ orders }, 'orders', DeviceOrder); + this.addEvent( + new DeviceOrdersChangedDomainEvent({ + aggregateId: this.id, + previousOrders: this.orders, + currentOrders: orders, + }), + ); this.props.orders = orders; } @@ -261,8 +279,17 @@ export class Device extends AggregateRoot { } /** Updates the device map. */ - updateMap(map?: DeviceMap): void { + updateMap(map: DeviceMap): void { + // TODO: prevent map changed event if the map is not changed + this.validateDefinedProp({ map }, 'map'); this.validateInstanceProp({ map }, 'map', DeviceMap); + this.addEvent( + new DeviceMapChangedDomainEvent({ + aggregateId: this.id, + previousMap: this.map, + currentMap: map, + }), + ); this.props.map = map; } diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index cfa2712f..9d2989b2 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,7 +1,11 @@ import type { LocateDeviceCommand } from './locate-device.command'; +import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; +import type { SetDeviceVoiceCommand } from './set-device-voice.command'; export type Commands = { LocateDeviceCommand: LocateDeviceCommand; + SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; + SetDeviceVoiceCommand: SetDeviceVoiceCommand; }; export type CommandNames = keyof Commands; diff --git a/packages/domain/src/commands/set-device-quiet-hours.command.test.ts b/packages/domain/src/commands/set-device-quiet-hours.command.test.ts new file mode 100644 index 00000000..949b3e7e --- /dev/null +++ b/packages/domain/src/commands/set-device-quiet-hours.command.test.ts @@ -0,0 +1,55 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeQuietHoursSettingProps } from '../test-support'; +import { QuietHoursSetting } from '../value-objects/quiet-hours-setting.value-object'; +import { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; +import type { SetDeviceQuietHoursCommandInput } from './set-device-quiet-hours.command'; + +describe('SetDeviceQuietHoursCommand', function () { + it('should be created', function () { + const input = givenASetDeviceQuietHoursCommandInput(); + const command = new SetDeviceQuietHoursCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.quietHours).to.be.equal(input.quietHours); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceQuietHoursCommand({ ...givenASetDeviceQuietHoursCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetDeviceQuietHoursCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceQuietHoursCommand({ ...givenASetDeviceQuietHoursCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetDeviceQuietHoursCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'quietHours' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceQuietHoursCommand({ ...givenASetDeviceQuietHoursCommandInput(), quietHours: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'quietHours' for SetDeviceQuietHoursCommand not provided`); + }); + + it("should throw an error when 'quietHours' is not an QuietHoursSetting", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceQuietHoursCommand({ ...givenASetDeviceQuietHoursCommandInput(), quietHours: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'quietHours' of SetDeviceQuietHoursCommand is not an instance of QuietHoursSetting`, + ); + }); +}); + +function givenASetDeviceQuietHoursCommandInput(): SetDeviceQuietHoursCommandInput { + return { deviceId: ID.generate(), quietHours: new QuietHoursSetting(givenSomeQuietHoursSettingProps()) }; +} diff --git a/packages/domain/src/commands/set-device-quiet-hours.command.ts b/packages/domain/src/commands/set-device-quiet-hours.command.ts new file mode 100644 index 00000000..10d656fe --- /dev/null +++ b/packages/domain/src/commands/set-device-quiet-hours.command.ts @@ -0,0 +1,24 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { QuietHoursSetting } from '../value-objects/quiet-hours-setting.value-object'; + +export interface SetDeviceQuietHoursCommandInput { + deviceId: ID; + quietHours: QuietHoursSetting; +} + +export class SetDeviceQuietHoursCommand extends Command { + get deviceId(): ID { + return this.props.deviceId; + } + + get quietHours(): QuietHoursSetting { + return this.props.quietHours; + } + + protected validate(props: SetDeviceQuietHoursCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'quietHours'); + this.validateInstanceProp(props, 'quietHours', QuietHoursSetting); + } +} diff --git a/packages/domain/src/commands/set-device-voice.command.test.ts b/packages/domain/src/commands/set-device-voice.command.test.ts new file mode 100644 index 00000000..9b0908a2 --- /dev/null +++ b/packages/domain/src/commands/set-device-voice.command.test.ts @@ -0,0 +1,55 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeVoiceSettingProps } from '../test-support'; +import { VoiceSetting } from '../value-objects/voice-setting.value-object'; +import { SetDeviceVoiceCommand } from './set-device-voice.command'; +import type { SetDeviceVoiceCommandInput } from './set-device-voice.command'; + +describe('SetDeviceVoiceCommand', function () { + it('should be created', function () { + const input = givenASetDeviceVoiceCommandInput(); + const command = new SetDeviceVoiceCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.voice).to.be.equal(input.voice); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceVoiceCommand({ ...givenASetDeviceVoiceCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetDeviceVoiceCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceVoiceCommand({ ...givenASetDeviceVoiceCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetDeviceVoiceCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'voice' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceVoiceCommand({ ...givenASetDeviceVoiceCommandInput(), voice: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'voice' for SetDeviceVoiceCommand not provided`); + }); + + it("should throw an error when 'voice' is not an VoiceSetting", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceVoiceCommand({ ...givenASetDeviceVoiceCommandInput(), voice: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'voice' of SetDeviceVoiceCommand is not an instance of VoiceSetting`, + ); + }); +}); + +function givenASetDeviceVoiceCommandInput(): SetDeviceVoiceCommandInput { + return { deviceId: ID.generate(), voice: new VoiceSetting(givenSomeVoiceSettingProps()) }; +} diff --git a/packages/domain/src/commands/set-device-voice.command.ts b/packages/domain/src/commands/set-device-voice.command.ts new file mode 100644 index 00000000..17a4f112 --- /dev/null +++ b/packages/domain/src/commands/set-device-voice.command.ts @@ -0,0 +1,24 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { VoiceSetting } from '../value-objects/voice-setting.value-object'; + +export interface SetDeviceVoiceCommandInput { + deviceId: ID; + voice: VoiceSetting; +} + +export class SetDeviceVoiceCommand extends Command { + get deviceId(): ID { + return this.props.deviceId; + } + + get voice(): VoiceSetting { + return this.props.voice; + } + + protected validate(props: SetDeviceVoiceCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'voice'); + this.validateInstanceProp(props, 'voice', VoiceSetting); + } +} diff --git a/packages/domain/src/domain-events/device-map-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-map-changed.domain-event.test.ts new file mode 100644 index 00000000..b176ddfb --- /dev/null +++ b/packages/domain/src/domain-events/device-map-changed.domain-event.test.ts @@ -0,0 +1,75 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceMap } from '../entities/device-map.entity'; +import { givenSomeDeviceMapProps } from '../test-support'; +import { DeviceMapChangedDomainEvent } from './device-map-changed.domain-event'; +import type { DeviceMapChangedDomainEventProps } from './device-map-changed.domain-event'; + +describe('DeviceMapChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceMapChangedDomainEventProps(); + const event = new DeviceMapChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousMap).to.be.undefined; + expect(event.currentMap).to.be.equal(props.currentMap); + }); + + it("should be created with 'previousMap'", function () { + const props = { + ...givenSomeDeviceMapChangedDomainEventProps(), + previousMap: new DeviceMap(givenSomeDeviceMapProps()), + }; + const event = new DeviceMapChangedDomainEvent(props); + + expect(event.previousMap).to.be.equal(props.previousMap); + }); + + it("should thrown an error when 'currentMap' is not provided", function () { + expect( + () => + new DeviceMapChangedDomainEvent({ + ...givenSomeDeviceMapChangedDomainEventProps(), + // @ts-expect-error - missing property + currentMap: undefined, + }), + ).to.throw(ArgumentNotProvidedException, `Property 'currentMap' for DeviceMapChangedDomainEvent not provided`); + }); + + it("should thrown an error when 'previousMap' is not an instance of DeviceMap", function () { + expect( + () => + new DeviceMapChangedDomainEvent({ + ...givenSomeDeviceMapChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousMap: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousMap' of DeviceMapChangedDomainEvent is not an instance of DeviceMap", + ); + }); + + it("should thrown an error when 'currentMap' is not an instance of DeviceMap", function () { + expect( + () => + new DeviceMapChangedDomainEvent({ + ...givenSomeDeviceMapChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentMap: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentMap' of DeviceMapChangedDomainEvent is not an instance of DeviceMap", + ); + }); +}); + +function givenSomeDeviceMapChangedDomainEventProps(): DeviceMapChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousMap: undefined, + currentMap: new DeviceMap(givenSomeDeviceMapProps()), + }; +} diff --git a/packages/domain/src/domain-events/device-map-changed.domain-event.ts b/packages/domain/src/domain-events/device-map-changed.domain-event.ts new file mode 100644 index 00000000..81eec8d7 --- /dev/null +++ b/packages/domain/src/domain-events/device-map-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceMap } from '../entities/device-map.entity'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceMapChangedDomainEventProps extends DomainEventProps { + previousMap?: DeviceMap; + currentMap: DeviceMap; +} + +export class DeviceMapChangedDomainEvent extends DomainEvent { + get previousMap(): DeviceMap | undefined { + return this.props.previousMap; + } + + get currentMap(): DeviceMap { + return this.props.currentMap; + } + + protected validate(props: DeviceMapChangedDomainEventProps): void { + if (props.previousMap) { + this.validateInstanceProp(props, 'previousMap', DeviceMap); + } + + this.validateDefinedProp(props, 'currentMap'); + this.validateInstanceProp(props, 'currentMap', DeviceMap); + } +} diff --git a/packages/domain/src/domain-events/device-orders-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-orders-changed.domain-event.test.ts new file mode 100644 index 00000000..2ace2d52 --- /dev/null +++ b/packages/domain/src/domain-events/device-orders-changed.domain-event.test.ts @@ -0,0 +1,106 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceOrder } from '../entities/device-order.entity'; +import { givenSomeDeviceOrderProps } from '../test-support'; +import { DeviceOrdersChangedDomainEvent } from './device-orders-changed.domain-event'; +import type { DeviceOrdersChangedDomainEventProps } from './device-orders-changed.domain-event'; + +describe('DeviceOrdersChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceOrdersChangedDomainEventProps(); + const event = new DeviceOrdersChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousOrders).to.be.undefined; + expect(event.currentOrders).to.be.equal(props.currentOrders); + }); + + it("should be created with 'previousOrders'", function () { + const props = { + ...givenSomeDeviceOrdersChangedDomainEventProps(), + previousOrders: [new DeviceOrder(givenSomeDeviceOrderProps())], + }; + const event = new DeviceOrdersChangedDomainEvent(props); + + expect(event.previousOrders).to.be.equal(props.previousOrders); + }); + + it("should thrown an error when 'currentOrders' is not provided", function () { + expect( + () => + new DeviceOrdersChangedDomainEvent({ + ...givenSomeDeviceOrdersChangedDomainEventProps(), + // @ts-expect-error - missing property + currentOrders: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentOrders' for DeviceOrdersChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousOrders' is not an array", function () { + expect( + () => + new DeviceOrdersChangedDomainEvent({ + ...givenSomeDeviceOrdersChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousOrders: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousOrders' of DeviceOrdersChangedDomainEvent is not an array", + ); + }); + + it("should thrown an error when 'previousOrders' is not an array of DeviceOrder", function () { + expect( + () => + new DeviceOrdersChangedDomainEvent({ + ...givenSomeDeviceOrdersChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousOrders: ['foo'], + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousOrders' of DeviceOrdersChangedDomainEvent is not an array of DeviceOrder", + ); + }); + + it("should thrown an error when 'currentOrders' is not an array", function () { + expect( + () => + new DeviceOrdersChangedDomainEvent({ + ...givenSomeDeviceOrdersChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentOrders: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentOrders' of DeviceOrdersChangedDomainEvent is not an array", + ); + }); + + it("should thrown an error when 'currentOrders' is not an array of DeviceOrder", function () { + expect( + () => + new DeviceOrdersChangedDomainEvent({ + ...givenSomeDeviceOrdersChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentOrders: ['foo'], + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentOrders' of DeviceOrdersChangedDomainEvent is not an array of DeviceOrder", + ); + }); +}); + +function givenSomeDeviceOrdersChangedDomainEventProps(): DeviceOrdersChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousOrders: undefined, + currentOrders: [new DeviceOrder(givenSomeDeviceOrderProps())], + }; +} diff --git a/packages/domain/src/domain-events/device-orders-changed.domain-event.ts b/packages/domain/src/domain-events/device-orders-changed.domain-event.ts new file mode 100644 index 00000000..769cd229 --- /dev/null +++ b/packages/domain/src/domain-events/device-orders-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceOrder } from '../entities/device-order.entity'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceOrdersChangedDomainEventProps extends DomainEventProps { + previousOrders?: DeviceOrder[]; + currentOrders: DeviceOrder[]; +} + +export class DeviceOrdersChangedDomainEvent extends DomainEvent { + get previousOrders(): DeviceOrder[] | undefined { + return this.props.previousOrders; + } + + get currentOrders(): DeviceOrder[] { + return this.props.currentOrders; + } + + protected validate(props: DeviceOrdersChangedDomainEventProps): void { + if (props.previousOrders) { + this.validateArrayProp(props, 'previousOrders', DeviceOrder); + } + + this.validateDefinedProp(props, 'currentOrders'); + this.validateArrayProp(props, 'currentOrders', DeviceOrder); + } +} diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index 9254a708..97f06a32 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -6,10 +6,12 @@ import type { DeviceCreatedDomainEvent } from './device-created.domain-event'; import type { DeviceErrorChangedDomainEvent } from './device-error-changed.domain-event'; import type { DeviceFanSpeedChangedDomainEvent } from './device-fan-speed-changed.domain-event'; import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; +import type { DeviceMapChangedDomainEvent } from './device-map-changed.domain-event'; import type { DeviceMapPendingDomainEvent } from './device-map-pending.domain-event'; import type { DeviceModeChangedDomainEvent } from './device-mode-changed.domain-event'; import type { DeviceMopAttachedDomainEvent } from './device-mop-attached.domain-event'; import type { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; +import type { DeviceOrdersChangedDomainEvent } from './device-orders-changed.domain-event'; import type { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; import type { DeviceStateChangedDomainEvent } from './device-state-changed.domain-event'; import type { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; @@ -24,10 +26,12 @@ export type DomainEvents = { DeviceErrorChangedDomainEvent: DeviceErrorChangedDomainEvent; DeviceFanSpeedChangedDomainEvent: DeviceFanSpeedChangedDomainEvent; DeviceLockedDomainEvent: DeviceLockedDomainEvent; + DeviceMapChangedDomainEvent: DeviceMapChangedDomainEvent; DeviceMapPendingDomainEvent: DeviceMapPendingDomainEvent; DeviceModeChangedDomainEvent: DeviceModeChangedDomainEvent; DeviceMopAttachedDomainEvent: DeviceMopAttachedDomainEvent; DeviceNetworkChangedDomainEvent: DeviceNetworkChangedDomainEvent; + DeviceOrdersChangedDomainEvent: DeviceOrdersChangedDomainEvent; DeviceSettingsChangedDomainEvent: DeviceSettingsChangedDomainEvent; DeviceStateChangedDomainEvent: DeviceStateChangedDomainEvent; DeviceVersionChangedDomainEvent: DeviceVersionChangedDomainEvent; diff --git a/packages/domain/src/domain-primitives/device-state.domain-primitive.ts b/packages/domain/src/domain-primitives/device-state.domain-primitive.ts index 5d64309c..b025f6f2 100644 --- a/packages/domain/src/domain-primitives/device-state.domain-primitive.ts +++ b/packages/domain/src/domain-primitives/device-state.domain-primitive.ts @@ -2,14 +2,14 @@ import { DomainPrimitive } from '@agnoc/toolkit'; /** The possible values of a device state. */ export enum DeviceStateValue { - Error = 'error', - Docked = 'docked', - Idle = 'idle', - Returning = 'returning', - Cleaning = 'cleaning', - Paused = 'paused', - ManualControl = 'manual_control', - Moving = 'moving', + Error = 'Error', + Docked = 'Docked', + Idle = 'Idle', + Returning = 'Returning', + Cleaning = 'Cleaning', + Paused = 'Paused', + ManualControl = 'ManualControl', + Moving = 'Moving', } /** Describe the state of a device. */ diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 7d3405a5..265ac8af 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -2,12 +2,26 @@ export * from './aggregate-roots/connection.aggregate-root'; export * from './aggregate-roots/device.aggregate-root'; export * from './commands/commands'; export * from './commands/locate-device.command'; +export * from './commands/set-device-quiet-hours.command'; +export * from './commands/set-device-voice.command'; export * from './domain-events/connection-device-changed.domain-event'; export * from './domain-events/device-battery-changed.domain-event'; +export * from './domain-events/device-clean-work-changed.domain-event'; export * from './domain-events/device-connected.domain-event'; export * from './domain-events/device-created.domain-event'; +export * from './domain-events/device-error-changed.domain-event'; +export * from './domain-events/device-fan-speed-changed.domain-event'; export * from './domain-events/device-locked.domain-event'; +export * from './domain-events/device-map-changed.domain-event'; +export * from './domain-events/device-map-pending.domain-event'; +export * from './domain-events/device-mode-changed.domain-event'; +export * from './domain-events/device-mop-attached.domain-event'; export * from './domain-events/device-network-changed.domain-event'; +export * from './domain-events/device-orders-changed.domain-event'; +export * from './domain-events/device-settings-changed.domain-event'; +export * from './domain-events/device-state-changed.domain-event'; +export * from './domain-events/device-version-changed.domain-event'; +export * from './domain-events/device-water-level-changed.domain-event'; export * from './domain-events/domain-events'; export * from './domain-primitives/clean-mode.domain-primitive'; export * from './domain-primitives/clean-size.domain-primitive'; @@ -33,12 +47,12 @@ export * from './repositories/connection.repository'; export * from './repositories/device.repository'; export * from './value-objects/device-clean-work.value-object'; export * from './value-objects/device-consumable.value-object'; +export * from './value-objects/device-network.value-object'; export * from './value-objects/device-setting.value-object'; export * from './value-objects/device-settings.value-object'; export * from './value-objects/device-system.value-object'; export * from './value-objects/device-time.value-object'; export * from './value-objects/device-version.value-object'; -export * from './value-objects/device-network.value-object'; export * from './value-objects/map-coordinate.value-object'; export * from './value-objects/map-pixel.value-object'; export * from './value-objects/map-position.value-object'; diff --git a/packages/schemas-tcp/src/index.proto b/packages/schemas-tcp/src/index.proto index e5bff3de..a119fd35 100644 --- a/packages/schemas-tcp/src/index.proto +++ b/packages/schemas-tcp/src/index.proto @@ -341,8 +341,8 @@ message DEVICE_EVENT_REPORT_CLEANTASK { required uint32 cleanId = 1; required uint32 startTime = 2; required uint32 endTime = 3; - required uint32 unk4 = 4; - required uint32 unk5 = 5; + required uint32 cleanSize = 4; + required uint32 cleanTime = 5; required uint32 unk6 = 6; required uint32 unk7 = 7; required Unk1 unk8 = 8; @@ -357,7 +357,7 @@ message DEVICE_CLEANMAP_BINDATA_REPORT_REQ { message DEVICE_CLEANMAP_BINDATA_REPORT_RSP { required int32 result = 1; - required uint32 cleanId = 3; + optional uint32 cleanId = 3; } message DEVICE_OFFLINE_CMD { @@ -569,3 +569,24 @@ message DEVICE_MOP_FLOOR_CLEAN_REQ { message DEVICE_MOP_FLOOR_CLEAN_RSP { required int32 result = 1; } + +message USER_DELETE_DEVICE_ORDER_REQ { + required string unk1 = 1; + required uint32 deviceId = 2; + required uint32 orderId = 3; +} + +message USER_DELETE_DEVICE_ORDER_RSP { + required int32 result = 1; + required uint32 unk1 = 3; +} + +message DEVICE_MAPID_SET_NAME_PARAMS_REQ { + required uint32 mapHeadId = 1; + required string name = 3; +} + +message DEVICE_MAPID_SET_NAME_PARAMS_RSP { + required int32 result = 1; + required uint32 unk1 = 3; +} diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts index b7cf6ef6..97c45c89 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.test.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.test.ts @@ -1,4 +1,4 @@ -import { anything, capture, imock, instance, when } from '@johanblumenberg/ts-mockito'; +import { anything, capture, defer, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { ID } from '../domain-primitives/id.domain-primitive'; import { AggregateRoot } from './aggregate-root.base'; @@ -48,6 +48,28 @@ describe('AggregateRoot', function () { expect(event.aggregateId).to.be.equal(id); }); + it('should publish domain events only once', async function () { + const id = ID.generate(); + const dummyAggregateRoot = new DummyAggregateRoot({ id }); + const deferEmit = defer(); + + when(eventBus.emit(anything(), anything())).thenReturn(deferEmit); + + dummyAggregateRoot.doSomething(); + + const promises = Promise.all([ + dummyAggregateRoot.publishEvents(instance(eventBus)), + dummyAggregateRoot.publishEvents(instance(eventBus)), + ]); + + await deferEmit.resolve(undefined); + await promises; + + expect(dummyAggregateRoot.domainEvents).to.be.lengthOf(0); + + verify(eventBus.emit(anything(), anything())).once(); + }); + it('should be able to clear domain events', function () { const dummyAggregateRoot = new DummyAggregateRoot({ id: ID.generate() }); diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts index 6aebab4f..1c3ae381 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -18,16 +18,18 @@ export abstract class AggregateRoot extends } async publishEvents(eventBus: EventBus): Promise { + const domainEvents = this.domainEvents; + + this.clearEvents(); + await Promise.all( - this.domainEvents.map(async (domainEvent) => { + domainEvents.map(async (domainEvent) => { this.debug( `publishing domain event '${domainEvent.constructor.name}' with data: ${JSON.stringify(domainEvent)}`, ); return eventBus.emit(domainEvent.constructor.name, domainEvent); }), ); - - this.clearEvents(); } protected addEvent(domainEvent: DomainEvent): void { diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index e986c5e6..5bc090bc 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -43,6 +43,7 @@ export * from './utils/is-empty.util'; export * from './utils/is-object.util'; export * from './utils/is-present.util'; export * from './utils/stream.util'; +export * from './utils/symmetric-difference.util'; export * from './utils/to-dash-case.util'; export * from './utils/to-stream.util'; export * from './utils/wait-for.util'; diff --git a/packages/toolkit/src/utils/symmetric-difference.util.test.ts b/packages/toolkit/src/utils/symmetric-difference.util.test.ts new file mode 100644 index 00000000..d252cbbe --- /dev/null +++ b/packages/toolkit/src/utils/symmetric-difference.util.test.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai'; +import { symmetricDifference } from './symmetric-difference.util'; + +describe('symmetricDifference', function () { + it('should return the symmetric difference', function () { + const firstArray = [1, 2, 3, 4, 5]; + const secondArray = [4, 5, 6]; + + const ret = symmetricDifference(firstArray, secondArray); + + expect(ret).to.be.deep.equal([1, 2, 3, 6]); + }); +}); diff --git a/packages/toolkit/src/utils/symmetric-difference.util.ts b/packages/toolkit/src/utils/symmetric-difference.util.ts new file mode 100644 index 00000000..0e3c15fb --- /dev/null +++ b/packages/toolkit/src/utils/symmetric-difference.util.ts @@ -0,0 +1,10 @@ +/** Returns the symmetric difference of two arrays. */ +export function symmetricDifference(firstArray: T[], secondArray: T[]): T[] { + const firstSet = new Set(firstArray); + const secondSet = new Set(secondArray); + + return [ + ...firstArray.filter((firstItem) => !secondSet.has(firstItem)), + ...secondArray.filter((secondItem) => !firstSet.has(secondItem)), + ]; +} diff --git a/packages/transport-tcp/src/value-objects/payload.value-object.test.ts b/packages/transport-tcp/src/value-objects/payload.value-object.test.ts index bfca9899..4bce65a0 100644 --- a/packages/transport-tcp/src/value-objects/payload.value-object.test.ts +++ b/packages/transport-tcp/src/value-objects/payload.value-object.test.ts @@ -50,11 +50,20 @@ describe('Payload', function () { it('should return a string representation of the Payload', function () { const payload = new Payload<'DEVICE_MAPID_PUSH_MAP_INFO'>({ opcode: OPCode.fromName('DEVICE_MAPID_PUSH_MAP_INFO'), - data: { mask: 0, mapGrid: Buffer.from('example') }, + data: { + mask: 0, + mapGrid: Buffer.from('example'), + historyHeadInfo: { + mapHeadId: 1, + poseId: 1, + pointList: new Array(20).fill(0).map((_, i) => ({ flag: i, x: i, y: i })), + pointNumber: 0, + }, + }, }); expect(payload.toString()).to.be.equal( - '{"opcode":"DEVICE_MAPID_PUSH_MAP_INFO","data":{"mask":0,"mapGrid":"[Buffer]"}}', + '{"opcode":"DEVICE_MAPID_PUSH_MAP_INFO","data":{"mask":0,"mapGrid":"[Buffer]","historyHeadInfo":{"mapHeadId":1,"poseId":1,"pointList":[{"flag":0,"x":0,"y":0},{"flag":1,"x":1,"y":1},{"flag":2,"x":2,"y":2},{"flag":3,"x":3,"y":3},{"flag":4,"x":4,"y":4},{"flag":5,"x":5,"y":5},{"flag":6,"x":6,"y":6},{"flag":7,"x":7,"y":7},{"flag":8,"x":8,"y":8},{"flag":9,"x":9,"y":9},"[20 more items...]"],"pointNumber":0}}}', ); }); }); diff --git a/packages/transport-tcp/src/value-objects/payload.value-object.ts b/packages/transport-tcp/src/value-objects/payload.value-object.ts index 5ee1ed58..8bed152b 100644 --- a/packages/transport-tcp/src/value-objects/payload.value-object.ts +++ b/packages/transport-tcp/src/value-objects/payload.value-object.ts @@ -57,5 +57,9 @@ function filterProperties(_: string, value: unknown) { return '[Buffer]'; } + if (Array.isArray(value) && value.length > 10) { + return value.slice(0, 10).concat(`[${value.length} more items...]`) as unknown[]; + } + return value; } From 656d369b03bd4607f002293eaee0efc68f8ae190 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 28 Mar 2023 19:12:06 +0200 Subject: [PATCH 28/38] chore(eslint-config): add tsdoc-required plugin --- package.json | 2 +- packages/eslint-config/package.json | 1 + packages/eslint-config/typescript.js | 3 ++- packages/toolkit/src/utils/to-stream.util.ts | 1 + yarn.lock | 5 +++++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fe1e457e..4e73d3c8 100755 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "clean:packages": "lerna run clean", "dependency:upgrade": "ncu --upgrade --peer --deep && yarn-deduplicate --strategy fewer && yarn install", "lint": "npm-run-all --aggregate-output --continue-on-error --parallel 'lint:!(fix)'", - "lint:code": "eslint --cache --max-warnings 0 .", + "lint:code": "eslint --cache .", "lint:code:fix": "eslint --fix .", "lint:style": "prettier --check .", "lint:style:fix": "prettier --write .", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 427e0669..ff02f8fa 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -10,6 +10,7 @@ "./typescript": "./typescript.js" }, "dependencies": { + "@guardian/eslint-plugin-tsdoc-required": "^0.1.3", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "eslint": "^8.36.0", diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index 057174f4..152ddb5a 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -18,7 +18,7 @@ module.exports = { 'plugin:chai-friendly/recommended', 'plugin:prettier/recommended', ], - plugins: ['eslint-plugin-tsdoc'], + plugins: ['eslint-plugin-tsdoc', '@guardian/eslint-plugin-tsdoc-required'], settings: { 'import/parsers': { '@typescript-eslint/parser': ['.ts'], @@ -77,6 +77,7 @@ module.exports = { groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], }, ], + '@guardian/tsdoc-required/tsdoc-required': 'warn', }, overrides: [ { diff --git a/packages/toolkit/src/utils/to-stream.util.ts b/packages/toolkit/src/utils/to-stream.util.ts index 373c842c..2969c651 100644 --- a/packages/toolkit/src/utils/to-stream.util.ts +++ b/packages/toolkit/src/utils/to-stream.util.ts @@ -1,5 +1,6 @@ import { Readable } from 'stream'; +/** Convert a buffer to a readable stream. */ export function toStream(buffer: Buffer): Readable { return Readable.from(buffer, { objectMode: false }); } diff --git a/yarn.lock b/yarn.lock index 1da752eb..572b1fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,6 +401,11 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@guardian/eslint-plugin-tsdoc-required@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@guardian/eslint-plugin-tsdoc-required/-/eslint-plugin-tsdoc-required-0.1.3.tgz#1c5517753db676cd8119c0fc0c50eab3b0ff6e77" + integrity sha512-ohk4/a0Kq9oEq4sSBFS+0XU3AjANwcEbK5HgBHNyIBZgc+7SU0ws+3/7W9QrBZts1lXkOhqBrIW+D+PG5Rk39A== + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" From 8a2fe07e803657391adb60d920032f09e3e2bf67 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Wed, 29 Mar 2023 17:45:23 +0200 Subject: [PATCH 29/38] feat: add basic control commands --- .../packet-connection.aggregate-root.test.ts | 4 +- .../packet-connection.aggregate-root.ts | 4 +- .../packet.event-handler.ts | 2 +- .../locate-device.command-handler.test.ts | 7 +- .../locate-device.command-handler.ts | 4 +- .../pause-cleaning.command-handler.test.ts | 150 ++++++++ .../pause-cleaning.command-handler.ts | 83 +++++ .../return-home.command-handler.test.ts | 52 +++ .../return-home.command-handler.ts | 23 ++ ...device-quiet-hours.command-handler.test.ts | 7 +- .../set-device-quiet-hours.command-handler.ts | 4 +- .../set-device-voice.command-handler.test.ts | 110 ++++++ .../set-device-voice.command-handler.ts | 70 ++++ .../start-cleaning.command-handler.test.ts | 277 +++++++++++++++ .../start-cleaning.command-handler.ts | 141 ++++++++ .../stop-cleaning.command-handler.test.ts | 52 +++ .../stop-cleaning.command-handler.ts | 30 ++ .../ntp-server.connection-handler.test.ts | 0 .../ntp-server.connection-handler.ts | 0 .../packet-server.connection-handler.test.ts | 22 +- .../packet-server.connection-handler.ts | 12 +- ...nected-event-handler.event-handler.test.ts | 5 +- ...s-connected-event-handler.event-handler.ts | 2 +- ...locked-event-handler.event-handler.test.ts | 5 +- ...e-is-locked-event-handler.event-handler.ts | 2 +- ...ction-device-changed.event-handler.test.ts | 25 +- .../packet.event-bus.test.ts | 0 .../src/{ => event-buses}/packet.event-bus.ts | 2 +- .../src/factories/connection.factory.test.ts | 2 +- .../src/factories/connection.factory.ts | 2 +- packages/adapter-tcp/src/index.ts | 8 - .../src/{ => objects}/packet.message.test.ts | 19 +- .../src/{ => objects}/packet.message.ts | 8 +- .../client-heartbeat.event-handler.test.ts | 2 +- .../client-heartbeat.event-handler.ts | 4 +- .../client-login.event-handler.test.ts | 2 +- .../client-login.event-handler.ts | 4 +- ...evice-battery-update.event-handler.test.ts | 2 +- .../device-battery-update.event-handler.ts | 4 +- ...lean-map-data-report.event-handler.test.ts | 2 +- ...ice-clean-map-data-report.event-handler.ts | 4 +- ...ice-clean-map-report.event-handler.test.ts | 2 +- .../device-clean-map-report.event-handler.ts | 4 +- ...ce-clean-task-report.event-handler.test.ts | 2 +- .../device-clean-task-report.event-handler.ts | 4 +- ...e-get-all-global-map.event-handler.test.ts | 2 +- ...device-get-all-global-map.event-handler.ts | 4 +- .../device-located.event-handler.test.ts | 2 +- .../device-located.event-handler.ts | 4 +- .../device-locked.event-handler.test.ts | 2 +- .../device-locked.event-handler.ts | 4 +- ...rger-position-update.event-handler.test.ts | 2 +- ...p-charger-position-update.event-handler.ts | 4 +- .../device-map-update.event-handler.test.ts | 2 +- .../device-map-update.event-handler.ts | 4 +- ...p-work-status-update.event-handler.test.ts | 2 +- ...ce-map-work-status-update.event-handler.ts | 4 +- ...vice-memory-map-info.event-handler.test.ts | 2 +- .../device-memory-map-info.event-handler.ts | 4 +- ...evice-network-update.event-handler.test.ts | 2 +- .../device-network-update.event-handler.ts | 4 +- .../device-offline.event-handler.test.ts | 2 +- .../device-offline.event-handler.ts | 4 +- ...ce-order-list-update.event-handler.test.ts | 2 +- .../device-order-list-update.event-handler.ts | 4 +- .../device-register.event-handler.test.ts | 2 +- .../device-register.event-handler.ts | 4 +- ...vice-settings-update.event-handler.test.ts | 2 +- .../device-settings-update.event-handler.ts | 4 +- .../device-time-update.event-handler.test.ts | 2 +- .../device-time-update.event-handler.ts | 4 +- .../device-upgrade-info.event-handler.test.ts | 2 +- .../device-upgrade-info.event-handler.ts | 4 +- ...evice-version-update.event-handler.test.ts | 2 +- .../device-version-update.event-handler.ts | 4 +- .../connection-device-updater.service.test.ts | 2 +- .../connection-device-updater.service.ts | 2 +- .../device-mode-changer.service.test.ts | 329 ++++++++++++++++++ .../services/device-mode-changer.service.ts | 109 ++++++ .../packet-connection-finder.service.test.ts | 17 +- .../packet-connection-finder.service.ts | 8 +- .../packet-event-publisher.service.test.ts | 6 +- .../packet-event-publisher.service.ts | 4 +- packages/adapter-tcp/src/tcp.server.ts | 31 +- .../connection.aggregate-root.test.ts | 20 +- .../connection.aggregate-root.ts | 12 +- packages/domain/src/commands/commands.ts | 10 + .../src/commands/locate-device.command.ts | 4 + .../commands/pause-cleaning.command.test.ts | 34 ++ .../src/commands/pause-cleaning.command.ts | 20 ++ .../src/commands/return-home.command.test.ts | 34 ++ .../src/commands/return-home.command.ts | 20 ++ .../set-device-quiet-hours.command.ts | 6 + .../src/commands/set-device-voice.command.ts | 6 + .../commands/start-cleaning.command.test.ts | 34 ++ .../src/commands/start-cleaning.command.ts | 20 ++ .../commands/stop-cleaning.command.test.ts | 34 ++ .../src/commands/stop-cleaning.command.ts | 20 ++ packages/domain/src/index.ts | 4 + .../src/repositories/connection.repository.ts | 7 +- packages/eslint-config/typescript.js | 7 +- packages/toolkit/src/index.ts | 2 +- .../src/services/waiter.service.test.ts | 65 ++++ .../toolkit/src/services/waiter.service.ts | 61 ++++ .../toolkit/src/utils/wait-for.util.test.ts | 43 --- packages/toolkit/src/utils/wait-for.util.ts | 40 --- 106 files changed, 2003 insertions(+), 268 deletions(-) rename packages/adapter-tcp/src/{ => base-classes}/packet.event-handler.ts (86%) create mode 100644 packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts create mode 100644 packages/adapter-tcp/src/command-handlers/return-home.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/return-home.command-handler.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.ts create mode 100644 packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts create mode 100644 packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts rename packages/adapter-tcp/src/{ => connection-handlers}/ntp-server.connection-handler.test.ts (100%) rename packages/adapter-tcp/src/{ => connection-handlers}/ntp-server.connection-handler.ts (100%) rename packages/adapter-tcp/src/{ => connection-handlers}/packet-server.connection-handler.test.ts (88%) rename packages/adapter-tcp/src/{ => connection-handlers}/packet-server.connection-handler.ts (84%) rename packages/adapter-tcp/src/{ => event-buses}/packet.event-bus.test.ts (100%) rename packages/adapter-tcp/src/{ => event-buses}/packet.event-bus.ts (85%) rename packages/adapter-tcp/src/{ => objects}/packet.message.test.ts (86%) rename packages/adapter-tcp/src/{ => objects}/packet.message.ts (86%) rename packages/adapter-tcp/src/{ => services}/connection-device-updater.service.test.ts (96%) rename packages/adapter-tcp/src/{ => services}/connection-device-updater.service.ts (90%) create mode 100644 packages/adapter-tcp/src/services/device-mode-changer.service.test.ts create mode 100644 packages/adapter-tcp/src/services/device-mode-changer.service.ts rename packages/adapter-tcp/src/{ => services}/packet-connection-finder.service.test.ts (75%) rename packages/adapter-tcp/src/{ => services}/packet-connection-finder.service.ts (63%) rename packages/adapter-tcp/src/{ => services}/packet-event-publisher.service.test.ts (90%) rename packages/adapter-tcp/src/{ => services}/packet-event-publisher.service.ts (88%) create mode 100644 packages/domain/src/commands/pause-cleaning.command.test.ts create mode 100644 packages/domain/src/commands/pause-cleaning.command.ts create mode 100644 packages/domain/src/commands/return-home.command.test.ts create mode 100644 packages/domain/src/commands/return-home.command.ts create mode 100644 packages/domain/src/commands/start-cleaning.command.test.ts create mode 100644 packages/domain/src/commands/start-cleaning.command.ts create mode 100644 packages/domain/src/commands/stop-cleaning.command.test.ts create mode 100644 packages/domain/src/commands/stop-cleaning.command.ts create mode 100644 packages/toolkit/src/services/waiter.service.test.ts create mode 100644 packages/toolkit/src/services/waiter.service.ts delete mode 100644 packages/toolkit/src/utils/wait-for.util.test.ts delete mode 100644 packages/toolkit/src/utils/wait-for.util.ts diff --git a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts index 12beb90d..7ee6b634 100644 --- a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.test.ts @@ -7,8 +7,8 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { anything, deepEqual, defer, imock, instance, spy, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { PacketConnection } from './packet-connection.aggregate-root'; -import type { PacketEventBus } from '../packet.event-bus'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventBus } from '../event-buses/packet.event-bus'; +import type { PacketMessage } from '../objects/packet.message'; import type { PacketFactory, PacketMapper } from '@agnoc/transport-tcp'; import type { AddressInfo } from 'net'; diff --git a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts index 2678a165..aa953bbf 100644 --- a/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts +++ b/packages/adapter-tcp/src/aggregate-roots/packet-connection.aggregate-root.ts @@ -1,8 +1,8 @@ import { Connection } from '@agnoc/domain'; import { DomainException, ID } from '@agnoc/toolkit'; import { PacketSocket } from '@agnoc/transport-tcp'; -import type { PacketEventBus } from '../packet.event-bus'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventBus } from '../event-buses/packet.event-bus'; +import type { PacketMessage } from '../objects/packet.message'; import type { ConnectionProps } from '@agnoc/domain'; import type { Packet, PacketFactory, PayloadDataName, PayloadDataFrom, CreatePacketProps } from '@agnoc/transport-tcp'; diff --git a/packages/adapter-tcp/src/packet.event-handler.ts b/packages/adapter-tcp/src/base-classes/packet.event-handler.ts similarity index 86% rename from packages/adapter-tcp/src/packet.event-handler.ts rename to packages/adapter-tcp/src/base-classes/packet.event-handler.ts index 4d8bbbbe..67f20e33 100644 --- a/packages/adapter-tcp/src/packet.event-handler.ts +++ b/packages/adapter-tcp/src/base-classes/packet.event-handler.ts @@ -1,4 +1,4 @@ -import type { PacketMessage } from './packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { EventHandler } from '@agnoc/toolkit'; import type { PayloadDataName } from '@agnoc/transport-tcp'; diff --git a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts index 24b3e72a..e3e43f26 100644 --- a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts +++ b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.test.ts @@ -4,13 +4,14 @@ import { imock, instance, when, verify, anything, deepEqual } from '@johanblumen import { expect } from 'chai'; import { LocateDeviceCommandHandler } from './locate-device.command-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; describe('LocateDeviceCommandHandler', function () { let packetConnectionFinderService: PacketConnectionFinderService; let commandHandler: LocateDeviceCommandHandler; - let packetConnection: PacketConnection; + let packetConnection: PacketConnection & ConnectionWithDevice; let packetMessage: PacketMessage; beforeEach(function () { diff --git a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts index 945a489e..21ef246d 100644 --- a/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/locate-device.command-handler.ts @@ -1,5 +1,5 @@ -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { CommandHandler, LocateDeviceCommand } from '@agnoc/domain'; export class LocateDeviceCommandHandler implements CommandHandler { diff --git a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts new file mode 100644 index 00000000..b585200c --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts @@ -0,0 +1,150 @@ +import { DeviceMap, DeviceMode, DeviceModeValue, MapPosition, PauseCleaningCommand } from '@agnoc/domain'; +import { givenSomeDeviceMapProps } from '@agnoc/domain/test-support'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { PauseCleaningCommandHandler } from './pause-cleaning.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, Device } from '@agnoc/domain'; + +describe('PauseCleaningCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: PauseCleaningCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + packetConnectionFinderService = imock(); + commandHandler = new PauseCleaningCommandHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('PauseCleaningCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + + it('should pause cleaning', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 2, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + + it('should pause zone cleaning', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + }); + + it('should pause mop cleaning', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', deepEqual({ ctrlValue: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP')).once(); + }); + + it('should pause spot cleaning', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + id: new ID(1), + currentSpot: new MapPosition({ x: 0, y: 0, phi: 0 }), + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.map).thenReturn(deviceMap); + + await commandHandler.handle(command); + + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_NAVIGATION_REQ', + deepEqual({ + mapHeadId: 1, + poseX: 0, + poseY: 0, + posePhi: 0, + ctrlValue: 2, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + }); + + it('should throw an error when trying to spot clean without map', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.map).thenReturn(undefined); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to pause spot cleaning, no map available', + ); + }); + + it('should throw an error when trying to spot clean without current spot', async function () { + const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + currentSpot: undefined, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.map).thenReturn(deviceMap); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to pause spot cleaning, no spot selected', + ); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts new file mode 100644 index 00000000..e569ebe7 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts @@ -0,0 +1,83 @@ +import { DeviceModeValue } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, PauseCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; + +export class PauseCleaningCommandHandler implements CommandHandler { + readonly forName = 'PauseCleaningCommand'; + + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + + async handle(event: PauseCleaningCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + const device = connection.device; + const deviceModeValue = device.mode?.value; + + if (deviceModeValue === DeviceModeValue.Zone) { + return this.pauseZoneCleaning(connection); + } + + if (deviceModeValue === DeviceModeValue.Mop) { + return this.pauseMopCleaning(connection); + } + + if (deviceModeValue === DeviceModeValue.Spot) { + return this.pauseSpotCleaning(connection); + } + + return this.pauseAutoCleaning(connection); + } + + private async pauseZoneCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Pause, + }); + + response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); + } + + private async pauseSpotCleaning(connection: PacketConnection & ConnectionWithDevice) { + if (!connection.device.map) { + throw new DomainException('Unable to pause spot cleaning, no map available'); + } + + if (!connection.device.map.currentSpot) { + throw new DomainException('Unable to pause spot cleaning, no spot selected'); + } + + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_NAVIGATION_REQ', { + mapHeadId: connection.device.map.id.value, + poseX: connection.device.map.currentSpot.x, + poseY: connection.device.map.currentSpot.y, + posePhi: connection.device.map.currentSpot.phi, + ctrlValue: ModeCtrlValue.Pause, + }); + + response.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP'); + } + + private async pauseMopCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Pause, + }); + + response.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP'); + } + + private async pauseAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Pause, + cleanType: 2, + }); + + response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/return-home.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/return-home.command-handler.test.ts new file mode 100644 index 00000000..0fba4c9f --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/return-home.command-handler.test.ts @@ -0,0 +1,52 @@ +import { ReturnHomeCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ReturnHomeCommandHandler } from './return-home.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; + +describe('ReturnHomeCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: ReturnHomeCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + + beforeEach(function () { + packetConnectionFinderService = imock(); + commandHandler = new ReturnHomeCommandHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('ReturnHomeCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const command = new ReturnHomeCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + + it('should stop cleaning', async function () { + const command = new ReturnHomeCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_CHARGE_REQ', deepEqual({ enable: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_CHARGE_RSP')).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/return-home.command-handler.ts b/packages/adapter-tcp/src/command-handlers/return-home.command-handler.ts new file mode 100644 index 00000000..097d0e16 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/return-home.command-handler.ts @@ -0,0 +1,23 @@ +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, ReturnHomeCommand } from '@agnoc/domain'; + +export class ReturnHomeCommandHandler implements CommandHandler { + readonly forName = 'ReturnHomeCommand'; + + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + + async handle(event: ReturnHomeCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + const response: PacketMessage = await connection.sendAndWait('DEVICE_CHARGE_REQ', { + enable: 1, + }); + + response.assertPayloadName('DEVICE_CHARGE_RSP'); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts index a26c2090..d27c4b50 100644 --- a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts +++ b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.test.ts @@ -5,13 +5,14 @@ import { imock, instance, when, verify, anything, deepEqual } from '@johanblumen import { expect } from 'chai'; import { SetDeviceQuietHoursCommandHandler } from './set-device-quiet-hours.command-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; describe('SetDeviceQuietHoursCommandHandler', function () { let packetConnectionFinderService: PacketConnectionFinderService; let commandHandler: SetDeviceQuietHoursCommandHandler; - let packetConnection: PacketConnection; + let packetConnection: PacketConnection & ConnectionWithDevice; let packetMessage: PacketMessage; beforeEach(function () { diff --git a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts index 0db68bc6..d6234dd2 100644 --- a/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/set-device-quiet-hours.command-handler.ts @@ -1,5 +1,5 @@ -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { CommandHandler, SetDeviceQuietHoursCommand } from '@agnoc/domain'; export class SetDeviceQuietHoursCommandHandler implements CommandHandler { diff --git a/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.test.ts new file mode 100644 index 00000000..6075584b --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.test.ts @@ -0,0 +1,110 @@ +import { DeviceSettings, SetDeviceVoiceCommand, VoiceSetting } from '@agnoc/domain'; +import { givenSomeDeviceSettingsProps } from '@agnoc/domain/test-support'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetDeviceVoiceCommandHandler } from './set-device-voice.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('SetDeviceVoiceCommandHandler', function () { + let voiceSettingMapper: VoiceSettingMapper; + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetDeviceVoiceCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + voiceSettingMapper = imock(); + deviceRepository = imock(); + packetConnectionFinderService = imock(); + commandHandler = new SetDeviceVoiceCommandHandler( + instance(packetConnectionFinderService), + instance(voiceSettingMapper), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetDeviceVoiceCommand'); + }); + + describe('#handle()', function () { + it('should change and set voice setting', async function () { + const voice = { isEnabled: true, volume: 5 }; + const voiceSetting = new VoiceSetting({ isEnabled: true, volume: 50 }); + const deviceSettings = new DeviceSettings(givenSomeDeviceSettingsProps()); + const command = new SetDeviceVoiceCommand({ + deviceId: new ID(1), + voice: voiceSetting, + }); + + when(voiceSettingMapper.fromDomain(anything())).thenReturn(voice); + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.settings).thenReturn(deviceSettings); + + await commandHandler.handle(command); + + verify(voiceSettingMapper.fromDomain(command.voice)).once(); + verify( + packetConnection.sendAndWait('USER_SET_DEVICE_CTRL_SETTING_REQ', deepEqual({ voiceMode: true, volume: 5 })), + ).once(); + verify(packetMessage.assertPayloadName('USER_SET_DEVICE_CTRL_SETTING_RSP')).once(); + verify( + device.updateSettings( + deepEqual(new DeviceSettings({ ...givenSomeDeviceSettingsProps(), voice: voiceSetting })), + ), + ).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const voiceSetting = new VoiceSetting({ isEnabled: true, volume: 50 }); + const command = new SetDeviceVoiceCommand({ + deviceId: new ID(1), + voice: voiceSetting, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + + it('should throw an error when device has no settings', async function () { + const voiceSetting = new VoiceSetting({ isEnabled: true, volume: 50 }); + const command = new SetDeviceVoiceCommand({ + deviceId: new ID(1), + voice: voiceSetting, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.settings).thenReturn(undefined); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to set voice setting when device settings are not available', + ); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.ts new file mode 100644 index 00000000..160366d2 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-voice.command-handler.ts @@ -0,0 +1,70 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { + CommandHandler, + ConnectionWithDevice, + Device, + DeviceRepository, + DeviceSettings, + SetDeviceVoiceCommand, +} from '@agnoc/domain'; + +export class SetDeviceVoiceCommandHandler implements CommandHandler { + readonly forName = 'SetDeviceVoiceCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly voiceSettingMapper: VoiceSettingMapper, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: SetDeviceVoiceCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + this.assertDeviceSettings(connection); + + await this.sendDeviceVoice(event, connection); + await this.updateDeviceSettings(connection, event); + } + + private assertDeviceSettings( + connection: PacketConnection & ConnectionWithDevice, + ): asserts connection is PacketConnection & ConnectionWithDevice { + if (!connection.device.settings) { + throw new DomainException('Unable to set voice setting when device settings are not available'); + } + } + + private async updateDeviceSettings( + connection: PacketConnection & ConnectionWithDevice, + event: SetDeviceVoiceCommand, + ) { + const settings = connection.device.settings.clone({ voice: event.voice }); + + connection.device.updateSettings(settings); + + await this.deviceRepository.saveOne(connection.device); + } + + private async sendDeviceVoice(event: SetDeviceVoiceCommand, connection: PacketConnection & ConnectionWithDevice) { + const { isEnabled, volume } = this.voiceSettingMapper.fromDomain(event.voice); + + const response: PacketMessage = await connection.sendAndWait('USER_SET_DEVICE_CTRL_SETTING_REQ', { + voiceMode: isEnabled, + volume, + }); + + response.assertPayloadName('USER_SET_DEVICE_CTRL_SETTING_RSP'); + } +} + +interface DeviceWithSettings extends Device { + settings: DeviceSettings; +} diff --git a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts new file mode 100644 index 00000000..993e3590 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts @@ -0,0 +1,277 @@ +import { + DeviceMap, + DeviceMode, + DeviceModeValue, + DeviceState, + DeviceStateValue, + MapCoordinate, + MapPosition, + Room, + StartCleaningCommand, + Zone, +} from '@agnoc/domain'; +import { givenSomeDeviceMapProps, givenSomeRoomProps } from '@agnoc/domain/test-support'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { StartCleaningCommandHandler } from './start-cleaning.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, Device, DeviceSystem } from '@agnoc/domain'; + +describe('StartCleaningCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let deviceModeChangerService: DeviceModeChangerService; + let commandHandler: StartCleaningCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + let deviceSystem: DeviceSystem; + + beforeEach(function () { + packetConnectionFinderService = imock(); + deviceModeChangerService = imock(); + commandHandler = new StartCleaningCommandHandler( + instance(packetConnectionFinderService), + instance(deviceModeChangerService), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + deviceSystem = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('StartCleaningCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + + it('should start cleaning', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(undefined); + when(device.hasMopAttached).thenReturn(false); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.None)), + ), + ).once(); + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + + it('should enable whole clean when supported', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + id: new ID(1), + rooms: [new Room({ ...givenSomeRoomProps(), id: new ID(1), name: 'Room 1' })], + restrictedZones: [new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 0, y: 0 })] })], + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(device.mode).thenReturn(undefined); + when(device.state).thenReturn(new DeviceState(DeviceStateValue.Docked)); + when(device.hasMopAttached).thenReturn(false); + when(device.map).thenReturn(deviceMap); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.None)), + ), + ).once(); + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_PLAN_PARAMS_REQ', + deepEqual({ + mapHeadId: 1, + mapName: 'Default', + planId: 2, + planName: 'Default', + roomList: [{ roomId: 1, roomName: 'Room 1', enable: true }], + areaInfo: { + mapHeadId: 1, + planId: 2, + cleanAreaLength: 1, + cleanAreaList: [{ cleanAreaId: 1, type: 0, coordinateLength: 1, coordinateList: [{ x: 0, y: 0 }] }], + }, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP')).once(); + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + + it('should not enable whole clean when device has no map', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(device.mode).thenReturn(undefined); + when(device.state).thenReturn(new DeviceState(DeviceStateValue.Docked)); + when(device.hasMopAttached).thenReturn(false); + when(device.map).thenReturn(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + + it('should start zone cleaning', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); + when(device.hasMopAttached).thenReturn(false); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.None)), + ), + ).once(); + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + }); + + it('should start mop cleaning', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(device.hasMopAttached).thenReturn(true); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode(instance(packetConnection), deepEqual(new DeviceMode(DeviceModeValue.Mop))), + ).once(); + verify(packetConnection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP')).once(); + }); + + it('should start spot cleaning', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + id: new ID(1), + currentSpot: new MapPosition({ x: 0, y: 0, phi: 0 }), + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.hasMopAttached).thenReturn(false); + when(device.map).thenReturn(deviceMap); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.None)), + ), + ).once(); + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_NAVIGATION_REQ', + deepEqual({ + mapHeadId: 1, + poseX: 0, + poseY: 0, + posePhi: 0, + ctrlValue: 1, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + }); + + it('should throw an error when trying to spot clean without map', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.hasMopAttached).thenReturn(false); + when(device.map).thenReturn(undefined); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to start spot cleaning, no map available', + ); + }); + + it('should throw an error when trying to spot clean without current spot', async function () { + const command = new StartCleaningCommand({ deviceId: new ID(1) }); + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + currentSpot: undefined, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + when(device.hasMopAttached).thenReturn(false); + when(device.map).thenReturn(deviceMap); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to start spot cleaning, no spot selected', + ); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts new file mode 100644 index 00000000..835d6d5c --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts @@ -0,0 +1,141 @@ +import { DeviceCapability, DeviceMode, DeviceModeValue, DeviceStateValue } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, StartCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; + +export class StartCleaningCommandHandler implements CommandHandler { + readonly forName = 'StartCleaningCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceModeChangerService: DeviceModeChangerService, + ) {} + + async handle(event: StartCleaningCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + await this.changeDeviceMode(connection); + + const device = connection.device; + const deviceModeValue = device.mode?.value; + + if (deviceModeValue === DeviceModeValue.Zone) { + return this.startZoneCleaning(connection); + } + + if (deviceModeValue === DeviceModeValue.Mop) { + return this.startMopCleaning(connection); + } + + if (deviceModeValue === DeviceModeValue.Spot) { + return this.startSpotCleaning(connection); + } + + if (this.isDockedAndSupportsMapPlans(connection)) { + await this.enableWholeClean(connection); + } + + return this.startAutoCleaning(connection); + } + + private async changeDeviceMode(connection: PacketConnection & ConnectionWithDevice) { + const deviceModeValue = connection.device.hasMopAttached ? DeviceModeValue.Mop : DeviceModeValue.None; + const deviceMode = new DeviceMode(deviceModeValue); + + await this.deviceModeChangerService.changeMode(connection, deviceMode); + } + + private async startZoneCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Start, + }); + + response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); + } + + private async startSpotCleaning(connection: PacketConnection & ConnectionWithDevice) { + if (!connection.device.map) { + throw new DomainException('Unable to start spot cleaning, no map available'); + } + + if (!connection.device.map.currentSpot) { + throw new DomainException('Unable to start spot cleaning, no spot selected'); + } + + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_NAVIGATION_REQ', { + mapHeadId: connection.device.map.id.value, + poseX: connection.device.map.currentSpot.x, + poseY: connection.device.map.currentSpot.y, + posePhi: connection.device.map.currentSpot.phi, + ctrlValue: ModeCtrlValue.Start, + }); + + response.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP'); + } + + private async startMopCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Start, + }); + + response.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP'); + } + + private async startAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Start, + cleanType: 2, + }); + + response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + } + + private isDockedAndSupportsMapPlans(connection: PacketConnection & ConnectionWithDevice) { + const supportsMapPlans = connection.device.system.supports(DeviceCapability.MAP_PLANS); + const deviceStateValue = connection.device.state?.value; + + return supportsMapPlans && deviceStateValue === DeviceStateValue.Docked; + } + + private async enableWholeClean(connection: PacketConnection & ConnectionWithDevice) { + if (!connection.device.map) { + return; + } + + const { id, restrictedZones, rooms } = connection.device.map; + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', { + mapHeadId: id.value, + // FIXME: this will change user map name. + mapName: 'Default', + planId: 2, + // FIXME: this will change user plan name. + planName: 'Default', + roomList: rooms.map((room) => ({ + roomId: room.id.value, + roomName: room.name, + enable: true, + })), + areaInfo: { + mapHeadId: id.value, + planId: 2, + cleanAreaLength: restrictedZones.length, + cleanAreaList: restrictedZones.map((zone) => ({ + cleanAreaId: zone.id.value, + type: 0, + coordinateLength: zone.coordinates.length, + coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), + })), + }, + }); + + response.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP'); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts new file mode 100644 index 00000000..1c69961a --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts @@ -0,0 +1,52 @@ +import { StopCleaningCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { StopCleaningCommandHandler } from './stop-cleaning.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; + +describe('StopCleaningCommandHandler', function () { + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: StopCleaningCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + + beforeEach(function () { + packetConnectionFinderService = imock(); + commandHandler = new StopCleaningCommandHandler(instance(packetConnectionFinderService)); + packetConnection = imock(); + packetMessage = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('StopCleaningCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const command = new StopCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + + it('should stop cleaning', async function () { + const command = new StopCleaningCommand({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 0, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts new file mode 100644 index 00000000..b24af91b --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts @@ -0,0 +1,30 @@ +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, StopCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; + +export class StopCleaningCommandHandler implements CommandHandler { + readonly forName = 'StopCleaningCommand'; + + constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + + async handle(event: StopCleaningCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + return this.stopAutoCleaning(connection); + } + + private async stopAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Stop, + cleanType: 2, + }); + + response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + } +} diff --git a/packages/adapter-tcp/src/ntp-server.connection-handler.test.ts b/packages/adapter-tcp/src/connection-handlers/ntp-server.connection-handler.test.ts similarity index 100% rename from packages/adapter-tcp/src/ntp-server.connection-handler.test.ts rename to packages/adapter-tcp/src/connection-handlers/ntp-server.connection-handler.test.ts diff --git a/packages/adapter-tcp/src/ntp-server.connection-handler.ts b/packages/adapter-tcp/src/connection-handlers/ntp-server.connection-handler.ts similarity index 100% rename from packages/adapter-tcp/src/ntp-server.connection-handler.ts rename to packages/adapter-tcp/src/connection-handlers/ntp-server.connection-handler.ts diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.test.ts b/packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.test.ts similarity index 88% rename from packages/adapter-tcp/src/packet-server.connection-handler.test.ts rename to packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.test.ts index 90c3402e..b109c18f 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.test.ts +++ b/packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.test.ts @@ -1,11 +1,11 @@ import { anything, capture, defer, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; +import { PacketMessage } from '../objects/packet.message'; import { PackerServerConnectionHandler } from './packet-server.connection-handler'; -import { PacketMessage } from './packet.message'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; -import type { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; -import type { PacketConnectionFactory } from './factories/connection.factory'; -import type { PacketEventPublisherService } from './packet-event-publisher.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFactory } from '../factories/connection.factory'; +import type { ConnectionDeviceUpdaterService } from '../services/connection-device-updater.service'; +import type { PacketEventPublisherService } from '../services/packet-event-publisher.service'; import type { ConnectionRepository } from '@agnoc/domain'; import type { Packet, PacketServer, PacketSocket } from '@agnoc/transport-tcp'; @@ -43,7 +43,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); verify(packetServer.on('connection', anything())).once(); verify(packetServer.once('close')).once(); @@ -54,7 +54,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); when(packetConnection.socket).thenReturn(instance(packetSocket)); @@ -73,7 +73,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); when(packetConnection.socket).thenReturn(instance(packetSocket)); @@ -92,7 +92,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); await onClose.resolve(undefined); @@ -104,7 +104,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); when(packetConnection.socket).thenReturn(instance(packetSocket)); @@ -131,7 +131,7 @@ describe('PackerServerConnectionHandler', function () { when(packetServer.once(anything())).thenResolve(onClose); - handler.addServers(instance(packetServer)); + handler.register(instance(packetServer)); when(packetConnectionFactory.create(anything())).thenReturn(instance(packetConnection)); when(packetConnection.socket).thenReturn(instance(packetSocket)); diff --git a/packages/adapter-tcp/src/packet-server.connection-handler.ts b/packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.ts similarity index 84% rename from packages/adapter-tcp/src/packet-server.connection-handler.ts rename to packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.ts index e5439e18..6c8e6bbc 100644 --- a/packages/adapter-tcp/src/packet-server.connection-handler.ts +++ b/packages/adapter-tcp/src/connection-handlers/packet-server.connection-handler.ts @@ -1,9 +1,9 @@ import { ID } from '@agnoc/toolkit'; -import { PacketMessage } from './packet.message'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; -import type { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; -import type { PacketConnectionFactory } from './factories/connection.factory'; -import type { PacketEventPublisherService } from './packet-event-publisher.service'; +import { PacketMessage } from '../objects/packet.message'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnectionFactory } from '../factories/connection.factory'; +import type { ConnectionDeviceUpdaterService } from '../services/connection-device-updater.service'; +import type { PacketEventPublisherService } from '../services/packet-event-publisher.service'; import type { ConnectionRepository } from '@agnoc/domain'; import type { PacketServer, Packet, PacketSocket } from '@agnoc/transport-tcp'; @@ -17,7 +17,7 @@ export class PackerServerConnectionHandler { private readonly packetEventPublisherService: PacketEventPublisherService, ) {} - addServers(...servers: PacketServer[]): void { + register(...servers: PacketServer[]): void { servers.forEach((server) => { this.servers.set(server, new Set()); this.addListeners(server); diff --git a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts index 3e5ea18c..964bc8ad 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.test.ts @@ -4,12 +4,13 @@ import { anything, deepEqual, imock, instance, verify, when } from '@johanblumen import { expect } from 'chai'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './lock-device-when-device-is-connected-event-handler.event-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; describe('LockDeviceWhenDeviceIsConnectedEventHandler', function () { let packetConnectionFinderService: PacketConnectionFinderService; let eventHandler: LockDeviceWhenDeviceIsConnectedEventHandler; - let packetConnection: PacketConnection; + let packetConnection: PacketConnection & ConnectionWithDevice; beforeEach(function () { packetConnectionFinderService = imock(); diff --git a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts index 8986f5ec..ed417714 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler.ts @@ -1,4 +1,4 @@ -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { DomainEventHandler, DeviceConnectedDomainEvent } from '@agnoc/domain'; export class LockDeviceWhenDeviceIsConnectedEventHandler implements DomainEventHandler { diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts index ea86dcca..a00c4f10 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.test.ts @@ -5,12 +5,13 @@ import { imock, instance, when, anything, verify, deepEqual } from '@johanblumen import { expect } from 'chai'; import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './query-device-info-when-device-is-locked-event-handler.event-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; describe('QueryDeviceInfoWhenDeviceIsLockedEventHandler', function () { let packetConnectionFinderService: PacketConnectionFinderService; let eventHandler: QueryDeviceInfoWhenDeviceIsLockedEventHandler; - let packetConnection: PacketConnection; + let packetConnection: PacketConnection & ConnectionWithDevice; beforeEach(function () { packetConnectionFinderService = imock(); diff --git a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts index 5d4d29b9..812b70af 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler.ts @@ -1,5 +1,5 @@ import { DeviceCapability } from '@agnoc/domain'; -import type { PacketConnectionFinderService } from '../packet-connection-finder.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { DeviceLockedDomainEvent, DomainEventHandler } from '@agnoc/domain'; export class QueryDeviceInfoWhenDeviceIsLockedEventHandler implements DomainEventHandler { diff --git a/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts index 8c9d5b6d..c0c14c07 100644 --- a/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts +++ b/packages/adapter-tcp/src/domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler.test.ts @@ -3,14 +3,13 @@ import { ID } from '@agnoc/toolkit'; import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler } from './set-device-connected-when-connection-device-changed.event-handler'; -import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { ConnectionRepository, DeviceRepository, Device } from '@agnoc/domain'; +import type { ConnectionRepository, DeviceRepository, Device, ConnectionWithDevice } from '@agnoc/domain'; describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function () { let connectionRepository: ConnectionRepository; let deviceRepository: DeviceRepository; let eventHandler: SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler; - let packetConnection: PacketConnection; + let connection: ConnectionWithDevice; let device: Device; beforeEach(function () { @@ -20,7 +19,7 @@ describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function ( instance(connectionRepository), instance(deviceRepository), ); - packetConnection = imock(); + connection = imock(); device = imock(); }); @@ -33,12 +32,9 @@ describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function ( const currentDeviceId = new ID(2); const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); - when(connectionRepository.findByDeviceId(anything())).thenResolve([ - instance(packetConnection), - instance(packetConnection), - ]); + when(connectionRepository.findByDeviceId(anything())).thenResolve([instance(connection), instance(connection)]); when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); - when(packetConnection.connectionType).thenReturn('PACKET'); + when(connection.connectionType).thenReturn('PACKET'); when(device.isConnected).thenReturn(false); await eventHandler.handle(event); @@ -64,9 +60,9 @@ describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function ( const currentDeviceId = new ID(2); const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); - when(connectionRepository.findByDeviceId(anything())).thenResolve([instance(packetConnection)]); + when(connectionRepository.findByDeviceId(anything())).thenResolve([instance(connection)]); when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); - when(packetConnection.connectionType).thenReturn('PACKET'); + when(connection.connectionType).thenReturn('PACKET'); when(device.isConnected).thenReturn(false); await eventHandler.handle(event); @@ -81,12 +77,9 @@ describe('SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler', function ( const currentDeviceId = new ID(2); const event = new ConnectionDeviceChangedDomainEvent({ aggregateId: new ID(1), currentDeviceId }); - when(connectionRepository.findByDeviceId(anything())).thenResolve([ - instance(packetConnection), - instance(packetConnection), - ]); + when(connectionRepository.findByDeviceId(anything())).thenResolve([instance(connection), instance(connection)]); when(deviceRepository.findOneById(anything())).thenResolve(instance(device)); - when(packetConnection.connectionType).thenReturn('OTHER'); + when(connection.connectionType).thenReturn('OTHER'); when(device.isConnected).thenReturn(false); await eventHandler.handle(event); diff --git a/packages/adapter-tcp/src/packet.event-bus.test.ts b/packages/adapter-tcp/src/event-buses/packet.event-bus.test.ts similarity index 100% rename from packages/adapter-tcp/src/packet.event-bus.test.ts rename to packages/adapter-tcp/src/event-buses/packet.event-bus.test.ts diff --git a/packages/adapter-tcp/src/packet.event-bus.ts b/packages/adapter-tcp/src/event-buses/packet.event-bus.ts similarity index 85% rename from packages/adapter-tcp/src/packet.event-bus.ts rename to packages/adapter-tcp/src/event-buses/packet.event-bus.ts index f5387f2d..580e8c34 100644 --- a/packages/adapter-tcp/src/packet.event-bus.ts +++ b/packages/adapter-tcp/src/event-buses/packet.event-bus.ts @@ -1,5 +1,5 @@ import { EventBus } from '@agnoc/toolkit'; -import type { PacketMessage } from './packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { PayloadDataName } from '@agnoc/transport-tcp'; /** Events for the packet event bus. */ diff --git a/packages/adapter-tcp/src/factories/connection.factory.test.ts b/packages/adapter-tcp/src/factories/connection.factory.test.ts index 248b0b81..deae0074 100644 --- a/packages/adapter-tcp/src/factories/connection.factory.test.ts +++ b/packages/adapter-tcp/src/factories/connection.factory.test.ts @@ -4,7 +4,7 @@ import { PacketSocket } from '@agnoc/transport-tcp'; import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { PacketConnectionFactory } from './connection.factory'; -import type { PacketEventBus } from '../packet.event-bus'; +import type { PacketEventBus } from '../event-buses/packet.event-bus'; import type { PacketFactory, PacketMapper } from '@agnoc/transport-tcp'; describe('PacketConnectionFactory', function () { diff --git a/packages/adapter-tcp/src/factories/connection.factory.ts b/packages/adapter-tcp/src/factories/connection.factory.ts index 431cdfe4..077963e0 100644 --- a/packages/adapter-tcp/src/factories/connection.factory.ts +++ b/packages/adapter-tcp/src/factories/connection.factory.ts @@ -1,6 +1,6 @@ import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { PacketConnectionProps } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketEventBus } from '../packet.event-bus'; +import type { PacketEventBus } from '../event-buses/packet.event-bus'; import type { Factory } from '@agnoc/toolkit'; import type { PacketFactory } from '@agnoc/transport-tcp'; diff --git a/packages/adapter-tcp/src/index.ts b/packages/adapter-tcp/src/index.ts index bac62133..d73e748b 100644 --- a/packages/adapter-tcp/src/index.ts +++ b/packages/adapter-tcp/src/index.ts @@ -1,9 +1 @@ -export * from './mappers/device-battery.mapper'; -export * from './mappers/device-error.mapper'; -export * from './mappers/device-fan-speed.mapper'; -export * from './mappers/device-mode.mapper'; -export * from './mappers/device-order.mapper'; -export * from './mappers/device-state.mapper'; -export * from './mappers/voice-setting.mapper'; -export * from './mappers/device-water-level.mapper'; export * from './tcp.server'; diff --git a/packages/adapter-tcp/src/packet.message.test.ts b/packages/adapter-tcp/src/objects/packet.message.test.ts similarity index 86% rename from packages/adapter-tcp/src/packet.message.test.ts rename to packages/adapter-tcp/src/objects/packet.message.test.ts index 0e17bbd9..ef4ab5b1 100644 --- a/packages/adapter-tcp/src/packet.message.test.ts +++ b/packages/adapter-tcp/src/objects/packet.message.test.ts @@ -6,7 +6,7 @@ import { givenSomePacketProps, givenSomePayloadProps } from '@agnoc/transport-tc import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { PacketMessage } from './packet.message'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; describe('PacketMessage', function () { let packetConnection: PacketConnection; @@ -99,21 +99,10 @@ describe('PacketMessage', function () { }); describe('#assertDevice()', function () { - it('should throw an error when the connection does not has a device set', function () { - when(packetConnection.device).thenReturn(undefined); + it('should invoke connection device assertion', function () { + packetMessage.assertDevice(); - expect(() => packetMessage.assertDevice()).to.throw( - DomainException, - 'Connection does not have a reference to a device', - ); - }); - - it('should not throw an error when the connection has a device set', function () { - const device = new Device(givenSomeDeviceProps()); - - when(packetConnection.device).thenReturn(device); - - expect(() => packetMessage.assertDevice()).to.not.throw(DomainException); + verify(packetConnection.assertDevice()).once(); }); }); }); diff --git a/packages/adapter-tcp/src/packet.message.ts b/packages/adapter-tcp/src/objects/packet.message.ts similarity index 86% rename from packages/adapter-tcp/src/packet.message.ts rename to packages/adapter-tcp/src/objects/packet.message.ts index e7b3a033..8b83fd9e 100644 --- a/packages/adapter-tcp/src/packet.message.ts +++ b/packages/adapter-tcp/src/objects/packet.message.ts @@ -1,5 +1,5 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { Device } from '@agnoc/domain'; import type { Packet, PayloadDataFrom, PayloadDataName } from '@agnoc/transport-tcp'; @@ -18,10 +18,12 @@ export class PacketMessage { return this.connection.respondAndWait(name, object, this.packet); } + // TODO: move this to packet. hasPayloadName(name: Name): this is PacketMessage { return this.packet.payload.opcode.value === (name as string); } + // TODO: move this to packet. assertPayloadName(name: Name): asserts this is PacketMessage { if (!this.hasPayloadName(name)) { throw new DomainException( @@ -31,8 +33,6 @@ export class PacketMessage { } assertDevice(): asserts this is PacketMessage & { device: Device } { - if (!this.device) { - throw new DomainException('Connection does not have a reference to a device'); - } + this.connection.assertDevice(); } } diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts index 90f721f8..0273a168 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.test.ts @@ -1,7 +1,7 @@ import { deepEqual, imock, instance, verify } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { ClientHeartbeatEventHandler } from './client-heartbeat.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('ClientHeartbeatEventHandler', function () { let eventHandler: ClientHeartbeatEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts index 882649d3..f5c03a02 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-heartbeat.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class ClientHeartbeatEventHandler implements PacketEventHandler { readonly forName = 'CLIENT_HEARTBEAT_REQ'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts index d5dada18..2993fbef 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.test.ts @@ -3,7 +3,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { ClientLoginEventHandler } from './client-login.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device } from '@agnoc/domain'; describe('ClientLoginEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts index 511b98b1..d559582a 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/client-login.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class ClientLoginEventHandler implements PacketEventHandler { readonly forName = 'CLIENT_ONLINE_REQ'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts index bc705090..19c5a1a1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.test.ts @@ -5,7 +5,7 @@ import { anything, deepEqual, imock, instance, verify, when } from '@johanblumen import { expect } from 'chai'; import { DeviceBatteryUpdateEventHandler } from './device-battery-update.event-handler'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceBatteryUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts index 826d59bb..26becae1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-battery-update.event-handler.ts @@ -1,6 +1,6 @@ +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceBatteryUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts index 92cce37d..4aedd464 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.test.ts @@ -3,7 +3,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceCleanMapDataReportEventHandler } from './device-clean-map-data-report.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceCleanMapDataReportEventHandler', function () { let eventHandler: DeviceCleanMapDataReportEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts index ac203bfa..49f09b32 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-data-report.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceCleanMapDataReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_CLEANMAP_BINDATA_REPORT_REQ'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts index 39135a55..f690256f 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.test.ts @@ -3,7 +3,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceCleanMapReportEventHandler } from './device-clean-map-report.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceCleanMapReportEventHandler', function () { let eventHandler: DeviceCleanMapReportEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts index 2b09a0ca..e184a9ba 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-map-report.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceCleanMapReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANMAP'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts index 4e6ef67e..1e2a5244 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.test.ts @@ -3,7 +3,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceCleanTaskReportEventHandler } from './device-clean-task-report.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceCleanTaskReportEventHandler', function () { let eventHandler: DeviceCleanTaskReportEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts index 26f9575d..6f32e0fc 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-clean-task-report.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceCleanTaskReportEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_EVENT_REPORT_CLEANTASK'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts index 8c3dc730..a2597cd1 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.test.ts @@ -1,7 +1,7 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceGetAllGlobalMapEventHandler } from './device-get-all-global-map.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceGetAllGlobalMapEventHandler', function () { let eventHandler: DeviceGetAllGlobalMapEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts index cf8c68c1..067866e8 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-get-all-global-map.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceGetAllGlobalMapEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GET_ALL_GLOBAL_MAP_INFO_RSP'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts index 31c2b4e6..3a726c99 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.test.ts @@ -1,7 +1,7 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceLocatedEventHandler } from './device-located.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceLocatedEventHandler', function () { let eventHandler: DeviceLocatedEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts index 6b12e9a3..a3775eb9 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-located.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceLocatedEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_SEEK_LOCATION_RSP'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts index d94f1119..c811edda 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.test.ts @@ -1,7 +1,7 @@ import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceLockedEventHandler } from './device-locked.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceLockedEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts index 4a5b9824..ba505c4e 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-locked.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceLockedEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts index 076ad9d3..8a08b6d0 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.test.ts @@ -4,7 +4,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { anything, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceMapChargerPositionUpdateEventHandler } from './device-map-charger-position-update.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository, DeviceMap } from '@agnoc/domain'; describe('DeviceMapChargerPositionUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts index 91684d04..4ecf0adf 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-charger-position-update.event-handler.ts @@ -1,6 +1,6 @@ import { MapPosition } from '@agnoc/domain'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapChargerPositionUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts index e98c0b2a..0e649811 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.test.ts @@ -29,7 +29,7 @@ import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; import type { DeviceStateMapper } from '../mappers/device-state.mapper'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceMapUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts index abe9c58b..f35a805b 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-update.event-handler.ts @@ -10,13 +10,13 @@ import { Zone, } from '@agnoc/domain'; import { ID, isPresent } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; import type { DeviceStateMapper } from '../mappers/device-state.mapper'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts index 4ef6c92c..7c1670d8 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.test.ts @@ -25,7 +25,7 @@ import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; import type { DeviceStateMapper } from '../mappers/device-state.mapper'; import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceMapWorkStatusUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts index 994c9e51..e2a70cd4 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-map-work-status-update.event-handler.ts @@ -1,13 +1,13 @@ import { CleanSize, DeviceCleanWork, DeviceTime } from '@agnoc/domain'; import { isPresent } from '@agnoc/toolkit'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; import type { DeviceBatteryMapper } from '../mappers/device-battery.mapper'; import type { DeviceErrorMapper } from '../mappers/device-error.mapper'; import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; import type { DeviceModeMapper } from '../mappers/device-mode.mapper'; import type { DeviceStateMapper } from '../mappers/device-state.mapper'; import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceMapWorkStatusUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts index 48d5994c..810a9335 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.test.ts @@ -1,7 +1,7 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceMemoryMapInfoEventHandler } from './device-memory-map-info.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceMemoryMapInfoEventHandler', function () { let eventHandler: DeviceMemoryMapInfoEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts index 60407bb7..4525b2cc 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-memory-map-info.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceMemoryMapInfoEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_MAPID_PUSH_ALL_MEMORY_MAP_INFO'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts index 18257b1f..ea58d56b 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.test.ts @@ -4,7 +4,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { capture, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceNetworkUpdateEventHandler } from './device-network-update.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceNetworkUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts index f5f5356a..39c31b63 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-network-update.event-handler.ts @@ -1,6 +1,6 @@ import { DeviceNetwork } from '@agnoc/domain'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceNetworkUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts index f966f8ad..6774fce9 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.test.ts @@ -1,7 +1,7 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceOfflineEventHandler } from './device-offline.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceOfflineEventHandler', function () { let eventHandler: DeviceOfflineEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts index 2e0b94da..fab22059 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-offline.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceOfflineEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_OFFLINE_CMD'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts index 55678e2f..1774ed64 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.test.ts @@ -6,7 +6,7 @@ import { anything, deepEqual, imock, instance, verify, when } from '@johanblumen import { expect } from 'chai'; import { DeviceOrderListUpdateEventHandler } from './device-order-list-update.event-handler'; import type { DeviceOrderMapper } from '../mappers/device-order.mapper'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceOrderListUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts index 2ca7cc00..aa2bb07e 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-order-list-update.event-handler.ts @@ -1,6 +1,6 @@ +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; import type { DeviceOrderMapper } from '../mappers/device-order.mapper'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceOrderListUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts index 005db065..63fe95bd 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.test.ts @@ -4,7 +4,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceRegisterEventHandler } from './device-register.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; describe('DeviceRegisterEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts index f7377910..2ec184de 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-register.event-handler.ts @@ -1,7 +1,7 @@ import { Device, DeviceBattery, DeviceBatteryMaxValue, DeviceSystem, DeviceVersion } from '@agnoc/domain'; import { ID } from '@agnoc/toolkit'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceRegisterEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts index 8043c1ad..943b3fcb 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.test.ts @@ -6,7 +6,7 @@ import { anything, capture, deepEqual, imock, instance, verify, when } from '@jo import { expect } from 'chai'; import { DeviceSettingsUpdateEventHandler } from './device-settings-update.event-handler'; import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceSettingsUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts index 9a11725f..5c79d116 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-settings-update.event-handler.ts @@ -1,7 +1,7 @@ import { DeviceSetting, DeviceSettings, DeviceTime, QuietHoursSetting } from '@agnoc/domain'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; import type { VoiceSettingMapper } from '../mappers/voice-setting.mapper'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceSettingsUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts index 8a874c4c..e7e3da7d 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.test.ts @@ -1,7 +1,7 @@ import { imock, instance } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceTimeUpdateEventHandler } from './device-time-update.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceTimeUpdateEventHandler', function () { let eventHandler: DeviceTimeUpdateEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts index 4f191fa1..8fa32242 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-time-update.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceTimeUpdateEventHandler implements PacketEventHandler { readonly forName = 'DEVICE_GETTIME_RSP'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts index 29d00ec9..d33e1f4d 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.test.ts @@ -3,7 +3,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceUpgradeInfoEventHandler } from './device-upgrade-info.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; describe('DeviceUpgradeInfoEventHandler', function () { let eventHandler: DeviceUpgradeInfoEventHandler; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts index 6b7749cf..f6556f1b 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-upgrade-info.event-handler.ts @@ -1,5 +1,5 @@ -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; export class DeviceUpgradeInfoEventHandler implements PacketEventHandler { readonly forName = 'PUSH_DEVICE_PACKAGE_UPGRADE_INFO_REQ'; diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts index 84046432..49c8a80d 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.test.ts @@ -4,7 +4,7 @@ import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { DeviceVersionUpdateEventHandler } from './device-version-update.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketMessage } from '../objects/packet.message'; import type { Device, DeviceRepository } from '@agnoc/domain'; describe('DeviceVersionUpdateEventHandler', function () { diff --git a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts index c0a5fdfb..3406078c 100644 --- a/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts +++ b/packages/adapter-tcp/src/packet-event-handlers/device-version-update.event-handler.ts @@ -1,6 +1,6 @@ import { DeviceVersion } from '@agnoc/domain'; -import type { PacketEventHandler } from '../packet.event-handler'; -import type { PacketMessage } from '../packet.message'; +import type { PacketEventHandler } from '../base-classes/packet.event-handler'; +import type { PacketMessage } from '../objects/packet.message'; import type { DeviceRepository } from '@agnoc/domain'; export class DeviceVersionUpdateEventHandler implements PacketEventHandler { diff --git a/packages/adapter-tcp/src/connection-device-updater.service.test.ts b/packages/adapter-tcp/src/services/connection-device-updater.service.test.ts similarity index 96% rename from packages/adapter-tcp/src/connection-device-updater.service.test.ts rename to packages/adapter-tcp/src/services/connection-device-updater.service.test.ts index 3b123685..69205a99 100644 --- a/packages/adapter-tcp/src/connection-device-updater.service.test.ts +++ b/packages/adapter-tcp/src/services/connection-device-updater.service.test.ts @@ -5,7 +5,7 @@ import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { anything, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; import { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { ConnectionRepository, DeviceRepository } from '@agnoc/domain'; describe('ConnectionDeviceUpdaterService', function () { diff --git a/packages/adapter-tcp/src/connection-device-updater.service.ts b/packages/adapter-tcp/src/services/connection-device-updater.service.ts similarity index 90% rename from packages/adapter-tcp/src/connection-device-updater.service.ts rename to packages/adapter-tcp/src/services/connection-device-updater.service.ts index 0005c561..8a70026b 100644 --- a/packages/adapter-tcp/src/connection-device-updater.service.ts +++ b/packages/adapter-tcp/src/services/connection-device-updater.service.ts @@ -1,4 +1,4 @@ -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; import type { ConnectionRepository, DeviceRepository, Device } from '@agnoc/domain'; import type { ID } from '@agnoc/toolkit'; import type { Packet } from '@agnoc/transport-tcp'; diff --git a/packages/adapter-tcp/src/services/device-mode-changer.service.test.ts b/packages/adapter-tcp/src/services/device-mode-changer.service.test.ts new file mode 100644 index 00000000..098e64de --- /dev/null +++ b/packages/adapter-tcp/src/services/device-mode-changer.service.test.ts @@ -0,0 +1,329 @@ +import { DeviceCapability, DeviceMode, DeviceModeValue } from '@agnoc/domain'; +import { ArgumentInvalidException, DomainException } from '@agnoc/toolkit'; +import { anything, capture, deepEqual, imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { DeviceModeChangerService } from './device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { Device, DeviceSystem } from '@agnoc/domain'; +import type { WaiterService } from '@agnoc/toolkit'; + +describe('DeviceModeChangerService', function () { + let waiterService: WaiterService; + let service: DeviceModeChangerService; + let packetConnection: PacketConnection; + let device: Device; + let packetMessage: PacketMessage; + + beforeEach(function () { + waiterService = imock(); + service = new DeviceModeChangerService(instance(waiterService)); + packetConnection = imock(); + device = imock(); + packetMessage = imock(); + }); + + describe('#changeMode()', function () { + describe('to None', function () { + it('should do nothing when the mode is already none', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.None)); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.None)); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait(anything(), anything())).never(); + }); + + it('should change device mode to none', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.None)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.None)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify( + packetConnection.sendAndWait( + 'DEVICE_AUTO_CLEAN_REQ', + deepEqual({ + ctrlValue: 0, + cleanType: 2, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should throw an error when device mode change times out', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenReject(new Error('Timeout')); + + await expect( + service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.None)), + ).to.be.rejectedWith(DomainException, `Unable to change device mode from 'Unknown' to 'None'`); + + verify(packetConnection.assertDevice()).once(); + verify( + packetConnection.sendAndWait( + 'DEVICE_AUTO_CLEAN_REQ', + deepEqual({ + ctrlValue: 0, + cleanType: 2, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + }); + + describe('to Spot', function () { + it('should do nothing when the mode is already spot', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Spot)); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait(anything(), anything())).never(); + }); + + it('should change device mode to spot on device with map plans support', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Spot)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify(deviceSystem.supports(DeviceCapability.MAP_PLANS)).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x7aff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should change device mode to spot on device without map plans support', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Spot)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify(deviceSystem.supports(DeviceCapability.MAP_PLANS)).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x2ff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should throw an error when device mode change times out', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenReject(new Error('Timeout')); + + await expect( + service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Spot)), + ).to.be.rejectedWith(DomainException, `Unable to change device mode from 'Unknown' to 'Spot'`); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x7aff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + }); + + describe('to Zone', function () { + it('should do nothing when the mode is already zone', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Zone)); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait(anything(), anything())).never(); + }); + + it('should change device mode to zone on device with map plans support', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Zone)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify(deviceSystem.supports(DeviceCapability.MAP_PLANS)).once(); + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 0 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x79ff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should change device mode to zone on device without map plans support', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(false); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Zone)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify(deviceSystem.supports(DeviceCapability.MAP_PLANS)).once(); + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 0 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x1ff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should throw an error when device mode change times out', async function () { + const deviceSystem: DeviceSystem = imock(); + + when(packetConnection.device).thenReturn(instance(device)); + when(device.system).thenReturn(instance(deviceSystem)); + when(deviceSystem.supports(anything())).thenReturn(true); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenReject(new Error('Timeout')); + + await expect( + service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Zone)), + ).to.be.rejectedWith(DomainException, `Unable to change device mode from 'Unknown' to 'Zone'`); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 0 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', deepEqual({ mask: 0x79ff }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + }); + + describe('to Mop', function () { + it('should do nothing when the mode is already mop', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Mop)); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait(anything(), anything())).never(); + }); + + it('should change device mode to mop', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.None)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenResolve(); + + await service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Mop)); + + const [callback] = capture(waiterService.waitFor).first(); + + expect(callback()).to.be.false; + + when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); + + expect(callback()).to.be.true; + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_INTO_MODEIDLE_INFO_REQ', deepEqual({ mode: 7 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_INTO_MODEIDLE_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + + it('should throw an error when device mode change times out', async function () { + when(packetConnection.device).thenReturn(instance(device)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(waiterService.waitFor(anything(), anything())).thenReject(new Error('Timeout')); + + await expect( + service.changeMode(instance(packetConnection), new DeviceMode(DeviceModeValue.Mop)), + ).to.be.rejectedWith(DomainException, `Unable to change device mode from 'Unknown' to 'Mop'`); + + verify(packetConnection.assertDevice()).once(); + verify(packetConnection.sendAndWait('DEVICE_MAPID_INTO_MODEIDLE_INFO_REQ', deepEqual({ mode: 7 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_INTO_MODEIDLE_INFO_RSP')).once(); + verify(waiterService.waitFor(anything(), deepEqual({ timeout: 5000 }))).once(); + }); + }); + + it('should thrown an error when the mode is not supported', async function () { + const deviceMode: DeviceMode = imock(); + + when(deviceMode.equals(anything())).thenReturn(false); + when(deviceMode.value).thenReturn('Unknown' as DeviceModeValue); + + await expect(service.changeMode(instance(packetConnection), instance(deviceMode))).to.be.rejectedWith( + ArgumentInvalidException, + `Value 'Unknown' is not supported in DeviceModeChangerService`, + ); + }); + }); +}); diff --git a/packages/adapter-tcp/src/services/device-mode-changer.service.ts b/packages/adapter-tcp/src/services/device-mode-changer.service.ts new file mode 100644 index 00000000..62f07602 --- /dev/null +++ b/packages/adapter-tcp/src/services/device-mode-changer.service.ts @@ -0,0 +1,109 @@ +import { DeviceCapability, DeviceModeValue } from '@agnoc/domain'; +import { ArgumentInvalidException, DomainException } from '@agnoc/toolkit'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceMode, ConnectionWithDevice } from '@agnoc/domain'; +import type { WaiterService } from '@agnoc/toolkit'; + +const MODE_CHANGE_TIMEOUT = 5000; + +export const ModeCtrlValue = { + Stop: 0, + Start: 1, + Pause: 2, +} as const; + +export class DeviceModeChangerService { + constructor(private readonly waiterService: WaiterService) {} + + async changeMode(connection: PacketConnection, mode: DeviceMode): Promise { + connection.assertDevice(); + + if (mode.equals(connection.device.mode)) { + return; + } + + if (mode.value === DeviceModeValue.None) { + await this.changeModeToNone(connection); + } else if (mode.value === DeviceModeValue.Spot) { + await this.changeModeToSpot(connection); + } else if (mode.value === DeviceModeValue.Zone) { + await this.changeModeToZone(connection); + } else if (mode.value === DeviceModeValue.Mop) { + await this.changeModeToMop(connection); + } else { + throw new ArgumentInvalidException( + `Value '${mode.value as string}' is not supported in ${this.constructor.name}`, + ); + } + + return this.waitForModeChange(connection, mode); + } + + private async changeModeToNone(connection: PacketConnection): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Stop, + cleanType: 2, + }); + + response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + } + + private async changeModeToSpot(connection: PacketConnection & ConnectionWithDevice): Promise { + let mask = 0x78ff | 0x200; + + if (!connection.device.system.supports(DeviceCapability.MAP_PLANS)) { + mask = 0xff | 0x200; + } + + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', { mask }); + + response.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'); + } + + private async changeModeToZone(connection: PacketConnection & ConnectionWithDevice): Promise { + let response: PacketMessage; + + response = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { + ctrlValue: ModeCtrlValue.Stop, + }); + + response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); + + let mask = 0x78ff | 0x100; + + if (!connection.device.system.supports(DeviceCapability.MAP_PLANS)) { + mask = 0xff | 0x100; + } + + response = await connection.sendAndWait('DEVICE_MAPID_GET_GLOBAL_INFO_REQ', { mask }); + + response.assertPayloadName('DEVICE_MAPID_GET_GLOBAL_INFO_RSP'); + } + + private async changeModeToMop(connection: PacketConnection & ConnectionWithDevice): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_INTO_MODEIDLE_INFO_REQ', { + mode: 7, + }); + + response.assertPayloadName('DEVICE_MAPID_INTO_MODEIDLE_INFO_RSP'); + } + + private async waitForModeChange( + connection: PacketConnection & ConnectionWithDevice, + mode: DeviceMode, + ): Promise { + const callback = () => mode.equals(connection.device.mode); + const options = { timeout: MODE_CHANGE_TIMEOUT }; + + try { + await this.waiterService.waitFor(callback, options); + } catch (err) { + throw new DomainException( + `Unable to change device mode from '${connection.device.mode?.value ?? 'Unknown'}' to '${mode.value}'`, + undefined, + { cause: err }, + ); + } + } +} diff --git a/packages/adapter-tcp/src/packet-connection-finder.service.test.ts b/packages/adapter-tcp/src/services/packet-connection-finder.service.test.ts similarity index 75% rename from packages/adapter-tcp/src/packet-connection-finder.service.test.ts rename to packages/adapter-tcp/src/services/packet-connection-finder.service.test.ts index c0413a9c..f636d3d4 100644 --- a/packages/adapter-tcp/src/packet-connection-finder.service.test.ts +++ b/packages/adapter-tcp/src/services/packet-connection-finder.service.test.ts @@ -2,37 +2,36 @@ import { DomainException, ID } from '@agnoc/toolkit'; import { imock, instance, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; import { PacketConnectionFinderService } from './packet-connection-finder.service'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; -import type { ConnectionRepository } from '@agnoc/domain'; +import type { ConnectionRepository, ConnectionWithDevice } from '@agnoc/domain'; describe('PacketConnectionFinderService', function () { let connectionRepository: ConnectionRepository; let service: PacketConnectionFinderService; - let packetConnection: PacketConnection; + let connection: ConnectionWithDevice; beforeEach(function () { connectionRepository = imock(); service = new PacketConnectionFinderService(instance(connectionRepository)); - packetConnection = imock(); + connection = imock(); }); describe('#findByDeviceId()', function () { it('should find a connection by device id when it is a packet connection', async function () { const deviceId = new ID(1); - when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(packetConnection)]); - when(packetConnection.connectionType).thenReturn('PACKET'); + when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(connection)]); + when(connection.connectionType).thenReturn('PACKET'); const ret = await service.findByDeviceId(deviceId); - expect(ret).to.equal(instance(packetConnection)); + expect(ret).to.equal(instance(connection)); }); it('should not find anything when is it not a packet connection', async function () { const deviceId = new ID(1); - when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(packetConnection)]); - when(packetConnection.connectionType).thenReturn('OTHER'); + when(connectionRepository.findByDeviceId(deviceId)).thenResolve([instance(connection)]); + when(connection.connectionType).thenReturn('OTHER'); const ret = await service.findByDeviceId(deviceId); diff --git a/packages/adapter-tcp/src/packet-connection-finder.service.ts b/packages/adapter-tcp/src/services/packet-connection-finder.service.ts similarity index 63% rename from packages/adapter-tcp/src/packet-connection-finder.service.ts rename to packages/adapter-tcp/src/services/packet-connection-finder.service.ts index 1bbe641e..d2166333 100644 --- a/packages/adapter-tcp/src/packet-connection-finder.service.ts +++ b/packages/adapter-tcp/src/services/packet-connection-finder.service.ts @@ -1,19 +1,19 @@ import { DomainException } from '@agnoc/toolkit'; -import { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; -import type { ConnectionRepository, Connection } from '@agnoc/domain'; +import { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { ConnectionRepository, Connection, ConnectionWithDevice } from '@agnoc/domain'; import type { ID } from '@agnoc/toolkit'; export class PacketConnectionFinderService { constructor(private readonly connectionRepository: ConnectionRepository) {} - async findByDeviceId(deviceId: ID): Promise { + async findByDeviceId(deviceId: ID): Promise<(PacketConnection & ConnectionWithDevice) | undefined> { const connections = await this.connectionRepository.findByDeviceId(deviceId); if (connections.length === 0) { throw new DomainException(`Unable to find a connection for the device with id ${deviceId.value}`); } - return connections.find((connection: Connection): connection is PacketConnection => + return connections.find((connection: Connection): connection is PacketConnection & ConnectionWithDevice => PacketConnection.isPacketConnection(connection), ); } diff --git a/packages/adapter-tcp/src/packet-event-publisher.service.test.ts b/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts similarity index 90% rename from packages/adapter-tcp/src/packet-event-publisher.service.test.ts rename to packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts index 59f9c43f..10ff36f4 100644 --- a/packages/adapter-tcp/src/packet-event-publisher.service.test.ts +++ b/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts @@ -3,10 +3,10 @@ import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { imock, instance, anything, verify, when } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; +import { PacketMessage } from '../objects/packet.message'; import { PacketEventPublisherService } from './packet-event-publisher.service'; -import { PacketMessage } from './packet.message'; -import type { PacketConnection } from './aggregate-roots/packet-connection.aggregate-root'; -import type { PacketEventBus } from './packet.event-bus'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketEventBus } from '../event-buses/packet.event-bus'; describe('PacketEventPublisherService', function () { let packetEventBus: PacketEventBus; diff --git a/packages/adapter-tcp/src/packet-event-publisher.service.ts b/packages/adapter-tcp/src/services/packet-event-publisher.service.ts similarity index 88% rename from packages/adapter-tcp/src/packet-event-publisher.service.ts rename to packages/adapter-tcp/src/services/packet-event-publisher.service.ts index 01b19079..1dab811c 100644 --- a/packages/adapter-tcp/src/packet-event-publisher.service.ts +++ b/packages/adapter-tcp/src/services/packet-event-publisher.service.ts @@ -1,6 +1,6 @@ import { DomainException } from '@agnoc/toolkit'; -import type { PacketEventBus, PacketEventBusEvents } from './packet.event-bus'; -import type { PacketMessage } from './packet.message'; +import type { PacketEventBus, PacketEventBusEvents } from '../event-buses/packet.event-bus'; +import type { PacketMessage } from '../objects/packet.message'; import type { PayloadDataName } from '@agnoc/transport-tcp'; export class PacketEventPublisherService { diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 35e59322..ea0eec8f 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -1,4 +1,4 @@ -import { EventHandlerRegistry } from '@agnoc/toolkit'; +import { EventHandlerRegistry, WaiterService } from '@agnoc/toolkit'; import { getCustomDecoders, getProtobufRoot, @@ -9,10 +9,13 @@ import { PacketFactory, } from '@agnoc/transport-tcp'; import { LocateDeviceCommandHandler } from './command-handlers/locate-device.command-handler'; -import { ConnectionDeviceUpdaterService } from './connection-device-updater.service'; +import { StartCleaningCommandHandler } from './command-handlers/start-cleaning.command-handler'; +import { NTPServerConnectionHandler } from './connection-handlers/ntp-server.connection-handler'; +import { PackerServerConnectionHandler } from './connection-handlers/packet-server.connection-handler'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; import { QueryDeviceInfoWhenDeviceIsLockedEventHandler } from './domain-event-handlers/query-device-info-when-device-is-locked-event-handler.event-handler'; import { SetDeviceAsConnectedWhenConnectionDeviceAddedEventHandler } from './domain-event-handlers/set-device-connected-when-connection-device-changed.event-handler'; +import { PacketEventBus } from './event-buses/packet.event-bus'; import { PacketConnectionFactory } from './factories/connection.factory'; import { CleanModeMapper } from './mappers/clean-mode.mapper'; import { DeviceBatteryMapper } from './mappers/device-battery.mapper'; @@ -24,8 +27,6 @@ import { DeviceStateMapper } from './mappers/device-state.mapper'; import { DeviceWaterLevelMapper } from './mappers/device-water-level.mapper'; import { VoiceSettingMapper } from './mappers/voice-setting.mapper'; import { WeekDayListMapper } from './mappers/week-day-list.mapper'; -import { NTPServerConnectionHandler } from './ntp-server.connection-handler'; -import { PacketConnectionFinderService } from './packet-connection-finder.service'; import { ClientHeartbeatEventHandler } from './packet-event-handlers/client-heartbeat.event-handler'; import { ClientLoginEventHandler } from './packet-event-handlers/client-login.event-handler'; import { DeviceBatteryUpdateEventHandler } from './packet-event-handlers/device-battery-update.event-handler'; @@ -47,9 +48,10 @@ import { DeviceSettingsUpdateEventHandler } from './packet-event-handlers/device import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-time-update.event-handler'; import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-upgrade-info.event-handler'; import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; -import { PacketEventPublisherService } from './packet-event-publisher.service'; -import { PackerServerConnectionHandler } from './packet-server.connection-handler'; -import { PacketEventBus } from './packet.event-bus'; +import { ConnectionDeviceUpdaterService } from './services/connection-device-updater.service'; +import { DeviceModeChangerService } from './services/device-mode-changer.service'; +import { PacketConnectionFinderService } from './services/packet-connection-finder.service'; +import { PacketEventPublisherService } from './services/packet-event-publisher.service'; import type { CommandsOrQueries, ConnectionRepository, DeviceRepository } from '@agnoc/domain'; import type { Server, TaskHandlerRegistry } from '@agnoc/toolkit'; import type { AddressInfo } from 'net'; @@ -96,14 +98,18 @@ export class TCPServer implements Server { const packetEventBus = new PacketEventBus(); const packetEventHandlerRegistry = new EventHandlerRegistry(packetEventBus); - // Connection - const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); + // Services + const waiterService = new WaiterService(); const connectionDeviceUpdaterService = new ConnectionDeviceUpdaterService( this.connectionRepository, deviceRepository, ); const packetEventPublisherService = new PacketEventPublisherService(packetEventBus); const packetConnectionFinderService = new PacketConnectionFinderService(this.connectionRepository); + const deviceModeChangerService = new DeviceModeChangerService(waiterService); + + // Connection + const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); const connectionManager = new PackerServerConnectionHandler( this.connectionRepository, packetConnectionFactory, @@ -111,7 +117,7 @@ export class TCPServer implements Server { packetEventPublisherService, ); - connectionManager.addServers(this.cmdServer, this.mapServer); + connectionManager.register(this.cmdServer, this.mapServer); // Time Sync server controller const ntpServerConnectionHandler = new NTPServerConnectionHandler(packetFactory); @@ -166,7 +172,10 @@ export class TCPServer implements Server { ); // Command event handlers - this.commandQueryHandlerRegistry.register(new LocateDeviceCommandHandler(packetConnectionFinderService)); + this.commandQueryHandlerRegistry.register( + new LocateDeviceCommandHandler(packetConnectionFinderService), + new StartCleaningCommandHandler(packetConnectionFinderService, deviceModeChangerService), + ); } async listen(options: TCPAdapterListenOptions = listenDefaultOptions): Promise { diff --git a/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts index 398336fb..f3034fb6 100644 --- a/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/connection.aggregate-root.test.ts @@ -1,4 +1,4 @@ -import { AggregateRoot, ArgumentInvalidException } from '@agnoc/toolkit'; +import { AggregateRoot, ArgumentInvalidException, DomainException } from '@agnoc/toolkit'; import { expect } from 'chai'; import { ConnectionDeviceChangedDomainEvent } from '../domain-events/connection-device-changed.domain-event'; import { givenSomeDeviceProps, givenSomeConnectionProps } from '../test-support'; @@ -109,6 +109,24 @@ describe('Connection', function () { ); }); }); + + describe('#assertDevice()', function () { + it('should throw an error when the connection does not has a device set', function () { + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device: undefined }); + + expect(() => connection.assertDevice()).to.throw( + DomainException, + 'Connection does not have a reference to a device', + ); + }); + + it('should not throw an error when the connection has a device set', function () { + const device = new Device(givenSomeDeviceProps()); + const connection = new DummyConnection({ ...givenSomeConnectionProps(), device }); + + expect(() => connection.assertDevice()).to.not.throw(DomainException); + }); + }); }); class DummyConnection extends Connection { diff --git a/packages/domain/src/aggregate-roots/connection.aggregate-root.ts b/packages/domain/src/aggregate-roots/connection.aggregate-root.ts index ffc6c2ea..8681f3b4 100644 --- a/packages/domain/src/aggregate-roots/connection.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/connection.aggregate-root.ts @@ -1,4 +1,4 @@ -import { AggregateRoot } from '@agnoc/toolkit'; +import { AggregateRoot, DomainException } from '@agnoc/toolkit'; import { ConnectionDeviceChangedDomainEvent } from '../domain-events/connection-device-changed.domain-event'; import { Device } from './device.aggregate-root'; import type { EntityProps } from '@agnoc/toolkit'; @@ -7,6 +7,10 @@ export interface ConnectionProps extends EntityProps { device?: Device; } +export interface ConnectionWithDevice extends Connection { + device: T; +} + export abstract class Connection extends AggregateRoot { abstract readonly connectionType: string; @@ -34,6 +38,12 @@ export abstract class Connection { + /** Returns the ID of the device to locate. */ get deviceId(): ID { return this.props.deviceId; } diff --git a/packages/domain/src/commands/pause-cleaning.command.test.ts b/packages/domain/src/commands/pause-cleaning.command.test.ts new file mode 100644 index 00000000..2f5bcd19 --- /dev/null +++ b/packages/domain/src/commands/pause-cleaning.command.test.ts @@ -0,0 +1,34 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { PauseCleaningCommand } from './pause-cleaning.command'; +import type { PauseCleaningCommandInput } from './pause-cleaning.command'; + +describe('PauseCleaningCommand', function () { + it('should be created', function () { + const input = givenAPauseCleaningCommandInput(); + const command = new PauseCleaningCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new PauseCleaningCommand({ ...givenAPauseCleaningCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for PauseCleaningCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new PauseCleaningCommand({ ...givenAPauseCleaningCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of PauseCleaningCommand is not an instance of ID`, + ); + }); +}); + +function givenAPauseCleaningCommandInput(): PauseCleaningCommandInput { + return { deviceId: ID.generate() }; +} diff --git a/packages/domain/src/commands/pause-cleaning.command.ts b/packages/domain/src/commands/pause-cleaning.command.ts new file mode 100644 index 00000000..8256059b --- /dev/null +++ b/packages/domain/src/commands/pause-cleaning.command.ts @@ -0,0 +1,20 @@ +import { Command, ID } from '@agnoc/toolkit'; + +/** Input for the command pausing the cleaning process of a device. */ +export interface PauseCleaningCommandInput { + /** ID of the device. */ + deviceId: ID; +} + +/** Command for pausing the cleaning process of a device. */ +export class PauseCleaningCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: PauseCleaningCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } +} diff --git a/packages/domain/src/commands/return-home.command.test.ts b/packages/domain/src/commands/return-home.command.test.ts new file mode 100644 index 00000000..7e8d5a2c --- /dev/null +++ b/packages/domain/src/commands/return-home.command.test.ts @@ -0,0 +1,34 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { ReturnHomeCommand } from './return-home.command'; +import type { ReturnHomeCommandInput } from './return-home.command'; + +describe('ReturnHomeCommand', function () { + it('should be created', function () { + const input = givenAReturnHomeCommandInput(); + const command = new ReturnHomeCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new ReturnHomeCommand({ ...givenAReturnHomeCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for ReturnHomeCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new ReturnHomeCommand({ ...givenAReturnHomeCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of ReturnHomeCommand is not an instance of ID`, + ); + }); +}); + +function givenAReturnHomeCommandInput(): ReturnHomeCommandInput { + return { deviceId: ID.generate() }; +} diff --git a/packages/domain/src/commands/return-home.command.ts b/packages/domain/src/commands/return-home.command.ts new file mode 100644 index 00000000..20e253ab --- /dev/null +++ b/packages/domain/src/commands/return-home.command.ts @@ -0,0 +1,20 @@ +import { Command, ID } from '@agnoc/toolkit'; + +/** Input for the command returning a device to its home position. */ +export interface ReturnHomeCommandInput { + /** ID of the device. */ + deviceId: ID; +} + +/** Command for returning a device to its home position. */ +export class ReturnHomeCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: ReturnHomeCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } +} diff --git a/packages/domain/src/commands/set-device-quiet-hours.command.ts b/packages/domain/src/commands/set-device-quiet-hours.command.ts index 10d656fe..345937ff 100644 --- a/packages/domain/src/commands/set-device-quiet-hours.command.ts +++ b/packages/domain/src/commands/set-device-quiet-hours.command.ts @@ -1,16 +1,22 @@ import { Command, ID } from '@agnoc/toolkit'; import { QuietHoursSetting } from '../value-objects/quiet-hours-setting.value-object'; +/** Input for the command setting the quiet hours of a device. */ export interface SetDeviceQuietHoursCommandInput { + /** ID of the device to set the quiet hours for. */ deviceId: ID; + /** Quiet hours to set. */ quietHours: QuietHoursSetting; } +/** Command that sets the quiet hours of a device. */ export class SetDeviceQuietHoursCommand extends Command { + /** Returns the ID of the device to set the quiet hours for. */ get deviceId(): ID { return this.props.deviceId; } + /** Returns the quiet hours to set. */ get quietHours(): QuietHoursSetting { return this.props.quietHours; } diff --git a/packages/domain/src/commands/set-device-voice.command.ts b/packages/domain/src/commands/set-device-voice.command.ts index 17a4f112..b91588a2 100644 --- a/packages/domain/src/commands/set-device-voice.command.ts +++ b/packages/domain/src/commands/set-device-voice.command.ts @@ -1,16 +1,22 @@ import { Command, ID } from '@agnoc/toolkit'; import { VoiceSetting } from '../value-objects/voice-setting.value-object'; +/** Input for the command setting the voice setting of a device. */ export interface SetDeviceVoiceCommandInput { + /** ID of the device to set the voice setting for. */ deviceId: ID; + /** Voice setting to set. */ voice: VoiceSetting; } +/** Command that sets the voice setting of a device. */ export class SetDeviceVoiceCommand extends Command { + /** Returns the ID of the device to set the voice setting for. */ get deviceId(): ID { return this.props.deviceId; } + /** Returns the voice setting to set. */ get voice(): VoiceSetting { return this.props.voice; } diff --git a/packages/domain/src/commands/start-cleaning.command.test.ts b/packages/domain/src/commands/start-cleaning.command.test.ts new file mode 100644 index 00000000..b43c13a5 --- /dev/null +++ b/packages/domain/src/commands/start-cleaning.command.test.ts @@ -0,0 +1,34 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { StartCleaningCommand } from './start-cleaning.command'; +import type { StartCleaningCommandInput } from './start-cleaning.command'; + +describe('StartCleaningCommand', function () { + it('should be created', function () { + const input = givenAStartCleaningCommandInput(); + const command = new StartCleaningCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new StartCleaningCommand({ ...givenAStartCleaningCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for StartCleaningCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new StartCleaningCommand({ ...givenAStartCleaningCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of StartCleaningCommand is not an instance of ID`, + ); + }); +}); + +function givenAStartCleaningCommandInput(): StartCleaningCommandInput { + return { deviceId: ID.generate() }; +} diff --git a/packages/domain/src/commands/start-cleaning.command.ts b/packages/domain/src/commands/start-cleaning.command.ts new file mode 100644 index 00000000..3d5c995b --- /dev/null +++ b/packages/domain/src/commands/start-cleaning.command.ts @@ -0,0 +1,20 @@ +import { Command, ID } from '@agnoc/toolkit'; + +/** Input for the command starting the cleaning process of a device. */ +export interface StartCleaningCommandInput { + /** ID of the device. */ + deviceId: ID; +} + +/** Command for starting the cleaning process of a device. */ +export class StartCleaningCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: StartCleaningCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } +} diff --git a/packages/domain/src/commands/stop-cleaning.command.test.ts b/packages/domain/src/commands/stop-cleaning.command.test.ts new file mode 100644 index 00000000..2f63e741 --- /dev/null +++ b/packages/domain/src/commands/stop-cleaning.command.test.ts @@ -0,0 +1,34 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { StopCleaningCommand } from './stop-cleaning.command'; +import type { StopCleaningCommandInput } from './stop-cleaning.command'; + +describe('StopCleaningCommand', function () { + it('should be created', function () { + const input = givenAStopCleaningCommandInput(); + const command = new StopCleaningCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new StopCleaningCommand({ ...givenAStopCleaningCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for StopCleaningCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new StopCleaningCommand({ ...givenAStopCleaningCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of StopCleaningCommand is not an instance of ID`, + ); + }); +}); + +function givenAStopCleaningCommandInput(): StopCleaningCommandInput { + return { deviceId: ID.generate() }; +} diff --git a/packages/domain/src/commands/stop-cleaning.command.ts b/packages/domain/src/commands/stop-cleaning.command.ts new file mode 100644 index 00000000..e0d6c51f --- /dev/null +++ b/packages/domain/src/commands/stop-cleaning.command.ts @@ -0,0 +1,20 @@ +import { Command, ID } from '@agnoc/toolkit'; + +/** Input for the command stopping the cleaning process of a device. */ +export interface StopCleaningCommandInput { + /** ID of the device. */ + deviceId: ID; +} + +/** Command for stopping the cleaning process of a device. */ +export class StopCleaningCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: StopCleaningCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 265ac8af..5643b8aa 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -2,8 +2,12 @@ export * from './aggregate-roots/connection.aggregate-root'; export * from './aggregate-roots/device.aggregate-root'; export * from './commands/commands'; export * from './commands/locate-device.command'; +export * from './commands/pause-cleaning.command'; +export * from './commands/return-home.command'; export * from './commands/set-device-quiet-hours.command'; export * from './commands/set-device-voice.command'; +export * from './commands/start-cleaning.command'; +export * from './commands/stop-cleaning.command'; export * from './domain-events/connection-device-changed.domain-event'; export * from './domain-events/device-battery-changed.domain-event'; export * from './domain-events/device-clean-work-changed.domain-event'; diff --git a/packages/domain/src/repositories/connection.repository.ts b/packages/domain/src/repositories/connection.repository.ts index 0205cbcb..07160351 100644 --- a/packages/domain/src/repositories/connection.repository.ts +++ b/packages/domain/src/repositories/connection.repository.ts @@ -1,5 +1,5 @@ import { Repository } from '@agnoc/toolkit'; -import type { Connection } from '../aggregate-roots/connection.aggregate-root'; +import type { Connection, ConnectionWithDevice } from '../aggregate-roots/connection.aggregate-root'; import type { ID } from '@agnoc/toolkit'; export interface ConnectionRepositoryPorts { @@ -7,9 +7,10 @@ export interface ConnectionRepositoryPorts { } export class ConnectionRepository extends Repository implements ConnectionRepositoryPorts { - async findByDeviceId(deviceId: ID): Promise { + async findByDeviceId(deviceId: ID): Promise<(Connection & ConnectionWithDevice)[]> { const connections = this.adapter.getAll() as Connection[]; - return connections.filter((connection) => connection.device?.id.equals(deviceId)); + return connections.filter((connection) => deviceId.equals(connection.device?.id)) as (Connection & + ConnectionWithDevice)[]; } } diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index 152ddb5a..2e64e48b 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -77,7 +77,6 @@ module.exports = { groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], }, ], - '@guardian/tsdoc-required/tsdoc-required': 'warn', }, overrides: [ { @@ -89,5 +88,11 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', }, }, + { + files: ['packages/toolkit/src/**/*.ts'], + rules: { + '@guardian/tsdoc-required/tsdoc-required': 'warn', + }, + }, ], }; diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 5bc090bc..386826e5 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -27,6 +27,7 @@ export * from './exceptions/argument-out-of-range.exception'; export * from './exceptions/domain.exception'; export * from './exceptions/not-implemented.exception'; export * from './exceptions/timeout.exception'; +export * from './services/waiter.service'; export * from './streams/buffer-writer.stream'; export * from './task-handler.registry'; export * from './types/constructor.type'; @@ -46,4 +47,3 @@ export * from './utils/stream.util'; export * from './utils/symmetric-difference.util'; export * from './utils/to-dash-case.util'; export * from './utils/to-stream.util'; -export * from './utils/wait-for.util'; diff --git a/packages/toolkit/src/services/waiter.service.test.ts b/packages/toolkit/src/services/waiter.service.test.ts new file mode 100644 index 00000000..24711630 --- /dev/null +++ b/packages/toolkit/src/services/waiter.service.test.ts @@ -0,0 +1,65 @@ +import { setTimeout } from 'timers/promises'; +import { fnmock, instance, nextTick, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { TimeoutException } from '../exceptions/timeout.exception'; +import { WaiterService } from './waiter.service'; +import type { WaitForCallback } from './waiter.service'; + +describe('WaiterService', function () { + let service: WaiterService; + + beforeEach(function () { + service = new WaiterService(); + }); + + it('resolves when condition is met with default options', async function () { + const callback: WaitForCallback = fnmock(); + const promise = service.waitFor(instance(callback)); + + when(callback()).thenReturn(false).thenReturn(true); + + await nextTick(); + + verify(callback()).once(); + + await setTimeout(30); + + verify(callback()).once(); + + await setTimeout(30); + + verify(callback()).twice(); + + return promise; + }); + + it('resolves when condition is met with custom interval', async function () { + const callback: WaitForCallback = fnmock(); + const promise = service.waitFor(instance(callback), { interval: 30 }); + + when(callback()).thenReturn(false).thenReturn(true); + + await nextTick(); + + verify(callback()).once(); + + await setTimeout(40); + + verify(callback()).twice(); + + return promise; + }); + + it('rejects when condition is not met and timeout triggers', async function () { + const callback: WaitForCallback = fnmock(); + const promise = service.waitFor(instance(callback), { timeout: 100 }); + + when(callback()).thenReturn(false); + + await setTimeout(110); + + verify(callback()).twice(); + + await expect(promise).to.be.rejectedWith(TimeoutException, 'Timeout waiting for condition'); + }); +}); diff --git a/packages/toolkit/src/services/waiter.service.ts b/packages/toolkit/src/services/waiter.service.ts new file mode 100644 index 00000000..1eaaf358 --- /dev/null +++ b/packages/toolkit/src/services/waiter.service.ts @@ -0,0 +1,61 @@ +import { TimeoutException } from '../exceptions/timeout.exception'; + +/** Callback function signature for the `waitFor` method. */ +export type WaitForCallback = () => boolean; + +/** Options for the `waitFor` method. */ +export interface WaitForOptions { + /** + * Timeout in milliseconds. Set to -1 to disable. + * + * @defaultValue -1 + */ + timeout: number; + + /** + * Interval to check the condition in milliseconds. + * + * @defaultValue 50 + */ + interval: number; +} + +const DEFAULT_OPTIONS = { + timeout: -1, + interval: 50, +}; + +/** Service with utility functions for waiting for a given condition to be met. */ +export class WaiterService { + /** Waits for a callback to return true. */ + async waitFor(callback: WaitForCallback, options?: Partial): Promise { + return new Promise((resolve, reject) => { + const opts = Object.assign({}, DEFAULT_OPTIONS, options); + const isTimeoutEnabled = opts.timeout !== -1; + let abortTimer: NodeJS.Timer; + let checkTimer: NodeJS.Timer; + + function check() { + const ret = callback(); + + if (ret) { + clearTimeout(abortTimer); + resolve(); + } else { + checkTimer = setTimeout(check, opts.interval); + } + } + + function abort() { + clearTimeout(checkTimer); + reject(new TimeoutException('Timeout waiting for condition')); + } + + if (isTimeoutEnabled) { + abortTimer = setTimeout(abort, opts.timeout); + } + + process.nextTick(check); + }); + } +} diff --git a/packages/toolkit/src/utils/wait-for.util.test.ts b/packages/toolkit/src/utils/wait-for.util.test.ts deleted file mode 100644 index 9aa183f2..00000000 --- a/packages/toolkit/src/utils/wait-for.util.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { setTimeout } from 'timers/promises'; -import { expect } from 'chai'; -import { TimeoutException } from '../exceptions/timeout.exception'; -import { waitFor } from './wait-for.util'; - -describe('wait-for.util', function () { - it('resolves when condition is met', async function () { - let check = false, - called = false; - const promise = waitFor(() => check); - - void promise.then(() => { - called = true; - }); - - expect(called).to.be.false; - - check = true; - - await setTimeout(50); - - expect(called).to.be.true; - }); - - it('rejects when condition is not met and timeout triggers', async function () { - let called = false; - const check = false; - const promise = waitFor(() => check, { timeout: 100 }); - - void promise.catch((e) => { - expect(e).to.be.instanceof(TimeoutException); - called = true; - }); - - await setTimeout(50); - - expect(called).to.be.false; - - await setTimeout(50); - - expect(called).to.be.true; - }); -}); diff --git a/packages/toolkit/src/utils/wait-for.util.ts b/packages/toolkit/src/utils/wait-for.util.ts deleted file mode 100644 index 3c717259..00000000 --- a/packages/toolkit/src/utils/wait-for.util.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TimeoutException } from '../exceptions/timeout.exception'; - -export type WaitForCallback = () => boolean; - -export interface WaitForOptions { - timeout: number; - interval: number; -} - -const DEFAULT_OPTIONS = { - timeout: 2 ** 31 - 1, - interval: 50, -}; - -export function waitFor(callback: WaitForCallback, options?: Partial): Promise { - return new Promise((resolve, reject) => { - const opts = Object.assign({}, DEFAULT_OPTIONS, options); - - function check() { - const ret = callback(); - - if (ret) { - clearTimeout(abortTimer); - resolve(); - } else { - checkTimer = setTimeout(check, opts.interval); - } - } - - function abort() { - clearTimeout(checkTimer); - reject(new TimeoutException('Timeout waiting for condition')); - } - - const abortTimer = setTimeout(abort, opts.timeout); - let checkTimer: NodeJS.Timer; - - process.nextTick(check); - }); -} From 8cd28a8806c922e1b365be031bfd90efc1b15058 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Fri, 31 Mar 2023 11:01:10 +0200 Subject: [PATCH 30/38] feat: add `SetCarpetModeCommand` --- .../set-carpet-mode.command-handler.test.ts | 92 +++++++++++++++++++ .../set-carpet-mode.command-handler.ts | 68 ++++++++++++++ packages/domain/src/commands/commands.ts | 2 + .../commands/set-carpet-mode.command.test.ts | 54 +++++++++++ .../src/commands/set-carpet-mode.command.ts | 30 ++++++ packages/domain/src/index.ts | 1 + 6 files changed, 247 insertions(+) create mode 100644 packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.ts create mode 100644 packages/domain/src/commands/set-carpet-mode.command.test.ts create mode 100644 packages/domain/src/commands/set-carpet-mode.command.ts diff --git a/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.test.ts new file mode 100644 index 00000000..ae2ac1e0 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.test.ts @@ -0,0 +1,92 @@ +import { DeviceSetting, DeviceSettings, SetCarpetModeCommand } from '@agnoc/domain'; +import { givenSomeDeviceSettingsProps } from '@agnoc/domain/test-support'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetCarpetModeCommandHandler } from './set-carpet-mode.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('SetCarpetModeCommandHandler', function () { + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetCarpetModeCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + packetConnectionFinderService = imock(); + commandHandler = new SetCarpetModeCommandHandler( + instance(packetConnectionFinderService), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetCarpetModeCommand'); + }); + + describe('#handle()', function () { + it('should change and set carpet mode', async function () { + const carpetMode = new DeviceSetting({ isEnabled: true }); + const deviceSettings = new DeviceSettings(givenSomeDeviceSettingsProps()); + const command = new SetCarpetModeCommand({ deviceId: new ID(1), carpetMode }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.settings).thenReturn(deviceSettings); + + await commandHandler.handle(command); + + verify( + packetConnection.sendAndWait('USER_SET_DEVICE_CLEANPREFERENCE_REQ', deepEqual({ carpetTurbo: true })), + ).once(); + verify(packetMessage.assertPayloadName('USER_SET_DEVICE_CLEANPREFERENCE_RSP')).once(); + verify( + device.updateSettings(deepEqual(new DeviceSettings({ ...givenSomeDeviceSettingsProps(), carpetMode }))), + ).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const carpetMode = new DeviceSetting({ isEnabled: true }); + const command = new SetCarpetModeCommand({ deviceId: new ID(1), carpetMode }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + + it('should throw an error when device has no settings', async function () { + const carpetMode = new DeviceSetting({ isEnabled: true }); + const command = new SetCarpetModeCommand({ deviceId: new ID(1), carpetMode }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.settings).thenReturn(undefined); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to set voice setting when device settings are not available', + ); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.ts new file mode 100644 index 00000000..48276cbd --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-carpet-mode.command-handler.ts @@ -0,0 +1,68 @@ +import { DomainException } from '@agnoc/toolkit'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { + CommandHandler, + ConnectionWithDevice, + Device, + DeviceRepository, + DeviceSettings, + SetCarpetModeCommand, +} from '@agnoc/domain'; + +export class SetCarpetModeCommandHandler implements CommandHandler { + readonly forName = 'SetCarpetModeCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: SetCarpetModeCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + this.assertDeviceSettings(connection); + + await this.sendCarpetMode(event, connection); + await this.updateDeviceSettings(event, connection); + } + + private assertDeviceSettings( + connection: PacketConnection & ConnectionWithDevice, + ): asserts connection is PacketConnection & ConnectionWithDevice { + if (!connection.device.settings) { + throw new DomainException('Unable to set voice setting when device settings are not available'); + } + } + + private async updateDeviceSettings( + { carpetMode }: SetCarpetModeCommand, + connection: PacketConnection & ConnectionWithDevice, + ) { + const settings = connection.device.settings.clone({ carpetMode }); + + connection.device.updateSettings(settings); + + await this.deviceRepository.saveOne(connection.device); + } + + private async sendCarpetMode( + { carpetMode }: SetCarpetModeCommand, + connection: PacketConnection & ConnectionWithDevice, + ) { + const response: PacketMessage = await connection.sendAndWait('USER_SET_DEVICE_CLEANPREFERENCE_REQ', { + carpetTurbo: carpetMode.isEnabled, + }); + + response.assertPayloadName('USER_SET_DEVICE_CLEANPREFERENCE_RSP'); + } +} + +interface DeviceWithSettings extends Device { + settings: DeviceSettings; +} diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index f7489590..17c295aa 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,6 +1,7 @@ import type { LocateDeviceCommand } from './locate-device.command'; import type { PauseCleaningCommand } from './pause-cleaning.command'; import type { ReturnHomeCommand } from './return-home.command'; +import type { SetCarpetModeCommand } from './set-carpet-mode.command'; import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; import type { SetDeviceVoiceCommand } from './set-device-voice.command'; import type { StartCleaningCommand } from './start-cleaning.command'; @@ -11,6 +12,7 @@ export type Commands = { LocateDeviceCommand: LocateDeviceCommand; PauseCleaningCommand: PauseCleaningCommand; ReturnHomeCommand: ReturnHomeCommand; + SetCarpetModeCommand: SetCarpetModeCommand; SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; SetDeviceVoiceCommand: SetDeviceVoiceCommand; StartCleaningCommand: StartCleaningCommand; diff --git a/packages/domain/src/commands/set-carpet-mode.command.test.ts b/packages/domain/src/commands/set-carpet-mode.command.test.ts new file mode 100644 index 00000000..8eef5543 --- /dev/null +++ b/packages/domain/src/commands/set-carpet-mode.command.test.ts @@ -0,0 +1,54 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceSetting } from '../value-objects/device-setting.value-object'; +import { SetCarpetModeCommand } from './set-carpet-mode.command'; +import type { SetCarpetModeCommandInput } from './set-carpet-mode.command'; + +describe('SetCarpetModeCommand', function () { + it('should be created', function () { + const input = givenASetCarpetModeCommandInput(); + const command = new SetCarpetModeCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.carpetMode).to.be.equal(input.carpetMode); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetCarpetModeCommand({ ...givenASetCarpetModeCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetCarpetModeCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetCarpetModeCommand({ ...givenASetCarpetModeCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetCarpetModeCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'carpetMode' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetCarpetModeCommand({ ...givenASetCarpetModeCommandInput(), carpetMode: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'carpetMode' for SetCarpetModeCommand not provided`); + }); + + it("should throw an error when 'carpetMode' is not an VoiceSetting", function () { + expect( + // @ts-expect-error - invalid property + () => new SetCarpetModeCommand({ ...givenASetCarpetModeCommandInput(), carpetMode: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'carpetMode' of SetCarpetModeCommand is not an instance of DeviceSetting`, + ); + }); +}); + +function givenASetCarpetModeCommandInput(): SetCarpetModeCommandInput { + return { deviceId: ID.generate(), carpetMode: new DeviceSetting({ isEnabled: true }) }; +} diff --git a/packages/domain/src/commands/set-carpet-mode.command.ts b/packages/domain/src/commands/set-carpet-mode.command.ts new file mode 100644 index 00000000..05af5aeb --- /dev/null +++ b/packages/domain/src/commands/set-carpet-mode.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { DeviceSetting } from '../value-objects/device-setting.value-object'; + +/** Input for the command stopping the cleaning process of a device. */ +export interface SetCarpetModeCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Carpet mode setting. */ + carpetMode: DeviceSetting; +} + +/** Command for stopping the cleaning process of a device. */ +export class SetCarpetModeCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the carpet mode setting. */ + get carpetMode(): DeviceSetting { + return this.props.carpetMode; + } + + protected validate(props: SetCarpetModeCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'carpetMode'); + this.validateInstanceProp(props, 'carpetMode', DeviceSetting); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 5643b8aa..7c6582d8 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -4,6 +4,7 @@ export * from './commands/commands'; export * from './commands/locate-device.command'; export * from './commands/pause-cleaning.command'; export * from './commands/return-home.command'; +export * from './commands/set-carpet-mode.command'; export * from './commands/set-device-quiet-hours.command'; export * from './commands/set-device-voice.command'; export * from './commands/start-cleaning.command'; From 1bea2acd15455cacc65989b4778e533f97062ce6 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sat, 1 Apr 2023 12:47:17 +0200 Subject: [PATCH 31/38] feat: add consumables commands --- .../reset-consumable.command-handler.test.ts | 102 +++++++++++++++++ .../reset-consumable.command-handler.ts | 70 ++++++++++++ ...t-device-consumables.query-handler.test.ts | 98 ++++++++++++++++ .../get-device-consumables.query-handler.ts | 72 ++++++++++++ .../packet-event-publisher.service.test.ts | 12 +- .../packet-event-publisher.service.ts | 19 ++-- packages/adapter-tcp/src/tcp.server.ts | 20 +++- .../device.aggregate-root.test.ts | 17 ++- .../aggregate-roots/device.aggregate-root.ts | 12 +- packages/domain/src/commands/commands.ts | 2 + .../commands/reset-consumable.command.test.ts | 54 +++++++++ .../src/commands/reset-consumable.command.ts | 30 +++++ ...e-consumables-changed.domain-event.test.ts | 106 ++++++++++++++++++ ...device-consumables-changed.domain-event.ts | 27 +++++ packages/domain/src/index.ts | 2 + ...uery.test.ts => find-device.query.test.ts} | 0 .../get-device-consumables.query.test.ts | 71 ++++++++++++ .../queries/get-device-consumables.query.ts | 26 +++++ packages/domain/src/queries/queries.ts | 2 + packages/domain/src/test-support.ts | 2 +- .../device-consumable.value-object.test.ts | 14 +-- .../device-consumable.value-object.ts | 12 +- .../src/base-classes/aggregate-root.base.ts | 5 +- .../toolkit/src/base-classes/task-bus.base.ts | 7 +- 24 files changed, 739 insertions(+), 43 deletions(-) create mode 100644 packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.ts create mode 100644 packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.test.ts create mode 100644 packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.ts create mode 100644 packages/domain/src/commands/reset-consumable.command.test.ts create mode 100644 packages/domain/src/commands/reset-consumable.command.ts create mode 100644 packages/domain/src/domain-events/device-consumables-changed.domain-event.test.ts create mode 100644 packages/domain/src/domain-events/device-consumables-changed.domain-event.ts rename packages/domain/src/queries/{find-device-query.test.ts => find-device.query.test.ts} (100%) create mode 100644 packages/domain/src/queries/get-device-consumables.query.test.ts create mode 100644 packages/domain/src/queries/get-device-consumables.query.ts diff --git a/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.test.ts new file mode 100644 index 00000000..84992d82 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.test.ts @@ -0,0 +1,102 @@ +import { DeviceConsumable, DeviceConsumableType, ResetConsumableCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ResetConsumableCommandHandler } from './reset-consumable.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('ResetConsumableCommandHandler', function () { + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: ResetConsumableCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + packetConnectionFinderService = imock(); + commandHandler = new ResetConsumableCommandHandler( + instance(packetConnectionFinderService), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('ResetConsumableCommand'); + }); + + describe('#handle()', function () { + it('should reset an existing consumable', async function () { + const mainBrush = new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 100 }); + const sideBrush = new DeviceConsumable({ type: DeviceConsumableType.SideBrush, hoursUsed: 50 }); + const command = new ResetConsumableCommand({ + deviceId: new ID(1), + consumable: mainBrush, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.consumables).thenReturn([mainBrush, sideBrush]); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_MAPID_SET_CONSUMABLES_PARAM_REQ', deepEqual({ itemId: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_CONSUMABLES_PARAM_RSP')).once(); + verify( + device.updateConsumables( + deepEqual([new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 0 }), sideBrush]), + ), + ).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should reset a non existing consumable', async function () { + const mainBrush = new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 100 }); + const command = new ResetConsumableCommand({ + deviceId: new ID(1), + consumable: mainBrush, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.consumables).thenReturn(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_MAPID_SET_CONSUMABLES_PARAM_REQ', deepEqual({ itemId: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_CONSUMABLES_PARAM_RSP')).once(); + verify( + device.updateConsumables( + deepEqual([new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 0 })]), + ), + ).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const consumable = new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 100 }); + const command = new ResetConsumableCommand({ + deviceId: new ID(1), + consumable, + }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.ts b/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.ts new file mode 100644 index 00000000..77b4ea6f --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/reset-consumable.command-handler.ts @@ -0,0 +1,70 @@ +import { DeviceConsumable, DeviceConsumableType } from '@agnoc/domain'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { + CommandHandler, + ResetConsumableCommand, + DeviceRepository, + ConnectionWithDevice, + Device, +} from '@agnoc/domain'; + +export class ResetConsumableCommandHandler implements CommandHandler { + readonly forName = 'ResetConsumableCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: ResetConsumableCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + await this.resetConsumable(connection, event); + await this.updateDeviceConsumables(connection, event); + } + + private async resetConsumable( + connection: PacketConnection & ConnectionWithDevice, + event: ResetConsumableCommand, + ) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_CONSUMABLES_PARAM_REQ', { + itemId: DeviceConsumableItemId[event.consumable.type], + }); + + response.assertPayloadName('DEVICE_MAPID_SET_CONSUMABLES_PARAM_RSP'); + } + + private async updateDeviceConsumables( + connection: PacketConnection & ConnectionWithDevice, + event: ResetConsumableCommand, + ) { + const resetConsumable = new DeviceConsumable({ + type: event.consumable.type, + hoursUsed: 0, + }); + const consumables = connection.device.consumables?.map((consumable) => { + if (consumable.type === event.consumable.type) { + return resetConsumable; + } + + return consumable; + }); + + connection.device.updateConsumables(consumables ?? [resetConsumable]); + + await this.deviceRepository.saveOne(connection.device); + } +} + +const DeviceConsumableItemId = { + [DeviceConsumableType.MainBrush]: 1, + [DeviceConsumableType.SideBrush]: 2, + [DeviceConsumableType.Filter]: 3, + [DeviceConsumableType.Dishcloth]: 4, +}; diff --git a/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.test.ts b/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.test.ts new file mode 100644 index 00000000..b484fdf2 --- /dev/null +++ b/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.test.ts @@ -0,0 +1,98 @@ +import { DeviceConsumable, DeviceConsumableType, GetDeviceConsumablesQuery } from '@agnoc/domain'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { Payload, OPCode, Packet } from '@agnoc/transport-tcp'; +import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { GetDeviceConsumablesQueryHandler } from './get-device-consumables.query-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('GetDeviceConsumablesQueryHandler', function () { + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let queryHandler: GetDeviceConsumablesQueryHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + deviceRepository = imock(); + packetConnectionFinderService = imock(); + queryHandler = new GetDeviceConsumablesQueryHandler( + instance(packetConnectionFinderService), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(queryHandler.forName).to.be.equal('GetDeviceConsumablesQuery'); + }); + + describe('#handle()', function () { + it('should throw an error when no connection is found', async function () { + const query = new GetDeviceConsumablesQuery({ deviceId: new ID(1) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await expect(queryHandler.handle(query)).to.be.rejectedWith( + DomainException, + 'Unable to find a device connected with id 1', + ); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + }); + + it('should get consumables', async function () { + const query = new GetDeviceConsumablesQuery({ deviceId: new ID(1) }); + const payload = new Payload({ + opcode: OPCode.fromName('DEVICE_MAPID_GET_CONSUMABLES_PARAM_REQ'), + data: { + mainBrushTime: 1, + sideBrushTime: 2, + filterTime: 3, + dishclothTime: 4, + }, + }); + const packet = new Packet({ ...givenSomePacketProps(), payload }); + const consumables = [ + new DeviceConsumable({ + type: DeviceConsumableType.MainBrush, + hoursUsed: 1, + }), + new DeviceConsumable({ + type: DeviceConsumableType.SideBrush, + hoursUsed: 2, + }), + new DeviceConsumable({ + type: DeviceConsumableType.Filter, + hoursUsed: 3, + }), + new DeviceConsumable({ + type: DeviceConsumableType.Dishcloth, + hoursUsed: 4, + }), + ]; + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetMessage.packet).thenReturn(packet); + when(packetConnection.device).thenReturn(instance(device)); + + const ret = await queryHandler.handle(query); + + expect(ret.consumables).to.be.deep.equal(consumables); + + verify(packetConnection.sendAndWait('DEVICE_MAPID_GET_CONSUMABLES_PARAM_REQ', deepEqual({}))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_GET_CONSUMABLES_PARAM_RSP')).once(); + verify(device.updateConsumables(deepEqual(consumables))).once(); + verify(deviceRepository.saveOne(anything())).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.ts b/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.ts new file mode 100644 index 00000000..aaa2117c --- /dev/null +++ b/packages/adapter-tcp/src/query-handlers/get-device-consumables.query-handler.ts @@ -0,0 +1,72 @@ +import { DeviceConsumable, DeviceConsumableType } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { + GetDeviceConsumablesQuery, + GetDeviceConsumablesQueryOutput, + QueryHandler, + DeviceRepository, + ConnectionWithDevice, + Device, +} from '@agnoc/domain'; + +export class GetDeviceConsumablesQueryHandler implements QueryHandler { + readonly forName = 'GetDeviceConsumablesQuery'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: GetDeviceConsumablesQuery): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + throw new DomainException(`Unable to find a device connected with id ${event.deviceId.value}`); + } + + const consumables = await this.getConsumables(connection); + + await this.updateDeviceConsumables(connection, consumables); + + return { consumables }; + } + + private async getConsumables(connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_GET_CONSUMABLES_PARAM_REQ', {}); + + response.assertPayloadName('DEVICE_MAPID_GET_CONSUMABLES_PARAM_RSP'); + + const data = response.packet.payload.data; + const consumables = [ + new DeviceConsumable({ + type: DeviceConsumableType.MainBrush, + hoursUsed: data.mainBrushTime, + }), + new DeviceConsumable({ + type: DeviceConsumableType.SideBrush, + hoursUsed: data.sideBrushTime, + }), + new DeviceConsumable({ + type: DeviceConsumableType.Filter, + hoursUsed: data.filterTime, + }), + new DeviceConsumable({ + type: DeviceConsumableType.Dishcloth, + hoursUsed: data.dishclothTime, + }), + ]; + return consumables; + } + + private async updateDeviceConsumables( + connection: PacketConnection & ConnectionWithDevice, + consumables: DeviceConsumable[], + ) { + connection.device.updateConsumables(consumables); + + await this.deviceRepository.saveOne(connection.device); + } +} diff --git a/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts b/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts index 10ff36f4..00aa4f6f 100644 --- a/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts +++ b/packages/adapter-tcp/src/services/packet-event-publisher.service.test.ts @@ -1,8 +1,6 @@ -import { DomainException } from '@agnoc/toolkit'; import { Packet } from '@agnoc/transport-tcp'; import { givenSomePacketProps } from '@agnoc/transport-tcp/test-support'; import { imock, instance, anything, verify, when } from '@johanblumenberg/ts-mockito'; -import { expect } from 'chai'; import { PacketMessage } from '../objects/packet.message'; import { PacketEventPublisherService } from './packet-event-publisher.service'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; @@ -33,19 +31,17 @@ describe('PacketEventPublisherService', function () { verify(packetEventBus.emit(packet.payload.opcode.name, packetMessage)).once(); }); - it('should throw an error when there is no handlers for the packet', async function () { + it('should do nothing when there is no handlers for the packet', async function () { const packet = new Packet(givenSomePacketProps()); const packetMessage = new PacketMessage(instance(packetConnection), packet); when(packetEventBus.listenerCount(anything())).thenReturn(0); - await expect(service.publishPacketMessage(packetMessage)).to.be.rejectedWith( - DomainException, - `No event handler found for packet event 'DEVICE_GETTIME_RSP'`, - ); + await service.publishPacketMessage(packetMessage); verify(packetEventBus.listenerCount(packet.payload.opcode.name)).once(); - verify(packetEventBus.emit(anything(), anything())).never(); + verify(packetEventBus.emit(packet.sequence.toString(), packetMessage)).once(); + verify(packetEventBus.emit(packet.payload.opcode.name, packetMessage)).never(); }); }); }); diff --git a/packages/adapter-tcp/src/services/packet-event-publisher.service.ts b/packages/adapter-tcp/src/services/packet-event-publisher.service.ts index 1dab811c..b721685e 100644 --- a/packages/adapter-tcp/src/services/packet-event-publisher.service.ts +++ b/packages/adapter-tcp/src/services/packet-event-publisher.service.ts @@ -1,31 +1,26 @@ -import { DomainException } from '@agnoc/toolkit'; +import { debug } from '@agnoc/toolkit'; import type { PacketEventBus, PacketEventBusEvents } from '../event-buses/packet.event-bus'; import type { PacketMessage } from '../objects/packet.message'; import type { PayloadDataName } from '@agnoc/transport-tcp'; export class PacketEventPublisherService { + private readonly debug = debug(__filename); + constructor(private readonly packetEventBus: PacketEventBus) {} async publishPacketMessage(packetMessage: PacketMessage): Promise { const name = packetMessage.packet.payload.opcode.name as PayloadDataName; const sequence = packetMessage.packet.sequence.toString(); - this.checkForPacketEventHandler(name); - // Emit the packet event by the sequence string. // This is used to wait for a response from a packet. await this.packetEventBus.emit(sequence, packetMessage as PacketEventBusEvents[PayloadDataName]); // Emit the packet event by the opcode name. - await this.packetEventBus.emit(name, packetMessage as PacketEventBusEvents[PayloadDataName]); - } - - private checkForPacketEventHandler(event: PayloadDataName) { - const count = this.packetEventBus.listenerCount(event); - - // Throw an error if there is no event handler for the packet event. - if (count === 0) { - throw new DomainException(`No event handler found for packet event '${event}'`); + if (this.packetEventBus.listenerCount(name) !== 0) { + await this.packetEventBus.emit(name, packetMessage as PacketEventBusEvents[PayloadDataName]); + } else { + this.debug(`unhandled packet event '${name}': ${packetMessage.packet.toString()}`); } } } diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index ea0eec8f..6e07de61 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -9,7 +9,14 @@ import { PacketFactory, } from '@agnoc/transport-tcp'; import { LocateDeviceCommandHandler } from './command-handlers/locate-device.command-handler'; +import { PauseCleaningCommandHandler } from './command-handlers/pause-cleaning.command-handler'; +import { ResetConsumableCommandHandler } from './command-handlers/reset-consumable.command-handler'; +import { ReturnHomeCommandHandler } from './command-handlers/return-home.command-handler'; +import { SetCarpetModeCommandHandler } from './command-handlers/set-carpet-mode.command-handler'; +import { SetDeviceQuietHoursCommandHandler } from './command-handlers/set-device-quiet-hours.command-handler'; +import { SetDeviceVoiceCommandHandler } from './command-handlers/set-device-voice.command-handler'; import { StartCleaningCommandHandler } from './command-handlers/start-cleaning.command-handler'; +import { StopCleaningCommandHandler } from './command-handlers/stop-cleaning.command-handler'; import { NTPServerConnectionHandler } from './connection-handlers/ntp-server.connection-handler'; import { PackerServerConnectionHandler } from './connection-handlers/packet-server.connection-handler'; import { LockDeviceWhenDeviceIsConnectedEventHandler } from './domain-event-handlers/lock-device-when-device-is-connected-event-handler.event-handler'; @@ -48,6 +55,7 @@ import { DeviceSettingsUpdateEventHandler } from './packet-event-handlers/device import { DeviceTimeUpdateEventHandler } from './packet-event-handlers/device-time-update.event-handler'; import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-upgrade-info.event-handler'; import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; +import { GetDeviceConsumablesQueryHandler } from './query-handlers/get-device-consumables.query-handler'; import { ConnectionDeviceUpdaterService } from './services/connection-device-updater.service'; import { DeviceModeChangerService } from './services/device-mode-changer.service'; import { PacketConnectionFinderService } from './services/packet-connection-finder.service'; @@ -80,7 +88,7 @@ export class TCPServer implements Server { // Mappers const deviceFanSpeedMapper = new DeviceFanSpeedMapper(); const deviceWaterLevelMapper = new DeviceWaterLevelMapper(); - const deviceVoiceMapper = new VoiceSettingMapper(); + const voiceSettingMapper = new VoiceSettingMapper(); const deviceStateMapper = new DeviceStateMapper(); const deviceModeMapper = new DeviceModeMapper(); const deviceErrorMapper = new DeviceErrorMapper(); @@ -149,7 +157,7 @@ export class TCPServer implements Server { new DeviceOfflineEventHandler(), new DeviceOrderListUpdateEventHandler(deviceOrderMapper, this.deviceRepository), new DeviceRegisterEventHandler(this.deviceRepository), - new DeviceSettingsUpdateEventHandler(deviceVoiceMapper, this.deviceRepository), + new DeviceSettingsUpdateEventHandler(voiceSettingMapper, this.deviceRepository), new DeviceTimeUpdateEventHandler(), new DeviceUpgradeInfoEventHandler(), new DeviceVersionUpdateEventHandler(this.deviceRepository), @@ -173,8 +181,16 @@ export class TCPServer implements Server { // Command event handlers this.commandQueryHandlerRegistry.register( + new GetDeviceConsumablesQueryHandler(packetConnectionFinderService, this.deviceRepository), new LocateDeviceCommandHandler(packetConnectionFinderService), + new PauseCleaningCommandHandler(packetConnectionFinderService), + new ResetConsumableCommandHandler(packetConnectionFinderService, this.deviceRepository), + new ReturnHomeCommandHandler(packetConnectionFinderService), + new SetCarpetModeCommandHandler(packetConnectionFinderService, this.deviceRepository), + new SetDeviceQuietHoursCommandHandler(packetConnectionFinderService), + new SetDeviceVoiceCommandHandler(packetConnectionFinderService, voiceSettingMapper, this.deviceRepository), new StartCleaningCommandHandler(packetConnectionFinderService, deviceModeChangerService), + new StopCleaningCommandHandler(packetConnectionFinderService), ); } diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts index 43e1f10d..507d18ce 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.test.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceConsumablesChangedDomainEvent } from '../domain-events/device-consumables-changed.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; @@ -460,12 +461,20 @@ describe('Device', function () { describe('#updateConsumables()', function () { it('should update the device consumables', function () { - const device = new Device(givenSomeDeviceProps()); - const consumables = [new DeviceConsumable(givenSomeDeviceConsumableProps())]; + const previousConsumables = [new DeviceConsumable(givenSomeDeviceConsumableProps())]; + const currentConsumables = [new DeviceConsumable(givenSomeDeviceConsumableProps())]; + const device = new Device({ ...givenSomeDeviceProps(), consumables: previousConsumables }); - device.updateConsumables(consumables); + device.updateConsumables(currentConsumables); - expect(device.consumables).to.be.equal(consumables); + expect(device.consumables).to.be.equal(currentConsumables); + + const event = device.domainEvents[1] as DeviceConsumablesChangedDomainEvent; + + expect(event).to.be.instanceOf(DeviceConsumablesChangedDomainEvent); + expect(event.aggregateId).to.equal(device.id); + expect(event.previousConsumables).to.be.equal(previousConsumables); + expect(event.currentConsumables).to.be.equal(currentConsumables); }); }); diff --git a/packages/domain/src/aggregate-roots/device.aggregate-root.ts b/packages/domain/src/aggregate-roots/device.aggregate-root.ts index 66f3cadb..66621cb3 100644 --- a/packages/domain/src/aggregate-roots/device.aggregate-root.ts +++ b/packages/domain/src/aggregate-roots/device.aggregate-root.ts @@ -2,6 +2,7 @@ import { AggregateRoot, ID, symmetricDifference } from '@agnoc/toolkit'; import { DeviceBatteryChangedDomainEvent } from '../domain-events/device-battery-changed.domain-event'; import { DeviceCleanWorkChangedDomainEvent } from '../domain-events/device-clean-work-changed.domain-event'; import { DeviceConnectedDomainEvent } from '../domain-events/device-connected.domain-event'; +import { DeviceConsumablesChangedDomainEvent } from '../domain-events/device-consumables-changed.domain-event'; import { DeviceCreatedDomainEvent } from '../domain-events/device-created.domain-event'; import { DeviceErrorChangedDomainEvent } from '../domain-events/device-error-changed.domain-event'; import { DeviceFanSpeedChangedDomainEvent } from '../domain-events/device-fan-speed-changed.domain-event'; @@ -273,8 +274,17 @@ export class Device extends AggregateRoot { } /** Updates the device consumables. */ - updateConsumables(consumables?: DeviceConsumable[]): void { + updateConsumables(consumables: DeviceConsumable[]): void { + // TODO: should we check if the consumables are changed? + this.validateDefinedProp({ consumables }, 'consumables'); this.validateArrayProp({ consumables }, 'consumables', DeviceConsumable); + this.addEvent( + new DeviceConsumablesChangedDomainEvent({ + aggregateId: this.id, + previousConsumables: this.consumables, + currentConsumables: consumables, + }), + ); this.props.consumables = consumables; } diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index 17c295aa..ec82e737 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,5 +1,6 @@ import type { LocateDeviceCommand } from './locate-device.command'; import type { PauseCleaningCommand } from './pause-cleaning.command'; +import type { ResetConsumableCommand } from './reset-consumable.command'; import type { ReturnHomeCommand } from './return-home.command'; import type { SetCarpetModeCommand } from './set-carpet-mode.command'; import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; @@ -11,6 +12,7 @@ import type { StopCleaningCommand } from './stop-cleaning.command'; export type Commands = { LocateDeviceCommand: LocateDeviceCommand; PauseCleaningCommand: PauseCleaningCommand; + ResetConsumableCommand: ResetConsumableCommand; ReturnHomeCommand: ReturnHomeCommand; SetCarpetModeCommand: SetCarpetModeCommand; SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; diff --git a/packages/domain/src/commands/reset-consumable.command.test.ts b/packages/domain/src/commands/reset-consumable.command.test.ts new file mode 100644 index 00000000..a752cf30 --- /dev/null +++ b/packages/domain/src/commands/reset-consumable.command.test.ts @@ -0,0 +1,54 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceConsumable, DeviceConsumableType } from '../value-objects/device-consumable.value-object'; +import { ResetConsumableCommand } from './reset-consumable.command'; +import type { ResetConsumableCommandInput } from './reset-consumable.command'; + +describe('ResetConsumableCommand', function () { + it('should be created', function () { + const input = givenAResetConsumableCommandInput(); + const command = new ResetConsumableCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.consumable).to.be.equal(input.consumable); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new ResetConsumableCommand({ ...givenAResetConsumableCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for ResetConsumableCommand not provided`, + ); + }); + + it("should throw an error when 'consumable' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new ResetConsumableCommand({ ...givenAResetConsumableCommandInput(), consumable: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'consumable' for ResetConsumableCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new ResetConsumableCommand({ ...givenAResetConsumableCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of ResetConsumableCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'consumable' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new ResetConsumableCommand({ ...givenAResetConsumableCommandInput(), consumable: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'consumable' of ResetConsumableCommand is not an instance of DeviceConsumable`, + ); + }); +}); + +function givenAResetConsumableCommandInput(): ResetConsumableCommandInput { + return { + deviceId: ID.generate(), + consumable: new DeviceConsumable({ type: DeviceConsumableType.MainBrush, hoursUsed: 5 }), + }; +} diff --git a/packages/domain/src/commands/reset-consumable.command.ts b/packages/domain/src/commands/reset-consumable.command.ts new file mode 100644 index 00000000..d1ed4c50 --- /dev/null +++ b/packages/domain/src/commands/reset-consumable.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; + +/** Input for the command resetting a consumable. */ +export interface ResetConsumableCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Consumable to reset. */ + consumable: DeviceConsumable; +} + +/** Command that locates a device. */ +export class ResetConsumableCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the consumable to reset. */ + get consumable(): DeviceConsumable { + return this.props.consumable; + } + + protected validate(props: ResetConsumableCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'consumable'); + this.validateInstanceProp(props, 'consumable', DeviceConsumable); + } +} diff --git a/packages/domain/src/domain-events/device-consumables-changed.domain-event.test.ts b/packages/domain/src/domain-events/device-consumables-changed.domain-event.test.ts new file mode 100644 index 00000000..2a066aa2 --- /dev/null +++ b/packages/domain/src/domain-events/device-consumables-changed.domain-event.test.ts @@ -0,0 +1,106 @@ +import { DomainEvent, ArgumentNotProvidedException, ArgumentInvalidException, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceConsumableProps } from '../test-support'; +import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; +import { DeviceConsumablesChangedDomainEvent } from './device-consumables-changed.domain-event'; +import type { DeviceConsumablesChangedDomainEventProps } from './device-consumables-changed.domain-event'; + +describe('DeviceConsumablesChangedDomainEvent', function () { + it('should be created', function () { + const props = givenSomeDeviceConsumablesChangedDomainEventProps(); + const event = new DeviceConsumablesChangedDomainEvent(props); + + expect(event).to.be.instanceOf(DomainEvent); + expect(event.aggregateId).to.be.equal(props.aggregateId); + expect(event.previousConsumables).to.be.undefined; + expect(event.currentConsumables).to.be.equal(props.currentConsumables); + }); + + it("should be created with 'previousConsumables'", function () { + const props = { + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + previousConsumables: [new DeviceConsumable(givenSomeDeviceConsumableProps())], + }; + const event = new DeviceConsumablesChangedDomainEvent(props); + + expect(event.previousConsumables).to.be.equal(props.previousConsumables); + }); + + it("should thrown an error when 'currentConsumables' is not provided", function () { + expect( + () => + new DeviceConsumablesChangedDomainEvent({ + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + // @ts-expect-error - missing property + currentConsumables: undefined, + }), + ).to.throw( + ArgumentNotProvidedException, + `Property 'currentConsumables' for DeviceConsumablesChangedDomainEvent not provided`, + ); + }); + + it("should thrown an error when 'previousConsumables' is not an array", function () { + expect( + () => + new DeviceConsumablesChangedDomainEvent({ + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousConsumables: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousConsumables' of DeviceConsumablesChangedDomainEvent is not an array", + ); + }); + + it("should thrown an error when 'previousConsumables' is not an array of DeviceConsumable", function () { + expect( + () => + new DeviceConsumablesChangedDomainEvent({ + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + // @ts-expect-error - invalid property + previousConsumables: ['foo'], + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'previousConsumables' of DeviceConsumablesChangedDomainEvent is not an array of DeviceConsumable", + ); + }); + + it("should thrown an error when 'currentConsumables' is not an array", function () { + expect( + () => + new DeviceConsumablesChangedDomainEvent({ + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentConsumables: 'foo', + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentConsumables' of DeviceConsumablesChangedDomainEvent is not an array", + ); + }); + + it("should thrown an error when 'currentConsumables' is not an array of DeviceConsumable", function () { + expect( + () => + new DeviceConsumablesChangedDomainEvent({ + ...givenSomeDeviceConsumablesChangedDomainEventProps(), + // @ts-expect-error - invalid property + currentConsumables: ['foo'], + }), + ).to.throw( + ArgumentInvalidException, + "Value 'foo' for property 'currentConsumables' of DeviceConsumablesChangedDomainEvent is not an array of DeviceConsumable", + ); + }); +}); + +function givenSomeDeviceConsumablesChangedDomainEventProps(): DeviceConsumablesChangedDomainEventProps { + return { + aggregateId: ID.generate(), + previousConsumables: undefined, + currentConsumables: [new DeviceConsumable(givenSomeDeviceConsumableProps())], + }; +} diff --git a/packages/domain/src/domain-events/device-consumables-changed.domain-event.ts b/packages/domain/src/domain-events/device-consumables-changed.domain-event.ts new file mode 100644 index 00000000..be7a3f87 --- /dev/null +++ b/packages/domain/src/domain-events/device-consumables-changed.domain-event.ts @@ -0,0 +1,27 @@ +import { DomainEvent } from '@agnoc/toolkit'; +import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; +import type { DomainEventProps } from '@agnoc/toolkit'; + +export interface DeviceConsumablesChangedDomainEventProps extends DomainEventProps { + previousConsumables?: DeviceConsumable[]; + currentConsumables: DeviceConsumable[]; +} + +export class DeviceConsumablesChangedDomainEvent extends DomainEvent { + get previousConsumables(): DeviceConsumable[] | undefined { + return this.props.previousConsumables; + } + + get currentConsumables(): DeviceConsumable[] { + return this.props.currentConsumables; + } + + protected validate(props: DeviceConsumablesChangedDomainEventProps): void { + if (props.previousConsumables) { + this.validateArrayProp(props, 'previousConsumables', DeviceConsumable); + } + + this.validateDefinedProp(props, 'currentConsumables'); + this.validateArrayProp(props, 'currentConsumables', DeviceConsumable); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 7c6582d8..6552327d 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -3,6 +3,7 @@ export * from './aggregate-roots/device.aggregate-root'; export * from './commands/commands'; export * from './commands/locate-device.command'; export * from './commands/pause-cleaning.command'; +export * from './commands/reset-consumable.command'; export * from './commands/return-home.command'; export * from './commands/set-carpet-mode.command'; export * from './commands/set-device-quiet-hours.command'; @@ -47,6 +48,7 @@ export * from './event-handlers/command.task-handler'; export * from './event-handlers/domain.event-handler'; export * from './event-handlers/query.task-handler'; export * from './queries/find-device.query'; +export * from './queries/get-device-consumables.query'; export * from './queries/queries'; export * from './repositories/connection.repository'; export * from './repositories/device.repository'; diff --git a/packages/domain/src/queries/find-device-query.test.ts b/packages/domain/src/queries/find-device.query.test.ts similarity index 100% rename from packages/domain/src/queries/find-device-query.test.ts rename to packages/domain/src/queries/find-device.query.test.ts diff --git a/packages/domain/src/queries/get-device-consumables.query.test.ts b/packages/domain/src/queries/get-device-consumables.query.test.ts new file mode 100644 index 00000000..b0887271 --- /dev/null +++ b/packages/domain/src/queries/get-device-consumables.query.test.ts @@ -0,0 +1,71 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, ID, Query } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeDeviceConsumableProps } from '../test-support'; +import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; +import { GetDeviceConsumablesQuery } from './get-device-consumables.query'; +import type { GetDeviceConsumablesQueryInput, GetDeviceConsumablesQueryOutput } from './get-device-consumables.query'; + +describe('GetDeviceConsumablesQuery', function () { + it('should be created', function () { + const input = givenAGetDeviceConsumablesQueryInput(); + const query = new GetDeviceConsumablesQuery(input); + + expect(query).to.be.instanceOf(Query); + expect(query.deviceId).to.be.equal(input.deviceId); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new GetDeviceConsumablesQuery({ ...givenAGetDeviceConsumablesQueryInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for GetDeviceConsumablesQuery not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new GetDeviceConsumablesQuery({ ...givenAGetDeviceConsumablesQueryInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of GetDeviceConsumablesQuery is not an instance of ID`, + ); + }); + + it("should throw an error when 'consumables' is not provided", function () { + const query = new GetDeviceConsumablesQuery(givenAGetDeviceConsumablesQueryInput()); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAGetDeviceConsumablesQueryOutput(), consumables: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'consumables' for GetDeviceConsumablesQuery not provided`, + ); + }); + + it("should throw an error when 'consumables' is not an array", function () { + const query = new GetDeviceConsumablesQuery(givenAGetDeviceConsumablesQueryInput()); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAGetDeviceConsumablesQueryOutput(), consumables: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'consumables' of GetDeviceConsumablesQuery is not an array`, + ); + }); + + it("should throw an error when 'consumables' is not an array of DeviceConsumable", function () { + const query = new GetDeviceConsumablesQuery(givenAGetDeviceConsumablesQueryInput()); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAGetDeviceConsumablesQueryOutput(), consumables: ['foo'] })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'consumables' of GetDeviceConsumablesQuery is not an array of DeviceConsumable`, + ); + }); +}); + +function givenAGetDeviceConsumablesQueryInput(): GetDeviceConsumablesQueryInput { + return { deviceId: ID.generate() }; +} + +function givenAGetDeviceConsumablesQueryOutput(): GetDeviceConsumablesQueryOutput { + return { consumables: [new DeviceConsumable(givenSomeDeviceConsumableProps())] }; +} diff --git a/packages/domain/src/queries/get-device-consumables.query.ts b/packages/domain/src/queries/get-device-consumables.query.ts new file mode 100644 index 00000000..6c70d696 --- /dev/null +++ b/packages/domain/src/queries/get-device-consumables.query.ts @@ -0,0 +1,26 @@ +import { ID, Query } from '@agnoc/toolkit'; +import { DeviceConsumable } from '../value-objects/device-consumable.value-object'; + +export interface GetDeviceConsumablesQueryInput { + deviceId: ID; +} + +export interface GetDeviceConsumablesQueryOutput { + consumables: DeviceConsumable[]; +} + +export class GetDeviceConsumablesQuery extends Query { + get deviceId(): ID { + return this.props.deviceId; + } + + protected validate(props: GetDeviceConsumablesQueryInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + } + + override validateOutput(output: GetDeviceConsumablesQueryOutput): void { + this.validateDefinedProp(output, 'consumables'); + this.validateArrayProp(output, 'consumables', DeviceConsumable); + } +} diff --git a/packages/domain/src/queries/queries.ts b/packages/domain/src/queries/queries.ts index 65bf1406..0a93b0a6 100644 --- a/packages/domain/src/queries/queries.ts +++ b/packages/domain/src/queries/queries.ts @@ -1,7 +1,9 @@ import type { FindDeviceQuery } from './find-device.query'; +import type { GetDeviceConsumablesQuery } from './get-device-consumables.query'; export type Queries = { FindDeviceQuery: FindDeviceQuery; + GetDeviceConsumablesQuery: GetDeviceConsumablesQuery; }; export type QueryNames = keyof Queries; diff --git a/packages/domain/src/test-support.ts b/packages/domain/src/test-support.ts index 686068c3..11cb6cd3 100644 --- a/packages/domain/src/test-support.ts +++ b/packages/domain/src/test-support.ts @@ -75,7 +75,7 @@ export function givenSomeDeviceCleanWorkProps(): DeviceCleanWorkProps { export function givenSomeDeviceConsumableProps(): DeviceConsumableProps { return { type: DeviceConsumableType.MainBrush, - minutesUsed: 1, + hoursUsed: 1, }; } diff --git a/packages/domain/src/value-objects/device-consumable.value-object.test.ts b/packages/domain/src/value-objects/device-consumable.value-object.test.ts index d7a4ce23..91ee3ff9 100644 --- a/packages/domain/src/value-objects/device-consumable.value-object.test.ts +++ b/packages/domain/src/value-objects/device-consumable.value-object.test.ts @@ -9,7 +9,7 @@ describe('DeviceConsumable', function () { const deviceConsumable = new DeviceConsumable(deviceConsumableProps); expect(deviceConsumable.type).to.be.equal(deviceConsumableProps.type); - expect(deviceConsumable.minutesUsed).to.be.equal(deviceConsumableProps.minutesUsed); + expect(deviceConsumable.hoursUsed).to.be.equal(deviceConsumableProps.hoursUsed); }); it("should throw an error when 'type' property is not provided", function () { @@ -28,19 +28,19 @@ describe('DeviceConsumable', function () { ); }); - it("should throw an error when 'minutesUsed' property is not provided", function () { + it("should throw an error when 'hoursUsed' property is not provided", function () { // @ts-expect-error - missing property - expect(() => new DeviceConsumable({ ...givenSomeDeviceConsumableProps(), minutesUsed: undefined })).to.throw( + expect(() => new DeviceConsumable({ ...givenSomeDeviceConsumableProps(), hoursUsed: undefined })).to.throw( ArgumentNotProvidedException, - `Property 'minutesUsed' for DeviceConsumable not provided`, + `Property 'hoursUsed' for DeviceConsumable not provided`, ); }); - it("should throw an error when 'minutesUsed' property is invalid", function () { + it("should throw an error when 'hoursUsed' property is invalid", function () { // @ts-expect-error - invalid property - expect(() => new DeviceConsumable({ ...givenSomeDeviceConsumableProps(), minutesUsed: 'foo' })).to.throw( + expect(() => new DeviceConsumable({ ...givenSomeDeviceConsumableProps(), hoursUsed: 'foo' })).to.throw( ArgumentInvalidException, - `Value 'foo' for property 'minutesUsed' of DeviceConsumable is not a number`, + `Value 'foo' for property 'hoursUsed' of DeviceConsumable is not a number`, ); }); }); diff --git a/packages/domain/src/value-objects/device-consumable.value-object.ts b/packages/domain/src/value-objects/device-consumable.value-object.ts index 6c44d9b5..bfd7e6d2 100644 --- a/packages/domain/src/value-objects/device-consumable.value-object.ts +++ b/packages/domain/src/value-objects/device-consumable.value-object.ts @@ -13,7 +13,7 @@ export interface DeviceConsumableProps { /** The type of the consumable. */ type: DeviceConsumableType; /** The minutes used of the consumable. */ - minutesUsed: number; + hoursUsed: number; } /** Describe a device consumable. */ @@ -23,17 +23,17 @@ export class DeviceConsumable extends ValueObject { return this.props.type; } - /** Returns the minutes used of the consumable. */ - get minutesUsed(): number { - return this.props.minutesUsed; + /** Returns the hours used of the consumable. */ + get hoursUsed(): number { + return this.props.hoursUsed; } protected validate(props: DeviceConsumableProps): void { - const keys: (keyof DeviceConsumableProps)[] = ['type', 'minutesUsed']; + const keys: (keyof DeviceConsumableProps)[] = ['type', 'hoursUsed']; keys.forEach((prop) => this.validateDefinedProp(props, prop)); this.validateListProp(props, 'type', Object.values(DeviceConsumableType)); - this.validateNumberProp(props, 'minutesUsed'); + this.validateNumberProp(props, 'hoursUsed'); } } diff --git a/packages/toolkit/src/base-classes/aggregate-root.base.ts b/packages/toolkit/src/base-classes/aggregate-root.base.ts index 1c3ae381..35b5a9e3 100644 --- a/packages/toolkit/src/base-classes/aggregate-root.base.ts +++ b/packages/toolkit/src/base-classes/aggregate-root.base.ts @@ -5,18 +5,22 @@ import type { DomainEvent } from './domain-event.base'; import type { EntityProps } from './entity.base'; import type { EventBus } from './event-bus.base'; +/** Base class for aggregate roots. */ export abstract class AggregateRoot extends Entity { private readonly debug = debug(__filename).extend(toDashCase(this.constructor.name)).extend(this.id.toString()); readonly #domainEvents = new Set(); + /** Returns the domain events that have been added to the aggregate root. */ get domainEvents(): DomainEvent[] { return [...this.#domainEvents.values()]; } + /** Clears the domain events that have been added to the aggregate root. */ clearEvents(): void { this.#domainEvents.clear(); } + /** Publishes the domain events that have been added to the aggregate root and clears them. */ async publishEvents(eventBus: EventBus): Promise { const domainEvents = this.domainEvents; @@ -33,7 +37,6 @@ export abstract class AggregateRoot extends } protected addEvent(domainEvent: DomainEvent): void { - this.debug(`adding domain event '${domainEvent.constructor.name}' with data: ${JSON.stringify(domainEvent)}`); this.#domainEvents.add(domainEvent); } } diff --git a/packages/toolkit/src/base-classes/task-bus.base.ts b/packages/toolkit/src/base-classes/task-bus.base.ts index 315d4963..b9b5eaae 100644 --- a/packages/toolkit/src/base-classes/task-bus.base.ts +++ b/packages/toolkit/src/base-classes/task-bus.base.ts @@ -3,18 +3,22 @@ import { debug } from '../utils/debug.util'; import { toDashCase } from '../utils/to-dash-case.util'; import type { Task, TaskOutput } from './task.base'; +/** Task handler signature. */ export type TaskBusSubscribeHandler = ( task: Events[Name], ) => TaskOutput | Promise>; +/** Task bus tasks signature. */ export type TaskBusTasks = { [key: string]: Task; }; +/** Base class for task buses. */ export abstract class TaskBus { private readonly debug = debug(__filename).extend(toDashCase(this.constructor.name)); private readonly handlers = new Map>>(); + /** Subscribes a task handler to a task. */ subscribe(name: Name, handler: TaskBusSubscribeHandler): void { this.debug(`subscribing to task '${name.toString()}'`); @@ -25,10 +29,11 @@ export abstract class TaskBus { this.handlers.get(name)?.add(handler as TaskBusSubscribeHandler); } + /** Triggers a task handler and returns the output. */ async trigger(task: Task): Promise> { const name = task.constructor.name as keyof Tasks & string; - this.debug(`triggering task '${name}'`); + this.debug(`triggering task '${name}' with input: ${JSON.stringify(task)}`); const handlers = this.handlers.get(name as keyof Tasks); From c458a2b0ae272a1cf16303b61a0684277629b183 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Sat, 1 Apr 2023 13:04:24 +0200 Subject: [PATCH 32/38] feat: add set fan speed command --- .../set-fan-speed.command-handler.test.ts | 71 +++++++++++++++++++ .../set-fan-speed.command-handler.ts | 40 +++++++++++ packages/adapter-tcp/src/tcp.server.ts | 2 + packages/domain/src/commands/commands.ts | 2 + .../commands/set-fan-speed.command.test.ts | 54 ++++++++++++++ .../src/commands/set-fan-speed.command.ts | 30 ++++++++ packages/domain/src/index.ts | 1 + 7 files changed, 200 insertions(+) create mode 100644 packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.ts create mode 100644 packages/domain/src/commands/set-fan-speed.command.test.ts create mode 100644 packages/domain/src/commands/set-fan-speed.command.ts diff --git a/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.test.ts new file mode 100644 index 00000000..463a7c0e --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.test.ts @@ -0,0 +1,71 @@ +import { DeviceFanSpeed, DeviceFanSpeedValue, SetFanSpeedCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetFanSpeedCommandHandler } from './set-fan-speed.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('SetFanSpeedCommandHandler', function () { + let deviceFanSpeedMapper: DeviceFanSpeedMapper; + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetFanSpeedCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + deviceFanSpeedMapper = imock(); + deviceRepository = imock(); + packetConnectionFinderService = imock(); + commandHandler = new SetFanSpeedCommandHandler( + instance(packetConnectionFinderService), + instance(deviceFanSpeedMapper), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetFanSpeedCommand'); + }); + + describe('#handle()', function () { + it('should set the fan speed', async function () { + const fanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const command = new SetFanSpeedCommand({ deviceId: new ID(1), fanSpeed }); + + when(deviceFanSpeedMapper.fromDomain(anything())).thenReturn(1); + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_SET_CLEAN_PREFERENCE_REQ', deepEqual({ mode: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_SET_CLEAN_PREFERENCE_RSP')).once(); + verify(device.updateFanSpeed(fanSpeed)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const fanSpeed = new DeviceFanSpeed(DeviceFanSpeedValue.Low); + const command = new SetFanSpeedCommand({ deviceId: new ID(1), fanSpeed }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.ts new file mode 100644 index 00000000..e2aa3e12 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-fan-speed.command-handler.ts @@ -0,0 +1,40 @@ +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceFanSpeedMapper } from '../mappers/device-fan-speed.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, ConnectionWithDevice, DeviceRepository, SetFanSpeedCommand } from '@agnoc/domain'; + +export class SetFanSpeedCommandHandler implements CommandHandler { + readonly forName = 'SetFanSpeedCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceFanSpeedMapper: DeviceFanSpeedMapper, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: SetFanSpeedCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + await this.setFanSpeed(event, connection); + await this.updateDevice(event, connection); + } + + private async setFanSpeed({ fanSpeed }: SetFanSpeedCommand, connection: PacketConnection & ConnectionWithDevice) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_SET_CLEAN_PREFERENCE_REQ', { + mode: this.deviceFanSpeedMapper.fromDomain(fanSpeed), + }); + + response.assertPayloadName('DEVICE_SET_CLEAN_PREFERENCE_RSP'); + } + + private async updateDevice({ fanSpeed }: SetFanSpeedCommand, connection: PacketConnection & ConnectionWithDevice) { + connection.device.updateFanSpeed(fanSpeed); + + await this.deviceRepository.saveOne(connection.device); + } +} diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 6e07de61..7ce92a1a 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -15,6 +15,7 @@ import { ReturnHomeCommandHandler } from './command-handlers/return-home.command import { SetCarpetModeCommandHandler } from './command-handlers/set-carpet-mode.command-handler'; import { SetDeviceQuietHoursCommandHandler } from './command-handlers/set-device-quiet-hours.command-handler'; import { SetDeviceVoiceCommandHandler } from './command-handlers/set-device-voice.command-handler'; +import { SetFanSpeedCommandHandler } from './command-handlers/set-fan-speed.command-handler'; import { StartCleaningCommandHandler } from './command-handlers/start-cleaning.command-handler'; import { StopCleaningCommandHandler } from './command-handlers/stop-cleaning.command-handler'; import { NTPServerConnectionHandler } from './connection-handlers/ntp-server.connection-handler'; @@ -189,6 +190,7 @@ export class TCPServer implements Server { new SetCarpetModeCommandHandler(packetConnectionFinderService, this.deviceRepository), new SetDeviceQuietHoursCommandHandler(packetConnectionFinderService), new SetDeviceVoiceCommandHandler(packetConnectionFinderService, voiceSettingMapper, this.deviceRepository), + new SetFanSpeedCommandHandler(packetConnectionFinderService, deviceFanSpeedMapper, this.deviceRepository), new StartCleaningCommandHandler(packetConnectionFinderService, deviceModeChangerService), new StopCleaningCommandHandler(packetConnectionFinderService), ); diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index ec82e737..cdef23df 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -5,6 +5,7 @@ import type { ReturnHomeCommand } from './return-home.command'; import type { SetCarpetModeCommand } from './set-carpet-mode.command'; import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; import type { SetDeviceVoiceCommand } from './set-device-voice.command'; +import type { SetFanSpeedCommand } from './set-fan-speed.command'; import type { StartCleaningCommand } from './start-cleaning.command'; import type { StopCleaningCommand } from './stop-cleaning.command'; @@ -17,6 +18,7 @@ export type Commands = { SetCarpetModeCommand: SetCarpetModeCommand; SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; SetDeviceVoiceCommand: SetDeviceVoiceCommand; + SetFanSpeedCommand: SetFanSpeedCommand; StartCleaningCommand: StartCleaningCommand; StopCleaningCommand: StopCleaningCommand; }; diff --git a/packages/domain/src/commands/set-fan-speed.command.test.ts b/packages/domain/src/commands/set-fan-speed.command.test.ts new file mode 100644 index 00000000..dc7d8365 --- /dev/null +++ b/packages/domain/src/commands/set-fan-speed.command.test.ts @@ -0,0 +1,54 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceFanSpeed, DeviceFanSpeedValue } from '../domain-primitives/device-fan-speed.domain-primitive'; +import { SetFanSpeedCommand } from './set-fan-speed.command'; +import type { SetFanSpeedCommandInput } from './set-fan-speed.command'; + +describe('SetFanSpeedCommand', function () { + it('should be created', function () { + const input = givenASetFanSpeedCommandInput(); + const command = new SetFanSpeedCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.fanSpeed).to.be.equal(input.fanSpeed); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetFanSpeedCommand({ ...givenASetFanSpeedCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetFanSpeedCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetFanSpeedCommand({ ...givenASetFanSpeedCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetFanSpeedCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'fanSpeed' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetFanSpeedCommand({ ...givenASetFanSpeedCommandInput(), fanSpeed: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'fanSpeed' for SetFanSpeedCommand not provided`); + }); + + it("should throw an error when 'fanSpeed' is not an DeviceFanSpeed", function () { + expect( + // @ts-expect-error - invalid property + () => new SetFanSpeedCommand({ ...givenASetFanSpeedCommandInput(), fanSpeed: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'fanSpeed' of SetFanSpeedCommand is not an instance of DeviceFanSpeed`, + ); + }); +}); + +function givenASetFanSpeedCommandInput(): SetFanSpeedCommandInput { + return { deviceId: ID.generate(), fanSpeed: new DeviceFanSpeed(DeviceFanSpeedValue.Low) }; +} diff --git a/packages/domain/src/commands/set-fan-speed.command.ts b/packages/domain/src/commands/set-fan-speed.command.ts new file mode 100644 index 00000000..98178039 --- /dev/null +++ b/packages/domain/src/commands/set-fan-speed.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { DeviceFanSpeed } from '../domain-primitives/device-fan-speed.domain-primitive'; + +/** Input for the command stopping the cleaning process of a device. */ +export interface SetFanSpeedCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Fan speed setting. */ + fanSpeed: DeviceFanSpeed; +} + +/** Command for stopping the cleaning process of a device. */ +export class SetFanSpeedCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the fan speed. */ + get fanSpeed(): DeviceFanSpeed { + return this.props.fanSpeed; + } + + protected validate(props: SetFanSpeedCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'fanSpeed'); + this.validateInstanceProp(props, 'fanSpeed', DeviceFanSpeed); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 6552327d..32ed3c5f 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -8,6 +8,7 @@ export * from './commands/return-home.command'; export * from './commands/set-carpet-mode.command'; export * from './commands/set-device-quiet-hours.command'; export * from './commands/set-device-voice.command'; +export * from './commands/set-fan-speed.command'; export * from './commands/start-cleaning.command'; export * from './commands/stop-cleaning.command'; export * from './domain-events/connection-device-changed.domain-event'; From 1dd015984f1e323b2c664cac15f662083edbc643 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Mon, 3 Apr 2023 13:06:30 +0200 Subject: [PATCH 33/38] feat: add more device services --- .../pause-cleaning.command-handler.test.ts | 51 +++----- .../pause-cleaning.command-handler.ts | 48 ++----- .../set-water-level.command-handler.test.ts | 71 +++++++++++ .../set-water-level.command-handler.ts | 46 +++++++ .../start-cleaning.command-handler.test.ts | 110 ++++++---------- .../start-cleaning.command-handler.ts | 95 +++----------- .../stop-cleaning.command-handler.test.ts | 21 ++-- .../stop-cleaning.command-handler.ts | 21 ++-- .../services/device-cleaning.service.test.ts | 113 +++++++++++++++++ .../src/services/device-cleaning.service.ts | 47 +++++++ .../src/services/device-map.service.test.ts | 117 ++++++++++++++++++ .../src/services/device-map.service.ts | 57 +++++++++ .../services/device-mode-changer.service.ts | 10 +- packages/adapter-tcp/src/tcp.server.ts | 17 ++- packages/domain/src/commands/commands.ts | 2 + .../commands/set-water-level.command.test.ts | 54 ++++++++ .../src/commands/set-water-level.command.ts | 30 +++++ packages/domain/src/index.ts | 1 + 18 files changed, 655 insertions(+), 256 deletions(-) create mode 100644 packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.ts create mode 100644 packages/adapter-tcp/src/services/device-cleaning.service.test.ts create mode 100644 packages/adapter-tcp/src/services/device-cleaning.service.ts create mode 100644 packages/adapter-tcp/src/services/device-map.service.test.ts create mode 100644 packages/adapter-tcp/src/services/device-map.service.ts create mode 100644 packages/domain/src/commands/set-water-level.command.test.ts create mode 100644 packages/domain/src/commands/set-water-level.command.ts diff --git a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts index b585200c..7fe4226d 100644 --- a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts +++ b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.test.ts @@ -1,26 +1,30 @@ import { DeviceMap, DeviceMode, DeviceModeValue, MapPosition, PauseCleaningCommand } from '@agnoc/domain'; import { givenSomeDeviceMapProps } from '@agnoc/domain/test-support'; import { DomainException, ID } from '@agnoc/toolkit'; -import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { imock, instance, when, verify, anything } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; import { PauseCleaningCommandHandler } from './pause-cleaning.command-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { ConnectionWithDevice, Device } from '@agnoc/domain'; describe('PauseCleaningCommandHandler', function () { + let deviceCleaningService: DeviceCleaningService; let packetConnectionFinderService: PacketConnectionFinderService; let commandHandler: PauseCleaningCommandHandler; let packetConnection: PacketConnection & ConnectionWithDevice; - let packetMessage: PacketMessage; let device: Device; beforeEach(function () { + deviceCleaningService = imock(); packetConnectionFinderService = imock(); - commandHandler = new PauseCleaningCommandHandler(instance(packetConnectionFinderService)); + commandHandler = new PauseCleaningCommandHandler( + instance(packetConnectionFinderService), + instance(deviceCleaningService), + ); packetConnection = imock(); - packetMessage = imock(); device = imock(); }); @@ -36,88 +40,68 @@ describe('PauseCleaningCommandHandler', function () { await commandHandler.handle(command); - verify(packetConnection.sendAndWait(anything(), anything())).never(); - verify(packetMessage.assertPayloadName(anything())).never(); + verify(deviceCleaningService.autoCleaning(anything(), anything())).never(); }); - it('should pause cleaning', async function () { + it('should pause auto cleaning', async function () { const command = new PauseCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(undefined); await commandHandler.handle(command); - verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 2, cleanType: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(deviceCleaningService.autoCleaning(instance(packetConnection), ModeCtrlValue.Pause)); }); it('should pause zone cleaning', async function () { const command = new PauseCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); await commandHandler.handle(command); - verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + verify(deviceCleaningService.zoneCleaning(instance(packetConnection), ModeCtrlValue.Pause)); }); it('should pause mop cleaning', async function () { const command = new PauseCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); await commandHandler.handle(command); - verify(packetConnection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', deepEqual({ ctrlValue: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP')).once(); + verify(deviceCleaningService.mopCleaning(instance(packetConnection), ModeCtrlValue.Pause)); }); it('should pause spot cleaning', async function () { const command = new PauseCleaningCommand({ deviceId: new ID(1) }); + const currentSpot = new MapPosition({ x: 0, y: 0, phi: 0 }); const deviceMap = new DeviceMap({ ...givenSomeDeviceMapProps(), id: new ID(1), - currentSpot: new MapPosition({ x: 0, y: 0, phi: 0 }), + currentSpot, }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.map).thenReturn(deviceMap); await commandHandler.handle(command); - verify( - packetConnection.sendAndWait( - 'DEVICE_MAPID_SET_NAVIGATION_REQ', - deepEqual({ - mapHeadId: 1, - poseX: 0, - poseY: 0, - posePhi: 0, - ctrlValue: 2, - }), - ), - ).once(); - verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + verify(deviceCleaningService.spotCleaning(instance(packetConnection), currentSpot, ModeCtrlValue.Pause)); }); it('should throw an error when trying to spot clean without map', async function () { const command = new PauseCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.map).thenReturn(undefined); @@ -136,7 +120,6 @@ describe('PauseCleaningCommandHandler', function () { }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.map).thenReturn(deviceMap); diff --git a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts index e569ebe7..df52ab80 100644 --- a/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/pause-cleaning.command-handler.ts @@ -2,14 +2,17 @@ import { DeviceModeValue } from '@agnoc/domain'; import { DomainException } from '@agnoc/toolkit'; import { ModeCtrlValue } from '../services/device-mode-changer.service'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { CommandHandler, PauseCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; export class PauseCleaningCommandHandler implements CommandHandler { readonly forName = 'PauseCleaningCommand'; - constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceCleaningService: DeviceCleaningService, + ) {} async handle(event: PauseCleaningCommand): Promise { const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); @@ -22,26 +25,18 @@ export class PauseCleaningCommandHandler implements CommandHandler { const deviceModeValue = device.mode?.value; if (deviceModeValue === DeviceModeValue.Zone) { - return this.pauseZoneCleaning(connection); + return this.deviceCleaningService.zoneCleaning(connection, ModeCtrlValue.Pause); } if (deviceModeValue === DeviceModeValue.Mop) { - return this.pauseMopCleaning(connection); + return this.deviceCleaningService.mopCleaning(connection, ModeCtrlValue.Pause); } if (deviceModeValue === DeviceModeValue.Spot) { return this.pauseSpotCleaning(connection); } - return this.pauseAutoCleaning(connection); - } - - private async pauseZoneCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Pause, - }); - - response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); + return this.deviceCleaningService.autoCleaning(connection, ModeCtrlValue.Pause); } private async pauseSpotCleaning(connection: PacketConnection & ConnectionWithDevice) { @@ -53,31 +48,6 @@ export class PauseCleaningCommandHandler implements CommandHandler { throw new DomainException('Unable to pause spot cleaning, no spot selected'); } - const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_NAVIGATION_REQ', { - mapHeadId: connection.device.map.id.value, - poseX: connection.device.map.currentSpot.x, - poseY: connection.device.map.currentSpot.y, - posePhi: connection.device.map.currentSpot.phi, - ctrlValue: ModeCtrlValue.Pause, - }); - - response.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP'); - } - - private async pauseMopCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Pause, - }); - - response.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP'); - } - - private async pauseAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Pause, - cleanType: 2, - }); - - response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + await this.deviceCleaningService.spotCleaning(connection, connection.device.map.currentSpot, ModeCtrlValue.Pause); } } diff --git a/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.test.ts new file mode 100644 index 00000000..ac149c1f --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.test.ts @@ -0,0 +1,71 @@ +import { DeviceWaterLevel, DeviceWaterLevelValue, SetWaterLevelCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetWaterLevelCommandHandler } from './set-water-level.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, DeviceRepository, Device } from '@agnoc/domain'; + +describe('SetWaterLevelCommandHandler', function () { + let deviceWaterLevelMapper: DeviceWaterLevelMapper; + let deviceRepository: DeviceRepository; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetWaterLevelCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + deviceWaterLevelMapper = imock(); + deviceRepository = imock(); + packetConnectionFinderService = imock(); + commandHandler = new SetWaterLevelCommandHandler( + instance(packetConnectionFinderService), + instance(deviceWaterLevelMapper), + instance(deviceRepository), + ); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetWaterLevelCommand'); + }); + + describe('#handle()', function () { + it('should set the fan speed', async function () { + const waterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Low); + const command = new SetWaterLevelCommand({ deviceId: new ID(1), waterLevel }); + + when(deviceWaterLevelMapper.fromDomain(anything())).thenReturn(1); + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait('DEVICE_SET_CLEAN_PREFERENCE_REQ', deepEqual({ mode: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_SET_CLEAN_PREFERENCE_RSP')).once(); + verify(device.updateWaterLevel(waterLevel)).once(); + verify(deviceRepository.saveOne(instance(device))).once(); + }); + + it('should do nothing when no connection is found', async function () { + const waterLevel = new DeviceWaterLevel(DeviceWaterLevelValue.Low); + const command = new SetWaterLevelCommand({ deviceId: new ID(1), waterLevel }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(packetConnection.sendAndWait(anything(), anything())).never(); + verify(packetMessage.assertPayloadName(anything())).never(); + verify(device.updateSettings(anything())).never(); + verify(deviceRepository.saveOne(anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.ts new file mode 100644 index 00000000..e84067e2 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-water-level.command-handler.ts @@ -0,0 +1,46 @@ +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceWaterLevelMapper } from '../mappers/device-water-level.mapper'; +import type { PacketMessage } from '../objects/packet.message'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, ConnectionWithDevice, DeviceRepository, SetWaterLevelCommand } from '@agnoc/domain'; + +export class SetWaterLevelCommandHandler implements CommandHandler { + readonly forName = 'SetWaterLevelCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceWaterLevelMapper: DeviceWaterLevelMapper, + private readonly deviceRepository: DeviceRepository, + ) {} + + async handle(event: SetWaterLevelCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + await this.setWaterLevel(event, connection); + await this.updateDevice(event, connection); + } + + private async setWaterLevel( + { waterLevel }: SetWaterLevelCommand, + connection: PacketConnection & ConnectionWithDevice, + ) { + const response: PacketMessage = await connection.sendAndWait('DEVICE_SET_CLEAN_PREFERENCE_REQ', { + mode: this.deviceWaterLevelMapper.fromDomain(waterLevel), + }); + + response.assertPayloadName('DEVICE_SET_CLEAN_PREFERENCE_RSP'); + } + + private async updateDevice( + { waterLevel }: SetWaterLevelCommand, + connection: PacketConnection & ConnectionWithDevice, + ) { + connection.device.updateWaterLevel(waterLevel); + + await this.deviceRepository.saveOne(connection.device); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts index 993e3590..28a3ea8a 100644 --- a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts +++ b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.test.ts @@ -4,41 +4,44 @@ import { DeviceModeValue, DeviceState, DeviceStateValue, - MapCoordinate, MapPosition, - Room, StartCleaningCommand, - Zone, } from '@agnoc/domain'; -import { givenSomeDeviceMapProps, givenSomeRoomProps } from '@agnoc/domain/test-support'; +import { givenSomeDeviceMapProps } from '@agnoc/domain/test-support'; import { DomainException, ID } from '@agnoc/toolkit'; import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; import { StartCleaningCommandHandler } from './start-cleaning.command-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceMapService } from '../services/device-map.service'; import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { ConnectionWithDevice, Device, DeviceSystem } from '@agnoc/domain'; describe('StartCleaningCommandHandler', function () { + let deviceCleaningService: DeviceCleaningService; + let deviceMapService: DeviceMapService; let packetConnectionFinderService: PacketConnectionFinderService; let deviceModeChangerService: DeviceModeChangerService; let commandHandler: StartCleaningCommandHandler; let packetConnection: PacketConnection & ConnectionWithDevice; - let packetMessage: PacketMessage; let device: Device; let deviceSystem: DeviceSystem; beforeEach(function () { + deviceCleaningService = imock(); + deviceMapService = imock(); packetConnectionFinderService = imock(); deviceModeChangerService = imock(); commandHandler = new StartCleaningCommandHandler( instance(packetConnectionFinderService), instance(deviceModeChangerService), + instance(deviceCleaningService), + instance(deviceMapService), ); packetConnection = imock(); - packetMessage = imock(); device = imock(); deviceSystem = imock(); }); @@ -55,15 +58,14 @@ describe('StartCleaningCommandHandler', function () { await commandHandler.handle(command); - verify(packetConnection.sendAndWait(anything(), anything())).never(); - verify(packetMessage.assertPayloadName(anything())).never(); + verify(deviceModeChangerService.changeMode(anything(), anything())).never(); + verify(deviceCleaningService.autoCleaning(anything(), anything())).never(); }); - it('should start cleaning', async function () { + it('should start auto cleaning', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.system).thenReturn(instance(deviceSystem)); when(deviceSystem.supports(anything())).thenReturn(false); @@ -78,21 +80,14 @@ describe('StartCleaningCommandHandler', function () { deepEqual(new DeviceMode(DeviceModeValue.None)), ), ).once(); - verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(deviceCleaningService.autoCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); }); it('should enable whole clean when supported', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); - const deviceMap = new DeviceMap({ - ...givenSomeDeviceMapProps(), - id: new ID(1), - rooms: [new Room({ ...givenSomeRoomProps(), id: new ID(1), name: 'Room 1' })], - restrictedZones: [new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 0, y: 0 })] })], - }); + const deviceMap = new DeviceMap(givenSomeDeviceMapProps()); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.system).thenReturn(instance(deviceSystem)); when(deviceSystem.supports(anything())).thenReturn(true); @@ -110,33 +105,17 @@ describe('StartCleaningCommandHandler', function () { ), ).once(); verify( - packetConnection.sendAndWait( - 'DEVICE_MAPID_SET_PLAN_PARAMS_REQ', - deepEqual({ - mapHeadId: 1, - mapName: 'Default', - planId: 2, - planName: 'Default', - roomList: [{ roomId: 1, roomName: 'Room 1', enable: true }], - areaInfo: { - mapHeadId: 1, - planId: 2, - cleanAreaLength: 1, - cleanAreaList: [{ cleanAreaId: 1, type: 0, coordinateLength: 1, coordinateList: [{ x: 0, y: 0 }] }], - }, - }), + deviceMapService.enableWholeClean( + instance(packetConnection) as PacketConnection & ConnectionWithDevice, ), ).once(); - verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP')).once(); - verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(deviceCleaningService.autoCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); }); it('should not enable whole clean when device has no map', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); when(device.system).thenReturn(instance(deviceSystem)); when(deviceSystem.supports(anything())).thenReturn(true); @@ -147,18 +126,21 @@ describe('StartCleaningCommandHandler', function () { await commandHandler.handle(command); - verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.None)), + ), + ).once(); + verify(deviceMapService.enableWholeClean(anything())).never(); + verify(deviceCleaningService.autoCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); }); it('should start zone cleaning', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); - when(device.system).thenReturn(instance(deviceSystem)); - when(deviceSystem.supports(anything())).thenReturn(false); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Zone)); when(device.hasMopAttached).thenReturn(false); @@ -170,18 +152,14 @@ describe('StartCleaningCommandHandler', function () { deepEqual(new DeviceMode(DeviceModeValue.None)), ), ).once(); - verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + verify(deviceCleaningService.zoneCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); }); it('should start mop cleaning', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); - when(device.system).thenReturn(instance(deviceSystem)); - when(deviceSystem.supports(anything())).thenReturn(false); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Mop)); when(device.hasMopAttached).thenReturn(true); @@ -190,23 +168,19 @@ describe('StartCleaningCommandHandler', function () { verify( deviceModeChangerService.changeMode(instance(packetConnection), deepEqual(new DeviceMode(DeviceModeValue.Mop))), ).once(); - verify(packetConnection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP')).once(); + verify(deviceCleaningService.mopCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); }); it('should start spot cleaning', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); + const currentSpot = new MapPosition({ x: 0, y: 0, phi: 0 }); const deviceMap = new DeviceMap({ ...givenSomeDeviceMapProps(), - id: new ID(1), - currentSpot: new MapPosition({ x: 0, y: 0, phi: 0 }), + currentSpot, }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); - when(device.system).thenReturn(instance(deviceSystem)); - when(deviceSystem.supports(anything())).thenReturn(false); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.hasMopAttached).thenReturn(false); when(device.map).thenReturn(deviceMap); @@ -219,29 +193,14 @@ describe('StartCleaningCommandHandler', function () { deepEqual(new DeviceMode(DeviceModeValue.None)), ), ).once(); - verify( - packetConnection.sendAndWait( - 'DEVICE_MAPID_SET_NAVIGATION_REQ', - deepEqual({ - mapHeadId: 1, - poseX: 0, - poseY: 0, - posePhi: 0, - ctrlValue: 1, - }), - ), - ).once(); - verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + verify(deviceCleaningService.spotCleaning(instance(packetConnection), currentSpot, ModeCtrlValue.Start)).once(); }); it('should throw an error when trying to spot clean without map', async function () { const command = new StartCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); - when(device.system).thenReturn(instance(deviceSystem)); - when(deviceSystem.supports(anything())).thenReturn(false); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.hasMopAttached).thenReturn(false); when(device.map).thenReturn(undefined); @@ -260,10 +219,7 @@ describe('StartCleaningCommandHandler', function () { }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); when(packetConnection.device).thenReturn(instance(device)); - when(device.system).thenReturn(instance(deviceSystem)); - when(deviceSystem.supports(anything())).thenReturn(false); when(device.mode).thenReturn(new DeviceMode(DeviceModeValue.Spot)); when(device.hasMopAttached).thenReturn(false); when(device.map).thenReturn(deviceMap); @@ -275,3 +231,7 @@ describe('StartCleaningCommandHandler', function () { }); }); }); + +interface DeviceWithMap extends Device { + map: DeviceMap; +} diff --git a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts index 835d6d5c..1aabd7a0 100644 --- a/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/start-cleaning.command-handler.ts @@ -2,10 +2,11 @@ import { DeviceCapability, DeviceMode, DeviceModeValue, DeviceStateValue } from import { DomainException } from '@agnoc/toolkit'; import { ModeCtrlValue } from '../services/device-mode-changer.service'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceMapService } from '../services/device-map.service'; import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; -import type { CommandHandler, StartCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; +import type { CommandHandler, StartCleaningCommand, ConnectionWithDevice, Device, DeviceMap } from '@agnoc/domain'; export class StartCleaningCommandHandler implements CommandHandler { readonly forName = 'StartCleaningCommand'; @@ -13,6 +14,8 @@ export class StartCleaningCommandHandler implements CommandHandler { constructor( private readonly packetConnectionFinderService: PacketConnectionFinderService, private readonly deviceModeChangerService: DeviceModeChangerService, + private readonly deviceCleaningService: DeviceCleaningService, + private readonly deviceMapService: DeviceMapService, ) {} async handle(event: StartCleaningCommand): Promise { @@ -28,22 +31,22 @@ export class StartCleaningCommandHandler implements CommandHandler { const deviceModeValue = device.mode?.value; if (deviceModeValue === DeviceModeValue.Zone) { - return this.startZoneCleaning(connection); + return this.deviceCleaningService.zoneCleaning(connection, ModeCtrlValue.Start); } if (deviceModeValue === DeviceModeValue.Mop) { - return this.startMopCleaning(connection); + return this.deviceCleaningService.mopCleaning(connection, ModeCtrlValue.Start); } if (deviceModeValue === DeviceModeValue.Spot) { return this.startSpotCleaning(connection); } - if (this.isDockedAndSupportsMapPlans(connection)) { - await this.enableWholeClean(connection); + if (this.isDockedAndSupportsMapPlansAndHasMap(connection)) { + await this.deviceMapService.enableWholeClean(connection); } - return this.startAutoCleaning(connection); + return this.deviceCleaningService.autoCleaning(connection, ModeCtrlValue.Start); } private async changeDeviceMode(connection: PacketConnection & ConnectionWithDevice) { @@ -53,14 +56,6 @@ export class StartCleaningCommandHandler implements CommandHandler { await this.deviceModeChangerService.changeMode(connection, deviceMode); } - private async startZoneCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Start, - }); - - response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); - } - private async startSpotCleaning(connection: PacketConnection & ConnectionWithDevice) { if (!connection.device.map) { throw new DomainException('Unable to start spot cleaning, no map available'); @@ -70,72 +65,20 @@ export class StartCleaningCommandHandler implements CommandHandler { throw new DomainException('Unable to start spot cleaning, no spot selected'); } - const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_NAVIGATION_REQ', { - mapHeadId: connection.device.map.id.value, - poseX: connection.device.map.currentSpot.x, - poseY: connection.device.map.currentSpot.y, - posePhi: connection.device.map.currentSpot.phi, - ctrlValue: ModeCtrlValue.Start, - }); - - response.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP'); - } - - private async startMopCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Start, - }); - - response.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP'); - } - - private async startAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Start, - cleanType: 2, - }); - - response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + return this.deviceCleaningService.spotCleaning(connection, connection.device.map.currentSpot, ModeCtrlValue.Start); } - private isDockedAndSupportsMapPlans(connection: PacketConnection & ConnectionWithDevice) { + private isDockedAndSupportsMapPlansAndHasMap( + connection: PacketConnection & ConnectionWithDevice, + ): connection is PacketConnection & ConnectionWithDevice { const supportsMapPlans = connection.device.system.supports(DeviceCapability.MAP_PLANS); const deviceStateValue = connection.device.state?.value; + const hasMap = connection.device.map; - return supportsMapPlans && deviceStateValue === DeviceStateValue.Docked; + return Boolean(supportsMapPlans && deviceStateValue === DeviceStateValue.Docked && hasMap); } +} - private async enableWholeClean(connection: PacketConnection & ConnectionWithDevice) { - if (!connection.device.map) { - return; - } - - const { id, restrictedZones, rooms } = connection.device.map; - const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', { - mapHeadId: id.value, - // FIXME: this will change user map name. - mapName: 'Default', - planId: 2, - // FIXME: this will change user plan name. - planName: 'Default', - roomList: rooms.map((room) => ({ - roomId: room.id.value, - roomName: room.name, - enable: true, - })), - areaInfo: { - mapHeadId: id.value, - planId: 2, - cleanAreaLength: restrictedZones.length, - cleanAreaList: restrictedZones.map((zone) => ({ - cleanAreaId: zone.id.value, - type: 0, - coordinateLength: zone.coordinates.length, - coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), - })), - }, - }); - - response.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP'); - } +interface DeviceWithMap extends Device { + map: DeviceMap; } diff --git a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts index 1c69961a..b2300038 100644 --- a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts +++ b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.test.ts @@ -1,24 +1,28 @@ import { StopCleaningCommand } from '@agnoc/domain'; import { ID } from '@agnoc/toolkit'; -import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { imock, instance, when, verify, anything } from '@johanblumenberg/ts-mockito'; import { expect } from 'chai'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; import { StopCleaningCommandHandler } from './stop-cleaning.command-handler'; import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; import type { ConnectionWithDevice } from '@agnoc/domain'; describe('StopCleaningCommandHandler', function () { + let deviceCleaningService: DeviceCleaningService; let packetConnectionFinderService: PacketConnectionFinderService; let commandHandler: StopCleaningCommandHandler; let packetConnection: PacketConnection & ConnectionWithDevice; - let packetMessage: PacketMessage; beforeEach(function () { + deviceCleaningService = imock(); packetConnectionFinderService = imock(); - commandHandler = new StopCleaningCommandHandler(instance(packetConnectionFinderService)); + commandHandler = new StopCleaningCommandHandler( + instance(packetConnectionFinderService), + instance(deviceCleaningService), + ); packetConnection = imock(); - packetMessage = imock(); }); it('should define the name', function () { @@ -33,20 +37,17 @@ describe('StopCleaningCommandHandler', function () { await commandHandler.handle(command); - verify(packetConnection.sendAndWait(anything(), anything())).never(); - verify(packetMessage.assertPayloadName(anything())).never(); + verify(deviceCleaningService.autoCleaning(anything(), anything())).never(); }); it('should stop cleaning', async function () { const command = new StopCleaningCommand({ deviceId: new ID(1) }); when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); - when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); await commandHandler.handle(command); - verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 0, cleanType: 2 }))).once(); - verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + verify(deviceCleaningService.autoCleaning(instance(packetConnection), ModeCtrlValue.Stop)).once(); }); }); }); diff --git a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts index b24af91b..630171a8 100644 --- a/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts +++ b/packages/adapter-tcp/src/command-handlers/stop-cleaning.command-handler.ts @@ -1,13 +1,15 @@ import { ModeCtrlValue } from '../services/device-mode-changer.service'; -import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; -import type { PacketMessage } from '../objects/packet.message'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; -import type { CommandHandler, StopCleaningCommand, ConnectionWithDevice } from '@agnoc/domain'; +import type { CommandHandler, StopCleaningCommand } from '@agnoc/domain'; export class StopCleaningCommandHandler implements CommandHandler { readonly forName = 'StopCleaningCommand'; - constructor(private readonly packetConnectionFinderService: PacketConnectionFinderService) {} + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceCleaningService: DeviceCleaningService, + ) {} async handle(event: StopCleaningCommand): Promise { const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); @@ -16,15 +18,6 @@ export class StopCleaningCommandHandler implements CommandHandler { return; } - return this.stopAutoCleaning(connection); - } - - private async stopAutoCleaning(connection: PacketConnection & ConnectionWithDevice) { - const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { - ctrlValue: ModeCtrlValue.Stop, - cleanType: 2, - }); - - response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + return this.deviceCleaningService.autoCleaning(connection, ModeCtrlValue.Stop); } } diff --git a/packages/adapter-tcp/src/services/device-cleaning.service.test.ts b/packages/adapter-tcp/src/services/device-cleaning.service.test.ts new file mode 100644 index 00000000..e6dc762e --- /dev/null +++ b/packages/adapter-tcp/src/services/device-cleaning.service.test.ts @@ -0,0 +1,113 @@ +import { DeviceMap, MapPosition } from '@agnoc/domain'; +import { givenSomeDeviceMapProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { DeviceCleaningService } from './device-cleaning.service'; +import { ModeCtrlValue } from './device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { ConnectionWithDevice, Device } from '@agnoc/domain'; + +describe('DeviceCleaningService', function () { + let service: DeviceCleaningService; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: Device; + + beforeEach(function () { + service = new DeviceCleaningService(); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + describe('#autoCleaning()', function () { + it('should start auto cleaning', async function () { + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await service.autoCleaning(instance(packetConnection), ModeCtrlValue.Start); + + verify(packetConnection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', deepEqual({ ctrlValue: 1, cleanType: 2 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AUTO_CLEAN_RSP')).once(); + }); + }); + + describe('#mopCleaning()', function () { + it('should start mop cleaning', async function () { + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + + await service.mopCleaning(instance(packetConnection), ModeCtrlValue.Start); + + verify(packetConnection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP')).once(); + }); + }); + + describe('#spotCleaning()', function () { + it('should start spot cleaning with map id', async function () { + const deviceMap = new DeviceMap({ ...givenSomeDeviceMapProps(), id: new ID(1) }); + + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.map).thenReturn(deviceMap); + + await service.spotCleaning( + instance(packetConnection), + new MapPosition({ x: 1, y: 2, phi: 3 }), + ModeCtrlValue.Start, + ); + + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_NAVIGATION_REQ', + deepEqual({ + mapHeadId: 1, + poseX: 1, + poseY: 2, + posePhi: 3, + ctrlValue: 1, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + }); + + it('should start spot cleaning with no map id', async function () { + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.map).thenReturn(undefined); + + await service.spotCleaning( + instance(packetConnection), + new MapPosition({ x: 1, y: 2, phi: 3 }), + ModeCtrlValue.Start, + ); + + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_NAVIGATION_REQ', + deepEqual({ + mapHeadId: 0, + poseX: 1, + poseY: 2, + posePhi: 3, + ctrlValue: 1, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP')).once(); + }); + }); + + describe('#zoneCleaning()', function () { + it('should start zone cleaning', async function () { + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + + await service.zoneCleaning(instance(packetConnection), ModeCtrlValue.Start); + + verify(packetConnection.sendAndWait('DEVICE_AREA_CLEAN_REQ', deepEqual({ ctrlValue: 1 }))).once(); + verify(packetMessage.assertPayloadName('DEVICE_AREA_CLEAN_RSP')).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/services/device-cleaning.service.ts b/packages/adapter-tcp/src/services/device-cleaning.service.ts new file mode 100644 index 00000000..5b37a1a9 --- /dev/null +++ b/packages/adapter-tcp/src/services/device-cleaning.service.ts @@ -0,0 +1,47 @@ +import type { ModeCtrlValue } from './device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { ConnectionWithDevice, MapPosition } from '@agnoc/domain'; + +export class DeviceCleaningService { + async zoneCleaning(connection: PacketConnection & ConnectionWithDevice, mode: ModeCtrlValue): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AREA_CLEAN_REQ', { + ctrlValue: mode, + }); + + response.assertPayloadName('DEVICE_AREA_CLEAN_RSP'); + } + + async spotCleaning( + connection: PacketConnection & ConnectionWithDevice, + position: MapPosition, + mode: ModeCtrlValue, + ): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_NAVIGATION_REQ', { + mapHeadId: connection.device.map?.id.value ?? 0, + poseX: position.x, + poseY: position.y, + posePhi: position.phi, + ctrlValue: mode, + }); + + response.assertPayloadName('DEVICE_MAPID_SET_NAVIGATION_RSP'); + } + + async mopCleaning(connection: PacketConnection & ConnectionWithDevice, mode: ModeCtrlValue): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MOP_FLOOR_CLEAN_REQ', { + ctrlValue: mode, + }); + + response.assertPayloadName('DEVICE_MOP_FLOOR_CLEAN_RSP'); + } + + async autoCleaning(connection: PacketConnection & ConnectionWithDevice, mode: ModeCtrlValue): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_AUTO_CLEAN_REQ', { + ctrlValue: mode, + cleanType: 2, + }); + + response.assertPayloadName('DEVICE_AUTO_CLEAN_RSP'); + } +} diff --git a/packages/adapter-tcp/src/services/device-map.service.test.ts b/packages/adapter-tcp/src/services/device-map.service.test.ts new file mode 100644 index 00000000..74231161 --- /dev/null +++ b/packages/adapter-tcp/src/services/device-map.service.test.ts @@ -0,0 +1,117 @@ +import { DeviceMap, MapCoordinate, Room, Zone } from '@agnoc/domain'; +import { givenSomeDeviceMapProps, givenSomeRoomProps } from '@agnoc/domain/test-support'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { DeviceMapService } from './device-map.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { ConnectionWithDevice, Device } from '@agnoc/domain'; + +describe('DeviceMapService', function () { + let service: DeviceMapService; + let packetConnection: PacketConnection & ConnectionWithDevice; + let packetMessage: PacketMessage; + let device: DeviceWithMap; + + beforeEach(function () { + service = new DeviceMapService(); + packetConnection = imock(); + packetMessage = imock(); + device = imock(); + }); + + describe('#setMapZones()', function () { + it('should set map zones with map id', async function () { + const deviceMap = new DeviceMap({ ...givenSomeDeviceMapProps(), id: new ID(1) }); + const zones = [ + new Zone({ + id: new ID(1), + coordinates: [new MapCoordinate({ x: 1, y: 2 }), new MapCoordinate({ x: 3, y: 4 })], + }), + new Zone({ + id: new ID(2), + coordinates: [new MapCoordinate({ x: 5, y: 6 }), new MapCoordinate({ x: 7, y: 8 })], + }), + ]; + + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.map).thenReturn(deviceMap); + + await service.setMapZones(instance(packetConnection), zones); + + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_AREA_CLEAN_INFO_REQ', + deepEqual({ + mapHeadId: 1, + planId: 0, + cleanAreaLength: 2, + cleanAreaList: [ + { + cleanAreaId: 1, + type: 0, + coordinateLength: 2, + coordinateList: [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + ], + }, + { + cleanAreaId: 2, + type: 0, + coordinateLength: 2, + coordinateList: [ + { x: 5, y: 6 }, + { x: 7, y: 8 }, + ], + }, + ], + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_AREA_CLEAN_INFO_RSP')).once(); + }); + }); + + describe('#enableWholeClean()', function () { + it('should enable whole clean', async function () { + const deviceMap = new DeviceMap({ + ...givenSomeDeviceMapProps(), + id: new ID(1), + rooms: [new Room({ ...givenSomeRoomProps(), id: new ID(1), name: 'Room 1' })], + restrictedZones: [new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 0, y: 0 })] })], + }); + + when(packetConnection.sendAndWait(anything(), anything())).thenResolve(instance(packetMessage)); + when(packetConnection.device).thenReturn(instance(device)); + when(device.map).thenReturn(deviceMap); + + await service.enableWholeClean(instance(packetConnection)); + + verify( + packetConnection.sendAndWait( + 'DEVICE_MAPID_SET_PLAN_PARAMS_REQ', + deepEqual({ + mapHeadId: 1, + mapName: 'Default', + planId: 2, + planName: 'Default', + roomList: [{ roomId: 1, roomName: 'Room 1', enable: true }], + areaInfo: { + mapHeadId: 1, + planId: 2, + cleanAreaLength: 1, + cleanAreaList: [{ cleanAreaId: 1, type: 0, coordinateLength: 1, coordinateList: [{ x: 0, y: 0 }] }], + }, + }), + ), + ).once(); + verify(packetMessage.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP')).once(); + }); + }); +}); + +interface DeviceWithMap extends Device { + map: DeviceMap; +} diff --git a/packages/adapter-tcp/src/services/device-map.service.ts b/packages/adapter-tcp/src/services/device-map.service.ts new file mode 100644 index 00000000..ebc2de9e --- /dev/null +++ b/packages/adapter-tcp/src/services/device-map.service.ts @@ -0,0 +1,57 @@ +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { PacketMessage } from '../objects/packet.message'; +import type { ConnectionWithDevice, Device, DeviceMap, Zone } from '@agnoc/domain'; + +export class DeviceMapService { + async setMapZones(connection: PacketConnection & ConnectionWithDevice, zones: Zone[]): Promise { + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_AREA_CLEAN_INFO_REQ', { + mapHeadId: connection.device.map.id.value, + planId: 0, + cleanAreaLength: zones.length, + cleanAreaList: zones.map((zone) => { + return { + cleanAreaId: zone.id.value, + type: 0, + coordinateLength: zone.coordinates.length, + coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), + }; + }), + }); + + response.assertPayloadName('DEVICE_MAPID_SET_AREA_CLEAN_INFO_RSP'); + } + + async enableWholeClean(connection: PacketConnection & ConnectionWithDevice): Promise { + const { id, restrictedZones, rooms } = connection.device.map; + const response: PacketMessage = await connection.sendAndWait('DEVICE_MAPID_SET_PLAN_PARAMS_REQ', { + mapHeadId: id.value, + // FIXME: this will change user map name. + mapName: 'Default', + planId: 2, + // FIXME: this will change user plan name. + planName: 'Default', + roomList: rooms.map((room) => ({ + roomId: room.id.value, + roomName: room.name, + enable: true, + })), + areaInfo: { + mapHeadId: id.value, + planId: 2, + cleanAreaLength: restrictedZones.length, + cleanAreaList: restrictedZones.map((zone) => ({ + cleanAreaId: zone.id.value, + type: 0, + coordinateLength: zone.coordinates.length, + coordinateList: zone.coordinates.map(({ x, y }) => ({ x, y })), + })), + }, + }); + + response.assertPayloadName('DEVICE_MAPID_SET_PLAN_PARAMS_RSP'); + } +} + +interface DeviceWithMap extends Device { + map: DeviceMap; +} diff --git a/packages/adapter-tcp/src/services/device-mode-changer.service.ts b/packages/adapter-tcp/src/services/device-mode-changer.service.ts index 62f07602..c3423a58 100644 --- a/packages/adapter-tcp/src/services/device-mode-changer.service.ts +++ b/packages/adapter-tcp/src/services/device-mode-changer.service.ts @@ -7,11 +7,11 @@ import type { WaiterService } from '@agnoc/toolkit'; const MODE_CHANGE_TIMEOUT = 5000; -export const ModeCtrlValue = { - Stop: 0, - Start: 1, - Pause: 2, -} as const; +export enum ModeCtrlValue { + Stop = 0, + Start = 1, + Pause = 2, +} export class DeviceModeChangerService { constructor(private readonly waiterService: WaiterService) {} diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 7ce92a1a..fb0d6d92 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -16,6 +16,7 @@ import { SetCarpetModeCommandHandler } from './command-handlers/set-carpet-mode. import { SetDeviceQuietHoursCommandHandler } from './command-handlers/set-device-quiet-hours.command-handler'; import { SetDeviceVoiceCommandHandler } from './command-handlers/set-device-voice.command-handler'; import { SetFanSpeedCommandHandler } from './command-handlers/set-fan-speed.command-handler'; +import { SetWaterLevelCommandHandler } from './command-handlers/set-water-level.command-handler'; import { StartCleaningCommandHandler } from './command-handlers/start-cleaning.command-handler'; import { StopCleaningCommandHandler } from './command-handlers/stop-cleaning.command-handler'; import { NTPServerConnectionHandler } from './connection-handlers/ntp-server.connection-handler'; @@ -58,6 +59,8 @@ import { DeviceUpgradeInfoEventHandler } from './packet-event-handlers/device-up import { DeviceVersionUpdateEventHandler } from './packet-event-handlers/device-version-update.event-handler'; import { GetDeviceConsumablesQueryHandler } from './query-handlers/get-device-consumables.query-handler'; import { ConnectionDeviceUpdaterService } from './services/connection-device-updater.service'; +import { DeviceCleaningService } from './services/device-cleaning.service'; +import { DeviceMapService } from './services/device-map.service'; import { DeviceModeChangerService } from './services/device-mode-changer.service'; import { PacketConnectionFinderService } from './services/packet-connection-finder.service'; import { PacketEventPublisherService } from './services/packet-event-publisher.service'; @@ -116,6 +119,8 @@ export class TCPServer implements Server { const packetEventPublisherService = new PacketEventPublisherService(packetEventBus); const packetConnectionFinderService = new PacketConnectionFinderService(this.connectionRepository); const deviceModeChangerService = new DeviceModeChangerService(waiterService); + const deviceCleaningService = new DeviceCleaningService(); + const deviceMapService = new DeviceMapService(); // Connection const packetConnectionFactory = new PacketConnectionFactory(packetEventBus, packetFactory); @@ -184,15 +189,21 @@ export class TCPServer implements Server { this.commandQueryHandlerRegistry.register( new GetDeviceConsumablesQueryHandler(packetConnectionFinderService, this.deviceRepository), new LocateDeviceCommandHandler(packetConnectionFinderService), - new PauseCleaningCommandHandler(packetConnectionFinderService), + new PauseCleaningCommandHandler(packetConnectionFinderService, deviceCleaningService), new ResetConsumableCommandHandler(packetConnectionFinderService, this.deviceRepository), new ReturnHomeCommandHandler(packetConnectionFinderService), new SetCarpetModeCommandHandler(packetConnectionFinderService, this.deviceRepository), new SetDeviceQuietHoursCommandHandler(packetConnectionFinderService), new SetDeviceVoiceCommandHandler(packetConnectionFinderService, voiceSettingMapper, this.deviceRepository), new SetFanSpeedCommandHandler(packetConnectionFinderService, deviceFanSpeedMapper, this.deviceRepository), - new StartCleaningCommandHandler(packetConnectionFinderService, deviceModeChangerService), - new StopCleaningCommandHandler(packetConnectionFinderService), + new SetWaterLevelCommandHandler(packetConnectionFinderService, deviceWaterLevelMapper, this.deviceRepository), + new StartCleaningCommandHandler( + packetConnectionFinderService, + deviceModeChangerService, + deviceCleaningService, + deviceMapService, + ), + new StopCleaningCommandHandler(packetConnectionFinderService, deviceCleaningService), ); } diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index cdef23df..0c31031d 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -6,6 +6,7 @@ import type { SetCarpetModeCommand } from './set-carpet-mode.command'; import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; import type { SetDeviceVoiceCommand } from './set-device-voice.command'; import type { SetFanSpeedCommand } from './set-fan-speed.command'; +import type { SetWaterLevelCommand } from './set-water-level.command'; import type { StartCleaningCommand } from './start-cleaning.command'; import type { StopCleaningCommand } from './stop-cleaning.command'; @@ -19,6 +20,7 @@ export type Commands = { SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; SetDeviceVoiceCommand: SetDeviceVoiceCommand; SetFanSpeedCommand: SetFanSpeedCommand; + SetWaterLevelCommand: SetWaterLevelCommand; StartCleaningCommand: StartCleaningCommand; StopCleaningCommand: StopCleaningCommand; }; diff --git a/packages/domain/src/commands/set-water-level.command.test.ts b/packages/domain/src/commands/set-water-level.command.test.ts new file mode 100644 index 00000000..a5bc8474 --- /dev/null +++ b/packages/domain/src/commands/set-water-level.command.test.ts @@ -0,0 +1,54 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceWaterLevel, DeviceWaterLevelValue } from '../domain-primitives/device-water-level.domain-primitive'; +import { SetWaterLevelCommand } from './set-water-level.command'; +import type { SetWaterLevelCommandInput } from './set-water-level.command'; + +describe('SetWaterLevelCommand', function () { + it('should be created', function () { + const input = givenASetWaterLevelCommandInput(); + const command = new SetWaterLevelCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.waterLevel).to.be.equal(input.waterLevel); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetWaterLevelCommand({ ...givenASetWaterLevelCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetWaterLevelCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetWaterLevelCommand({ ...givenASetWaterLevelCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetWaterLevelCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'waterLevel' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetWaterLevelCommand({ ...givenASetWaterLevelCommandInput(), waterLevel: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'waterLevel' for SetWaterLevelCommand not provided`); + }); + + it("should throw an error when 'waterLevel' is not an DeviceWaterLevel", function () { + expect( + // @ts-expect-error - invalid property + () => new SetWaterLevelCommand({ ...givenASetWaterLevelCommandInput(), waterLevel: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'waterLevel' of SetWaterLevelCommand is not an instance of DeviceWaterLevel`, + ); + }); +}); + +function givenASetWaterLevelCommandInput(): SetWaterLevelCommandInput { + return { deviceId: ID.generate(), waterLevel: new DeviceWaterLevel(DeviceWaterLevelValue.Low) }; +} diff --git a/packages/domain/src/commands/set-water-level.command.ts b/packages/domain/src/commands/set-water-level.command.ts new file mode 100644 index 00000000..bf8d8b28 --- /dev/null +++ b/packages/domain/src/commands/set-water-level.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { DeviceWaterLevel } from '../domain-primitives/device-water-level.domain-primitive'; + +/** Input for the command stopping the cleaning process of a device. */ +export interface SetWaterLevelCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Fan speed setting. */ + waterLevel: DeviceWaterLevel; +} + +/** Command for stopping the cleaning process of a device. */ +export class SetWaterLevelCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the fan speed. */ + get waterLevel(): DeviceWaterLevel { + return this.props.waterLevel; + } + + protected validate(props: SetWaterLevelCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'waterLevel'); + this.validateInstanceProp(props, 'waterLevel', DeviceWaterLevel); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 32ed3c5f..73c3e734 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -9,6 +9,7 @@ export * from './commands/set-carpet-mode.command'; export * from './commands/set-device-quiet-hours.command'; export * from './commands/set-device-voice.command'; export * from './commands/set-fan-speed.command'; +export * from './commands/set-water-level.command'; export * from './commands/start-cleaning.command'; export * from './commands/stop-cleaning.command'; export * from './domain-events/connection-device-changed.domain-event'; From 81af21edc0f92974e7b6220a326b1c2a2515253f Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 11 Apr 2023 08:21:54 +0200 Subject: [PATCH 34/38] feat: add clean spot & zone commands --- .../clean-spot.command-handler.test.ts | 70 ++++++++++++ .../clean-spot.command-handler.ts | 29 +++++ .../clean-zones.command-handler.test.ts | 100 ++++++++++++++++++ .../clean-zones.command-handler.ts | 46 ++++++++ packages/adapter-tcp/src/tcp.server.ts | 9 ++ .../src/commands/clean-spot.command.test.ts | 53 ++++++++++ .../domain/src/commands/clean-spot.command.ts | 30 ++++++ .../src/commands/clean-zones.command.test.ts | 61 +++++++++++ .../src/commands/clean-zones.command.ts | 30 ++++++ packages/domain/src/commands/commands.ts | 4 + packages/domain/src/index.ts | 2 + 11 files changed, 434 insertions(+) create mode 100644 packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.ts create mode 100644 packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.ts create mode 100644 packages/domain/src/commands/clean-spot.command.test.ts create mode 100644 packages/domain/src/commands/clean-spot.command.ts create mode 100644 packages/domain/src/commands/clean-zones.command.test.ts create mode 100644 packages/domain/src/commands/clean-zones.command.ts diff --git a/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.test.ts new file mode 100644 index 00000000..b4b24bbe --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.test.ts @@ -0,0 +1,70 @@ +import { CleanSpotCommand, DeviceMode, DeviceModeValue, MapCoordinate, MapPosition } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import { CleanSpotCommandHandler } from './clean-spot.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; + +describe('CleanSpotCommandHandler', function () { + let deviceModeChangerService: DeviceModeChangerService; + let deviceCleaningService: DeviceCleaningService; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: CleanSpotCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + + beforeEach(function () { + deviceModeChangerService = imock(); + deviceCleaningService = imock(); + packetConnectionFinderService = imock(); + commandHandler = new CleanSpotCommandHandler( + instance(packetConnectionFinderService), + instance(deviceModeChangerService), + instance(deviceCleaningService), + ); + packetConnection = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('CleanSpotCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const command = new CleanSpotCommand({ deviceId: new ID(1), spot: new MapCoordinate({ x: 1, y: 2 }) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(deviceModeChangerService.changeMode(anything(), anything())).never(); + verify(deviceCleaningService.spotCleaning(anything(), anything(), anything())).never(); + }); + + it('should clean a spot', async function () { + const command = new CleanSpotCommand({ deviceId: new ID(1), spot: new MapCoordinate({ x: 1, y: 2 }) }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.Spot)), + ), + ).once(); + verify( + deviceCleaningService.spotCleaning( + instance(packetConnection), + deepEqual(new MapPosition({ x: 1, y: 2, phi: 0 })), + ModeCtrlValue.Start, + ), + ).once(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.ts b/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.ts new file mode 100644 index 00000000..8075cd58 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/clean-spot.command-handler.ts @@ -0,0 +1,29 @@ +import { DeviceMode, DeviceModeValue, MapPosition } from '@agnoc/domain'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, CleanSpotCommand } from '@agnoc/domain'; + +export class CleanSpotCommandHandler implements CommandHandler { + readonly forName = 'CleanSpotCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceModeChangerService: DeviceModeChangerService, + private readonly deviceCleaningService: DeviceCleaningService, + ) {} + + async handle(event: CleanSpotCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + const { x, y } = event.spot; + + await this.deviceModeChangerService.changeMode(connection, new DeviceMode(DeviceModeValue.Spot)); + await this.deviceCleaningService.spotCleaning(connection, new MapPosition({ x, y, phi: 0 }), ModeCtrlValue.Start); + } +} diff --git a/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.test.ts new file mode 100644 index 00000000..c4a914e9 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.test.ts @@ -0,0 +1,100 @@ +import { CleanZonesCommand, DeviceMode, DeviceModeValue, MapCoordinate, Zone } from '@agnoc/domain'; +import { DomainException, ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything, deepEqual } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import { CleanZonesCommandHandler } from './clean-zones.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceMapService } from '../services/device-map.service'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice, Device, DeviceMap } from '@agnoc/domain'; + +describe('CleanZonesCommandHandler', function () { + let deviceMapService: DeviceMapService; + let deviceModeChangerService: DeviceModeChangerService; + let deviceCleaningService: DeviceCleaningService; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: CleanZonesCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + let device: Device; + let deviceMap: DeviceMap; + + beforeEach(function () { + deviceMapService = imock(); + deviceModeChangerService = imock(); + deviceCleaningService = imock(); + packetConnectionFinderService = imock(); + commandHandler = new CleanZonesCommandHandler( + instance(packetConnectionFinderService), + instance(deviceModeChangerService), + instance(deviceMapService), + instance(deviceCleaningService), + ); + packetConnection = imock(); + device = imock(); + deviceMap = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('CleanZonesCommand'); + }); + + describe('#handle()', function () { + it('should do nothing when no connection is found', async function () { + const zone = new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 1, y: 2 })] }); + const command = new CleanZonesCommand({ deviceId: new ID(1), zones: [zone] }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(deviceModeChangerService.changeMode(anything(), anything())).never(); + verify(deviceMapService.setMapZones(anything(), anything())).never(); + verify(deviceCleaningService.zoneCleaning(anything(), anything())).never(); + }); + + it('should clean zones', async function () { + const zone = new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 1, y: 2 })] }); + const command = new CleanZonesCommand({ deviceId: new ID(1), zones: [zone] }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(instance(device) as DeviceWithMap); + when(device.map).thenReturn(instance(deviceMap)); + + await commandHandler.handle(command); + + verify( + deviceModeChangerService.changeMode( + instance(packetConnection), + deepEqual(new DeviceMode(DeviceModeValue.Zone)), + ), + ).once(); + verify(deviceMapService.setMapZones(instance(packetConnection), deepEqual([zone]))).once(); + verify(deviceCleaningService.zoneCleaning(instance(packetConnection), ModeCtrlValue.Start)).once(); + }); + + it('should throw an error when device has no map', async function () { + const zone = new Zone({ id: new ID(1), coordinates: [new MapCoordinate({ x: 1, y: 2 })] }); + const command = new CleanZonesCommand({ deviceId: new ID(1), zones: [zone] }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + when(packetConnection.device).thenReturn(instance(device) as DeviceWithMap); + when(device.map).thenReturn(undefined); + + await expect(commandHandler.handle(command)).to.be.rejectedWith( + DomainException, + 'Unable to clean zones without a device with a map', + ); + + verify(deviceModeChangerService.changeMode(anything(), anything())).never(); + verify(deviceMapService.setMapZones(anything(), anything())).never(); + verify(deviceCleaningService.zoneCleaning(anything(), anything())).never(); + }); + }); +}); + +interface DeviceWithMap extends Device { + map: DeviceMap; +} diff --git a/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.ts b/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.ts new file mode 100644 index 00000000..22531cf3 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/clean-zones.command-handler.ts @@ -0,0 +1,46 @@ +import { DeviceMode, DeviceModeValue } from '@agnoc/domain'; +import { DomainException } from '@agnoc/toolkit'; +import { ModeCtrlValue } from '../services/device-mode-changer.service'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceCleaningService } from '../services/device-cleaning.service'; +import type { DeviceMapService } from '../services/device-map.service'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, CleanZonesCommand, ConnectionWithDevice, Device, DeviceMap } from '@agnoc/domain'; + +export class CleanZonesCommandHandler implements CommandHandler { + readonly forName = 'CleanZonesCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceModeChangerService: DeviceModeChangerService, + private readonly deviceMapService: DeviceMapService, + private readonly deviceCleaningService: DeviceCleaningService, + ) {} + + async handle(event: CleanZonesCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + this.assertDeviceMap(connection); + + await this.deviceModeChangerService.changeMode(connection, new DeviceMode(DeviceModeValue.Zone)); + await this.deviceMapService.setMapZones(connection, event.zones); + await this.deviceCleaningService.zoneCleaning(connection, ModeCtrlValue.Start); + } + + assertDeviceMap( + connection: PacketConnection & ConnectionWithDevice, + ): asserts connection is PacketConnection & ConnectionWithDevice { + if (!connection.device.map) { + throw new DomainException('Unable to clean zones without a device with a map'); + } + } +} + +interface DeviceWithMap extends Device { + map: DeviceMap; +} diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index fb0d6d92..334b06c7 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -8,6 +8,8 @@ import { PayloadDataParserService, PacketFactory, } from '@agnoc/transport-tcp'; +import { CleanSpotCommandHandler } from './command-handlers/clean-spot.command-handler'; +import { CleanZonesCommandHandler } from './command-handlers/clean-zones.command-handler'; import { LocateDeviceCommandHandler } from './command-handlers/locate-device.command-handler'; import { PauseCleaningCommandHandler } from './command-handlers/pause-cleaning.command-handler'; import { ResetConsumableCommandHandler } from './command-handlers/reset-consumable.command-handler'; @@ -187,6 +189,13 @@ export class TCPServer implements Server { // Command event handlers this.commandQueryHandlerRegistry.register( + new CleanSpotCommandHandler(packetConnectionFinderService, deviceModeChangerService, deviceCleaningService), + new CleanZonesCommandHandler( + packetConnectionFinderService, + deviceModeChangerService, + deviceMapService, + deviceCleaningService, + ), new GetDeviceConsumablesQueryHandler(packetConnectionFinderService, this.deviceRepository), new LocateDeviceCommandHandler(packetConnectionFinderService), new PauseCleaningCommandHandler(packetConnectionFinderService, deviceCleaningService), diff --git a/packages/domain/src/commands/clean-spot.command.test.ts b/packages/domain/src/commands/clean-spot.command.test.ts new file mode 100644 index 00000000..db2a382c --- /dev/null +++ b/packages/domain/src/commands/clean-spot.command.test.ts @@ -0,0 +1,53 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { givenSomeMapCoordinateProps } from '../test-support'; +import { MapCoordinate } from '../value-objects/map-coordinate.value-object'; +import { CleanSpotCommand } from './clean-spot.command'; +import type { CleanSpotCommandInput } from './clean-spot.command'; + +describe('CleanSpotCommand', function () { + it('should be created', function () { + const input = givenACleanSpotCommandInput(); + const command = new CleanSpotCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.spot).to.be.equal(input.spot); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new CleanSpotCommand({ ...givenACleanSpotCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for CleanSpotCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new CleanSpotCommand({ ...givenACleanSpotCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of CleanSpotCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'spot' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new CleanSpotCommand({ ...givenACleanSpotCommandInput(), spot: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'spot' for CleanSpotCommand not provided`, + ); + }); + + it("should throw an error when 'spot' is not an MapCoordinate", function () { + // @ts-expect-error - invalid property + expect(() => new CleanSpotCommand({ ...givenACleanSpotCommandInput(), spot: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'spot' of CleanSpotCommand is not an instance of MapCoordinate`, + ); + }); +}); + +function givenACleanSpotCommandInput(): CleanSpotCommandInput { + return { deviceId: ID.generate(), spot: new MapCoordinate(givenSomeMapCoordinateProps()) }; +} diff --git a/packages/domain/src/commands/clean-spot.command.ts b/packages/domain/src/commands/clean-spot.command.ts new file mode 100644 index 00000000..6d24623b --- /dev/null +++ b/packages/domain/src/commands/clean-spot.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { MapCoordinate } from '../value-objects/map-coordinate.value-object'; + +/** Input for the command cleaning a spot. */ +export interface CleanSpotCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Coordinate of the spot to clean. */ + spot: MapCoordinate; +} + +/** Command for starting the cleaning process of a device. */ +export class CleanSpotCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the position of the spot to clean. */ + get spot(): MapCoordinate { + return this.props.spot; + } + + protected validate(props: CleanSpotCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'spot'); + this.validateInstanceProp(props, 'spot', MapCoordinate); + } +} diff --git a/packages/domain/src/commands/clean-zones.command.test.ts b/packages/domain/src/commands/clean-zones.command.test.ts new file mode 100644 index 00000000..3ae0cc8e --- /dev/null +++ b/packages/domain/src/commands/clean-zones.command.test.ts @@ -0,0 +1,61 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { Zone } from '../entities/zone.entity'; +import { givenSomeZoneProps } from '../test-support'; +import { CleanZonesCommand } from './clean-zones.command'; +import type { CleanZonesCommandInput } from './clean-zones.command'; + +describe('CleanZonesCommand', function () { + it('should be created', function () { + const input = givenACleanZonesCommandInput(); + const command = new CleanZonesCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.zones).to.be.equal(input.zones); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new CleanZonesCommand({ ...givenACleanZonesCommandInput(), deviceId: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'deviceId' for CleanZonesCommand not provided`, + ); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + // @ts-expect-error - invalid property + expect(() => new CleanZonesCommand({ ...givenACleanZonesCommandInput(), deviceId: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of CleanZonesCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'zones' is not provided", function () { + // @ts-expect-error - missing property + expect(() => new CleanZonesCommand({ ...givenACleanZonesCommandInput(), zones: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'zones' for CleanZonesCommand not provided`, + ); + }); + + it("should throw an error when 'zones' is not an array", function () { + // @ts-expect-error - invalid property + expect(() => new CleanZonesCommand({ ...givenACleanZonesCommandInput(), zones: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'zones' of CleanZonesCommand is not an array`, + ); + }); + + it("should throw an error when 'zones' is not an array of Zone", function () { + // @ts-expect-error - invalid property + expect(() => new CleanZonesCommand({ ...givenACleanZonesCommandInput(), zones: ['foo'] })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'zones' of CleanZonesCommand is not an array of Zone`, + ); + }); +}); + +function givenACleanZonesCommandInput(): CleanZonesCommandInput { + return { deviceId: ID.generate(), zones: [new Zone(givenSomeZoneProps())] }; +} diff --git a/packages/domain/src/commands/clean-zones.command.ts b/packages/domain/src/commands/clean-zones.command.ts new file mode 100644 index 00000000..6125267f --- /dev/null +++ b/packages/domain/src/commands/clean-zones.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { Zone } from '../entities/zone.entity'; + +/** Input for the command cleaning zones. */ +export interface CleanZonesCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Zones to clean. */ + zones: Zone[]; +} + +/** Command for starting the cleaning process of a device. */ +export class CleanZonesCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the zones to clean. */ + get zones(): Zone[] { + return this.props.zones; + } + + protected validate(props: CleanZonesCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'zones'); + this.validateArrayProp(props, 'zones', Zone); + } +} diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index 0c31031d..f502496a 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,3 +1,5 @@ +import type { CleanSpotCommand } from './clean-spot.command'; +import type { CleanZonesCommand } from './clean-zones.command'; import type { LocateDeviceCommand } from './locate-device.command'; import type { PauseCleaningCommand } from './pause-cleaning.command'; import type { ResetConsumableCommand } from './reset-consumable.command'; @@ -12,6 +14,8 @@ import type { StopCleaningCommand } from './stop-cleaning.command'; /** Commands that can be executed. */ export type Commands = { + CleanSpotCommand: CleanSpotCommand; + CleanZonesCommand: CleanZonesCommand; LocateDeviceCommand: LocateDeviceCommand; PauseCleaningCommand: PauseCleaningCommand; ResetConsumableCommand: ResetConsumableCommand; diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 73c3e734..e5ab507b 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -1,5 +1,7 @@ export * from './aggregate-roots/connection.aggregate-root'; export * from './aggregate-roots/device.aggregate-root'; +export * from './commands/clean-spot.command'; +export * from './commands/clean-zones.command'; export * from './commands/commands'; export * from './commands/locate-device.command'; export * from './commands/pause-cleaning.command'; From f183fe9406c0a38938a5c31421e454f1c0254425 Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 11 Apr 2023 08:59:51 +0200 Subject: [PATCH 35/38] feat: add set device mode command --- .../set-device-mode.command-handler.test.ts | 54 +++++++++++++++++++ .../set-device-mode.command-handler.ts | 22 ++++++++ packages/adapter-tcp/src/tcp.server.ts | 2 + packages/domain/src/commands/commands.ts | 2 + .../commands/set-device-mode.command.test.ts | 54 +++++++++++++++++++ .../src/commands/set-device-mode.command.ts | 30 +++++++++++ packages/domain/src/index.ts | 1 + 7 files changed, 165 insertions(+) create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.test.ts create mode 100644 packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.ts create mode 100644 packages/domain/src/commands/set-device-mode.command.test.ts create mode 100644 packages/domain/src/commands/set-device-mode.command.ts diff --git a/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.test.ts b/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.test.ts new file mode 100644 index 00000000..77821b30 --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.test.ts @@ -0,0 +1,54 @@ +import { DeviceMode, DeviceModeValue, SetDeviceModeCommand } from '@agnoc/domain'; +import { ID } from '@agnoc/toolkit'; +import { imock, instance, when, verify, anything } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { SetDeviceModeCommandHandler } from './set-device-mode.command-handler'; +import type { PacketConnection } from '../aggregate-roots/packet-connection.aggregate-root'; +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { ConnectionWithDevice } from '@agnoc/domain'; + +describe('SetDeviceModeCommandHandler', function () { + let deviceModeChangerService: DeviceModeChangerService; + let packetConnectionFinderService: PacketConnectionFinderService; + let commandHandler: SetDeviceModeCommandHandler; + let packetConnection: PacketConnection & ConnectionWithDevice; + + beforeEach(function () { + deviceModeChangerService = imock(); + packetConnectionFinderService = imock(); + commandHandler = new SetDeviceModeCommandHandler( + instance(packetConnectionFinderService), + instance(deviceModeChangerService), + ); + packetConnection = imock(); + }); + + it('should define the name', function () { + expect(commandHandler.forName).to.be.equal('SetDeviceModeCommand'); + }); + + describe('#handle()', function () { + it('should change and set device mode', async function () { + const mode = new DeviceMode(DeviceModeValue.Mop); + const command = new SetDeviceModeCommand({ deviceId: new ID(1), mode }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(instance(packetConnection)); + + await commandHandler.handle(command); + + verify(deviceModeChangerService.changeMode(instance(packetConnection), mode)).once(); + }); + + it('should do nothing when no connection is found', async function () { + const mode = new DeviceMode(DeviceModeValue.Mop); + const command = new SetDeviceModeCommand({ deviceId: new ID(1), mode }); + + when(packetConnectionFinderService.findByDeviceId(anything())).thenResolve(undefined); + + await commandHandler.handle(command); + + verify(deviceModeChangerService.changeMode(anything(), anything())).never(); + }); + }); +}); diff --git a/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.ts b/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.ts new file mode 100644 index 00000000..63b2b3fc --- /dev/null +++ b/packages/adapter-tcp/src/command-handlers/set-device-mode.command-handler.ts @@ -0,0 +1,22 @@ +import type { DeviceModeChangerService } from '../services/device-mode-changer.service'; +import type { PacketConnectionFinderService } from '../services/packet-connection-finder.service'; +import type { CommandHandler, SetDeviceModeCommand } from '@agnoc/domain'; + +export class SetDeviceModeCommandHandler implements CommandHandler { + readonly forName = 'SetDeviceModeCommand'; + + constructor( + private readonly packetConnectionFinderService: PacketConnectionFinderService, + private readonly deviceModeChangerService: DeviceModeChangerService, + ) {} + + async handle(event: SetDeviceModeCommand): Promise { + const connection = await this.packetConnectionFinderService.findByDeviceId(event.deviceId); + + if (!connection) { + return; + } + + await this.deviceModeChangerService.changeMode(connection, event.mode); + } +} diff --git a/packages/adapter-tcp/src/tcp.server.ts b/packages/adapter-tcp/src/tcp.server.ts index 334b06c7..f48352e1 100644 --- a/packages/adapter-tcp/src/tcp.server.ts +++ b/packages/adapter-tcp/src/tcp.server.ts @@ -15,6 +15,7 @@ import { PauseCleaningCommandHandler } from './command-handlers/pause-cleaning.c import { ResetConsumableCommandHandler } from './command-handlers/reset-consumable.command-handler'; import { ReturnHomeCommandHandler } from './command-handlers/return-home.command-handler'; import { SetCarpetModeCommandHandler } from './command-handlers/set-carpet-mode.command-handler'; +import { SetDeviceModeCommandHandler } from './command-handlers/set-device-mode.command-handler'; import { SetDeviceQuietHoursCommandHandler } from './command-handlers/set-device-quiet-hours.command-handler'; import { SetDeviceVoiceCommandHandler } from './command-handlers/set-device-voice.command-handler'; import { SetFanSpeedCommandHandler } from './command-handlers/set-fan-speed.command-handler'; @@ -202,6 +203,7 @@ export class TCPServer implements Server { new ResetConsumableCommandHandler(packetConnectionFinderService, this.deviceRepository), new ReturnHomeCommandHandler(packetConnectionFinderService), new SetCarpetModeCommandHandler(packetConnectionFinderService, this.deviceRepository), + new SetDeviceModeCommandHandler(packetConnectionFinderService, deviceModeChangerService), new SetDeviceQuietHoursCommandHandler(packetConnectionFinderService), new SetDeviceVoiceCommandHandler(packetConnectionFinderService, voiceSettingMapper, this.deviceRepository), new SetFanSpeedCommandHandler(packetConnectionFinderService, deviceFanSpeedMapper, this.deviceRepository), diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index f502496a..689dfeb8 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -5,6 +5,7 @@ import type { PauseCleaningCommand } from './pause-cleaning.command'; import type { ResetConsumableCommand } from './reset-consumable.command'; import type { ReturnHomeCommand } from './return-home.command'; import type { SetCarpetModeCommand } from './set-carpet-mode.command'; +import type { SetDeviceModeCommand } from './set-device-mode.command'; import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; import type { SetDeviceVoiceCommand } from './set-device-voice.command'; import type { SetFanSpeedCommand } from './set-fan-speed.command'; @@ -21,6 +22,7 @@ export type Commands = { ResetConsumableCommand: ResetConsumableCommand; ReturnHomeCommand: ReturnHomeCommand; SetCarpetModeCommand: SetCarpetModeCommand; + SetDeviceModeCommand: SetDeviceModeCommand; SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; SetDeviceVoiceCommand: SetDeviceVoiceCommand; SetFanSpeedCommand: SetFanSpeedCommand; diff --git a/packages/domain/src/commands/set-device-mode.command.test.ts b/packages/domain/src/commands/set-device-mode.command.test.ts new file mode 100644 index 00000000..81397268 --- /dev/null +++ b/packages/domain/src/commands/set-device-mode.command.test.ts @@ -0,0 +1,54 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Command, ID } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { DeviceMode, DeviceModeValue } from '../domain-primitives/device-mode.domain-primitive'; +import { SetDeviceModeCommand } from './set-device-mode.command'; +import type { SetDeviceModeCommandInput } from './set-device-mode.command'; + +describe('SetDeviceModeCommand', function () { + it('should be created', function () { + const input = givenASetDeviceModeCommandInput(); + const command = new SetDeviceModeCommand(input); + + expect(command).to.be.instanceOf(Command); + expect(command.deviceId).to.be.equal(input.deviceId); + expect(command.mode).to.be.equal(input.mode); + }); + + it("should throw an error when 'deviceId' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceModeCommand({ ...givenASetDeviceModeCommandInput(), deviceId: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'deviceId' for SetDeviceModeCommand not provided`); + }); + + it("should throw an error when 'deviceId' is not an ID", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceModeCommand({ ...givenASetDeviceModeCommandInput(), deviceId: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'deviceId' of SetDeviceModeCommand is not an instance of ID`, + ); + }); + + it("should throw an error when 'mode' is not provided", function () { + expect( + // @ts-expect-error - missing property + () => new SetDeviceModeCommand({ ...givenASetDeviceModeCommandInput(), mode: undefined }), + ).to.throw(ArgumentNotProvidedException, `Property 'mode' for SetDeviceModeCommand not provided`); + }); + + it("should throw an error when 'mode' is not an DeviceMode", function () { + expect( + // @ts-expect-error - invalid property + () => new SetDeviceModeCommand({ ...givenASetDeviceModeCommandInput(), mode: 'foo' }), + ).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'mode' of SetDeviceModeCommand is not an instance of DeviceMode`, + ); + }); +}); + +function givenASetDeviceModeCommandInput(): SetDeviceModeCommandInput { + return { deviceId: ID.generate(), mode: new DeviceMode(DeviceModeValue.None) }; +} diff --git a/packages/domain/src/commands/set-device-mode.command.ts b/packages/domain/src/commands/set-device-mode.command.ts new file mode 100644 index 00000000..68264fb9 --- /dev/null +++ b/packages/domain/src/commands/set-device-mode.command.ts @@ -0,0 +1,30 @@ +import { Command, ID } from '@agnoc/toolkit'; +import { DeviceMode } from '../domain-primitives/device-mode.domain-primitive'; + +/** Input for the command setting the mode of a device. */ +export interface SetDeviceModeCommandInput { + /** ID of the device. */ + deviceId: ID; + /** Device mode setting. */ + mode: DeviceMode; +} + +/** Command for stopping the cleaning process of a device. */ +export class SetDeviceModeCommand extends Command { + /** Returns the ID of the device. */ + get deviceId(): ID { + return this.props.deviceId; + } + + /** Returns the device mode setting. */ + get mode(): DeviceMode { + return this.props.mode; + } + + protected validate(props: SetDeviceModeCommandInput): void { + this.validateDefinedProp(props, 'deviceId'); + this.validateInstanceProp(props, 'deviceId', ID); + this.validateDefinedProp(props, 'mode'); + this.validateInstanceProp(props, 'mode', DeviceMode); + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index e5ab507b..a0c1bd14 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -8,6 +8,7 @@ export * from './commands/pause-cleaning.command'; export * from './commands/reset-consumable.command'; export * from './commands/return-home.command'; export * from './commands/set-carpet-mode.command'; +export * from './commands/set-device-mode.command'; export * from './commands/set-device-quiet-hours.command'; export * from './commands/set-device-voice.command'; export * from './commands/set-fan-speed.command'; From 14e98a535babdebb85bf269269d27cecfe7f337b Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 11 Apr 2023 11:06:32 +0200 Subject: [PATCH 36/38] docs(toolkit): add type docs --- .../toolkit/src/adapters/memory.adapter.ts | 1 + .../toolkit/src/base-classes/adapter.base.ts | 1 + .../toolkit/src/base-classes/command.base.ts | 1 + .../src/base-classes/domain-event.base.ts | 5 +++ .../src/base-classes/domain-primitive.base.ts | 12 ++++--- .../toolkit/src/base-classes/entity.base.ts | 3 ++ .../src/base-classes/event-bus.base.ts | 1 + .../src/base-classes/exception.base.ts | 6 ++++ .../toolkit/src/base-classes/factory.base.ts | 1 + .../toolkit/src/base-classes/query.base.ts | 1 + .../src/base-classes/repository.base.ts | 1 + .../toolkit/src/base-classes/server.base.ts | 1 + .../toolkit/src/base-classes/task.base.ts | 6 ++++ .../src/base-classes/validatable.base.ts | 11 +++++-- .../src/decorators/bind.decorator.test.ts | 31 ------------------- .../toolkit/src/decorators/bind.decorator.ts | 25 --------------- .../exceptions/argument-invalid.exception.ts | 1 + .../argument-not-provided.exception.ts | 1 + .../argument-out-of-range.exception.ts | 1 + .../src/exceptions/domain.exception.ts | 1 + .../exceptions/not-implemented.exception.ts | 1 + .../src/exceptions/timeout.exception.ts | 1 + packages/toolkit/src/index.ts | 2 +- .../src/streams/buffer-writer.stream.ts | 3 +- .../toolkit/src/types/constructor.type.ts | 1 + .../toolkit/src/types/deep-partial.type.ts | 1 + .../src/types/instance-type-props.type.ts | 21 +++++++++++++ .../toolkit/src/types/object-literal.type.ts | 1 + .../toolkit/src/types/require-one.type.ts | 2 ++ packages/toolkit/src/types/value-of.type.ts | 1 + .../src/utils/convert-props-to-object.util.ts | 2 ++ packages/toolkit/src/utils/debug.util.ts | 1 + .../toolkit/src/utils/flip-object.util.ts | 4 ++- packages/toolkit/src/utils/has-key.util.ts | 1 + .../toolkit/src/utils/interpolate.util.ts | 4 +++ packages/toolkit/src/utils/is-empty.util.ts | 13 ++++++++ packages/toolkit/src/utils/is-object.util.ts | 1 + packages/toolkit/src/utils/is-present.util.ts | 1 + packages/toolkit/src/utils/stream.util.ts | 20 ++++++++++++ .../toolkit/src/utils/to-dash-case.util.ts | 1 + 40 files changed, 128 insertions(+), 65 deletions(-) delete mode 100644 packages/toolkit/src/decorators/bind.decorator.test.ts delete mode 100644 packages/toolkit/src/decorators/bind.decorator.ts create mode 100644 packages/toolkit/src/types/instance-type-props.type.ts diff --git a/packages/toolkit/src/adapters/memory.adapter.ts b/packages/toolkit/src/adapters/memory.adapter.ts index d4f4177c..cfac612f 100644 --- a/packages/toolkit/src/adapters/memory.adapter.ts +++ b/packages/toolkit/src/adapters/memory.adapter.ts @@ -1,6 +1,7 @@ import { Adapter } from '../base-classes/adapter.base'; import type { ID } from '../domain-primitives/id.domain-primitive'; +/** Adapter for storing data in memory. */ export class MemoryAdapter extends Adapter { private readonly data = new Map(); diff --git a/packages/toolkit/src/base-classes/adapter.base.ts b/packages/toolkit/src/base-classes/adapter.base.ts index f44aa03e..f02dd9bc 100644 --- a/packages/toolkit/src/base-classes/adapter.base.ts +++ b/packages/toolkit/src/base-classes/adapter.base.ts @@ -1,5 +1,6 @@ import type { ID } from '../domain-primitives/id.domain-primitive'; +/** Base class for all adapters. */ export abstract class Adapter { abstract getAll(): unknown[]; abstract get(id: ID): unknown; diff --git a/packages/toolkit/src/base-classes/command.base.ts b/packages/toolkit/src/base-classes/command.base.ts index 186a3276..0e0f1f2d 100644 --- a/packages/toolkit/src/base-classes/command.base.ts +++ b/packages/toolkit/src/base-classes/command.base.ts @@ -1,3 +1,4 @@ import { Task } from './task.base'; +/** Base class for commands. */ export abstract class Command extends Task {} diff --git a/packages/toolkit/src/base-classes/domain-event.base.ts b/packages/toolkit/src/base-classes/domain-event.base.ts index c78e560f..580138e9 100644 --- a/packages/toolkit/src/base-classes/domain-event.base.ts +++ b/packages/toolkit/src/base-classes/domain-event.base.ts @@ -1,14 +1,19 @@ import { ID } from '../domain-primitives/id.domain-primitive'; import { Validatable } from './validatable.base'; +/** Properties for domain events. */ export interface DomainEventProps { + /** ID of the aggregate the event belongs to. */ aggregateId: ID; } +/** Metadata for domain events. */ export interface DomainEventMetadata { + /** Timestamp of the event. */ timestamp: number; } +/** Base class for domain events. */ export abstract class DomainEvent extends Validatable { readonly id = ID.generate(); readonly eventName = this.constructor.name; diff --git a/packages/toolkit/src/base-classes/domain-primitive.base.ts b/packages/toolkit/src/base-classes/domain-primitive.base.ts index 6dbae8eb..ccdec1f6 100644 --- a/packages/toolkit/src/base-classes/domain-primitive.base.ts +++ b/packages/toolkit/src/base-classes/domain-primitive.base.ts @@ -1,13 +1,17 @@ import { ArgumentInvalidException } from '../exceptions/argument-invalid.exception'; import { ValueObject } from './value-object.base'; -export type Primitives = string | number | boolean | bigint | Date; -export interface DomainPrimitiveProps { +/** Possible domain primitive types. */ +export type DomainPrimitives = string | number | boolean | bigint | Date; + +/** Props for the DomainPrimitive class. */ +export interface DomainPrimitiveProps { + /** The value of the primitive. */ value: T; } /** Abstract base class that provides basic tools for building the domain primitives of the domain. */ -export abstract class DomainPrimitive extends ValueObject> { +export abstract class DomainPrimitive extends ValueObject> { /** Checks if the provided value is a primitive value. */ constructor(value: T) { checkIfPrimitiveValue(new.target.name, value); @@ -38,7 +42,7 @@ export abstract class DomainPrimitive extends ValueObject< protected abstract override validate(props: DomainPrimitiveProps): void; } -function isPrimitiveValue(value: unknown): value is Primitives { +function isPrimitiveValue(value: unknown): value is DomainPrimitives { return ( typeof value === 'string' || typeof value === 'number' || diff --git a/packages/toolkit/src/base-classes/entity.base.ts b/packages/toolkit/src/base-classes/entity.base.ts index 57670801..6aeb6dbd 100644 --- a/packages/toolkit/src/base-classes/entity.base.ts +++ b/packages/toolkit/src/base-classes/entity.base.ts @@ -3,10 +3,13 @@ import { convertPropsToObject } from '../utils/convert-props-to-object.util'; import { isPresent } from '../utils/is-present.util'; import { Validatable } from './validatable.base'; +/** Props for all entities. */ export interface EntityProps { + /** ID of the entity. */ id: ID; } +/** Base class for all entities. */ export abstract class Entity extends Validatable { constructor(props: T) { super(props); diff --git a/packages/toolkit/src/base-classes/event-bus.base.ts b/packages/toolkit/src/base-classes/event-bus.base.ts index 351f7023..5f81e91a 100644 --- a/packages/toolkit/src/base-classes/event-bus.base.ts +++ b/packages/toolkit/src/base-classes/event-bus.base.ts @@ -1,4 +1,5 @@ import Emittery from 'emittery'; +/** Base class for event bus. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export abstract class EventBus extends Emittery {} diff --git a/packages/toolkit/src/base-classes/exception.base.ts b/packages/toolkit/src/base-classes/exception.base.ts index d0a7495a..f1f6831c 100644 --- a/packages/toolkit/src/base-classes/exception.base.ts +++ b/packages/toolkit/src/base-classes/exception.base.ts @@ -1,10 +1,16 @@ import type { ObjectLiteral } from '../types/object-literal.type'; +/** JSON representation of an exception. */ export interface JSONException { + /** Name of the exception. */ name: string; + /** Message of the exception. */ message: string; + /** Stack trace of the exception. */ stack?: string; + /** Metadata of the exception. */ metadata?: ObjectLiteral; + /** Cause of the exception. */ cause?: JSONException | unknown; } diff --git a/packages/toolkit/src/base-classes/factory.base.ts b/packages/toolkit/src/base-classes/factory.base.ts index ba605813..3525a06b 100644 --- a/packages/toolkit/src/base-classes/factory.base.ts +++ b/packages/toolkit/src/base-classes/factory.base.ts @@ -1,3 +1,4 @@ +/** Base class for factories. */ export abstract class Factory { abstract create(...args: unknown[]): T; } diff --git a/packages/toolkit/src/base-classes/query.base.ts b/packages/toolkit/src/base-classes/query.base.ts index 62bcd5b9..9b1fcd2f 100644 --- a/packages/toolkit/src/base-classes/query.base.ts +++ b/packages/toolkit/src/base-classes/query.base.ts @@ -1,3 +1,4 @@ import { Task } from './task.base'; +/** Base class for all queries. */ export abstract class Query extends Task {} diff --git a/packages/toolkit/src/base-classes/repository.base.ts b/packages/toolkit/src/base-classes/repository.base.ts index d13b534f..7ae6e85a 100644 --- a/packages/toolkit/src/base-classes/repository.base.ts +++ b/packages/toolkit/src/base-classes/repository.base.ts @@ -5,6 +5,7 @@ import type { EntityProps } from './entity.base'; import type { EventBus } from './event-bus.base'; import type { ID } from '../domain-primitives/id.domain-primitive'; +/** Base class for repositories. */ export abstract class Repository> { constructor(private readonly eventBus: EventBus, protected readonly adapter: Adapter) {} diff --git a/packages/toolkit/src/base-classes/server.base.ts b/packages/toolkit/src/base-classes/server.base.ts index de40bfa6..e023b06c 100644 --- a/packages/toolkit/src/base-classes/server.base.ts +++ b/packages/toolkit/src/base-classes/server.base.ts @@ -1,3 +1,4 @@ +/** Base class for server implementations. */ export abstract class Server { abstract listen(options?: unknown): Promise; abstract close(): Promise; diff --git a/packages/toolkit/src/base-classes/task.base.ts b/packages/toolkit/src/base-classes/task.base.ts index f0c639a4..ba19b621 100644 --- a/packages/toolkit/src/base-classes/task.base.ts +++ b/packages/toolkit/src/base-classes/task.base.ts @@ -1,13 +1,19 @@ import { ID } from '../domain-primitives/id.domain-primitive'; import { Validatable } from './validatable.base'; +/** Input props for tasks. */ export type TaskInput = T extends Task ? Input : never; + +/** Output props for tasks. */ export type TaskOutput = T extends Task ? Output : never; +/** Metadata for tasks. */ export interface TaskMetadata { + /** Timestamp of the task. */ timestamp: number; } +/** Base class for tasks. */ export abstract class Task extends Validatable implements Task { readonly id = ID.generate(); readonly taskName = this.constructor.name; diff --git a/packages/toolkit/src/base-classes/validatable.base.ts b/packages/toolkit/src/base-classes/validatable.base.ts index 9cbf018e..1a9134d5 100644 --- a/packages/toolkit/src/base-classes/validatable.base.ts +++ b/packages/toolkit/src/base-classes/validatable.base.ts @@ -39,7 +39,11 @@ export abstract class Validatable { } /** Checks whether a prop is a number. Optionally check if the number is contained in a range. */ - protected validateNumberProp(props: T, propName: K, range?: NumberRange): void { + protected validateNumberProp( + props: T, + propName: K, + range?: ValidateNumberRange, + ): void { const value = props[propName]; if (!isPresent(value)) { @@ -124,7 +128,10 @@ export abstract class Validatable { } } -export interface NumberRange { +/** Defines a range of numbers. */ +export interface ValidateNumberRange { + /** The minimum value of the range. */ min?: number; + /** The maximum value of the range. */ max?: number; } diff --git a/packages/toolkit/src/decorators/bind.decorator.test.ts b/packages/toolkit/src/decorators/bind.decorator.test.ts deleted file mode 100644 index f7f09e96..00000000 --- a/packages/toolkit/src/decorators/bind.decorator.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect } from 'chai'; -import { bind } from './bind.decorator'; - -describe('bind.decorator', function () { - it('binds a class method', function () { - class Foo { - wow = 1; - - @bind - bar() { - return this.wow; - } - } - - const foo = new Foo(); - // eslint-disable-next-line @typescript-eslint/unbound-method - const bar = foo.bar; - - expect(bar()).to.be.equal(foo.wow); - }); - - it('throws an error when applied to a class', function () { - expect(() => { - // @ts-expect-error bad signature - @bind - class Foo {} - - new Foo(); - }).to.throw(TypeError); - }); -}); diff --git a/packages/toolkit/src/decorators/bind.decorator.ts b/packages/toolkit/src/decorators/bind.decorator.ts deleted file mode 100644 index 94f557f5..00000000 --- a/packages/toolkit/src/decorators/bind.decorator.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -export function bind( - _: unknown, - propertyKey: string, - descriptor: TypedPropertyDescriptor, -): TypedPropertyDescriptor { - if (!descriptor || typeof descriptor.value !== 'function') { - throw new TypeError(`Only methods can be decorated with @bind. <${propertyKey}> is not a method!`); - } - - return { - configurable: true, - get(this: T): T { - const bound = (descriptor.value as Function).bind(this) as T; - - Object.defineProperty(this, propertyKey, { - value: bound, - configurable: true, - writable: true, - }); - - return bound; - }, - }; -} diff --git a/packages/toolkit/src/exceptions/argument-invalid.exception.ts b/packages/toolkit/src/exceptions/argument-invalid.exception.ts index cff5f15c..603b2700 100644 --- a/packages/toolkit/src/exceptions/argument-invalid.exception.ts +++ b/packages/toolkit/src/exceptions/argument-invalid.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when an argument is invalid. */ export class ArgumentInvalidException extends Exception {} diff --git a/packages/toolkit/src/exceptions/argument-not-provided.exception.ts b/packages/toolkit/src/exceptions/argument-not-provided.exception.ts index 0134f47a..fcb69685 100644 --- a/packages/toolkit/src/exceptions/argument-not-provided.exception.ts +++ b/packages/toolkit/src/exceptions/argument-not-provided.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when an argument is not provided. */ export class ArgumentNotProvidedException extends Exception {} diff --git a/packages/toolkit/src/exceptions/argument-out-of-range.exception.ts b/packages/toolkit/src/exceptions/argument-out-of-range.exception.ts index 74673adb..76c9b7f7 100644 --- a/packages/toolkit/src/exceptions/argument-out-of-range.exception.ts +++ b/packages/toolkit/src/exceptions/argument-out-of-range.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when an argument is out of range. */ export class ArgumentOutOfRangeException extends Exception {} diff --git a/packages/toolkit/src/exceptions/domain.exception.ts b/packages/toolkit/src/exceptions/domain.exception.ts index c06ea3cb..2497b669 100644 --- a/packages/toolkit/src/exceptions/domain.exception.ts +++ b/packages/toolkit/src/exceptions/domain.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when a domain error occurs. */ export class DomainException extends Exception {} diff --git a/packages/toolkit/src/exceptions/not-implemented.exception.ts b/packages/toolkit/src/exceptions/not-implemented.exception.ts index 9679b461..3eb2f348 100644 --- a/packages/toolkit/src/exceptions/not-implemented.exception.ts +++ b/packages/toolkit/src/exceptions/not-implemented.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when a method or function has not been implemented. */ export class NotImplementedException extends Exception {} diff --git a/packages/toolkit/src/exceptions/timeout.exception.ts b/packages/toolkit/src/exceptions/timeout.exception.ts index 47a1fb75..395194f4 100644 --- a/packages/toolkit/src/exceptions/timeout.exception.ts +++ b/packages/toolkit/src/exceptions/timeout.exception.ts @@ -1,3 +1,4 @@ import { Exception } from '../base-classes/exception.base'; +/** An exception that is thrown when a timeout occurs. */ export class TimeoutException extends Exception {} diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 386826e5..267f8f2a 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -18,7 +18,6 @@ export * from './base-classes/task-handler.base'; export * from './base-classes/task.base'; export * from './base-classes/validatable.base'; export * from './base-classes/value-object.base'; -export * from './decorators/bind.decorator'; export * from './domain-primitives/id.domain-primitive'; export * from './event-handler.registry'; export * from './exceptions/argument-invalid.exception'; @@ -32,6 +31,7 @@ export * from './streams/buffer-writer.stream'; export * from './task-handler.registry'; export * from './types/constructor.type'; export * from './types/deep-partial.type'; +export * from './types/instance-type-props.type'; export * from './types/object-literal.type'; export * from './types/require-one.type'; export * from './types/value-of.type'; diff --git a/packages/toolkit/src/streams/buffer-writer.stream.ts b/packages/toolkit/src/streams/buffer-writer.stream.ts index 1a53a9f5..f8a49250 100644 --- a/packages/toolkit/src/streams/buffer-writer.stream.ts +++ b/packages/toolkit/src/streams/buffer-writer.stream.ts @@ -1,7 +1,8 @@ import { Writable } from 'stream'; -export type Callback = (error?: Error | null) => void; +type Callback = (error?: Error | null) => void; +/** A stream that writes to a buffer. */ export class BufferWriter extends Writable { buffer: Buffer; diff --git a/packages/toolkit/src/types/constructor.type.ts b/packages/toolkit/src/types/constructor.type.ts index 2626110a..b3cb5461 100644 --- a/packages/toolkit/src/types/constructor.type.ts +++ b/packages/toolkit/src/types/constructor.type.ts @@ -1,2 +1,3 @@ +/** Generic constructor type. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructor = new (...args: any[]) => any; diff --git a/packages/toolkit/src/types/deep-partial.type.ts b/packages/toolkit/src/types/deep-partial.type.ts index 4fff1889..d0c1a94d 100644 --- a/packages/toolkit/src/types/deep-partial.type.ts +++ b/packages/toolkit/src/types/deep-partial.type.ts @@ -1,3 +1,4 @@ +/** Type an object to be partially deep. */ export type DeepPartial = { [P in keyof T]?: DeepPartial; }; diff --git a/packages/toolkit/src/types/instance-type-props.type.ts b/packages/toolkit/src/types/instance-type-props.type.ts new file mode 100644 index 00000000..519a3a08 --- /dev/null +++ b/packages/toolkit/src/types/instance-type-props.type.ts @@ -0,0 +1,21 @@ +import type { Constructor } from './constructor.type'; + +/** + * Returns the instance type of each property of a given type. + * + * @example + * ```typescript + * class A {} + * class B {} + * + * const Map = { + * A, + * B, + * }; + * + * type Map = InstanceTypeProps; // Map { A: A; B: B; } + * ``` + */ +export type InstanceTypeProps> = { + [K in keyof T]: InstanceType; +}; diff --git a/packages/toolkit/src/types/object-literal.type.ts b/packages/toolkit/src/types/object-literal.type.ts index 256d7367..847a89dd 100644 --- a/packages/toolkit/src/types/object-literal.type.ts +++ b/packages/toolkit/src/types/object-literal.type.ts @@ -1 +1,2 @@ +/** Helper type to create an object literal type. */ export type ObjectLiteral = Record; diff --git a/packages/toolkit/src/types/require-one.type.ts b/packages/toolkit/src/types/require-one.type.ts index 77d87bc9..166bebc4 100644 --- a/packages/toolkit/src/types/require-one.type.ts +++ b/packages/toolkit/src/types/require-one.type.ts @@ -1,8 +1,10 @@ +/** Require at least one of the given keys. */ export type RequireAtLeastOne = Pick> & { [K in Keys]-?: Required> & Partial>>; }[Keys]; +/** Require only one of the given keys. */ export type RequireOnlyOne = Pick> & { [K in Keys]-?: Required> & Partial, undefined>>; diff --git a/packages/toolkit/src/types/value-of.type.ts b/packages/toolkit/src/types/value-of.type.ts index e9ec02ba..a393ff69 100644 --- a/packages/toolkit/src/types/value-of.type.ts +++ b/packages/toolkit/src/types/value-of.type.ts @@ -1 +1,2 @@ +/** Get the value type of an map type. */ export type ValueOf = ObjectType[ValueType]; diff --git a/packages/toolkit/src/utils/convert-props-to-object.util.ts b/packages/toolkit/src/utils/convert-props-to-object.util.ts index 84c61afc..34aa893d 100644 --- a/packages/toolkit/src/utils/convert-props-to-object.util.ts +++ b/packages/toolkit/src/utils/convert-props-to-object.util.ts @@ -5,9 +5,11 @@ function convertToRaw(item: unknown): unknown { if (isObject(item) && typeof item.toJSON === 'function') { return item.toJSON(); } + return item; } +/** Converts an object to an serializable json object. */ export function convertPropsToObject(props: unknown): ObjectLiteral { if (!isObject(props)) { throw new TypeError(`Unable to convert props type <${typeof props}> to object`); diff --git a/packages/toolkit/src/utils/debug.util.ts b/packages/toolkit/src/utils/debug.util.ts index 52834501..4c2123cf 100644 --- a/packages/toolkit/src/utils/debug.util.ts +++ b/packages/toolkit/src/utils/debug.util.ts @@ -2,6 +2,7 @@ import path from 'path'; import logger from 'debug'; import type { Debugger } from 'debug'; +/** Create a debug instance from a filename. */ export function debug(filename: string): Debugger { const extname = path.extname(filename); const basename = path.basename(filename, extname); diff --git a/packages/toolkit/src/utils/flip-object.util.ts b/packages/toolkit/src/utils/flip-object.util.ts index 48f5b886..80561cbe 100644 --- a/packages/toolkit/src/utils/flip-object.util.ts +++ b/packages/toolkit/src/utils/flip-object.util.ts @@ -1,11 +1,13 @@ -export type AllValues> = { +type AllValues> = { [P in keyof T]: { key: P; value: T[P] }; }[keyof T]; +/** Type an object to be flipped. */ export type FlipObject> = { [P in AllValues['value']]: Extract, { value: P }>['key']; }; +/** Flip the keys and values of an object. */ export function flipObject>(obj: T): FlipObject { const entries = Object.entries(obj) as [keyof T, T[keyof T]][]; diff --git a/packages/toolkit/src/utils/has-key.util.ts b/packages/toolkit/src/utils/has-key.util.ts index bb347338..8143ba88 100644 --- a/packages/toolkit/src/utils/has-key.util.ts +++ b/packages/toolkit/src/utils/has-key.util.ts @@ -1,3 +1,4 @@ +/** Checks if the object has the given key. */ export function hasKey(obj: O, key: K): obj is O & Record { return Object.prototype.hasOwnProperty.call(obj, key); } diff --git a/packages/toolkit/src/utils/interpolate.util.ts b/packages/toolkit/src/utils/interpolate.util.ts index a7c1f32d..9c86401e 100644 --- a/packages/toolkit/src/utils/interpolate.util.ts +++ b/packages/toolkit/src/utils/interpolate.util.ts @@ -1,10 +1,14 @@ import { ArgumentOutOfRangeException } from '../exceptions/argument-out-of-range.exception'; +/** Range of values to interpolate from. */ export interface InterpolateRange { + /** Minimum value of the range. */ min: number; + /** Maximum value of the range. */ max: number; } +/** Interpolate a value from one range to another. */ export function interpolate(value: number, from: InterpolateRange, to: InterpolateRange): number { if (value < from.min || value > from.max) { throw new ArgumentOutOfRangeException( diff --git a/packages/toolkit/src/utils/is-empty.util.ts b/packages/toolkit/src/utils/is-empty.util.ts index 424fa0cf..19378478 100644 --- a/packages/toolkit/src/utils/is-empty.util.ts +++ b/packages/toolkit/src/utils/is-empty.util.ts @@ -1,3 +1,16 @@ +/** + * Checks if the value is an empty value. + * + * @example + * ```typescript + * isEmpty(null); // true + * isEmpty(undefined); // true + * isEmpty(''); // true + * isEmpty([]); // true + * isEmpty({}); // true + * isEmpty([null]); // true + * ``` + */ export function isEmpty(value: unknown): boolean { if (typeof value === 'number' || typeof value === 'boolean') { return false; diff --git a/packages/toolkit/src/utils/is-object.util.ts b/packages/toolkit/src/utils/is-object.util.ts index b60c0fb2..f891909e 100644 --- a/packages/toolkit/src/utils/is-object.util.ts +++ b/packages/toolkit/src/utils/is-object.util.ts @@ -1,6 +1,7 @@ import { isPresent } from './is-present.util'; import type { ObjectLiteral } from '../types/object-literal.type'; +/** Checks if the value is an object. */ export function isObject(obj: unknown): obj is ObjectLiteral { return isPresent(obj) && !Array.isArray(obj) && typeof obj === 'object'; } diff --git a/packages/toolkit/src/utils/is-present.util.ts b/packages/toolkit/src/utils/is-present.util.ts index 899659b3..1f38e86a 100644 --- a/packages/toolkit/src/utils/is-present.util.ts +++ b/packages/toolkit/src/utils/is-present.util.ts @@ -1,3 +1,4 @@ +/** Checks if the value is not null or undefined. */ export function isPresent(value: T): value is NonNullable { return value !== null && value !== undefined; } diff --git a/packages/toolkit/src/utils/stream.util.ts b/packages/toolkit/src/utils/stream.util.ts index d520562b..963a3f4d 100644 --- a/packages/toolkit/src/utils/stream.util.ts +++ b/packages/toolkit/src/utils/stream.util.ts @@ -72,18 +72,37 @@ function writeFn(size: number, method: M) { }; } +/** Read a byte (uint8) from the stream. */ export const readByte = readFn(1, 'readUInt8'); + +/** Read a short (uint16) from the stream. */ export const readShort = readFn(2, 'readUInt16LE'); + +/** Read a word (uint32) from the stream. */ export const readWord = readFn(4, 'readUInt32LE'); + +/** Read a float (float32) from the stream. */ export const readFloat = readFn(4, 'readFloatLE'); + +/** Read a long (uint64) from the stream. */ export const readLong = readFn(8, 'readBigUInt64LE'); +/** Write a byte (uint8) to the stream. */ export const writeByte = writeFn(1, 'writeUInt8'); + +/** Write a short (uint16) to the stream. */ export const writeShort = writeFn(2, 'writeUInt16LE'); + +/** Write a word (uint32) to the stream. */ export const writeWord = writeFn(4, 'writeUInt32LE'); + +/** Write a float (float32) to the stream. */ export const writeFloat = writeFn(4, 'writeFloatLE'); + +/** Write a long (uint64) to the stream. */ export const writeLong = writeFn(8, 'writeBigUInt64LE'); +/** Read a string from the stream. */ export function readString(stream: Readable): string { const length = readByte(stream); @@ -98,6 +117,7 @@ export function readString(stream: Readable): string { return ''; } +/** Write a string to the stream. */ export function writeString(stream: Writable, value: string): void { writeByte(stream, value.length); diff --git a/packages/toolkit/src/utils/to-dash-case.util.ts b/packages/toolkit/src/utils/to-dash-case.util.ts index 26531f61..f5fb3d8b 100644 --- a/packages/toolkit/src/utils/to-dash-case.util.ts +++ b/packages/toolkit/src/utils/to-dash-case.util.ts @@ -1,3 +1,4 @@ +/** Converts a string to dash case. */ export function toDashCase(str: string): string { return str .replace(/[_. ]/g, '-') From e863e7a2d6894daeec762a054351e0bad07f84df Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 11 Apr 2023 11:07:36 +0200 Subject: [PATCH 37/38] chore(domain): add command, query and domain event dictionaries --- packages/domain/src/commands/commands.ts | 66 ++++++++-------- .../domain/src/domain-events/domain-events.ts | 77 ++++++++++--------- packages/domain/src/queries/queries.ts | 13 ++-- 3 files changed, 83 insertions(+), 73 deletions(-) diff --git a/packages/domain/src/commands/commands.ts b/packages/domain/src/commands/commands.ts index 689dfeb8..8cf3520a 100644 --- a/packages/domain/src/commands/commands.ts +++ b/packages/domain/src/commands/commands.ts @@ -1,35 +1,39 @@ -import type { CleanSpotCommand } from './clean-spot.command'; -import type { CleanZonesCommand } from './clean-zones.command'; -import type { LocateDeviceCommand } from './locate-device.command'; -import type { PauseCleaningCommand } from './pause-cleaning.command'; -import type { ResetConsumableCommand } from './reset-consumable.command'; -import type { ReturnHomeCommand } from './return-home.command'; -import type { SetCarpetModeCommand } from './set-carpet-mode.command'; -import type { SetDeviceModeCommand } from './set-device-mode.command'; -import type { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; -import type { SetDeviceVoiceCommand } from './set-device-voice.command'; -import type { SetFanSpeedCommand } from './set-fan-speed.command'; -import type { SetWaterLevelCommand } from './set-water-level.command'; -import type { StartCleaningCommand } from './start-cleaning.command'; -import type { StopCleaningCommand } from './stop-cleaning.command'; +import { CleanSpotCommand } from './clean-spot.command'; +import { CleanZonesCommand } from './clean-zones.command'; +import { LocateDeviceCommand } from './locate-device.command'; +import { PauseCleaningCommand } from './pause-cleaning.command'; +import { ResetConsumableCommand } from './reset-consumable.command'; +import { ReturnHomeCommand } from './return-home.command'; +import { SetCarpetModeCommand } from './set-carpet-mode.command'; +import { SetDeviceModeCommand } from './set-device-mode.command'; +import { SetDeviceQuietHoursCommand } from './set-device-quiet-hours.command'; +import { SetDeviceVoiceCommand } from './set-device-voice.command'; +import { SetFanSpeedCommand } from './set-fan-speed.command'; +import { SetWaterLevelCommand } from './set-water-level.command'; +import { StartCleaningCommand } from './start-cleaning.command'; +import { StopCleaningCommand } from './stop-cleaning.command'; +import type { InstanceTypeProps } from '@agnoc/toolkit'; /** Commands that can be executed. */ -export type Commands = { - CleanSpotCommand: CleanSpotCommand; - CleanZonesCommand: CleanZonesCommand; - LocateDeviceCommand: LocateDeviceCommand; - PauseCleaningCommand: PauseCleaningCommand; - ResetConsumableCommand: ResetConsumableCommand; - ReturnHomeCommand: ReturnHomeCommand; - SetCarpetModeCommand: SetCarpetModeCommand; - SetDeviceModeCommand: SetDeviceModeCommand; - SetDeviceQuietHoursCommand: SetDeviceQuietHoursCommand; - SetDeviceVoiceCommand: SetDeviceVoiceCommand; - SetFanSpeedCommand: SetFanSpeedCommand; - SetWaterLevelCommand: SetWaterLevelCommand; - StartCleaningCommand: StartCleaningCommand; - StopCleaningCommand: StopCleaningCommand; -}; +export const Commands = { + CleanSpotCommand, + CleanZonesCommand, + LocateDeviceCommand, + PauseCleaningCommand, + ResetConsumableCommand, + ReturnHomeCommand, + SetCarpetModeCommand, + SetDeviceModeCommand, + SetDeviceQuietHoursCommand, + SetDeviceVoiceCommand, + SetFanSpeedCommand, + SetWaterLevelCommand, + StartCleaningCommand, + StopCleaningCommand, +} as const; + +/** Commands that can be executed. */ +export type Commands = InstanceTypeProps; /** Names of commands that can be executed. */ -export type CommandNames = keyof Commands; +export type CommandNames = keyof typeof Commands; diff --git a/packages/domain/src/domain-events/domain-events.ts b/packages/domain/src/domain-events/domain-events.ts index 97f06a32..703a36d6 100644 --- a/packages/domain/src/domain-events/domain-events.ts +++ b/packages/domain/src/domain-events/domain-events.ts @@ -1,41 +1,44 @@ -import type { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; -import type { DeviceBatteryChangedDomainEvent } from './device-battery-changed.domain-event'; -import type { DeviceCleanWorkChangedDomainEvent } from './device-clean-work-changed.domain-event'; -import type { DeviceConnectedDomainEvent } from './device-connected.domain-event'; -import type { DeviceCreatedDomainEvent } from './device-created.domain-event'; -import type { DeviceErrorChangedDomainEvent } from './device-error-changed.domain-event'; -import type { DeviceFanSpeedChangedDomainEvent } from './device-fan-speed-changed.domain-event'; -import type { DeviceLockedDomainEvent } from './device-locked.domain-event'; -import type { DeviceMapChangedDomainEvent } from './device-map-changed.domain-event'; -import type { DeviceMapPendingDomainEvent } from './device-map-pending.domain-event'; -import type { DeviceModeChangedDomainEvent } from './device-mode-changed.domain-event'; -import type { DeviceMopAttachedDomainEvent } from './device-mop-attached.domain-event'; -import type { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; -import type { DeviceOrdersChangedDomainEvent } from './device-orders-changed.domain-event'; -import type { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; -import type { DeviceStateChangedDomainEvent } from './device-state-changed.domain-event'; -import type { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; -import type { DeviceWaterLevelChangedDomainEvent } from './device-water-level-changed.domain-event'; +import { ConnectionDeviceChangedDomainEvent } from './connection-device-changed.domain-event'; +import { DeviceBatteryChangedDomainEvent } from './device-battery-changed.domain-event'; +import { DeviceCleanWorkChangedDomainEvent } from './device-clean-work-changed.domain-event'; +import { DeviceConnectedDomainEvent } from './device-connected.domain-event'; +import { DeviceCreatedDomainEvent } from './device-created.domain-event'; +import { DeviceErrorChangedDomainEvent } from './device-error-changed.domain-event'; +import { DeviceFanSpeedChangedDomainEvent } from './device-fan-speed-changed.domain-event'; +import { DeviceLockedDomainEvent } from './device-locked.domain-event'; +import { DeviceMapChangedDomainEvent } from './device-map-changed.domain-event'; +import { DeviceMapPendingDomainEvent } from './device-map-pending.domain-event'; +import { DeviceModeChangedDomainEvent } from './device-mode-changed.domain-event'; +import { DeviceMopAttachedDomainEvent } from './device-mop-attached.domain-event'; +import { DeviceNetworkChangedDomainEvent } from './device-network-changed.domain-event'; +import { DeviceOrdersChangedDomainEvent } from './device-orders-changed.domain-event'; +import { DeviceSettingsChangedDomainEvent } from './device-settings-changed.domain-event'; +import { DeviceStateChangedDomainEvent } from './device-state-changed.domain-event'; +import { DeviceVersionChangedDomainEvent } from './device-version-changed.domain-event'; +import { DeviceWaterLevelChangedDomainEvent } from './device-water-level-changed.domain-event'; +import type { InstanceTypeProps } from '@agnoc/toolkit'; -export type DomainEvents = { - ConnectionDeviceChangedDomainEvent: ConnectionDeviceChangedDomainEvent; - DeviceBatteryChangedDomainEvent: DeviceBatteryChangedDomainEvent; - DeviceCleanWorkChangedDomainEvent: DeviceCleanWorkChangedDomainEvent; - DeviceConnectedDomainEvent: DeviceConnectedDomainEvent; - DeviceCreatedDomainEvent: DeviceCreatedDomainEvent; - DeviceErrorChangedDomainEvent: DeviceErrorChangedDomainEvent; - DeviceFanSpeedChangedDomainEvent: DeviceFanSpeedChangedDomainEvent; - DeviceLockedDomainEvent: DeviceLockedDomainEvent; - DeviceMapChangedDomainEvent: DeviceMapChangedDomainEvent; - DeviceMapPendingDomainEvent: DeviceMapPendingDomainEvent; - DeviceModeChangedDomainEvent: DeviceModeChangedDomainEvent; - DeviceMopAttachedDomainEvent: DeviceMopAttachedDomainEvent; - DeviceNetworkChangedDomainEvent: DeviceNetworkChangedDomainEvent; - DeviceOrdersChangedDomainEvent: DeviceOrdersChangedDomainEvent; - DeviceSettingsChangedDomainEvent: DeviceSettingsChangedDomainEvent; - DeviceStateChangedDomainEvent: DeviceStateChangedDomainEvent; - DeviceVersionChangedDomainEvent: DeviceVersionChangedDomainEvent; - DeviceWaterLevelChangedDomainEvent: DeviceWaterLevelChangedDomainEvent; +export const DomainEvents = { + ConnectionDeviceChangedDomainEvent, + DeviceBatteryChangedDomainEvent, + DeviceCleanWorkChangedDomainEvent, + DeviceConnectedDomainEvent, + DeviceCreatedDomainEvent, + DeviceErrorChangedDomainEvent, + DeviceFanSpeedChangedDomainEvent, + DeviceLockedDomainEvent, + DeviceMapChangedDomainEvent, + DeviceMapPendingDomainEvent, + DeviceModeChangedDomainEvent, + DeviceMopAttachedDomainEvent, + DeviceNetworkChangedDomainEvent, + DeviceOrdersChangedDomainEvent, + DeviceSettingsChangedDomainEvent, + DeviceStateChangedDomainEvent, + DeviceVersionChangedDomainEvent, + DeviceWaterLevelChangedDomainEvent, }; +export type DomainEvents = InstanceTypeProps; + export type DomainEventNames = keyof DomainEvents; diff --git a/packages/domain/src/queries/queries.ts b/packages/domain/src/queries/queries.ts index 0a93b0a6..7cb5d4c2 100644 --- a/packages/domain/src/queries/queries.ts +++ b/packages/domain/src/queries/queries.ts @@ -1,9 +1,12 @@ -import type { FindDeviceQuery } from './find-device.query'; -import type { GetDeviceConsumablesQuery } from './get-device-consumables.query'; +import { FindDeviceQuery } from './find-device.query'; +import { GetDeviceConsumablesQuery } from './get-device-consumables.query'; +import type { InstanceTypeProps } from '@agnoc/toolkit'; -export type Queries = { - FindDeviceQuery: FindDeviceQuery; - GetDeviceConsumablesQuery: GetDeviceConsumablesQuery; +export const Queries = { + FindDeviceQuery, + GetDeviceConsumablesQuery, }; +export type Queries = InstanceTypeProps; + export type QueryNames = keyof Queries; From 968423dca42162fb55052809474e12cacd4a139e Mon Sep 17 00:00:00 2001 From: adrigzr Date: Tue, 11 Apr 2023 16:30:34 +0200 Subject: [PATCH 38/38] feat(cli): add basic REPL command --- packages/cli/README.md | 10 ++ packages/cli/package.json | 3 + packages/cli/src/cli.ts | 30 ++++++ .../cli/src/commands/repl.command.test.ts | 100 ++++++++++++++++++ packages/cli/src/commands/repl.command.ts | 45 ++++++++ packages/cli/test/acceptance/cli.test.ts | 6 ++ packages/cli/tsconfig.build.json | 8 +- packages/core/src/agnoc.server.ts | 6 +- .../find-devices.query-handler.test.ts | 35 ++++++ .../find-devices.query-handler.ts | 13 +++ packages/domain/src/index.ts | 1 + .../src/queries/find-devices.query.test.ts | 48 +++++++++ .../domain/src/queries/find-devices.query.ts | 17 +++ packages/domain/src/queries/queries.ts | 2 + .../src/base-classes/validatable.base.test.ts | 15 ++- .../src/base-classes/validatable.base.ts | 44 ++++---- 16 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 packages/cli/src/commands/repl.command.test.ts create mode 100644 packages/cli/src/commands/repl.command.ts create mode 100644 packages/core/src/query-handlers/find-devices.query-handler.test.ts create mode 100644 packages/core/src/query-handlers/find-devices.query-handler.ts create mode 100644 packages/domain/src/queries/find-devices.query.test.ts create mode 100644 packages/domain/src/queries/find-devices.query.ts diff --git a/packages/cli/README.md b/packages/cli/README.md index ad3a9dd9..2d0deff5 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -24,6 +24,16 @@ $ agnoc --help ## Commands +### REPL + +Start a simplified REPL to control devices using queries and commands. + +``` +$ agnoc repl +agnoc> { devices } = await server.trigger(new Queries.FindDevicesQuery()) +agnoc> await server.trigger(new Commands.LocateDeviceCommand({ deviceId: devices[0].id })) +``` + ### Wlan This command configures the wifi connection of the robot to the given one. diff --git a/packages/cli/package.json b/packages/cli/package.json index 1dd09fcb..a84c168d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -81,6 +81,9 @@ } }, "dependencies": { + "@agnoc/adapter-tcp": "^0.18.0-next.0", + "@agnoc/core": "^0.18.0-next.0", + "@agnoc/domain": "^0.18.0-next.0", "@agnoc/toolkit": "^0.18.0-next.0", "@agnoc/transport-tcp": "^0.18.0-next.0", "chalk": "^4.1.1", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index dd344d82..e67d01da 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,4 +1,9 @@ /* istanbul ignore file */ +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { start as startREPL } from 'node:repl'; +import { TCPServer } from '@agnoc/adapter-tcp'; +import { AgnocServer } from '@agnoc/core'; import { PayloadDataParserService, getProtobufRoot, @@ -13,6 +18,7 @@ import wifi from 'node-wifi'; import { DecodeCommand } from './commands/decode.command'; import { EncodeCommand } from './commands/encode.command'; import { ReadCommand } from './commands/read.command'; +import { REPLCommand } from './commands/repl.command'; import { WlanConfigCommand } from './commands/wlan-config.command'; import { WlanCommand } from './commands/wlan.command'; import type { Stdio } from './interfaces/stdio'; @@ -40,6 +46,17 @@ export function main(): void { const encodeCommand = new EncodeCommand(stdio, packetMapper); const wlanConfigCommand = new WlanConfigCommand('192.168.5.1', 6008); const wlanCommand = new WlanCommand(cliUx, wifi, wlanConfigCommand); + const agnocServer = new AgnocServer(); + + agnocServer.buildAdapter( + (container) => + new TCPServer( + container.deviceRepository, + container.connectionRepository, + container.domainEventHandlerRegistry, + container.commandQueryHandlerRegistry, + ), + ); function handleError(e: Error): void { cliUx.action.stop(chalk.red('!')); @@ -113,6 +130,19 @@ export function main(): void { cliUx.action.stop(); }); + program + .command('repl') + .description('start a REPL with a server listening for connections') + .option('-t, --history ', 'file to store the REPL history.', path.join(tmpdir(), 'agnoc-repl-history')) + .action(({ history }: { history: string }) => { + const repl = startREPL({ prompt: 'agnoc> ' }); + const replCommand = new REPLCommand(agnocServer, repl); + + return replCommand.action({ + historyFilename: history, + }); + }); + program.addHelpCommand('help [command]', 'display help for command'); program.parse(process.argv); diff --git a/packages/cli/src/commands/repl.command.test.ts b/packages/cli/src/commands/repl.command.test.ts new file mode 100644 index 00000000..d5279174 --- /dev/null +++ b/packages/cli/src/commands/repl.command.test.ts @@ -0,0 +1,100 @@ +import { imock, verify, instance, anything, when, capture } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { restore } from 'mock-fs'; +import { REPLCommand } from './repl.command'; +import type { Server } from '@agnoc/toolkit'; +import type { Context } from 'node:vm'; +import type { REPLServer } from 'repl'; + +describe('REPLCommand', function () { + let server: Server; + let repl: REPLServer; + let command: REPLCommand; + let historyFilename: string; + + beforeEach(function () { + server = imock(); + repl = imock(); + historyFilename = 'historyFilename'; + command = new REPLCommand(instance(server), instance(repl)); + }); + + afterEach(function () { + restore(); + }); + + it('should start a server', async function () { + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(null, instance(repl)), + ); + + await command.action({ historyFilename }); + + verify(server.listen()).once(); + }); + + it('should set repl context', async function () { + const context: Context = {}; + + when(repl.context).thenReturn(context); + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(null, instance(repl)), + ); + + await command.action({ historyFilename }); + + expect(context.server).to.be.equal(instance(server)); + }); + + it('should set repl history', async function () { + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(null, instance(repl)), + ); + + await command.action({ historyFilename }); + + verify(repl.setupHistory(historyFilename, anything())).once(); + }); + + it('should throw an error when history cannot be set', async function () { + const error = new Error('unexpected'); + + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(error, instance(repl)), + ); + + await expect(command.action({ historyFilename })).to.be.rejectedWith(error); + }); + + it('should close the server on exit event', async function () { + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(null, instance(repl)), + ); + + await command.action({ historyFilename }); + + const [, onExit] = capture<'exit', () => void>(repl.on).second(); + + onExit(); + + verify(server.close()).once(); + }); + + it('should set the context on reset event', async function () { + const context: Context = {}; + + when(repl.setupHistory(anything(), anything())).thenCall((_, callback: SetupHistoryCallback) => + callback(null, instance(repl)), + ); + + await command.action({ historyFilename }); + + const [, onReset] = capture<'reset', (context: Context) => void>(repl.on).first(); + + onReset(context); + + expect(context.server).to.be.equal(instance(server)); + }); +}); + +type SetupHistoryCallback = (err: Error | null, repl: REPLServer) => void; diff --git a/packages/cli/src/commands/repl.command.ts b/packages/cli/src/commands/repl.command.ts new file mode 100644 index 00000000..b0394f27 --- /dev/null +++ b/packages/cli/src/commands/repl.command.ts @@ -0,0 +1,45 @@ +import { promisify } from 'util'; +import * as Domain from '@agnoc/domain'; +import type { Command } from '../interfaces/command'; +import type { Server } from '@agnoc/toolkit'; +import type { REPLServer } from 'node:repl'; +import type { Context } from 'node:vm'; + +export interface REPLCommandOptions { + historyFilename: string; +} + +export class REPLCommand implements Command { + constructor(private readonly server: Server, private readonly repl: REPLServer) {} + + async action(options: REPLCommandOptions): Promise { + this.setContext(this.repl.context); + this.addListeners(); + + await this.setHistory(options.historyFilename); + await this.server.listen(); + } + + private addListeners() { + this.repl.on('reset', (context) => { + this.setContext(context); + }); + + this.repl.on('exit', () => { + void this.server.close(); + }); + } + + private setContext(context: Context) { + Object.assign(context, { + ...Domain, + server: this.server, + }); + } + + private setHistory(historyFilename: string) { + const setupHistory = promisify(this.repl.setupHistory.bind(this.repl)); + + return setupHistory(historyFilename); + } +} diff --git a/packages/cli/test/acceptance/cli.test.ts b/packages/cli/test/acceptance/cli.test.ts index 8693c6ae..065c2ff7 100644 --- a/packages/cli/test/acceptance/cli.test.ts +++ b/packages/cli/test/acceptance/cli.test.ts @@ -45,4 +45,10 @@ describe('cli', function () { expect(stdout).to.include('Usage: agnoc wlan:config'); }); + + it('has a repl command', async function () { + const { stdout } = await execa('node', [filename, 'repl', '-h']); + + expect(stdout).to.include('Usage: agnoc repl'); + }); }); diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 2ffae57e..8dbb7c0f 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -7,5 +7,11 @@ }, "include": ["src", "types"], "exclude": ["**/*.test.ts"], - "references": [{ "path": "../toolkit/tsconfig.build.json" }, { "path": "../transport-tcp/tsconfig.build.json" }] + "references": [ + { "path": "../toolkit/tsconfig.build.json" }, + { "path": "../transport-tcp/tsconfig.build.json" }, + { "path": "../domain/tsconfig.build.json" }, + { "path": "../adapter-tcp/tsconfig.build.json" }, + { "path": "../core/tsconfig.build.json" } + ] } diff --git a/packages/core/src/agnoc.server.ts b/packages/core/src/agnoc.server.ts index a7653c0d..8727ae9d 100644 --- a/packages/core/src/agnoc.server.ts +++ b/packages/core/src/agnoc.server.ts @@ -1,6 +1,7 @@ import { CommandQueryBus, ConnectionRepository, DeviceRepository, DomainEventBus } from '@agnoc/domain'; import { EventHandlerRegistry, MemoryAdapter, TaskHandlerRegistry } from '@agnoc/toolkit'; import { FindDeviceQueryHandler } from './query-handlers/find-device.query-handler'; +import { FindDevicesQueryHandler } from './query-handlers/find-devices.query-handler'; import type { DomainEventNames, DomainEvents, CommandsOrQueries } from '@agnoc/domain'; import type { Server, TaskOutput } from '@agnoc/toolkit'; @@ -53,7 +54,10 @@ export class AgnocServer implements Server { } private registerHandlers(): void { - this.commandQueryHandlerRegistry.register(new FindDeviceQueryHandler(this.deviceRepository)); + this.commandQueryHandlerRegistry.register( + new FindDeviceQueryHandler(this.deviceRepository), + new FindDevicesQueryHandler(this.deviceRepository), + ); } } diff --git a/packages/core/src/query-handlers/find-devices.query-handler.test.ts b/packages/core/src/query-handlers/find-devices.query-handler.test.ts new file mode 100644 index 00000000..83877e78 --- /dev/null +++ b/packages/core/src/query-handlers/find-devices.query-handler.test.ts @@ -0,0 +1,35 @@ +import { Device, FindDevicesQuery } from '@agnoc/domain'; +import { givenSomeDeviceProps } from '@agnoc/domain/test-support'; +import { imock, instance, verify, when } from '@johanblumenberg/ts-mockito'; +import { expect } from 'chai'; +import { FindDevicesQueryHandler } from './find-devices.query-handler'; +import type { DeviceRepository } from '@agnoc/domain'; + +describe('FindDevicesQueryHandler', function () { + let deviceRepository: DeviceRepository; + let queryHandler: FindDevicesQueryHandler; + + beforeEach(function () { + deviceRepository = imock(); + queryHandler = new FindDevicesQueryHandler(instance(deviceRepository)); + }); + + it('should define the name', function () { + expect(queryHandler.forName).to.be.equal('FindDevicesQuery'); + }); + + describe('#handle()', function () { + it('should find some devices', async function () { + const devices = [new Device(givenSomeDeviceProps())]; + const query = new FindDevicesQuery(); + + when(deviceRepository.findAll()).thenResolve(devices); + + const result = await queryHandler.handle(query); + + expect(result).to.be.deep.equal({ devices }); + + verify(deviceRepository.findAll()).once(); + }); + }); +}); diff --git a/packages/core/src/query-handlers/find-devices.query-handler.ts b/packages/core/src/query-handlers/find-devices.query-handler.ts new file mode 100644 index 00000000..3d72430f --- /dev/null +++ b/packages/core/src/query-handlers/find-devices.query-handler.ts @@ -0,0 +1,13 @@ +import type { QueryHandler, FindDevicesQueryOutput, DeviceRepository, FindDevicesQuery } from '@agnoc/domain'; + +export class FindDevicesQueryHandler implements QueryHandler { + readonly forName = 'FindDevicesQuery'; + + constructor(private readonly deviceRepository: DeviceRepository) {} + + async handle(_: FindDevicesQuery): Promise { + const devices = await this.deviceRepository.findAll(); + + return { devices }; + } +} diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index a0c1bd14..9137afd0 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -53,6 +53,7 @@ export * from './event-handlers/command.task-handler'; export * from './event-handlers/domain.event-handler'; export * from './event-handlers/query.task-handler'; export * from './queries/find-device.query'; +export * from './queries/find-devices.query'; export * from './queries/get-device-consumables.query'; export * from './queries/queries'; export * from './repositories/connection.repository'; diff --git a/packages/domain/src/queries/find-devices.query.test.ts b/packages/domain/src/queries/find-devices.query.test.ts new file mode 100644 index 00000000..cb6e9c60 --- /dev/null +++ b/packages/domain/src/queries/find-devices.query.test.ts @@ -0,0 +1,48 @@ +import { ArgumentInvalidException, ArgumentNotProvidedException, Query } from '@agnoc/toolkit'; +import { expect } from 'chai'; +import { Device } from '../aggregate-roots/device.aggregate-root'; +import { givenSomeDeviceProps } from '../test-support'; +import { FindDevicesQuery } from './find-devices.query'; +import type { FindDevicesQueryOutput } from './find-devices.query'; + +describe('FindDevicesQuery', function () { + it('should be created', function () { + const query = new FindDevicesQuery(); + + expect(query).to.be.instanceOf(Query); + }); + + it("should throw an error when 'devices' is not provided", function () { + const query = new FindDevicesQuery(); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAFindDevicesQueryOutput(), devices: undefined })).to.throw( + ArgumentNotProvidedException, + `Property 'devices' for FindDevicesQuery not provided`, + ); + }); + + it("should throw an error when 'devices' is not an array", function () { + const query = new FindDevicesQuery(); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAFindDevicesQueryOutput(), devices: 'foo' })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'devices' of FindDevicesQuery is not an array`, + ); + }); + + it("should throw an error when 'devices' is not an array of Devices", function () { + const query = new FindDevicesQuery(); + + // @ts-expect-error - missing property + expect(() => query.validateOutput({ ...givenAFindDevicesQueryOutput(), devices: ['foo'] })).to.throw( + ArgumentInvalidException, + `Value 'foo' for property 'devices' of FindDevicesQuery is not an array of Device`, + ); + }); +}); + +function givenAFindDevicesQueryOutput(): FindDevicesQueryOutput { + return { devices: [new Device(givenSomeDeviceProps())] }; +} diff --git a/packages/domain/src/queries/find-devices.query.ts b/packages/domain/src/queries/find-devices.query.ts new file mode 100644 index 00000000..28740c62 --- /dev/null +++ b/packages/domain/src/queries/find-devices.query.ts @@ -0,0 +1,17 @@ +import { Query } from '@agnoc/toolkit'; +import { Device } from '../aggregate-roots/device.aggregate-root'; + +export interface FindDevicesQueryOutput { + devices: Device[]; +} + +export class FindDevicesQuery extends Query { + protected validate(): void { + // noop + } + + override validateOutput(output: FindDevicesQueryOutput): void { + this.validateDefinedProp(output, 'devices'); + this.validateArrayProp(output, 'devices', Device); + } +} diff --git a/packages/domain/src/queries/queries.ts b/packages/domain/src/queries/queries.ts index 7cb5d4c2..39291a72 100644 --- a/packages/domain/src/queries/queries.ts +++ b/packages/domain/src/queries/queries.ts @@ -1,9 +1,11 @@ import { FindDeviceQuery } from './find-device.query'; +import { FindDevicesQuery } from './find-devices.query'; import { GetDeviceConsumablesQuery } from './get-device-consumables.query'; import type { InstanceTypeProps } from '@agnoc/toolkit'; export const Queries = { FindDeviceQuery, + FindDevicesQuery, GetDeviceConsumablesQuery, }; diff --git a/packages/toolkit/src/base-classes/validatable.base.test.ts b/packages/toolkit/src/base-classes/validatable.base.test.ts index be994f1c..32885f24 100644 --- a/packages/toolkit/src/base-classes/validatable.base.test.ts +++ b/packages/toolkit/src/base-classes/validatable.base.test.ts @@ -5,28 +5,27 @@ import { ArgumentOutOfRangeException } from '../exceptions/argument-out-of-range import { Validatable } from './validatable.base'; describe('Validatable', function () { - it('should check for empty props', function () { + it('should check for empty props', function (done) { type ValidatableProps = { foo: string }; class DummyValidatable extends Validatable { - protected validate(_: ValidatableProps): void { - // noop + protected validate(props: ValidatableProps): void { + expect(props).to.be.undefined; + done(); } } // @ts-expect-error - missing properties - expect(() => new DummyValidatable()).to.throw( - ArgumentNotProvidedException, - 'Cannot create DummyValidatable from empty properties', - ); + new DummyValidatable(); }); - it('should invoke validate method', function () { + it('should invoke validate method', function (done) { type ValidatableProps = { foo: string }; class DummyValidatable extends Validatable { protected validate(props: ValidatableProps): void { expect(props).to.deep.equal({ foo: 'bar' }); + done(); } } diff --git a/packages/toolkit/src/base-classes/validatable.base.ts b/packages/toolkit/src/base-classes/validatable.base.ts index 1a9134d5..72f14823 100644 --- a/packages/toolkit/src/base-classes/validatable.base.ts +++ b/packages/toolkit/src/base-classes/validatable.base.ts @@ -1,7 +1,6 @@ import { ArgumentInvalidException } from '../exceptions/argument-invalid.exception'; import { ArgumentNotProvidedException } from '../exceptions/argument-not-provided.exception'; import { ArgumentOutOfRangeException } from '../exceptions/argument-out-of-range.exception'; -import { isEmpty } from '../utils/is-empty.util'; import { isPresent } from '../utils/is-present.util'; import type { Constructor } from '../types/constructor.type'; @@ -9,7 +8,6 @@ import type { Constructor } from '../types/constructor.type'; export abstract class Validatable { /** Checks if the provided props are empty and invokes the `validate` method. */ constructor(props: T) { - this.checkIfEmpty(props); this.validate(props); } @@ -17,8 +15,8 @@ export abstract class Validatable { protected abstract validate(props: T): void; /** Checks whether a prop is included in a list. */ - protected validateListProp(props: T, propName: K, list: T[K][]): void { - const value = props[propName]; + protected validateListProp(props: T | undefined, propName: K, list: T[K][]): void { + const value = props?.[propName]; if (isPresent(value) && !list.includes(value)) { throw new ArgumentInvalidException( @@ -28,8 +26,8 @@ export abstract class Validatable { } /** Checks whether a prop is a positive integer. */ - protected validatePositiveIntegerProp(props: T, propName: K): void { - const value = props[propName]; + protected validatePositiveIntegerProp(props: T | undefined, propName: K): void { + const value = props?.[propName]; if (isPresent(value) && (typeof value !== 'number' || !Number.isInteger(value) || value < 0)) { throw new ArgumentInvalidException( @@ -40,11 +38,11 @@ export abstract class Validatable { /** Checks whether a prop is a number. Optionally check if the number is contained in a range. */ protected validateNumberProp( - props: T, + props: T | undefined, propName: K, range?: ValidateNumberRange, ): void { - const value = props[propName]; + const value = props?.[propName]; if (!isPresent(value)) { return; @@ -68,15 +66,19 @@ export abstract class Validatable { } /** Checks whether a prop is defined. */ - protected validateDefinedProp(props: T, propName: K): void { - if (!isPresent(props[propName])) { + protected validateDefinedProp(props: T | undefined, propName: K): void { + if (!isPresent(props?.[propName])) { throw new ArgumentNotProvidedException(`Property '${propName}' for ${this.constructor.name} not provided`); } } /** Checks whether a prop is an instance of a class. */ - protected validateInstanceProp(props: T, propName: K, ctor: Constructor): void { - const value = props[propName]; + protected validateInstanceProp( + props: T | undefined, + propName: K, + ctor: Constructor, + ): void { + const value = props?.[propName]; if (isPresent(value) && !(value instanceof ctor)) { throw new ArgumentInvalidException( @@ -88,8 +90,8 @@ export abstract class Validatable { } /** Checks whether a prop is of a certain type. */ - protected validateTypeProp(props: T, propName: K, type: string): void { - const value = props[propName]; + protected validateTypeProp(props: T | undefined, propName: K, type: string): void { + const value = props?.[propName]; if (isPresent(value) && typeof value !== type) { throw new ArgumentInvalidException( @@ -99,8 +101,12 @@ export abstract class Validatable { } /** Checks whether a prop is an array of instances of a class. */ - protected validateArrayProp(props: T, propName: K, ctor: Constructor): void { - const value = props[propName]; + protected validateArrayProp( + props: T | undefined, + propName: K, + ctor: Constructor, + ): void { + const value = props?.[propName]; if (!isPresent(value)) { return; @@ -120,12 +126,6 @@ export abstract class Validatable { ); } } - - private checkIfEmpty(props: T): void { - if (isEmpty(props)) { - throw new ArgumentNotProvidedException(`Cannot create ${this.constructor.name} from empty properties`); - } - } } /** Defines a range of numbers. */