Skip to content

Commit ac13526

Browse files
committed
chore: update readme
1 parent 4af4790 commit ac13526

File tree

1 file changed

+249
-5
lines changed

1 file changed

+249
-5
lines changed

packages/redis-fluent-keys/README.md

Lines changed: 249 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,250 @@
1-
# Open-source stack
1+
# Redis fluent keys: Finally, Typesafe Redis Keys You'll Actually Enjoy! ✨
22

3-
This is a placeholder npm package for the open-source-stack by Forge 42.
4-
It is a full starter stack to develop CJS/ESM compatible npm packages with TypeScript, Vitest, Biome and GitHub Actions.
5-
Find the template here:
6-
https://github.com/forge-42/open-source-stack
3+
![GitHub Repo stars](https://img.shields.io/github/stars/flixy-dev/redis-fluent-keys?style=social)
4+
![npm](https://img.shields.io/npm/v/@flixy-dev/redis-fluent-keys?style=plastic)
5+
![GitHub](https://img.shields.io/github/license/flixy-dev/redis-fluent-keys?style=plastic)
6+
![npm](https://img.shields.io/npm/dy/@flixy-dev/redis-fluent-keys?style=plastic)
7+
![npm](https://img.shields.io/npm/dw/@flixy-dev/redis-fluent-keys?style=plastic)
8+
![GitHub top language](https://img.shields.io/github/languages/top/flixy-dev/redis-fluent-keys?style=plastic)
9+
10+
**(Because stringly-typed keys are just asking for trouble, right?)**
11+
12+
Ugh, Redis keys. We all use 'em, but managing them can be a pain:
13+
14+
* Typo in `users:profile:usr_123` vs `user:profile:user_123`? Good luck finding that bug! 😭
15+
* Inconsistent naming conventions across your app? Chaos!
16+
* Need to refactor a key structure? Prepare for a risky find-and-replace adventure. 😬
17+
* Want to include a user ID or timestamp? Hope you format that template string correctly *every single time*.
18+
19+
**Enough is enough!** This little library helps you define your Redis key structures in one place, using plain TypeScript, and gives you back **fully typesafe functions** to generate those keys.
20+
21+
**What you get:**
22+
23+
***Autocomplete Heaven:** Define your keys, get autocomplete for paths and placeholders.
24+
* 🔒 **Bulletproof Type Safety:** Pass the wrong type (like a `number` for a `userId` string)? TypeScript yells at you *before* you deploy. Forget a placeholder? Compile error!
25+
* 🌳 **Organized Structure:** Define keys in a nested way that makes sense for your domain.
26+
* ⚙️ **Refactor with Confidence:** Change a key definition in one place, TypeScript guides you to fix all the usages.
27+
* 😎 **Awesome DX:** Simple API, minimal boilerplate, focuses on getting the job done cleanly.
28+
29+
Works perfectly with [`ioredis`](https://github.com/luin/ioredis) or any other Redis client that just needs the final key string.
30+
31+
## Installation
32+
33+
```bash
34+
npm install @flixy-dev/redis-fluent-keys --save
35+
# or
36+
yarn add @flixy-dev/redis-fluent-keys
37+
# or
38+
pnpm add @flixy-dev/redis-fluent-keys
39+
bun add @flixy-dev/redis-fluent-keys
40+
```
41+
42+
# Quick Start
43+
Let's see how easy this is:
44+
45+
```ts
46+
// src/redis-keys.ts
47+
import { createKeyBuilder, p } from 'redis-fluent-keys';
48+
49+
// 1. Create a key builder instance (separator defaults to ':')
50+
const keyBuilder = createKeyBuilder();
51+
// const keyBuilder = createKeyBuilder({ separator: '__' }); // Custom separator!
52+
53+
// 2. Define your key schema
54+
export const redisKeys = keyBuilder({
55+
// A simple static key
56+
config: {
57+
cacheVersion: ['config', 'cacheVersion'], // -> config:cacheVersion
58+
},
59+
60+
// Keys related to users
61+
users: {
62+
// A key with a dynamic part (placeholder)
63+
profile: ['users', p('userId'), 'profile'], // -> users:{userId}:profile
64+
65+
// Another static one nested
66+
allActiveSet: ['users', 'all', 'active'], // -> users:all:active
67+
},
68+
});
69+
70+
// -------------------------------------
71+
72+
// src/some-service.ts
73+
import Redis from 'ioredis';
74+
import { redisKeys } from './redis-keys'; // Import your defined keys
75+
76+
const redis = new Redis(); // Your ioredis instance
77+
78+
async function getUserProfile(id: string) {
79+
// 3. Use the typesafe function! ✨
80+
const key = redisKeys.users.profile({ userId: id });
81+
// key will be "users:id:profile"
82+
83+
console.log(`Fetching from Redis key: ${key}`);
84+
const profileData = await redis.hgetall(key);
85+
86+
// Trying to misuse it? TypeScript catches it!
87+
// const wrongKey = redisKeys.users.profile({}); // TS Error: userId missing!
88+
// const wrongKey2 = redisKeys.users.profile({ userId: 123 }); // TS Error: userId needs string!
89+
90+
return profileData;
91+
}
92+
93+
async function getCacheVersion() {
94+
// No arguments needed for static keys!
95+
const key = redisKeys.config.cacheVersion();
96+
// key will be "config:cacheVersion"
97+
return redis.get(key);
98+
}
99+
100+
getUserProfile('usr_987');
101+
getCacheVersion();
102+
```
103+
104+
See? Define once, use everywhere safely!
105+
106+
# Features Deep Dive Placeholders (`p`, `p.number`, `p.boolean`)
107+
108+
Dynamic parts are the heart of most Redis keys. We use the `p()` helper:
109+
`p('placeholderName')`: Creates a placeholder expecting a string. Infers the name `"placeholderName"` literally for the argument object. (This is the default and most common).
110+
`p.number('placeholderName')`: Creates a placeholder expecting a number.
111+
`p.boolean('placeholderName')`: Creates a placeholder expecting a boolean (will be converted to "true" or "false" in the key).
112+
113+
```ts
114+
const keys = createKeyBuilder()({
115+
user: p('userId'), // -> {userId} (string)
116+
productStock: ['products', p.number('productId'), 'stock'], // -> products:{productId}:stock
117+
featureFlag: ['features', p('flagName'), p.boolean('isEnabled')], // -> features:{flagName}:{isEnabled}
118+
});
119+
120+
const userKey = keys.user({ userId: 'user-123' }); // "user-123"
121+
const stockKey = keys.productStock({ productId: 55 }); // "products:55:stock"
122+
const flagKey = keys.featureFlag({ flagName: 'newUI', isEnabled: true }); // "features:newUI:true"
123+
124+
// Compile-time errors:
125+
// const badStock = keys.productStock({ productId: 'abc' }); // TS Error! Expects number
126+
// const badFlag = keys.featureFlag({ flagName: 'oldUI' }); // TS Error! isEnabled missing
127+
```
128+
129+
# Nesting (The Easy Way)
130+
131+
Organize your keys logically using nested objects. The object keys automatically become part of the prefix.
132+
133+
```ts
134+
const keys = createKeyBuilder()({
135+
users: { // "users" becomes a prefix
136+
all: ['all'], // -> users:all
137+
settings: { // "settings" becomes a prefix
138+
byUser: [p('userId')], // -> users:settings:{userId}
139+
notifications: { // "notifications" becomes a prefix
140+
email: ['email', p('userId')], // -> users:settings:notifications:email:{userId}
141+
}
142+
}
143+
},
144+
cache: { // "cache" becomes a prefix
145+
images: ['images'], // -> cache:images
146+
}
147+
});
148+
149+
const settingsKey = keys.users.settings.byUser({ userId: 'u-456' });
150+
// "users:settings:u-456"
151+
const emailKey = keys.users.settings.notifications.email({ userId: 'u-789' });
152+
// "users:settings:notifications:email:u-789"
153+
const imgKey = keys.cache.images();
154+
// "cache:images"
155+
```
156+
157+
# Parameterized Nesting (`parameterize`)
158+
159+
Sometimes, the nesting level itself needs a dynamic value (like accessing keys for a *specific* user). That's where parameterize comes in!
160+
161+
Think about it: how would you define keys like `user:{userId}:profile` AND `user:{userId}:settings` using the nesting above? You can't easily make `user:{userId}` the prefix directly.
162+
163+
`parameterize` solves this:
164+
165+
```ts
166+
import { createKeyBuilder, p, parameterize } from 'redis-fluent-keys';
167+
168+
const keys = createKeyBuilder()({
169+
// Parameterize the 'user' level by userId
170+
user: parameterize(p('userId'), { // Now requires { userId: string } to access inner keys
171+
// Inside here, "user:{userId}" is the implicit prefix!
172+
173+
profile: ['profile'], // Definition is just the final part
174+
// -> user:{userId}:profile
175+
176+
settings: ['settings'], // Definition is just the final part
177+
// -> user:{userId}:settings
178+
179+
orders: { // You can still nest further statically
180+
all: ['all'], // -> user:{userId}:orders:all (Fixed schema example)
181+
byId: [p.number('orderId')], // -> user:{userId}:orders:{orderId} (Fixed schema example)
182+
}
183+
}),
184+
185+
// You can parameterize with multiple placeholders too!
186+
tenantResource: parameterize(
187+
[p('tenantId'), p.number('resourceId')], // Requires { tenantId: string, resourceId: number }
188+
{
189+
config: ['config'], // -> tenantResource:{tenantId}:{resourceId}:config (Fixed schema example)
190+
status: ['status'], // -> tenantResource:{tenantId}:{resourceId}:status (Fixed schema example)
191+
}
192+
),
193+
194+
// A regular key for comparison
195+
globalConfig: ['global', 'config'],
196+
});
197+
198+
// --- Usage ---
199+
200+
// 1. Call the parameterized function first to get the access object for that user
201+
const userAccess = keys.user({ userId: 'u-abc' });
202+
203+
// 2. Now use the returned object like normal
204+
const profileKey = userAccess.profile(); // -> "user:u-abc:profile"
205+
const settingsKey = userAccess.settings(); // -> "user:u-abc:settings"
206+
const orderKey = userAccess.orders.byId({ orderId: 99 }); // -> "user:u-abc:orders:99"
207+
208+
// Multi-parameter example
209+
const tenantAccess = keys.tenantResource({ tenantId: 'acme', resourceId: 123 });
210+
const configKey = tenantAccess.config(); // -> "tenantResource:acme:123:config"
211+
212+
// Trying to access before parameterizing? TS Error!
213+
// const badAccess = keys.user.profile(); // TS Error! 'profile' doesn't exist directly on keys.user
214+
```
215+
216+
`parameterize` returns a function. You call that function with the required path parameters, and *it* returns the object containing the next level of key builders, now correctly prefixed! Pretty neat, huh? 🤔
217+
218+
# Custom Separator
219+
220+
Don't like `:`? No problem!
221+
222+
```ts
223+
const keyBuilder = createKeyBuilder({ separator: '::' });
224+
225+
const keys = keyBuilder({
226+
user: ['user', p('id')]
227+
});
228+
229+
const key = keys.user({ id: '123' }); // -> user::123
230+
```
231+
232+
# API Reference
233+
234+
- `createKeyBuilder(options?: { separator?: string }): (schema) => KeyBuilderResult`
235+
- Creates the builder factory. Call the returned function with your schema object.
236+
- `p<const Name extends string>(name: Name): Placeholder<string, Name>`
237+
- Creates a string placeholder, inferring the literal name.
238+
- `p.number<const Name extends string>(name: Name): Placeholder<number, Name>`
239+
- Creates a number placeholder.
240+
- `p.boolean<const Name extends string>(name: Name): Placeholder<boolean, Name>`
241+
- Creates a boolean placeholder.
242+
- `parameterize<const P, const S>(placeholders: P, nestedSchema: S): Parameterized<P, S>`
243+
- Defines a schema level that requires runtime parameters (placeholders) to access the nestedSchema. placeholders can be a single p() result or a readonly array/tuple of them.
244+
245+
# Contributing
246+
247+
Found a bug? Have an idea? Feel free to open an issue or submit a PR!
248+
249+
# License
250+
MIT License. Use it, love it, break it, fix it. ❤️

0 commit comments

Comments
 (0)