@reharik/smart-enum 0.3.1 → 0.3.3

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.
Files changed (35) hide show
  1. package/README.md +230 -728
  2. package/dist/{chunk-YEU2HCVA.js → chunk-3RPLSRWV.js} +2 -2
  3. package/dist/{chunk-RO2L7N2F.js → chunk-CZ6TMH26.js} +2 -2
  4. package/dist/{chunk-PUK5FOIE.js → chunk-SOZ6HB6K.js} +5 -5
  5. package/dist/{chunk-PUK5FOIE.js.map → chunk-SOZ6HB6K.js.map} +1 -1
  6. package/dist/{chunk-3N6NMQCD.js → chunk-TXDI7ASS.js} +2 -2
  7. package/dist/core.d.cts +2 -2
  8. package/dist/core.d.ts +2 -2
  9. package/dist/core.js +2 -2
  10. package/dist/database.cjs.map +1 -1
  11. package/dist/database.d.cts +2 -2
  12. package/dist/database.d.ts +2 -2
  13. package/dist/database.js +2 -2
  14. package/dist/graphql.cjs +46 -0
  15. package/dist/graphql.cjs.map +1 -0
  16. package/dist/graphql.d.cts +5 -0
  17. package/dist/graphql.d.ts +5 -0
  18. package/dist/graphql.js +19 -0
  19. package/dist/graphql.js.map +1 -0
  20. package/dist/index.cjs +4 -4
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +4 -4
  25. package/dist/{transformation-BPyoxZyV.d.cts → transformation-CVR7F8nY.d.cts} +1 -1
  26. package/dist/{transformation-BPyoxZyV.d.ts → transformation-CVR7F8nY.d.ts} +1 -1
  27. package/dist/transport.cjs +4 -4
  28. package/dist/transport.cjs.map +1 -1
  29. package/dist/transport.d.cts +2 -2
  30. package/dist/transport.d.ts +2 -2
  31. package/dist/transport.js +2 -2
  32. package/package.json +15 -2
  33. /package/dist/{chunk-YEU2HCVA.js.map → chunk-3RPLSRWV.js.map} +0 -0
  34. /package/dist/{chunk-RO2L7N2F.js.map → chunk-CZ6TMH26.js.map} +0 -0
  35. /package/dist/{chunk-3N6NMQCD.js.map → chunk-TXDI7ASS.js.map} +0 -0
package/README.md CHANGED
@@ -1,878 +1,380 @@
1
- # Smart Enums
1
+ # @reharik/smart-enum
2
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.
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
- ## CAVEAT?
5
+ ## Why not just use TypeScript enums?
6
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.
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
- ## Changelog
9
+ The common workarounds each solve one piece:
10
10
 
11
- See [CHANGELOG.md](packages/core/CHANGELOG.md) for a detailed list of changes and version history.
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** (const ME = "me") — scattered and hard to iterate
12
15
 
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:
16
+ Smart Enums give you all of it in one construct:
23
17
 
24
18
  ```typescript
25
- const Colors = enumeration('Colors', {
26
- input: ['red', 'blue', 'green'] as const,
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
- // 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 }
26
+ Status.active;
27
+ // { key: 'active', value: 'ACTIVE', display: 'Active', index: 1 }
32
28
 
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']
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
- ## Installation
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 ts-smart-enum
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
- 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.
44
+ ## Creating enums
52
45
 
53
- ## Tree-Shaking Support
46
+ ### From an array
54
47
 
55
- Smart Enums supports tree-shaking with multiple entry points for optimal bundle sizes:
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
- // 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';
51
+ const Color = enumeration('Color', {
52
+ input: ['red', 'blue', 'green'] as const,
53
+ });
54
+ type Color = Enumeration<typeof Color>;
74
55
 
75
- // Full API (backward compatible)
76
- import {
77
- enumeration,
78
- serializeSmartEnums,
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
- **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)
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
- ### Knex (optional)
64
+ ### From an object
91
65
 
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:
66
+ When you need custom values, display strings, or extra fields per member:
99
67
 
100
68
  ```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 });
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
- console.log(Priority.urgent.value); // "URGENT"
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
- ## Core Features
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
- ### Type Safety
86
+ ### Custom fields
136
87
 
137
- Smart Enums are fully type-safe:
88
+ Any extra properties you put on a member are preserved with full type inference:
138
89
 
