Turso و Drizzle ORM مع Next.js: قواعد بيانات جاهزة للحوسبة الطرفية في 2026

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

ما ستتعلمه: كيفية ربط Turso (قاعدة بيانات SQLite موزعة) مع Next.js 15 عبر Drizzle ORM، وإنشاء مخططات آمنة النوع، وتنفيذ عمليات الترحيل، والنشر على Vercel Edge Functions بزمن استجابة أقل من 10 مللي ثانية حول العالم.

المقدمة

تعمل قواعد البيانات التقليدية مثل PostgreSQL و MySQL من مركز بيانات واحد. عندما يستعلم مستخدم في تونس من خادم في باريس، يضيف زمن الشبكة من 30 إلى 80 مللي ثانية لكل استعلام. اضرب ذلك في 5 استعلامات لكل صفحة وستحصل على نصف ثانية من التأخير الذي لا مفر منه.

Turso يحل هذه المشكلة باستخدام LibSQL، وهو fork مفتوح المصدر من SQLite مصمم للتوزيع على الحوسبة الطرفية. يتم نسخ قاعدة بياناتك تلقائيًا عبر عشرات المناطق. مع Drizzle ORM — وهو ORM خفيف وآمن النوع لـ TypeScript — و Edge Functions في Next.js 15، تحصل على حزمة كاملة حيث يُقدَّم كل استعلام من أقرب مركز بيانات للمستخدم.

في هذا الدليل، سنبني تطبيقًا لإدارة الإشارات المرجعية (bookmarks) مع عمليات CRUD كاملة ونشر جاهز للحوسبة الطرفية.

المتطلبات الأساسية

قبل البدء، تأكد من أن لديك:

  • Node.js 20+ مثبت (node --version)
  • حساب Turso مجاني على turso.tech
  • Turso CLI مثبت: brew install tursodatabase/tap/turso (macOS) أو curl -sSfL https://get.tur.so/install.sh | bash (Linux)
  • معرفة أساسية بـ Next.js App Router و TypeScript
  • محرر كود (يُنصح بـ VS Code)

ما ستبنيه

تطبيق إشارات مرجعية بالميزات التالية:

  • إنشاء وقراءة وتحديث وحذف الإشارات المرجعية
  • تصنيف بالوسوم
  • بحث نصي كامل
  • مسارات API تعمل على Edge Runtime
  • زمن استجابة أقل من 10 مللي ثانية بفضل نسخ Turso

الخطوة 1: إنشاء مشروع Next.js

ابدأ بتهيئة مشروع Next.js 15 جديد مع TypeScript:

npx create-next-app@latest turso-bookmarks --typescript --tailwind --eslint --app --src-dir
cd turso-bookmarks

اختر الخيارات الافتراضية عندما يطلب منك CLI. يجب أن يكون لديك هيكل مشروع قياسي مع مجلد src/app/.

الخطوة 2: إعداد Turso

المصادقة عبر CLI

سجّل الدخول إلى Turso من الطرفية:

turso auth login

سيفتح هذا المتصفح للمصادقة. بعد تسجيل الدخول، أنشئ قاعدة بياناتك:

turso db create bookmarks-db --group default

النسخ التلقائي: ينسخ Turso قاعدة بياناتك تلقائيًا عبر مناطق مجموعتك. تستخدم مجموعة default أقرب منطقة لك. أضف نسخًا إضافية بـ turso db replicate bookmarks-db [region].

الحصول على بيانات الاعتماد

احصل على رابط الاتصال والرمز المميز:

turso db show bookmarks-db --url
turso db tokens create bookmarks-db

أنشئ ملف .env.local في جذر مشروعك:

TURSO_DATABASE_URL=libsql://bookmarks-db-[your-org].turso.io
TURSO_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...

الأمان: لا تقم أبدًا بإضافة رموز Turso إلى Git. أضف .env.local إلى .gitignore (يفعل Next.js هذا افتراضيًا).

الخطوة 3: تثبيت التبعيات

