@plazmodium/odin 0.3.2-beta → 0.3.4-beta
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 +82 -11
- package/builtin/ODIN.md +1045 -0
- package/builtin/agent-definitions/README.md +170 -0
- package/builtin/agent-definitions/_shared-context.md +377 -0
- package/builtin/agent-definitions/architect.md +627 -0
- package/builtin/agent-definitions/builder.md +716 -0
- package/builtin/agent-definitions/discovery.md +293 -0
- package/builtin/agent-definitions/documenter.md +238 -0
- package/builtin/agent-definitions/guardian.md +1049 -0
- package/builtin/agent-definitions/integrator.md +363 -0
- package/builtin/agent-definitions/planning.md +236 -0
- package/builtin/agent-definitions/product.md +405 -0
- package/builtin/agent-definitions/release.md +430 -0
- package/builtin/agent-definitions/reviewer.md +447 -0
- package/builtin/agent-definitions/watcher.md +402 -0
- package/builtin/skills/api/graphql/SKILL.md +548 -0
- package/builtin/skills/api/grpc/SKILL.md +554 -0
- package/builtin/skills/api/rest-api/SKILL.md +469 -0
- package/builtin/skills/api/trpc/SKILL.md +503 -0
- package/builtin/skills/architecture/clean-architecture/SKILL.md +141 -0
- package/builtin/skills/architecture/domain-driven-design/SKILL.md +129 -0
- package/builtin/skills/architecture/event-driven/SKILL.md +145 -0
- package/builtin/skills/architecture/microservices/SKILL.md +143 -0
- package/builtin/skills/architecture/tla-precheck/SKILL.md +171 -0
- package/builtin/skills/backend/golang-gin/SKILL.md +141 -0
- package/builtin/skills/backend/nodejs-express/SKILL.md +277 -0
- package/builtin/skills/backend/nodejs-fastify/SKILL.md +152 -0
- package/builtin/skills/backend/python-django/SKILL.md +128 -0
- package/builtin/skills/backend/python-fastapi/SKILL.md +140 -0
- package/builtin/skills/database/mongodb/SKILL.md +132 -0
- package/builtin/skills/database/postgresql/SKILL.md +120 -0
- package/builtin/skills/database/prisma-orm/SKILL.md +366 -0
- package/builtin/skills/database/redis/SKILL.md +140 -0
- package/builtin/skills/database/supabase/SKILL.md +416 -0
- package/builtin/skills/devops/aws/SKILL.md +382 -0
- package/builtin/skills/devops/docker/SKILL.md +359 -0
- package/builtin/skills/devops/github-actions/SKILL.md +435 -0
- package/builtin/skills/devops/kubernetes/SKILL.md +459 -0
- package/builtin/skills/devops/terraform/SKILL.md +453 -0
- package/builtin/skills/frontend/alpine-dev/SKILL.md +27 -0
- package/builtin/skills/frontend/angular-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/astro-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/htmx-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/nextjs-dev/SKILL.md +470 -0
- package/builtin/skills/frontend/react-patterns/SKILL.md +166 -0
- package/builtin/skills/frontend/svelte-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/tailwindcss/SKILL.md +131 -0
- package/builtin/skills/frontend/vuejs-dev/SKILL.md +28 -0
- package/builtin/skills/generic-dev/SKILL.md +307 -0
- package/builtin/skills/testing/cypress/SKILL.md +372 -0
- package/builtin/skills/testing/jest/SKILL.md +176 -0
- package/builtin/skills/testing/playwright/SKILL.md +341 -0
- package/builtin/skills/testing/unit-tests-eval-sdd/SKILL.md +73 -0
- package/builtin/skills/testing/unit-tests-sdd/SKILL.md +83 -0
- package/builtin/skills/testing/vitest/SKILL.md +249 -0
- package/dist/adapters/skills/filesystem.d.ts.map +1 -1
- package/dist/adapters/skills/filesystem.js +2 -18
- package/dist/adapters/skills/filesystem.js.map +1 -1
- package/dist/builtin-assets.d.ts +8 -0
- package/dist/builtin-assets.d.ts.map +1 -0
- package/dist/builtin-assets.js +90 -0
- package/dist/builtin-assets.js.map +1 -0
- package/dist/init.js +69 -11
- package/dist/init.js.map +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/prepare-phase-context.d.ts.map +1 -1
- package/dist/tools/prepare-phase-context.js +5 -0
- package/dist/tools/prepare-phase-context.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prisma-orm
|
|
3
|
+
description: Prisma ORM expertise for type-safe database access, schema design, migrations, and query optimization
|
|
4
|
+
category: database
|
|
5
|
+
version: "5.x"
|
|
6
|
+
depends_on:
|
|
7
|
+
- postgresql
|
|
8
|
+
compatible_with:
|
|
9
|
+
- mysql
|
|
10
|
+
- nodejs-express
|
|
11
|
+
- nextjs-dev
|
|
12
|
+
- typescript
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Prisma ORM Development
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
Prisma is a next-generation ORM for Node.js and TypeScript that provides type-safe database access, automated migrations, and an intuitive data modeling language.
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
prisma/
|
|
25
|
+
├── schema.prisma # Database schema
|
|
26
|
+
├── migrations/ # Migration history
|
|
27
|
+
│ └── 20240115_init/
|
|
28
|
+
│ └── migration.sql
|
|
29
|
+
└── seed.ts # Database seeding
|
|
30
|
+
|
|
31
|
+
src/
|
|
32
|
+
├── lib/
|
|
33
|
+
│ └── prisma.ts # Prisma client singleton
|
|
34
|
+
└── services/
|
|
35
|
+
└── user.service.ts # Using Prisma client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Schema Design
|
|
39
|
+
|
|
40
|
+
### Basic Schema
|
|
41
|
+
|
|
42
|
+
```prisma
|
|
43
|
+
// prisma/schema.prisma
|
|
44
|
+
generator client {
|
|
45
|
+
provider = "prisma-client-js"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
datasource db {
|
|
49
|
+
provider = "postgresql"
|
|
50
|
+
url = env("DATABASE_URL")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
model User {
|
|
54
|
+
id String @id @default(uuid())
|
|
55
|
+
email String @unique
|
|
56
|
+
name String?
|
|
57
|
+
password String
|
|
58
|
+
role Role @default(USER)
|
|
59
|
+
posts Post[]
|
|
60
|
+
profile Profile?
|
|
61
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
62
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
63
|
+
|
|
64
|
+
@@map("users")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
model Post {
|
|
68
|
+
id String @id @default(uuid())
|
|
69
|
+
title String
|
|
70
|
+
content String?
|
|
71
|
+
published Boolean @default(false)
|
|
72
|
+
author User @relation(fields: [authorId], references: [id])
|
|
73
|
+
authorId String @map("author_id")
|
|
74
|
+
categories Category[]
|
|
75
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
76
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
77
|
+
|
|
78
|
+
@@index([authorId])
|
|
79
|
+
@@map("posts")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
model Profile {
|
|
83
|
+
id String @id @default(uuid())
|
|
84
|
+
bio String?
|
|
85
|
+
avatar String?
|
|
86
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
87
|
+
userId String @unique @map("user_id")
|
|
88
|
+
|
|
89
|
+
@@map("profiles")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
model Category {
|
|
93
|
+
id String @id @default(uuid())
|
|
94
|
+
name String @unique
|
|
95
|
+
posts Post[]
|
|
96
|
+
|
|
97
|
+
@@map("categories")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
enum Role {
|
|
101
|
+
USER
|
|
102
|
+
ADMIN
|
|
103
|
+
MODERATOR
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Prisma Client Setup
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// src/lib/prisma.ts
|
|
111
|
+
import { PrismaClient } from '@prisma/client';
|
|
112
|
+
|
|
113
|
+
const globalForPrisma = globalThis as unknown as {
|
|
114
|
+
prisma: PrismaClient | undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
118
|
+
log: process.env.NODE_ENV === 'development'
|
|
119
|
+
? ['query', 'error', 'warn']
|
|
120
|
+
: ['error'],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
124
|
+
globalForPrisma.prisma = prisma;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default prisma;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Common Query Patterns
|
|
131
|
+
|
|
132
|
+
### CRUD Operations
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import prisma from '@/lib/prisma';
|
|
136
|
+
|
|
137
|
+
// Create
|
|
138
|
+
const user = await prisma.user.create({
|
|
139
|
+
data: {
|
|
140
|
+
email: 'user@example.com',
|
|
141
|
+
name: 'John Doe',
|
|
142
|
+
password: hashedPassword,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Read (single)
|
|
147
|
+
const user = await prisma.user.findUnique({
|
|
148
|
+
where: { id: userId },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Read (with relations)
|
|
152
|
+
const userWithPosts = await prisma.user.findUnique({
|
|
153
|
+
where: { id: userId },
|
|
154
|
+
include: {
|
|
155
|
+
posts: true,
|
|
156
|
+
profile: true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Read (many with filtering)
|
|
161
|
+
const users = await prisma.user.findMany({
|
|
162
|
+
where: {
|
|
163
|
+
email: { contains: '@company.com' },
|
|
164
|
+
role: 'USER',
|
|
165
|
+
},
|
|
166
|
+
orderBy: { createdAt: 'desc' },
|
|
167
|
+
take: 10,
|
|
168
|
+
skip: 0,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Update
|
|
172
|
+
const updated = await prisma.user.update({
|
|
173
|
+
where: { id: userId },
|
|
174
|
+
data: { name: 'Jane Doe' },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Delete
|
|
178
|
+
await prisma.user.delete({
|
|
179
|
+
where: { id: userId },
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Select Specific Fields
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const users = await prisma.user.findMany({
|
|
187
|
+
select: {
|
|
188
|
+
id: true,
|
|
189
|
+
email: true,
|
|
190
|
+
name: true,
|
|
191
|
+
// password intentionally excluded
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Nested Writes
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Create user with profile
|
|
200
|
+
const user = await prisma.user.create({
|
|
201
|
+
data: {
|
|
202
|
+
email: 'user@example.com',
|
|
203
|
+
name: 'John',
|
|
204
|
+
password: hash,
|
|
205
|
+
profile: {
|
|
206
|
+
create: {
|
|
207
|
+
bio: 'Developer',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
include: { profile: true },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Create post with categories
|
|
215
|
+
const post = await prisma.post.create({
|
|
216
|
+
data: {
|
|
217
|
+
title: 'My Post',
|
|
218
|
+
content: 'Content...',
|
|
219
|
+
authorId: userId,
|
|
220
|
+
categories: {
|
|
221
|
+
connectOrCreate: [
|
|
222
|
+
{
|
|
223
|
+
where: { name: 'Tech' },
|
|
224
|
+
create: { name: 'Tech' },
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Transactions
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Sequential transaction
|
|
236
|
+
const [user, post] = await prisma.$transaction([
|
|
237
|
+
prisma.user.create({ data: userData }),
|
|
238
|
+
prisma.post.create({ data: postData }),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Interactive transaction
|
|
242
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
243
|
+
const user = await tx.user.create({ data: userData });
|
|
244
|
+
|
|
245
|
+
if (someCondition) {
|
|
246
|
+
throw new Error('Rollback!');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const post = await tx.post.create({
|
|
250
|
+
data: { ...postData, authorId: user.id },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return { user, post };
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Aggregations
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Count
|
|
261
|
+
const count = await prisma.user.count({
|
|
262
|
+
where: { role: 'USER' },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Group by
|
|
266
|
+
const postsByUser = await prisma.post.groupBy({
|
|
267
|
+
by: ['authorId'],
|
|
268
|
+
_count: { id: true },
|
|
269
|
+
orderBy: { _count: { id: 'desc' } },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Aggregate
|
|
273
|
+
const stats = await prisma.post.aggregate({
|
|
274
|
+
_count: true,
|
|
275
|
+
_avg: { views: true },
|
|
276
|
+
_max: { views: true },
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Migrations
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
# Create migration
|
|
284
|
+
npx prisma migrate dev --name add_user_role
|
|
285
|
+
|
|
286
|
+
# Apply migrations (production)
|
|
287
|
+
npx prisma migrate deploy
|
|
288
|
+
|
|
289
|
+
# Reset database (development only!)
|
|
290
|
+
npx prisma migrate reset
|
|
291
|
+
|
|
292
|
+
# Generate client after schema changes
|
|
293
|
+
npx prisma generate
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Best Practices
|
|
297
|
+
|
|
298
|
+
1. **Use singleton pattern** - Prevent connection pool exhaustion
|
|
299
|
+
2. **Select only needed fields** - Reduce data transfer
|
|
300
|
+
3. **Use transactions for related writes** - Ensure data consistency
|
|
301
|
+
4. **Add indexes for query fields** - `@@index([fieldName])`
|
|
302
|
+
5. **Use `@map` for snake_case** - Keep DB conventions, use camelCase in code
|
|
303
|
+
6. **Archive instead of delete** - Add `isArchived Boolean @default(false)` and `archivedAt DateTime?` for recoverable records. "Soft delete" is a misnomer - use archive terminology to match real-world concepts
|
|
304
|
+
7. **Always include `updatedAt`** - Use `@updatedAt` for automatic tracking
|
|
305
|
+
|
|
306
|
+
## Gotchas & Pitfalls
|
|
307
|
+
|
|
308
|
+
- **N+1 queries** - Use `include` or `select` with relations, not separate queries
|
|
309
|
+
- **Connection limits** - Use singleton pattern, especially in serverless
|
|
310
|
+
- **Migration conflicts** - Don't edit existing migrations, create new ones
|
|
311
|
+
- **Type mismatches** - Run `prisma generate` after schema changes
|
|
312
|
+
- **Forgetting indexes** - Add `@@index` for frequently queried fields
|
|
313
|
+
- **BigInt serialization** - JSON.stringify doesn't handle BigInt, convert to string
|
|
314
|
+
|
|
315
|
+
## Integration with Next.js
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// app/api/users/route.ts
|
|
319
|
+
import prisma from '@/lib/prisma';
|
|
320
|
+
import { NextResponse } from 'next/server';
|
|
321
|
+
|
|
322
|
+
export async function GET() {
|
|
323
|
+
const users = await prisma.user.findMany({
|
|
324
|
+
select: { id: true, email: true, name: true },
|
|
325
|
+
});
|
|
326
|
+
return NextResponse.json(users);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Seeding
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// prisma/seed.ts
|
|
334
|
+
import { PrismaClient } from '@prisma/client';
|
|
335
|
+
|
|
336
|
+
const prisma = new PrismaClient();
|
|
337
|
+
|
|
338
|
+
async function main() {
|
|
339
|
+
await prisma.user.upsert({
|
|
340
|
+
where: { email: 'admin@example.com' },
|
|
341
|
+
update: {},
|
|
342
|
+
create: {
|
|
343
|
+
email: 'admin@example.com',
|
|
344
|
+
name: 'Admin',
|
|
345
|
+
password: 'hashed_password',
|
|
346
|
+
role: 'ADMIN',
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
main()
|
|
352
|
+
.catch((e) => {
|
|
353
|
+
console.error(e);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
})
|
|
356
|
+
.finally(() => prisma.$disconnect());
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
// package.json
|
|
361
|
+
{
|
|
362
|
+
"prisma": {
|
|
363
|
+
"seed": "ts-node prisma/seed.ts"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: redis
|
|
3
|
+
description: Redis in-memory data store patterns for caching, sessions, queues, and real-time features
|
|
4
|
+
category: database
|
|
5
|
+
version: "7.x"
|
|
6
|
+
compatible_with:
|
|
7
|
+
- nodejs-express
|
|
8
|
+
- nodejs-fastify
|
|
9
|
+
- python-fastapi
|
|
10
|
+
- python-django
|
|
11
|
+
- golang-gin
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Redis
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Redis is an in-memory data structure store used as a cache, message broker, and database. This skill covers caching patterns, data structures, and common use cases.
|
|
19
|
+
|
|
20
|
+
## Common Use Cases
|
|
21
|
+
|
|
22
|
+
### Caching
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Cache-aside pattern (most common)
|
|
26
|
+
async function getUser(id: string): Promise<User> {
|
|
27
|
+
const cacheKey = `user:${id}`;
|
|
28
|
+
|
|
29
|
+
// 1. Check cache
|
|
30
|
+
const cached = await redis.get(cacheKey);
|
|
31
|
+
if (cached) return JSON.parse(cached);
|
|
32
|
+
|
|
33
|
+
// 2. Cache miss → fetch from DB
|
|
34
|
+
const user = await db.users.findById(id);
|
|
35
|
+
if (!user) throw new NotFoundError();
|
|
36
|
+
|
|
37
|
+
// 3. Populate cache with TTL
|
|
38
|
+
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1 hour
|
|
39
|
+
|
|
40
|
+
return user;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Invalidate on mutation
|
|
44
|
+
async function updateUser(id: string, data: UpdateUserDto): Promise<User> {
|
|
45
|
+
const user = await db.users.update(id, data);
|
|
46
|
+
await redis.del(`user:${id}`); // Invalidate cache
|
|
47
|
+
return user;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Sessions
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Store session data with TTL
|
|
55
|
+
await redis.set(`session:${sessionId}`, JSON.stringify({
|
|
56
|
+
userId: user.id,
|
|
57
|
+
role: user.role,
|
|
58
|
+
loginAt: Date.now(),
|
|
59
|
+
}), 'EX', 86400); // 24 hours
|
|
60
|
+
|
|
61
|
+
// Retrieve
|
|
62
|
+
const session = JSON.parse(await redis.get(`session:${sessionId}`) || 'null');
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Rate Limiting
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Sliding window rate limiter
|
|
69
|
+
async function checkRateLimit(ip: string, limit: number, windowSec: number): Promise<boolean> {
|
|
70
|
+
const key = `ratelimit:${ip}`;
|
|
71
|
+
const current = await redis.incr(key);
|
|
72
|
+
|
|
73
|
+
if (current === 1) {
|
|
74
|
+
await redis.expire(key, windowSec);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return current <= limit;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Pub/Sub
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Publisher
|
|
85
|
+
await redis.publish('notifications', JSON.stringify({
|
|
86
|
+
userId: '123',
|
|
87
|
+
type: 'new_message',
|
|
88
|
+
payload: { from: 'Jane', text: 'Hello' },
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Subscriber
|
|
92
|
+
const sub = redis.duplicate();
|
|
93
|
+
await sub.subscribe('notifications');
|
|
94
|
+
sub.on('message', (channel, message) => {
|
|
95
|
+
const notification = JSON.parse(message);
|
|
96
|
+
sendToWebSocket(notification.userId, notification);
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Data Structures
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Strings — basic key-value, counters
|
|
104
|
+
SET user:123:name "John"
|
|
105
|
+
INCR page:views:homepage
|
|
106
|
+
|
|
107
|
+
# Hashes — object-like storage (memory efficient)
|
|
108
|
+
HSET user:123 name "John" email "john@example.com" role "admin"
|
|
109
|
+
HGET user:123 email
|
|
110
|
+
|
|
111
|
+
# Lists — queues, recent items
|
|
112
|
+
LPUSH queue:emails '{"to":"john@..."}'
|
|
113
|
+
RPOP queue:emails
|
|
114
|
+
|
|
115
|
+
# Sets — unique collections, tags
|
|
116
|
+
SADD post:456:tags "javascript" "react" "tutorial"
|
|
117
|
+
SISMEMBER post:456:tags "react"
|
|
118
|
+
|
|
119
|
+
# Sorted Sets — leaderboards, time-series
|
|
120
|
+
ZADD leaderboard 1500 "player1" 2300 "player2"
|
|
121
|
+
ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Best Practices
|
|
125
|
+
|
|
126
|
+
1. **Use key namespaces** — `entity:id:field` pattern (`user:123:profile`)
|
|
127
|
+
2. **Always set TTL** — prevent unbounded memory growth; use `EX` or `EXPIREAT`
|
|
128
|
+
3. **Use pipelines** — batch multiple commands in one round trip
|
|
129
|
+
4. **Prefer hashes** for objects — more memory-efficient than separate string keys
|
|
130
|
+
5. **Avoid large keys** — break large values into smaller structures or use Streams
|
|
131
|
+
6. **Use Lua scripts** for atomic operations — `EVAL` guarantees atomicity
|
|
132
|
+
7. **Monitor memory** — use `INFO memory` and set `maxmemory-policy` (e.g., `allkeys-lru`)
|
|
133
|
+
|
|
134
|
+
## Gotchas
|
|
135
|
+
|
|
136
|
+
- **No built-in persistence guarantees** — RDB snapshots + AOF help, but Redis is not a primary database
|
|
137
|
+
- **Single-threaded** — long-running commands (KEYS *, large SORT) block all clients; use SCAN instead
|
|
138
|
+
- **Pub/Sub is fire-and-forget** — messages are lost if no subscriber is listening; use Streams for durability
|
|
139
|
+
- **Serialization overhead** — JSON.stringify/parse adds latency; consider MessagePack for large payloads
|
|
140
|
+
- **Connection limits** — each subscriber needs its own connection; use connection pooling
|