139
90
  ```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
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
- Override or add custom auto-formatting:
99
+ AppError.notFound.source; // 'api' (literal type)
100
+ AppError.notFound.httpStatus; // 404 (literal type)
160
101
 
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"
102
+ // You can Extract by custom fields:
103
+ type ApiErrors = Extract<Enumeration<typeof AppError>, { source: 'api' }>;
175
104
  ```
176
105
 
177
- ## Utility Methods
106
+ ### Custom auto-formatters
178
107
 
179
- ### Lookup Methods
108
+ Override how `value`, `display`, or any auto-generated property is derived from the key:
180
109
 
181
110
  ```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');
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
- // Lookup by key
189
- const keyItem = Colors.fromKey('red');
119
+ Routes.userProfile.value; // '/userProfile'
120
+ Routes.userProfile.slug; // 'user-profile'
190
121
  ```
191
122
 
192
- ### Conversion Methods
193
-
194
- ```typescript
195
- // Get all values
196
- const values = Colors.values(); // ['RED', 'BLUE', 'GREEN']
123
+ ## Lookup methods
197
124
 
198
- // Get all keys
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
- // Get all enum items as array
202
- const items = Colors.items();
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
- ## Advanced Usage
137
+ ## Subsetting by a custom field
206
138
 
207
- ### Custom Properties
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
- 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
- };
142
+ import { getSubsetByProp, subsetByProp } from '@reharik/smart-enum';
215
143
 
216
- const Colors = enumeration('Colors', {
217
- input,
218
- });
144
+ const apiErrors = getSubsetByProp(AppError, 'source', 'api' as const);
219
145
 
220
- console.log(Colors.red.hex); // '#FF0000'
221
- console.log(Colors.blue.rgb); // [0, 0, 255]
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
- ### 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);
151
+ // Curried form:
152
+ const bySource = subsetByProp('source');
153
+ const authErrors = bySource(AppError, 'auth' as const);
249
154
  ```
250
155
 
251
- ### React/Frontend Usage
156
+ ## Tree-shaking
252
157
 
253
- Perfect for form selects and dropdowns:
158
+ The package has multiple entry points so you only pay for what you import:
254
159
 
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
- ```
160
+ ```typescript
161
+ // Core only — enumeration + type guards + subset helpers (~149 bytes)
162
+ import { enumeration } from '@reharik/smart-enum/core';
273
163
 
274
- ### Serialization and Reviving (Transformations)
164
+ // Core + transport serialization/revival (~406 bytes)
165
+ import { serializeForTransport } from '@reharik/smart-enum/transport';
275
166
 
276
- When sending data over the wire or persisting to a database, replace enum items with strings and revive them back.
167
+ // Core + database serialization/revival (~379 bytes)
168
+ import { prepareForDatabase } from '@reharik/smart-enum/database';
277
169
 
278
- ```ts
170
+ // Everything (~598 bytes)
279
171
  import {
280
- serializeSmartEnums,
281
- reviveSmartEnums,
282
172
  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
- // }
173
+ serializeSmartEnums,
174
+ prepareForDatabase,
175
+ } from '@reharik/smart-enum';
321
176
  ```
322
177
 
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.
178
+ ## Serialization and transport
334
179
 
