Astro กับ Turso: SQLite บน Edge

#astro13 เม.ย. 2569

Astro กับ Turso: SQLite บน Edge

Turso เป็น database platform ที่สร้างบน libSQL (fork ของ SQLite) ออกแบบมาสำหรับ edge deployments ด้วย latency ต่ำมากและ global replication ทำให้ Turso เป็นตัวเลือกที่น่าสนใจสำหรับ Astro applications ที่ต้องการ database ที่เร็วและ scalable

ทำไมต้องใช้ Turso?

  • Edge-native: ออกแบบมาสำหรับ edge deployments
  • SQLite Compatible: ใช้ SQL syntax ที่คุ้นเคย
  • Low Latency: database อยู่ใกล้ผู้ใช้
  • Embedded Replicas: sync ข้อมูลมาไว้ local
  • Generous Free Tier: 500 databases ฟรี

การติดตั้ง

# ติดตั้ง Turso CLI
curl -sSfL https://get.tur.so/install.sh | bash

# Login
turso auth login

# สร้าง database
turso db create my-astro-db

# ดู connection URL
turso db show my-astro-db

# สร้าง auth token
turso db tokens create my-astro-db

ติดตั้ง client library:

npm install @libsql/client

ตั้งค่า environment variables:

# .env
TURSO_DATABASE_URL=libsql://your-db.turso.io
TURSO_AUTH_TOKEN=your-auth-token

สร้าง Database Client

// src/lib/turso.ts
import { createClient } from '@libsql/client';

export const turso = createClient({
  url: import.meta.env.TURSO_DATABASE_URL,
  authToken: import.meta.env.TURSO_AUTH_TOKEN,
});

// สำหรับ local development
export const localTurso = createClient({
  url: 'file:local.db',
});

สร้าง Tables

// src/db/migrate.ts
import { turso } from '../lib/turso';

export async function runMigrations() {
  await turso.executeMultiple(`
    CREATE TABLE IF NOT EXISTS users (
      id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
      email TEXT UNIQUE NOT NULL,
      name TEXT NOT NULL,
      role TEXT NOT NULL DEFAULT 'user',
      created_at INTEGER NOT NULL DEFAULT (unixepoch())
    );

    CREATE TABLE IF NOT EXISTS posts (
      id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
      title TEXT NOT NULL,
      slug TEXT UNIQUE NOT NULL,
      content TEXT NOT NULL,
      published INTEGER NOT NULL DEFAULT 0,
      author_id TEXT NOT NULL,
      view_count INTEGER NOT NULL DEFAULT 0,
      created_at INTEGER NOT NULL DEFAULT (unixepoch()),
      updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
      FOREIGN KEY (author_id) REFERENCES users(id)
    );

    CREATE TABLE IF NOT EXISTS tags (
      id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
      name TEXT UNIQUE NOT NULL
    );

    CREATE TABLE IF NOT EXISTS post_tags (
      post_id TEXT NOT NULL,
      tag_id TEXT NOT NULL,
      PRIMARY KEY (post_id, tag_id),
      FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
      FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
    );

    CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
    CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published);
    CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(author_id);
  `);

  console.log('Migrations completed!');
}

CRUD Operations

// src/lib/db.ts
import { turso } from './turso';

export interface Post {
  id: string;
  title: string;
  slug: string;
  content: string;
  published: boolean;
  authorId: string;
  viewCount: number;
  createdAt: number;
}

export async function getPosts(options?: {
  limit?: number;
  offset?: number;
  published?: boolean;
}) {
  const limit = options?.limit ?? 10;
  const offset = options?.offset ?? 0;
  const published = options?.published ?? true;

  const result = await turso.execute({
    sql: `
      SELECT p.*, u.name as author_name
      FROM posts p
      JOIN users u ON p.author_id = u.id
      WHERE p.published = ?
      ORDER BY p.created_at DESC
      LIMIT ? OFFSET ?
    `,
    args: [published ? 1 : 0, limit, offset],
  });

  return result.rows.map((row) => ({
    id: row.id as string,
    title: row.title as string,
    slug: row.slug as string,
    content: row.content as string,
    published: row.published === 1,
    authorId: row.author_id as string,
    authorName: row.author_name as string,
    viewCount: row.view_count as number,
    createdAt: row.created_at as number,
  }));
}

export async function getPostBySlug(slug: string) {
  const result = await turso.execute({
    sql: `
      SELECT p.*, u.name as author_name, u.email as author_email
      FROM posts p
      JOIN users u ON p.author_id = u.id
      WHERE p.slug = ? AND p.published = 1
      LIMIT 1
    `,
    args: [slug],
  });

  if (result.rows.length === 0) return null;

  const row = result.rows[0];
  return {
    id: row.id as string,
    title: row.title as string,
    slug: row.slug as string,
    content: row.content as string,
    authorName: row.author_name as string,
    viewCount: row.view_count as number,
  };
}

export async function createPost(data: {
  title: string;
  slug: string;
  content: string;
  authorId: string;
}) {
  const result = await turso.execute({
    sql: `
      INSERT INTO posts (title, slug, content, author_id)
      VALUES (?, ?, ?, ?)
      RETURNING *
    `,
    args: [data.title, data.slug, data.content, data.authorId],
  });

  return result.rows[0];
}

export async function incrementViewCount(id: string) {
  await turso.execute({
    sql: 'UPDATE posts SET view_count = view_count + 1 WHERE id = ?',
    args: [id],
  });
}

Drizzle ORM กับ Turso

ใช้ Drizzle ORM สำหรับ type-safety:

// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const posts = sqliteTable('posts', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  slug: text('slug').notNull().unique(),
  content: text('content').notNull(),
  published: integer('published', { mode: 'boolean' }).default(false),
  authorId: text('author_id').notNull(),
  viewCount: integer('view_count').default(0),
  createdAt: integer('created_at', { mode: 'timestamp' }),
});
// src/lib/drizzleTurso.ts
import { drizzle } from 'drizzle-orm/libsql';
import { turso } from './turso';
import * as schema from '../db/schema';

export const db = drizzle(turso, { schema });

Embedded Replicas

สำหรับ performance สูงสุด ใช้ embedded replicas:

// src/lib/tursoEmbedded.ts
import { createClient } from '@libsql/client';

export const tursoEmbedded = createClient({
  url: 'file:local-replica.db',
  syncUrl: import.meta.env.TURSO_DATABASE_URL,
  authToken: import.meta.env.TURSO_AUTH_TOKEN,
  syncInterval: 60, // sync ทุก 60 วินาที
});

// Sync ก่อนใช้งาน
await tursoEmbedded.sync();

สรุป

Turso กับ Astro เป็นคู่ที่ยอดเยี่ยมสำหรับ edge deployments ด้วย SQLite compatibility, low latency, และ global replication ทำให้ database queries เร็วมากไม่ว่าผู้ใช้จะอยู่ที่ไหนในโลก เหมาะสำหรับ applications ที่ต้องการ performance สูงและ cost-effective