@rtpaulino/entity 0.11.0 → 0.12.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/README.md +326 -0
- package/dist/lib/entity-utils.d.ts +98 -0
- package/dist/lib/entity-utils.d.ts.map +1 -1
- package/dist/lib/entity-utils.js +250 -1
- package/dist/lib/entity-utils.js.map +1 -1
- package/dist/lib/property.d.ts +109 -6
- package/dist/lib/property.d.ts.map +1 -1
- package/dist/lib/property.js +167 -14
- package/dist/lib/property.js.map +1 -1
- package/dist/lib/types.d.ts +80 -2
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1 +1,327 @@
|
|
|
1
1
|
# @rtpaulino/entity
|
|
2
|
+
|
|
3
|
+
A TypeScript entity framework with decorators for metadata-driven serialization and deserialization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type-safe decorators** for entity properties
|
|
8
|
+
- **Serialization** via `EntityUtils.toJSON()`
|
|
9
|
+
- **Deserialization** via `EntityUtils.parse()`
|
|
10
|
+
- **Helper decorators** for common types (String, Number, Boolean, Date, BigInt, Entity, Array)
|
|
11
|
+
- **Nested entities** with automatic recursive handling
|
|
12
|
+
- **Optional properties** with null/undefined support
|
|
13
|
+
- **Sparse arrays** for arrays with null/undefined elements
|
|
14
|
+
- **Passthrough mode** for generic types like `Record<string, unknown>`
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @rtpaulino/entity reflect-metadata
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Make sure to enable decorators in your `tsconfig.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"compilerOptions": {
|
|
27
|
+
"experimentalDecorators": true,
|
|
28
|
+
"emitDecoratorMetadata": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Basic Usage
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import 'reflect-metadata';
|
|
37
|
+
import {
|
|
38
|
+
Entity,
|
|
39
|
+
StringProperty,
|
|
40
|
+
NumberProperty,
|
|
41
|
+
EntityUtils,
|
|
42
|
+
} from '@rtpaulino/entity';
|
|
43
|
+
|
|
44
|
+
@Entity()
|
|
45
|
+
class User {
|
|
46
|
+
@StringProperty()
|
|
47
|
+
name!: string;
|
|
48
|
+
|
|
49
|
+
@NumberProperty()
|
|
50
|
+
age!: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Serialization
|
|
54
|
+
const user = new User();
|
|
55
|
+
user.name = 'Alice';
|
|
56
|
+
user.age = 30;
|
|
57
|
+
|
|
58
|
+
const json = EntityUtils.toJSON(user);
|
|
59
|
+
// { name: 'Alice', age: 30 }
|
|
60
|
+
|
|
61
|
+
// Deserialization
|
|
62
|
+
const parsed = EntityUtils.parse(User, json);
|
|
63
|
+
// parsed is a User instance with name='Alice' and age=30
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Property Decorators
|
|
67
|
+
|
|
68
|
+
### Basic Types
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
@Entity()
|
|
72
|
+
class Example {
|
|
73
|
+
@StringProperty()
|
|
74
|
+
text!: string;
|
|
75
|
+
|
|
76
|
+
@NumberProperty()
|
|
77
|
+
count!: number;
|
|
78
|
+
|
|
79
|
+
@BooleanProperty()
|
|
80
|
+
active!: boolean;
|
|
81
|
+
|
|
82
|
+
@DateProperty()
|
|
83
|
+
createdAt!: Date;
|
|
84
|
+
|
|
85
|
+
@BigIntProperty()
|
|
86
|
+
largeNumber!: bigint;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Optional Properties
|
|
91
|
+
|
|
92
|
+
Use `optional: true` to allow `null` or `undefined`:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
@Entity()
|
|
96
|
+
class User {
|
|
97
|
+
@StringProperty()
|
|
98
|
+
name!: string;
|
|
99
|
+
|
|
100
|
+
@StringProperty({ optional: true })
|
|
101
|
+
email?: string | null;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Nested Entities
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
@Entity()
|
|
109
|
+
class Address {
|
|
110
|
+
@StringProperty()
|
|
111
|
+
city!: string;
|
|
112
|
+
|
|
113
|
+
@StringProperty()
|
|
114
|
+
country!: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Entity()
|
|
118
|
+
class User {
|
|
119
|
+
@StringProperty()
|
|
120
|
+
name!: string;
|
|
121
|
+
|
|
122
|
+
@EntityProperty(() => Address)
|
|
123
|
+
address!: Address;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Arrays
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
@Entity()
|
|
131
|
+
class Team {
|
|
132
|
+
@ArrayProperty(() => String)
|
|
133
|
+
members!: string[];
|
|
134
|
+
|
|
135
|
+
@ArrayProperty(() => Number)
|
|
136
|
+
scores!: number[];
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Sparse Arrays
|
|
141
|
+
|
|
142
|
+
Use `sparse: true` to allow `null` or `undefined` elements in arrays:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
@Entity()
|
|
146
|
+
class Data {
|
|
147
|
+
@ArrayProperty(() => String, { sparse: true })
|
|
148
|
+
values!: (string | null)[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const data = new Data();
|
|
152
|
+
data.values = ['a', null, 'b', undefined, 'c'];
|
|
153
|
+
|
|
154
|
+
const json = EntityUtils.toJSON(data);
|
|
155
|
+
// { values: ['a', null, 'b', null, 'c'] }
|
|
156
|
+
|
|
157
|
+
const parsed = EntityUtils.parse(Data, json);
|
|
158
|
+
// parsed.values is ['a', null, 'b', null, 'c']
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Passthrough for Generic Types
|
|
162
|
+
|
|
163
|
+
By default, the library throws errors for unknown types to prevent accidental misuse. Use `@PassthroughProperty()` decorator or `passthrough: true` option to explicitly handle generic types like `Record<string, unknown>`, `any`, or custom objects:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
@Entity()
|
|
167
|
+
class Config {
|
|
168
|
+
@StringProperty()
|
|
169
|
+
name!: string;
|
|
170
|
+
|
|
171
|
+
@PassthroughProperty()
|
|
172
|
+
metadata!: Record<string, unknown>;
|
|
173
|
+
|
|
174
|
+
@PassthroughProperty()
|
|
175
|
+
customData!: any;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const config = new Config();
|
|
179
|
+
config.name = 'MyConfig';
|
|
180
|
+
config.metadata = {
|
|
181
|
+
version: '1.0.0',
|
|
182
|
+
tags: ['production', 'stable'],
|
|
183
|
+
nested: { deeply: { nested: { value: 42 } } },
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Serialization - data passes through as-is
|
|
187
|
+
const json = EntityUtils.toJSON(config);
|
|
188
|
+
|
|
189
|
+
// Deserialization - data passes through as-is
|
|
190
|
+
const parsed = EntityUtils.parse(Config, json);
|
|
191
|
+
// parsed.metadata is exactly what was in the JSON
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Important:** Passthrough cannot be combined with `array`, `optional`, or `sparse` options. Use `@PassthroughProperty()` for the cleanest API.
|
|
195
|
+
|
|
196
|
+
**Without passthrough, unknown types throw errors:**
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
@Entity()
|
|
200
|
+
class BadExample {
|
|
201
|
+
@Property({ type: () => Symbol }) // Symbol is not supported
|
|
202
|
+
value!: symbol;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// This will throw:
|
|
206
|
+
// "Property 'value' has unknown type constructor. Supported types are: String, Number, Boolean, Date, BigInt, and @Entity() classes."
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Advanced Patterns
|
|
210
|
+
|
|
211
|
+
### Arrays of Entities
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
@Entity()
|
|
215
|
+
class Comment {
|
|
216
|
+
@StringProperty()
|
|
217
|
+
text!: string;
|
|
218
|
+
|
|
219
|
+
@DateProperty()
|
|
220
|
+
createdAt!: Date;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Entity()
|
|
224
|
+
class Post {
|
|
225
|
+
@StringProperty()
|
|
226
|
+
title!: string;
|
|
227
|
+
|
|
228
|
+
@ArrayProperty(() => Comment)
|
|
229
|
+
comments!: Comment[];
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Complex Nested Structures
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
@Entity()
|
|
237
|
+
class Organization {
|
|
238
|
+
@StringProperty()
|
|
239
|
+
name!: string;
|
|
240
|
+
|
|
241
|
+
@ArrayProperty(() => User)
|
|
242
|
+
admins!: User[];
|
|
243
|
+
|
|
244
|
+
@Property({ passthrough: true })
|
|
245
|
+
settings!: Record<string, unknown>;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## API Reference
|
|
250
|
+
|
|
251
|
+
### `EntityUtils.toJSON(entity)`
|
|
252
|
+
|
|
253
|
+
Serializes an entity to a plain JavaScript object.
|
|
254
|
+
|
|
255
|
+
- Throws error for properties with unknown types (unless `passthrough: true`)
|
|
256
|
+
- Handles nested entities recursively
|
|
257
|
+
- Converts Date to ISO string, BigInt to string
|
|
258
|
+
- Preserves null/undefined for optional properties
|
|
259
|
+
|
|
260
|
+
### `EntityUtils.parse<T>(entityClass, plainObject)`
|
|
261
|
+
|
|
262
|
+
Deserializes a plain object to an entity instance.
|
|
263
|
+
|
|
264
|
+
- Validates all required properties are present
|
|
265
|
+
- Validates property types match metadata
|
|
266
|
+
- Throws error for unknown types (unless `passthrough: true`)
|
|
267
|
+
- Handles nested entities recursively
|
|
268
|
+
- Converts ISO strings back to Date, strings back to BigInt
|
|
269
|
+
|
|
270
|
+
### Property Options
|
|
271
|
+
|
|
272
|
+
The `@Property()` decorator requires configuration options. Available options are:
|
|
273
|
+
|
|
274
|
+
- `type: () => any` - **Required.** Type constructor for the property
|
|
275
|
+
- `array?: boolean` - Whether the property is an array
|
|
276
|
+
- `optional?: boolean` - Allow null/undefined values
|
|
277
|
+
- `sparse?: boolean` - Allow null/undefined in array elements (requires `array: true`)
|
|
278
|
+
- `passthrough?: boolean` - Bypass type validation for generic types (cannot be combined with array, optional, or sparse)
|
|
279
|
+
- `equals?: (a, b) => boolean` - Custom equality function for this property
|
|
280
|
+
|
|
281
|
+
**Note:** Use helper decorators like `@StringProperty()`, `@PassthroughProperty()`, etc. for cleaner, more readable code. These helpers automatically provide the required `type` parameter.
|
|
282
|
+
|
|
283
|
+
## Error Handling
|
|
284
|
+
|
|
285
|
+
The library validates configuration at decorator time and throws descriptive errors for common mistakes:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Missing required property
|
|
289
|
+
EntityUtils.parse(User, {});
|
|
290
|
+
// Error: Property 'name' is required but missing from input
|
|
291
|
+
|
|
292
|
+
// Null/undefined on required property
|
|
293
|
+
EntityUtils.parse(User, { name: null, age: 30 });
|
|
294
|
+
// Error: Property 'name' cannot be null or undefined
|
|
295
|
+
|
|
296
|
+
// Sparse without array (fails at decorator time)
|
|
297
|
+
@Property({ type: () => String, sparse: true }) // Missing array: true
|
|
298
|
+
values!: string[];
|
|
299
|
+
// Error: Property 'values' has sparse: true but array is not true
|
|
300
|
+
|
|
301
|
+
// Passthrough combined with other options (fails at decorator time)
|
|
302
|
+
@Property({ type: () => Object, passthrough: true, array: true })
|
|
303
|
+
data!: any;
|
|
304
|
+
// Error: Property 'data' has passthrough: true and array: true. Passthrough cannot be combined with array
|
|
305
|
+
|
|
306
|
+
// Unknown type without passthrough
|
|
307
|
+
@Property({ type: () => WeakMap })
|
|
308
|
+
map!: WeakMap<any, any>;
|
|
309
|
+
// Error: Property 'map' has unknown type constructor
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## TypeScript Integration
|
|
313
|
+
|
|
314
|
+
All methods are fully typed:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const user = EntityUtils.parse(User, json); // user is typed as User
|
|
318
|
+
const json = EntityUtils.toJSON(user); // json is Record<string, unknown>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
MIT
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
```
|
|
@@ -31,5 +31,103 @@ export declare class EntityUtils {
|
|
|
31
31
|
newValue: unknown;
|
|
32
32
|
}[];
|
|
33
33
|
static changes<T extends object>(oldEntity: T, newEntity: T): Partial<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Serializes an entity to a plain object, converting only properties decorated with @Property()
|
|
36
|
+
*
|
|
37
|
+
* @param entity - The entity instance to serialize
|
|
38
|
+
* @returns A plain object containing only the serialized decorated properties
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* Serialization rules:
|
|
42
|
+
* - Only properties decorated with @Property() are included
|
|
43
|
+
* - If a property has a custom toJSON() method, it will be used
|
|
44
|
+
* - Nested entities are recursively serialized using EntityUtils.toJSON()
|
|
45
|
+
* - Arrays are mapped with toJSON() applied to each element
|
|
46
|
+
* - Date objects are serialized to ISO strings
|
|
47
|
+
* - bigint values are serialized to strings
|
|
48
|
+
* - undefined values are excluded from the output
|
|
49
|
+
* - null values are included in the output
|
|
50
|
+
* - Circular references are not supported (will cause stack overflow)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* @Entity()
|
|
55
|
+
* class Address {
|
|
56
|
+
* @Property() street: string;
|
|
57
|
+
* @Property() city: string;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @Entity()
|
|
61
|
+
* class User {
|
|
62
|
+
* @Property() name: string;
|
|
63
|
+
* @Property() address: Address;
|
|
64
|
+
* @Property() createdAt: Date;
|
|
65
|
+
* undecorated: string; // Will not be serialized
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* const user = new User();
|
|
69
|
+
* user.name = 'John';
|
|
70
|
+
* user.address = new Address();
|
|
71
|
+
* user.address.street = '123 Main St';
|
|
72
|
+
* user.address.city = 'Boston';
|
|
73
|
+
* user.createdAt = new Date('2024-01-01');
|
|
74
|
+
* user.undecorated = 'ignored';
|
|
75
|
+
*
|
|
76
|
+
* const json = EntityUtils.toJSON(user);
|
|
77
|
+
* // {
|
|
78
|
+
* // name: 'John',
|
|
79
|
+
* // address: { street: '123 Main St', city: 'Boston' },
|
|
80
|
+
* // createdAt: '2024-01-01T00:00:00.000Z'
|
|
81
|
+
* // }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
static toJSON<T extends object>(entity: T): Record<string, unknown>;
|
|
85
|
+
/**
|
|
86
|
+
* Serializes a single value according to the toJSON rules
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
private static serializeValue;
|
|
90
|
+
/**
|
|
91
|
+
* Deserializes a plain object to an entity instance
|
|
92
|
+
*
|
|
93
|
+
* @param entityClass - The entity class constructor
|
|
94
|
+
* @param plainObject - The plain object to deserialize
|
|
95
|
+
* @returns A new instance of the entity with deserialized values
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Deserialization rules:
|
|
99
|
+
* - All @Property() decorators must include type metadata for parse() to work
|
|
100
|
+
* - Properties without type metadata will throw an error
|
|
101
|
+
* - Required properties (optional !== true) must be present and not null/undefined
|
|
102
|
+
* - Optional properties (optional === true) can be undefined or null
|
|
103
|
+
* - Arrays are supported with the array: true option
|
|
104
|
+
* - Nested entities are recursively deserialized
|
|
105
|
+
* - Type conversion is strict (no coercion)
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* @Entity()
|
|
110
|
+
* class User {
|
|
111
|
+
* @Property({ type: () => String }) name!: string;
|
|
112
|
+
* @Property({ type: () => Number }) age!: number;
|
|
113
|
+
* @Property({ type: () => String, optional: true }) email?: string;
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* const json = { name: 'John', age: 30 };
|
|
117
|
+
* const user = EntityUtils.parse(User, json);
|
|
118
|
+
* // user is a properly typed User instance
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
static parse<T extends object>(entityClass: new () => T, plainObject: Record<string, unknown>): T;
|
|
122
|
+
/**
|
|
123
|
+
* Deserializes a single value according to the type metadata
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
private static deserializeValue;
|
|
127
|
+
/**
|
|
128
|
+
* Deserializes a single non-array value
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
private static deserializeSingleValue;
|
|
34
132
|
}
|
|
35
133
|
//# sourceMappingURL=entity-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-utils.d.ts","sourceRoot":"","sources":["../../src/lib/entity-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entity-utils.d.ts","sourceRoot":"","sources":["../../src/lib/entity-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,eAAe,EAChB,MAAM,YAAY,CAAC;AAGpB,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,MAAM;IAmB5C,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAQhD,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAoChD,MAAM,CAAC,kBAAkB,CACvB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,eAAe,GAAG,SAAS;IA8B9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO;IA2B9C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,EAC1B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,CAAC,GACX;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE;IAoC/D,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAaxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAmBnE;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAsD7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC3B,WAAW,EAAE,UAAU,CAAC,EACxB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,CAAC;IA6CJ;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IA4C/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;CAoFtC"}
|
package/dist/lib/entity-utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ENTITY_METADATA_KEY, PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ENTITY_METADATA_KEY, PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
2
2
|
import { isEqualWith } from 'lodash-es';
|
|
3
3
|
export class EntityUtils {
|
|
4
4
|
/**
|
|
@@ -144,6 +144,255 @@ export class EntityUtils {
|
|
|
144
144
|
return acc;
|
|
145
145
|
}, {});
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Serializes an entity to a plain object, converting only properties decorated with @Property()
|
|
149
|
+
*
|
|
150
|
+
* @param entity - The entity instance to serialize
|
|
151
|
+
* @returns A plain object containing only the serialized decorated properties
|
|
152
|
+
*
|
|
153
|
+
* @remarks
|
|
154
|
+
* Serialization rules:
|
|
155
|
+
* - Only properties decorated with @Property() are included
|
|
156
|
+
* - If a property has a custom toJSON() method, it will be used
|
|
157
|
+
* - Nested entities are recursively serialized using EntityUtils.toJSON()
|
|
158
|
+
* - Arrays are mapped with toJSON() applied to each element
|
|
159
|
+
* - Date objects are serialized to ISO strings
|
|
160
|
+
* - bigint values are serialized to strings
|
|
161
|
+
* - undefined values are excluded from the output
|
|
162
|
+
* - null values are included in the output
|
|
163
|
+
* - Circular references are not supported (will cause stack overflow)
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* @Entity()
|
|
168
|
+
* class Address {
|
|
169
|
+
* @Property() street: string;
|
|
170
|
+
* @Property() city: string;
|
|
171
|
+
* }
|
|
172
|
+
*
|
|
173
|
+
* @Entity()
|
|
174
|
+
* class User {
|
|
175
|
+
* @Property() name: string;
|
|
176
|
+
* @Property() address: Address;
|
|
177
|
+
* @Property() createdAt: Date;
|
|
178
|
+
* undecorated: string; // Will not be serialized
|
|
179
|
+
* }
|
|
180
|
+
*
|
|
181
|
+
* const user = new User();
|
|
182
|
+
* user.name = 'John';
|
|
183
|
+
* user.address = new Address();
|
|
184
|
+
* user.address.street = '123 Main St';
|
|
185
|
+
* user.address.city = 'Boston';
|
|
186
|
+
* user.createdAt = new Date('2024-01-01');
|
|
187
|
+
* user.undecorated = 'ignored';
|
|
188
|
+
*
|
|
189
|
+
* const json = EntityUtils.toJSON(user);
|
|
190
|
+
* // {
|
|
191
|
+
* // name: 'John',
|
|
192
|
+
* // address: { street: '123 Main St', city: 'Boston' },
|
|
193
|
+
* // createdAt: '2024-01-01T00:00:00.000Z'
|
|
194
|
+
* // }
|
|
195
|
+
* ```
|
|
196
|
+
*/ static toJSON(entity) {
|
|
197
|
+
const result = {};
|
|
198
|
+
const keys = this.getPropertyKeys(entity);
|
|
199
|
+
for (const key of keys){
|
|
200
|
+
const value = entity[key];
|
|
201
|
+
// Skip undefined values
|
|
202
|
+
if (value === undefined) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const options = this.getPropertyOptions(entity, key);
|
|
206
|
+
result[key] = this.serializeValue(value, options);
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Serializes a single value according to the toJSON rules
|
|
212
|
+
* @private
|
|
213
|
+
*/ static serializeValue(value, options) {
|
|
214
|
+
if (value === null) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (value === undefined) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const passthrough = options?.passthrough === true;
|
|
221
|
+
if (passthrough) {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(value)) {
|
|
225
|
+
if (options?.serialize) {
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
227
|
+
return value.map((item)=>options.serialize(item));
|
|
228
|
+
}
|
|
229
|
+
return value.map((item)=>this.serializeValue(item));
|
|
230
|
+
}
|
|
231
|
+
if (options?.serialize) {
|
|
232
|
+
return options.serialize(value);
|
|
233
|
+
}
|
|
234
|
+
if (value instanceof Date) {
|
|
235
|
+
return value.toISOString();
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === 'bigint') {
|
|
238
|
+
return value.toString();
|
|
239
|
+
}
|
|
240
|
+
if (this.isEntity(value)) {
|
|
241
|
+
return this.toJSON(value);
|
|
242
|
+
}
|
|
243
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Cannot serialize value of type '${typeof value}'. Use passthrough: true in @Property() to explicitly allow serialization of unknown types.`);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Deserializes a plain object to an entity instance
|
|
250
|
+
*
|
|
251
|
+
* @param entityClass - The entity class constructor
|
|
252
|
+
* @param plainObject - The plain object to deserialize
|
|
253
|
+
* @returns A new instance of the entity with deserialized values
|
|
254
|
+
*
|
|
255
|
+
* @remarks
|
|
256
|
+
* Deserialization rules:
|
|
257
|
+
* - All @Property() decorators must include type metadata for parse() to work
|
|
258
|
+
* - Properties without type metadata will throw an error
|
|
259
|
+
* - Required properties (optional !== true) must be present and not null/undefined
|
|
260
|
+
* - Optional properties (optional === true) can be undefined or null
|
|
261
|
+
* - Arrays are supported with the array: true option
|
|
262
|
+
* - Nested entities are recursively deserialized
|
|
263
|
+
* - Type conversion is strict (no coercion)
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* @Entity()
|
|
268
|
+
* class User {
|
|
269
|
+
* @Property({ type: () => String }) name!: string;
|
|
270
|
+
* @Property({ type: () => Number }) age!: number;
|
|
271
|
+
* @Property({ type: () => String, optional: true }) email?: string;
|
|
272
|
+
* }
|
|
273
|
+
*
|
|
274
|
+
* const json = { name: 'John', age: 30 };
|
|
275
|
+
* const user = EntityUtils.parse(User, json);
|
|
276
|
+
* // user is a properly typed User instance
|
|
277
|
+
* ```
|
|
278
|
+
*/ static parse(entityClass, plainObject) {
|
|
279
|
+
const instance = new entityClass();
|
|
280
|
+
const keys = this.getPropertyKeys(instance);
|
|
281
|
+
for (const key of keys){
|
|
282
|
+
const options = this.getPropertyOptions(instance, key);
|
|
283
|
+
if (!options) {
|
|
284
|
+
throw new Error(`Property '${key}' has no metadata. This should not happen if @Property() was used correctly.`);
|
|
285
|
+
}
|
|
286
|
+
if (options.passthrough === true) {
|
|
287
|
+
const value = plainObject[key];
|
|
288
|
+
instance[key] = value;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const value = plainObject[key];
|
|
292
|
+
const isOptional = options.optional === true;
|
|
293
|
+
if (!(key in plainObject)) {
|
|
294
|
+
if (!isOptional) {
|
|
295
|
+
throw new Error(`Property '${key}' is required but missing from input`);
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (value === null || value === undefined) {
|
|
300
|
+
if (!isOptional) {
|
|
301
|
+
throw new Error(`Property '${key}' cannot be null or undefined`);
|
|
302
|
+
}
|
|
303
|
+
instance[key] = value;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
instance[key] = this.deserializeValue(value, options, key);
|
|
307
|
+
}
|
|
308
|
+
return instance;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Deserializes a single value according to the type metadata
|
|
312
|
+
* @private
|
|
313
|
+
*/ static deserializeValue(value, options, propertyKey) {
|
|
314
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
315
|
+
const typeConstructor = options.type();
|
|
316
|
+
const isArray = options.array === true;
|
|
317
|
+
const isSparse = options.sparse === true;
|
|
318
|
+
if (isArray) {
|
|
319
|
+
if (!Array.isArray(value)) {
|
|
320
|
+
throw new Error(`Property '${propertyKey}' expects an array but received ${typeof value}`);
|
|
321
|
+
}
|
|
322
|
+
return value.map((item, index)=>{
|
|
323
|
+
if (item === null || item === undefined) {
|
|
324
|
+
if (!isSparse) {
|
|
325
|
+
throw new Error(`Property '${propertyKey}[${index}]' cannot be null or undefined. Use sparse: true to allow null/undefined elements in arrays.`);
|
|
326
|
+
}
|
|
327
|
+
return item;
|
|
328
|
+
}
|
|
329
|
+
if (options.deserialize) {
|
|
330
|
+
return options.deserialize(item);
|
|
331
|
+
}
|
|
332
|
+
return this.deserializeSingleValue(item, typeConstructor, `${propertyKey}[${index}]`);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (options.deserialize) {
|
|
336
|
+
return options.deserialize(value);
|
|
337
|
+
}
|
|
338
|
+
return this.deserializeSingleValue(value, typeConstructor, propertyKey);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Deserializes a single non-array value
|
|
342
|
+
* @private
|
|
343
|
+
*/ static deserializeSingleValue(value, typeConstructor, propertyKey) {
|
|
344
|
+
if (typeConstructor === String) {
|
|
345
|
+
if (typeof value !== 'string') {
|
|
346
|
+
throw new Error(`Property '${propertyKey}' expects a string but received ${typeof value}`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
if (typeConstructor === Number) {
|
|
351
|
+
if (typeof value !== 'number') {
|
|
352
|
+
throw new Error(`Property '${propertyKey}' expects a number but received ${typeof value}`);
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
if (typeConstructor === Boolean) {
|
|
357
|
+
if (typeof value !== 'boolean') {
|
|
358
|
+
throw new Error(`Property '${propertyKey}' expects a boolean but received ${typeof value}`);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
if (typeConstructor === BigInt) {
|
|
363
|
+
if (typeof value === 'bigint') {
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
if (typeof value === 'string') {
|
|
367
|
+
try {
|
|
368
|
+
return BigInt(value);
|
|
369
|
+
} catch {
|
|
370
|
+
throw new Error(`Property '${propertyKey}' cannot parse '${value}' as BigInt`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`Property '${propertyKey}' expects a bigint or string but received ${typeof value}`);
|
|
374
|
+
}
|
|
375
|
+
if (typeConstructor === Date) {
|
|
376
|
+
if (value instanceof Date) {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
if (typeof value === 'string') {
|
|
380
|
+
const date = new Date(value);
|
|
381
|
+
if (isNaN(date.getTime())) {
|
|
382
|
+
throw new Error(`Property '${propertyKey}' cannot parse '${value}' as Date`);
|
|
383
|
+
}
|
|
384
|
+
return date;
|
|
385
|
+
}
|
|
386
|
+
throw new Error(`Property '${propertyKey}' expects a Date or ISO string but received ${typeof value}`);
|
|
387
|
+
}
|
|
388
|
+
if (this.isEntity(typeConstructor)) {
|
|
389
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
390
|
+
throw new Error(`Property '${propertyKey}' expects an object but received ${typeof value}`);
|
|
391
|
+
}
|
|
392
|
+
return this.parse(typeConstructor, value);
|
|
393
|
+
}
|
|
394
|
+
throw new Error(`Property '${propertyKey}' has unknown type constructor. Supported types are: String, Number, Boolean, Date, BigInt, and @Entity() classes. Use passthrough: true to explicitly allow unknown types.`);
|
|
395
|
+
}
|
|
147
396
|
}
|
|
148
397
|
|
|
149
398
|
//# sourceMappingURL=entity-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/entity-utils.ts"],"sourcesContent":["import {\n ENTITY_METADATA_KEY,\n PROPERTY_METADATA_KEY,\n PROPERTY_OPTIONS_METADATA_KEY,\n PropertyOptions,\n} from './types.js';\nimport { isEqualWith } from 'lodash-es';\n\nexport class EntityUtils {\n /**\n * Checks if a given object is an instance of a class decorated with @Entity()\n * or if the provided value is an entity class itself\n *\n * @param obj - The object or class to check\n * @returns true if the object is an entity instance or entity class, false otherwise\n *\n * @example\n * ```typescript\n * @Entity()\n * class User {\n * name: string;\n * }\n *\n * const user = new User();\n * console.log(EntityUtils.isEntity(user)); // true\n * console.log(EntityUtils.isEntity(User)); // true\n * console.log(EntityUtils.isEntity({})); // false\n * ```\n */\n static isEntity(obj: unknown): obj is object {\n if (obj == null) {\n return false;\n }\n\n // Check if obj is a constructor function (class)\n if (typeof obj === 'function') {\n return Reflect.hasMetadata(ENTITY_METADATA_KEY, obj);\n }\n\n // Check if obj is an object instance\n if (typeof obj !== 'object' || Array.isArray(obj)) {\n return false;\n }\n\n const constructor = Object.getPrototypeOf(obj).constructor;\n return Reflect.hasMetadata(ENTITY_METADATA_KEY, constructor);\n }\n\n static sameEntity(a: object, b: object): boolean {\n if (!this.isEntity(a) || !this.isEntity(b)) {\n return false;\n }\n\n return Object.getPrototypeOf(a) === Object.getPrototypeOf(b);\n }\n\n static getPropertyKeys(target: object): string[] {\n // Determine if we're dealing with a prototype or an instance\n let currentProto: any;\n\n // Check if target is a prototype by checking if it has a constructor property\n // and if target === target.constructor.prototype\n if (target.constructor && target === target.constructor.prototype) {\n // target is already a prototype\n currentProto = target;\n } else {\n // target is an instance, get its prototype\n currentProto = Object.getPrototypeOf(target);\n }\n\n const keys: string[] = [];\n const seen = new Set<string>();\n\n // Walk the prototype chain to collect all inherited properties\n while (currentProto && currentProto !== Object.prototype) {\n // Use getOwnMetadata to only get metadata directly on this prototype\n const protoKeys: string[] =\n Reflect.getOwnMetadata(PROPERTY_METADATA_KEY, currentProto) || [];\n\n for (const key of protoKeys) {\n if (!seen.has(key)) {\n seen.add(key);\n keys.push(key);\n }\n }\n\n currentProto = Object.getPrototypeOf(currentProto);\n }\n\n return keys;\n }\n\n static getPropertyOptions(\n target: object,\n propertyKey: string,\n ): PropertyOptions | undefined {\n // Determine if we're dealing with a prototype or an instance\n let currentProto: any;\n\n // Check if target is a prototype by checking if it has a constructor property\n // and if target === target.constructor.prototype\n if (target.constructor && target === target.constructor.prototype) {\n // target is already a prototype\n currentProto = target;\n } else {\n // target is an instance, get its prototype\n currentProto = Object.getPrototypeOf(target);\n }\n\n // Walk the prototype chain to find the property options\n while (currentProto && currentProto !== Object.prototype) {\n const protoOptions: Record<string, PropertyOptions> =\n Reflect.getOwnMetadata(PROPERTY_OPTIONS_METADATA_KEY, currentProto) ||\n {};\n\n if (protoOptions[propertyKey]) {\n return protoOptions[propertyKey];\n }\n\n currentProto = Object.getPrototypeOf(currentProto);\n }\n\n return undefined;\n }\n\n static equals(a: unknown, b: unknown): boolean {\n return isEqualWith(a, b, (val1, val2) => {\n if (this.isEntity(val1)) {\n if (!this.sameEntity(val1, val2)) {\n return false;\n }\n\n const diff = this.diff(val1, val2);\n\n return diff.length === 0;\n } else if (\n val1 != null &&\n val2 != null &&\n typeof val1 === 'object' &&\n !Array.isArray(val1) &&\n typeof val2 === 'object' &&\n !Array.isArray(val2) &&\n 'equals' in val1 &&\n typeof val1.equals === 'function'\n ) {\n return val1.equals(val2);\n }\n\n return undefined;\n });\n }\n\n static diff<T extends object>(\n oldEntity: T,\n newEntity: T,\n ): { property: string; oldValue: unknown; newValue: unknown }[] {\n if (!this.sameEntity(oldEntity, newEntity)) {\n throw new Error('Entities must be of the same type to compute diff');\n }\n\n const diffs: { property: string; oldValue: unknown; newValue: unknown }[] =\n [];\n\n const keys = this.getPropertyKeys(oldEntity);\n\n for (const key of keys) {\n const oldValue = (oldEntity as any)[key];\n const newValue = (newEntity as any)[key];\n\n // Check if there's a custom equals function for this property\n const propertyOptions = this.getPropertyOptions(oldEntity, key);\n\n let areEqual: boolean;\n if (oldValue == null && newValue == null) {\n areEqual = oldValue === newValue;\n } else if (oldValue == null || newValue == null) {\n areEqual = false;\n } else {\n areEqual = propertyOptions?.equals\n ? propertyOptions.equals(oldValue, newValue)\n : this.equals(oldValue, newValue);\n }\n\n if (!areEqual) {\n diffs.push({ property: key, oldValue, newValue });\n }\n }\n\n return diffs;\n }\n\n static changes<T extends object>(oldEntity: T, newEntity: T): Partial<T> {\n if (!this.sameEntity(oldEntity, newEntity)) {\n throw new Error('Entities must be of the same type to compute changes');\n }\n\n const diff = this.diff(oldEntity, newEntity);\n\n return diff.reduce((acc, { property, newValue }) => {\n (acc as any)[property] = newValue;\n return acc;\n }, {} as Partial<T>);\n }\n}\n"],"names":["ENTITY_METADATA_KEY","PROPERTY_METADATA_KEY","PROPERTY_OPTIONS_METADATA_KEY","isEqualWith","EntityUtils","isEntity","obj","Reflect","hasMetadata","Array","isArray","constructor","Object","getPrototypeOf","sameEntity","a","b","getPropertyKeys","target","currentProto","prototype","keys","seen","Set","protoKeys","getOwnMetadata","key","has","add","push","getPropertyOptions","propertyKey","protoOptions","undefined","equals","val1","val2","diff","length","oldEntity","newEntity","Error","diffs","oldValue","newValue","propertyOptions","areEqual","property","changes","reduce","acc"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/entity-utils.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n ENTITY_METADATA_KEY,\n PROPERTY_METADATA_KEY,\n PROPERTY_OPTIONS_METADATA_KEY,\n PropertyOptions,\n} from './types.js';\nimport { isEqualWith } from 'lodash-es';\n\nexport class EntityUtils {\n /**\n * Checks if a given object is an instance of a class decorated with @Entity()\n * or if the provided value is an entity class itself\n *\n * @param obj - The object or class to check\n * @returns true if the object is an entity instance or entity class, false otherwise\n *\n * @example\n * ```typescript\n * @Entity()\n * class User {\n * name: string;\n * }\n *\n * const user = new User();\n * console.log(EntityUtils.isEntity(user)); // true\n * console.log(EntityUtils.isEntity(User)); // true\n * console.log(EntityUtils.isEntity({})); // false\n * ```\n */\n static isEntity(obj: unknown): obj is object {\n if (obj == null) {\n return false;\n }\n\n // Check if obj is a constructor function (class)\n if (typeof obj === 'function') {\n return Reflect.hasMetadata(ENTITY_METADATA_KEY, obj);\n }\n\n // Check if obj is an object instance\n if (typeof obj !== 'object' || Array.isArray(obj)) {\n return false;\n }\n\n const constructor = Object.getPrototypeOf(obj).constructor;\n return Reflect.hasMetadata(ENTITY_METADATA_KEY, constructor);\n }\n\n static sameEntity(a: object, b: object): boolean {\n if (!this.isEntity(a) || !this.isEntity(b)) {\n return false;\n }\n\n return Object.getPrototypeOf(a) === Object.getPrototypeOf(b);\n }\n\n static getPropertyKeys(target: object): string[] {\n // Determine if we're dealing with a prototype or an instance\n let currentProto: any;\n\n // Check if target is a prototype by checking if it has a constructor property\n // and if target === target.constructor.prototype\n if (target.constructor && target === target.constructor.prototype) {\n // target is already a prototype\n currentProto = target;\n } else {\n // target is an instance, get its prototype\n currentProto = Object.getPrototypeOf(target);\n }\n\n const keys: string[] = [];\n const seen = new Set<string>();\n\n // Walk the prototype chain to collect all inherited properties\n while (currentProto && currentProto !== Object.prototype) {\n // Use getOwnMetadata to only get metadata directly on this prototype\n const protoKeys: string[] =\n Reflect.getOwnMetadata(PROPERTY_METADATA_KEY, currentProto) || [];\n\n for (const key of protoKeys) {\n if (!seen.has(key)) {\n seen.add(key);\n keys.push(key);\n }\n }\n\n currentProto = Object.getPrototypeOf(currentProto);\n }\n\n return keys;\n }\n\n static getPropertyOptions(\n target: object,\n propertyKey: string,\n ): PropertyOptions | undefined {\n // Determine if we're dealing with a prototype or an instance\n let currentProto: any;\n\n // Check if target is a prototype by checking if it has a constructor property\n // and if target === target.constructor.prototype\n if (target.constructor && target === target.constructor.prototype) {\n // target is already a prototype\n currentProto = target;\n } else {\n // target is an instance, get its prototype\n currentProto = Object.getPrototypeOf(target);\n }\n\n // Walk the prototype chain to find the property options\n while (currentProto && currentProto !== Object.prototype) {\n const protoOptions: Record<string, PropertyOptions> =\n Reflect.getOwnMetadata(PROPERTY_OPTIONS_METADATA_KEY, currentProto) ||\n {};\n\n if (protoOptions[propertyKey]) {\n return protoOptions[propertyKey];\n }\n\n currentProto = Object.getPrototypeOf(currentProto);\n }\n\n return undefined;\n }\n\n static equals(a: unknown, b: unknown): boolean {\n return isEqualWith(a, b, (val1, val2) => {\n if (this.isEntity(val1)) {\n if (!this.sameEntity(val1, val2)) {\n return false;\n }\n\n const diff = this.diff(val1, val2);\n\n return diff.length === 0;\n } else if (\n val1 != null &&\n val2 != null &&\n typeof val1 === 'object' &&\n !Array.isArray(val1) &&\n typeof val2 === 'object' &&\n !Array.isArray(val2) &&\n 'equals' in val1 &&\n typeof val1.equals === 'function'\n ) {\n return val1.equals(val2);\n }\n\n return undefined;\n });\n }\n\n static diff<T extends object>(\n oldEntity: T,\n newEntity: T,\n ): { property: string; oldValue: unknown; newValue: unknown }[] {\n if (!this.sameEntity(oldEntity, newEntity)) {\n throw new Error('Entities must be of the same type to compute diff');\n }\n\n const diffs: { property: string; oldValue: unknown; newValue: unknown }[] =\n [];\n\n const keys = this.getPropertyKeys(oldEntity);\n\n for (const key of keys) {\n const oldValue = (oldEntity as any)[key];\n const newValue = (newEntity as any)[key];\n\n // Check if there's a custom equals function for this property\n const propertyOptions = this.getPropertyOptions(oldEntity, key);\n\n let areEqual: boolean;\n if (oldValue == null && newValue == null) {\n areEqual = oldValue === newValue;\n } else if (oldValue == null || newValue == null) {\n areEqual = false;\n } else {\n areEqual = propertyOptions?.equals\n ? propertyOptions.equals(oldValue, newValue)\n : this.equals(oldValue, newValue);\n }\n\n if (!areEqual) {\n diffs.push({ property: key, oldValue, newValue });\n }\n }\n\n return diffs;\n }\n\n static changes<T extends object>(oldEntity: T, newEntity: T): Partial<T> {\n if (!this.sameEntity(oldEntity, newEntity)) {\n throw new Error('Entities must be of the same type to compute changes');\n }\n\n const diff = this.diff(oldEntity, newEntity);\n\n return diff.reduce((acc, { property, newValue }) => {\n (acc as any)[property] = newValue;\n return acc;\n }, {} as Partial<T>);\n }\n\n /**\n * Serializes an entity to a plain object, converting only properties decorated with @Property()\n *\n * @param entity - The entity instance to serialize\n * @returns A plain object containing only the serialized decorated properties\n *\n * @remarks\n * Serialization rules:\n * - Only properties decorated with @Property() are included\n * - If a property has a custom toJSON() method, it will be used\n * - Nested entities are recursively serialized using EntityUtils.toJSON()\n * - Arrays are mapped with toJSON() applied to each element\n * - Date objects are serialized to ISO strings\n * - bigint values are serialized to strings\n * - undefined values are excluded from the output\n * - null values are included in the output\n * - Circular references are not supported (will cause stack overflow)\n *\n * @example\n * ```typescript\n * @Entity()\n * class Address {\n * @Property() street: string;\n * @Property() city: string;\n * }\n *\n * @Entity()\n * class User {\n * @Property() name: string;\n * @Property() address: Address;\n * @Property() createdAt: Date;\n * undecorated: string; // Will not be serialized\n * }\n *\n * const user = new User();\n * user.name = 'John';\n * user.address = new Address();\n * user.address.street = '123 Main St';\n * user.address.city = 'Boston';\n * user.createdAt = new Date('2024-01-01');\n * user.undecorated = 'ignored';\n *\n * const json = EntityUtils.toJSON(user);\n * // {\n * // name: 'John',\n * // address: { street: '123 Main St', city: 'Boston' },\n * // createdAt: '2024-01-01T00:00:00.000Z'\n * // }\n * ```\n */\n static toJSON<T extends object>(entity: T): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n const keys = this.getPropertyKeys(entity);\n\n for (const key of keys) {\n const value = (entity as any)[key];\n\n // Skip undefined values\n if (value === undefined) {\n continue;\n }\n\n const options = this.getPropertyOptions(entity, key);\n result[key] = this.serializeValue(value, options);\n }\n\n return result;\n }\n\n /**\n * Serializes a single value according to the toJSON rules\n * @private\n */\n private static serializeValue(\n value: unknown,\n options?: PropertyOptions,\n ): unknown {\n if (value === null) {\n return null;\n }\n\n if (value === undefined) {\n return undefined;\n }\n\n const passthrough = options?.passthrough === true;\n if (passthrough) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (options?.serialize) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return value.map((item) => options.serialize!(item as any));\n }\n return value.map((item) => this.serializeValue(item));\n }\n\n if (options?.serialize) {\n return options.serialize(value as any);\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (typeof value === 'bigint') {\n return value.toString();\n }\n\n if (this.isEntity(value)) {\n return this.toJSON(value);\n }\n\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n throw new Error(\n `Cannot serialize value of type '${typeof value}'. Use passthrough: true in @Property() to explicitly allow serialization of unknown types.`,\n );\n }\n\n /**\n * Deserializes a plain object to an entity instance\n *\n * @param entityClass - The entity class constructor\n * @param plainObject - The plain object to deserialize\n * @returns A new instance of the entity with deserialized values\n *\n * @remarks\n * Deserialization rules:\n * - All @Property() decorators must include type metadata for parse() to work\n * - Properties without type metadata will throw an error\n * - Required properties (optional !== true) must be present and not null/undefined\n * - Optional properties (optional === true) can be undefined or null\n * - Arrays are supported with the array: true option\n * - Nested entities are recursively deserialized\n * - Type conversion is strict (no coercion)\n *\n * @example\n * ```typescript\n * @Entity()\n * class User {\n * @Property({ type: () => String }) name!: string;\n * @Property({ type: () => Number }) age!: number;\n * @Property({ type: () => String, optional: true }) email?: string;\n * }\n *\n * const json = { name: 'John', age: 30 };\n * const user = EntityUtils.parse(User, json);\n * // user is a properly typed User instance\n * ```\n */\n static parse<T extends object>(\n entityClass: new () => T,\n plainObject: Record<string, unknown>,\n ): T {\n const instance = new entityClass();\n const keys = this.getPropertyKeys(instance);\n\n for (const key of keys) {\n const options = this.getPropertyOptions(instance, key);\n\n if (!options) {\n throw new Error(\n `Property '${key}' has no metadata. This should not happen if @Property() was used correctly.`,\n );\n }\n\n if (options.passthrough === true) {\n const value = plainObject[key];\n (instance as any)[key] = value;\n continue;\n }\n\n const value = plainObject[key];\n const isOptional = options.optional === true;\n\n if (!(key in plainObject)) {\n if (!isOptional) {\n throw new Error(\n `Property '${key}' is required but missing from input`,\n );\n }\n continue;\n }\n\n if (value === null || value === undefined) {\n if (!isOptional) {\n throw new Error(`Property '${key}' cannot be null or undefined`);\n }\n (instance as any)[key] = value;\n continue;\n }\n\n (instance as any)[key] = this.deserializeValue(value, options, key);\n }\n\n return instance;\n }\n\n /**\n * Deserializes a single value according to the type metadata\n * @private\n */\n private static deserializeValue(\n value: unknown,\n options: PropertyOptions,\n propertyKey: string,\n ): unknown {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const typeConstructor = options.type!();\n const isArray = options.array === true;\n const isSparse = options.sparse === true;\n\n if (isArray) {\n if (!Array.isArray(value)) {\n throw new Error(\n `Property '${propertyKey}' expects an array but received ${typeof value}`,\n );\n }\n\n return value.map((item, index) => {\n if (item === null || item === undefined) {\n if (!isSparse) {\n throw new Error(\n `Property '${propertyKey}[${index}]' cannot be null or undefined. Use sparse: true to allow null/undefined elements in arrays.`,\n );\n }\n return item;\n }\n if (options.deserialize) {\n return options.deserialize(item);\n }\n return this.deserializeSingleValue(\n item,\n typeConstructor,\n `${propertyKey}[${index}]`,\n );\n });\n }\n\n if (options.deserialize) {\n return options.deserialize(value);\n }\n\n return this.deserializeSingleValue(value, typeConstructor, propertyKey);\n }\n\n /**\n * Deserializes a single non-array value\n * @private\n */\n private static deserializeSingleValue(\n value: unknown,\n typeConstructor: any,\n propertyKey: string,\n ): unknown {\n if (typeConstructor === String) {\n if (typeof value !== 'string') {\n throw new Error(\n `Property '${propertyKey}' expects a string but received ${typeof value}`,\n );\n }\n return value;\n }\n\n if (typeConstructor === Number) {\n if (typeof value !== 'number') {\n throw new Error(\n `Property '${propertyKey}' expects a number but received ${typeof value}`,\n );\n }\n return value;\n }\n\n if (typeConstructor === Boolean) {\n if (typeof value !== 'boolean') {\n throw new Error(\n `Property '${propertyKey}' expects a boolean but received ${typeof value}`,\n );\n }\n return value;\n }\n\n if (typeConstructor === BigInt) {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'string') {\n try {\n return BigInt(value);\n } catch {\n throw new Error(\n `Property '${propertyKey}' cannot parse '${value}' as BigInt`,\n );\n }\n }\n throw new Error(\n `Property '${propertyKey}' expects a bigint or string but received ${typeof value}`,\n );\n }\n\n if (typeConstructor === Date) {\n if (value instanceof Date) {\n return value;\n }\n if (typeof value === 'string') {\n const date = new Date(value);\n if (isNaN(date.getTime())) {\n throw new Error(\n `Property '${propertyKey}' cannot parse '${value}' as Date`,\n );\n }\n return date;\n }\n throw new Error(\n `Property '${propertyKey}' expects a Date or ISO string but received ${typeof value}`,\n );\n }\n\n if (this.isEntity(typeConstructor)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(\n `Property '${propertyKey}' expects an object but received ${typeof value}`,\n );\n }\n return this.parse(\n typeConstructor as new () => object,\n value as Record<string, unknown>,\n );\n }\n\n throw new Error(\n `Property '${propertyKey}' has unknown type constructor. Supported types are: String, Number, Boolean, Date, BigInt, and @Entity() classes. Use passthrough: true to explicitly allow unknown types.`,\n );\n }\n}\n"],"names":["ENTITY_METADATA_KEY","PROPERTY_METADATA_KEY","PROPERTY_OPTIONS_METADATA_KEY","isEqualWith","EntityUtils","isEntity","obj","Reflect","hasMetadata","Array","isArray","constructor","Object","getPrototypeOf","sameEntity","a","b","getPropertyKeys","target","currentProto","prototype","keys","seen","Set","protoKeys","getOwnMetadata","key","has","add","push","getPropertyOptions","propertyKey","protoOptions","undefined","equals","val1","val2","diff","length","oldEntity","newEntity","Error","diffs","oldValue","newValue","propertyOptions","areEqual","property","changes","reduce","acc","toJSON","entity","result","value","options","serializeValue","passthrough","serialize","map","item","Date","toISOString","toString","parse","entityClass","plainObject","instance","isOptional","optional","deserializeValue","typeConstructor","type","array","isSparse","sparse","index","deserialize","deserializeSingleValue","String","Number","Boolean","BigInt","date","isNaN","getTime"],"mappings":"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"}
|
package/dist/lib/property.d.ts
CHANGED
|
@@ -1,21 +1,124 @@
|
|
|
1
|
-
import { PropertyOptions } from './types.js';
|
|
1
|
+
import { AnyCtor, type CtorLike, PropertyOptions } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Property decorator that marks class properties with metadata.
|
|
4
4
|
* This decorator can be used to identify and track properties within classes.
|
|
5
5
|
*
|
|
6
|
-
* @param options -
|
|
6
|
+
* @param options - Configuration for the property (type is required)
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* class User {
|
|
10
|
-
* @Property()
|
|
10
|
+
* @Property({ type: () => String })
|
|
11
11
|
* name: string;
|
|
12
12
|
*
|
|
13
|
-
* @Property({ equals: (a, b) => a.toLowerCase() === b.toLowerCase() })
|
|
13
|
+
* @Property({ type: () => String, equals: (a, b) => a.toLowerCase() === b.toLowerCase() })
|
|
14
14
|
* email: string;
|
|
15
15
|
*
|
|
16
|
-
* @Property()
|
|
16
|
+
* @Property({ type: () => Number })
|
|
17
17
|
* age: number;
|
|
18
18
|
* }
|
|
19
19
|
*/
|
|
20
|
-
export declare function Property<T
|
|
20
|
+
export declare function Property<T, C extends CtorLike<T>>(options: PropertyOptions<T, C>): PropertyDecorator;
|
|
21
|
+
/**
|
|
22
|
+
* Helper decorator for string properties
|
|
23
|
+
* @example
|
|
24
|
+
* class User {
|
|
25
|
+
* @StringProperty()
|
|
26
|
+
* name!: string;
|
|
27
|
+
*
|
|
28
|
+
* @StringProperty({ optional: true })
|
|
29
|
+
* nickname?: string;
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export declare function StringProperty(options?: Omit<PropertyOptions<string, StringConstructor>, 'type'>): PropertyDecorator;
|
|
33
|
+
/**
|
|
34
|
+
* Helper decorator for number properties
|
|
35
|
+
* @example
|
|
36
|
+
* class User {
|
|
37
|
+
* @NumberProperty()
|
|
38
|
+
* age!: number;
|
|
39
|
+
*
|
|
40
|
+
* @NumberProperty({ optional: true })
|
|
41
|
+
* score?: number;
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export declare function NumberProperty(options?: Omit<PropertyOptions<number, NumberConstructor>, 'type'>): PropertyDecorator;
|
|
45
|
+
/**
|
|
46
|
+
* Helper decorator for boolean properties
|
|
47
|
+
* @example
|
|
48
|
+
* class User {
|
|
49
|
+
* @BooleanProperty()
|
|
50
|
+
* active!: boolean;
|
|
51
|
+
*
|
|
52
|
+
* @BooleanProperty({ optional: true })
|
|
53
|
+
* verified?: boolean;
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
export declare function BooleanProperty(options?: Omit<PropertyOptions<boolean, BooleanConstructor>, 'type'>): PropertyDecorator;
|
|
57
|
+
/**
|
|
58
|
+
* Helper decorator for Date properties
|
|
59
|
+
* @example
|
|
60
|
+
* class User {
|
|
61
|
+
* @DateProperty()
|
|
62
|
+
* createdAt!: Date;
|
|
63
|
+
*
|
|
64
|
+
* @DateProperty({ optional: true })
|
|
65
|
+
* deletedAt?: Date;
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
export declare function DateProperty(options?: Omit<PropertyOptions<Date, DateConstructor>, 'type'>): PropertyDecorator;
|
|
69
|
+
/**
|
|
70
|
+
* Helper decorator for BigInt properties
|
|
71
|
+
* @example
|
|
72
|
+
* class User {
|
|
73
|
+
* @BigIntProperty()
|
|
74
|
+
* id!: bigint;
|
|
75
|
+
*
|
|
76
|
+
* @BigIntProperty({ optional: true })
|
|
77
|
+
* balance?: bigint;
|
|
78
|
+
* }
|
|
79
|
+
*/
|
|
80
|
+
export declare function BigIntProperty(options?: Omit<PropertyOptions<bigint, BigIntConstructor>, 'type'>): PropertyDecorator;
|
|
81
|
+
/**
|
|
82
|
+
* Helper decorator for entity properties
|
|
83
|
+
* @example
|
|
84
|
+
* class User {
|
|
85
|
+
* @EntityProperty(() => Address)
|
|
86
|
+
* address!: Address;
|
|
87
|
+
*
|
|
88
|
+
* @EntityProperty(() => Profile, { optional: true })
|
|
89
|
+
* profile?: Profile;
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
export declare function EntityProperty<T, C extends AnyCtor<T>>(type: () => C, options?: Omit<PropertyOptions<T, C>, 'type'>): PropertyDecorator;
|
|
93
|
+
/**
|
|
94
|
+
* Helper decorator for array properties
|
|
95
|
+
* @example
|
|
96
|
+
* class User {
|
|
97
|
+
* @ArrayProperty(() => String)
|
|
98
|
+
* tags!: string[];
|
|
99
|
+
*
|
|
100
|
+
* @ArrayProperty(() => Phone)
|
|
101
|
+
* phones!: Phone[];
|
|
102
|
+
*
|
|
103
|
+
* @ArrayProperty(() => Number, { optional: true })
|
|
104
|
+
* scores?: number[];
|
|
105
|
+
*
|
|
106
|
+
* @ArrayProperty(() => String, { sparse: true })
|
|
107
|
+
* sparseList!: (string | null)[];
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
export declare function ArrayProperty<T, C extends CtorLike<T>>(type: () => C, options?: Omit<PropertyOptions<T, C>, 'type' | 'array'>): PropertyDecorator;
|
|
111
|
+
/**
|
|
112
|
+
* Helper decorator for passthrough properties that bypass type validation.
|
|
113
|
+
* Use this for generic types like Record<string, unknown>, any, or custom objects.
|
|
114
|
+
* @example
|
|
115
|
+
* class Config {
|
|
116
|
+
* @PassthroughProperty()
|
|
117
|
+
* metadata!: Record<string, unknown>;
|
|
118
|
+
*
|
|
119
|
+
* @PassthroughProperty()
|
|
120
|
+
* customData!: any;
|
|
121
|
+
* }
|
|
122
|
+
*/
|
|
123
|
+
export declare function PassthroughProperty(): PropertyDecorator;
|
|
21
124
|
//# sourceMappingURL=property.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property.d.ts","sourceRoot":"","sources":["../../src/lib/property.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"property.d.ts","sourceRoot":"","sources":["../../src/lib/property.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,OAAO,EACP,KAAK,QAAQ,EAGb,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,EAC/C,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC7B,iBAAiB,CAqEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,GACjE,iBAAiB,CAEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,GACjE,iBAAiB,CAEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC,GACnE,iBAAiB,CAEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,GAC7D,iBAAiB,CAEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,GACjE,iBAAiB,CAEnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,EACpD,IAAI,EAAE,MAAM,CAAC,EACb,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,GAC5C,iBAAiB,CAEnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,EACpD,IAAI,EAAE,MAAM,CAAC,EACb,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GACtD,iBAAiB,CAEnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,iBAAiB,CAGvD"}
|
package/dist/lib/property.js
CHANGED
|
@@ -1,42 +1,195 @@
|
|
|
1
|
-
import { PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-wrapper-object-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Property decorator that marks class properties with metadata.
|
|
4
4
|
* This decorator can be used to identify and track properties within classes.
|
|
5
5
|
*
|
|
6
|
-
* @param options -
|
|
6
|
+
* @param options - Configuration for the property (type is required)
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* class User {
|
|
10
|
-
* @Property()
|
|
10
|
+
* @Property({ type: () => String })
|
|
11
11
|
* name: string;
|
|
12
12
|
*
|
|
13
|
-
* @Property({ equals: (a, b) => a.toLowerCase() === b.toLowerCase() })
|
|
13
|
+
* @Property({ type: () => String, equals: (a, b) => a.toLowerCase() === b.toLowerCase() })
|
|
14
14
|
* email: string;
|
|
15
15
|
*
|
|
16
|
-
* @Property()
|
|
16
|
+
* @Property({ type: () => Number })
|
|
17
17
|
* age: number;
|
|
18
18
|
* }
|
|
19
19
|
*/ export function Property(options) {
|
|
20
20
|
return (target, propertyKey)=>{
|
|
21
|
-
// Only support string property keys
|
|
22
21
|
if (typeof propertyKey !== 'string') {
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
|
-
// Get existing metadata from own property only (not from prototype chain)
|
|
26
24
|
const existingProperties = Reflect.getOwnMetadata(PROPERTY_METADATA_KEY, target) || [];
|
|
27
|
-
// Add this property if not already tracked
|
|
28
25
|
if (!existingProperties.includes(propertyKey)) {
|
|
29
26
|
existingProperties.push(propertyKey);
|
|
30
27
|
}
|
|
31
|
-
// Store updated metadata on the target itself
|
|
32
28
|
Reflect.defineMetadata(PROPERTY_METADATA_KEY, existingProperties, target);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
if (options.passthrough === true) {
|
|
30
|
+
if (options.array === true) {
|
|
31
|
+
throw new Error(`Property '${propertyKey}' has passthrough: true and array: true. Passthrough cannot be combined with array.`);
|
|
32
|
+
}
|
|
33
|
+
if (options.optional === true) {
|
|
34
|
+
throw new Error(`Property '${propertyKey}' has passthrough: true and optional: true. Passthrough cannot be combined with optional.`);
|
|
35
|
+
}
|
|
36
|
+
if (options.sparse === true) {
|
|
37
|
+
throw new Error(`Property '${propertyKey}' has passthrough: true and sparse: true. Passthrough cannot be combined with sparse.`);
|
|
38
|
+
}
|
|
39
|
+
if (options.serialize !== undefined || options.deserialize !== undefined) {
|
|
40
|
+
throw new Error(`Property '${propertyKey}' has passthrough: true and custom serialize/deserialize functions. Passthrough cannot be combined with serialize or deserialize.`);
|
|
41
|
+
}
|
|
38
42
|
}
|
|
43
|
+
if (options.sparse === true && options.array !== true) {
|
|
44
|
+
throw new Error(`Property '${propertyKey}' has sparse: true but array is not true. The sparse option only applies to arrays.`);
|
|
45
|
+
}
|
|
46
|
+
// Validate serialize/deserialize pairing
|
|
47
|
+
const hasSerialize = options.serialize !== undefined;
|
|
48
|
+
const hasDeserialize = options.deserialize !== undefined;
|
|
49
|
+
if (hasSerialize !== hasDeserialize) {
|
|
50
|
+
throw new Error(`Property '${propertyKey}' must define both serialize and deserialize functions, or neither. Found only ${hasSerialize ? 'serialize' : 'deserialize'}.`);
|
|
51
|
+
}
|
|
52
|
+
const existingOptions = Reflect.getOwnMetadata(PROPERTY_OPTIONS_METADATA_KEY, target) || {};
|
|
53
|
+
existingOptions[propertyKey] = options;
|
|
54
|
+
Reflect.defineMetadata(PROPERTY_OPTIONS_METADATA_KEY, existingOptions, target);
|
|
39
55
|
};
|
|
40
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Helper decorator for string properties
|
|
59
|
+
* @example
|
|
60
|
+
* class User {
|
|
61
|
+
* @StringProperty()
|
|
62
|
+
* name!: string;
|
|
63
|
+
*
|
|
64
|
+
* @StringProperty({ optional: true })
|
|
65
|
+
* nickname?: string;
|
|
66
|
+
* }
|
|
67
|
+
*/ export function StringProperty(options) {
|
|
68
|
+
return Property({
|
|
69
|
+
...options,
|
|
70
|
+
type: ()=>String
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Helper decorator for number properties
|
|
75
|
+
* @example
|
|
76
|
+
* class User {
|
|
77
|
+
* @NumberProperty()
|
|
78
|
+
* age!: number;
|
|
79
|
+
*
|
|
80
|
+
* @NumberProperty({ optional: true })
|
|
81
|
+
* score?: number;
|
|
82
|
+
* }
|
|
83
|
+
*/ export function NumberProperty(options) {
|
|
84
|
+
return Property({
|
|
85
|
+
...options,
|
|
86
|
+
type: ()=>Number
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Helper decorator for boolean properties
|
|
91
|
+
* @example
|
|
92
|
+
* class User {
|
|
93
|
+
* @BooleanProperty()
|
|
94
|
+
* active!: boolean;
|
|
95
|
+
*
|
|
96
|
+
* @BooleanProperty({ optional: true })
|
|
97
|
+
* verified?: boolean;
|
|
98
|
+
* }
|
|
99
|
+
*/ export function BooleanProperty(options) {
|
|
100
|
+
return Property({
|
|
101
|
+
...options,
|
|
102
|
+
type: ()=>Boolean
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Helper decorator for Date properties
|
|
107
|
+
* @example
|
|
108
|
+
* class User {
|
|
109
|
+
* @DateProperty()
|
|
110
|
+
* createdAt!: Date;
|
|
111
|
+
*
|
|
112
|
+
* @DateProperty({ optional: true })
|
|
113
|
+
* deletedAt?: Date;
|
|
114
|
+
* }
|
|
115
|
+
*/ export function DateProperty(options) {
|
|
116
|
+
return Property({
|
|
117
|
+
...options,
|
|
118
|
+
type: ()=>Date
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Helper decorator for BigInt properties
|
|
123
|
+
* @example
|
|
124
|
+
* class User {
|
|
125
|
+
* @BigIntProperty()
|
|
126
|
+
* id!: bigint;
|
|
127
|
+
*
|
|
128
|
+
* @BigIntProperty({ optional: true })
|
|
129
|
+
* balance?: bigint;
|
|
130
|
+
* }
|
|
131
|
+
*/ export function BigIntProperty(options) {
|
|
132
|
+
return Property({
|
|
133
|
+
...options,
|
|
134
|
+
type: ()=>BigInt
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Helper decorator for entity properties
|
|
139
|
+
* @example
|
|
140
|
+
* class User {
|
|
141
|
+
* @EntityProperty(() => Address)
|
|
142
|
+
* address!: Address;
|
|
143
|
+
*
|
|
144
|
+
* @EntityProperty(() => Profile, { optional: true })
|
|
145
|
+
* profile?: Profile;
|
|
146
|
+
* }
|
|
147
|
+
*/ export function EntityProperty(type, options) {
|
|
148
|
+
return Property({
|
|
149
|
+
...options,
|
|
150
|
+
type
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Helper decorator for array properties
|
|
155
|
+
* @example
|
|
156
|
+
* class User {
|
|
157
|
+
* @ArrayProperty(() => String)
|
|
158
|
+
* tags!: string[];
|
|
159
|
+
*
|
|
160
|
+
* @ArrayProperty(() => Phone)
|
|
161
|
+
* phones!: Phone[];
|
|
162
|
+
*
|
|
163
|
+
* @ArrayProperty(() => Number, { optional: true })
|
|
164
|
+
* scores?: number[];
|
|
165
|
+
*
|
|
166
|
+
* @ArrayProperty(() => String, { sparse: true })
|
|
167
|
+
* sparseList!: (string | null)[];
|
|
168
|
+
* }
|
|
169
|
+
*/ export function ArrayProperty(type, options) {
|
|
170
|
+
return Property({
|
|
171
|
+
...options,
|
|
172
|
+
type,
|
|
173
|
+
array: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Helper decorator for passthrough properties that bypass type validation.
|
|
178
|
+
* Use this for generic types like Record<string, unknown>, any, or custom objects.
|
|
179
|
+
* @example
|
|
180
|
+
* class Config {
|
|
181
|
+
* @PassthroughProperty()
|
|
182
|
+
* metadata!: Record<string, unknown>;
|
|
183
|
+
*
|
|
184
|
+
* @PassthroughProperty()
|
|
185
|
+
* customData!: any;
|
|
186
|
+
* }
|
|
187
|
+
*/ export function PassthroughProperty() {
|
|
188
|
+
// Use a dummy type since type is mandatory but not used with passthrough
|
|
189
|
+
return Property({
|
|
190
|
+
type: ()=>Object,
|
|
191
|
+
passthrough: true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
41
194
|
|
|
42
195
|
//# sourceMappingURL=property.js.map
|
package/dist/lib/property.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/property.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/lib/property.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-wrapper-object-types */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n AnyCtor,\n type CtorLike,\n PROPERTY_METADATA_KEY,\n PROPERTY_OPTIONS_METADATA_KEY,\n PropertyOptions,\n} from './types.js';\n\n/**\n * Property decorator that marks class properties with metadata.\n * This decorator can be used to identify and track properties within classes.\n *\n * @param options - Configuration for the property (type is required)\n *\n * @example\n * class User {\n * @Property({ type: () => String })\n * name: string;\n *\n * @Property({ type: () => String, equals: (a, b) => a.toLowerCase() === b.toLowerCase() })\n * email: string;\n *\n * @Property({ type: () => Number })\n * age: number;\n * }\n */\nexport function Property<T, C extends CtorLike<T>>(\n options: PropertyOptions<T, C>,\n): PropertyDecorator {\n return (target: object, propertyKey: string | symbol): void => {\n if (typeof propertyKey !== 'string') {\n return;\n }\n\n const existingProperties: string[] =\n Reflect.getOwnMetadata(PROPERTY_METADATA_KEY, target) || [];\n\n if (!existingProperties.includes(propertyKey)) {\n existingProperties.push(propertyKey);\n }\n\n Reflect.defineMetadata(PROPERTY_METADATA_KEY, existingProperties, target);\n\n if (options.passthrough === true) {\n if (options.array === true) {\n throw new Error(\n `Property '${propertyKey}' has passthrough: true and array: true. Passthrough cannot be combined with array.`,\n );\n }\n if (options.optional === true) {\n throw new Error(\n `Property '${propertyKey}' has passthrough: true and optional: true. Passthrough cannot be combined with optional.`,\n );\n }\n if (options.sparse === true) {\n throw new Error(\n `Property '${propertyKey}' has passthrough: true and sparse: true. Passthrough cannot be combined with sparse.`,\n );\n }\n if (\n options.serialize !== undefined ||\n options.deserialize !== undefined\n ) {\n throw new Error(\n `Property '${propertyKey}' has passthrough: true and custom serialize/deserialize functions. Passthrough cannot be combined with serialize or deserialize.`,\n );\n }\n }\n\n if (options.sparse === true && options.array !== true) {\n throw new Error(\n `Property '${propertyKey}' has sparse: true but array is not true. The sparse option only applies to arrays.`,\n );\n }\n\n // Validate serialize/deserialize pairing\n const hasSerialize = options.serialize !== undefined;\n const hasDeserialize = options.deserialize !== undefined;\n if (hasSerialize !== hasDeserialize) {\n throw new Error(\n `Property '${propertyKey}' must define both serialize and deserialize functions, or neither. Found only ${hasSerialize ? 'serialize' : 'deserialize'}.`,\n );\n }\n\n const existingOptions: Record<\n string,\n PropertyOptions<any, any>\n > = Reflect.getOwnMetadata(PROPERTY_OPTIONS_METADATA_KEY, target) || {};\n\n existingOptions[propertyKey] = options;\n\n Reflect.defineMetadata(\n PROPERTY_OPTIONS_METADATA_KEY,\n existingOptions,\n target,\n );\n };\n}\n\n/**\n * Helper decorator for string properties\n * @example\n * class User {\n * @StringProperty()\n * name!: string;\n *\n * @StringProperty({ optional: true })\n * nickname?: string;\n * }\n */\nexport function StringProperty(\n options?: Omit<PropertyOptions<string, StringConstructor>, 'type'>,\n): PropertyDecorator {\n return Property({ ...options, type: () => String });\n}\n\n/**\n * Helper decorator for number properties\n * @example\n * class User {\n * @NumberProperty()\n * age!: number;\n *\n * @NumberProperty({ optional: true })\n * score?: number;\n * }\n */\nexport function NumberProperty(\n options?: Omit<PropertyOptions<number, NumberConstructor>, 'type'>,\n): PropertyDecorator {\n return Property({ ...options, type: () => Number });\n}\n\n/**\n * Helper decorator for boolean properties\n * @example\n * class User {\n * @BooleanProperty()\n * active!: boolean;\n *\n * @BooleanProperty({ optional: true })\n * verified?: boolean;\n * }\n */\nexport function BooleanProperty(\n options?: Omit<PropertyOptions<boolean, BooleanConstructor>, 'type'>,\n): PropertyDecorator {\n return Property({ ...options, type: () => Boolean });\n}\n\n/**\n * Helper decorator for Date properties\n * @example\n * class User {\n * @DateProperty()\n * createdAt!: Date;\n *\n * @DateProperty({ optional: true })\n * deletedAt?: Date;\n * }\n */\nexport function DateProperty(\n options?: Omit<PropertyOptions<Date, DateConstructor>, 'type'>,\n): PropertyDecorator {\n return Property({ ...options, type: () => Date });\n}\n\n/**\n * Helper decorator for BigInt properties\n * @example\n * class User {\n * @BigIntProperty()\n * id!: bigint;\n *\n * @BigIntProperty({ optional: true })\n * balance?: bigint;\n * }\n */\nexport function BigIntProperty(\n options?: Omit<PropertyOptions<bigint, BigIntConstructor>, 'type'>,\n): PropertyDecorator {\n return Property({ ...options, type: () => BigInt });\n}\n\n/**\n * Helper decorator for entity properties\n * @example\n * class User {\n * @EntityProperty(() => Address)\n * address!: Address;\n *\n * @EntityProperty(() => Profile, { optional: true })\n * profile?: Profile;\n * }\n */\nexport function EntityProperty<T, C extends AnyCtor<T>>(\n type: () => C,\n options?: Omit<PropertyOptions<T, C>, 'type'>,\n): PropertyDecorator {\n return Property<T, C>({ ...options, type });\n}\n\n/**\n * Helper decorator for array properties\n * @example\n * class User {\n * @ArrayProperty(() => String)\n * tags!: string[];\n *\n * @ArrayProperty(() => Phone)\n * phones!: Phone[];\n *\n * @ArrayProperty(() => Number, { optional: true })\n * scores?: number[];\n *\n * @ArrayProperty(() => String, { sparse: true })\n * sparseList!: (string | null)[];\n * }\n */\nexport function ArrayProperty<T, C extends CtorLike<T>>(\n type: () => C,\n options?: Omit<PropertyOptions<T, C>, 'type' | 'array'>,\n): PropertyDecorator {\n return Property({ ...options, type, array: true });\n}\n\n/**\n * Helper decorator for passthrough properties that bypass type validation.\n * Use this for generic types like Record<string, unknown>, any, or custom objects.\n * @example\n * class Config {\n * @PassthroughProperty()\n * metadata!: Record<string, unknown>;\n *\n * @PassthroughProperty()\n * customData!: any;\n * }\n */\nexport function PassthroughProperty(): PropertyDecorator {\n // Use a dummy type since type is mandatory but not used with passthrough\n return Property({ type: () => Object, passthrough: true });\n}\n"],"names":["PROPERTY_METADATA_KEY","PROPERTY_OPTIONS_METADATA_KEY","Property","options","target","propertyKey","existingProperties","Reflect","getOwnMetadata","includes","push","defineMetadata","passthrough","array","Error","optional","sparse","serialize","undefined","deserialize","hasSerialize","hasDeserialize","existingOptions","StringProperty","type","String","NumberProperty","Number","BooleanProperty","Boolean","DateProperty","Date","BigIntProperty","BigInt","EntityProperty","ArrayProperty","PassthroughProperty","Object"],"mappings":"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"}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -10,16 +10,94 @@ export declare const PROPERTY_OPTIONS_METADATA_KEY: unique symbol;
|
|
|
10
10
|
* Metadata key used to store entity information
|
|
11
11
|
*/
|
|
12
12
|
export declare const ENTITY_METADATA_KEY: unique symbol;
|
|
13
|
+
export type AnyCtor<T = any> = abstract new (...args: any[]) => T;
|
|
14
|
+
export type BuiltinCtors = StringConstructor | NumberConstructor | BooleanConstructor | BigIntConstructor | SymbolConstructor | DateConstructor;
|
|
15
|
+
export type CtorLike<T> = AnyCtor<T> | BuiltinCtors;
|
|
16
|
+
export type InstanceOfCtorLike<C> = C extends StringConstructor ? string : C extends NumberConstructor ? number : C extends BooleanConstructor ? boolean : C extends BigIntConstructor ? bigint : C extends SymbolConstructor ? symbol : C extends DateConstructor ? Date : C extends AnyCtor<infer T> ? T : never;
|
|
13
17
|
/**
|
|
14
18
|
* Options for the Property decorator
|
|
15
19
|
*/
|
|
16
|
-
export interface PropertyOptions<T = any> {
|
|
20
|
+
export interface PropertyOptions<T = any, C extends CtorLike<T> = AnyCtor<T> | BuiltinCtors> {
|
|
17
21
|
/**
|
|
18
22
|
* Custom equality comparison function for this property
|
|
19
23
|
* @param a - First value to compare
|
|
20
24
|
* @param b - Second value to compare
|
|
21
25
|
* @returns true if values are equal, false otherwise
|
|
22
26
|
*/
|
|
23
|
-
equals?: (a:
|
|
27
|
+
equals?: (a: InstanceOfCtorLike<C>, b: InstanceOfCtorLike<C>) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Type constructor for this property. Required for EntityUtils.parse() support.
|
|
30
|
+
* Use a function that returns the type constructor to support forward references.
|
|
31
|
+
* @example
|
|
32
|
+
* @Property({ type: () => String })
|
|
33
|
+
* name!: string;
|
|
34
|
+
*
|
|
35
|
+
* @Property({ type: () => Address })
|
|
36
|
+
* address!: Address;
|
|
37
|
+
*/
|
|
38
|
+
type: () => C;
|
|
39
|
+
/**
|
|
40
|
+
* Whether this property is an array. Defaults to false.
|
|
41
|
+
* When true, the deserializer will map over array elements.
|
|
42
|
+
* @example
|
|
43
|
+
* @Property({ type: () => String, array: true })
|
|
44
|
+
* tags!: string[];
|
|
45
|
+
*/
|
|
46
|
+
array?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether this property is optional. Defaults to false.
|
|
49
|
+
* When true, the property can be undefined or null.
|
|
50
|
+
* When false, the property must be present and not null/undefined.
|
|
51
|
+
* @example
|
|
52
|
+
* @Property({ type: () => String, optional: true })
|
|
53
|
+
* nickname?: string;
|
|
54
|
+
*/
|
|
55
|
+
optional?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the array can contain null/undefined elements. Defaults to false.
|
|
58
|
+
* Only applicable when array is true.
|
|
59
|
+
* When false (default), null/undefined elements will cause an error.
|
|
60
|
+
* When true, null/undefined elements are allowed in the array.
|
|
61
|
+
* @example
|
|
62
|
+
* @Property({ type: () => String, array: true, sparse: true })
|
|
63
|
+
* tags!: (string | null)[];
|
|
64
|
+
*/
|
|
65
|
+
sparse?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether to bypass type validation and pass values through as-is.
|
|
68
|
+
* Use this for generic types like Record<string, unknown> or any.
|
|
69
|
+
* When true, no type checking or transformation is performed.
|
|
70
|
+
* Also bypasses any custom serialize/deserialize callbacks.
|
|
71
|
+
* @example
|
|
72
|
+
* @Property({ passthrough: true })
|
|
73
|
+
* metadata!: Record<string, unknown>;
|
|
74
|
+
*/
|
|
75
|
+
passthrough?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Custom serialization function to convert the property value to JSON-compatible format.
|
|
78
|
+
* Must be paired with deserialize - both must be defined together or both omitted.
|
|
79
|
+
* Not used when passthrough is true.
|
|
80
|
+
* @example
|
|
81
|
+
* @Property({
|
|
82
|
+
* type: () => MyClass,
|
|
83
|
+
* serialize: (value) => ({ data: value.toData() }),
|
|
84
|
+
* deserialize: (json) => MyClass.fromData(json.data)
|
|
85
|
+
* })
|
|
86
|
+
* myProperty!: MyClass;
|
|
87
|
+
*/
|
|
88
|
+
serialize?: (value: InstanceOfCtorLike<C>) => unknown;
|
|
89
|
+
/**
|
|
90
|
+
* Custom deserialization function to convert JSON data back to the property type.
|
|
91
|
+
* Must be paired with serialize - both must be defined together or both omitted.
|
|
92
|
+
* Not used when passthrough is true.
|
|
93
|
+
* @example
|
|
94
|
+
* @Property({
|
|
95
|
+
* type: () => MyClass,
|
|
96
|
+
* serialize: (value) => ({ data: value.toData() }),
|
|
97
|
+
* deserialize: (json) => MyClass.fromData(json.data)
|
|
98
|
+
* })
|
|
99
|
+
* myProperty!: MyClass;
|
|
100
|
+
*/
|
|
101
|
+
deserialize?: (serialized: unknown) => InstanceOfCtorLike<C>;
|
|
24
102
|
}
|
|
25
103
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,qBAAqB,eAA8B,CAAC;AAEjE;;GAEG;AACH,eAAO,MAAM,6BAA6B,eAEzC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAA4B,CAAC;AAE7D,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,GAAG,IAAI,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAElE,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,CAAC;AAEpB,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC;AAEpD,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,CAAC,SAAS,iBAAiB,GAC3D,MAAM,GACN,CAAC,SAAS,iBAAiB,GACzB,MAAM,GACN,CAAC,SAAS,kBAAkB,GAC1B,OAAO,GACP,CAAC,SAAS,iBAAiB,GACzB,MAAM,GACN,CAAC,SAAS,iBAAiB,GACzB,MAAM,GACN,CAAC,SAAS,eAAe,GACvB,IAAI,GACJ,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GACxB,CAAC,GACD,KAAK,CAAC;AAEtB;;GAEG;AACH,MAAM,WAAW,eAAe,CAC9B,CAAC,GAAG,GAAG,EACP,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY;IAEjD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;IAEzE;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAC,CAAC;IAEd;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;IAEtD;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,kBAAkB,CAAC,CAAC,CAAC,CAAC;CAC9D"}
|
package/dist/lib/types.js
CHANGED
package/dist/lib/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/types.ts"],"sourcesContent":["/**\n * Metadata key used to store property information\n */\nexport const PROPERTY_METADATA_KEY = Symbol('property:metadata');\n\n/**\n * Metadata key used to store property options\n */\nexport const PROPERTY_OPTIONS_METADATA_KEY = Symbol(\n 'property:options:metadata',\n);\n\n/**\n * Metadata key used to store entity information\n */\nexport const ENTITY_METADATA_KEY = Symbol('entity:metadata');\n\n/**\n * Options for the Property decorator\n */\nexport interface PropertyOptions
|
|
1
|
+
{"version":3,"sources":["../../src/lib/types.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-wrapper-object-types */\n/**\n * Metadata key used to store property information\n */\nexport const PROPERTY_METADATA_KEY = Symbol('property:metadata');\n\n/**\n * Metadata key used to store property options\n */\nexport const PROPERTY_OPTIONS_METADATA_KEY = Symbol(\n 'property:options:metadata',\n);\n\n/**\n * Metadata key used to store entity information\n */\nexport const ENTITY_METADATA_KEY = Symbol('entity:metadata');\n\nexport type AnyCtor<T = any> = abstract new (...args: any[]) => T;\n\nexport type BuiltinCtors =\n | StringConstructor\n | NumberConstructor\n | BooleanConstructor\n | BigIntConstructor\n | SymbolConstructor\n | DateConstructor;\n\nexport type CtorLike<T> = AnyCtor<T> | BuiltinCtors;\n\nexport type InstanceOfCtorLike<C> = C extends StringConstructor\n ? string\n : C extends NumberConstructor\n ? number\n : C extends BooleanConstructor\n ? boolean\n : C extends BigIntConstructor\n ? bigint\n : C extends SymbolConstructor\n ? symbol\n : C extends DateConstructor\n ? Date\n : C extends AnyCtor<infer T>\n ? T\n : never;\n\n/**\n * Options for the Property decorator\n */\nexport interface PropertyOptions<\n T = any,\n C extends CtorLike<T> = AnyCtor<T> | BuiltinCtors,\n> {\n /**\n * Custom equality comparison function for this property\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns true if values are equal, false otherwise\n */\n equals?: (a: InstanceOfCtorLike<C>, b: InstanceOfCtorLike<C>) => boolean;\n\n /**\n * Type constructor for this property. Required for EntityUtils.parse() support.\n * Use a function that returns the type constructor to support forward references.\n * @example\n * @Property({ type: () => String })\n * name!: string;\n *\n * @Property({ type: () => Address })\n * address!: Address;\n */\n type: () => C;\n\n /**\n * Whether this property is an array. Defaults to false.\n * When true, the deserializer will map over array elements.\n * @example\n * @Property({ type: () => String, array: true })\n * tags!: string[];\n */\n array?: boolean;\n\n /**\n * Whether this property is optional. Defaults to false.\n * When true, the property can be undefined or null.\n * When false, the property must be present and not null/undefined.\n * @example\n * @Property({ type: () => String, optional: true })\n * nickname?: string;\n */\n optional?: boolean;\n\n /**\n * Whether the array can contain null/undefined elements. Defaults to false.\n * Only applicable when array is true.\n * When false (default), null/undefined elements will cause an error.\n * When true, null/undefined elements are allowed in the array.\n * @example\n * @Property({ type: () => String, array: true, sparse: true })\n * tags!: (string | null)[];\n */\n sparse?: boolean;\n\n /**\n * Whether to bypass type validation and pass values through as-is.\n * Use this for generic types like Record<string, unknown> or any.\n * When true, no type checking or transformation is performed.\n * Also bypasses any custom serialize/deserialize callbacks.\n * @example\n * @Property({ passthrough: true })\n * metadata!: Record<string, unknown>;\n */\n passthrough?: boolean;\n\n /**\n * Custom serialization function to convert the property value to JSON-compatible format.\n * Must be paired with deserialize - both must be defined together or both omitted.\n * Not used when passthrough is true.\n * @example\n * @Property({\n * type: () => MyClass,\n * serialize: (value) => ({ data: value.toData() }),\n * deserialize: (json) => MyClass.fromData(json.data)\n * })\n * myProperty!: MyClass;\n */\n serialize?: (value: InstanceOfCtorLike<C>) => unknown;\n\n /**\n * Custom deserialization function to convert JSON data back to the property type.\n * Must be paired with serialize - both must be defined together or both omitted.\n * Not used when passthrough is true.\n * @example\n * @Property({\n * type: () => MyClass,\n * serialize: (value) => ({ data: value.toData() }),\n * deserialize: (json) => MyClass.fromData(json.data)\n * })\n * myProperty!: MyClass;\n */\n deserialize?: (serialized: unknown) => InstanceOfCtorLike<C>;\n}\n"],"names":["PROPERTY_METADATA_KEY","Symbol","PROPERTY_OPTIONS_METADATA_KEY","ENTITY_METADATA_KEY"],"mappings":"AAAA,qDAAqD,GACrD,6DAA6D,GAC7D;;CAEC,GACD,OAAO,MAAMA,wBAAwBC,OAAO,qBAAqB;AAEjE;;CAEC,GACD,OAAO,MAAMC,gCAAgCD,OAC3C,6BACA;AAEF;;CAEC,GACD,OAAO,MAAME,sBAAsBF,OAAO,mBAAmB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtpaulino/entity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@rtpaulino/core": "^0.
|
|
50
|
+
"@rtpaulino/core": "^0.13.0",
|
|
51
51
|
"@swc/helpers": "~0.5.18",
|
|
52
52
|
"lodash-es": "^4.17.22"
|
|
53
53
|
},
|