semola 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +669 -0
- package/dist/lib/cache/index.d.ts +21 -0
- package/dist/lib/cache/index.d.ts.map +1 -0
- package/dist/lib/cache/index.js +45 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/types.d.ts +5 -0
- package/dist/lib/cache/types.d.ts.map +1 -0
- package/dist/lib/cache/types.js +2 -0
- package/dist/lib/cache/types.js.map +1 -0
- package/dist/lib/errors/index.d.ts +9 -0
- package/dist/lib/errors/index.d.ts.map +1 -0
- package/dist/lib/errors/index.js +25 -0
- package/dist/lib/errors/index.js.map +1 -0
- package/dist/lib/errors/types.d.ts +2 -0
- package/dist/lib/errors/types.d.ts.map +1 -0
- package/dist/lib/errors/types.js +2 -0
- package/dist/lib/errors/types.js.map +1 -0
- package/dist/lib/http/index.d.ts +29 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +641 -0
- package/dist/lib/http/index.js.map +1 -0
- package/dist/lib/http/types.d.ts +175 -0
- package/dist/lib/http/types.d.ts.map +1 -0
- package/dist/lib/http/types.js +2 -0
- package/dist/lib/http/types.js.map +1 -0
- package/dist/lib/i18n/index.d.ts +18 -0
- package/dist/lib/i18n/index.d.ts.map +1 -0
- package/dist/lib/i18n/index.js +38 -0
- package/dist/lib/i18n/index.js.map +1 -0
- package/dist/lib/i18n/types.d.ts +15 -0
- package/dist/lib/i18n/types.d.ts.map +1 -0
- package/dist/lib/i18n/types.js +2 -0
- package/dist/lib/i18n/types.js.map +1 -0
- package/dist/lib/policy/index.d.ts +9 -0
- package/dist/lib/policy/index.d.ts.map +1 -0
- package/dist/lib/policy/index.js +51 -0
- package/dist/lib/policy/index.js.map +1 -0
- package/dist/lib/policy/index.test.d.ts +2 -0
- package/dist/lib/policy/index.test.d.ts.map +1 -0
- package/dist/lib/policy/index.test.js +328 -0
- package/dist/lib/policy/index.test.js.map +1 -0
- package/dist/lib/policy/types.d.ts +27 -0
- package/dist/lib/policy/types.d.ts.map +1 -0
- package/dist/lib/policy/types.js +2 -0
- package/dist/lib/policy/types.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
# ts-kit
|
|
2
|
+
|
|
3
|
+
A TypeScript utility kit providing type-safe error handling, caching, internationalization, policy-based authorization, and developer tools.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install ts-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add ts-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Policy
|
|
18
|
+
|
|
19
|
+
A type-safe policy-based authorization system for defining and enforcing access control rules with conditional logic.
|
|
20
|
+
|
|
21
|
+
#### Import
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Policy } from "ts-kit/policy";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### API
|
|
28
|
+
|
|
29
|
+
**`new Policy()`**
|
|
30
|
+
|
|
31
|
+
Creates a new policy instance for managing authorization rules.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const policy = new Policy();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`policy.allow<T>(params: AllowParams<T>)`**
|
|
38
|
+
|
|
39
|
+
Defines a rule that grants permission for an action on an entity, optionally with conditions and a reason.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
type Post = {
|
|
43
|
+
id: number;
|
|
44
|
+
title: string;
|
|
45
|
+
authorId: number;
|
|
46
|
+
status: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Allow reading all published posts
|
|
50
|
+
policy.allow<Post>({
|
|
51
|
+
action: "read",
|
|
52
|
+
entity: "Post",
|
|
53
|
+
reason: "Public posts are visible to everyone",
|
|
54
|
+
conditions: {
|
|
55
|
+
status: "published"
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Allow all read access without conditions
|
|
60
|
+
policy.allow({
|
|
61
|
+
action: "read",
|
|
62
|
+
entity: "Comment",
|
|
63
|
+
reason: "Comments are public"
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**`policy.forbid<T>(params: ForbidParams<T>)`**
|
|
68
|
+
|
|
69
|
+
Defines a rule that denies permission for an action on an entity, optionally with conditions and a reason.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Forbid updating published posts
|
|
73
|
+
policy.forbid<Post>({
|
|
74
|
+
action: "update",
|
|
75
|
+
entity: "Post",
|
|
76
|
+
reason: "Published posts cannot be modified",
|
|
77
|
+
conditions: {
|
|
78
|
+
status: "published"
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Forbid deleting admin users
|
|
83
|
+
policy.forbid({
|
|
84
|
+
action: "delete",
|
|
85
|
+
entity: "User",
|
|
86
|
+
reason: "You cannot delete admins"
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`policy.can<T>(action: Action, entity: Entity, object?: T): CanResult`**
|
|
91
|
+
|
|
92
|
+
Checks if an action is permitted on an entity, optionally validating against an object's conditions. Returns a result object with `allowed` (boolean) and optional `reason` (string).
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const post: Post = {
|
|
96
|
+
id: 1,
|
|
97
|
+
title: "My Post",
|
|
98
|
+
authorId: 1,
|
|
99
|
+
status: "published"
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
policy.can<Post>("read", "Post", post);
|
|
103
|
+
// { allowed: true, reason: "Public posts are visible to everyone" }
|
|
104
|
+
|
|
105
|
+
policy.can<Post>("update", "Post", post);
|
|
106
|
+
// { allowed: false, reason: "Published posts cannot be modified" }
|
|
107
|
+
|
|
108
|
+
policy.can("delete", "Post");
|
|
109
|
+
// { allowed: false, reason: undefined }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Types
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
type Action = "read" | "create" | "update" | "delete" | (string & {});
|
|
116
|
+
type Entity = string;
|
|
117
|
+
type Conditions<T> = Partial<T>;
|
|
118
|
+
|
|
119
|
+
type CanResult = {
|
|
120
|
+
allowed: boolean;
|
|
121
|
+
reason?: string;
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Features
|
|
126
|
+
|
|
127
|
+
- **Type-safe conditions**: Conditions are validated against the object type
|
|
128
|
+
- **Flexible actions**: Built-in CRUD actions plus custom string actions
|
|
129
|
+
- **Multiple conditions**: Rules can match multiple object properties
|
|
130
|
+
- **Allow/Forbid semantics**: Explicit permission and denial rules
|
|
131
|
+
- **Human-readable reasons**: Optional explanations for authorization decisions
|
|
132
|
+
- **No match defaults to deny**: Conservative security model
|
|
133
|
+
- **Zero dependencies**: Pure TypeScript implementation
|
|
134
|
+
|
|
135
|
+
#### Usage Example
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { Policy } from "ts-kit/policy";
|
|
139
|
+
|
|
140
|
+
type Post = {
|
|
141
|
+
id: number;
|
|
142
|
+
title: string;
|
|
143
|
+
authorId: number;
|
|
144
|
+
status: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Create policy
|
|
148
|
+
const policy = new Policy();
|
|
149
|
+
|
|
150
|
+
// Define rules with reasons
|
|
151
|
+
policy.allow<Post>({
|
|
152
|
+
action: "read",
|
|
153
|
+
entity: "Post",
|
|
154
|
+
reason: "Published posts are publicly accessible",
|
|
155
|
+
conditions: {
|
|
156
|
+
status: "published"
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
policy.allow<Post>({
|
|
161
|
+
action: "update",
|
|
162
|
+
entity: "Post",
|
|
163
|
+
reason: "Draft posts can be edited",
|
|
164
|
+
conditions: {
|
|
165
|
+
status: "draft"
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
policy.forbid<Post>({
|
|
170
|
+
action: "delete",
|
|
171
|
+
entity: "Post",
|
|
172
|
+
reason: "Published posts cannot be deleted",
|
|
173
|
+
conditions: {
|
|
174
|
+
status: "published"
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Check permissions
|
|
179
|
+
const publishedPost: Post = {
|
|
180
|
+
id: 1,
|
|
181
|
+
title: "Hello World",
|
|
182
|
+
authorId: 1,
|
|
183
|
+
status: "published"
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const draftPost: Post = {
|
|
187
|
+
id: 2,
|
|
188
|
+
title: "Work in Progress",
|
|
189
|
+
authorId: 1,
|
|
190
|
+
status: "draft"
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Check permissions with reasons
|
|
194
|
+
const readResult = policy.can<Post>("read", "Post", publishedPost);
|
|
195
|
+
console.log(readResult);
|
|
196
|
+
// { allowed: true, reason: "Published posts are publicly accessible" }
|
|
197
|
+
|
|
198
|
+
const updateDraftResult = policy.can<Post>("update", "Post", draftPost);
|
|
199
|
+
console.log(updateDraftResult);
|
|
200
|
+
// { allowed: true, reason: "Draft posts can be edited" }
|
|
201
|
+
|
|
202
|
+
const deleteResult = policy.can<Post>("delete", "Post", publishedPost);
|
|
203
|
+
console.log(deleteResult);
|
|
204
|
+
// { allowed: false, reason: "Published posts cannot be deleted" }
|
|
205
|
+
|
|
206
|
+
// Use in authorization middleware
|
|
207
|
+
function authorize<T>(action: Action, entity: Entity, object?: T) {
|
|
208
|
+
const result = policy.can(action, entity, object);
|
|
209
|
+
if (!result.allowed) {
|
|
210
|
+
throw new Error(result.reason || "Unauthorized");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Protect routes with meaningful error messages
|
|
215
|
+
authorize<Post>("delete", "Post", publishedPost);
|
|
216
|
+
// throws Error: "Published posts cannot be deleted"
|
|
217
|
+
|
|
218
|
+
authorize<Post>("read", "Post", publishedPost);
|
|
219
|
+
// passes
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Internationalization (i18n)
|
|
223
|
+
|
|
224
|
+
A fully type-safe internationalization utility with compile-time validation of translation keys and parameters.
|
|
225
|
+
|
|
226
|
+
#### Import
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { I18n } from "ts-kit/i18n";
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### API
|
|
233
|
+
|
|
234
|
+
**`new I18n<TLocales, TDefaultLocale>(config)`**
|
|
235
|
+
|
|
236
|
+
Creates a new i18n instance with type-safe locale management.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const i18n = new I18n({
|
|
240
|
+
defaultLocale: "en",
|
|
241
|
+
locales: {
|
|
242
|
+
en: {
|
|
243
|
+
common: {
|
|
244
|
+
hello: "Hello, world",
|
|
245
|
+
sayHi: "Hi, {name:string}",
|
|
246
|
+
age: "I am {age:number} years old",
|
|
247
|
+
active: "Status: {active:boolean}"
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
es: {
|
|
251
|
+
common: {
|
|
252
|
+
hello: "Hola, mundo",
|
|
253
|
+
sayHi: "Hola, {name:string}",
|
|
254
|
+
age: "Tengo {age:number} años",
|
|
255
|
+
active: "Estado: {active:boolean}"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} as const // Required for type inference
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**`i18n.translate(key, params?)`**
|
|
263
|
+
|
|
264
|
+
Translates a key with optional parameters. Fully type-safe - invalid keys, missing parameters, or wrong parameter types cause compile-time errors.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Basic translation
|
|
268
|
+
i18n.translate("common.hello")
|
|
269
|
+
// "Hello, world"
|
|
270
|
+
|
|
271
|
+
// With parameters
|
|
272
|
+
i18n.translate("common.sayHi", { name: "Leonardo" })
|
|
273
|
+
// "Hi, Leonardo"
|
|
274
|
+
|
|
275
|
+
i18n.translate("common.age", { age: 25 })
|
|
276
|
+
// "I am 25 years old"
|
|
277
|
+
|
|
278
|
+
// Type errors (won't compile)
|
|
279
|
+
i18n.translate("invalid.key") // ✗ Invalid key
|
|
280
|
+
i18n.translate("common.sayHi") // ✗ Missing required params
|
|
281
|
+
i18n.translate("common.sayHi", { name: 123 }) // ✗ Wrong param type
|
|
282
|
+
i18n.translate("common.hello", { name: "x" }) // ✗ Unnecessary params
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**`i18n.setLocale(locale)`**
|
|
286
|
+
|
|
287
|
+
Switches to a different locale. Type-safe - only valid locale keys are accepted.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
i18n.setLocale("es")
|
|
291
|
+
i18n.translate("common.hello")
|
|
292
|
+
// "Hola, mundo"
|
|
293
|
+
|
|
294
|
+
i18n.setLocale("invalid") // ✗ Type error
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**`i18n.getLocale()`**
|
|
298
|
+
|
|
299
|
+
Returns the current locale.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const currentLocale = i18n.getLocale()
|
|
303
|
+
// "es"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Parameter Syntax
|
|
307
|
+
|
|
308
|
+
Translation strings support typed parameters with the syntax `{paramName:type}`:
|
|
309
|
+
|
|
310
|
+
- `{name:string}` - String parameter
|
|
311
|
+
- `{age:number}` - Number parameter
|
|
312
|
+
- `{active:boolean}` - Boolean parameter
|
|
313
|
+
|
|
314
|
+
The type system extracts these at compile time and enforces them in the `translate()` method.
|
|
315
|
+
|
|
316
|
+
#### Features
|
|
317
|
+
|
|
318
|
+
- **Type-safe keys**: Only valid nested keys accepted (e.g., `"common.hello"`)
|
|
319
|
+
- **Type-safe parameters**: Parameter types validated at compile time
|
|
320
|
+
- **Type-safe locales**: Only defined locale keys can be set
|
|
321
|
+
- **Nested translations**: Support for deeply nested translation objects
|
|
322
|
+
- **Locale fallback**: Falls back to default locale if translation missing
|
|
323
|
+
- **Zero runtime overhead**: No runtime type checking - pure TypeScript validation
|
|
324
|
+
- **Const assertion required**: Use `as const` on locale objects for proper type inference
|
|
325
|
+
|
|
326
|
+
#### Usage Example
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { I18n } from "ts-kit/i18n";
|
|
330
|
+
|
|
331
|
+
const translations = {
|
|
332
|
+
en: {
|
|
333
|
+
auth: {
|
|
334
|
+
welcome: "Welcome back, {name:string}!",
|
|
335
|
+
loginSuccess: "Successfully logged in",
|
|
336
|
+
loginFailed: "Login failed"
|
|
337
|
+
},
|
|
338
|
+
profile: {
|
|
339
|
+
age: "Age: {age:number}",
|
|
340
|
+
verified: "Verified: {status:boolean}"
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
es: {
|
|
344
|
+
auth: {
|
|
345
|
+
welcome: "Bienvenido, {name:string}!",
|
|
346
|
+
loginSuccess: "Inicio de sesión exitoso",
|
|
347
|
+
loginFailed: "Inicio de sesión fallido"
|
|
348
|
+
},
|
|
349
|
+
profile: {
|
|
350
|
+
age: "Edad: {age:number}",
|
|
351
|
+
verified: "Verificado: {status:boolean}"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} as const;
|
|
355
|
+
|
|
356
|
+
const i18n = new I18n({
|
|
357
|
+
defaultLocale: "en",
|
|
358
|
+
locales: translations
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Use in your app
|
|
362
|
+
function greetUser(name: string) {
|
|
363
|
+
return i18n.translate("auth.welcome", { name });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function showProfile(age: number, verified: boolean) {
|
|
367
|
+
console.log(i18n.translate("profile.age", { age }));
|
|
368
|
+
console.log(i18n.translate("profile.verified", { status: verified }));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Switch language
|
|
372
|
+
i18n.setLocale("es");
|
|
373
|
+
greetUser("Maria"); // "Bienvenido, Maria!"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Cache
|
|
377
|
+
|
|
378
|
+
A type-safe Redis cache wrapper with TTL support and result-based error handling. Built on Bun's native Redis client.
|
|
379
|
+
|
|
380
|
+
#### Import
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { Cache } from "ts-kit/cache";
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### API
|
|
387
|
+
|
|
388
|
+
**`new Cache<T>(options: CacheOptions)`**
|
|
389
|
+
|
|
390
|
+
Creates a new cache instance with optional TTL configuration.
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
type CacheOptions = {
|
|
394
|
+
redis: Bun.RedisClient;
|
|
395
|
+
ttl?: number; // Time-to-live in milliseconds
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const cache = new Cache<User>({
|
|
399
|
+
redis: redisClient,
|
|
400
|
+
ttl: 60000 // Optional: cache entries expire after 60 seconds
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**`cache.get(key: string)`**
|
|
405
|
+
|
|
406
|
+
Retrieves a value from the cache. Returns a result tuple with the parsed value or an error.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const [error, user] = await cache.get("user:123");
|
|
410
|
+
|
|
411
|
+
if (error) {
|
|
412
|
+
switch (error.type) {
|
|
413
|
+
case "NotFoundError":
|
|
414
|
+
console.log("Cache miss");
|
|
415
|
+
break;
|
|
416
|
+
case "CacheError":
|
|
417
|
+
console.error("Cache error:", error.message);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
console.log("Cache hit:", user);
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**`cache.set(key: string, value: T)`**
|
|
426
|
+
|
|
427
|
+
Stores a value in the cache with automatic JSON serialization. Applies TTL if configured.
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
const [error, data] = await cache.set("user:123", { id: 123, name: "John" });
|
|
431
|
+
|
|
432
|
+
if (error) {
|
|
433
|
+
console.error("Failed to cache:", error.message);
|
|
434
|
+
} else {
|
|
435
|
+
console.log("Cached successfully");
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**`cache.delete(key: string)`**
|
|
440
|
+
|
|
441
|
+
Removes a key from the cache.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
const [error] = await cache.delete("user:123");
|
|
445
|
+
|
|
446
|
+
if (error) {
|
|
447
|
+
console.error("Failed to delete:", error.message);
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### Usage Example
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import { Cache } from "ts-kit/cache";
|
|
455
|
+
|
|
456
|
+
type User = {
|
|
457
|
+
id: number;
|
|
458
|
+
name: string;
|
|
459
|
+
email: string;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Create cache instance
|
|
463
|
+
const userCache = new Cache<User>({
|
|
464
|
+
redis: new Bun.RedisClient("redis://localhost:6379"),
|
|
465
|
+
ttl: 300000 // 5 minutes
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Get or fetch user
|
|
469
|
+
async function getUser(id: string) {
|
|
470
|
+
// Try cache first
|
|
471
|
+
const [cacheError, cachedUser] = await userCache.get(`user:${id}`);
|
|
472
|
+
|
|
473
|
+
if (!cacheError) {
|
|
474
|
+
return ok(cachedUser);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Cache miss - fetch from database
|
|
478
|
+
const [dbError, user] = await fetchUserFromDB(id);
|
|
479
|
+
|
|
480
|
+
if (dbError) {
|
|
481
|
+
return err("NotFoundError", "User not found");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Store in cache for next time
|
|
485
|
+
await userCache.set(`user:${id}`, user);
|
|
486
|
+
|
|
487
|
+
return ok(user);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Note on lifecycle management:** The `Cache` class does not manage the Redis client lifecycle. Since you provide the client when creating the cache, you're responsible for closing it when done:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const redis = new Bun.RedisClient("redis://localhost:6379");
|
|
495
|
+
const cache = new Cache({ redis });
|
|
496
|
+
|
|
497
|
+
// Use the cache...
|
|
498
|
+
|
|
499
|
+
// Clean up when done
|
|
500
|
+
await redis.quit();
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Error Utilities
|
|
504
|
+
|
|
505
|
+
Result-based error handling inspired by functional programming patterns. Avoid throwing exceptions and handle errors explicitly with type-safe tuples.
|
|
506
|
+
|
|
507
|
+
#### Import
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { ok, err, mightThrow, mightThrowSync } from "ts-kit/errors";
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### API
|
|
514
|
+
|
|
515
|
+
**`ok<T>(data: T)`**
|
|
516
|
+
|
|
517
|
+
Creates a successful result tuple.
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const result = ok({ userId: 123, name: "John" });
|
|
521
|
+
// [null, { userId: 123, name: "John" }]
|
|
522
|
+
|
|
523
|
+
const [error, data] = result;
|
|
524
|
+
|
|
525
|
+
if (error) {
|
|
526
|
+
// Handle error
|
|
527
|
+
} else {
|
|
528
|
+
console.log(data.userId); // Type-safe access
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**`err<T>(type: T, message: string)`**
|
|
533
|
+
|
|
534
|
+
Creates an error result tuple with a typed error object.
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
const result = err("NotFoundError", "User not found");
|
|
538
|
+
// [{ type: "NotFoundError", message: "User not found" }, null]
|
|
539
|
+
|
|
540
|
+
const [error, data] = result;
|
|
541
|
+
|
|
542
|
+
if (error) {
|
|
543
|
+
console.log(error.type); // "NotFoundError"
|
|
544
|
+
console.log(error.message); // "User not found"
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Common error types:** `NotFoundError`, `UnauthorizedError`, `InternalServerError`, `ValidationError`, or any custom string.
|
|
549
|
+
|
|
550
|
+
**`mightThrow<T>(promise: Promise<T>)`**
|
|
551
|
+
|
|
552
|
+
Wraps async operations that might throw into result tuples.
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
const [error, data] = await mightThrow(fetch('/api/users'));
|
|
556
|
+
|
|
557
|
+
if (error) {
|
|
558
|
+
console.error("Request failed:", error);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
console.log("Success:", data);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**`mightThrowSync<T>(fn: () => T)`**
|
|
566
|
+
|
|
567
|
+
Wraps synchronous operations that might throw into result tuples.
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
const [error, data] = mightThrowSync(() => JSON.parse(input));
|
|
571
|
+
|
|
572
|
+
if (error) {
|
|
573
|
+
console.error("Parse failed:", error);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
console.log("Parsed:", data);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
#### Usage Example
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import { ok, err, mightThrow } from "ts-kit/errors";
|
|
584
|
+
|
|
585
|
+
async function getUser(id: string) {
|
|
586
|
+
if (!id) {
|
|
587
|
+
return err("ValidationError", "User ID is required");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const [fetchError, response] = await mightThrow(
|
|
591
|
+
fetch(`/api/users/${id}`)
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
if (fetchError) {
|
|
595
|
+
return err("InternalServerError", "Failed to fetch user");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const [parseError, user] = await mightThrow(response.json());
|
|
599
|
+
|
|
600
|
+
if (parseError) {
|
|
601
|
+
return err("InternalServerError", "Invalid response format");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return ok(user);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Usage
|
|
608
|
+
const [error, user] = await getUser("123");
|
|
609
|
+
|
|
610
|
+
if (error) {
|
|
611
|
+
switch (error.type) {
|
|
612
|
+
case "ValidationError":
|
|
613
|
+
console.log("Validation failed:", error.message);
|
|
614
|
+
break;
|
|
615
|
+
case "NotFoundError":
|
|
616
|
+
console.log("User not found");
|
|
617
|
+
break;
|
|
618
|
+
default:
|
|
619
|
+
console.log("Error:", error.message);
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
console.log("User:", user);
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
## Publishing
|
|
627
|
+
|
|
628
|
+
This package uses GitHub Actions to automatically publish to npm. To publish a new version:
|
|
629
|
+
|
|
630
|
+
1. Update the version in `package.json`:
|
|
631
|
+
```bash
|
|
632
|
+
bun version <major|minor|patch>
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
2. Create a new release on GitHub:
|
|
636
|
+
- Go to the [Releases page](https://github.com/leonardodipace/ts-kit/releases)
|
|
637
|
+
- Click "Create a new release"
|
|
638
|
+
- Create a new tag (e.g., `v0.3.0`)
|
|
639
|
+
- Publish the release
|
|
640
|
+
|
|
641
|
+
The GitHub Action will automatically:
|
|
642
|
+
- Run checks and tests
|
|
643
|
+
- Build the package
|
|
644
|
+
- Publish to npm with provenance
|
|
645
|
+
|
|
646
|
+
Alternatively, you can manually trigger the workflow from the Actions tab and optionally specify a version.
|
|
647
|
+
|
|
648
|
+
**Note:** This package uses npm's Trusted Publishing feature, so no NPM_TOKEN is required. The workflow authenticates using GitHub's OIDC token with the `id-token: write` permission.
|
|
649
|
+
|
|
650
|
+
## Development
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
# Install dependencies
|
|
654
|
+
bun install
|
|
655
|
+
|
|
656
|
+
# Build package
|
|
657
|
+
bun run build
|
|
658
|
+
|
|
659
|
+
# Build types
|
|
660
|
+
bun run build:types
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## License
|
|
664
|
+
|
|
665
|
+
MIT © Leonardo Dipace
|
|
666
|
+
|
|
667
|
+
## Repository
|
|
668
|
+
|
|
669
|
+
[https://github.com/leonardodipace/ts-kit](https://github.com/leonardodipace/ts-kit)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CacheOptions } from "./types.js";
|
|
2
|
+
export declare class Cache<T> {
|
|
3
|
+
private options;
|
|
4
|
+
constructor(options: CacheOptions);
|
|
5
|
+
get(key: string): Promise<readonly [{
|
|
6
|
+
readonly type: "CacheError";
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}, null] | readonly [{
|
|
9
|
+
readonly type: "NotFoundError";
|
|
10
|
+
readonly message: string;
|
|
11
|
+
}, null] | readonly [null, T]>;
|
|
12
|
+
set(key: string, value: T): Promise<readonly [{
|
|
13
|
+
readonly type: "CacheError";
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}, null] | readonly [null, T]>;
|
|
16
|
+
delete(key: string): Promise<readonly [{
|
|
17
|
+
readonly type: "CacheError";
|
|
18
|
+
readonly message: string;
|
|
19
|
+
}, null] | readonly [null, number | null]>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,KAAK,CAAC,CAAC;IAClB,OAAO,CAAC,OAAO,CAAe;gBAElB,OAAO,EAAE,YAAY;IAIpB,GAAG,CAAC,GAAG,EAAE,MAAM;;;;;;;IAoBf,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;;;;IA0BzB,MAAM,CAAC,GAAG,EAAE,MAAM;;;;CAShC"}
|