ثبّت Drizzle ORM مع مشغل LibSQL:

npm install drizzle-orm @libsql/client
npm install -D drizzle-kit

إليك ما تفعله كل حزمة:

الحزمةالدور
drizzle-ormORM آمن النوع لـ TypeScript
@libsql/clientمشغل LibSQL لـ Turso
drizzle-kitCLI للترحيل والاستبطان

الخطوة 4: إعداد Drizzle

إعداد عميل قاعدة البيانات

أنشئ ملف الاتصال src/lib/db.ts:

import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";
 
const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});
 
export const db = drizzle(client, { schema });

إعداد Drizzle Kit

أنشئ drizzle.config.ts في جذر المشروع:

import { defineConfig } from "drizzle-kit";
 
export default defineConfig({
  schema: "./src/lib/schema.ts",
  out: "./drizzle",
  dialect: "turso",
  dbCredentials: {
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN,
  },
});

لهجة Turso: منذ الإصدار Drizzle Kit v0.22+، يتم دعم لهجة turso أصليًا. وهي تتعامل مع خصوصيات LibSQL مثل أنواع integer للقيم المنطقية و text للتواريخ.

الخطوة 5: تعريف المخطط

أنشئ src/lib/schema.ts مع الجداول لتطبيق الإشارات المرجعية:

import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { relations } from "drizzle-orm";
 
// جدول الإشارات المرجعية
export const bookmarks = sqliteTable("bookmarks", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  url: text("url").notNull(),
  description: text("description"),
  favicon: text("favicon"),
  createdAt: text("created_at")
    .notNull()
    .$defaultFn(() => new Date().toISOString()),
  updatedAt: text("updated_at")
    .notNull()
    .$defaultFn(() => new Date().toISOString()),
});
 
// جدول الوسوم
export const tags = sqliteTable("tags", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
  color: text("color").notNull().default("#6366f1"),
});
 
// جدول الربط بين الإشارات والوسوم
export const bookmarkTags = sqliteTable("bookmark_tags", {
  bookmarkId: integer("bookmark_id")
    .notNull()
    .references(() => bookmarks.id, { onDelete: "cascade" }),
  tagId: integer("tag_id")
    .notNull()
    .references(() => tags.id, { onDelete: "cascade" }),
});
 
// العلاقات
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
  bookmarkTags: many(bookmarkTags),
}));
 
export const tagsRelations = relations(tags, ({ many }) => ({
  bookmarkTags: many(bookmarkTags),
}));
 
export const bookmarkTagsRelations = relations(bookmarkTags, ({ one }) => ({
  bookmark: one(bookmarks, {
    fields: [bookmarkTags.bookmarkId],
    references: [bookmarks.id],
  }),
  tag: one(tags, {
    fields: [bookmarkTags.tagId],
    references: [tags.id],
  }),
}));

النقاط الرئيسية في المخطط

  • أنواع SQLite: يستخدم LibSQL النوعين text و integer كأنواع أساسية. تُخزَّن التواريخ كـ text بصيغة ISO 8601.
  • علاقات متعدد-بمتعدد: جدول bookmark_tags يربط الإشارات المرجعية والوسوم عبر جدول وسيط.
  • الحذف المتتالي: حذف إشارة مرجعية يحذف تلقائيًا ارتباطات الوسوم الخاصة بها.
  • القيم الافتراضية: تُولِّد $defaultFn الطوابع الزمنية على جانب التطبيق، وليس جانب قاعدة البيانات.

الخطوة 6: تنفيذ عمليات الترحيل

أنشئ ملفات ترحيل SQL من مخططك:

npx drizzle-kit generate

يُنشئ هذا مجلد drizzle/ بملفات SQL مرقمة. طبّقها على قاعدة بيانات Turso:

npx drizzle-kit migrate

تحقق من إنشاء الجداول:

turso db shell bookmarks-db ".tables"

يجب أن ترى: bookmarks، tags، bookmark_tags وجداول ترحيل Drizzle الداخلية.

