Skip to content

Commit 00ad0c9

Browse files
Merge pull request #52 from technote-space/release/next-v0.17.1
release: v0.18.0
2 parents 6e0a228 + 40acdd9 commit 00ad0c9

File tree

6 files changed

+53
-50
lines changed

6 files changed

+53
-50
lines changed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Input → fromInput() → Inner → toOutput() → Output (cached & frozen)
4646
#### Entity Pattern
4747
- **Base class**: `Entity<Props>`
4848
- **Factory methods**: `_create()` (with validation), `_reconstruct()` (skip validation), `_update()` (partial update)
49-
- **Property access**: Both `entity.get('prop')` and direct `entity.prop` via Proxy
49+
- **Property access**: `entity.get('prop')`
5050
- **Identity-based equality**: Must implement `equals()` method
5151

5252
### Code Organization
@@ -87,4 +87,4 @@ Input → fromInput() → Inner → toOutput() → Output (cached & frozen)
8787
1. Define props interface with Value Object properties
8888
2. Implement static factory methods with proper generics
8989
3. Implement `equals()` method based on business identity
90-
4. Add comprehensive tests covering creation, updates, and validation
90+
4. Add comprehensive tests covering creation, updates, and validation

README.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,6 @@ name = 'New Name'; // エラー: Cannot assign to 'name' because it is a read-on
129129

130130
Entity は識別子を持ち、ライフサイクルを通じて同一性を維持するオブジェクトを表現するための基本クラスです。
131131

