📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,446 @@
# SQLite CRUD Patterns
Complete examples for database operations with bun:sqlite.
## Basic Repository
```typescript
import { Database } from 'bun:sqlite';
type User = {
id: string;
email: string;
name: string;
createdAt: Date;
};
type UserRow = {
id: string;
email: string;
name: string;
created_at: string;
};
class UserRepository {
private stmt: {
findById: ReturnType<Database['prepare']>;
findByEmail: ReturnType<Database['prepare']>;
findAll: ReturnType<Database['prepare']>;
create: ReturnType<Database['prepare']>;
update: ReturnType<Database['prepare']>;
delete: ReturnType<Database['prepare']>;
};
constructor(private db: Database) {
// Initialize schema
this.db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Prepare statements once
this.stmt = {
findById: this.db.prepare('SELECT * FROM users WHERE id = ?'),
findByEmail: this.db.prepare('SELECT * FROM users WHERE email = ?'),
findAll: this.db.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ?'),
create: this.db.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *'),
update: this.db.prepare('UPDATE users SET email = ?, name = ? WHERE id = ? RETURNING *'),
delete: this.db.prepare('DELETE FROM users WHERE id = ? RETURNING *')
};
}
findById(id: string): User | null {
const row = this.stmt.findById.get(id) as UserRow | null;
return row ? this.mapRow(row) : null;
}
findByEmail(email: string): User | null {
const row = this.stmt.findByEmail.get(email) as UserRow | null;
return row ? this.mapRow(row) : null;
}
findAll(limit = 100): User[] {
const rows = this.stmt.findAll.all(limit) as UserRow[];
return rows.map(row => this.mapRow(row));
}
create(data: { email: string; name: string }): User {
const id = crypto.randomUUID();
const row = this.stmt.create.get(id, data.email, data.name) as UserRow;
return this.mapRow(row);
}
update(id: string, data: { email?: string; name?: string }): User | null {
const existing = this.findById(id);
if (!existing) return null;
const row = this.stmt.update.get(
data.email ?? existing.email,
data.name ?? existing.name,
id
) as UserRow;
return this.mapRow(row);
}
delete(id: string): boolean {
const row = this.stmt.delete.get(id);
return row !== null;
}
private mapRow(row: UserRow): User {
return {
id: row.id,
email: row.email,
name: row.name,
createdAt: new Date(row.created_at)
};
}
}
```
## Usage
```typescript
const db = new Database('app.db');
const users = new UserRepository(db);
// Create
const user = users.create({
email: 'alice@example.com',
name: 'Alice'
});
console.log('Created:', user.id);
// Read
const found = users.findById(user.id);
console.log('Found:', found?.email);
// Update
const updated = users.update(user.id, { name: 'Alice Smith' });
console.log('Updated:', updated?.name);
// Delete
const deleted = users.delete(user.id);
console.log('Deleted:', deleted);
// List
const allUsers = users.findAll(10);
console.log('All users:', allUsers.length);
db.close();
```
## With Transactions
```typescript
class AccountRepository {
constructor(private db: Database) {
this.db.run(`
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
balance INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
}
transfer = this.db.transaction((fromId: string, toId: string, amount: number) => {
// Check balance
const from = this.db.prepare('SELECT balance FROM accounts WHERE id = ?').get(fromId) as { balance: number } | null;
if (!from) {
throw new Error('Source account not found');
}
if (from.balance < amount) {
throw new Error('Insufficient funds');
}
// Debit
this.db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').run(amount, fromId);
// Credit
this.db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').run(amount, toId);
return { fromId, toId, amount };
});
bulkCreate = this.db.transaction((accounts: Array<{ userId: string; balance: number }>) => {
const stmt = this.db.prepare('INSERT INTO accounts (id, user_id, balance) VALUES (?, ?, ?)');
const created = [];
for (const account of accounts) {
const id = crypto.randomUUID();
stmt.run(id, account.userId, account.balance);
created.push(id);
}
return created;
});
}
```
## Pagination
```typescript
type PaginatedResult<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
};
class UserRepository {
// ... other methods
findPaginated(page: number, pageSize: number): PaginatedResult<User> {
const offset = (page - 1) * pageSize;
const countResult = this.db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number };
const total = countResult.count;
const rows = this.db.prepare(`
SELECT * FROM users
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`).all(pageSize, offset) as UserRow[];
return {
items: rows.map(row => this.mapRow(row)),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
};
}
}
// Usage
const result = users.findPaginated(1, 20);
console.log(`Page ${result.page} of ${result.totalPages}`);
console.log(`Showing ${result.items.length} of ${result.total} users`);
```
## Search with Full-Text
```typescript
class PostRepository {
constructor(private db: Database) {
// Create main table
this.db.run(`
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
user_id TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create FTS index
this.db.run(`
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
title,
content,
content='posts',
content_rowid='rowid'
)
`);
// Triggers to keep FTS in sync
this.db.run(`
CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
INSERT INTO posts_fts(rowid, title, content)
VALUES (new.rowid, new.title, new.content);
END
`);
this.db.run(`
CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, title, content)
VALUES ('delete', old.rowid, old.title, old.content);
END
`);
this.db.run(`
CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, title, content)
VALUES ('delete', old.rowid, old.title, old.content);
INSERT INTO posts_fts(rowid, title, content)
VALUES (new.rowid, new.title, new.content);
END
`);
}
search(query: string, limit = 20) {
return this.db.prepare(`
SELECT posts.*, bm25(posts_fts) as rank
FROM posts
JOIN posts_fts ON posts.rowid = posts_fts.rowid
WHERE posts_fts MATCH ?
ORDER BY rank
LIMIT ?
`).all(query, limit);
}
}
// Usage
const posts = new PostRepository(db);
const results = posts.search('typescript tutorial');
```
## JSON Storage
```typescript
type Settings = {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
};
class SettingsRepository {
constructor(private db: Database) {
this.db.run(`
CREATE TABLE IF NOT EXISTS settings (
user_id TEXT PRIMARY KEY,
data TEXT NOT NULL
)
`);
}
get(userId: string): Settings | null {
const row = this.db.prepare('SELECT data FROM settings WHERE user_id = ?').get(userId) as { data: string } | null;
if (!row) return null;
return JSON.parse(row.data);
}
set(userId: string, settings: Settings): void {
this.db.prepare(`
INSERT INTO settings (user_id, data) VALUES (?, ?)
ON CONFLICT (user_id) DO UPDATE SET data = excluded.data
`).run(userId, JSON.stringify(settings));
}
update(userId: string, partial: Partial<Settings>): Settings | null {
const existing = this.get(userId);
if (!existing) return null;
const updated = { ...existing, ...partial };
this.set(userId, updated);
return updated;
}
// Query JSON fields directly
findByTheme(theme: 'light' | 'dark') {
return this.db.prepare(`
SELECT user_id FROM settings
WHERE json_extract(data, '$.theme') = ?
`).all(theme);
}
}
```
## With Hono API
```typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { Database } from 'bun:sqlite';
import { createFactory } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
type Env = {
Variables: {
db: Database;
users: UserRepository;
};
};
const factory = createFactory<Env>();
const dbMiddleware = factory.createMiddleware(async (c, next) => {
const db = new Database('app.db');
const users = new UserRepository(db);
c.set('db', db);
c.set('users', users);
try {
await next();
} finally {
db.close();
}
});
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100)
});
const UpdateUserSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(1).max(100).optional()
});
const app = factory.createApp()
.use('*', dbMiddleware)
.get('/users', (c) => {
const users = c.get('users');
const limit = Number(c.req.query('limit')) || 20;
return c.json({ users: users.findAll(limit) });
})
.get('/users/:id', (c) => {
const users = c.get('users');
const user = users.findById(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
})
.post('/users', zValidator('json', CreateUserSchema), (c) => {
const users = c.get('users');
const data = c.req.valid('json');
const existing = users.findByEmail(data.email);
if (existing) {
throw new HTTPException(409, { message: 'Email already registered' });
}
const user = users.create(data);
return c.json({ user }, 201);
})
.patch('/users/:id', zValidator('json', UpdateUserSchema), (c) => {
const users = c.get('users');
const data = c.req.valid('json');
const user = users.update(c.req.param('id'), data);
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
})
.delete('/users/:id', (c) => {
const users = c.get('users');
const deleted = users.delete(c.req.param('id'));
if (!deleted) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ deleted: true });
});
export default app;
```
@@ -0,0 +1,427 @@
# File Upload Patterns
Streaming file handling with Bun.file and Bun.write.
## Basic Upload
```typescript
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
const app = new Hono();
app.post('/upload', async (c) => {
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
const filename = `${crypto.randomUUID()}-${file.name}`;
const filepath = `./uploads/${filename}`;
await Bun.write(filepath, file);
return c.json({
filename,
size: file.size,
type: file.type
}, 201);
});
```
## With Validation
```typescript
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
app.post('/upload/image', async (c) => {
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
// Validate type
if (!ALLOWED_TYPES.includes(file.type)) {
throw new HTTPException(400, {
message: `Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}`
});
}
// Validate size
if (file.size > MAX_SIZE) {
throw new HTTPException(400, {
message: `File too large. Max size: ${MAX_SIZE / 1024 / 1024}MB`
});
}
// Generate safe filename
const ext = file.name.split('.').pop()?.toLowerCase() || 'bin';
const filename = `${crypto.randomUUID()}.${ext}`;
const filepath = `./uploads/${filename}`;
await Bun.write(filepath, file);
return c.json({ filename, size: file.size, type: file.type }, 201);
});
```
## Multiple Files
```typescript
app.post('/upload/multiple', async (c) => {
const body = await c.req.parseBody({ all: true });
const files = body.files as File[];
if (!files || files.length === 0) {
throw new HTTPException(400, { message: 'At least one file is required' });
}
const results = [];
for (const file of files) {
if (file.size > MAX_SIZE) {
throw new HTTPException(400, {
message: `File ${file.name} exceeds max size`
});
}
const ext = file.name.split('.').pop()?.toLowerCase() || 'bin';
const filename = `${crypto.randomUUID()}.${ext}`;
const filepath = `./uploads/${filename}`;
await Bun.write(filepath, file);
results.push({
original: file.name,
filename,
size: file.size,
type: file.type
});
}
return c.json({ files: results }, 201);
});
```
## Streaming Large Files
```typescript
app.post('/upload/large', async (c) => {
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
const filename = `${crypto.randomUUID()}.bin`;
const filepath = `./uploads/${filename}`;
// Stream directly to disk — efficient for large files
await Bun.write(filepath, file.stream());
return c.json({ filename, size: file.size }, 201);
});
```
## Download Files
```typescript
app.get('/files/:filename', async (c) => {
const filename = c.req.param('filename');
// Prevent directory traversal
if (filename.includes('..') || filename.includes('/')) {
throw new HTTPException(400, { message: 'Invalid filename' });
}
const filepath = `./uploads/${filename}`;
const file = Bun.file(filepath);
if (!(await file.exists())) {
throw new HTTPException(404, { message: 'File not found' });
}
return c.body(file.stream(), {
headers: {
'Content-Type': file.type,
'Content-Length': file.size.toString(),
'Content-Disposition': `attachment; filename="${filename}"`
}
});
});
```
## Inline Display (Images)
```typescript
app.get('/images/:filename', async (c) => {
const filename = c.req.param('filename');
if (filename.includes('..') || filename.includes('/')) {
throw new HTTPException(400, { message: 'Invalid filename' });
}
const filepath = `./uploads/${filename}`;
const file = Bun.file(filepath);
if (!(await file.exists())) {
throw new HTTPException(404, { message: 'File not found' });
}
// Verify it's an image
if (!file.type.startsWith('image/')) {
throw new HTTPException(400, { message: 'Not an image' });
}
return c.body(file.stream(), {
headers: {
'Content-Type': file.type,
'Content-Length': file.size.toString(),
'Cache-Control': 'public, max-age=31536000' // 1 year cache
}
});
});
```
## With Database Metadata
```typescript
import { Database } from 'bun:sqlite';
type FileRecord = {
id: string;
filename: string;
originalName: string;
mimeType: string;
size: number;
uploadedAt: string;
userId: string;
};
class FileRepository {
constructor(private db: Database) {
this.db.run(`
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
filename TEXT UNIQUE NOT NULL,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size INTEGER NOT NULL,
uploaded_at TEXT DEFAULT CURRENT_TIMESTAMP,
user_id TEXT NOT NULL
)
`);
}
create(data: Omit<FileRecord, 'id' | 'uploadedAt'>): FileRecord {
const id = crypto.randomUUID();
return this.db.prepare(`
INSERT INTO files (id, filename, original_name, mime_type, size, user_id)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING *
`).get(id, data.filename, data.originalName, data.mimeType, data.size, data.userId) as FileRecord;
}
findById(id: string): FileRecord | null {
return this.db.prepare('SELECT * FROM files WHERE id = ?').get(id) as FileRecord | null;
}
findByUser(userId: string): FileRecord[] {
return this.db.prepare('SELECT * FROM files WHERE user_id = ? ORDER BY uploaded_at DESC').all(userId) as FileRecord[];
}
delete(id: string): boolean {
const result = this.db.prepare('DELETE FROM files WHERE id = ? RETURNING filename').get(id) as { filename: string } | null;
return result !== null;
}
}
// API with metadata
app.post('/files', async (c) => {
const userId = c.get('userId'); // From auth middleware
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
const ext = file.name.split('.').pop()?.toLowerCase() || 'bin';
const filename = `${crypto.randomUUID()}.${ext}`;
const filepath = `./uploads/${filename}`;
await Bun.write(filepath, file);
const files = c.get('files') as FileRepository;
const record = files.create({
filename,
originalName: file.name,
mimeType: file.type,
size: file.size,
userId
});
return c.json({ file: record }, 201);
});
app.delete('/files/:id', async (c) => {
const files = c.get('files') as FileRepository;
const record = files.findById(c.req.param('id'));
if (!record) {
throw new HTTPException(404, { message: 'File not found' });
}
// Delete from disk
const filepath = `./uploads/${record.filename}`;
const file = Bun.file(filepath);
if (await file.exists()) {
await Bun.write(filepath, ''); // Clear file
// Or use node:fs for actual deletion
}
// Delete from database
files.delete(record.id);
return c.json({ deleted: true });
});
```
## Image Processing
```typescript
import sharp from 'sharp'; // npm install sharp
const THUMBNAIL_SIZE = 200;
app.post('/upload/image-with-thumbnail', async (c) => {
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
if (!file.type.startsWith('image/')) {
throw new HTTPException(400, { message: 'Must be an image' });
}
const id = crypto.randomUUID();
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
// Save original
const originalPath = `./uploads/${id}.${ext}`;
const buffer = await file.arrayBuffer();
await Bun.write(originalPath, buffer);
// Create thumbnail
const thumbnailPath = `./uploads/${id}-thumb.${ext}`;
await sharp(Buffer.from(buffer))
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, { fit: 'cover' })
.toFile(thumbnailPath);
return c.json({
id,
original: `${id}.${ext}`,
thumbnail: `${id}-thumb.${ext}`,
size: file.size,
type: file.type
}, 201);
});
```
## Presigned URLs (S3-style)
```typescript
import { sign, verify } from 'hono/jwt';
const SECRET = Bun.env.JWT_SECRET!;
const EXPIRY = 3600; // 1 hour
// Generate presigned URL
app.post('/files/:id/presign', async (c) => {
const fileId = c.req.param('id');
const files = c.get('files') as FileRepository;
const record = files.findById(fileId);
if (!record) {
throw new HTTPException(404, { message: 'File not found' });
}
const token = await sign({
fileId,
exp: Math.floor(Date.now() / 1000) + EXPIRY
}, SECRET);
const url = `${c.req.url.split('/files')[0]}/download?token=${token}`;
return c.json({ url, expiresIn: EXPIRY });
});
// Download with presigned URL
app.get('/download', async (c) => {
const token = c.req.query('token');
if (!token) {
throw new HTTPException(401, { message: 'Token required' });
}
try {
const payload = await verify(token, SECRET);
const fileId = payload.fileId as string;
const files = c.get('files') as FileRepository;
const record = files.findById(fileId);
if (!record) {
throw new HTTPException(404, { message: 'File not found' });
}
const filepath = `./uploads/${record.filename}`;
const file = Bun.file(filepath);
return c.body(file.stream(), {
headers: {
'Content-Type': record.mimeType,
'Content-Length': record.size.toString(),
'Content-Disposition': `attachment; filename="${record.originalName}"`
}
});
} catch {
throw new HTTPException(401, { message: 'Invalid or expired token' });
}
});
```
## Cleanup Old Files
```typescript
import { readdir, unlink, stat } from 'node:fs/promises';
import { join } from 'node:path';
async function cleanupOldFiles(directory: string, maxAgeDays: number) {
const files = await readdir(directory);
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
for (const filename of files) {
const filepath = join(directory, filename);
const stats = await stat(filepath);
if (stats.mtimeMs < cutoff) {
await unlink(filepath);
console.log(`Deleted old file: ${filename}`);
}
}
}
// Run cleanup every hour
setInterval(() => {
cleanupOldFiles('./uploads', 30); // Delete files older than 30 days
}, 60 * 60 * 1000);
```