@reharik/smart-enum 0.0.3 → 0.0.5
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 +878 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
# Smart Enums
|
|
2
|
+
|
|
3
|
+
A TypeScript library for creating type-safe, feature-rich enumerations with built-in utility methods. Stop juggling between constants, arrays, objects, and string unions - use Smart Enums instead.
|
|
4
|
+
|
|
5
|
+
## CAVEAT?
|
|
6
|
+
|
|
7
|
+
While I believe this library is super useful, I am currently refining it via dogfood, trying to get as smooth a process down as possible. This is leading to somewhat frequent releases with possibly breaking changes. As it is a new library, and I presume no one is using it I"m not being very considerate. If you discover this library and want to use it, please tell me and I will stop breaking it :) and employ better release practices.
|
|
8
|
+
|
|
9
|
+
## Changelog
|
|
10
|
+
|
|
11
|
+
See [CHANGELOG.md](packages/core/CHANGELOG.md) for a detailed list of changes and version history.
|
|
12
|
+
|
|
13
|
+
## Why Smart Enums?
|
|
14
|
+
|
|
15
|
+
Traditional approaches to enums in JavaScript/TypeScript have limitations:
|
|
16
|
+
|
|
17
|
+
- Plain objects `{ME: "me", YOU: "you"}` lack utility methods
|
|
18
|
+
- Constants `const ME = "me"` are scattered and hard to iterate
|
|
19
|
+
- Arrays `["me", "you"]` don't provide lookups
|
|
20
|
+
- String unions `type No = "please" | "pretty please"` are compile-time only
|
|
21
|
+
|
|
22
|
+
Smart Enums give you the best of all worlds:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const Colors = enumeration('Colors', {
|
|
26
|
+
input: ['red', 'blue', 'green'] as const,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// You get a rich object structure:
|
|
30
|
+
Colors.red; // { key: "red", value: "RED", display: "Red", index: 0 }
|
|
31
|
+
Colors.blue; // { key: "blue", value: "BLUE", display: "Blue", index: 1 }
|
|
32
|
+
|
|
33
|
+
// Plus powerful utility methods:
|
|
34
|
+
Colors.fromValue('RED'); // Returns Colors.red
|
|
35
|
+
Colors.items(); // Returns all enum items
|
|
36
|
+
Colors.values(); // Returns ['RED', 'BLUE', 'GREEN']
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install ts-smart-enum
|
|
43
|
+
# or
|
|
44
|
+
yarn add ts-smart-enum
|
|
45
|
+
# or
|
|
46
|
+
pnpm add ts-smart-enum
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The npm package name is `**ts-smart-enum**` (it was previously published as `smart-enums`).
|
|
50
|
+
|
|
51
|
+
**Installing from GitHub:** point your dependency at this repo with npm’s `path` option (for example `github:YOUR_ORG/YOUR_REPO#path:packages/core`). The package’s **`prepare`** script runs on install and builds `dist/`, so built files do not need to be committed. If you depend on **`path:packages/knex`**, its `prepare` builds **`packages/core` first** (then the Knex package) so declaration files exist before the Knex build runs.
|
|
52
|
+
|
|
53
|
+
## Tree-Shaking Support
|
|
54
|
+
|
|
55
|
+
Smart Enums supports tree-shaking with multiple entry points for optimal bundle sizes:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// Core functionality only (smallest bundle)
|
|
59
|
+
import { enumeration, isSmartEnumItem } from 'ts-smart-enum/core';
|
|
60
|
+
|
|
61
|
+
// Core + API transport utilities
|
|
62
|
+
import {
|
|
63
|
+
enumeration,
|
|
64
|
+
serializeForTransport,
|
|
65
|
+
reviveAfterTransport,
|
|
66
|
+
} from 'ts-smart-enum/transport';
|
|
67
|
+
|
|
68
|
+
// Core + database utilities
|
|
69
|
+
import {
|
|
70
|
+
enumeration,
|
|
71
|
+
prepareForDatabase,
|
|
72
|
+
reviveRowFromDatabase,
|
|
73
|
+
} from 'ts-smart-enum/database';
|
|
74
|
+
|
|
75
|
+
// Full API (backward compatible)
|
|
76
|
+
import {
|
|
77
|
+
enumeration,
|
|
78
|
+
serializeSmartEnums,
|
|
79
|
+
prepareForDatabase,
|
|
80
|
+
} from 'ts-smart-enum';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Bundle Size Comparison:**
|
|
84
|
+
|
|
85
|
+
- `ts-smart-enum/core`: ~149 bytes (minimal)
|
|
86
|
+
- `ts-smart-enum/transport`: ~406 bytes (core + serialization)
|
|
87
|
+
- `ts-smart-enum/database`: ~379 bytes (core + database utilities)
|
|
88
|
+
- `ts-smart-enum`: ~598 bytes (everything)
|
|
89
|
+
|
|
90
|
+
### Knex (optional)
|
|
91
|
+
|
|
92
|
+
For Knex, use [`@reharik/smart-enum-knex`](https://www.npmjs.com/package/@reharik/smart-enum-knex): it adds `withEnumRevival` and `createSmartEnumPostProcessResponse` on top of the same explicit `FieldEnumMapping` + `reviveRowFromDatabase` flow as `ts-smart-enum/database`. Revival is **not** inferred from the schema; you attach mapping per query via `queryContext`.
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
### Creating Enums from Arrays
|
|
97
|
+
|
|
98
|
+
The simplest way to create a Smart Enum:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { enumeration, Enumeration } from 'ts-smart-enum';
|
|
102
|
+
|
|
103
|
+
const input = ['pending', 'active', 'completed', 'archived'] as const;
|
|
104
|
+
|
|
105
|
+
const Status = enumeration('Status', { input });
|
|
106
|
+
type Status = Enumeration<typeof Status>;
|
|
107
|
+
|
|
108
|
+
// Use it:
|
|
109
|
+
console.log(Status.active.value); // "ACTIVE"
|
|
110
|
+
console.log(Status.active.display); // "Active"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Creating Enums from Objects
|
|
114
|
+
|
|
115
|
+
For more control over the values:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { enumeration, Enumeration } from 'ts-smart-enum';
|
|
119
|
+
|
|
120
|
+
const input = {
|
|
121
|
+
low: { value: 'LOW', display: 'Low Priority' },
|
|
122
|
+
medium: { value: 'MIDDLE', display: 'Medium Priority' },
|
|
123
|
+
high: { value: 'HIGH', display: 'High Priority' },
|
|
124
|
+
urgent: { value: 'URGENT', display: 'Urgent!!!' },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const Priority = enumeration('Priority', { input });
|
|
128
|
+
type Priority = Enumeration<typeof Priority>;
|
|
129
|
+
|
|
130
|
+
console.log(Priority.urgent.value); // "URGENT"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Core Features
|
|
134
|
+
|
|
135
|
+
### Type Safety
|
|
136
|
+
|
|
137
|
+
Smart Enums are fully type-safe:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
function processColor(color: Color) {
|
|
141
|
+
console.log(color.display);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
processColor(Colors.red); // ✅ Works
|
|
145
|
+
processColor(Status.active); // ❌ Type error
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Auto-Generated Properties
|
|
149
|
+
|
|
150
|
+
Each enum item automatically gets:
|
|
151
|
+
|
|
152
|
+
- `key` - The original key (e.g., "red")
|
|
153
|
+
- `value` - Constant case version (e.g., "RED")
|
|
154
|
+
- `display` - Human-readable version (e.g., "Red")
|
|
155
|
+
- `index` - Position in the enum (e.g., 0, 1, 2)
|
|
156
|
+
|
|
157
|
+
### Custom Property Formatters
|
|
158
|
+
|
|
159
|
+
Override or add custom auto-formatting:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const input = ['userProfile', 'adminDashboard', 'settingsPage'] as const;
|
|
163
|
+
const propertyAutoFormatters = [
|
|
164
|
+
{ key: 'path', format: k => `/${k}` },
|
|
165
|
+
{ key: 'slug', format: k => k.toLowerCase().replace(/([A-Z])/g, '-$1') },
|
|
166
|
+
{ key: 'value', format: k => k.toLowerCase() },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const Routes = enumeration('Routes', { input, propertyAutoFormatters });
|
|
170
|
+
type Routes = Enumeration<typeof Routes>;
|
|
171
|
+
|
|
172
|
+
console.log(Routes.userProfile.path); // "/userProfile"
|
|
173
|
+
console.log(Routes.userProfile.slug); // "/user-profile"
|
|
174
|
+
console.log(Routes.userProfile.value); // "/user-profile"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Utility Methods
|
|
178
|
+
|
|
179
|
+
### Lookup Methods
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Get enum item by value (throws if not found)
|
|
183
|
+
const item = Colors.fromValue('RED');
|
|
184
|
+
|
|
185
|
+
// Safe lookup (returns undefined if not found)
|
|
186
|
+
const maybeItem = Colors.tryFromValue('PURPLE');
|
|
187
|
+
|
|
188
|
+
// Lookup by key
|
|
189
|
+
const keyItem = Colors.fromKey('red');
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Conversion Methods
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Get all values
|
|
196
|
+
const values = Colors.values(); // ['RED', 'BLUE', 'GREEN']
|
|
197
|
+
|
|
198
|
+
// Get all keys
|
|
199
|
+
const keys = Colors.keys(); // ['red', 'blue', 'green']
|
|
200
|
+
|
|
201
|
+
// Get all enum items as array
|
|
202
|
+
const items = Colors.items();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Advanced Usage
|
|
206
|
+
|
|
207
|
+
### Custom Properties
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const input = {
|
|
211
|
+
red: { hex: '#FF0000', rgb: [255, 0, 0] },
|
|
212
|
+
blue: { hex: '#0000FF', rgb: [0, 0, 255] },
|
|
213
|
+
green: { hex: '#00FF00', rgb: [0, 255, 0] },
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const Colors = enumeration('Colors', {
|
|
217
|
+
input,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
console.log(Colors.red.hex); // '#FF0000'
|
|
221
|
+
console.log(Colors.blue.rgb); // [0, 0, 255]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Custom Field Values
|
|
225
|
+
|
|
226
|
+
Extract custom values from enum items:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
type PageExtensions = { slug: string; title: string };
|
|
230
|
+
const input = {
|
|
231
|
+
home: { slug: '/', title: 'Home Page' },
|
|
232
|
+
about: { slug: '/about', title: 'About Us' },
|
|
233
|
+
contact: { slug: '/contact', title: 'Contact' },
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const Pages = enumeration('Pages', { input });
|
|
237
|
+
type Pages = Enumeration<typeof Pages>;
|
|
238
|
+
|
|
239
|
+
const slug = Pages.about.slug; // '/about'
|
|
240
|
+
|
|
241
|
+
// Get all slugs
|
|
242
|
+
const slugs = Pages.items().map(item => item.slug);
|
|
243
|
+
// Returns: ['/', '/about', '/contact']
|
|
244
|
+
|
|
245
|
+
// Filter out undefined values
|
|
246
|
+
const titles = Pages.items()
|
|
247
|
+
.map(item => item.title)
|
|
248
|
+
.filter(Boolean);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### React/Frontend Usage
|
|
252
|
+
|
|
253
|
+
Perfect for form selects and dropdowns:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
function ColorSelector() {
|
|
257
|
+
const [selected, setSelected] = useState(Colors.red);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<select
|
|
261
|
+
value={selected.value}
|
|
262
|
+
onChange={e => setSelected(Colors.fromValue(e.target.value))}
|
|
263
|
+
>
|
|
264
|
+
{Colors.items().map(item => (
|
|
265
|
+
<option key={item.value} value={item.value}>
|
|
266
|
+
{item.display}
|
|
267
|
+
</option>
|
|
268
|
+
))}
|
|
269
|
+
</select>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Serialization and Reviving (Transformations)
|
|
275
|
+
|
|
276
|
+
When sending data over the wire or persisting to a database, replace enum items with strings and revive them back.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import {
|
|
280
|
+
serializeSmartEnums,
|
|
281
|
+
reviveSmartEnums,
|
|
282
|
+
enumeration,
|
|
283
|
+
type Enumeration,
|
|
284
|
+
} from 'ts-smart-enum';
|
|
285
|
+
|
|
286
|
+
const statusInput = ['pending', 'active', 'completed'] as const;
|
|
287
|
+
const Status = enumeration('Status', { input: statusInput });
|
|
288
|
+
type Status = Enumeration<typeof Status>;
|
|
289
|
+
|
|
290
|
+
const colorInput = ['red', 'blue', 'green'] as const;
|
|
291
|
+
const Color = enumeration('Color', { input: colorInput });
|
|
292
|
+
type Color = Enumeration<typeof Color>;
|
|
293
|
+
|
|
294
|
+
const dto = {
|
|
295
|
+
id: '123',
|
|
296
|
+
status: Status.active,
|
|
297
|
+
favoriteColor: Color.red,
|
|
298
|
+
history: [Status.pending, Status.completed],
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Serialize enum items to self-describing objects
|
|
302
|
+
const wire = serializeSmartEnums(dto);
|
|
303
|
+
// Result: {
|
|
304
|
+
// id: '123',
|
|
305
|
+
// status: { __smart_enum_type: 'Status', value: 'ACTIVE' },
|
|
306
|
+
// favoriteColor: { __smart_enum_type: 'Color', value: 'RED' },
|
|
307
|
+
// history: [
|
|
308
|
+
// { __smart_enum_type: 'Status', value: 'PENDING' },
|
|
309
|
+
// { __smart_enum_type: 'Status', value: 'COMPLETED' }
|
|
310
|
+
// ]
|
|
311
|
+
// }
|
|
312
|
+
|
|
313
|
+
// Revive using registry
|
|
314
|
+
const revived = reviveSmartEnums(wire, { Status, Color });
|
|
315
|
+
// Result: {
|
|
316
|
+
// id: '123',
|
|
317
|
+
// status: Status.active, // Full enum item restored
|
|
318
|
+
// favoriteColor: Color.red, // Full enum item restored
|
|
319
|
+
// history: [Status.pending, Status.completed]
|
|
320
|
+
// }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Notes:
|
|
324
|
+
|
|
325
|
+
- All enums now require an `enumType` parameter for serialization/revival
|
|
326
|
+
- Serialization creates self-describing objects with `__smart_enum_type` and `value`
|
|
327
|
+
- Reviving uses a registry to map enum types back to their instances
|
|
328
|
+
- Cyclic references are preserved during serialization
|
|
329
|
+
- Use `as const` on the registry object for best type inference
|
|
330
|
+
|
|
331
|
+
## Transport Utilities
|
|
332
|
+
|
|
333
|
+
For API communication, use the transport utilities to serialize enums for sending over the wire and revive them on the receiving end.
|
|
334
|
+
|
|
335
|
+
### Basic Transport Usage
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { enumeration } from 'ts-smart-enum/core';
|
|
339
|
+
import {
|
|
340
|
+
initializeSmartEnumMappings,
|
|
341
|
+
serializeForTransport,
|
|
342
|
+
reviveAfterTransport,
|
|
343
|
+
} from 'ts-smart-enum';
|
|
344
|
+
|
|
345
|
+
// Create enums
|
|
346
|
+
const UserStatus = enumeration('UserStatus', {
|
|
347
|
+
input: ['pending', 'active', 'suspended'] as const,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const Priority = enumeration('Priority', {
|
|
351
|
+
input: ['low', 'medium', 'high', 'urgent'] as const,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Required once before calling reviveAfterTransport
|
|
355
|
+
initializeSmartEnumMappings({
|
|
356
|
+
enumRegistry: { UserStatus, Priority },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// API endpoint - serialize for sending
|
|
360
|
+
const apiData = {
|
|
361
|
+
user: {
|
|
362
|
+
id: '123',
|
|
363
|
+
status: UserStatus.active,
|
|
364
|
+
profile: {
|
|
365
|
+
priority: Priority.high,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
orders: [
|
|
369
|
+
{ id: 'o1', status: UserStatus.pending },
|
|
370
|
+
{ id: 'o2', status: UserStatus.active },
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Serialize for transport (creates self-describing objects)
|
|
375
|
+
const wireData = serializeForTransport(apiData);
|
|
376
|
+
// Result: {
|
|
377
|
+
// user: {
|
|
378
|
+
// id: '123',
|
|
379
|
+
// status: { __smart_enum_type: 'UserStatus', value: 'ACTIVE' },
|
|
380
|
+
// profile: {
|
|
381
|
+
// priority: { __smart_enum_type: 'Priority', value: 'HIGH' }
|
|
382
|
+
// }
|
|
383
|
+
// },
|
|
384
|
+
// orders: [
|
|
385
|
+
// { id: 'o1', status: { __smart_enum_type: 'UserStatus', value: 'PENDING' } },
|
|
386
|
+
// { id: 'o2', status: { __smart_enum_type: 'UserStatus', value: 'ACTIVE' } }
|
|
387
|
+
// ]
|
|
388
|
+
// }
|
|
389
|
+
|
|
390
|
+
// Client-side - revive after receiving
|
|
391
|
+
const revivedData = reviveAfterTransport(wireData);
|
|
392
|
+
// Result: Original apiData with full enum items restored
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Express.js Integration
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import express from 'express';
|
|
399
|
+
import {
|
|
400
|
+
initializeSmartEnumMappings,
|
|
401
|
+
serializeForTransport,
|
|
402
|
+
reviveAfterTransport,
|
|
403
|
+
} from 'ts-smart-enum';
|
|
404
|
+
|
|
405
|
+
const app = express();
|
|
406
|
+
|
|
407
|
+
initializeSmartEnumMappings({
|
|
408
|
+
enumRegistry: { UserStatus, Priority },
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Middleware to revive enums from incoming requests
|
|
412
|
+
app.use(express.json());
|
|
413
|
+
app.use((req, res, next) => {
|
|
414
|
+
if (req.body) {
|
|
415
|
+
req.body = reviveAfterTransport(req.body);
|
|
416
|
+
}
|
|
417
|
+
next();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// API endpoint
|
|
421
|
+
app.get('/api/users/:id', (req, res) => {
|
|
422
|
+
const user = getUserById(req.params.id);
|
|
423
|
+
|
|
424
|
+
// Serialize for response
|
|
425
|
+
res.json(serializeForTransport(user));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// POST endpoint
|
|
429
|
+
app.post('/api/users', (req, res) => {
|
|
430
|
+
// req.body is automatically revived with enum items
|
|
431
|
+
const { status, priority } = req.body;
|
|
432
|
+
|
|
433
|
+
// Use enum items directly
|
|
434
|
+
if (status === UserStatus.active) {
|
|
435
|
+
// Handle active user
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const newUser = createUser(req.body);
|
|
439
|
+
res.json(serializeForTransport(newUser));
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### React/Fetch Integration
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// API client with automatic enum handling
|
|
447
|
+
class ApiClient {
|
|
448
|
+
async get<T>(url: string): Promise<T> {
|
|
449
|
+
const response = await fetch(url);
|
|
450
|
+
const data = await response.json();
|
|
451
|
+
|
|
452
|
+
// Revive enums in response
|
|
453
|
+
return reviveAfterTransport(data) as T;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async post<T>(url: string, data: any): Promise<T> {
|
|
457
|
+
// Serialize enums for sending
|
|
458
|
+
const serializedData = serializeForTransport(data);
|
|
459
|
+
|
|
460
|
+
const response = await fetch(url, {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
463
|
+
body: JSON.stringify(serializedData),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const result = await response.json();
|
|
467
|
+
return reviveAfterTransport(result) as T;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Usage in React component
|
|
472
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
473
|
+
const [user, setUser] = useState<User | null>(null);
|
|
474
|
+
const apiClient = new ApiClient();
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
apiClient.get<User>(`/api/users/${userId}`).then(setUser);
|
|
478
|
+
}, [userId]);
|
|
479
|
+
|
|
480
|
+
const updateStatus = async (newStatus: typeof UserStatus.active) => {
|
|
481
|
+
const updatedUser = await apiClient.post<User>(`/api/users/${userId}`, {
|
|
482
|
+
status: newStatus, // Enum item sent directly
|
|
483
|
+
});
|
|
484
|
+
setUser(updatedUser);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<div>
|
|
489
|
+
<h2>{user?.name}</h2>
|
|
490
|
+
<p>Status: {user?.status.display}</p>
|
|
491
|
+
<button onClick={() => updateStatus(UserStatus.active)}>
|
|
492
|
+
Activate User
|
|
493
|
+
</button>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Database utilities
|
|
500
|
+
|
|
501
|
+
**Outbound serialization is generic:** `prepareForDatabase` walks your payload and replaces smart enum items with their `.value` strings. You can also bind `someItem.toPostgres()` for PostgreSQL drivers that honor that hook (outbound only; it does not revive reads).
|
|
502
|
+
|
|
503
|
+
**Inbound deserialization is not automatic:** a plain string column does not say which enum type it belongs to, and the same property name (e.g. `status`) is not unique across your schema. The previous **learned / global-registry database revival model has been removed**.
|
|
504
|
+
|
|
505
|
+
**Recommended approach:** at the repository or query boundary, pass **explicit metadata**: map column names (or full paths for nested JSON) to the **actual enum object** (anything with `tryFromValue`), not to type name strings.
|
|
506
|
+
|
|
507
|
+
### `reviveRowFromDatabase` (flat rows)
|
|
508
|
+
|
|
509
|
+
Shallow-clones the row. For each entry in `fieldEnumMapping`, if the value is a string, `tryFromValue` is called; on success the string is replaced by the enum item. `strict: true` throws `EnumRevivalError` when a mapped string is invalid; otherwise the raw value is kept.
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { enumeration } from 'ts-smart-enum/core';
|
|
513
|
+
import { reviveRowFromDatabase } from 'ts-smart-enum/database';
|
|
514
|
+
|
|
515
|
+
const UserStatus = enumeration('UserStatus', {
|
|
516
|
+
input: ['pending', 'active'] as const,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const row = { id: '1', status: 'ACTIVE' };
|
|
520
|
+
const revived = reviveRowFromDatabase(row, {
|
|
521
|
+
fieldEnumMapping: { status: UserStatus },
|
|
522
|
+
strict: true,
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### `revivePayloadFromDatabase` (explicit paths only)
|
|
527
|
+
|
|
528
|
+
Deep-clones with `structuredClone`, then revives only the paths you list (e.g. `status`, `profile.priority`, `items[].kind`). No leaf-name guessing and no registry.
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { revivePayloadFromDatabase } from 'ts-smart-enum/database';
|
|
532
|
+
|
|
533
|
+
const doc = await loadJsonColumn();
|
|
534
|
+
const out = revivePayloadFromDatabase(doc, {
|
|
535
|
+
pathEnumMapping: {
|
|
536
|
+
'user.status': UserStatus,
|
|
537
|
+
'lines[].kind': LineKind,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Transport vs database
|
|
543
|
+
|
|
544
|
+
`initializeSmartEnumMappings` / `reviveAfterTransport` are for **wire payloads** that include `__smart_enum_type`. They are exported from `ts-smart-enum/transport` (and the root package), not from the old database utilities.
|
|
545
|
+
|
|
546
|
+
## Logging
|
|
547
|
+
|
|
548
|
+
The library includes a flexible logging system that allows you to integrate with your preferred logging solution. By default, it uses console logging.
|
|
549
|
+
|
|
550
|
+
### Basic Usage
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { enumeration, initializeSmartEnumMappings } from 'ts-smart-enum';
|
|
554
|
+
|
|
555
|
+
// Console logging is enabled by default with 'error' level (minimal output)
|
|
556
|
+
initializeSmartEnumMappings({
|
|
557
|
+
enumRegistry: { UserStatus, Priority },
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Enable debug logging for development
|
|
561
|
+
initializeSmartEnumMappings({
|
|
562
|
+
enumRegistry: { UserStatus, Priority },
|
|
563
|
+
logLevel: 'debug',
|
|
564
|
+
});
|
|
565
|
+
// [ts-smart-enum:info] Initialized smart enum mappings { enumCount: 2, enumTypes: ['UserStatus', 'Priority'], logLevel: 'debug' }
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Log Level Configuration
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { initializeSmartEnumMappings, type LogLevel } from 'ts-smart-enum';
|
|
572
|
+
|
|
573
|
+
// Available log levels (default: 'error')
|
|
574
|
+
const logLevels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
|
|
575
|
+
|
|
576
|
+
// Production: minimal logging (errors only)
|
|
577
|
+
initializeSmartEnumMappings({
|
|
578
|
+
enumRegistry: { UserStatus, Priority },
|
|
579
|
+
logLevel: 'error', // default
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Development: verbose logging
|
|
583
|
+
initializeSmartEnumMappings({
|
|
584
|
+
enumRegistry: { UserStatus, Priority },
|
|
585
|
+
logLevel: 'debug',
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Custom logger with log level filtering
|
|
589
|
+
initializeSmartEnumMappings({
|
|
590
|
+
enumRegistry: { UserStatus, Priority },
|
|
591
|
+
logLevel: 'info',
|
|
592
|
+
logger: {
|
|
593
|
+
debug: (msg, ...args) => console.debug(`[my-app] ${msg}`, ...args),
|
|
594
|
+
info: (msg, ...args) => console.info(`[my-app] ${msg}`, ...args),
|
|
595
|
+
warn: (msg, ...args) => console.warn(`[my-app] ${msg}`, ...args),
|
|
596
|
+
error: (msg, ...args) => console.error(`[my-app] ${msg}`, ...args),
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Custom Logger Integration
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import { initializeSmartEnumMappings, type Logger } from 'ts-smart-enum';
|
|
605
|
+
import winston from 'winston';
|
|
606
|
+
|
|
607
|
+
// Create your logger
|
|
608
|
+
const logger = winston.createLogger({
|
|
609
|
+
level: 'info',
|
|
610
|
+
format: winston.format.json(),
|
|
611
|
+
transports: [new winston.transports.Console()],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Use custom logger with ts-smart-enum
|
|
615
|
+
initializeSmartEnumMappings({
|
|
616
|
+
enumRegistry: { UserStatus, Priority },
|
|
617
|
+
logLevel: 'debug',
|
|
618
|
+
logger: {
|
|
619
|
+
debug: (msg, ...args) => logger.debug(`[ts-smart-enum] ${msg}`, ...args),
|
|
620
|
+
info: (msg, ...args) => logger.info(`[ts-smart-enum] ${msg}`, ...args),
|
|
621
|
+
warn: (msg, ...args) => logger.warn(`[ts-smart-enum] ${msg}`, ...args),
|
|
622
|
+
error: (msg, ...args) => logger.error(`[ts-smart-enum] ${msg}`, ...args),
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Log Levels
|
|
628
|
+
|
|
629
|
+
- `**debug**`: Reserved for future verbose transport diagnostics
|
|
630
|
+
- `**info**`: General information (e.g. registry initialization)
|
|
631
|
+
- `**warn**`: Warning messages (missing configurations, failed operations)
|
|
632
|
+
- `**error**`: Error conditions (missing enum types, invalid data)
|
|
633
|
+
|
|
634
|
+
### Disabling Logging
|
|
635
|
+
|
|
636
|
+
To disable logging in production, you can set a no-op logger:
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { initializeSmartEnumMappings } from 'ts-smart-enum';
|
|
640
|
+
|
|
641
|
+
// Disable logging for production
|
|
642
|
+
initializeSmartEnumMappings({
|
|
643
|
+
enumRegistry: { UserStatus, Priority },
|
|
644
|
+
logLevel: 'error', // minimal logging
|
|
645
|
+
logger: {
|
|
646
|
+
debug: () => {},
|
|
647
|
+
info: () => {},
|
|
648
|
+
warn: () => {},
|
|
649
|
+
error: () => {},
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Logging events
|
|
655
|
+
|
|
656
|
+
- **Initialization**: When `initializeSmartEnumMappings` is called (transport registry)
|
|
657
|
+
- **Warnings / errors**: Emitted by other helpers when misconfigured (see JSDoc per API)
|
|
658
|
+
|
|
659
|
+
### Prisma Integration
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
import { PrismaClient } from '@prisma/client';
|
|
663
|
+
import {
|
|
664
|
+
prepareForDatabase,
|
|
665
|
+
reviveRowFromDatabase,
|
|
666
|
+
} from 'ts-smart-enum/database';
|
|
667
|
+
|
|
668
|
+
const prisma = new PrismaClient();
|
|
669
|
+
|
|
670
|
+
// Create user with enum data
|
|
671
|
+
async function createUser(userData: {
|
|
672
|
+
name: string;
|
|
673
|
+
status: typeof UserStatus.active;
|
|
674
|
+
priority: typeof Priority.high;
|
|
675
|
+
}) {
|
|
676
|
+
// Convert enums to strings for database
|
|
677
|
+
const dbData = prepareForDatabase(userData);
|
|
678
|
+
|
|
679
|
+
return prisma.user.create({
|
|
680
|
+
data: {
|
|
681
|
+
name: dbData.name,
|
|
682
|
+
status: dbData.status, // 'ACTIVE'
|
|
683
|
+
priority: dbData.priority, // 'HIGH'
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Get user and revive enums
|
|
689
|
+
async function getUser(id: string) {
|
|
690
|
+
const dbUser = await prisma.user.findUnique({
|
|
691
|
+
where: { id },
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
if (!dbUser) return null;
|
|
695
|
+
|
|
696
|
+
// Revive enums from database strings
|
|
697
|
+
return reviveRowFromDatabase(dbUser as Record<string, unknown>, {
|
|
698
|
+
fieldEnumMapping: {
|
|
699
|
+
status: UserStatus,
|
|
700
|
+
priority: Priority,
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Usage
|
|
706
|
+
const user = await getUser('123');
|
|
707
|
+
if (user) {
|
|
708
|
+
console.log(user.status.display); // "Active"
|
|
709
|
+
console.log(user.priority.value); // "HIGH"
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### TypeORM Integration
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
|
717
|
+
import {
|
|
718
|
+
prepareForDatabase,
|
|
719
|
+
reviveRowFromDatabase,
|
|
720
|
+
} from 'ts-smart-enum/database';
|
|
721
|
+
|
|
722
|
+
@Entity()
|
|
723
|
+
export class User {
|
|
724
|
+
@PrimaryGeneratedColumn()
|
|
725
|
+
id: number;
|
|
726
|
+
|
|
727
|
+
@Column()
|
|
728
|
+
name: string;
|
|
729
|
+
|
|
730
|
+
@Column()
|
|
731
|
+
status: string; // Stores enum value as string
|
|
732
|
+
|
|
733
|
+
@Column()
|
|
734
|
+
priority: string; // Stores enum value as string
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Repository with enum handling
|
|
738
|
+
export class UserRepository {
|
|
739
|
+
constructor(private repository: Repository<User>) {}
|
|
740
|
+
|
|
741
|
+
async save(userData: {
|
|
742
|
+
name: string;
|
|
743
|
+
status: typeof UserStatus.active;
|
|
744
|
+
priority: typeof Priority.high;
|
|
745
|
+
}) {
|
|
746
|
+
const dbData = prepareForDatabase(userData);
|
|
747
|
+
return this.repository.save(dbData);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async findById(id: number) {
|
|
751
|
+
const dbUser = await this.repository.findOne({ where: { id } });
|
|
752
|
+
if (!dbUser) return null;
|
|
753
|
+
|
|
754
|
+
return reviveRowFromDatabase(dbUser as Record<string, unknown>, {
|
|
755
|
+
fieldEnumMapping: {
|
|
756
|
+
status: UserStatus,
|
|
757
|
+
priority: Priority,
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Validation & Type Guards
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
function processStatus(statusValue: string) {
|
|
768
|
+
const status = Status.tryFromValue(statusValue);
|
|
769
|
+
|
|
770
|
+
if (!status) {
|
|
771
|
+
throw new Error(`Invalid status: ${statusValue}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Now status is typed as a valid enum item
|
|
775
|
+
switch (status.key) {
|
|
776
|
+
case 'pending':
|
|
777
|
+
// Handle pending
|
|
778
|
+
break;
|
|
779
|
+
case 'active':
|
|
780
|
+
// Handle active
|
|
781
|
+
break;
|
|
782
|
+
// TypeScript ensures all cases are handled
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## JSDoc and IDE Support
|
|
788
|
+
|
|
789
|
+
All public APIs are documented with JSDoc. In supported editors (VS Code, WebStorm, etc.) you get:
|
|
790
|
+
|
|
791
|
+
- **Hover tooltips** with descriptions and parameter/return types
|
|
792
|
+
- `**@example` blocks** on key functions (e.g. `enumeration`, `serializeSmartEnums`, `reviveSmartEnums`, `isSmartEnumItem`, `isSmartEnum`, `prepareForDatabase`, `reviveRowFromDatabase`, transport and database helpers) so you can copy-paste or follow patterns without leaving the editor
|
|
793
|
+
|
|
794
|
+
Import from the entry point you use (`ts-smart-enum`, `ts-smart-enum/core`, `ts-smart-enum/transport`, or `ts-smart-enum/database`) and hover over symbols to see the inline docs and examples.
|
|
795
|
+
|
|
796
|
+
## API Reference
|
|
797
|
+
|
|
798
|
+
### `enumeration(enumType, props)`
|
|
799
|
+
|
|
800
|
+
Main factory function for creating Smart Enums.
|
|
801
|
+
|
|
802
|
+
**Parameters:**
|
|
803
|
+
|
|
804
|
+
- `enumType` - String identifier for the enum type (required for serialization/revival)
|
|
805
|
+
- `props.input` - Array of strings, object with BaseEnum values, or existing enum
|
|
806
|
+
- `props.propertyAutoFormatters` - Optional custom formatters for auto-generated properties
|
|
807
|
+
|
|
808
|
+
**Returns:** An enum object with all items as properties plus extension methods
|
|
809
|
+
|
|
810
|
+
### Extension Methods
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
| Method | Description | Returns |
|
|
814
|
+
| --------------------- | --------------------------------------- | ------------------------- |
|
|
815
|
+
| `fromValue(value)` | Get item by value (throws if not found) | `Enumeration` |
|
|
816
|
+
| `tryFromValue(value)` | Get item by value (safe) | `Enumeration | undefined` |
|
|
817
|
+
| `fromKey(key)` | Get item by key (throws if not found) | `Enumeration` |
|
|
818
|
+
| `tryFromKey(key)` | Get item by key (safe) | `Enumeration | undefined` |
|
|
819
|
+
| `items()` | Get all items as array | `Enumeration[]` |
|
|
820
|
+
| `values()` | Get all values | `string[]` |
|
|
821
|
+
| `keys()` | Get all keys | `string[]` |
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
## Migration Guide
|
|
825
|
+
|
|
826
|
+
### From Plain Objects
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
// Before
|
|
830
|
+
const COLORS = {
|
|
831
|
+
RED: 'red',
|
|
832
|
+
BLUE: 'blue',
|
|
833
|
+
GREEN: 'green',
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// After
|
|
837
|
+
const Colors = enumeration('Colors', {
|
|
838
|
+
input: ['red', 'blue', 'green'] as const,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Usage changes from COLORS.RED to Colors.red.value
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### From String Unions
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
// Before
|
|
848
|
+
type Status = 'pending' | 'active' | 'completed';
|
|
849
|
+
|
|
850
|
+
// After
|
|
851
|
+
const Status = enumeration('Status', {
|
|
852
|
+
input: ['pending', 'active', 'completed'] as const,
|
|
853
|
+
});
|
|
854
|
+
type Status = Enumeration<typeof Status>;
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
## Best Practices
|
|
858
|
+
|
|
859
|
+
1. **Always use `as const`** for array inputs to preserve literal types
|
|
860
|
+
2. **Always provide `enumType`** parameter for serialization/revival support
|
|
861
|
+
3. **Use `tryFrom`* methods** when dealing with external data
|
|
862
|
+
4. **Leverage filtering** to hide deprecated items from users
|
|
863
|
+
5. **Add custom properties** for domain-specific needs
|
|
864
|
+
|
|
865
|
+
## Contributing
|
|
866
|
+
|
|
867
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
868
|
+
|
|
869
|
+
## License
|
|
870
|
+
|
|
871
|
+
MIT
|
|
872
|
+
|
|
873
|
+
## Support
|
|
874
|
+
|
|
875
|
+
- 📧 Email: [harik.raif@gmail.com](mailto:harik.raif@gmail.com)
|
|
876
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/reharik/smart-enums/issues)
|
|
877
|
+
- 📖 Docs: [Full Documentation](https://docs.reharik.com/ts-smart-enum)
|
|
878
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reharik/smart-enum",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"clean": "rm -rf dist",
|
|
38
38
|
"build": "npm run clean && tsup",
|
|
39
|
-
"prepublishOnly": "npm run build",
|
|
39
|
+
"prepublishOnly": "node -e \"require('fs').copyFileSync('../../README.md', 'README.md')\" && npm run build",
|
|
40
40
|
"test": "jest --config jest.config.cjs",
|
|
41
41
|
"dance": "npm run build && npm run test"
|
|
42
42
|
},
|