132-
Entity のプロパティへのアクセスは以下の2つの方法で行えます:
133-
1. `get` メソッドを使用: `entity.get('propertyName')`
134-
2. 直接プロパティとしてアクセス: `entity.propertyName`
135-
136132
Entity を実装する際は、`_create``_reconstruct``_update` メソッドに対して、実装する Entity の型(例:`User`)をジェネリクスの型パラメータとして渡すことが重要です:
137133
```typescript
138134
// 正しい使用法
@@ -199,16 +195,10 @@ const newStatus = new UserStatus('inactive');
199195
const updatedUser = user.update({ status: newStatus });
200196

201197
// プロパティの取得
202-
// get メソッドを使用
203198
user.get('name').value; // 'John Doe'
204199
user.get('email').value; // '[email protected]'
205200
user.get('status')?.value; // undefined
206201

207-
// 直接プロパティとしてアクセス(新機能)
208-
user.name.value; // 'John Doe'
209-
user.email.value; // '[email protected]'
210-
user.status?.value; // undefined
211-
212202
// 比較
213203
user.equals(updatedUser); // true(email が同じため)
214204
user.equals(User.create(new UserName('Jane Doe'), new UserEmail('[email protected]'))); // false

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@technote-space/vo-entity-ts",
3-
"version": "0.17.1",
3+
"version": "0.18.0",
44
"description": "",
55
"keywords": [
66
"ddd",

src/entity/index.spec.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest';
22
import type { ValidationException } from '../exceptions/validation.js';
33
import { Collection } from '../valueObject/collection.js';
4+
import type { ValidationError } from '../valueObject/index.js';
45
import { Text } from '../valueObject/text.js';
56
import { Entity } from './index.js';
67

@@ -12,6 +13,20 @@ class TestText extends Text {
1213
protected override getValidationMaxLength(): number | undefined {
1314
return 5;
1415
}
16+
17+
public override getErrors(
18+
name: string,
19+
prev?: Readonly<Text>,
20+
): ValidationError[] | undefined {
21+
const results = super.getErrors(name);
22+
const errors = results || [];
23+
24+
if (prev && this.value === prev.value) {
25+
errors.push({ name, error: '前回と同じ値は使用できません' });
26+
}
27+
28+
return errors.length ? errors : undefined;
29+
}
1530
}
1631

1732
class TestTextCollection extends Collection<TestText> {}
@@ -176,11 +191,9 @@ describe('Entity', () => {
176191

177192
expect(test.get('text3')?.value).toBe('1');
178193
expect(test.get('text4')?.value).toBe('abcde');
179-
expect(test.text3?.value).toBe('1');
180-
expect(test.text4?.value).toBe('abcde');
181-
expect(test.collection?.length).toBe(2);
182-
expect(test.collection?.at(0)?.value).toBe('1');
183-
expect(test.collection?.at(1)?.value).toBe('2');
194+
expect(test.get('collection')?.length).toBe(2);
195+
expect(test.get('collection')?.at(0)?.value).toBe('1');
196+
expect(test.get('collection')?.at(1)?.value).toBe('2');
184197
});
185198

186199
it('should throw error', () => {
@@ -208,10 +221,18 @@ describe('Entity', () => {
208221
expect(error?.message).toBe('バリデーションエラーが発生しました');
209222
expect(error?.errors).toEqual({
210223
text4: ['5文字より短く入力してください'],
224+
'collection[0]': ['前回と同じ値は使用できません'],
211225
'collection[1]': ['5文字より短く入力してください'],
212226
});
213227
});
214228
});
229+
230+
describe('name', () => {
231+
it('should return constructor name', () => {
232+
const instance = TestEntity.create(new TestText(1), new TestText('1'));
233+
expect(instance.constructor.name).toBe('TestEntity');
234+
});
235+
});
215236
});
216237

217238
describe('getProps', () => {

src/entity/index.ts

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,11 @@ type EntityObject<E extends Entity> = E extends Entity<infer Props>
2626
: undefined;
2727
}
2828
: never;
29-
export type EntityInstance<E extends Entity> = E & InferProps<E>;
3029

3130
// biome-ignore lint/suspicious/noExplicitAny:
3231
export abstract class Entity<Props extends EntityPropsType = any> {
3332
protected constructor(private readonly props: Props) {
3433
Object.freeze(this.props);
35-
36-
// biome-ignore lint/correctness/noConstructorReturn:
37-
return new Proxy(this, {
38-
get(target, prop) {
39-
// Handle property access for props
40-
if (typeof prop === 'string' && prop in target.props) {
41-
return target.props[prop];
42-
}
43-
44-
return Reflect.get(target, prop);
45-
},
46-
});
4734
}
4835

4936
public get<Key extends keyof Props>(key: Key): Props[Key] {
@@ -52,41 +39,45 @@ export abstract class Entity<Props extends EntityPropsType = any> {
5239

5340
protected static _create<Instance extends Entity>(
5441
props: InferProps<Instance>,
55-
): EntityInstance<Instance> {
42+
): Instance {
5643
// biome-ignore lint/complexity/noThisInStatic:
5744
const instance = Reflect.construct(this, [props]) as Instance;
5845
instance.validate();
59-
return instance as EntityInstance<Instance>;
46+
return instance;
6047
}
6148

6249
protected static _reconstruct<Instance extends Entity>(
6350
props: InferProps<Instance>,
64-
): EntityInstance<Instance> {
51+
): Instance {
6552
// biome-ignore lint/complexity/noThisInStatic:
66-
return Reflect.construct(this, [props]) as EntityInstance<Instance>;
53+
return Reflect.construct(this, [props]) as Instance;
6754
}
6855

6956
protected static _update<Instance extends Entity>(
7057
target: Entity<InferProps<Instance>>,
7158
props: Partial<InferProps<Instance>>,
72-
): EntityInstance<Instance> {
59+
): Instance {
7360
// biome-ignore lint/complexity/noThisInStatic:
7461
const instance = Reflect.construct(this, [
7562
{ ...target.props, ...props },
7663
]) as Instance;
77-
instance.validate(target);
78-
return instance as EntityInstance<Instance>;
64+
instance.validate(target, Object.keys(props));
65+
return instance;
7966
}
8067

8168
public abstract equals(other: Entity<Props>): boolean;
8269

83-
public getErrors(prev?: Entity<Props>): ValidationErrors {
70+
public getErrors(
71+
prev?: Entity<Props>,
72+
targetKeys?: (keyof Props)[],
73+
): ValidationErrors {
8474
return Object.keys(this.props).reduce((acc, key) => {
8575
const member = this.props[key];
86-
const prevValue: typeof member | undefined = prev
87-
? // biome-ignore lint/suspicious/noExplicitAny:
88-
((prev[key as keyof Entity] as any) ?? undefined)
89-
: undefined;
76+
const prevValue: typeof member | undefined =
77+
prev && targetKeys?.includes(key)
78+
? // biome-ignore lint/suspicious/noExplicitAny:
79+
((prev.props[key as keyof Entity] as any) ?? undefined)
80+
: undefined;
9081

9182
if (member && member instanceof Collection) {
9283
const name = key.replace(/^_/, '');
@@ -95,9 +86,7 @@ export abstract class Entity<Props extends EntityPropsType = any> {
9586
v.getErrors(
9687
`${name}[${index}]`,
9788
// biome-ignore lint/suspicious/noExplicitAny:
98-
(prevValue as Collection<ValueObject<any, any>>)?.find((p) =>
99-
p.equals(v),
100-
),
89+
(prevValue as Collection<ValueObject<any, any>>)?.at(index),
10190
),
10291
)
10392
.filter((e): e is ValidationError[] => !!e)
@@ -132,9 +121,12 @@ export abstract class Entity<Props extends EntityPropsType = any> {
132121
}, {} as ValidationErrors);
133122
}
134123

135-
// biome-ignore lint/suspicious/noConfusingVoidType:
136-
private validate(prev?: Entity<Props>): void | never {
137-
const errors = this.getErrors(prev);
124+
private validate(
125+
prev?: Entity<Props>,
126+
targetKeys?: (keyof Props)[],
127+
// biome-ignore lint/suspicious/noConfusingVoidType:
128+
): void | never {
129+
const errors = this.getErrors(prev, targetKeys);
138130
if (Object.keys(errors).length) {
139131
throw new ValidationException(errors);
140132
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export { Text } from './valueObject/text.js';
1414
export { Url } from './valueObject/url.js';
1515
export { Email } from './valueObject/email.js';
1616

17-
export { Entity, type EntityInstance } from './entity/index.js';
17+
export { Entity } from './entity/index.js';
1818
export { Collection } from './valueObject/collection.js';
1919

2020
export { Exception } from './exceptions/exception.js';

0 commit comments

Comments
 (0)