335
- ### Basic Transport Usage
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 { enumeration } from 'ts-smart-enum/core';
339
- import {
340
- initializeSmartEnumMappings,
341
- serializeForTransport,
342
- reviveAfterTransport,
343
- } from 'ts-smart-enum';
183
+ import { serializeSmartEnums, reviveSmartEnums } from '@reharik/smart-enum';
344
184
 
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
- });
185
+ const dto = { id: '1', status: Status.active, color: Color.red };
353
186
 
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
- };
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
- // 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
193
+ // Revive
194
+ const revived = reviveSmartEnums(wire, { Status, Color });
195
+ // revived.status === Status.active ✓
393
196
  ```
394
197
 
395
- ### Express.js Integration
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 'ts-smart-enum';
404
-
405
- const app = express();
207
+ } from '@reharik/smart-enum/transport';
406
208
 
209
+ // At startup
407
210
  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));
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
- 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
- }
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 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).
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 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.
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 { enumeration } from 'ts-smart-enum/core';
513
- import { reviveRowFromDatabase } from 'ts-smart-enum/database';
226
+ import {
227
+ prepareForDatabase,
228
+ reviveRowFromDatabase,
229
+ revivePayloadFromDatabase,
230
+ } from '@reharik/smart-enum/database';
514
231
 
515
- const UserStatus = enumeration('UserStatus', {
516
- input: ['pending', 'active'] as const,
517
- });
232
+ // Write
233
+ const dbRow = prepareForDatabase({ name: 'Alice', status: Status.active });
234
+ // { name: 'Alice', status: 'ACTIVE' }
518
235
 
519
- const row = { id: '1', status: 'ACTIVE' };
236
+ // Read flat row
520
237
  const revived = reviveRowFromDatabase(row, {
521
- fieldEnumMapping: { status: UserStatus },
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
- ### `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, {
242
+ // Read — nested payload (e.g. JSONB column)
243
+ const doc = revivePayloadFromDatabase(payload, {
535
244
  pathEnumMapping: {
536
- 'user.status': UserStatus,
537
- 'lines[].kind': LineKind,
245
+ 'user.status': Status,
246
+ 'items[].kind': ItemKind,
538
247
  },
539
248
  });
540
249
  ```
541
250
 
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.
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
- ### Basic Usage
253
+ ## Type guards
551
254
 
552
255
  ```typescript
553
- import { enumeration, initializeSmartEnumMappings } from 'ts-smart-enum';
256
+ import { isSmartEnumItem, isSmartEnum } from '@reharik/smart-enum';
554
257
 
555
- // Console logging is enabled by default with 'error' level (minimal output)
556
- initializeSmartEnumMappings({
557
- enumRegistry: { UserStatus, Priority },
558
- });
258
+ isSmartEnumItem(Status.active); // true
259
+ isSmartEnumItem({ key: 'x' }); // false (plain object)
559
260
 
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' }
261
+ isSmartEnum(Status); // true (the enum object)
262
+ isSmartEnum(Status.active); // false (a member, not the enum)
566
263
  ```
567
264
 
568
- ### Log Level Configuration
265
+ ## GraphQL integration
569
266
 
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
- });
267
+ ### Server-side: resolvers returning smart-enum instances
581
268
 
582
- // Development: verbose logging
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
- // 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
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 { 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),
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
- ### 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)
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
- ### Disabling Logging
296
+ ### Client-side: Apollo cache rehydration
635
297
 
636
- To disable logging in production, you can set a no-op logger:
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 { initializeSmartEnumMappings } from 'ts-smart-enum';
301
+ import { InMemoryCache } from '@apollo/client';
302
+ import { smartEnumTypePolicies } from './generated/graphql-smart-enum-type-policies';
640
303
 
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: () => {},
304
+ const cache = new InMemoryCache({
305
+ typePolicies: {
306
+ ...smartEnumTypePolicies,
650
307
  },
651
308
  });
652
- ```
653
-
654
- ### Logging events
655
309
 
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
- }
310
+ // In a component:
311
+ const { data } = useQuery(GET_ORDER);
312
+ data.order.status.display; // 'Paid' — not just 'PAID'
711
313
  ```
712
314
 
713
- ### TypeORM Integration
315
+ See the [`type-policies` plugin README](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) for setup details.
714
316
 
715
- ```typescript
716
- import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
717
- import {
718
- prepareForDatabase,
719
- reviveRowFromDatabase,
720
- } from 'ts-smart-enum/database';
317
+ ## React
721
318
 
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
319
+ ```tsx
320
+ function StatusPicker() {
321
+ const [selected, setSelected] = useState(Status.pending);
765
322
 
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
- }
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
- ## 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:**
338
+ ## The `enumType` string
803
339
 
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
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
- **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
342
+ For hand-authored enums, keep it matching the variable name by convention:
827
343
 
828
344
  ```typescript
829
- // Before
830
- const COLORS = {
831
- RED: 'red',
832
- BLUE: 'blue',
833
- GREEN: 'green',
834
- };
345
+ const Status = enumeration('Status', { input: ... });
346
+ // ^^^^^^ ^^^^^^ ← keep these in sync
347
+ ```
835
348
 
836
- // After
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
- // Usage changes from COLORS.RED to Colors.red.value
842
- ```
351
+ ## Logging
843
352
 
844
- ### From String Unions
353
+ Transport initialization supports pluggable logging:
845
354
 
846
355
  ```typescript
847
- // Before
848
- type Status = 'pending' | 'active' | 'completed';
849
-
850
- // After
851
- const Status = enumeration('Status', {
852
- input: ['pending', 'active', 'completed'] as const,
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
- ## 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
368
+ Default is `console` at `'error'` level.
864
369
 
865
- ## Contributing
370
+ ## Related packages
866
371
 
867
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
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
-