@invariant--labs/foundation 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pnp.cjs +22192 -0
- package/.pnp.loader.mjs +2126 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +527 -0
- package/README.md +3 -0
- package/eslint.config.mjs +52 -0
- package/invariant.json +22 -0
- package/jest/jest.config.base.ts +30 -0
- package/jest/jest.spec.base.ts +7 -0
- package/jest.config.js +49 -0
- package/package.json +99 -0
- package/src/core/application/index.ts +1 -0
- package/src/core/application/persistence/index.ts +3 -0
- package/src/core/application/persistence/query-builder.ts +2 -0
- package/src/core/application/persistence/read-projection.ts +2 -0
- package/src/core/application/persistence/repository.ts +23 -0
- package/src/core/domain/entities/aggregate-root.ts +34 -0
- package/src/core/domain/entities/entity.spec.ts +43 -0
- package/src/core/domain/entities/entity.ts +29 -0
- package/src/core/domain/entities/identifier.spec.ts +34 -0
- package/src/core/domain/entities/identifier.ts +16 -0
- package/src/core/domain/entities/index.ts +5 -0
- package/src/core/domain/entities/projection.ts +7 -0
- package/src/core/domain/entities/unique-entity-id.ts +9 -0
- package/src/core/domain/events/domain-event.ts +7 -0
- package/src/core/domain/events/index.ts +1 -0
- package/src/core/domain/index.ts +3 -0
- package/src/core/domain/value-objects/index.ts +2 -0
- package/src/core/domain/value-objects/range.ts +4 -0
- package/src/core/domain/value-objects/value-object.spec.ts +45 -0
- package/src/core/domain/value-objects/value-object.ts +17 -0
- package/src/core/errors/command-failure.error.spec.ts +30 -0
- package/src/core/errors/command-failure.error.ts +9 -0
- package/src/core/errors/command-filter.error.ts +3 -0
- package/src/core/errors/detailed.error.ts +25 -0
- package/src/core/errors/domain.error.spec.ts +27 -0
- package/src/core/errors/domain.error.ts +9 -0
- package/src/core/errors/entity-not-found.error.spec.ts +32 -0
- package/src/core/errors/entity-not-found.error.ts +9 -0
- package/src/core/errors/fake-implementation.error.spec.ts +27 -0
- package/src/core/errors/fake-implementation.error.ts +15 -0
- package/src/core/errors/index.ts +8 -0
- package/src/core/errors/query-failure.error.ts +8 -0
- package/src/core/errors/too-many-results.error.ts +3 -0
- package/src/core/index.ts +4 -0
- package/src/core/infrastructure/config/config.provider.ts +78 -0
- package/src/core/infrastructure/config/config.service.ts +25 -0
- package/src/core/infrastructure/config/index.ts +2 -0
- package/src/core/infrastructure/messaging/fake/fake-messaging.service.ts +17 -0
- package/src/core/infrastructure/messaging/fake/fake-queue-manager.service.ts +9 -0
- package/src/core/infrastructure/messaging/fake/fake-queue-messaging.service.ts +9 -0
- package/src/core/infrastructure/messaging/fake/index.ts +3 -0
- package/src/core/infrastructure/messaging/index.ts +6 -0
- package/src/core/infrastructure/messaging/messaging.service.ts +3 -0
- package/src/core/infrastructure/messaging/queue-manager.service.ts +6 -0
- package/src/core/infrastructure/messaging/queue-messaging.service.ts +3 -0
- package/src/core/infrastructure/messaging/rabbitmq/index.ts +2 -0
- package/src/core/infrastructure/messaging/rabbitmq/rabbit-messaging.service.ts +11 -0
- package/src/core/infrastructure/messaging/rabbitmq/rabbit-queue-messaging.service.ts +11 -0
- package/src/core/infrastructure/messaging/types.ts +28 -0
- package/src/core/infrastructure/persistence/errors/index.ts +2 -0
- package/src/core/infrastructure/persistence/errors/model-to-entity-conversion.error.ts +9 -0
- package/src/core/infrastructure/persistence/errors/persistence.error.ts +8 -0
- package/src/core/infrastructure/persistence/in-memory/fake.repository.ts +79 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.query-builder.ts +4 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.repository.spec.ts +50 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.repository.ts +81 -0
- package/src/core/infrastructure/persistence/in-memory/index.ts +3 -0
- package/src/core/infrastructure/persistence/index.ts +4 -0
- package/src/core/infrastructure/persistence/read-side/in-memory.query-builder.ts +4 -0
- package/src/core/infrastructure/persistence/read-side/index.ts +1 -0
- package/src/core/infrastructure/persistence/read-side/knex/index.ts +2 -0
- package/src/core/infrastructure/persistence/read-side/knex/knex-types.definition.ts +13 -0
- package/src/core/infrastructure/persistence/read-side/knex/knex.query-builder.ts +70 -0
- package/src/core/infrastructure/persistence/read-side/knex-query-builder.ts +70 -0
- package/src/core/infrastructure/persistence/read-side/knex-types.definition.ts +13 -0
- package/src/core/infrastructure/persistence/write-side/aggregate-typeorm-repository.ts +87 -0
- package/src/core/infrastructure/persistence/write-side/entity-typeorm-repository.ts +64 -0
- package/src/core/infrastructure/persistence/write-side/in-memory.repository.ts +82 -0
- package/src/core/infrastructure/persistence/write-side/index.ts +1 -0
- package/src/core/infrastructure/persistence/write-side/model-attributes.ts +4 -0
- package/src/core/infrastructure/persistence/write-side/orm-embedded-mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/orm-mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/aggregate-typeorm.repository.ts +87 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/entity-typeorm.repository.ts +64 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/index.ts +5 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/model-attributes.ts +4 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/orm-embedded.mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/orm.mapper.ts +11 -0
- package/src/core/types/architecture-layer.ts +52 -0
- package/src/core/types/array-element.ts +5 -0
- package/src/core/types/index.ts +2 -0
- package/src/index.ts +30 -0
- package/src/modules/config/config.module.ts +9 -0
- package/src/modules/config/index.ts +2 -0
- package/src/modules/graphql/index.ts +1 -0
- package/src/modules/graphql/paginated-response.object-type.ts +23 -0
- package/src/modules/healthcheck/healthcheck-out.dto.ts +43 -0
- package/src/modules/healthcheck/index.ts +1 -0
- package/src/modules/knex/index.ts +3 -0
- package/src/modules/knex/knex-core.module.ts +30 -0
- package/src/modules/knex/knex.decorator.ts +5 -0
- package/src/modules/knex/knex.interface.ts +12 -0
- package/src/modules/knex/knex.module.ts +14 -0
- package/src/modules/knex/knex.token.ts +2 -0
- package/src/modules/logger/app-logger.ts +28 -0
- package/src/modules/logger/index.ts +5 -0
- package/src/modules/logger/log.ts +26 -0
- package/src/modules/logger/logger.module.ts +47 -0
- package/src/modules/logger/logger.service.ts +131 -0
- package/src/modules/logger/transports/console-color.transport.ts +41 -0
- package/src/modules/logger/transports/console-json.transport.ts +23 -0
- package/src/modules/logger/transports/console.transport.ts +10 -0
- package/src/modules/logger/transports/fake.transport.ts +18 -0
- package/src/modules/logger/transports/index.ts +5 -0
- package/src/modules/logger/transports/logger-transport.ts +22 -0
- package/src/modules/messaging/index.ts +2 -0
- package/src/modules/messaging/messaging.module.ts +92 -0
- package/src/modules/queue/default-queue-name.resolver.ts +5 -0
- package/src/modules/queue/index.ts +3 -0
- package/src/modules/queue/queue.module.ts +73 -0
- package/src/modules/queue/rabbit-queue-manager.service.ts +67 -0
- package/src/nestjs/errors/handlers/error-handler.ts +38 -0
- package/src/nestjs/errors/handlers/error-handler.type.ts +23 -0
- package/src/nestjs/errors/handlers/generic-error-handler.ts +59 -0
- package/src/nestjs/errors/handlers/handle-errors.decorator.ts +43 -0
- package/src/nestjs/errors/handlers/index.ts +5 -0
- package/src/nestjs/errors/handlers/validation-error-handler.ts +46 -0
- package/src/nestjs/errors/index.ts +2 -0
- package/src/nestjs/errors/parsers/axios-metadata.parser.ts +21 -0
- package/src/nestjs/errors/parsers/error-metadata.definition.ts +1 -0
- package/src/nestjs/errors/parsers/index.ts +5 -0
- package/src/nestjs/errors/parsers/knex-metadata.parser.ts +18 -0
- package/src/nestjs/errors/parsers/typeorm-metadata.parser.ts +19 -0
- package/src/nestjs/errors/parsers/workos-metadata.parser.ts +17 -0
- package/src/nestjs/http/decorators/index.ts +1 -0
- package/src/nestjs/http/decorators/log-app-ctx.decorator.ts +32 -0
- package/src/nestjs/http/dtos/date-range.dto.ts +15 -0
- package/src/nestjs/http/dtos/index.ts +1 -0
- package/src/nestjs/http/filters/all-exceptions.filter.ts +92 -0
- package/src/nestjs/http/filters/command-failure.filter.ts +12 -0
- package/src/nestjs/http/filters/index.ts +4 -0
- package/src/nestjs/http/filters/rpc-exceptions.filter.ts +10 -0
- package/src/nestjs/http/filters/too-many-results.filter.ts +12 -0
- package/src/nestjs/http/index.ts +6 -0
- package/src/nestjs/http/interceptors/index.ts +2 -0
- package/src/nestjs/http/interceptors/log-app-ctx.interceptor.ts +109 -0
- package/src/nestjs/http/interceptors/serialize-output.interceptor.ts +19 -0
- package/src/nestjs/http/middleware/http-logger.middleware.ts +31 -0
- package/src/nestjs/http/middleware/index.ts +2 -0
- package/src/nestjs/http/middleware/logger.middleware.ts +21 -0
- package/src/nestjs/http/swagger/index.ts +1 -0
- package/src/nestjs/http/swagger/swagger.ts +44 -0
- package/src/nestjs/index.ts +2 -0
- package/src/testing/command-bus.stub.ts +33 -0
- package/src/testing/event-bus.stub.ts +56 -0
- package/src/testing/event-publisher.stub.ts +24 -0
- package/src/testing/fake-logger.ts +20 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/query-bus.stub.ts +27 -0
- package/src/testing/stub.spec.ts +250 -0
- package/src/testing/stub.ts +170 -0
- package/src/testing/stubs/command-bus.stub.ts +33 -0
- package/src/testing/stubs/event-bus.stub.ts +56 -0
- package/src/testing/stubs/event-publisher.stub.ts +24 -0
- package/src/testing/stubs/index.ts +5 -0
- package/src/testing/stubs/query-bus.stub.ts +27 -0
- package/src/testing/stubs/stub.ts +170 -0
- package/src/utils/array.spec.ts +29 -0
- package/src/utils/array.ts +10 -0
- package/src/utils/base64.spec.ts +18 -0
- package/src/utils/base64.ts +6 -0
- package/src/utils/collection.ts +4 -0
- package/src/utils/common.ts +13 -0
- package/src/utils/csv.ts +49 -0
- package/src/utils/date/date-range.ts +4 -0
- package/src/utils/date/date.spec.ts +100 -0
- package/src/utils/date/date.ts +177 -0
- package/src/utils/date/diff.spec.ts +66 -0
- package/src/utils/date/diffYear.spec.ts +23 -0
- package/src/utils/date/fillMissingRangeValues.spec.ts +523 -0
- package/src/utils/date/groubBy.spec.ts +183 -0
- package/src/utils/date/index.ts +2 -0
- package/src/utils/date/isSame.spec.ts +111 -0
- package/src/utils/file.spec.ts +66 -0
- package/src/utils/file.ts +5 -0
- package/src/utils/hash-key.ts +23 -0
- package/src/utils/index.ts +14 -0
- package/src/utils/invariant.ts +3 -0
- package/src/utils/iso-date.ts +11 -0
- package/src/utils/object.spec.ts +18 -0
- package/src/utils/object.ts +6 -0
- package/src/utils/paginated.ts +36 -0
- package/src/utils/string.spec.ts +10 -0
- package/src/utils/string.ts +19 -0
- package/src/utils/type.ts +9 -0
- package/src/utils/xml.ts +6 -0
- package/src/validation/ensure-array.decorator.ts +5 -0
- package/src/validation/index.ts +7 -0
- package/src/validation/is-iso-date.decorator.spec.ts +29 -0
- package/src/validation/is-iso-date.decorator.ts +30 -0
- package/src/validation/is-less-than-or-equal.decorator.spec.ts +30 -0
- package/src/validation/is-less-than-or-equal.decorator.ts +52 -0
- package/src/validation/is-less-than.decorator.spec.ts +36 -0
- package/src/validation/is-less-than.decorator.ts +52 -0
- package/src/validation/is-more-than-or-equal.decorator.spec.ts +30 -0
- package/src/validation/is-more-than-or-equal.decorator.ts +52 -0
- package/src/validation/is-more-than.decorator.spec.ts +36 -0
- package/src/validation/is-more-than.decorator.ts +52 -0
- package/src/validation/is-time-string.decorator.spec.ts +35 -0
- package/src/validation/is-time-string.decorator.ts +29 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +14 -0
package/jest.config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const sharedConfig = {
|
|
2
|
+
roots: ["<rootDir>/src"],
|
|
3
|
+
moduleDirectories: ["node_modules"],
|
|
4
|
+
testPathIgnorePatterns: ["./node_modules/"],
|
|
5
|
+
testEnvironment: "node",
|
|
6
|
+
transform: {
|
|
7
|
+
"^.+\\.(t|j)sx?$": [
|
|
8
|
+
"@swc/jest",
|
|
9
|
+
{
|
|
10
|
+
jsc: {
|
|
11
|
+
parser: {
|
|
12
|
+
syntax: "typescript",
|
|
13
|
+
decorators: true,
|
|
14
|
+
},
|
|
15
|
+
transform: {
|
|
16
|
+
legacyDecorator: true,
|
|
17
|
+
decoratorMetadata: true,
|
|
18
|
+
},
|
|
19
|
+
target: "es2022",
|
|
20
|
+
},
|
|
21
|
+
module: {
|
|
22
|
+
type: "commonjs",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
transformIgnorePatterns: ["/node_modules/(?!(jose|uuid)/)"],
|
|
28
|
+
moduleNameMapper: {
|
|
29
|
+
"^src/(.*)$": "<rootDir>/src/$1",
|
|
30
|
+
},
|
|
31
|
+
maxWorkers: "25%",
|
|
32
|
+
moduleFileExtensions: ["ts", "js", "json"],
|
|
33
|
+
restoreMocks: true,
|
|
34
|
+
workerIdleMemoryLimit: "1024MB",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
displayName: "foundation",
|
|
39
|
+
coverageDirectory: "./coverage",
|
|
40
|
+
...sharedConfig,
|
|
41
|
+
projects: [
|
|
42
|
+
{
|
|
43
|
+
displayName: "foundation-spec",
|
|
44
|
+
...sharedConfig,
|
|
45
|
+
testMatch: ["**/*.spec.ts"],
|
|
46
|
+
workerIdleMemoryLimit: "512MB",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@invariant--labs/foundation",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rimraf ./dist",
|
|
9
|
+
"build": "nest build",
|
|
10
|
+
"lint": "eslint src/",
|
|
11
|
+
"lint:types": "tsc --project ./tsconfig.json --noEmit",
|
|
12
|
+
"test": "NODE_ENV=test jest",
|
|
13
|
+
"test:spec": "yarn test --selectProjects=foundation-spec",
|
|
14
|
+
"test:integration": "echo 'integration tests not implemented'",
|
|
15
|
+
"test:e2e": "echo 'e2e tests not implemented'",
|
|
16
|
+
"prerelease": "yarn clean && yarn lint && yarn lint:types && yarn test",
|
|
17
|
+
"release": "yarn prerelease && yarn build && npm publish --access public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.39.2",
|
|
21
|
+
"@jest/globals": "^30.2.0",
|
|
22
|
+
"@nestjs/axios": "^4.0.1",
|
|
23
|
+
"@nestjs/platform-express": "^11.1.12",
|
|
24
|
+
"@nestjs/schematics": "^11.0.9",
|
|
25
|
+
"@nestjs/testing": "^11.1.12",
|
|
26
|
+
"@swc/core": "^1.11.29",
|
|
27
|
+
"@swc/jest": "^0.2.39",
|
|
28
|
+
"@types/base-64": "^1.0.2",
|
|
29
|
+
"@types/cls-hooked": "^4.3.9",
|
|
30
|
+
"@types/dotenv-safe": "^9.1.0",
|
|
31
|
+
"@types/express": "^5.0.6",
|
|
32
|
+
"@types/jest": "^30.0.0",
|
|
33
|
+
"@types/lodash": "^4.17.23",
|
|
34
|
+
"@types/luxon": "^3.7.1",
|
|
35
|
+
"@types/mime-types": "^3.0.1",
|
|
36
|
+
"@types/multer": "^2.0.0",
|
|
37
|
+
"@types/node": "^25.0.9",
|
|
38
|
+
"@types/passport": "^1.0.17",
|
|
39
|
+
"@types/supertest": "^6.0.3",
|
|
40
|
+
"@types/utf8": "^3.0.3",
|
|
41
|
+
"@types/uuid": "^11.0.0",
|
|
42
|
+
"eslint-config-prettier": "^10.1.8",
|
|
43
|
+
"eslint-plugin-no-only-tests": "^3.3.0",
|
|
44
|
+
"globals": "^17.0.0",
|
|
45
|
+
"jest": "^30.2.0",
|
|
46
|
+
"jest-util": "^30.2.0",
|
|
47
|
+
"rimraf": "^6.1.2",
|
|
48
|
+
"supertest": "^7.1.0",
|
|
49
|
+
"terser": "^5.46.0",
|
|
50
|
+
"tslib": "^2.8.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"typescript-eslint": "^8.53.1"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@nestjs/cli": "^11.0.16",
|
|
56
|
+
"@nestjs/common": "^11.1.12",
|
|
57
|
+
"@nestjs/core": "^11.1.12",
|
|
58
|
+
"@nestjs/cqrs": "^11.0.3",
|
|
59
|
+
"@nestjs/graphql": "^13.2.3",
|
|
60
|
+
"@nestjs/microservices": "^11.1.12",
|
|
61
|
+
"@nestjs/passport": "^11.0.5",
|
|
62
|
+
"@nestjs/swagger": "^11.2.5",
|
|
63
|
+
"@sentry/nestjs": "^10.35.0",
|
|
64
|
+
"@vercel/style-guide": "^6.0.0",
|
|
65
|
+
"amqp-connection-manager": "^5.0.0",
|
|
66
|
+
"amqplib": "^0.10.9",
|
|
67
|
+
"axios": "^1.13.2",
|
|
68
|
+
"base-64": "^1.0.0",
|
|
69
|
+
"class-transformer": "^0.5.1",
|
|
70
|
+
"class-validator": "^0.14.3",
|
|
71
|
+
"cls-hooked": "^4.2.2",
|
|
72
|
+
"dayjs": "^1.11.19",
|
|
73
|
+
"dotenv": "^17.2.3",
|
|
74
|
+
"dotenv-safe": "^9.1.0",
|
|
75
|
+
"eslint": "^9.39.2",
|
|
76
|
+
"express-basic-auth": "^1.2.1",
|
|
77
|
+
"fast-xml-parser": "^5.3.3",
|
|
78
|
+
"graphql": "^16.12.0",
|
|
79
|
+
"knex": "^3.1.0",
|
|
80
|
+
"lodash": "^4.17.21",
|
|
81
|
+
"luxon": "^3.7.2",
|
|
82
|
+
"mime-types": "^3.0.2",
|
|
83
|
+
"passport": "^0.7.0",
|
|
84
|
+
"passport-headerapikey": "^1.2.2",
|
|
85
|
+
"reflect-metadata": "^0.2.2",
|
|
86
|
+
"rxjs": "^7.8.2",
|
|
87
|
+
"shallow-equal-object": "^1.1.1",
|
|
88
|
+
"slugify": "^1.6.6",
|
|
89
|
+
"tiny-invariant": "^1.3.3",
|
|
90
|
+
"ts-custom-error": "^3.3.1",
|
|
91
|
+
"typeorm": "^0.3.28",
|
|
92
|
+
"utf8": "^3.0.0",
|
|
93
|
+
"uuid": "^13.0.0"
|
|
94
|
+
},
|
|
95
|
+
"volta": {
|
|
96
|
+
"node": "24.4.0",
|
|
97
|
+
"yarn": "4.7.0"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./persistence";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AggregateRoot, Entity, EntityProps } from "../../domain";
|
|
2
|
+
|
|
3
|
+
export abstract class Repository<DomainEntity extends Entity<EntityProps> | AggregateRoot<EntityProps>> {
|
|
4
|
+
abstract count(options: FindManyOptions<DomainEntity>): Promise<number>;
|
|
5
|
+
abstract exists(id: string): Promise<boolean>;
|
|
6
|
+
abstract findOne(id: string): Promise<DomainEntity | null>;
|
|
7
|
+
abstract findOneOrFail(id: string): Promise<DomainEntity>;
|
|
8
|
+
abstract findByIds(ids: string[]): Promise<DomainEntity[]>;
|
|
9
|
+
abstract save(entity: DomainEntity): Promise<DomainEntity>;
|
|
10
|
+
abstract saveAll(entities: DomainEntity[]): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FindOptions<T> {
|
|
14
|
+
filters?: { [P in keyof T]?: T[P] };
|
|
15
|
+
order?: { [P in keyof T]?: "ASC" | "DESC" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FindManyOptions<T> extends FindOptions<T> {
|
|
19
|
+
skip?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Predicate<T> = (item: T) => boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventPublisher, AggregateRoot as NestAggregateRoot } from "@nestjs/cqrs";
|
|
2
|
+
|
|
3
|
+
import { DomainEvent } from "../events/domain-event";
|
|
4
|
+
import { EntityProps } from "./entity";
|
|
5
|
+
import { UniqueEntityId } from "./unique-entity-id";
|
|
6
|
+
|
|
7
|
+
export abstract class AggregateRoot<Props extends EntityProps = EntityProps> extends NestAggregateRoot<DomainEvent> {
|
|
8
|
+
public readonly _id: UniqueEntityId;
|
|
9
|
+
|
|
10
|
+
constructor(protected props: Props) {
|
|
11
|
+
super();
|
|
12
|
+
this._id = props.id ?? new UniqueEntityId();
|
|
13
|
+
this.props.id = this._id;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get id(): UniqueEntityId {
|
|
17
|
+
return this._id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public equals(object?: AggregateRoot<Props>): boolean {
|
|
21
|
+
if (object == null) {return false;}
|
|
22
|
+
if (this === object) {return true;}
|
|
23
|
+
if (!(object instanceof AggregateRoot)) {return false;}
|
|
24
|
+
return this._id.equals(object._id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public mergeContext(eventPublisher: EventPublisher) {
|
|
28
|
+
eventPublisher.mergeObjectContext(this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public getProps() {
|
|
32
|
+
return { ...this.props, id: this._id };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { Entity, EntityProps } from "./entity";
|
|
4
|
+
import { UniqueEntityId } from "./unique-entity-id";
|
|
5
|
+
|
|
6
|
+
interface TestProps extends EntityProps {
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class TestEntity extends Entity<TestProps> {}
|
|
11
|
+
|
|
12
|
+
describe("Entity", () => {
|
|
13
|
+
it("should generate an id when none is provided", () => {
|
|
14
|
+
const entity = new TestEntity({ name: "test" });
|
|
15
|
+
expect(entity.id).toBeDefined();
|
|
16
|
+
expect(entity.id.toValue()).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should use the provided id", () => {
|
|
20
|
+
const id = new UniqueEntityId("custom-id");
|
|
21
|
+
const entity = new TestEntity({ name: "test", id });
|
|
22
|
+
expect(entity.id.toValue()).toBe("custom-id");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should be equal to another entity with the same id", () => {
|
|
26
|
+
const id = new UniqueEntityId("same");
|
|
27
|
+
const a = new TestEntity({ name: "a", id });
|
|
28
|
+
const b = new TestEntity({ name: "b", id });
|
|
29
|
+
expect(a.equals(b)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should not be equal to an entity with a different id", () => {
|
|
33
|
+
const a = new TestEntity({ name: "a", id: new UniqueEntityId("1") });
|
|
34
|
+
const b = new TestEntity({ name: "b", id: new UniqueEntityId("2") });
|
|
35
|
+
expect(a.equals(b)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should not be equal to null or undefined", () => {
|
|
39
|
+
const entity = new TestEntity({ name: "a" });
|
|
40
|
+
expect(entity.equals(undefined)).toBe(false);
|
|
41
|
+
expect(entity.equals(null as unknown as TestEntity)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { UniqueEntityId } from "./unique-entity-id";
|
|
2
|
+
|
|
3
|
+
export type EntityProps = {
|
|
4
|
+
id?: UniqueEntityId;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export abstract class Entity<Props extends EntityProps = EntityProps> {
|
|
8
|
+
public readonly _id: UniqueEntityId;
|
|
9
|
+
|
|
10
|
+
constructor(protected props: Props) {
|
|
11
|
+
this._id = props.id ?? new UniqueEntityId();
|
|
12
|
+
this.props.id = this._id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get id(): UniqueEntityId {
|
|
16
|
+
return this._id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public equals(object?: Entity<Props>): boolean {
|
|
20
|
+
if (object == null) {return false;}
|
|
21
|
+
if (this === object) {return true;}
|
|
22
|
+
if (!(object instanceof Entity)) {return false;}
|
|
23
|
+
return this._id.equals(object._id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getProps() {
|
|
27
|
+
return { ...this.props, id: this._id };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { Identifier } from "./identifier";
|
|
4
|
+
|
|
5
|
+
class TestId extends Identifier<string> {}
|
|
6
|
+
|
|
7
|
+
describe("Identifier", () => {
|
|
8
|
+
it("should store and return its value", () => {
|
|
9
|
+
const id = new TestId("abc");
|
|
10
|
+
expect(id.toValue()).toBe("abc");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should convert to string", () => {
|
|
14
|
+
const id = new TestId("abc");
|
|
15
|
+
expect(id.toString()).toBe("abc");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should be equal to another with same value", () => {
|
|
19
|
+
const id1 = new TestId("abc");
|
|
20
|
+
const id2 = new TestId("abc");
|
|
21
|
+
expect(id1.equals(id2)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should not be equal to another with different value", () => {
|
|
25
|
+
const id1 = new TestId("abc");
|
|
26
|
+
const id2 = new TestId("xyz");
|
|
27
|
+
expect(id1.equals(id2)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should not be equal to undefined", () => {
|
|
31
|
+
const id = new TestId("abc");
|
|
32
|
+
expect(id.equals(undefined)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class Identifier<T> {
|
|
2
|
+
constructor(private readonly value: T) {}
|
|
3
|
+
|
|
4
|
+
equals(id?: Identifier<T>): boolean {
|
|
5
|
+
if (!(id instanceof this.constructor)) {return false;}
|
|
6
|
+
return id.toValue() === this.value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toString() {
|
|
10
|
+
return String(this.value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toValue(): T {
|
|
14
|
+
return this.value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./domain-event";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { ValueObject } from "./value-object";
|
|
4
|
+
|
|
5
|
+
interface MoneyProps {
|
|
6
|
+
amount: number;
|
|
7
|
+
currency: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class Money extends ValueObject<MoneyProps> {
|
|
11
|
+
validate() {
|
|
12
|
+
if (this.props.amount < 0) {throw new Error("negative");}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("ValueObject", () => {
|
|
17
|
+
it("should freeze props on construction", () => {
|
|
18
|
+
const money = new Money({ amount: 10, currency: "EUR" });
|
|
19
|
+
expect(() => {
|
|
20
|
+
money.getProps().amount = 99;
|
|
21
|
+
}).toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should be equal when props match", () => {
|
|
25
|
+
const a = new Money({ amount: 10, currency: "EUR" });
|
|
26
|
+
const b = new Money({ amount: 10, currency: "EUR" });
|
|
27
|
+
expect(a.equals(b)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should not be equal when props differ", () => {
|
|
31
|
+
const a = new Money({ amount: 10, currency: "EUR" });
|
|
32
|
+
const b = new Money({ amount: 20, currency: "EUR" });
|
|
33
|
+
expect(a.equals(b)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not be equal to undefined", () => {
|
|
37
|
+
const money = new Money({ amount: 10, currency: "EUR" });
|
|
38
|
+
expect(money.equals(undefined)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should expose props via getProps", () => {
|
|
42
|
+
const money = new Money({ amount: 5, currency: "USD" });
|
|
43
|
+
expect(money.getProps()).toEqual({ amount: 5, currency: "USD" });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { shallowEqual } from "shallow-equal-object";
|
|
2
|
+
|
|
3
|
+
export abstract class ValueObject<Props extends object = object> {
|
|
4
|
+
constructor(protected props: Props) {
|
|
5
|
+
this.props = Object.freeze(props);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
public equals(vo?: ValueObject<Props>): boolean {
|
|
9
|
+
return !!vo?.props && shallowEqual(this.props, vo.props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getProps(): Props {
|
|
13
|
+
return this.props;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abstract validate(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { ArchitectureLevel } from "../types";
|
|
4
|
+
import { CommandFailureError } from "./command-failure.error";
|
|
5
|
+
|
|
6
|
+
describe("CommandFailureError", () => {
|
|
7
|
+
it("should store message and layer with command className", () => {
|
|
8
|
+
const error = new CommandFailureError("failed", "CreateUserHandler");
|
|
9
|
+
expect(error.message).toBe("failed");
|
|
10
|
+
expect(error.layer()).toEqual({
|
|
11
|
+
[ArchitectureLevel.APPLICATION]: { command: { className: "CreateUserHandler" } },
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should store details", () => {
|
|
16
|
+
const error = new CommandFailureError("fail", "X", { key: "val" });
|
|
17
|
+
expect(error.details()).toEqual({ key: "val" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should preserve the cause", () => {
|
|
21
|
+
const cause = new Error("root");
|
|
22
|
+
const error = new CommandFailureError("fail", "X", {}, cause);
|
|
23
|
+
expect(error.cause).toBe(cause);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should be an instance of Error", () => {
|
|
27
|
+
const error = new CommandFailureError("fail", "X");
|
|
28
|
+
expect(error).toBeInstanceOf(Error);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ArchitectureLevel } from "../types";
|
|
2
|
+
import { DetailedError } from "./detailed.error";
|
|
3
|
+
|
|
4
|
+
export class CommandFailureError extends DetailedError {
|
|
5
|
+
constructor(message: string, className: string, details: Record<string, unknown> = {}, cause?: unknown) {
|
|
6
|
+
super(message, { [ArchitectureLevel.APPLICATION]: { command: { className } } }, cause);
|
|
7
|
+
this.setDetails(details);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CustomError } from "ts-custom-error";
|
|
2
|
+
|
|
3
|
+
import { ArchitectureLayer } from "../types";
|
|
4
|
+
|
|
5
|
+
export abstract class DetailedError extends CustomError {
|
|
6
|
+
private detailsObject: Record<string, unknown> = {};
|
|
7
|
+
private readonly layerObject: ArchitectureLayer;
|
|
8
|
+
|
|
9
|
+
constructor(message: string, layer: ArchitectureLayer, cause?: unknown) {
|
|
10
|
+
super(message, { cause });
|
|
11
|
+
this.layerObject = layer;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
layer(): ArchitectureLayer {
|
|
15
|
+
return this.layerObject;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
details(): Record<string, unknown> {
|
|
19
|
+
return this.detailsObject;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected setDetails(details: Record<string, unknown>): void {
|
|
23
|
+
this.detailsObject = details;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { ArchitectureLevel } from "../types";
|
|
4
|
+
import { DomainError } from "./domain.error";
|
|
5
|
+
|
|
6
|
+
describe("DomainError", () => {
|
|
7
|
+
it("should wrap trace under DOMAIN layer", () => {
|
|
8
|
+
const trace = { entity: { className: "Invoice" } };
|
|
9
|
+
const error = new DomainError("invalid", trace);
|
|
10
|
+
expect(error.layer()).toEqual({
|
|
11
|
+
[ArchitectureLevel.DOMAIN]: { entity: { className: "Invoice" } },
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should store details", () => {
|
|
16
|
+
const trace = { entity: { className: "X" } };
|
|
17
|
+
const error = new DomainError("bad", trace, { field: "email" });
|
|
18
|
+
expect(error.details()).toEqual({ field: "email" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should preserve the cause", () => {
|
|
22
|
+
const cause = new Error("original");
|
|
23
|
+
const trace = { entity: { className: "X" } };
|
|
24
|
+
const error = new DomainError("bad", trace, {}, cause);
|
|
25
|
+
expect(error.cause).toBe(cause);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ArchitectureLayerTrace, ArchitectureLevel } from "../types";
|
|
2
|
+
import { DetailedError } from "./detailed.error";
|
|
3
|
+
|
|
4
|
+
export class DomainError extends DetailedError {
|
|
5
|
+
constructor(message: string, trace: ArchitectureLayerTrace, details: Record<string, unknown> = {}, cause?: unknown) {
|
|
6
|
+
super(message, { [ArchitectureLevel.DOMAIN]: { ...trace } }, cause);
|
|
7
|
+
this.setDetails(details);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { ArchitectureLevel } from "../types";
|
|
4
|
+
import { EntityNotFoundError } from "./entity-not-found.error";
|
|
5
|
+
|
|
6
|
+
describe("EntityNotFoundError", () => {
|
|
7
|
+
it("should include entity name in message", () => {
|
|
8
|
+
const layer = { [ArchitectureLevel.DOMAIN]: { entity: { className: "User" } } };
|
|
9
|
+
const error = new EntityNotFoundError("User", layer);
|
|
10
|
+
expect(error.message).toContain("User");
|
|
11
|
+
expect(error.message).toContain("cannot be retrieved");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should store the architecture layer", () => {
|
|
15
|
+
const layer = { [ArchitectureLevel.DOMAIN]: { entity: { className: "Order" } } };
|
|
16
|
+
const error = new EntityNotFoundError("Order", layer);
|
|
17
|
+
expect(error.layer()).toEqual(layer);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should store details when provided", () => {
|
|
21
|
+
const layer = { [ArchitectureLevel.DOMAIN]: { entity: { className: "X" } } };
|
|
22
|
+
const error = new EntityNotFoundError("X", layer, undefined, { id: "123" });
|
|
23
|
+
expect(error.details()).toEqual({ id: "123" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should preserve the cause", () => {
|
|
27
|
+
const cause = new Error("db down");
|
|
28
|
+
const layer = { [ArchitectureLevel.DOMAIN]: { entity: { className: "X" } } };
|
|
29
|
+
const error = new EntityNotFoundError("X", layer, cause);
|
|
30
|
+
expect(error.cause).toBe(cause);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ArchitectureLayer } from "../types";
|
|
2
|
+
import { DetailedError } from "./detailed.error";
|
|
3
|
+
|
|
4
|
+
export class EntityNotFoundError extends DetailedError {
|
|
5
|
+
constructor(entityName: string, layer: ArchitectureLayer, cause?: unknown, details: Record<string, unknown> = {}) {
|
|
6
|
+
super(`${entityName} cannot be retrieved`, layer, cause);
|
|
7
|
+
this.setDetails(details);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { ArchitectureLevel } from "../types";
|
|
4
|
+
import { FakeImplementationError } from "./fake-implementation.error";
|
|
5
|
+
|
|
6
|
+
describe("FakeImplementationError", () => {
|
|
7
|
+
it("should include class and method in message", () => {
|
|
8
|
+
const error = new FakeImplementationError("UserRepo", "findOne");
|
|
9
|
+
expect(error.message).toContain("UserRepo");
|
|
10
|
+
expect(error.message).toContain("fake");
|
|
11
|
+
expect(error.message).toContain("findOne");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should set layer to INFRASTRUCTURE", () => {
|
|
15
|
+
const error = new FakeImplementationError("UserRepo", "save");
|
|
16
|
+
expect(error.layer()).toEqual({
|
|
17
|
+
[ArchitectureLevel.INFRASTRUCTURE]: {
|
|
18
|
+
"persistence-fake": { className: "UserRepo" },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should be an instance of Error", () => {
|
|
24
|
+
const error = new FakeImplementationError("X", "y");
|
|
25
|
+
expect(error).toBeInstanceOf(Error);
|
|
26
|
+
});
|
|
27
|
+
});
|