عمليات الترحيل في الإنتاج: نفّذ دائمًا drizzle-kit generate محليًا وأضف ملفات SQL إلى Git. لا تنفذ أبدًا drizzle-kit push مباشرة في الإنتاج — استخدم drizzle-kit migrate الذي يطبق الملفات المُولَّدة بشكل حتمي.

الخطوة 7: إنشاء مسارات API للحوسبة الطرفية

الميزة الحقيقية لـ Turso تظهر مع Edge Functions. لننشئ مسارات API تعمل على edge runtime.

مسار GET/POST للإشارات المرجعية

أنشئ src/app/api/bookmarks/route.ts:

import { db } from "@/lib/db";
import { bookmarks, bookmarkTags, tags } from "@/lib/schema";
import { eq, like, desc } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
 
export const runtime = "edge";
 
// GET /api/bookmarks?search=next&tag=dev
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const search = searchParams.get("search");
  const tag = searchParams.get("tag");
 
  let query = db.query.bookmarks.findMany({
    with: {
      bookmarkTags: {
        with: {
          tag: true,
        },
      },
    },
    orderBy: [desc(bookmarks.createdAt)],
  });
 
  const results = await query;
 
  // تصفية في الذاكرة للبحث
  let filtered = results;
 
  if (search) {
    const term = search.toLowerCase();
    filtered = filtered.filter(
      (b) =>
        b.title.toLowerCase().includes(term) ||
        b.description?.toLowerCase().includes(term)
    );
  }
 
  if (tag) {
    filtered = filtered.filter((b) =>
      b.bookmarkTags.some((bt) => bt.tag.name === tag)
    );
  }
 
  return NextResponse.json(filtered);
}
 
// POST /api/bookmarks
export async function POST(request: NextRequest) {
  const body = await request.json();
  const { title, url, description, tagIds } = body;
 
  if (!title || !url) {
    return NextResponse.json(
      { error: "Title and URL are required" },
      { status: 400 }
    );
  }
 
  const [bookmark] = await db
    .insert(bookmarks)
    .values({ title, url, description })
    .returning();
 
  // ربط الوسوم
  if (tagIds && tagIds.length > 0) {
    await db.insert(bookmarkTags).values(
      tagIds.map((tagId: number) => ({
        bookmarkId: bookmark.id,
        tagId,
      }))
    );
  }
 
  return NextResponse.json(bookmark, { status: 201 });
}

مسار PUT/DELETE لإشارة مرجعية محددة

أنشئ src/app/api/bookmarks/[id]/route.ts:

import { db } from "@/lib/db";
import { bookmarks, bookmarkTags } from "@/lib/schema";
import { eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
 
export const runtime = "edge";
 
// PUT /api/bookmarks/:id
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const body = await request.json();
  const { title, url, description, tagIds } = body;
 
  const [updated] = await db
    .update(bookmarks)
    .set({
      title,
      url,
      description,
      updatedAt: new Date().toISOString(),
    })
    .where(eq(bookmarks.id, parseInt(id)))
    .returning();
 
  if (!updated) {
    return NextResponse.json(
      { error: "Bookmark not found" },
      { status: 404 }
    );
  }
 
  // تحديث الوسوم
  if (tagIds) {
    await db
      .delete(bookmarkTags)
      .where(eq(bookmarkTags.bookmarkId, updated.id));
 
    if (tagIds.length > 0) {
      await db.insert(bookmarkTags).values(
        tagIds.map((tagId: number) => ({
          bookmarkId: updated.id,
          tagId,
        }))
      );
    }
  }
 
  return NextResponse.json(updated);
}
 
// DELETE /api/bookmarks/:id
export async function DELETE(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
 
  const [deleted] = await db
    .delete(bookmarks)
    .where(eq(bookmarks.id, parseInt(id)))
    .returning();
 
  if (!deleted) {
    return NextResponse.json(
      { error: "Bookmark not found" },
      { status: 404 }
    );
  }
 
  return NextResponse.json({ success: true });
}

