@reharik/smart-enum 0.3.0 → 0.3.2
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 +230 -728
- package/dist/{chunk-NIAUHARU.js → chunk-3RPLSRWV.js} +2 -2
- package/dist/{chunk-F25J55WO.js → chunk-CZ6TMH26.js} +2 -2
- package/dist/{chunk-QLIBJ3CI.js → chunk-SOZ6HB6K.js} +5 -5
- package/dist/chunk-SOZ6HB6K.js.map +1 -0
- package/dist/{chunk-QTB7HI5X.js → chunk-TXDI7ASS.js} +2 -2
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/database.cjs.map +1 -1
- package/dist/database.d.cts +2 -2
- package/dist/database.d.ts +2 -2
- package/dist/database.js +2 -2
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/{transformation-Ds4CZAQM.d.cts → transformation-CVR7F8nY.d.cts} +21 -3
- package/dist/{transformation-Ds4CZAQM.d.ts → transformation-CVR7F8nY.d.ts} +21 -3
- package/dist/transport.cjs +4 -4
- package/dist/transport.cjs.map +1 -1
- package/dist/transport.d.cts +2 -2
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +2 -2
- package/package.json +15 -2
- package/dist/chunk-QLIBJ3CI.js.map +0 -1
- /package/dist/{chunk-NIAUHARU.js.map → chunk-3RPLSRWV.js.map} +0 -0
- /package/dist/{chunk-F25J55WO.js.map → chunk-CZ6TMH26.js.map} +0 -0
- /package/dist/{chunk-QTB7HI5X.js.map → chunk-TXDI7ASS.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,878 +1,380 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @reharik/smart-enum
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe, feature-rich enumerations for TypeScript. Every enum member is a frozen object with a key, a wire value, a display string, an index, and whatever custom fields you need — plus lookup, iteration, serialization, and database revival built in.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why not just use TypeScript enums?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
TypeScript's built-in `enum` gives you a name-to-value mapping and nothing else. You can't iterate members, attach metadata, look up by value at runtime, or serialize/revive across a network boundary without writing boilerplate every time.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The common workarounds each solve one piece:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- **Plain objects** (`{ ACTIVE: 'active' }`) — no iteration, no type narrowing, no metadata, annoying text duplication.
|
|
12
|
+
- **String unions** (`type Status = 'active' | 'inactive'`) — compile-time only. No runtime lookup, no `.display`, no `.items()`.
|
|
13
|
+
- **Arrays** (`['active', 'inactive'] as const`) — iterable, but no keyed access or metadata.
|
|
14
|
+
- **Constants** (Constants const ME = "me") — scattered and hard to iterate
|
|
12
15
|
|
|
13
|
-
|
|
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:
|
|
16
|
+
Smart Enums give you all of it in one construct:
|
|
23
17
|
|
|
24
18
|
```typescript
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
import { enumeration, type Enumeration } from '@reharik/smart-enum';
|
|
20
|
+
|
|
21
|
+
const Status = enumeration('Status', {
|
|
22
|
+
input: ['pending', 'active', 'completed'] as const,
|
|
27
23
|
});
|
|
24
|
+
type Status = Enumeration<typeof Status>;
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Colors.blue; // { key: "blue", value: "BLUE", display: "Blue", index: 1 }
|
|
26
|
+
Status.active;
|
|
27
|
+
// { key: 'active', value: 'ACTIVE', display: 'Active', index: 1 }
|
|
32
28
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
Status.fromValue('ACTIVE'); // Status.active
|
|
30
|
+
Status.tryFromValue('NOPE'); // undefined
|
|
31
|
+
Status.items(); // all members as a frozen array
|
|
32
|
+
Status.values(); // ['PENDING', 'ACTIVE', 'COMPLETED']
|
|
33
|
+
Status.keys(); // ['pending', 'active', 'completed']
|
|
37
34
|
```
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
Type safety works the way you'd expect — a function that takes `Status` won't accept a member from a different enum, even if the shapes look similar.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
40
39
|
|
|
41
40
|
```bash
|
|
42
|
-
npm install
|
|
43
|
-
# or
|
|
44
|
-
yarn add ts-smart-enum
|
|
45
|
-
# or
|
|
46
|
-
pnpm add ts-smart-enum
|
|
41
|
+
npm install @reharik/smart-enum
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
|
|
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.
|
|
44
|
+
## Creating enums
|
|
52
45
|
|
|
53
|
-
|
|
46
|
+
### From an array
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
The simplest form. Keys are the array values, wire values are auto-derived as `CONSTANT_CASE`, display strings as `Title Case`.
|
|
56
49
|
|
|
57
50
|
```typescript
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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';
|
|
51
|
+
const Color = enumeration('Color', {
|
|
52
|
+
input: ['red', 'blue', 'green'] as const,
|
|
53
|
+
});
|
|
54
|
+
type Color = Enumeration<typeof Color>;
|
|
74
55
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
prepareForDatabase,
|
|
80
|
-
} from 'ts-smart-enum';
|
|
56
|
+
Color.red.key; // 'red'
|
|
57
|
+
Color.red.value; // 'RED'
|
|
58
|
+
Color.red.display; // 'Red'
|
|
59
|
+
Color.red.index; // 0
|
|
81
60
|
```
|
|
82
61
|
|
|
83
|
-
**
|
|
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)
|
|
62
|
+
> **`as const` is required** on array inputs. Without it, TypeScript widens the type to `string[]` and you lose literal inference on keys and values.
|
|
89
63
|
|
|
90
|
-
###
|
|
64
|
+
### From an object
|
|
91
65
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
## Quick Start
|
|
95
|
-
|
|
96
|
-
### Creating Enums from Arrays
|
|
97
|
-
|
|
98
|
-
The simplest way to create a Smart Enum:
|
|
66
|
+
When you need custom values, display strings, or extra fields per member:
|
|
99
67
|
|
|
100
68
|
```typescript
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 });
|
|
69
|
+
const Priority = enumeration('Priority', {
|
|
70
|
+
input: {
|
|
71
|
+
low: { display: 'Low Priority' },
|
|
72
|
+
medium: { display: 'Medium Priority' },
|
|
73
|
+
high: { value: 'P1', display: 'High Priority' },
|
|
74
|
+
urgent: { value: 'P0', display: 'Urgent!!!' },
|
|
75
|
+
} as const,
|
|
76
|
+
});
|
|
128
77
|
type Priority = Enumeration<typeof Priority>;
|
|
129
78
|
|
|
130
|
-
|
|
79
|
+
Priority.high.value; // 'P1' (explicit)
|
|
80
|
+
Priority.low.value; // 'LOW' (auto-derived from key)
|
|
81
|
+
Priority.urgent.display; // 'Urgent!!!'
|
|
131
82
|
```
|
|
132
83
|
|
|
133
|
-
|
|
84
|
+
If you omit `value`, it's derived from the key via `constantCase`. If you omit `display`, it's derived via `capitalCase`. You can override either or both per member.
|
|
134
85
|
|
|
135
|
-
###
|
|
86
|
+
### Custom fields
|
|
136
87
|
|
|
137
|
-
|
|
88
|
+
Any extra properties you put on a member are preserved with full type inference:
|
|
138
89
|
|
|
139
90
|
```typescript
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
91
|
+
const AppError = enumeration('AppError', {
|
|
92
|
+
input: {
|
|
93
|
+
notFound: { source: 'api', httpStatus: 404 },
|
|
94
|
+
unauthorized: { source: 'auth', httpStatus: 401 },
|
|
95
|
+
serverError: { source: 'api', httpStatus: 500 },
|
|
96
|
+
} as const,
|
|
97
|
+
});
|
|
158
98
|
|
|
159
|
-
|
|
99
|
+
AppError.notFound.source; // 'api' (literal type)
|
|
100
|
+
AppError.notFound.httpStatus; // 404 (literal type)
|
|
160
101
|
|
|
161
|
-
|
|
162
|
-
|
|
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"
|
|
102
|
+
// You can Extract by custom fields:
|
|
103
|
+
type ApiErrors = Extract<Enumeration<typeof AppError>, { source: 'api' }>;
|
|
175
104
|
```
|
|
176
105
|
|
|
177
|
-
|
|
106
|
+
### Custom auto-formatters
|
|
178
107
|
|
|
179
|
-
|
|
108
|
+
Override how `value`, `display`, or any auto-generated property is derived from the key:
|
|
180
109
|
|
|
181
110
|
```typescript
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
111
|
+
const Routes = enumeration('Routes', {
|
|
112
|
+
input: ['userProfile', 'adminDashboard'] as const,
|
|
113
|
+
propertyAutoFormatters: [
|
|
114
|
+
{ key: 'value', format: k => `/${k}` },
|
|
115
|
+
{ key: 'slug', format: k => k.replace(/([A-Z])/g, '-$1').toLowerCase() },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
187
118
|
|
|
188
|
-
//
|
|
189
|
-
|
|
119
|
+
Routes.userProfile.value; // '/userProfile'
|
|
120
|
+
Routes.userProfile.slug; // 'user-profile'
|
|
190
121
|
```
|
|
191
122
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
// Get all values
|
|
196
|
-
const values = Colors.values(); // ['RED', 'BLUE', 'GREEN']
|
|
123
|
+
## Lookup methods
|
|
197
124
|
|
|
198
|
-
|
|
199
|
-
const keys = Colors.keys(); // ['red', 'blue', 'green']
|
|
125
|
+
Every enum object has these methods. Return types are fully narrowed to the enum's member union.
|
|
200
126
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
127
|
+
| Method | Description |
|
|
128
|
+
| --------------------- | ----------------------------------------------------- |
|
|
129
|
+
| `fromValue(value)` | Find by wire value. Throws if not found. |
|
|
130
|
+
| `tryFromValue(value)` | Find by wire value. Returns `undefined` if not found. |
|
|
131
|
+
| `fromKey(key)` | Find by key. Throws if not found. |
|
|
132
|
+
| `tryFromKey(key)` | Find by key. Returns `undefined` if not found. |
|
|
133
|
+
| `items()` | All members as a frozen array. |
|
|
134
|
+
| `values()` | All wire values as an array. |
|
|
135
|
+
| `keys()` | All keys as an array. |
|
|
204
136
|
|
|
205
|
-
##
|
|
137
|
+
## Subsetting by a custom field
|
|
206
138
|
|
|
207
|
-
|
|
139
|
+
Filter an enum down to members matching a property value. The result is a new enum-like object with its own `fromValue`, `items`, etc., scoped to the subset:
|
|
208
140
|
|
|
209
141
|
```typescript
|
|
210
|
-
|
|
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
|
-
};
|
|
142
|
+
import { getSubsetByProp, subsetByProp } from '@reharik/smart-enum';
|
|
215
143
|
|
|
216
|
-
const
|
|
217
|
-
input,
|
|
218
|
-
});
|
|
144
|
+
const apiErrors = getSubsetByProp(AppError, 'source', 'api' as const);
|
|
219
145
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
146
|
+
apiErrors.notFound; // same object as AppError.notFound
|
|
147
|
+
apiErrors.items(); // only api-source members
|
|
148
|
+
apiErrors.fromValue('500'); // works, scoped to subset
|
|
149
|
+
// apiErrors.unauthorized → not present (source is 'auth')
|
|
223
150
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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);
|
|
151
|
+
// Curried form:
|
|
152
|
+
const bySource = subsetByProp('source');
|
|
153
|
+
const authErrors = bySource(AppError, 'auth' as const);
|
|
249
154
|
```
|
|
250
155
|
|
|
251
|
-
|
|
156
|
+
## Tree-shaking
|
|
252
157
|
|
|
253
|
-
|
|
158
|
+
The package has multiple entry points so you only pay for what you import:
|
|
254
159
|
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
```
|
|
160
|
+
```typescript
|
|
161
|
+
// Core only — enumeration + type guards + subset helpers (~149 bytes)
|
|
162
|
+
import { enumeration } from '@reharik/smart-enum/core';
|
|
273
163
|
|
|
274
|
-
|
|
164
|
+
// Core + transport serialization/revival (~406 bytes)
|
|
165
|
+
import { serializeForTransport } from '@reharik/smart-enum/transport';
|
|
275
166
|
|
|
276
|
-
|
|
167
|
+
// Core + database serialization/revival (~379 bytes)
|
|
168
|
+
import { prepareForDatabase } from '@reharik/smart-enum/database';
|
|
277
169
|
|
|
278
|
-
|
|
170
|
+
// Everything (~598 bytes)
|
|
279
171
|
import {
|
|
280
|
-
serializeSmartEnums,
|
|
281
|
-
reviveSmartEnums,
|
|
282
172
|
enumeration,
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
// }
|
|
173
|
+
serializeSmartEnums,
|
|
174
|
+
prepareForDatabase,
|
|
175
|
+
} from '@reharik/smart-enum';
|
|
321
176
|
```
|
|
322
177
|
|
|
323
|
-
|
|
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.
|
|
178
|
+
## Serialization and transport
|
|
334
179
|
|
|
335
|
-
|
|
180
|
+
Smart enum items serialize to self-describing `{ __smart_enum_type, value }` objects for JSON transport. On the receiving end, a registry maps them back to live enum instances.
|
|
336
181
|
|
|
337
182
|
```typescript
|
|
338
|
-
import {
|
|
339
|
-
import {
|
|
340
|
-
initializeSmartEnumMappings,
|
|
341
|
-
serializeForTransport,
|
|
342
|
-
reviveAfterTransport,
|
|
343
|
-
} from 'ts-smart-enum';
|
|
183
|
+
import { serializeSmartEnums, reviveSmartEnums } from '@reharik/smart-enum';
|
|
344
184
|
|
|
345
|
-
|
|
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
|
-
});
|
|
185
|
+
const dto = { id: '1', status: Status.active, color: Color.red };
|
|
353
186
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
};
|
|
187
|
+
// Serialize
|
|
188
|
+
const wire = serializeSmartEnums(dto);
|
|
189
|
+
// { id: '1',
|
|
190
|
+
// status: { __smart_enum_type: 'Status', value: 'ACTIVE' },
|
|
191
|
+
// color: { __smart_enum_type: 'Color', value: 'RED' } }
|
|
373
192
|
|
|
374
|
-
//
|
|
375
|
-
const
|
|
376
|
-
//
|
|
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
|
|
193
|
+
// Revive
|
|
194
|
+
const revived = reviveSmartEnums(wire, { Status, Color });
|
|
195
|
+
// revived.status === Status.active ✓
|
|
393
196
|
```
|
|
394
197
|
|
|
395
|
-
###
|
|
198
|
+
### Global transport registry
|
|
199
|
+
|
|
200
|
+
For app-wide setup (e.g. Express middleware), register all enums once at startup:
|
|
396
201
|
|
|
397
202
|
```typescript
|
|
398
|
-
import express from 'express';
|
|
399
203
|
import {
|
|
400
204
|
initializeSmartEnumMappings,
|
|
401
205
|
serializeForTransport,
|
|
402
206
|
reviveAfterTransport,
|
|
403
|
-
} from '
|
|
404
|
-
|
|
405
|
-
const app = express();
|
|
207
|
+
} from '@reharik/smart-enum/transport';
|
|
406
208
|
|
|
209
|
+
// At startup
|
|
407
210
|
initializeSmartEnumMappings({
|
|
408
|
-
enumRegistry: {
|
|
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));
|
|
211
|
+
enumRegistry: { Status, Priority, Color },
|
|
440
212
|
});
|
|
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
213
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
}
|
|
214
|
+
// In a handler
|
|
215
|
+
const wire = serializeForTransport(responseData);
|
|
216
|
+
const revived = reviveAfterTransport(requestBody);
|
|
497
217
|
```
|
|
498
218
|
|
|
499
219
|
## Database utilities
|
|
500
220
|
|
|
501
|
-
**Outbound
|
|
221
|
+
**Outbound:** `prepareForDatabase` recursively replaces enum items with their `.value` strings. Each item also has a `.toPostgres()` method for PostgreSQL drivers that honor it.
|
|
502
222
|
|
|
503
|
-
**Inbound
|
|
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.
|
|
223
|
+
**Inbound:** Database columns are plain strings — they don't carry type information. Revival requires you to declare which columns map to which enums.
|
|
510
224
|
|
|
511
225
|
```typescript
|
|
512
|
-
import {
|
|
513
|
-
|
|
226
|
+
import {
|
|
227
|
+
prepareForDatabase,
|
|
228
|
+
reviveRowFromDatabase,
|
|
229
|
+
revivePayloadFromDatabase,
|
|
230
|
+
} from '@reharik/smart-enum/database';
|
|
514
231
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
232
|
+
// Write
|
|
233
|
+
const dbRow = prepareForDatabase({ name: 'Alice', status: Status.active });
|
|
234
|
+
// { name: 'Alice', status: 'ACTIVE' }
|
|
518
235
|
|
|
519
|
-
|
|
236
|
+
// Read — flat row
|
|
520
237
|
const revived = reviveRowFromDatabase(row, {
|
|
521
|
-
fieldEnumMapping: { status:
|
|
522
|
-
strict: true,
|
|
238
|
+
fieldEnumMapping: { status: Status, priority: Priority },
|
|
239
|
+
strict: true, // throw on unknown values instead of keeping raw string
|
|
523
240
|
});
|
|
524
|
-
```
|
|
525
241
|
|
|
526
|
-
|
|
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, {
|
|
242
|
+
// Read — nested payload (e.g. JSONB column)
|
|
243
|
+
const doc = revivePayloadFromDatabase(payload, {
|
|
535
244
|
pathEnumMapping: {
|
|
536
|
-
'user.status':
|
|
537
|
-
'
|
|
245
|
+
'user.status': Status,
|
|
246
|
+
'items[].kind': ItemKind,
|
|
538
247
|
},
|
|
539
248
|
});
|
|
540
249
|
```
|
|
541
250
|
|
|
542
|
-
|
|
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.
|
|
251
|
+
`strict: true` throws `EnumRevivalError` when a mapped string doesn't match any member. Without it, unrecognized values are left as-is.
|
|
549
252
|
|
|
550
|
-
|
|
253
|
+
## Type guards
|
|
551
254
|
|
|
552
255
|
```typescript
|
|
553
|
-
import {
|
|
256
|
+
import { isSmartEnumItem, isSmartEnum } from '@reharik/smart-enum';
|
|
554
257
|
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
enumRegistry: { UserStatus, Priority },
|
|
558
|
-
});
|
|
258
|
+
isSmartEnumItem(Status.active); // true
|
|
259
|
+
isSmartEnumItem({ key: 'x' }); // false (plain object)
|
|
559
260
|
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
enumRegistry: { UserStatus, Priority },
|
|
563
|
-
logLevel: 'debug',
|
|
564
|
-
});
|
|
565
|
-
// [ts-smart-enum:info] Initialized smart enum mappings { enumCount: 2, enumTypes: ['UserStatus', 'Priority'], logLevel: 'debug' }
|
|
261
|
+
isSmartEnum(Status); // true (the enum object)
|
|
262
|
+
isSmartEnum(Status.active); // false (a member, not the enum)
|
|
566
263
|
```
|
|
567
264
|
|
|
568
|
-
|
|
265
|
+
## GraphQL integration
|
|
569
266
|
|
|
570
|
-
|
|
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
|
-
});
|
|
267
|
+
### Server-side: resolvers returning smart-enum instances
|
|
581
268
|
|
|
582
|
-
|
|
583
|
-
initializeSmartEnumMappings({
|
|
584
|
-
enumRegistry: { UserStatus, Priority },
|
|
585
|
-
logLevel: 'debug',
|
|
586
|
-
});
|
|
269
|
+
Out of the box, if a GraphQL resolver returns a smart-enum item, the default `serialize` on `GraphQLEnumType` doesn't know how to extract the string value — it just passes the object through, which breaks the response.
|
|
587
270
|
|
|
588
|
-
|
|
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
|
|
271
|
+
`patchSchemaEnumSerializers` walks your executable schema at startup and patches every `GraphQLEnumType.serialize` to call `.value` on smart-enum instances. After that, resolvers can return smart-enum items directly:
|
|
602
272
|
|
|
603
273
|
```typescript
|
|
604
|
-
import {
|
|
605
|
-
import
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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),
|
|
274
|
+
import { patchSchemaEnumSerializers } from '@reharik/smart-enum';
|
|
275
|
+
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
276
|
+
|
|
277
|
+
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
|
278
|
+
|
|
279
|
+
// Call once at startup, after schema construction
|
|
280
|
+
patchSchemaEnumSerializers(schema);
|
|
281
|
+
|
|
282
|
+
// Now resolvers can return smart-enum items directly:
|
|
283
|
+
const resolvers = {
|
|
284
|
+
Query: {
|
|
285
|
+
order: () => ({
|
|
286
|
+
id: '1',
|
|
287
|
+
status: PaymentStatus.paid, // no .value needed
|
|
288
|
+
type: OrderType.online,
|
|
289
|
+
}),
|
|
623
290
|
},
|
|
624
|
-
}
|
|
291
|
+
};
|
|
625
292
|
```
|
|
626
293
|
|
|
627
|
-
|
|
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)
|
|
294
|
+
The patched `serialize` does `val?.value ?? val` — smart-enum items return their `.value` string, and raw strings pass through unchanged. This means you can adopt it incrementally without breaking existing resolvers that already return strings.
|
|
633
295
|
|
|
634
|
-
###
|
|
296
|
+
### Client-side: Apollo cache rehydration
|
|
635
297
|
|
|
636
|
-
|
|
298
|
+
The [`@reharik/graphql-codegen-smart-enum`](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) package includes a second codegen plugin that generates Apollo `typePolicies` with `read` functions for every enum field. Spread the output into your `InMemoryCache` and components receive smart-enum instances from queries instead of raw strings:
|
|
637
299
|
|
|
638
300
|
```typescript
|
|
639
|
-
import {
|
|
301
|
+
import { InMemoryCache } from '@apollo/client';
|
|
302
|
+
import { smartEnumTypePolicies } from './generated/graphql-smart-enum-type-policies';
|
|
640
303
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
logLevel: 'error', // minimal logging
|
|
645
|
-
logger: {
|
|
646
|
-
debug: () => {},
|
|
647
|
-
info: () => {},
|
|
648
|
-
warn: () => {},
|
|
649
|
-
error: () => {},
|
|
304
|
+
const cache = new InMemoryCache({
|
|
305
|
+
typePolicies: {
|
|
306
|
+
...smartEnumTypePolicies,
|
|
650
307
|
},
|
|
651
308
|
});
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
### Logging events
|
|
655
309
|
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
}
|
|
310
|
+
// In a component:
|
|
311
|
+
const { data } = useQuery(GET_ORDER);
|
|
312
|
+
data.order.status.display; // 'Paid' — not just 'PAID'
|
|
711
313
|
```
|
|
712
314
|
|
|
713
|
-
|
|
315
|
+
See the [`type-policies` plugin README](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) for setup details.
|
|
714
316
|
|
|
715
|
-
|
|
716
|
-
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
|
717
|
-
import {
|
|
718
|
-
prepareForDatabase,
|
|
719
|
-
reviveRowFromDatabase,
|
|
720
|
-
} from 'ts-smart-enum/database';
|
|
317
|
+
## React
|
|
721
318
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
|
319
|
+
```tsx
|
|
320
|
+
function StatusPicker() {
|
|
321
|
+
const [selected, setSelected] = useState(Status.pending);
|
|
765
322
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
break;
|
|
779
|
-
case 'active':
|
|
780
|
-
// Handle active
|
|
781
|
-
break;
|
|
782
|
-
// TypeScript ensures all cases are handled
|
|
783
|
-
}
|
|
323
|
+
return (
|
|
324
|
+
<select
|
|
325
|
+
value={selected.value}
|
|
326
|
+
onChange={e => setSelected(Status.fromValue(e.target.value))}
|
|
327
|
+
>
|
|
328
|
+
{Status.items().map(item => (
|
|
329
|
+
<option key={item.value} value={item.value}>
|
|
330
|
+
{item.display}
|
|
331
|
+
</option>
|
|
332
|
+
))}
|
|
333
|
+
</select>
|
|
334
|
+
);
|
|
784
335
|
}
|
|
785
336
|
```
|
|
786
337
|
|
|
787
|
-
##
|
|
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:**
|
|
338
|
+
## The `enumType` string
|
|
803
339
|
|
|
804
|
-
|
|
805
|
-
- `props.input` - Array of strings, object with BaseEnum values, or existing enum
|
|
806
|
-
- `props.propertyAutoFormatters` - Optional custom formatters for auto-generated properties
|
|
340
|
+
The first argument to `enumeration()` is a string name used for serialization identity (`__smart_enum_type`) and `toJSON()`. It must be unique across your app and consistent between serialization and revival.
|
|
807
341
|
|
|
808
|
-
|
|
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
|
|
342
|
+
For hand-authored enums, keep it matching the variable name by convention:
|
|
827
343
|
|
|
828
344
|
```typescript
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
BLUE: 'blue',
|
|
833
|
-
GREEN: 'green',
|
|
834
|
-
};
|
|
345
|
+
const Status = enumeration('Status', { input: ... });
|
|
346
|
+
// ^^^^^^ ^^^^^^ ← keep these in sync
|
|
347
|
+
```
|
|
835
348
|
|
|
836
|
-
|
|
837
|
-
const Colors = enumeration('Colors', {
|
|
838
|
-
input: ['red', 'blue', 'green'] as const,
|
|
839
|
-
});
|
|
349
|
+
If you use the [`@reharik/graphql-codegen-smart-enum`](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) codegen plugin, this is handled automatically — the GraphQL enum type name becomes the `enumType` string.
|
|
840
350
|
|
|
841
|
-
|
|
842
|
-
```
|
|
351
|
+
## Logging
|
|
843
352
|
|
|
844
|
-
|
|
353
|
+
Transport initialization supports pluggable logging:
|
|
845
354
|
|
|
846
355
|
```typescript
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
356
|
+
initializeSmartEnumMappings({
|
|
357
|
+
enumRegistry: { Status, Priority },
|
|
358
|
+
logLevel: 'debug', // 'debug' | 'info' | 'warn' | 'error'
|
|
359
|
+
logger: {
|
|
360
|
+
debug: (msg, ...args) => myLogger.debug(msg, ...args),
|
|
361
|
+
info: (msg, ...args) => myLogger.info(msg, ...args),
|
|
362
|
+
warn: (msg, ...args) => myLogger.warn(msg, ...args),
|
|
363
|
+
error: (msg, ...args) => myLogger.error(msg, ...args),
|
|
364
|
+
},
|
|
853
365
|
});
|
|
854
|
-
type Status = Enumeration<typeof Status>;
|
|
855
366
|
```
|
|
856
367
|
|
|
857
|
-
|
|
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
|
|
368
|
+
Default is `console` at `'error'` level.
|
|
864
369
|
|
|
865
|
-
##
|
|
370
|
+
## Related packages
|
|
866
371
|
|
|
867
|
-
|
|
372
|
+
| Package | Purpose |
|
|
373
|
+
| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
|
|
374
|
+
| [`@reharik/smart-enum-knex`](https://www.npmjs.com/package/@reharik/smart-enum-knex) | Knex query-level enum revival via `postProcessResponse` |
|
|
375
|
+
| [`@reharik/graphql-codegen-smart-enum`](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) | Generate smart-enum definitions from GraphQL schema enums |
|
|
376
|
+
| [`@reharik/graphql-codegen-smart-enum/type-policies`](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) | Generate Apollo `typePolicies` for client-side enum rehydration |
|
|
868
377
|
|
|
869
378
|
## License
|
|
870
379
|
|
|
871
380
|
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
|
-
|