fase(1): shared domain building blocks
This commit is contained in:
@@ -19,23 +19,23 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 1: Shared Domain — Building Blocks [PENDIENTE]
|
## Phase 1: Shared Domain — Building Blocks [COMPLETO]
|
||||||
Spec: `.ralph/specs/phase-01-shared-domain.md`
|
Spec: `.ralph/specs/phase-01-shared-domain.md`
|
||||||
|
|
||||||
- [ ] 1.1: Crear directorio `src/shared/domain/`
|
- [x] 1.1: Crear directorio `src/shared/domain/`
|
||||||
- [ ] 1.2: Crear `src/shared/domain/Result.ts` — Result<T, E> con Ok(), Err(), isOk(), isErr()
|
- [x] 1.2: Crear `src/shared/domain/Result.ts` — Result<T, E> con Ok(), Err(), isOk(), isErr()
|
||||||
- [ ] 1.3: Crear `src/shared/domain/UniqueId.ts` — UUID v4 wrapper con create(), toString(), equals()
|
- [x] 1.3: Crear `src/shared/domain/UniqueId.ts` — UUID v4 wrapper con create(), toString(), equals()
|
||||||
- [ ] 1.4: Crear `src/shared/domain/Entity.ts` — base class con _id: UniqueId, equals()
|
- [x] 1.4: Crear `src/shared/domain/Entity.ts` — base class con _id: UniqueId, equals()
|
||||||
- [ ] 1.5: Crear `src/shared/domain/AggregateRoot.ts` — extends Entity + domainEvents[], addDomainEvent(), clearEvents()
|
- [x] 1.5: Crear `src/shared/domain/AggregateRoot.ts` — extends Entity + domainEvents[], addDomainEvent(), clearEvents()
|
||||||
- [ ] 1.6: Crear `src/shared/domain/ValueObject.ts` — base class inmutable con props frozen, equals()
|
- [x] 1.6: Crear `src/shared/domain/ValueObject.ts` — base class inmutable con props frozen, equals()
|
||||||
- [ ] 1.7: Crear `src/shared/domain/DomainEvent.ts` — interface: eventId, eventName, aggregateId, occurredOn, payload
|
- [x] 1.7: Crear `src/shared/domain/DomainEvent.ts` — interface: eventId, eventName, aggregateId, occurredOn, payload
|
||||||
- [ ] 1.8: Crear `src/shared/application/UseCase.ts` — interface: execute(req) → Promise<Result<TRes, TErr>>
|
- [x] 1.8: Crear `src/shared/application/UseCase.ts` — interface: execute(req) → Promise<Result<TRes, TErr>>
|
||||||
- [ ] 1.9: Crear `src/shared/application/EventBus.ts` — interface: publish(event), subscribe(name, handler)
|
- [x] 1.9: Crear `src/shared/application/EventBus.ts` — interface: publish(event), subscribe(name, handler)
|
||||||
- [ ] 1.10: Crear `src/shared/application/EventHandler.ts` — interface: handle(event) → Promise<void>
|
- [x] 1.10: Crear `src/shared/application/EventHandler.ts` — interface: handle(event) → Promise<void>
|
||||||
- [ ] 1.11: Crear `src/shared/domain/index.ts` — barrel export de todo shared/domain
|
- [x] 1.11: Crear `src/shared/domain/index.ts` — barrel export de todo shared/domain
|
||||||
- [ ] 1.12: Crear `src/shared/application/index.ts` — barrel export de todo shared/application
|
- [x] 1.12: Crear `src/shared/application/index.ts` — barrel export de todo shared/application
|
||||||
- [ ] 1.13: Tests unitarios: Result (Ok/Err/isOk/isErr), Entity (equals by id), ValueObject (equals by props), UniqueId (create/equals)
|
- [x] 1.13: Tests unitarios: Result (Ok/Err/isOk/isErr), Entity (equals by id), ValueObject (equals by props), UniqueId (create/equals)
|
||||||
- [ ] 1.14: Verificar build completo + commit: `fase(1): shared domain building blocks`
|
- [x] 1.14: Verificar build completo + commit: `fase(1): shared domain building blocks`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2
dist/shared/application/EventBus.js
vendored
Normal file
2
dist/shared/application/EventBus.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
2
dist/shared/application/EventHandler.js
vendored
Normal file
2
dist/shared/application/EventHandler.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
2
dist/shared/application/UseCase.js
vendored
Normal file
2
dist/shared/application/UseCase.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
19
dist/shared/application/index.js
vendored
Normal file
19
dist/shared/application/index.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||||
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
__exportStar(require("./UseCase"), exports);
|
||||||
|
__exportStar(require("./EventBus"), exports);
|
||||||
|
__exportStar(require("./EventHandler"), exports);
|
||||||
22
dist/shared/domain/AggregateRoot.js
vendored
Normal file
22
dist/shared/domain/AggregateRoot.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AggregateRoot = void 0;
|
||||||
|
const Entity_1 = require("./Entity");
|
||||||
|
class AggregateRoot extends Entity_1.Entity {
|
||||||
|
constructor(props, id) {
|
||||||
|
super(props, id);
|
||||||
|
this._domainEvents = [];
|
||||||
|
}
|
||||||
|
get domainEvents() {
|
||||||
|
return this._domainEvents;
|
||||||
|
}
|
||||||
|
addDomainEvent(event) {
|
||||||
|
this._domainEvents.push(event);
|
||||||
|
}
|
||||||
|
clearEvents() {
|
||||||
|
const events = [...this._domainEvents];
|
||||||
|
this._domainEvents = [];
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.AggregateRoot = AggregateRoot;
|
||||||
2
dist/shared/domain/DomainEvent.js
vendored
Normal file
2
dist/shared/domain/DomainEvent.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
17
dist/shared/domain/Entity.js
vendored
Normal file
17
dist/shared/domain/Entity.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Entity = void 0;
|
||||||
|
const UniqueId_1 = require("./UniqueId");
|
||||||
|
class Entity {
|
||||||
|
constructor(props, id) {
|
||||||
|
this._id = id ?? UniqueId_1.UniqueId.create();
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
get id() { return this._id; }
|
||||||
|
equals(other) {
|
||||||
|
if (!other)
|
||||||
|
return false;
|
||||||
|
return this._id.equals(other._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Entity = Entity;
|
||||||
11
dist/shared/domain/Result.js
vendored
Normal file
11
dist/shared/domain/Result.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Err = exports.Ok = void 0;
|
||||||
|
exports.isOk = isOk;
|
||||||
|
exports.isErr = isErr;
|
||||||
|
const Ok = (value) => ({ ok: true, value });
|
||||||
|
exports.Ok = Ok;
|
||||||
|
const Err = (error) => ({ ok: false, error });
|
||||||
|
exports.Err = Err;
|
||||||
|
function isOk(r) { return r.ok; }
|
||||||
|
function isErr(r) { return !r.ok; }
|
||||||
18
dist/shared/domain/UniqueId.js
vendored
Normal file
18
dist/shared/domain/UniqueId.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.UniqueId = void 0;
|
||||||
|
const uuid_1 = require("uuid");
|
||||||
|
class UniqueId {
|
||||||
|
constructor(value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
static create() { return new UniqueId((0, uuid_1.v4)()); }
|
||||||
|
static from(value) { return new UniqueId(value); }
|
||||||
|
toString() { return this.value; }
|
||||||
|
equals(other) {
|
||||||
|
if (!other)
|
||||||
|
return false;
|
||||||
|
return this.value === other.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.UniqueId = UniqueId;
|
||||||
14
dist/shared/domain/ValueObject.js
vendored
Normal file
14
dist/shared/domain/ValueObject.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ValueObject = void 0;
|
||||||
|
class ValueObject {
|
||||||
|
constructor(props) {
|
||||||
|
this.props = Object.freeze(props);
|
||||||
|
}
|
||||||
|
equals(other) {
|
||||||
|
if (!other)
|
||||||
|
return false;
|
||||||
|
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ValueObject = ValueObject;
|
||||||
22
dist/shared/domain/index.js
vendored
Normal file
22
dist/shared/domain/index.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||||
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
__exportStar(require("./Result"), exports);
|
||||||
|
__exportStar(require("./UniqueId"), exports);
|
||||||
|
__exportStar(require("./Entity"), exports);
|
||||||
|
__exportStar(require("./AggregateRoot"), exports);
|
||||||
|
__exportStar(require("./ValueObject"), exports);
|
||||||
|
__exportStar(require("./DomainEvent"), exports);
|
||||||
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
@@ -13,7 +13,8 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"tailwindcss": "^4.2.1"
|
"tailwindcss": "^4.2.1",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/browser": "^4.0.18",
|
"@vitest/browser": "^4.0.18",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
@@ -2063,6 +2065,13 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.1",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
@@ -5011,6 +5020,19 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"tailwindcss": "^4.2.1"
|
"tailwindcss": "^4.2.1",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/browser": "^4.0.18",
|
"@vitest/browser": "^4.0.18",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
|||||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@types/express-rate-limit": "^5.1.3",
|
"@types/express-rate-limit": "^5.1.3",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/pino": "^7.0.4",
|
"@types/pino": "^7.0.4",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "^7.1.0",
|
||||||
"playwright": "^1.40.0",
|
"playwright": "^1.40.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.3"
|
"socket.io": "^4.8.3",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
@@ -1837,6 +1839,12 @@
|
|||||||
"@types/superagent": "^8.1.0"
|
"@types/superagent": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@@ -6480,6 +6488,19 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@types/express-rate-limit": "^5.1.3",
|
"@types/express-rate-limit": "^5.1.3",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/pino": "^7.0.4",
|
"@types/pino": "^7.0.4",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "^7.1.0",
|
||||||
"playwright": "^1.40.0",
|
"playwright": "^1.40.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.3"
|
"socket.io": "^4.8.3",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/shared/application/EventBus.ts
Normal file
7
src/shared/application/EventBus.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent } from '../domain/DomainEvent';
|
||||||
|
import { EventHandler } from './EventHandler';
|
||||||
|
|
||||||
|
export interface EventBus {
|
||||||
|
publish(event: DomainEvent): Promise<void>;
|
||||||
|
subscribe(eventName: string, handler: EventHandler): void;
|
||||||
|
}
|
||||||
5
src/shared/application/EventHandler.ts
Normal file
5
src/shared/application/EventHandler.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { DomainEvent } from '../domain/DomainEvent';
|
||||||
|
|
||||||
|
export interface EventHandler {
|
||||||
|
handle(event: DomainEvent): Promise<void>;
|
||||||
|
}
|
||||||
5
src/shared/application/UseCase.ts
Normal file
5
src/shared/application/UseCase.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Result } from '../domain/Result';
|
||||||
|
|
||||||
|
export interface UseCase<TRequest, TResponse, TError = Error> {
|
||||||
|
execute(request: TRequest): Promise<Result<TResponse, TError>>;
|
||||||
|
}
|
||||||
3
src/shared/application/index.ts
Normal file
3
src/shared/application/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './UseCase';
|
||||||
|
export * from './EventBus';
|
||||||
|
export * from './EventHandler';
|
||||||
25
src/shared/domain/AggregateRoot.ts
Normal file
25
src/shared/domain/AggregateRoot.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Entity } from './Entity';
|
||||||
|
import { DomainEvent } from './DomainEvent';
|
||||||
|
import { UniqueId } from './UniqueId';
|
||||||
|
|
||||||
|
export abstract class AggregateRoot<T> extends Entity<T> {
|
||||||
|
private _domainEvents: DomainEvent[] = [];
|
||||||
|
|
||||||
|
constructor(props: T, id?: UniqueId) {
|
||||||
|
super(props, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get domainEvents(): ReadonlyArray<DomainEvent> {
|
||||||
|
return this._domainEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addDomainEvent(event: DomainEvent): void {
|
||||||
|
this._domainEvents.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEvents(): DomainEvent[] {
|
||||||
|
const events = [...this._domainEvents];
|
||||||
|
this._domainEvents = [];
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/shared/domain/DomainEvent.ts
Normal file
7
src/shared/domain/DomainEvent.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface DomainEvent {
|
||||||
|
readonly eventId: string;
|
||||||
|
readonly eventName: string;
|
||||||
|
readonly aggregateId: string;
|
||||||
|
readonly occurredOn: Date;
|
||||||
|
readonly payload: Record<string, unknown>;
|
||||||
|
}
|
||||||
18
src/shared/domain/Entity.ts
Normal file
18
src/shared/domain/Entity.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { UniqueId } from './UniqueId';
|
||||||
|
|
||||||
|
export abstract class Entity<T> {
|
||||||
|
protected readonly _id: UniqueId;
|
||||||
|
protected props: T;
|
||||||
|
|
||||||
|
constructor(props: T, id?: UniqueId) {
|
||||||
|
this._id = id ?? UniqueId.create();
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): UniqueId { return this._id; }
|
||||||
|
|
||||||
|
equals(other?: Entity<T>): boolean {
|
||||||
|
if (!other) return false;
|
||||||
|
return this._id.equals(other._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/shared/domain/Result.ts
Normal file
8
src/shared/domain/Result.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
type ResultOk<T> = { readonly ok: true; readonly value: T };
|
||||||
|
type ResultErr<E> = { readonly ok: false; readonly error: E };
|
||||||
|
export type Result<T, E = Error> = ResultOk<T> | ResultErr<E>;
|
||||||
|
|
||||||
|
export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
|
||||||
|
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|
||||||
|
export function isOk<T, E>(r: Result<T, E>): r is ResultOk<T> { return r.ok; }
|
||||||
|
export function isErr<T, E>(r: Result<T, E>): r is ResultErr<E> { return !r.ok; }
|
||||||
15
src/shared/domain/UniqueId.ts
Normal file
15
src/shared/domain/UniqueId.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
export class UniqueId {
|
||||||
|
private constructor(private readonly value: string) {}
|
||||||
|
|
||||||
|
static create(): UniqueId { return new UniqueId(randomUUID()); }
|
||||||
|
static from(value: string): UniqueId { return new UniqueId(value); }
|
||||||
|
|
||||||
|
toString(): string { return this.value; }
|
||||||
|
|
||||||
|
equals(other?: UniqueId): boolean {
|
||||||
|
if (!other) return false;
|
||||||
|
return this.value === other.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/shared/domain/ValueObject.ts
Normal file
12
src/shared/domain/ValueObject.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export abstract class ValueObject<T> {
|
||||||
|
protected readonly props: T;
|
||||||
|
|
||||||
|
constructor(props: T) {
|
||||||
|
this.props = Object.freeze(props) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other?: ValueObject<T>): boolean {
|
||||||
|
if (!other) return false;
|
||||||
|
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/shared/domain/index.ts
Normal file
6
src/shared/domain/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './Result';
|
||||||
|
export * from './UniqueId';
|
||||||
|
export * from './Entity';
|
||||||
|
export * from './AggregateRoot';
|
||||||
|
export * from './ValueObject';
|
||||||
|
export * from './DomainEvent';
|
||||||
105
tests/shared/domain.test.ts
Normal file
105
tests/shared/domain.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Ok, Err, isOk, isErr } from '../../src/shared/domain/Result';
|
||||||
|
import { UniqueId } from '../../src/shared/domain/UniqueId';
|
||||||
|
import { Entity } from '../../src/shared/domain/Entity';
|
||||||
|
import { ValueObject } from '../../src/shared/domain/ValueObject';
|
||||||
|
|
||||||
|
// --- Result ---
|
||||||
|
describe('Result', () => {
|
||||||
|
it('Ok creates accessible value', () => {
|
||||||
|
const r = Ok(42);
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
if (r.ok) expect(r.value).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Err creates accessible error', () => {
|
||||||
|
const e = new Error('fail');
|
||||||
|
const r = Err(e);
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) expect(r.error).toBe(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isOk discriminates correctly', () => {
|
||||||
|
expect(isOk(Ok('x'))).toBe(true);
|
||||||
|
expect(isOk(Err('e'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isErr discriminates correctly', () => {
|
||||||
|
expect(isErr(Err('e'))).toBe(true);
|
||||||
|
expect(isErr(Ok('x'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- UniqueId ---
|
||||||
|
describe('UniqueId', () => {
|
||||||
|
it('create generates a valid UUID string', () => {
|
||||||
|
const id = UniqueId.create();
|
||||||
|
expect(id.toString()).toMatch(/^[0-9a-f-]{36}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('from preserves the value', () => {
|
||||||
|
const id = UniqueId.from('abc-123');
|
||||||
|
expect(id.toString()).toBe('abc-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns true for same value', () => {
|
||||||
|
const a = UniqueId.from('same');
|
||||||
|
const b = UniqueId.from('same');
|
||||||
|
expect(a.equals(b)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns false for different value', () => {
|
||||||
|
const a = UniqueId.create();
|
||||||
|
const b = UniqueId.create();
|
||||||
|
expect(a.equals(b)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns false for undefined', () => {
|
||||||
|
const a = UniqueId.create();
|
||||||
|
expect(a.equals(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Entity ---
|
||||||
|
class TestEntity extends Entity<{ name: string }> {}
|
||||||
|
|
||||||
|
describe('Entity', () => {
|
||||||
|
it('equals compares by id, not by props', () => {
|
||||||
|
const id = UniqueId.from('id-1');
|
||||||
|
const a = new TestEntity({ name: 'Alice' }, id);
|
||||||
|
const b = new TestEntity({ name: 'Bob' }, id);
|
||||||
|
expect(a.equals(b)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns false for different ids', () => {
|
||||||
|
const a = new TestEntity({ name: 'Alice' });
|
||||||
|
const b = new TestEntity({ name: 'Alice' });
|
||||||
|
expect(a.equals(b)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns false for undefined', () => {
|
||||||
|
const a = new TestEntity({ name: 'Alice' });
|
||||||
|
expect(a.equals(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- ValueObject ---
|
||||||
|
class TestVO extends ValueObject<{ amount: number; currency: string }> {}
|
||||||
|
|
||||||
|
describe('ValueObject', () => {
|
||||||
|
it('equals compares by props', () => {
|
||||||
|
const a = new TestVO({ amount: 100, currency: 'USD' });
|
||||||
|
const b = new TestVO({ amount: 100, currency: 'USD' });
|
||||||
|
expect(a.equals(b)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals returns false when props differ', () => {
|
||||||
|
const a = new TestVO({ amount: 100, currency: 'USD' });
|
||||||
|
const b = new TestVO({ amount: 200, currency: 'USD' });
|
||||||
|
expect(a.equals(b)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('props are frozen (immutable)', () => {
|
||||||
|
const vo = new TestVO({ amount: 100, currency: 'USD' });
|
||||||
|
expect(Object.isFrozen((vo as unknown as { props: unknown }).props)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user