مسار الوسوم

أنشئ src/app/api/tags/route.ts:

import { db } from "@/lib/db";
import { tags } from "@/lib/schema";
import { NextRequest, NextResponse } from "next/server";
 
export const runtime = "edge";
 
export async function GET() {
  const allTags = await db.select().from(tags);
  return NextResponse.json(allTags);
}
 
export async function POST(request: NextRequest) {
  const { name, color } = await request.json();
 
  if (!name) {
    return NextResponse.json(
      { error: "Tag name is required" },
      { status: 400 }
    );
  }
 
  const [tag] = await db
    .insert(tags)
    .values({ name, color: color || "#6366f1" })
    .returning();
 
  return NextResponse.json(tag, { status: 201 });
}

الخطوة 8: إنشاء Server Component الرئيسي

أنشئ الصفحة الرئيسية في src/app/page.tsx التي تستخدم Server Components للعرض الأولي:

import { db } from "@/lib/db";
import { bookmarks, tags } from "@/lib/schema";
import { desc } from "drizzle-orm";
import { BookmarkList } from "@/components/bookmark-list";
 
export const runtime = "edge";
 
export default async function HomePage() {
  const allBookmarks = await db.query.bookmarks.findMany({
    with: {
      bookmarkTags: {
        with: {
          tag: true,
        },
      },
    },
    orderBy: [desc(bookmarks.createdAt)],
  });
 
  const allTags = await db.select().from(tags);
 
  return (
    <main className="mx-auto max-w-4xl px-4 py-8">
      <h1 className="mb-8 text-3xl font-bold">إشاراتي المرجعية</h1>
      <BookmarkList
        initialBookmarks={allBookmarks}
        tags={allTags}
      />
    </main>
  );
}

Server Component + Edge: بإضافة export const runtime = "edge" إلى الصفحة، يُشغّل Next.js مكون الخادم في Edge Runtime. يُنفَّذ استعلام Drizzle إلى Turso من أقرب مركز بيانات طرفي — مما يؤدي إلى Time to First Byte فائق السرعة.

الخطوة 9: إنشاء Client Component

أنشئ src/components/bookmark-list.tsx للجزء التفاعلي:

"use client";
 
import { useState, useTransition } from "react";
 
type Tag = {
  id: number;
  name: string;
  color: string;
};
 
type BookmarkTag = {
  bookmarkId: number;
  tagId: number;
  tag: Tag;
};
 
type Bookmark = {
  id: number;
  title: string;
  url: string;
  description: string | null;
  favicon: string | null;
  createdAt: string;
  updatedAt: string;
  bookmarkTags: BookmarkTag[];
};
 
