feat: add bun-fullstack agent and update skills
This commit is contained in:
92
.opencode/skills/tech-stack/elysiajs/integrations/ai-sdk.md
Normal file
92
.opencode/skills/tech-stack/elysiajs/integrations/ai-sdk.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# AI SDK Integration
|
||||
|
||||
## What It Is
|
||||
Seamless integration with Vercel AI SDK via response streaming.
|
||||
|
||||
## Response Streaming
|
||||
Return `ReadableStream` or `Response` directly:
|
||||
```typescript
|
||||
import { streamText } from 'ai'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
|
||||
new Elysia().get('/', () => {
|
||||
const stream = streamText({
|
||||
model: openai('gpt-5'),
|
||||
system: 'You are Yae Miko from Genshin Impact',
|
||||
prompt: 'Hi! How are you doing?'
|
||||
})
|
||||
|
||||
return stream.textStream // ReadableStream
|
||||
// or
|
||||
return stream.toUIMessageStream() // UI Message Stream
|
||||
})
|
||||
```
|
||||
|
||||
Elysia auto-handles stream.
|
||||
|
||||
## Server-Sent Events
|
||||
Wrap `ReadableStream` with `sse`:
|
||||
```typescript
|
||||
import { sse } from 'elysia'
|
||||
|
||||
.get('/', () => {
|
||||
const stream = streamText({ /* ... */ })
|
||||
|
||||
return sse(stream.textStream)
|
||||
// or
|
||||
return sse(stream.toUIMessageStream())
|
||||
})
|
||||
```
|
||||
|
||||
Each chunk → SSE.
|
||||
|
||||
## As Response
|
||||
Return stream directly (no Eden type safety):
|
||||
```typescript
|
||||
.get('/', () => {
|
||||
const stream = streamText({ /* ... */ })
|
||||
|
||||
return stream.toTextStreamResponse()
|
||||
// or
|
||||
return stream.toUIMessageStreamResponse() // Uses SSE
|
||||
})
|
||||
```
|
||||
|
||||
## Manual Streaming
|
||||
Generator function for control:
|
||||
```typescript
|
||||
import { sse } from 'elysia'
|
||||
|
||||
.get('/', async function* () {
|
||||
const stream = streamText({ /* ... */ })
|
||||
|
||||
for await (const data of stream.textStream)
|
||||
yield sse({ data, event: 'message' })
|
||||
|
||||
yield sse({ event: 'done' })
|
||||
})
|
||||
```
|
||||
|
||||
## Fetch for Unsupported Models
|
||||
Direct fetch with streaming proxy:
|
||||
```typescript
|
||||
.get('/', () => {
|
||||
return fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are Yae Miko' },
|
||||
{ role: 'user', content: 'Hi! How are you doing?' }
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Elysia auto-proxies fetch response with streaming.
|
||||
59
.opencode/skills/tech-stack/elysiajs/integrations/astro.md
Normal file
59
.opencode/skills/tech-stack/elysiajs/integrations/astro.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Astro Integration - SKILLS.md
|
||||
|
||||
## What It Is
|
||||
Run Elysia on Astro via Astro Endpoint.
|
||||
|
||||
## Setup
|
||||
1. Set output to server:
|
||||
```javascript
|
||||
// astro.config.mjs
|
||||
export default defineConfig({
|
||||
output: 'server'
|
||||
})
|
||||
```
|
||||
|
||||
2. Create `pages/[...slugs].ts`
|
||||
3. Define Elysia server + export handlers:
|
||||
```typescript
|
||||
// pages/[...slugs].ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/api', () => 'hi')
|
||||
.post('/api', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||
|
||||
export const GET = handle
|
||||
export const POST = handle
|
||||
```
|
||||
|
||||
WinterCG compliance - works normally.
|
||||
|
||||
Recommended: Run Astro on Bun (Elysia designed for Bun).
|
||||
|
||||
## Prefix for Non-Root
|
||||
If placed in `pages/api/[...slugs].ts`, set prefix:
|
||||
```typescript
|
||||
// pages/api/[...slugs].ts
|
||||
const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', () => 'hi')
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||
|
||||
export const GET = handle
|
||||
export const POST = handle
|
||||
```
|
||||
|
||||
Ensures routing works in any location.
|
||||
|
||||
## Benefits
|
||||
Co-location of frontend + backend. End-to-end type safety with Eden.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
117
.opencode/skills/tech-stack/elysiajs/integrations/better-auth.md
Normal file
117
.opencode/skills/tech-stack/elysiajs/integrations/better-auth.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Better Auth Integration
|
||||
Elysia + Better Auth integration guide
|
||||
|
||||
## What It Is
|
||||
Framework-agnostic TypeScript auth/authz. Comprehensive features + plugin ecosystem.
|
||||
|
||||
## Setup
|
||||
```typescript
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: new Pool()
|
||||
})
|
||||
```
|
||||
|
||||
## Handler Mounting
|
||||
```typescript
|
||||
import { auth } from './auth'
|
||||
|
||||
new Elysia()
|
||||
.mount(auth.handler) // http://localhost:3000/api/auth
|
||||
.listen(3000)
|
||||
```
|
||||
|
||||
### Custom Endpoint
|
||||
```typescript
|
||||
// Mount with prefix
|
||||
.mount('/auth', auth.handler) // http://localhost:3000/auth/api/auth
|
||||
|
||||
// Customize basePath
|
||||
export const auth = betterAuth({
|
||||
basePath: '/api' // http://localhost:3000/auth/api
|
||||
})
|
||||
```
|
||||
|
||||
Cannot set `basePath` to empty or `/`.
|
||||
|
||||
## OpenAPI Integration
|
||||
Extract docs from Better Auth:
|
||||
```typescript
|
||||
import { openAPI } from 'better-auth/plugins'
|
||||
|
||||
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
|
||||
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
|
||||
|
||||
export const OpenAPI = {
|
||||
getPaths: (prefix = '/auth/api') =>
|
||||
getSchema().then(({ paths }) => {
|
||||
const reference: typeof paths = Object.create(null)
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
const key = prefix + path
|
||||
reference[key] = paths[path]
|
||||
|
||||
for (const method of Object.keys(paths[path])) {
|
||||
const operation = (reference[key] as any)[method]
|
||||
operation.tags = ['Better Auth']
|
||||
}
|
||||
}
|
||||
|
||||
return reference
|
||||
}) as Promise<any>,
|
||||
components: getSchema().then(({ components }) => components) as Promise<any>
|
||||
} as const
|
||||
```
|
||||
|
||||
Apply to Elysia:
|
||||
```typescript
|
||||
new Elysia().use(openapi({
|
||||
documentation: {
|
||||
components: await OpenAPI.components,
|
||||
paths: await OpenAPI.getPaths()
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
## CORS
|
||||
```typescript
|
||||
import { cors } from '@elysiajs/cors'
|
||||
|
||||
new Elysia()
|
||||
.use(cors({
|
||||
origin: 'http://localhost:3001',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}))
|
||||
.mount(auth.handler)
|
||||
```
|
||||
|
||||
## Macro for Auth
|
||||
Use macro + resolve for session/user:
|
||||
```typescript
|
||||
const betterAuth = new Elysia({ name: 'better-auth' })
|
||||
.mount(auth.handler)
|
||||
.macro({
|
||||
auth: {
|
||||
async resolve({ status, request: { headers } }) {
|
||||
const session = await auth.api.getSession({ headers })
|
||||
|
||||
if (!session) return status(401)
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
session: session.session
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
new Elysia()
|
||||
.use(betterAuth)
|
||||
.get('/user', ({ user }) => user, { auth: true })
|
||||
```
|
||||
|
||||
Access `user` and `session` in all routes.
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
# Cloudflare Worker Integration
|
||||
|
||||
## What It Is
|
||||
**Experimental** Cloudflare Worker adapter for Elysia.
|
||||
|
||||
## Setup
|
||||
1. Install Wrangler:
|
||||
```bash
|
||||
wrangler init elysia-on-cloudflare
|
||||
```
|
||||
|
||||
2. Apply adapter + compile:
|
||||
```typescript
|
||||
import { Elysia } from 'elysia'
|
||||
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
|
||||
|
||||
export default new Elysia({
|
||||
adapter: CloudflareAdapter
|
||||
})
|
||||
.get('/', () => 'Hello Cloudflare Worker!')
|
||||
.compile() // Required
|
||||
```
|
||||
|
||||
3. Set compatibility date (min `2025-06-01`):
|
||||
```json
|
||||
// wrangler.json
|
||||
{
|
||||
"name": "elysia-on-cloudflare",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-06-01"
|
||||
}
|
||||
```
|
||||
|
||||
4. Dev server:
|
||||
```bash
|
||||
wrangler dev
|
||||
# http://localhost:8787
|
||||
```
|
||||
|
||||
No `nodejs_compat` flag needed.
|
||||
|
||||
## Limitations
|
||||
1. `Elysia.file` + Static Plugin don't work (no `fs` module)
|
||||
2. OpenAPI Type Gen doesn't work (no `fs` module)
|
||||
3. Cannot define Response before server start
|
||||
4. Cannot inline values:
|
||||
```typescript
|
||||
// ❌ Throws error
|
||||
.get('/', 'Hello Elysia')
|
||||
|
||||
// ✅ Works
|
||||
.get('/', () => 'Hello Elysia')
|
||||
```
|
||||
|
||||
## Static Files
|
||||
Use Cloudflare's built-in static serving:
|
||||
```json
|
||||
// wrangler.json
|
||||
{
|
||||
"assets": { "directory": "public" }
|
||||
}
|
||||
```
|
||||
|
||||
Structure:
|
||||
```
|
||||
├─ public
|
||||
│ ├─ kyuukurarin.mp4
|
||||
│ └─ static/mika.webp
|
||||
```
|
||||
|
||||
Access:
|
||||
- `http://localhost:8787/kyuukurarin.mp4`
|
||||
- `http://localhost:8787/static/mika.webp`
|
||||
|
||||
## Binding
|
||||
Import env from `cloudflare:workers`:
|
||||
```typescript
|
||||
import { env } from 'cloudflare:workers'
|
||||
|
||||
export default new Elysia({ adapter: CloudflareAdapter })
|
||||
.get('/', () => `Hello ${await env.KV.get('my-key')}`)
|
||||
.compile()
|
||||
```
|
||||
|
||||
## AoT Compilation
|
||||
As of Elysia 1.4.7, AoT works with Cloudflare Worker. Drop `aot: false` flag.
|
||||
|
||||
Cloudflare now supports Function compilation during startup.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
34
.opencode/skills/tech-stack/elysiajs/integrations/deno.md
Normal file
34
.opencode/skills/tech-stack/elysiajs/integrations/deno.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Deno Integration
|
||||
Run Elysia on Deno
|
||||
|
||||
## What It Is
|
||||
Run Elysia on Deno via Web Standard Request/Response.
|
||||
|
||||
## Setup
|
||||
Wrap `Elysia.fetch` in `Deno.serve`:
|
||||
```typescript
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/', () => 'Hello Elysia')
|
||||
.listen(3000)
|
||||
|
||||
Deno.serve(app.fetch)
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
deno serve --watch src/index.ts
|
||||
```
|
||||
|
||||
## Port Config
|
||||
```typescript
|
||||
Deno.serve(app.fetch) // Default
|
||||
Deno.serve({ port: 8787 }, app.fetch) // Custom port
|
||||
```
|
||||
|
||||
## pnpm
|
||||
[Inference] pnpm doesn't auto-install peer deps. Manual install required:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
258
.opencode/skills/tech-stack/elysiajs/integrations/drizzle.md
Normal file
258
.opencode/skills/tech-stack/elysiajs/integrations/drizzle.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Drizzle Integration
|
||||
Elysia + Drizzle integration guide
|
||||
|
||||
## What It Is
|
||||
Headless TypeScript ORM. Convert Drizzle schema → Elysia validation models via `drizzle-typebox`.
|
||||
|
||||
## Flow
|
||||
```
|
||||
Drizzle → drizzle-typebox → Elysia validation → OpenAPI + Eden Treaty
|
||||
```
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
bun add drizzle-orm drizzle-typebox
|
||||
```
|
||||
|
||||
### Pin TypeBox Version
|
||||
Prevent Symbol conflicts:
|
||||
```bash
|
||||
grep "@sinclair/typebox" node_modules/elysia/package.json
|
||||
```
|
||||
|
||||
Add to `package.json`:
|
||||
```json
|
||||
{
|
||||
"overrides": {
|
||||
"@sinclair/typebox": "0.32.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Drizzle Schema
|
||||
```typescript
|
||||
// src/database/schema.ts
|
||||
import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
export const user = pgTable('user', {
|
||||
id: varchar('id').$defaultFn(() => createId()).primaryKey(),
|
||||
username: varchar('username').notNull().unique(),
|
||||
password: varchar('password').notNull(),
|
||||
email: varchar('email').notNull().unique(),
|
||||
salt: varchar('salt', { length: 64 }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||
})
|
||||
|
||||
export const table = { user } as const
|
||||
export type Table = typeof table
|
||||
```
|
||||
|
||||
## drizzle-typebox
|
||||
```typescript
|
||||
import { t } from 'elysia'
|
||||
import { createInsertSchema } from 'drizzle-typebox'
|
||||
import { table } from './database/schema'
|
||||
|
||||
const _createUser = createInsertSchema(table.user, {
|
||||
email: t.String({ format: 'email' }) // Replace with Elysia type
|
||||
})
|
||||
|
||||
new Elysia()
|
||||
.post('/sign-up', ({ body }) => {}, {
|
||||
body: t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||
})
|
||||
```
|
||||
|
||||
## Type Instantiation Error
|
||||
**Error**: "Type instantiation is possibly infinite"
|
||||
|
||||
**Cause**: Circular reference when nesting drizzle-typebox into Elysia schema.
|
||||
|
||||
**Fix**: Explicitly define type between them:
|
||||
```typescript
|
||||
// ✅ Works
|
||||
const _createUser = createInsertSchema(table.user, {
|
||||
email: t.String({ format: 'email' })
|
||||
})
|
||||
const createUser = t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||
|
||||
// ❌ Infinite loop
|
||||
const createUser = t.Omit(
|
||||
createInsertSchema(table.user, { email: t.String({ format: 'email' }) }),
|
||||
['id', 'salt', 'createdAt']
|
||||
)
|
||||
```
|
||||
|
||||
Always declare variable for drizzle-typebox then reference it.
|
||||
|
||||
## Utility Functions
|
||||
Copy as-is for simplified usage:
|
||||
```typescript
|
||||
// src/database/utils.ts
|
||||
/**
|
||||
* @lastModified 2025-02-04
|
||||
* @see https://elysiajs.com/recipe/drizzle.html#utility
|
||||
*/
|
||||
|
||||
import { Kind, type TObject } from '@sinclair/typebox'
|
||||
import {
|
||||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
BuildSchema,
|
||||
} from 'drizzle-typebox'
|
||||
|
||||
import { table } from './schema'
|
||||
import type { Table } from 'drizzle-orm'
|
||||
|
||||
type Spread<
|
||||
T extends TObject | Table,
|
||||
Mode extends 'select' | 'insert' | undefined,
|
||||
> =
|
||||
T extends TObject<infer Fields>
|
||||
? {
|
||||
[K in keyof Fields]: Fields[K]
|
||||
}
|
||||
: T extends Table
|
||||
? Mode extends 'select'
|
||||
? BuildSchema<
|
||||
'select',
|
||||
T['_']['columns'],
|
||||
undefined
|
||||
>['properties']
|
||||
: Mode extends 'insert'
|
||||
? BuildSchema<
|
||||
'insert',
|
||||
T['_']['columns'],
|
||||
undefined
|
||||
>['properties']
|
||||
: {}
|
||||
: {}
|
||||
|
||||
/**
|
||||
* Spread a Drizzle schema into a plain object
|
||||
*/
|
||||
export const spread = <
|
||||
T extends TObject | Table,
|
||||
Mode extends 'select' | 'insert' | undefined,
|
||||
>(
|
||||
schema: T,
|
||||
mode?: Mode,
|
||||
): Spread<T, Mode> => {
|
||||
const newSchema: Record<string, unknown> = {}
|
||||
let table
|
||||
|
||||
switch (mode) {
|
||||
case 'insert':
|
||||
case 'select':
|
||||
if (Kind in schema) {
|
||||
table = schema
|
||||
break
|
||||
}
|
||||
|
||||
table =
|
||||
mode === 'insert'
|
||||
? createInsertSchema(schema)
|
||||
: createSelectSchema(schema)
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
if (!(Kind in schema)) throw new Error('Expect a schema')
|
||||
table = schema
|
||||
}
|
||||
|
||||
for (const key of Object.keys(table.properties))
|
||||
newSchema[key] = table.properties[key]
|
||||
|
||||
return newSchema as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Spread a Drizzle Table into a plain object
|
||||
*
|
||||
* If `mode` is 'insert', the schema will be refined for insert
|
||||
* If `mode` is 'select', the schema will be refined for select
|
||||
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
|
||||
*/
|
||||
export const spreads = <
|
||||
T extends Record<string, TObject | Table>,
|
||||
Mode extends 'select' | 'insert' | undefined,
|
||||
>(
|
||||
models: T,
|
||||
mode?: Mode,
|
||||
): {
|
||||
[K in keyof T]: Spread<T[K], Mode>
|
||||
} => {
|
||||
const newSchema: Record<string, unknown> = {}
|
||||
const keys = Object.keys(models)
|
||||
|
||||
for (const key of keys) newSchema[key] = spread(models[key], mode)
|
||||
|
||||
return newSchema as any
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
```typescript
|
||||
// ✅ Using spread
|
||||
const user = spread(table.user, 'insert')
|
||||
const createUser = t.Object({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
|
||||
// ⚠️ Using t.Pick
|
||||
const _createUser = createInsertSchema(table.user)
|
||||
const createUser = t.Pick(_createUser, ['id', 'username', 'password'])
|
||||
```
|
||||
|
||||
## Table Singleton Pattern
|
||||
```typescript
|
||||
// src/database/model.ts
|
||||
import { table } from './schema'
|
||||
import { spreads } from './utils'
|
||||
|
||||
export const db = {
|
||||
insert: spreads({ user: table.user }, 'insert'),
|
||||
select: spreads({ user: table.user }, 'select')
|
||||
} as const
|
||||
```
|
||||
|
||||
Usage:
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { db } from './database/model'
|
||||
const { user } = db.insert
|
||||
|
||||
new Elysia()
|
||||
.post('/sign-up', ({ body }) => {}, {
|
||||
body: t.Object({
|
||||
id: user.username,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Refinement
|
||||
```typescript
|
||||
// src/database/model.ts
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
|
||||
|
||||
export const db = {
|
||||
insert: spreads({
|
||||
user: createInsertSchema(table.user, {
|
||||
email: t.String({ format: 'email' })
|
||||
})
|
||||
}, 'insert'),
|
||||
select: spreads({
|
||||
user: createSelectSchema(table.user, {
|
||||
email: t.String({ format: 'email' })
|
||||
})
|
||||
}, 'select')
|
||||
} as const
|
||||
```
|
||||
|
||||
`spread` skips refined schemas.
|
||||
95
.opencode/skills/tech-stack/elysiajs/integrations/expo.md
Normal file
95
.opencode/skills/tech-stack/elysiajs/integrations/expo.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Expo Integration
|
||||
Run Elysia on Expo (React Native)
|
||||
|
||||
## What It Is
|
||||
Create API routes in Expo app (SDK 50+, App Router v3).
|
||||
|
||||
## Setup
|
||||
1. Create `app/[...slugs]+api.ts`
|
||||
2. Define Elysia server
|
||||
3. Export `Elysia.fetch` as HTTP methods
|
||||
|
||||
```typescript
|
||||
// app/[...slugs]+api.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/', 'hello Expo')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
## Prefix for Non-Root
|
||||
If placed in `app/api/[...slugs]+api.ts`, set prefix:
|
||||
```typescript
|
||||
const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', 'Hello Expo')
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
Ensures routing works in any location.
|
||||
|
||||
## Eden (End-to-End Type Safety)
|
||||
1. Export type:
|
||||
```typescript
|
||||
// app/[...slugs]+api.ts
|
||||
const app = new Elysia()
|
||||
.get('/', 'Hello Nextjs')
|
||||
.post('/user', ({ body }) => body, {
|
||||
body: treaty.schema('User', { name: 'string' })
|
||||
})
|
||||
|
||||
export type app = typeof app
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
2. Create client:
|
||||
```typescript
|
||||
// lib/eden.ts
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
import type { app } from '../app/[...slugs]+api'
|
||||
|
||||
export const api = treaty<app>('localhost:3000/api')
|
||||
```
|
||||
|
||||
3. Use in components:
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { api } from '../lib/eden'
|
||||
|
||||
export default async function Page() {
|
||||
const message = await api.get()
|
||||
return <h1>Hello, {message}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
- Deploy as normal Elysia app OR
|
||||
- Use experimental Expo server runtime
|
||||
|
||||
With Expo runtime:
|
||||
```bash
|
||||
expo export
|
||||
# Creates dist/server/_expo/functions/[...slugs]+api.js
|
||||
```
|
||||
|
||||
Edge function, not normal server (no port allocation).
|
||||
|
||||
### Adapters
|
||||
- Express
|
||||
- Netlify
|
||||
- Vercel
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
103
.opencode/skills/tech-stack/elysiajs/integrations/nextjs.md
Normal file
103
.opencode/skills/tech-stack/elysiajs/integrations/nextjs.md
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
# Next.js Integration
|
||||
|
||||
## What It Is
|
||||
Run Elysia on Next.js App Router.
|
||||
|
||||
## Setup
|
||||
1. Create `app/api/[[...slugs]]/route.ts`
|
||||
2. Define Elysia + export handlers:
|
||||
```typescript
|
||||
// app/api/[[...slugs]]/route.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', 'Hello Nextjs')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
WinterCG compliance - works as normal Next.js API route.
|
||||
|
||||
## Prefix for Non-Root
|
||||
If placed in `app/user/[[...slugs]]/route.ts`, set prefix:
|
||||
```typescript
|
||||
const app = new Elysia({ prefix: '/user' })
|
||||
.get('/', 'Hello Nextjs')
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
## Eden (End-to-End Type Safety)
|
||||
Isomorphic fetch pattern:
|
||||
- Server: Direct calls (no network)
|
||||
- Client: Network calls
|
||||
|
||||
1. Export type:
|
||||
```typescript
|
||||
// app/api/[[...slugs]]/route.ts
|
||||
export const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', 'Hello Nextjs')
|
||||
.post('/user', ({ body }) => body, {
|
||||
body: treaty.schema('User', { name: 'string' })
|
||||
})
|
||||
|
||||
export type app = typeof app
|
||||
|
||||
export const GET = app.fetch
|
||||
export const POST = app.fetch
|
||||
```
|
||||
|
||||
2. Create client:
|
||||
```typescript
|
||||
// lib/eden.ts
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
import type { app } from '../app/api/[[...slugs]]/route'
|
||||
|
||||
export const api =
|
||||
typeof process !== 'undefined'
|
||||
? treaty(app).api
|
||||
: treaty<typeof app>('localhost:3000').api
|
||||
```
|
||||
|
||||
Use `typeof process` not `typeof window` (window undefined at build time → hydration error).
|
||||
|
||||
3. Use in components:
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { api } from '../lib/eden'
|
||||
|
||||
export default async function Page() {
|
||||
const message = await api.get()
|
||||
return <h1>Hello, {message}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
Works with server/client components + ISR.
|
||||
|
||||
## React Query
|
||||
```tsx
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
function App() {
|
||||
const { data: response } = useQuery({
|
||||
queryKey: ['get'],
|
||||
queryFn: () => getTreaty().get()
|
||||
})
|
||||
|
||||
return response?.data
|
||||
}
|
||||
```
|
||||
|
||||
Works with all React Query features.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
64
.opencode/skills/tech-stack/elysiajs/integrations/nodejs.md
Normal file
64
.opencode/skills/tech-stack/elysiajs/integrations/nodejs.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Node.js Integration
|
||||
Run Elysia on Node.js
|
||||
|
||||
## What It Is
|
||||
Runtime adapter to run Elysia on Node.js.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
bun add elysia @elysiajs/node
|
||||
```
|
||||
|
||||
## Setup
|
||||
Apply node adapter:
|
||||
```typescript
|
||||
import { Elysia } from 'elysia'
|
||||
import { node } from '@elysiajs/node'
|
||||
|
||||
const app = new Elysia({ adapter: node() })
|
||||
.get('/', () => 'Hello Elysia')
|
||||
.listen(3000)
|
||||
```
|
||||
|
||||
## Additional Setup (Recommended)
|
||||
Install `tsx` for hot-reload:
|
||||
```bash
|
||||
bun add -d tsx @types/node typescript
|
||||
```
|
||||
|
||||
Scripts in `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc src/index.ts --outDir dist",
|
||||
"start": "NODE_ENV=production node dist/index.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **dev**: Hot-reload dev mode
|
||||
- **build**: Production build
|
||||
- **start**: Production server
|
||||
|
||||
Create `tsconfig.json`:
|
||||
```bash
|
||||
tsc --init
|
||||
```
|
||||
|
||||
Update strict mode:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provides hot-reload + JSX support similar to `bun dev`.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
67
.opencode/skills/tech-stack/elysiajs/integrations/nuxt.md
Normal file
67
.opencode/skills/tech-stack/elysiajs/integrations/nuxt.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Nuxt Integration
|
||||
|
||||
## What It Is
|
||||
Community plugin `nuxt-elysia` for Nuxt API routes with Eden Treaty.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
bun add elysia @elysiajs/eden
|
||||
bun add -d nuxt-elysia
|
||||
```
|
||||
|
||||
## Setup
|
||||
1. Add to Nuxt config:
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
modules: ['nuxt-elysia']
|
||||
})
|
||||
```
|
||||
|
||||
2. Create `api.ts` at project root:
|
||||
```typescript
|
||||
// api.ts
|
||||
export default () => new Elysia()
|
||||
.get('/hello', () => ({ message: 'Hello world!' }))
|
||||
```
|
||||
|
||||
3. Use Eden Treaty:
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ data.message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const { data } = await useAsyncData(async () => {
|
||||
const { data, error } = await $api.hello.get()
|
||||
|
||||
if (error) throw new Error('Failed to call API')
|
||||
|
||||
return data
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
Auto-setup on Nuxt API route.
|
||||
|
||||
## Prefix
|
||||
Default: `/_api`. Customize:
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
nuxtElysia: {
|
||||
path: '/api'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Mounts on `/api` instead of `/_api`.
|
||||
|
||||
See [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) for more config.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
93
.opencode/skills/tech-stack/elysiajs/integrations/prisma.md
Normal file
93
.opencode/skills/tech-stack/elysiajs/integrations/prisma.md
Normal file
@@ -0,0 +1,93 @@
|
||||
|
||||
# Prisma Integration
|
||||
Elysia + Prisma integration guide
|
||||
|
||||
## What It Is
|
||||
Type-safe ORM. Generate Elysia validation models from Prisma schema via `prismabox`.
|
||||
|
||||
## Flow
|
||||
```
|
||||
Prisma → prismabox → Elysia validation → OpenAPI + Eden Treaty
|
||||
```
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
bun add @prisma/client prismabox && \
|
||||
bun add -d prisma
|
||||
```
|
||||
|
||||
## Prisma Schema
|
||||
Add `prismabox` generator:
|
||||
```prisma
|
||||
// prisma/schema.prisma
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator prismabox {
|
||||
provider = "prismabox"
|
||||
typeboxImportDependencyName = "elysia"
|
||||
typeboxImportVariableName = "t"
|
||||
inputModel = true
|
||||
output = "../generated/prismabox"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
}
|
||||
```
|
||||
|
||||
Generates:
|
||||
- `User` → `generated/prismabox/User.ts`
|
||||
- `Post` → `generated/prismabox/Post.ts`
|
||||
|
||||
## Using Generated Models
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
import { PrismaClient } from '../generated/prisma'
|
||||
import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
new Elysia()
|
||||
.put('/', async ({ body }) =>
|
||||
prisma.user.create({ data: body }), {
|
||||
body: UserPlainInputCreate,
|
||||
response: UserPlain
|
||||
}
|
||||
)
|
||||
.get('/id/:id', async ({ params: { id }, status }) => {
|
||||
const user = await prisma.user.findUnique({ where: { id } })
|
||||
|
||||
if (!user) return status(404, 'User not found')
|
||||
|
||||
return user
|
||||
}, {
|
||||
response: {
|
||||
200: UserPlain,
|
||||
404: t.String()
|
||||
}
|
||||
})
|
||||
.listen(3000)
|
||||
```
|
||||
|
||||
Reuses DB schema in Elysia validation models.
|
||||
134
.opencode/skills/tech-stack/elysiajs/integrations/react-email.md
Normal file
134
.opencode/skills/tech-stack/elysiajs/integrations/react-email.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# React Email Integration
|
||||
|
||||
## What It Is
|
||||
Use React components to create emails. Direct JSX import via Bun.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
bun add -d react-email
|
||||
bun add @react-email/components react react-dom
|
||||
```
|
||||
|
||||
Script in `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"email": "email dev --dir src/emails"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Email templates → `src/emails` directory.
|
||||
|
||||
### TypeScript
|
||||
Add to `tsconfig.json`:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Email Template
|
||||
```tsx
|
||||
// src/emails/otp.tsx
|
||||
import * as React from 'react'
|
||||
import { Tailwind, Section, Text } from '@react-email/components'
|
||||
|
||||
export default function OTPEmail({ otp }: { otp: number }) {
|
||||
return (
|
||||
<Tailwind>
|
||||
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
|
||||
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
|
||||
<Text className="text-xs font-medium text-violet-500">
|
||||
Verify your Email Address
|
||||
</Text>
|
||||
<Text className="text-gray-500 my-0">
|
||||
Use the following code to verify your email address
|
||||
</Text>
|
||||
<Text className="text-5xl font-bold pt-2">{otp}</Text>
|
||||
<Text className="text-gray-400 font-light text-xs pb-4">
|
||||
This code is valid for 10 minutes
|
||||
</Text>
|
||||
<Text className="text-gray-600 text-xs">
|
||||
Thank you for joining us
|
||||
</Text>
|
||||
</Section>
|
||||
</Section>
|
||||
</Tailwind>
|
||||
)
|
||||
}
|
||||
|
||||
OTPEmail.PreviewProps = { otp: 123456 }
|
||||
```
|
||||
|
||||
`@react-email/components` → email-client compatible (Gmail, Outlook). Tailwind support.
|
||||
|
||||
`PreviewProps` → playground only.
|
||||
|
||||
## Preview
|
||||
```bash
|
||||
bun email
|
||||
```
|
||||
|
||||
Opens browser with preview.
|
||||
|
||||
## Send Email
|
||||
Render with `react-dom/server`, submit via provider:
|
||||
|
||||
### Nodemailer
|
||||
```typescript
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import OTPEmail from './emails/otp'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: 'smtp.gehenna.sh',
|
||||
port: 465,
|
||||
auth: { user: 'makoto', pass: '12345678' }
|
||||
})
|
||||
|
||||
.get('/otp', async ({ body }) => {
|
||||
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||
const html = renderToStaticMarkup(<OTPEmail otp={otp} />)
|
||||
|
||||
await transporter.sendMail({
|
||||
from: '[email protected]',
|
||||
to: body,
|
||||
subject: 'Verify your email address',
|
||||
html
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}, {
|
||||
body: t.String({ format: 'email' })
|
||||
})
|
||||
```
|
||||
|
||||
### Resend
|
||||
```typescript
|
||||
import OTPEmail from './emails/otp'
|
||||
import Resend from 'resend'
|
||||
|
||||
const resend = new Resend('re_123456789')
|
||||
|
||||
.get('/otp', ({ body }) => {
|
||||
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||
|
||||
await resend.emails.send({
|
||||
from: '[email protected]',
|
||||
to: body,
|
||||
subject: 'Verify your email address',
|
||||
html: <OTPEmail otp={otp} /> // Direct JSX
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
```
|
||||
|
||||
Direct JSX import thanks to Bun.
|
||||
|
||||
Other providers: AWS SES, SendGrid.
|
||||
|
||||
See [React Email Integrations](https://react.email/docs/integrations/overview).
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
# SvelteKit Integration
|
||||
|
||||
## What It Is
|
||||
Run Elysia on SvelteKit server routes.
|
||||
|
||||
## Setup
|
||||
1. Create `src/routes/[...slugs]/+server.ts`
|
||||
2. Define Elysia server
|
||||
3. Export fallback handler:
|
||||
```typescript
|
||||
// src/routes/[...slugs]/+server.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/', 'hello SvelteKit')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
interface WithRequest {
|
||||
request: Request
|
||||
}
|
||||
|
||||
export const fallback = ({ request }: WithRequest) => app.handle(request)
|
||||
```
|
||||
|
||||
Treat as normal SvelteKit server route.
|
||||
|
||||
## Prefix for Non-Root
|
||||
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
|
||||
```typescript
|
||||
// src/routes/api/[...slugs]/+server.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', () => 'hi')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
|
||||
|
||||
export const fallback: RequestHandler = ({ request }) => app.handle(request)
|
||||
```
|
||||
|
||||
Ensures routing works in any location.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
# Tanstack Start Integration
|
||||
|
||||
## What It Is
|
||||
Elysia runs inside Tanstack Start server routes.
|
||||
|
||||
## Setup
|
||||
1. Create `src/routes/api.$.ts`
|
||||
2. Define Elysia server
|
||||
3. Export handlers in `server.handlers`:
|
||||
```typescript
|
||||
// src/routes/api.$.ts
|
||||
import { Elysia } from 'elysia'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||
|
||||
const app = new Elysia({
|
||||
prefix: '/api'
|
||||
}).get('/', 'Hello Elysia!')
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||
|
||||
export const Route = createFileRoute('/api/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Runs on `/api`. Add methods to `server.handlers` as needed.
|
||||
|
||||
## Eden (End-to-End Type Safety)
|
||||
Isomorphic pattern with `createIsomorphicFn`:
|
||||
```typescript
|
||||
// src/routes/api.$.ts
|
||||
export const getTreaty = createIsomorphicFn()
|
||||
.server(() => treaty(app).api)
|
||||
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||
```
|
||||
|
||||
- Server: Direct call (no HTTP overhead)
|
||||
- Client: HTTP call
|
||||
|
||||
## Loader Data
|
||||
Fetch before render:
|
||||
```tsx
|
||||
// src/routes/index.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { getTreaty } from './api.$'
|
||||
|
||||
export const Route = createFileRoute('/a')({
|
||||
component: App,
|
||||
loader: () => getTreaty().get().then((res) => res.data)
|
||||
})
|
||||
|
||||
function App() {
|
||||
const data = Route.useLoaderData()
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
Executed server-side during SSR. No HTTP overhead. Type-safe.
|
||||
|
||||
## React Query
|
||||
```tsx
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getTreaty } from './api.$'
|
||||
|
||||
function App() {
|
||||
const { data: response } = useQuery({
|
||||
queryKey: ['get'],
|
||||
queryFn: () => getTreaty().get()
|
||||
})
|
||||
|
||||
return response?.data
|
||||
}
|
||||
```
|
||||
|
||||
Works with all React Query features.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
55
.opencode/skills/tech-stack/elysiajs/integrations/vercel.md
Normal file
55
.opencode/skills/tech-stack/elysiajs/integrations/vercel.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Vercel Integration
|
||||
Deploy Elysia on Vercel
|
||||
|
||||
## What It Is
|
||||
Zero-config deployment on Vercel (Bun or Node runtime).
|
||||
|
||||
## Setup
|
||||
1. Create/import Elysia server in `src/index.ts`
|
||||
2. Export as default:
|
||||
```typescript
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
export default new Elysia()
|
||||
.get('/', () => 'Hello Vercel Function')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
```
|
||||
|
||||
3. Develop locally:
|
||||
```bash
|
||||
vc dev
|
||||
```
|
||||
|
||||
4. Deploy:
|
||||
```bash
|
||||
vc deploy
|
||||
```
|
||||
|
||||
## Node.js Runtime
|
||||
Set in `package.json`:
|
||||
```json
|
||||
{
|
||||
"name": "elysia-app",
|
||||
"type": "module"
|
||||
}
|
||||
```
|
||||
|
||||
## Bun Runtime
|
||||
Set in `vercel.json`:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"bunVersion": "1.x"
|
||||
}
|
||||
```
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
Vercel has zero config for Elysia. For additional config, see [Vercel docs](https://vercel.com/docs/frameworks/backend/elysia).
|
||||
Reference in New Issue
Block a user