@reharik/smart-enum 0.4.0 → 0.4.1

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 (2) hide show
  1. package/README.md +18 -362
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,380 +1,36 @@
1
1
  # @reharik/smart-enum
2
2
 
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
-
5
- ## Why not just use TypeScript enums?
6
-
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
-
9
- The common workarounds each solve one piece:
10
-
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
15
-
16
- Smart Enums give you all of it in one construct:
17
-
18
- ```typescript
19
- import { enumeration, type Enumeration } from '@reharik/smart-enum';
20
-
21
- const Status = enumeration('Status', {
22
- input: ['pending', 'active', 'completed'] as const,
23
- });
24
- type Status = Enumeration<typeof Status>;
25
-
26
- Status.active;
27
- // { key: 'active', value: 'ACTIVE', display: 'Active', index: 1 }
28
-
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']
34
- ```
35
-
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
39
-
40
- ```bash
41
- npm install @reharik/smart-enum
42
- ```
43
-
44
- ## Creating enums
45
-
46
- ### From an array
47
-
48
- The simplest form. Keys are the array values, wire values are auto-derived as `CONSTANT_CASE`, display strings as `Title Case`.
49
-
50
- ```typescript
51
- const Color = enumeration('Color', {
52
- input: ['red', 'blue', 'green'] as const,
53
- });
54
- type Color = Enumeration<typeof Color>;
55
-
56
- Color.red.key; // 'red'
57
- Color.red.value; // 'RED'
58
- Color.red.display; // 'Red'
59
- Color.red.index; // 0
60
- ```
61
-
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.
63
-
64
- ### From an object
65
-
66
- When you need custom values, display strings, or extra fields per member:
67
-
68
- ```typescript
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
- });
77
- type Priority = Enumeration<typeof Priority>;
78
-
79
- Priority.high.value; // 'P1' (explicit)
80
- Priority.low.value; // 'LOW' (auto-derived from key)
81
- Priority.urgent.display; // 'Urgent!!!'
82
- ```
83
-
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.
85
-
86
- ### Custom fields
87
-
88
- Any extra properties you put on a member are preserved with full type inference:
89
-
90
- ```typescript
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
- });
98
-
99
- AppError.notFound.source; // 'api' (literal type)
100
- AppError.notFound.httpStatus; // 404 (literal type)
101
-
102
- // You can Extract by custom fields:
103
- type ApiErrors = Extract<Enumeration<typeof AppError>, { source: 'api' }>;
104
- ```
105
-
106
- ### Custom auto-formatters
107
-
108
- Override how `value`, `display`, or any auto-generated property is derived from the key:
109
-
110
- ```typescript
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
- });
118
-
119
- Routes.userProfile.value; // '/userProfile'
120
- Routes.userProfile.slug; // 'user-profile'
121
- ```
122
-
123
- ## Lookup methods
124
-
125
- Every enum object has these methods. Return types are fully narrowed to the enum's member union.
126
-
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. |
136
-
137
- ## Subsetting by a custom field
138
-
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:
140
-
141
- ```typescript
142
- import { getSubsetByProp, subsetByProp } from '@reharik/smart-enum';
143
-
144
- const apiErrors = getSubsetByProp(AppError, 'source', 'api' as const);
145
-
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')
150
-
151
- // Curried form:
152
- const bySource = subsetByProp('source');
153
- const authErrors = bySource(AppError, 'auth' as const);
154
- ```
155
-
156
- ## Tree-shaking
157
-
158
- The package has multiple entry points so you only pay for what you import:
159
-
160
- ```typescript
161
- // Core only — enumeration + type guards + subset helpers (~149 bytes)
162
- import { enumeration } from '@reharik/smart-enum/core';
163
-
164
- // Core + transport serialization/revival (~406 bytes)
165
- import { serializeForTransport } from '@reharik/smart-enum/transport';
166
-
167
- // Core + database serialization/revival (~379 bytes)
168
- import { prepareForDatabase } from '@reharik/smart-enum/database';
169
-
170
- // Everything (~598 bytes)
171
- import {
172
- enumeration,
173
- serializeSmartEnums,
174
- prepareForDatabase,
175
- } from '@reharik/smart-enum';
176
- ```
177
-
178
- ## Serialization and transport
179
-
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.
181
-
182
- ```typescript
183
- import { serializeSmartEnums, reviveSmartEnums } from '@reharik/smart-enum';
184
-
185
- const dto = { id: '1', status: Status.active, color: Color.red };
186
-
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' } }
192
-
193
- // Revive
194
- const revived = reviveSmartEnums(wire, { Status, Color });
195
- // revived.status === Status.active ✓
196
- ```
197
-
198
- ### Global transport registry
199
-
200
- For app-wide setup (e.g. Express middleware), register all enums once at startup:
201
-
202
- ```typescript
203
- import {
204
- initializeSmartEnumMappings,
205
- serializeForTransport,
206
- reviveAfterTransport,
207
- } from '@reharik/smart-enum/transport';
208
-
209
- // At startup
210
- initializeSmartEnumMappings({
211
- enumRegistry: { Status, Priority, Color },
212
- });
213
-
214
- // In a handler
215
- const wire = serializeForTransport(responseData);
216
- const revived = reviveAfterTransport(requestBody);
217
- ```
218
-
219
- ## Database utilities
220
-
221
- **Outbound:** `prepareForDatabase` recursively replaces enum items with their `.value` strings. Each item also has a `.toPostgres()` method for PostgreSQL drivers that honor it.
222
-
223
- **Inbound:** Database columns are plain strings — they don't carry type information. Revival requires you to declare which columns map to which enums.
224
-
225
- ```typescript
226
- import {
227
- prepareForDatabase,
228
- reviveRowFromDatabase,
229
- revivePayloadFromDatabase,
230
- } from '@reharik/smart-enum/database';
231
-
232
- // Write
233
- const dbRow = prepareForDatabase({ name: 'Alice', status: Status.active });
234
- // { name: 'Alice', status: 'ACTIVE' }
235
-
236
- // Read — flat row
237
- const revived = reviveRowFromDatabase(row, {
238
- fieldEnumMapping: { status: Status, priority: Priority },
239
- strict: true, // throw on unknown values instead of keeping raw string
240
- });
241
-
242
- // Read — nested payload (e.g. JSONB column)
243
- const doc = revivePayloadFromDatabase(payload, {
244
- pathEnumMapping: {
245
- 'user.status': Status,
246
- 'items[].kind': ItemKind,
247
- },
248
- });
249
- ```
250
-
251
- `strict: true` throws `EnumRevivalError` when a mapped string doesn't match any member. Without it, unrecognized values are left as-is.
252
-
253
- ## Type guards
254
-
255
- ```typescript
256
- import { isSmartEnumItem, isSmartEnum } from '@reharik/smart-enum';
257
-
258
- isSmartEnumItem(Status.active); // true
259
- isSmartEnumItem({ key: 'x' }); // false (plain object)
260
-
261
- isSmartEnum(Status); // true (the enum object)
262
- isSmartEnum(Status.active); // false (a member, not the enum)
263
- ```
3
+ This package is built from `packages/core`. Full documentation (transport, database, GraphQL, and more) lives in the [repository README](../../README.md).
264
4
 