export function BookmarkList({
  initialBookmarks,
  tags,
}: {
  initialBookmarks: Bookmark[];
  tags: Tag[];
}) {
  const [bookmarksList, setBookmarks] = useState(initialBookmarks);
  const [search, setSearch] = useState("");
  const [selectedTag, setSelectedTag] = useState<string | null>(null);
  const [isPending, startTransition] = useTransition();
 
  const filteredBookmarks = bookmarksList.filter((b) => {
    const matchesSearch =
      !search ||
      b.title.toLowerCase().includes(search.toLowerCase()) ||
      b.description?.toLowerCase().includes(search.toLowerCase());
 
    const matchesTag =
      !selectedTag ||
      b.bookmarkTags.some((bt) => bt.tag.name === selectedTag);
 
    return matchesSearch && matchesTag;
  });
 
  async function handleDelete(id: number) {
    startTransition(async () => {
      const res = await fetch(`/api/bookmarks/${id}`, {
        method: "DELETE",
      });
      if (res.ok) {
        setBookmarks((prev) => prev.filter((b) => b.id !== id));
      }
    });
  }
 
  return (
    <div>
      {/* شريط البحث */}
      <div className="mb-6 flex gap-4">
        <input
          type="text"
          placeholder="بحث..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          className="flex-1 rounded-lg border px-4 py-2"
        />
        <select
          value={selectedTag || ""}
          onChange={(e) => setSelectedTag(e.target.value || null)}
          className="rounded-lg border px-4 py-2"
        >
          <option value="">جميع الوسوم</option>
          {tags.map((tag) => (
            <option key={tag.id} value={tag.name}>
              {tag.name}
            </option>
          ))}
        </select>
      </div>
 
      {/* قائمة الإشارات المرجعية */}
      <div className="space-y-4">
        {filteredBookmarks.map((bookmark) => (
          <div
            key={bookmark.id}
            className="rounded-lg border p-4 transition-shadow hover:shadow-md"
          >
            <div className="flex items-start justify-between">
              <div>
                <a
                  href={bookmark.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-lg font-semibold text-blue-600 hover:underline"
                >
                  {bookmark.title}
                </a>
                {bookmark.description && (
                  <p className="mt-1 text-gray-600">
                    {bookmark.description}
                  </p>
                )}
                <div className="mt-2 flex gap-2">
                  {bookmark.bookmarkTags.map((bt) => (
                    <span
                      key={bt.tagId}
                      className="rounded-full px-2 py-1 text-xs text-white"
                      style={{
                        backgroundColor: bt.tag.color,
                      }}
                    >
                      {bt.tag.name}
                    </span>
                  ))}
                </div>
              </div>
              <button
                onClick={() => handleDelete(bookmark.id)}
                disabled={isPending}
                className="text-red-500 hover:text-red-700"
              >
                حذف
              </button>
            </div>
          </div>
        ))}
      </div>
 
      {filteredBookmarks.length === 0 && (
        <p className="text-center text-gray-500">
          لم يتم العثور على إشارات مرجعية.
        </p>
      )}
    </div>
  );
}

الخطوة 10: إضافة سكريبت البذر

لاختبار تطبيقنا، أنشئ سكريبت بذر في src/lib/seed.ts:

import { db } from "./db";
import { bookmarks, tags, bookmarkTags } from "./schema";
 
async function seed() {
  console.log("Seeding database...");
 
  // إنشاء الوسوم
  const [devTag] = await db
    .insert(tags)
    .values({ name: "dev", color: "#6366f1" })
    .returning();
 
  const [designTag] = await db
    .insert(tags)
    .values({ name: "design", color: "#ec4899" })
    .returning();
 
  const [toolsTag] = await db
    .insert(tags)
    .values({ name: "tools", color: "#14b8a6" })
    .returning();
 
  // إنشاء الإشارات المرجعية
  const [bm1] = await db
    .insert(bookmarks)
    .values({
      title: "Next.js Documentation",
      url: "https://nextjs.org/docs",
      description: "التوثيق الرسمي لـ Next.js",
    })
    .returning();
 
  const [bm2] = await db
    .insert(bookmarks)
    .values({
      title: "Turso Documentation",
      url: "https://docs.turso.tech",
      description: "الدليل الشامل لـ Turso و LibSQL",
    })
    .returning();
 
  const [bm3] = await db
    .insert(bookmarks)
    .values({
      title: "Drizzle ORM",
      url: "https://orm.drizzle.team",
      description: "ORM بدون واجهة لـ TypeScript لـ SQL",
    })
    .returning();
 
  // ربط الوسوم بالإشارات المرجعية
  await db.insert(bookmarkTags).values([
    { bookmarkId: bm1.id, tagId: devTag.id },
    { bookmarkId: bm2.id, tagId: devTag.id },
    { bookmarkId: bm2.id, tagId: toolsTag.id },
    { bookmarkId: bm3.id, tagId: devTag.id },
    { bookmarkId: bm3.id, tagId: toolsTag.id },
  ]);
 
  console.log("Seed completed!");
}
 
seed().catch(console.error);

نفّذ سكريبت البذر:

npx tsx src/lib/seed.ts

الخطوة 11: تحسين أداء الحوسبة الطرفية

الاتصالات المستمرة

يدير عميل LibSQL تلقائيًا مجموعة اتصالات HTTP. بالنسبة لـ Edge Functions (التي هي عديمة الحالة)، ينشئ كل استدعاء اتصالًا جديدًا. يُحسّن Turso هذا باتصالات HTTP/2 المتعددة.

التخزين المؤقت مع Next.js

استخدم التخزين المؤقت لـ Next.js للبيانات التي نادرًا ما تتغير:

import { unstable_cache } from "next/cache";
 
export const getCachedTags = unstable_cache(
  async () => {
    return db.select().from(tags);
  },
  ["all-tags"],
  {
    revalidate: 3600, // ساعة واحدة
    tags: ["tags"],
  }
);

النسخ المدمجة (اختياري)

لزمن استجابة أقل في التطوير المحلي، يدعم Turso النسخ المدمجة — نسخة SQLite محلية تتزامن تلقائيًا:

import { createClient } from "@libsql/client";
 
const client = createClient({
  url: "file:local-replica.db",
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60, // مزامنة كل 60 ثانية
});

النسخ المدمجة و Edge: لا تعمل النسخ المدمجة في Edge Runtime (لا يوجد نظام ملفات). استخدمها فقط للتطوير المحلي أو خوادم Node.js التقليدية.

الخطوة 12: النشر على Vercel

إعداد متغيرات البيئة

في لوحة تحكم Vercel، أضف المتغيرات التالية:

  • TURSO_DATABASE_URL — رابط قاعدة بيانات Turso
  • TURSO_AUTH_TOKEN — الرمز المُولَّد بـ turso db tokens create

إعداد Edge Runtime

تأكد من أن مسارات API والصفحات تستخدم export const runtime = "edge". سينشر Vercel تلقائيًا هذه الوظائف على شبكته الطرفية العالمية.

النشر

npx vercel deploy --prod

أو اربط مستودع Git للنشر التلقائي مع كل push.

التحقق من الأداء

بعد النشر، اختبر زمن الاستجابة من مناطق مختلفة باستخدام curl:

curl -o /dev/null -s -w "Time: %{time_total}s\n" \
  https://your-app.vercel.app/api/bookmarks

يجب أن تلاحظ أوقات استجابة أقل من 50 مللي ثانية من معظم المناطق، حيث يكون معظمها بسبب مصافحة TLS. يستغرق استعلام Turso نفسه عادة بين 1 و 8 مللي ثانية.

الخطوة 13: إضافة البحث النصي الكامل

يدعم Turso البحث النصي الكامل عبر FTS5، وهو امتداد SQLite. لنضفه إلى تطبيقنا.

إنشاء جدول FTS

أنشئ ملف ترحيل يدوي drizzle/fts-setup.sql:

CREATE VIRTUAL TABLE IF NOT EXISTS bookmarks_fts USING fts5(
  title,
  description,
  content='bookmarks',
  content_rowid='id'
);
 
-- مشغلات لإبقاء FTS متزامنًا
CREATE TRIGGER IF NOT EXISTS bookmarks_ai AFTER INSERT ON bookmarks BEGIN
  INSERT INTO bookmarks_fts(rowid, title, description)
  VALUES (new.id, new.title, new.description);
END;
 
CREATE TRIGGER IF NOT EXISTS bookmarks_ad AFTER DELETE ON bookmarks BEGIN
  INSERT INTO bookmarks_fts(bookmarks_fts, rowid, title, description)
  VALUES('delete', old.id, old.title, old.description);
END;
 
CREATE TRIGGER IF NOT EXISTS bookmarks_au AFTER UPDATE ON bookmarks BEGIN
  INSERT INTO bookmarks_fts(bookmarks_fts, rowid, title, description)
  VALUES('delete', old.id, old.title, old.description);
  INSERT INTO bookmarks_fts(rowid, title, description)
  VALUES (new.id, new.title, new.description);
END;

طبّق هذا الترحيل يدويًا عبر shell الخاص بـ Turso:

turso db shell bookmarks-db < drizzle/fts-setup.sql

استعلام البحث

أضف مسار بحث في src/app/api/search/route.ts:

import { db } from "@/lib/db";
import { sql } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
 
export const runtime = "edge";
 
export async function GET(request: NextRequest) {
  const query = new URL(request.url).searchParams.get("q");
 
  if (!query) {
    return NextResponse.json([]);
  }
 
  const results = await db.all(
    sql`SELECT b.*, bm.rank
        FROM bookmarks_fts bm
        JOIN bookmarks b ON b.id = bm.rowid
        WHERE bookmarks_fts MATCH ${query}
        ORDER BY bm.rank
        LIMIT 20`
  );
 
  return NextResponse.json(results);
}

الاختبار والتحقق

للتحقق من أن كل شيء يعمل:

  1. شغّل خادم التطوير:

    npm run dev
  2. اختبر مسارات API:

    # عرض الإشارات المرجعية
    curl http://localhost:3000/api/bookmarks
     
    # إنشاء إشارة مرجعية
    curl -X POST http://localhost:3000/api/bookmarks \
      -H "Content-Type: application/json" \
      -d '{"title":"Test","url":"https://example.com"}'
     
    # بحث
    curl "http://localhost:3000/api/search?q=next"
  3. تحقق من البيانات في Turso:

    turso db shell bookmarks-db "SELECT * FROM bookmarks"

حل المشاكل الشائعة

خطأ "TURSO_DATABASE_URL is not defined"

تأكد من أن ملف .env.local موجود في جذر المشروع وأنك أعدت تشغيل خادم التطوير بعد إنشائه.

خطأ "Token expired"

تنتهي صلاحية رموز Turso. أعد إنشاء واحد بـ:

turso db tokens create bookmarks-db --expiration none

خطأ ترحيل "table already exists"

إذا نفذت عمليات الترحيل عدة مرات، احذف الجداول وأعد التنفيذ:

turso db shell bookmarks-db "DROP TABLE IF EXISTS bookmark_tags; DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS tags;"
npx drizzle-kit migrate

Edge Runtime "Module not found"

بعض حزم Node.js غير متوافقة مع Edge Runtime. تحقق من أنك تستخدم فقط @libsql/client (الذي يدعم HTTP) وليس better-sqlite3 أو أي مشغل أصلي آخر.

الخطوات التالية

لديك الآن تطبيق كامل مع قاعدة بيانات جاهزة للحوسبة الطرفية. إليك بعض الأفكار للمضي قدمًا:

  • إضافة المصادقة مع Better Auth أو Auth.js
  • تنفيذ نظام مفضلات مع تحديثات متفائلة
  • إضافة Server Actions للتعديلات من جانب الخادم بدون مسارات API
  • إعداد المراقبة مع OpenTelemetry
  • بناء إضافة متصفح تحفظ الإشارات المرجعية مباشرة

الخلاصة

في هذا الدليل، بنينا تطبيق إشارات مرجعية جاهز للحوسبة الطرفية من خلال الجمع بين ثلاث تقنيات قوية:

  • Turso (LibSQL) لقاعدة بيانات SQLite موزعة مع نسخ تلقائي
  • Drizzle ORM لاستعلامات TypeScript آمنة النوع وعمليات ترحيل تصريحية
  • Next.js 15 Edge Runtime لتنفيذ الكود في أقرب نقطة ممكنة من المستخدمين

هذه الحزمة مثالية للتطبيقات التي تتطلب أوقات استجابة فائقة السرعة حول العالم. قاعدة البيانات تسافر مع كودك — لم يعد عليك الاختيار بين الأداء والبساطة.

الكود الكامل لهذا الدليل يُعتبر نقطة انطلاق لمشاريعك الخاصة. يمثل مزيج Turso + Drizzle + Next.js Edge واحدة من أكثر المعماريات أداءً وراحة لتطوير الويب الحديث في 2026.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على دمج AI SDK لاستخدام الحاسوب.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة