@vytches/ddd-value-objects 0.26.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 +21 -0
- package/LLMGUIDE.md +233 -0
- package/README.md +1391 -0
- package/dist/base-value-object.d.ts +35 -0
- package/dist/branded-id.d.ts +58 -0
- package/dist/id.value-object.d.ts +106 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +248 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 VytchesDDD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LLMGUIDE.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# @vytches/ddd-value-objects - LLM Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides `BaseValueObject` for immutable domain values and `EntityId` for
|
|
6
|
+
strongly-typed aggregate identifiers with built-in validation across four ID
|
|
7
|
+
strategies (UUID, integer, bigint, text).
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { BaseValueObject, EntityId } from '@vytches/ddd-value-objects';
|
|
13
|
+
|
|
14
|
+
// Create a value object
|
|
15
|
+
class Money extends BaseValueObject<number> {
|
|
16
|
+
static create(amount: number): Money {
|
|
17
|
+
const vo = new Money(amount);
|
|
18
|
+
if (!vo.validate(amount)) throw new Error('Amount must be non-negative');
|
|
19
|
+
return vo;
|
|
20
|
+
}
|
|
21
|
+
validate(value: unknown): boolean {
|
|
22
|
+
return typeof value === 'number' && value >= 0;
|
|
23
|
+
}
|
|
24
|
+
add(other: Money): Money {
|
|
25
|
+
return Money.create(this.getValue() + other.getValue());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create entity IDs
|
|
30
|
+
const id = EntityId.create(); // random UUID
|
|
31
|
+
const fromDb = EntityId.fromUUID('550e8400-e29b-41d4-a716-446655440000');
|
|
32
|
+
const intId = EntityId.fromInteger(42);
|
|
33
|
+
const textId = EntityId.fromText('order-slug');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Key API
|
|
37
|
+
|
|
38
|
+
| Export | Kind | Description |
|
|
39
|
+
| ---------------------------- | ---------------- | --------------------------------------------------------------------------- |
|
|
40
|
+
| `BaseValueObject<T>` | abstract class | Immutable value holder; provides `equals`, `getValue`, `toString`, `toJSON` |
|
|
41
|
+
| `EntityId<T>` | class | Validated aggregate identifier; extends contracts `EntityId` |
|
|
42
|
+
| `EntityId.create()` | static method | Creates a new random-UUID `EntityId<string>` |
|
|
43
|
+
| `EntityId.fromUUID(v)` | static method | Parses and validates a UUID string |
|
|
44
|
+
| `EntityId.fromInteger(v)` | static method | Validates non-negative integer, stores as string |
|
|
45
|
+
| `EntityId.fromBigInt(v)` | static method | Validates bigint string/bigint, stores as string |
|
|
46
|
+
| `EntityId.fromText(v)` | static method | Validates non-empty text with safe characters |
|
|
47
|
+
| `EntityId.tryFromUUID(v)` | static method | Like `fromUUID` but returns `Result<EntityId, Error>` instead of throwing |
|
|
48
|
+
| `EntityId.tryFromInteger(v)` | static method | Like `fromInteger` but returns `Result<EntityId, Error>` |
|
|
49
|
+
| `EntityId.tryFromBigInt(v)` | static method | Like `fromBigInt` but returns `Result<EntityId, Error>` |
|
|
50
|
+
| `EntityId.tryFromText(v)` | static method | Like `fromText` but returns `Result<EntityId, Error>` |
|
|
51
|
+
| `EntityIdFactory` | class | **Deprecated.** Use `EntityId` static methods instead |
|
|
52
|
+
| `IEntityId<T>` | re-exported type | From `@vytches/ddd-contracts` — the interface |
|
|
53
|
+
| `IEntityIdFactory` | re-exported type | From `@vytches/ddd-contracts` — the factory interface |
|
|
54
|
+
| `IdType` | re-exported type | `'uuid' \| 'integer' \| 'text' \| 'bigint'` |
|
|
55
|
+
|
|
56
|
+
### `BaseValueObject<T>` method reference
|
|
57
|
+
|
|
58
|
+
| Method | Description |
|
|
59
|
+
| ----------------------------------- | -------------------------------------------------- |
|
|
60
|
+
| `getValue(): T` | Returns the raw wrapped value |
|
|
61
|
+
| `equals(other): boolean` | Structural equality by raw value (`===`) |
|
|
62
|
+
| `toString(): string` | `String(value)` |
|
|
63
|
+
| `toJSON(): string` | `JSON.stringify(value)` |
|
|
64
|
+
| `abstract validate(value): boolean` | Must be implemented; called in subclass `create()` |
|
|
65
|
+
|
|
66
|
+
## Patterns
|
|
67
|
+
|
|
68
|
+
### Validated value object with factory
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { BaseValueObject } from '@vytches/ddd-value-objects';
|
|
72
|
+
|
|
73
|
+
class EmailAddress extends BaseValueObject<string> {
|
|
74
|
+
static create(raw: string): EmailAddress {
|
|
75
|
+
const vo = new EmailAddress(raw.toLowerCase().trim());
|
|
76
|
+
if (!vo.validate(raw)) {
|
|
77
|
+
throw new Error(`Invalid email: ${raw}`);
|
|
78
|
+
}
|
|
79
|
+
return vo;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
validate(value: unknown): boolean {
|
|
83
|
+
return (
|
|
84
|
+
typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const email = EmailAddress.create('Alice@Example.com');
|
|
90
|
+
email.getValue(); // 'alice@example.com'
|
|
91
|
+
email.equals(EmailAddress.create('alice@example.com')); // true
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Using EntityId as aggregate key
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
98
|
+
import { AggregateRoot } from '@vytches/ddd-aggregates';
|
|
99
|
+
import type { IAggregateConstructorParams } from '@vytches/ddd-aggregates';
|
|
100
|
+
|
|
101
|
+
class Order extends AggregateRoot<string> {
|
|
102
|
+
constructor(params: IAggregateConstructorParams<string>) {
|
|
103
|
+
super(params);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static create(): Order {
|
|
107
|
+
return new Order({ id: EntityId.create(), version: 0 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const order = Order.create();
|
|
112
|
+
order.getId().getValue(); // UUID string
|
|
113
|
+
order.getId().getType(); // 'uuid'
|
|
114
|
+
order.getId().toString(); // same UUID string
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Comparing two IDs
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
121
|
+
|
|
122
|
+
const a = EntityId.fromUUID('550e8400-e29b-41d4-a716-446655440000');
|
|
123
|
+
const b = EntityId.fromUUID('550e8400-e29b-41d4-a716-446655440000');
|
|
124
|
+
|
|
125
|
+
a.equals(b); // true
|
|
126
|
+
a.getValue() === b.getValue(); // true — same raw string
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Anti-Patterns
|
|
130
|
+
|
|
131
|
+
**Making value objects mutable.** `BaseValueObject.value` is `readonly` — never
|
|
132
|
+
expose a setter. All transformations must return a new instance (e.g., `add()`
|
|
133
|
+
returns `Money.create(...)`).
|
|
134
|
+
|
|
135
|
+
**Not implementing `validate()`.** The abstract method must be implemented.
|
|
136
|
+
Returning `true` unconditionally defeats the purpose. Call `validate` inside the
|
|
137
|
+
static factory and throw or return `Result.fail` there — do not throw inside the
|
|
138
|
+
constructor.
|
|
139
|
+
|
|
140
|
+
**Using primitive types instead of value objects for domain concepts.** Passing
|
|
141
|
+
`string` for email or `number` for money loses invariant enforcement at compile
|
|
142
|
+
time. Wrap every domain-meaningful primitive in a `BaseValueObject` subclass.
|
|
143
|
+
|
|
144
|
+
**Using `EntityIdFactory` in new code.** `EntityIdFactory` is deprecated. Use
|
|
145
|
+
`EntityId.create()`, `EntityId.fromUUID()`, etc. directly. The factory class
|
|
146
|
+
exists only for backward compatibility.
|
|
147
|
+
|
|
148
|
+
**Calling `new EntityId(value, type)` directly in application code.** The
|
|
149
|
+
constructor does not validate the value in the base class. Always use the static
|
|
150
|
+
factory methods (`create`, `fromUUID`, etc.) which throw `InvalidParameterError`
|
|
151
|
+
or `MissingValueError` on bad input.
|
|
152
|
+
|
|
153
|
+
## Hidden Features
|
|
154
|
+
|
|
155
|
+
`EntityId.fromInteger` stores the integer as a string internally (returns
|
|
156
|
+
`EntityId<string>` not `EntityId<number>`) — this is intentional for consistent
|
|
157
|
+
serialization across all ID types.
|
|
158
|
+
|
|
159
|
+
`EntityId.fromBigInt` accepts both a native `bigint` and a `string`
|
|
160
|
+
representation — useful when reading from databases that return bigints as
|
|
161
|
+
strings.
|
|
162
|
+
|
|
163
|
+
`BaseValueObject.toJSON()` returns a JSON-serialized string of the value, not
|
|
164
|
+
the value itself — if you assign a value object to a JSON payload, call
|
|
165
|
+
`getValue()` instead of `toJSON()` to avoid double-encoding.
|
|
166
|
+
|
|
167
|
+
The `validate()` method on `BaseValueObject` receives `unknown` (not `T`) —
|
|
168
|
+
implement defensive type checks even though TypeScript narrows the parameter in
|
|
169
|
+
practice.
|
|
170
|
+
|
|
171
|
+
## Result-Returning Factories (since 0.24.0)
|
|
172
|
+
|
|
173
|
+
Prefer `tryFromX()` over `fromX()` — returns `Result<EntityId, Error>` instead
|
|
174
|
+
of throwing, enabling functional error handling.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
178
|
+
|
|
179
|
+
// Returns Result — never throws
|
|
180
|
+
const result = EntityId.tryFromUUID(userInput);
|
|
181
|
+
|
|
182
|
+
if (result.isFailure) {
|
|
183
|
+
console.log('Invalid ID:', result.error.message);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const id = result.value; // EntityId<string>
|
|
188
|
+
|
|
189
|
+
// Chain with Result.map/flatMap
|
|
190
|
+
const idString = EntityId.tryFromUUID(userInput).map(id => id.value);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Available: `tryFromUUID`, `tryFromInteger`, `tryFromBigInt`, `tryFromText`. The
|
|
194
|
+
`fromX()` throwing variants still work for backward compatibility.
|
|
195
|
+
|
|
196
|
+
## Branded ID Types (since 0.24.0)
|
|
197
|
+
|
|
198
|
+
Prevent accidentally passing an OrderId where a CustomerId is expected. Zero
|
|
199
|
+
runtime overhead — branding exists only in the type system.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import {
|
|
203
|
+
type BrandedId,
|
|
204
|
+
newBrandedId,
|
|
205
|
+
brandedIdFromUUID,
|
|
206
|
+
} from '@vytches/ddd-value-objects';
|
|
207
|
+
|
|
208
|
+
// Define domain-specific ID types
|
|
209
|
+
type OrderId = BrandedId<'Order'>;
|
|
210
|
+
type CustomerId = BrandedId<'Customer'>;
|
|
211
|
+
|
|
212
|
+
// Create branded IDs
|
|
213
|
+
const orderId: OrderId = newBrandedId<'Order'>();
|
|
214
|
+
const customerId: CustomerId = brandedIdFromUUID<'Customer'>('550e8400-...');
|
|
215
|
+
|
|
216
|
+
// Type-safe function — compile error if wrong ID type passed
|
|
217
|
+
function cancelOrder(id: OrderId): void {
|
|
218
|
+
/* ... */
|
|
219
|
+
}
|
|
220
|
+
cancelOrder(orderId); // OK
|
|
221
|
+
// cancelOrder(customerId); // Compile error!
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Available factories: `createBrandedId<Tag>(entityId)`, `newBrandedId<Tag>()`,
|
|
225
|
+
`brandedIdFromUUID<Tag>(uuid)`, `brandedIdFromText<Tag>(text)`.
|
|
226
|
+
|
|
227
|
+
## Package Dependencies
|
|
228
|
+
|
|
229
|
+
**Depends on:** `@vytches/ddd-contracts`, `@vytches/ddd-domain-primitives`,
|
|
230
|
+
`@vytches/ddd-utils`.
|
|
231
|
+
|
|
232
|
+
**Depended on by:** `@vytches/ddd-aggregates` (re-exports `EntityId` from
|
|
233
|
+
contracts), `@vytches/ddd-enterprise`.
|