265
5
  ## GraphQL integration
266
6
 
267
- ### Server-side: resolvers returning smart-enum instances
268
-
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.
270
-
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:
272
-
273
- ```typescript
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
- }),
290
- },
291
- };
292
- ```
293
-
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.
295
-
296
- ### Client-side: Apollo cache rehydration
297
-
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:
7
+ ### Client-side: outgoing variables
299
8
 
300
- ```typescript
301
- import { InMemoryCache } from '@apollo/client';
302
- import { smartEnumTypePolicies } from './generated/graphql-smart-enum-type-policies';
303
-
304
- const cache = new InMemoryCache({
305
- typePolicies: {
306
- ...smartEnumTypePolicies,
307
- },
308
- });
9
+ If your Apollo Client uses a link that processes variables against the schema — most commonly [`apollo-link-scalars`](https://www.npmjs.com/package/apollo-link-scalars) — that link will call `serialize` on every variable value, including smart-enum instances, _before_ JSON.stringify runs. GraphQL's default `serialize` on `GraphQLEnumType` doesn't know how to handle smart-enum objects and throws:
309
10
 
310
- // In a component:
311
- const { data } = useQuery(GET_ORDER);
312
- data.order.status.display; // 'Paid' — not just 'PAID'
313
11
  ```
314
-
315
- See the [`type-policies` plugin README](https://www.npmjs.com/package/@reharik/graphql-codegen-smart-enum) for setup details.
316
-
317
- ## React
318
-
319
- ```tsx
320
- function StatusPicker() {
321
- const [selected, setSelected] = useState(Status.pending);
322
-
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
- );
335
- }
12
+ Enum "ReactionEmoji" cannot represent value: { __smart_enum_type: ..., value: ... }
336
13
  ```
337
14
 
338
- ## The `enumType` string
339
-
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.
341
-
342
- For hand-authored enums, keep it matching the variable name by convention:
15
+ The same `patchSchemaEnumSerializers` that you use on the server also fixes this on the client. Patch your client schema before passing it to the link:
343
16
 
344
17
  ```typescript
345
- const Status = enumeration('Status', { input: ... });
346
- // ^^^^^^ ^^^^^^ ← keep these in sync
347
- ```
18
+ import { buildSchema } from 'graphql';
19
+ import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
20
+ import { withScalars } from 'apollo-link-scalars';
21
+ import { patchSchemaEnumSerializers } from '@reharik/smart-enum/graphql';
22
+ import { enumRegistry } from '@packages/contracts';
23
+ import sdl from './generated/schema.graphql?raw';
348
24
 
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.
25
+ const schema = buildSchema(sdl);
26
+ patchSchemaEnumSerializers(schema, enumRegistry);
350
27
 
351
- ## Logging
28
+ const scalarLink = withScalars({ schema, typesMap: { /* ... */ } });
352
29
 
353
- Transport initialization supports pluggable logging:
354
-
355
- ```typescript
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
- },
30
+ export const client = new ApolloClient({
31
+ link: ApolloLink.from([scalarLink, httpLink]),
32
+ cache: new InMemoryCache({ /* ... */ }),
365
33
  });
366
34
  ```
367
35
 
368
- Default is `console` at `'error'` level.
369
-
370
- ## Related packages
371
-
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 |
377
-
378
- ## License
379
-
380
- MIT
36
+ If you're not using a schema-aware link, you don't need this — Apollo's default behavior calls `JSON.stringify` on variables directly, which goes through smart-enum's `toJSON` (with `serializeAs: 'value'`) and produces the correct wire format.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reharik/smart-enum",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",