drimion 0.1.0
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/LICENSE +7 -0
- package/README.md +955 -0
- package/dist/cli/index.js +1045 -0
- package/dist/cli/templates/aggregate.ts.hbs +22 -0
- package/dist/cli/templates/entity.ts.hbs +16 -0
- package/dist/cli/templates/repository.ts.hbs +24 -0
- package/dist/cli/templates/use-case.ts.hbs +20 -0
- package/dist/cli/templates/value-object.ts.hbs +16 -0
- package/dist/kernel/core/aggregate.ts +234 -0
- package/dist/kernel/core/entity.ts +348 -0
- package/dist/kernel/core/id.ts +207 -0
- package/dist/kernel/core/index.ts +5 -0
- package/dist/kernel/core/repository.ts +81 -0
- package/dist/kernel/core/value-object.ts +309 -0
- package/dist/kernel/events/browser-event-manager.ts +241 -0
- package/dist/kernel/events/domain-event.ts +76 -0
- package/dist/kernel/events/event-bus.ts +158 -0
- package/dist/kernel/events/event-context.ts +95 -0
- package/dist/kernel/events/event-manager.ts +20 -0
- package/dist/kernel/events/event-utils.ts +19 -0
- package/dist/kernel/events/index.ts +7 -0
- package/dist/kernel/events/server-event-manager.ts +169 -0
- package/dist/kernel/helpers/auto-mapper.ts +222 -0
- package/dist/kernel/helpers/domain-classes.ts +162 -0
- package/dist/kernel/helpers/domain-error.ts +52 -0
- package/dist/kernel/helpers/getters-setters.ts +385 -0
- package/dist/kernel/helpers/index.ts +7 -0
- package/dist/kernel/index.ts +73 -0
- package/dist/kernel/libs/crypto.ts +33 -0
- package/dist/kernel/libs/index.ts +5 -0
- package/dist/kernel/libs/iterator.ts +298 -0
- package/dist/kernel/libs/result.ts +252 -0
- package/dist/kernel/libs/utils.ts +260 -0
- package/dist/kernel/libs/validator.ts +353 -0
- package/dist/kernel/types/adapter.types.ts +26 -0
- package/dist/kernel/types/command.types.ts +37 -0
- package/dist/kernel/types/entity.types.ts +60 -0
- package/dist/kernel/types/event.types.ts +129 -0
- package/dist/kernel/types/index.ts +26 -0
- package/dist/kernel/types/iterator.types.ts +39 -0
- package/dist/kernel/types/result.types.ts +122 -0
- package/dist/kernel/types/uid.types.ts +18 -0
- package/dist/kernel/types/utils.types.ts +120 -0
- package/dist/kernel/types/value-object.types.ts +20 -0
- package/dist/kernel/utils/date.utils.ts +111 -0
- package/dist/kernel/utils/index.ts +32 -0
- package/dist/kernel/utils/number.utils.ts +341 -0
- package/dist/kernel/utils/object.utils.ts +61 -0
- package/dist/kernel/utils/string.utils.ts +128 -0
- package/dist/kernel/utils/type.utils.ts +33 -0
- package/package.json +59 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { DomainEventPayload, EventEntry } from "../types/event.types";
|
|
3
|
+
import { BaseEventManager } from "./event-manager";
|
|
4
|
+
import { ValidateEventName } from "./event-utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Server-side singleton event manager backed by Node.js `EventEmitter`.
|
|
9
|
+
*
|
|
10
|
+
* Manages application-level event subscriptions and dispatches for server
|
|
11
|
+
* environments (Node.js, Bun, Deno). Supports wildcard event dispatch using
|
|
12
|
+
* `*` as a glob-style pattern.
|
|
13
|
+
*
|
|
14
|
+
* Use `EventContext.resolve()` to obtain this instance rather than
|
|
15
|
+
* constructing it directly. For most use cases, prefer `EventBus` —
|
|
16
|
+
* use this only when you need direct access to the `EventEmitter` transport.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const manager = ServerEventManager.instance();
|
|
21
|
+
*
|
|
22
|
+
* manager.subscribe('order:placed', (event) => {
|
|
23
|
+
* console.log(event.detail);
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* manager.dispatchEvent('order:placed', { orderId: '123' });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class ServerEventManager extends BaseEventManager {
|
|
30
|
+
private static _instance: ServerEventManager;
|
|
31
|
+
private readonly emitter: EventEmitter;
|
|
32
|
+
private readonly entries: EventEntry[] = [];
|
|
33
|
+
|
|
34
|
+
private constructor() {
|
|
35
|
+
super();
|
|
36
|
+
// Use `process.versions?.node` instead of just `process` to avoid false
|
|
37
|
+
// positives in bundled browser environments where `process` may be polyfilled
|
|
38
|
+
// by Webpack / Vite but `process.versions.node` is never set.
|
|
39
|
+
if (
|
|
40
|
+
typeof process === "undefined" ||
|
|
41
|
+
process.versions?.node === undefined ||
|
|
42
|
+
typeof EventEmitter === "undefined"
|
|
43
|
+
) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"ServerEventManager is not supported in this environment.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
this.emitter = new EventEmitter({ captureRejections: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @description
|
|
53
|
+
* Returns the singleton `ServerEventManager` instance, creating it if necessary.
|
|
54
|
+
*
|
|
55
|
+
* @returns The singleton `ServerEventManager`.
|
|
56
|
+
* @throws {Error} If called outside a Node.js-compatible environment.
|
|
57
|
+
*/
|
|
58
|
+
public static instance(): ServerEventManager {
|
|
59
|
+
if (!ServerEventManager._instance) {
|
|
60
|
+
ServerEventManager._instance = new ServerEventManager();
|
|
61
|
+
}
|
|
62
|
+
return ServerEventManager._instance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @description
|
|
67
|
+
* Checks whether an event with the given name is currently registered.
|
|
68
|
+
*
|
|
69
|
+
* @param eventName The event name to check.
|
|
70
|
+
* @returns `true` if at least one listener exists for this event name.
|
|
71
|
+
*/
|
|
72
|
+
public exists(eventName: string): boolean {
|
|
73
|
+
return (
|
|
74
|
+
this.emitter.listenerCount(eventName) > 0 ||
|
|
75
|
+
this.entries.some((e) => e.eventName === eventName)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @description
|
|
81
|
+
* Dispatches an event by name, forwarding optional arguments as the `detail` payload.
|
|
82
|
+
*
|
|
83
|
+
* Supports wildcard patterns — if `eventName` contains `*`, all registered events
|
|
84
|
+
* whose names match the resulting regex are dispatched.
|
|
85
|
+
*
|
|
86
|
+
* @param eventName The event name or wildcard pattern to dispatch.
|
|
87
|
+
* @param args Additional arguments forwarded as `detail` to handlers.
|
|
88
|
+
*/
|
|
89
|
+
public dispatchEvent(eventName: string, ...args: unknown[]): void {
|
|
90
|
+
ValidateEventName(eventName);
|
|
91
|
+
|
|
92
|
+
if (eventName.includes("*")) {
|
|
93
|
+
const regex = new RegExp(eventName.replace("*", ".*"));
|
|
94
|
+
for (const entry of this.entries) {
|
|
95
|
+
if (regex.test(entry.eventName)) {
|
|
96
|
+
this.emitter.emit(entry.eventName, { detail: args });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.emitter.emit(eventName, { detail: args });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @description
|
|
107
|
+
* Removes a registered event and its associated listener.
|
|
108
|
+
*
|
|
109
|
+
* @param eventName The name of the event to remove.
|
|
110
|
+
* @returns `true` if the event was found and removed; `false` otherwise.
|
|
111
|
+
*/
|
|
112
|
+
public removeEvent(eventName: string): boolean {
|
|
113
|
+
// const entry = this.entries.find((e) => e.eventName === eventName);
|
|
114
|
+
// if (!entry) return false;
|
|
115
|
+
// this.entries.splice(this.entries.indexOf(entry), 1);
|
|
116
|
+
// this.emitter.removeListener(eventName, entry.callback);
|
|
117
|
+
// return true;
|
|
118
|
+
|
|
119
|
+
const matching = this.entries.filter((e) => e.eventName === eventName);
|
|
120
|
+
|
|
121
|
+
if (matching.length === 0) return false;
|
|
122
|
+
this.entries.splice(
|
|
123
|
+
0,
|
|
124
|
+
this.entries.length,
|
|
125
|
+
...this.entries.filter((e) => e.eventName !== eventName),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const entry of matching) {
|
|
129
|
+
this.emitter.removeListener(eventName, entry.callback);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @description
|
|
137
|
+
* Subscribes a callback to the specified event name.
|
|
138
|
+
*
|
|
139
|
+
* If an event with the same name is already registered, this call is a no-op.
|
|
140
|
+
* Event names must follow the `context:EventName` format.
|
|
141
|
+
*
|
|
142
|
+
* @param eventName The event name to subscribe to.
|
|
143
|
+
* @param fn The callback to invoke when the event is dispatched.
|
|
144
|
+
*
|
|
145
|
+
* @throws {DomainError} If the event name does not follow the required format.
|
|
146
|
+
*/
|
|
147
|
+
public subscribe(
|
|
148
|
+
eventName: string,
|
|
149
|
+
fn: (event: DomainEventPayload) => void | Promise<void>,
|
|
150
|
+
): void {
|
|
151
|
+
ValidateEventName(eventName);
|
|
152
|
+
|
|
153
|
+
this.entries.push({ eventName, callback: fn });
|
|
154
|
+
this.emitter.addListener(eventName, fn);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @description
|
|
159
|
+
* Resets the cached event manager instance.
|
|
160
|
+
*
|
|
161
|
+
* Intended for use in tests where environment switching or clean state
|
|
162
|
+
* between test cases is required. Not recommended in production code.
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
public static __reset(): void {
|
|
167
|
+
ServerEventManager._instance = undefined as unknown as ServerEventManager;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import validator, { type Validator } from "../libs/validator";
|
|
2
|
+
import type { AnyObject } from "../types/utils.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @description
|
|
6
|
+
* Defines the shape of data used for mapping an entity's properties.
|
|
7
|
+
*/
|
|
8
|
+
export interface EntityAutoMapperPayload {
|
|
9
|
+
id: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @description
|
|
16
|
+
* The `AutoMapper` class is responsible for transforming domain resources (entities, value objects)
|
|
17
|
+
* into plain objects or primitive values. It provides methods to recursively process nested value objects, IDs,
|
|
18
|
+
* entities, and arrays, ensuring all complex data structures are serialized into a consistent object format.
|
|
19
|
+
*/
|
|
20
|
+
export class AutoMapper {
|
|
21
|
+
private validator: Validator = validator;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @description
|
|
25
|
+
* Transforms an entity into a plain object, including its associated meta properties
|
|
26
|
+
* (`id`, `createdAt`, `updatedAt`).
|
|
27
|
+
*
|
|
28
|
+
* This method:
|
|
29
|
+
* - Resolves IDs to their primitive value forms.
|
|
30
|
+
* - Recursively converts nested entities, aggregates, and value objects to plain objects.
|
|
31
|
+
* - Preserves arrays and transforms their elements as needed.
|
|
32
|
+
*
|
|
33
|
+
* @param entity The entity instance to be transformed into a plain object.
|
|
34
|
+
* @returns A plain object representing the entity, including its metadata and serialized properties.
|
|
35
|
+
*/
|
|
36
|
+
public entityToObj(entity: unknown): unknown {
|
|
37
|
+
if (this.validator.isID(entity)) {
|
|
38
|
+
return (entity as { value(): string }).value();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.validator.isSymbol(entity)) {
|
|
42
|
+
return (entity as symbol).description;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
this.validator.isBoolean(entity) ||
|
|
47
|
+
this.validator.isNumber(entity) ||
|
|
48
|
+
this.validator.isString(entity) ||
|
|
49
|
+
this.validator.isDate(entity) ||
|
|
50
|
+
entity === null
|
|
51
|
+
) {
|
|
52
|
+
return entity;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.validator.isValueObject(entity)) {
|
|
56
|
+
return this.valueObjectToObj(entity);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.validator.isArray(entity)) {
|
|
60
|
+
return (entity as unknown[]).map((item) => this.valueObjectToObj(item));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.validator.isObject(entity)) {
|
|
64
|
+
const result: AnyObject = {};
|
|
65
|
+
for (const key of Object.keys(entity as object)) {
|
|
66
|
+
result[key] = this.entityToObj((entity as AnyObject)[key]);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.validator.isEntity(entity) || this.validator.isAggregate(entity)) {
|
|
72
|
+
return this.serializeEntity(entity);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return entity;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @description
|
|
80
|
+
* Serializes an entity or aggregate into a plain object, extracting its identity
|
|
81
|
+
* metadata (`id`, `createdAt`, `updatedAt`) and recursively converting all nested
|
|
82
|
+
* properties into their primitive or serialized equivalents.
|
|
83
|
+
*
|
|
84
|
+
* This method is called internally by `entityToObj` when the target is confirmed
|
|
85
|
+
* to be an entity or aggregate (identified via `__kind` marker).
|
|
86
|
+
*
|
|
87
|
+
* @param entity The entity or aggregate instance to serialize. Expected to have
|
|
88
|
+
* a `props` object and an `id` field exposing a `.value()` method.
|
|
89
|
+
* @returns A plain object representation of the entity, including all metadata
|
|
90
|
+
* and recursively serialized properties.
|
|
91
|
+
*/
|
|
92
|
+
private serializeEntity(entity: unknown): unknown {
|
|
93
|
+
const e = entity as AnyObject;
|
|
94
|
+
const props = (e.props ?? {}) as AnyObject;
|
|
95
|
+
const id = (e.id as { value(): string } | undefined)?.value();
|
|
96
|
+
|
|
97
|
+
const result: AnyObject & Partial<EntityAutoMapperPayload> = {
|
|
98
|
+
id,
|
|
99
|
+
createdAt: props.createdAt as Date | undefined,
|
|
100
|
+
updatedAt: props.updatedAt as Date | undefined,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const key of Object.keys(props)) {
|
|
104
|
+
if (key === "createdAt" || key === "updatedAt") continue;
|
|
105
|
+
|
|
106
|
+
const val = props[key];
|
|
107
|
+
|
|
108
|
+
if (this.validator.isID(val)) {
|
|
109
|
+
result[key] = (val as { value(): string }).value();
|
|
110
|
+
} else if (this.validator.isArray(val)) {
|
|
111
|
+
result[key] = (val as unknown[]).map((item) => this.entityToObj(item));
|
|
112
|
+
} else if (this.validator.isEntity(val)) {
|
|
113
|
+
result[key] = this.entityToObj(val);
|
|
114
|
+
} else if (this.validator.isSymbol(val)) {
|
|
115
|
+
result[key] = (val as symbol).description;
|
|
116
|
+
} else {
|
|
117
|
+
result[key] = this.valueObjectToObj(val);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @description
|
|
126
|
+
* Converts a value object into a plain object or a primitive value.
|
|
127
|
+
*
|
|
128
|
+
* This method handles multiple scenarios, including:
|
|
129
|
+
* - Null values
|
|
130
|
+
* - Symbol values
|
|
131
|
+
* - ID values
|
|
132
|
+
* - Simple data types (strings, numbers, booleans, dates, objects)
|
|
133
|
+
* - Nested value objects
|
|
134
|
+
* - Arrays containing complex data
|
|
135
|
+
*
|
|
136
|
+
* @param valueObject An instance representing a value object to be transformed.
|
|
137
|
+
* @returns A plain object, primitive value, or serialized structure derived from the given value object.
|
|
138
|
+
*/
|
|
139
|
+
public valueObjectToObj(valueObject: unknown): unknown {
|
|
140
|
+
if (valueObject === null) return null;
|
|
141
|
+
if (typeof valueObject === "undefined") return undefined;
|
|
142
|
+
|
|
143
|
+
if (this.validator.isSymbol(valueObject)) {
|
|
144
|
+
return (valueObject as symbol).description;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.validator.isID(valueObject)) {
|
|
148
|
+
return (valueObject as { value(): string }).value();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
this.validator.isBoolean(valueObject) ||
|
|
153
|
+
this.validator.isNumber(valueObject) ||
|
|
154
|
+
this.validator.isString(valueObject) ||
|
|
155
|
+
this.validator.isDate(valueObject)
|
|
156
|
+
) {
|
|
157
|
+
return valueObject;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.validator.isArray(valueObject)) {
|
|
161
|
+
return (valueObject as unknown[]).map((item) =>
|
|
162
|
+
this.valueObjectToObj(item),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.validator.isObject(valueObject)) {
|
|
167
|
+
const result: AnyObject = {};
|
|
168
|
+
for (const key of Object.keys(valueObject as object)) {
|
|
169
|
+
result[key] = this.entityToObj((valueObject as AnyObject)[key]);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// treat as VO with props
|
|
175
|
+
const vo = valueObject as AnyObject;
|
|
176
|
+
const voProps = vo.props;
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
this.validator.isBoolean(voProps) ||
|
|
180
|
+
this.validator.isNumber(voProps) ||
|
|
181
|
+
this.validator.isString(voProps) ||
|
|
182
|
+
this.validator.isDate(voProps)
|
|
183
|
+
) {
|
|
184
|
+
return voProps;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.validator.isSymbol(voProps)) {
|
|
188
|
+
return (voProps as symbol).description;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.validator.isArray(voProps)) {
|
|
192
|
+
return (voProps as unknown[]).map((item) => this.valueObjectToObj(item));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.validator.isObject(voProps)) {
|
|
196
|
+
const props = voProps as AnyObject;
|
|
197
|
+
const result: AnyObject = {};
|
|
198
|
+
|
|
199
|
+
for (const key of Object.keys(props)) {
|
|
200
|
+
const val = props[key];
|
|
201
|
+
|
|
202
|
+
if (this.validator.isValueObject(val)) {
|
|
203
|
+
result[key] = this.valueObjectToObj(val);
|
|
204
|
+
} else if (this.validator.isID(val)) {
|
|
205
|
+
result[key] = (val as { value(): string }).value();
|
|
206
|
+
} else if (this.validator.isSymbol(val)) {
|
|
207
|
+
result[key] = (val as symbol).description;
|
|
208
|
+
} else if (this.validator.isArray(val)) {
|
|
209
|
+
result[key] = (val as unknown[]).map((item) =>
|
|
210
|
+
this.valueObjectToObj(item),
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
result[key] = val;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return this.entityToObj(voProps);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Iterator } from "../libs/iterator";
|
|
2
|
+
import { Result } from "../libs/result";
|
|
3
|
+
import type { IIterator } from "../types/iterator.types";
|
|
4
|
+
import type { IResult } from "../types/result.types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Represents the result of a `DomainClasseses.createMany()` operation.
|
|
9
|
+
*
|
|
10
|
+
* Contains both an iterator over each individual creation result and a combined
|
|
11
|
+
* result that reflects the overall success or failure of the batch operation.
|
|
12
|
+
*/
|
|
13
|
+
export interface CreateManyResult {
|
|
14
|
+
/**
|
|
15
|
+
* @description
|
|
16
|
+
* An iterator over each individual `Result` produced during the batch creation.
|
|
17
|
+
* Use `data.next()` to access results in the same order as the input entries.
|
|
18
|
+
*/
|
|
19
|
+
data: IIterator<IResult<unknown, unknown, unknown>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @description
|
|
23
|
+
* A combined `Result` representing the overall outcome.
|
|
24
|
+
* Will be a failure if any single entry failed to be created.
|
|
25
|
+
*/
|
|
26
|
+
result: IResult<unknown, unknown, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @description
|
|
31
|
+
* Represents a pairing of a domain class and the props needed to construct an instance of it.
|
|
32
|
+
* Used internally by the `DomainClasses` fluent builder.
|
|
33
|
+
*/
|
|
34
|
+
interface DomainClassesEntry {
|
|
35
|
+
class: {
|
|
36
|
+
create: (props: unknown) => IResult<unknown, unknown, unknown>;
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
39
|
+
props: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @description
|
|
44
|
+
* Fluent builder for creating multiple domain instances in a single batch operation.
|
|
45
|
+
*
|
|
46
|
+
* Chain `.prepare()` calls to register domain classes and their props,
|
|
47
|
+
* then call `.create()` to execute all instantiations at once.
|
|
48
|
+
*
|
|
49
|
+
* The combined `result` reflects the overall success or failure of the batch —
|
|
50
|
+
* it fails if any single entry fails. Individual results are accessible via the
|
|
51
|
+
* returned `data` iterator in the same order as the `.prepare()` calls.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const { result, data } = DomainClasses
|
|
56
|
+
* .prepare(Age, { value: 21 })
|
|
57
|
+
* .prepare(Name, { value: 'Alice' })
|
|
58
|
+
* .create();
|
|
59
|
+
*
|
|
60
|
+
* if (result.isSuccess()) {
|
|
61
|
+
* const age = data.next().value() as Age;
|
|
62
|
+
* const name = data.next().value() as Name;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class DomainClasses {
|
|
67
|
+
private readonly entries: DomainClassesEntry[] = [];
|
|
68
|
+
|
|
69
|
+
private constructor() {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @description
|
|
73
|
+
* Registers a domain class and its construction props for batch instantiation.
|
|
74
|
+
*
|
|
75
|
+
* Returns the same `DomainClasses` builder instance to allow chaining.
|
|
76
|
+
*
|
|
77
|
+
* @param domainClasses The domain class with a static `create` method.
|
|
78
|
+
* @param props The props to pass to the class's `create` method.
|
|
79
|
+
* @returns The current `DomainClasses` builder instance.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* DomainClasses.prepare(Money, { amount: 100 })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
public prepare<Props>(
|
|
87
|
+
domainClasses: DomainClassesEntry["class"],
|
|
88
|
+
props: Props,
|
|
89
|
+
): this {
|
|
90
|
+
this.entries.push({ class: domainClasses, props });
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @description
|
|
96
|
+
* Executes the batch creation of all registered domain class entries.
|
|
97
|
+
*
|
|
98
|
+
* Iterates over each registered entry and calls its class's static `create` method.
|
|
99
|
+
* If an entry's class does not expose a `create` method, a failure `Result` is
|
|
100
|
+
* recorded for that entry.
|
|
101
|
+
*
|
|
102
|
+
* @returns A `CreateManyResult` containing:
|
|
103
|
+
* - `result`: A combined `IResult` — fails if any entry failed.
|
|
104
|
+
* - `data`: An `IIterator` over each individual `IResult`, in registration order.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const { result, data } = DomainClasses
|
|
109
|
+
* .prepare(Age, { value: 21 })
|
|
110
|
+
* .prepare(Name, { value: 'Alice' })
|
|
111
|
+
* .create();
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
public create(): CreateManyResult {
|
|
115
|
+
const results: IResult<unknown, unknown, unknown>[] = [];
|
|
116
|
+
|
|
117
|
+
for (const entry of this.entries) {
|
|
118
|
+
if (typeof entry.class?.create !== "function") {
|
|
119
|
+
results.push(
|
|
120
|
+
Result.error(
|
|
121
|
+
`No static 'create' method found in class ${entry.class?.name}.`,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
results.push(entry.class.create(entry.props));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const iterator = Iterator.create({
|
|
130
|
+
initialData: results,
|
|
131
|
+
returnCurrentOnReversion: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
data: iterator,
|
|
136
|
+
result: Result.combine(results),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description
|
|
142
|
+
* Entry point for the `DomainClasses` fluent builder.
|
|
143
|
+
*
|
|
144
|
+
* Equivalent to `new DomainClasses().prepare(domainClasses, props)` — starts a new
|
|
145
|
+
* builder chain with the first class-props pair already registered.
|
|
146
|
+
*
|
|
147
|
+
* @param domainClasses The domain class with a static `create` method.
|
|
148
|
+
* @param props The props to pass to the class's `create` method.
|
|
149
|
+
* @returns A new `DomainClasses` builder instance with the first entry registered.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* DomainClasses.prepare(Age, { value: 21 }).prepare(Name, { value: 'Alice' }).create()
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
public static prepare<Props>(
|
|
157
|
+
domainClasses: DomainClassesEntry["class"],
|
|
158
|
+
props: Props,
|
|
159
|
+
): DomainClasses {
|
|
160
|
+
return new DomainClasses().prepare(domainClasses, props);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description
|
|
3
|
+
* Represents a domain-level error thrown when a business rule or invariant is violated.
|
|
4
|
+
*
|
|
5
|
+
* Unlike generic `Error`, `DomainError` carries structured context about where and why
|
|
6
|
+
* the violation occurred, making it easier to handle, log, and surface meaningful
|
|
7
|
+
* feedback to callers.
|
|
8
|
+
*
|
|
9
|
+
* Use `DomainError` whenever a domain operation fails due to an invariant violation —
|
|
10
|
+
* for example, when `set().to()` receives an invalid value, or when a required domain
|
|
11
|
+
* rule is not satisfied.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* throw new DomainError('Amount must be positive', {
|
|
16
|
+
* field: 'amount',
|
|
17
|
+
* context: 'Money',
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class DomainError extends Error {
|
|
22
|
+
public readonly field?: string;
|
|
23
|
+
public readonly context?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @description
|
|
27
|
+
* Creates a new `DomainError` instance.
|
|
28
|
+
*
|
|
29
|
+
* @param message A human-readable description of the violation.
|
|
30
|
+
* @param options Optional structured context about the violation.
|
|
31
|
+
* @param options.field The property or field name that caused the error.
|
|
32
|
+
* @param options.context The domain class or context name where the error originated.
|
|
33
|
+
*/
|
|
34
|
+
constructor(
|
|
35
|
+
message: string,
|
|
36
|
+
options?: { field?: string; context?: string; cause?: unknown },
|
|
37
|
+
) {
|
|
38
|
+
const formattedMessage = options?.context
|
|
39
|
+
? `[${options.context}] ${message}`
|
|
40
|
+
: message;
|
|
41
|
+
|
|
42
|
+
super(formattedMessage, { cause: options?.cause });
|
|
43
|
+
this.name = "DomainError";
|
|
44
|
+
this.field = options?.field;
|
|
45
|
+
this.context = options?.context;
|
|
46
|
+
|
|
47
|
+
// Maintains proper stack trace in V8 environments (Node.js, Bun, Chrome)
|
|
48
|
+
if (Error.captureStackTrace) {
|
|
49
|
+
Error.captureStackTrace(this, DomainError);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|