Kysely: منشئ استعلامات SQL آمن الأنواع مع Next.js و PostgreSQL 2026

AI Bot
بواسطة AI Bot ·

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

Kysely هو منشئ استعلامات SQL مصمم خصيصًا لـ TypeScript، يمنحك أمان ORM مع شفافية SQL الخام. على عكس Prisma أو Drizzle، لا يخفي Kysely أبدًا ما يتم تنفيذه فعليًا — كل استعلام يُقرأ مثل SQL، وكل عمود مكتوب بأنواع كاملة. في هذا الدرس، ستبني تطبيق Next.js 15 جاهزًا للإنتاج باستخدام Kysely مع PostgreSQL، يشمل عمليات الهجرة والاستعلامات المتقدمة والتكامل مع Server Actions.

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

قبل البدء، تأكد من توفر:

  • Node.js الإصدار 20 أو أحدث
  • نسخة من PostgreSQL الإصدار 15 أو أحدث (محلية أو سحابية — Neon أو Supabase أو Railway تعمل جميعها)
  • معرفة أساسية بـ TypeScript و SQL
  • محرر أكواد (يُنصح بـ VS Code)
  • معرفة بأساسيات Next.js App Router

ما الذي ستبنيه

واجهة برمجة تطبيقات كاملة لإدارة المهام لفريق صغير، تشمل:

  • تعريفات مخطط آمنة الأنواع تُولَّد تلقائيًا من PostgreSQL
  • عمليات CRUD عبر Server Actions في Next.js 15
  • وصلات معقدة وتجميعات وعمليات معاملات
  • إدارة هجرات المخطط بواسطة migrator الخاص بـ Kysely
  • IntelliSense كامل لكل عمود، في كل استعلام

في النهاية، ستحصل على أساس قوي يتوسع من جدول واحد إلى عشرات الجداول، مع أمان أنواع شامل من البداية للنهاية.

الخطوة 1: إعداد المشروع

أنشئ مشروع Next.js 15 جديدًا مع TypeScript و Tailwind CSS مُهيَّأَين مسبقًا.

npx create-next-app@latest kysely-tasks --typescript --tailwind --app --src-dir --eslint
cd kysely-tasks

تحقق من أن المشروع يعمل بشكل سليم قبل المتابعة.

npm run dev

الخطوة 2: تثبيت Kysely وتبعيات PostgreSQL

ثبّت Kysely مع مشغّل PostgreSQL الرسمي ومُولِّد أكواد يفحص مخطط قاعدة بياناتك.

npm install kysely pg
npm install -D @types/pg kysely-codegen tsx

تقرأ حزمة kysely-codegen مخطط PostgreSQL الحي وتُصدر أنواع TypeScript تلقائيًا — هذا هو السحر الذي يدعم IntelliSense الكامل دون كتابة الأنواع يدويًا.

الخطوة 3: إعداد متغيرات البيئة

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

DATABASE_URL=postgres://user:password@localhost:5432/kysely_tasks

إذا لم تكن لديك نسخة PostgreSQL محلية، قم بتشغيل واحدة بسرعة باستخدام Docker.

docker run --name kysely-pg -e POSTGRES_PASSWORD=password -e POSTGRES_DB=kysely_tasks -p 5432:5432 -d postgres:16

الخطوة 4: إنشاء أول عملية هجرة

يأتي Kysely مع migrator مدمج. أنشئ مجلد migrations وسكريبت لتشغيل عمليات الهجرة المعلقة.

أنشئ الملف src/db/migrations/2026_01_01_create_tasks.ts.

import { Kysely, sql } from "kysely";
 
export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable("users")
    .addColumn("id", "uuid", (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn("email", "varchar(255)", (col) => col.notNull().unique())
    .addColumn("name", "varchar(255)", (col) => col.notNull())
    .addColumn("created_at", "timestamptz", (col) =>
      col.notNull().defaultTo(sql`now()`)
    )
    .execute();
 
  await db.schema
    .createTable("tasks")
    .addColumn("id", "uuid", (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn("title", "varchar(500)", (col) => col.notNull())
    .addColumn("description", "text")
    .addColumn("status", "varchar(20)", (col) =>
      col.notNull().defaultTo("todo")
    )
    .addColumn("assignee_id", "uuid", (col) =>
      col.references("users.id").onDelete("set null")
    )
    .addColumn("due_date", "date")
    .addColumn("created_at", "timestamptz", (col) =>
      col.notNull().defaultTo(sql`now()`)
    )
    .execute();
 
  await db.schema
    .createIndex("tasks_assignee_idx")
    .on("tasks")
    .column("assignee_id")
    .execute();
}
 
export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable("tasks").execute();
  await db.schema.dropTable("users").execute();
}

لاحظ كيف أن كل نوع عمود وكل قيمة افتراضية وكل قيد يتم التعبير عنه بـ TypeScript خالص مع إكمال تلقائي كامل.

الخطوة 5: ربط الـ Migrator

أنشئ src/db/migrate.ts — وهو السكريبت الذي تشغّله لتطبيق عمليات الهجرة.

import { promises as fs } from "node:fs";
import path from "node:path";
import { Kysely, Migrator, FileMigrationProvider, PostgresDialect } from "kysely";
import { Pool } from "pg";
 
async function migrate() {
  const db = new Kysely<any>({
    dialect: new PostgresDialect({
      pool: new Pool({ connectionString: process.env.DATABASE_URL }),
    }),
  });
 
  const migrator = new Migrator({
    db,
    provider: new FileMigrationProvider({
      fs,
      path,
      migrationFolder: path.join(process.cwd(), "src/db/migrations"),
    }),
  });
 
  const { error, results } = await migrator.migrateToLatest();
 
  results?.forEach((r) => {
    if (r.status === "Success") {
      console.log(`تم تطبيق الهجرة "${r.migrationName}"`);
    } else if (r.status === "Error") {
      console.error(`فشل تطبيق "${r.migrationName}"`);
    }
  });
 
  if (error) {
    console.error("فشلت عملية الهجرة:", error);
    process.exit(1);
  }
 
  await db.destroy();
}
 
migrate();

أضف سكريبت إلى package.json.

{
  "scripts": {
    "db:migrate": "tsx --env-file=.env.local src/db/migrate.ts",
    "db:codegen": "kysely-codegen --out-file src/db/types.ts --camel-case"
  }
}

طبّق عمليات الهجرة الخاصة بك.

npm run db:migrate

الخطوة 6: توليد تعريفات الأنواع

شغّل الآن مولّد الأكواد. سيفحص قاعدة بياناتك الحية ويُصدر ملف types.ts يمثّل كل جدول.

DATABASE_URL=$DATABASE_URL npm run db:codegen

افتح src/db/types.ts — سترى واجهة DB كاملة الأنواع يستخدمها Kysely لفرض الأمان على كل استعلام.

export interface DB {
  users: Users;
  tasks: Tasks;
}
 
export interface Users {
  id: Generated<string>;
  email: string;
  name: string;
  createdAt: Generated<Date>;
}
 
export interface Tasks {
  id: Generated<string>;
  title: string;
  description: string | null;
  status: Generated<string>;
  assigneeId: string | null;
  dueDate: Date | null;
  createdAt: Generated<Date>;
}

الخطوة 7: إنشاء عميل قاعدة البيانات

أنشئ src/db/client.ts لتجسيد نسخة Kysely واحدة للتطبيق.

import { Kysely, PostgresDialect, CamelCasePlugin } from "kysely";
import { Pool } from "pg";
import type { DB } from "./types";
 
const globalForDb = globalThis as unknown as {
  db: Kysely<DB> | undefined;
};
 
export const db =
  globalForDb.db ??
  new Kysely<DB>({
    dialect: new PostgresDialect({
      pool: new Pool({
        connectionString: process.env.DATABASE_URL,
        max: 10,
      }),
    }),
    plugins: [new CamelCasePlugin()],
  });
 
if (process.env.NODE_ENV !== "production") {
  globalForDb.db = db;
}

يحوّل CamelCasePlugin تلقائيًا الأعمدة من snake_case في PostgreSQL إلى camelCase في TypeScript. يمنع التخزين المؤقت العام إعادة تحميل Next.js الفوري من إنشاء حمامات اتصالات مكررة في وضع التطوير.

الخطوة 8: بناء Server Actions لـ CRUD

أنشئ src/app/actions/tasks.ts — وحدة Server Actions تعمل بقوة Kysely.

"use server";
 
import { revalidatePath } from "next/cache";
import { db } from "@/db/client";
 
export async function createTask(formData: FormData) {
  const title = formData.get("title") as string;
  const assigneeId = (formData.get("assigneeId") as string) || null;
 
  const task = await db
    .insertInto("tasks")
    .values({
      title,
      assigneeId,
    })
    .returningAll()
    .executeTakeFirstOrThrow();
 
  revalidatePath("/");
  return task;
}
 
export async function listTasks() {
  return db
    .selectFrom("tasks")
    .leftJoin("users", "users.id", "tasks.assigneeId")
    .select([
      "tasks.id",
      "tasks.title",
      "tasks.description",
      "tasks.status",
      "tasks.dueDate",
      "users.name as assigneeName",
    ])
    .orderBy("tasks.createdAt", "desc")
    .execute();
}
 
export async function updateTaskStatus(id: string, status: string) {
  await db
    .updateTable("tasks")
    .set({ status })
    .where("id", "=", id)
    .execute();
 
  revalidatePath("/");
}
 
export async function deleteTask(id: string) {
  await db.deleteFrom("tasks").where("id", "=", id).execute();
  revalidatePath("/");
}

لاحظ IntelliSense — جرّب إعادة تسمية tasks.title إلى tasks.titlex وراقب TypeScript يرفض البناء فورًا. لا يوجد عبء وقت تشغيل لأن كل شيء يُترجم إلى SQL خالص.

الخطوة 9: بناء واجهة المستخدم

أنشئ صفحة قائمة مهام بسيطة في src/app/page.tsx.

import { listTasks, createTask, updateTaskStatus } from "./actions/tasks";
 
export default async function HomePage() {
  const tasks = await listTasks();
 
  return (
    <main className="max-w-3xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">مهام الفريق</h1>
 
      <form action={createTask} className="flex gap-2 mb-8">
        <input
          type="text"
          name="title"
          placeholder="ما الذي يجب فعله؟"
          className="flex-1 border rounded px-3 py-2"
          required
        />
        <button type="submit" className="bg-black text-white px-4 py-2 rounded">
          إضافة
        </button>
      </form>
 
      <ul className="space-y-2">
        {tasks.map((task) => (
          <li
            key={task.id}
            className="border rounded p-4 flex items-center justify-between"
          >
            <div>
              <div className="font-medium">{task.title}</div>
              {task.assigneeName && (
                <div className="text-sm text-gray-600">
                  مُسنَدة إلى {task.assigneeName}
                </div>
              )}
            </div>
            <form
              action={async () => {
                "use server";
                await updateTaskStatus(
                  task.id,
                  task.status === "done" ? "todo" : "done"
                );
              }}
            >
              <button className="text-sm text-blue-600">
                {task.status === "done" ? "إعادة فتح" : "إنجاز"}
              </button>
            </form>
          </li>
        ))}
      </ul>
    </main>
  );
}

الخطوة 10: إتقان الاستعلامات المتقدمة

يتألق Kysely حقًا في استعلامات SQL المتقدمة. إليك أنماطًا ستستخدمها باستمرار.

التجميعات والتجميع حسب المجموعة

import { sql } from "kysely";
 
const stats = await db
  .selectFrom("tasks")
  .select([
    "status",
    db.fn.count<number>("id").as("total"),
    sql<number>`COUNT(*) FILTER (WHERE due_date < now())`.as("overdue"),
  ])
  .groupBy("status")
  .execute();

Common Table Expressions

const result = await db
  .with("recent_tasks", (cte) =>
    cte
      .selectFrom("tasks")
      .selectAll()
      .where("createdAt", ">", new Date(Date.now() - 7 * 86400000))
  )
  .selectFrom("recent_tasks")
  .selectAll()
  .where("status", "=", "todo")
  .execute();

المعاملات (Transactions)

await db.transaction().execute(async (trx) => {
  const user = await trx
    .insertInto("users")
    .values({ email: "alice@example.com", name: "Alice" })
    .returning("id")
    .executeTakeFirstOrThrow();
 
  await trx
    .insertInto("tasks")
    .values({
      title: "أهلًا بك معنا",
      assigneeId: user.id,
    })
    .execute();
});

إذا رمى أي شيء داخل دالة الاستدعاء استثناءً، يقوم Kysely تلقائيًا بإلغاء المعاملة بالكامل.

الخطوة 11: مخارج SQL الخام الآمنة الأنواع

عندما تحتاج إلى ميزات PostgreSQL لم يتمكن Kysely من تجريدها بعد، عُد إلى SQL الخام المكتوب الأنواع.

import { sql } from "kysely";
 
const fuzzyMatches = await db
  .selectFrom("tasks")
  .select(["id", "title"])
  .where(sql<boolean>`title % ${"deploy"}`)
  .orderBy(sql`similarity(title, ${"deploy"})`, "desc")
  .limit(10)
  .execute();

يحافظ القالب sql على استدلال الأنواع من البداية للنهاية مع السماح لك باستخدام أي امتداد من امتدادات PostgreSQL تحتاجه.

الخطوة 12: اختبار التنفيذ الخاص بك

شغّل قاعدة بيانات مؤقتة للاختبارات باستخدام pg-mem أو حاوية Docker، ثم نفّذ اختبارات التكامل باستخدام Vitest.

import { describe, it, expect, beforeEach } from "vitest";
import { db } from "@/db/client";
 
describe("tasks", () => {
  beforeEach(async () => {
    await db.deleteFrom("tasks").execute();
  });
 
  it("ينشئ ويقرأ مهمة", async () => {
    await db
      .insertInto("tasks")
      .values({ title: "إطلاق" })
      .execute();
 
    const found = await db
      .selectFrom("tasks")
      .selectAll()
      .where("title", "=", "إطلاق")
      .executeTakeFirst();
 
    expect(found?.title).toBe("إطلاق");
  });
});

استكشاف الأخطاء وإصلاحها

المشكلات الشائعة وكيفية حلها.

  • استنفاد بحيرة الاتصال: قلّل من قيمة max أو أغلق الوصول إلى قاعدة البيانات في استدعاءات serverless طويلة الأمد عبر db.destroy().
  • مخرجات codegen فارغة: تأكّد من إمكانية الوصول إلى DATABASE_URL من الصدفة الخاصة بك ومن وجود جدول واحد على الأقل معرَّف من المستخدم في قاعدة البيانات.
  • عدم تطابق CamelCase: إذا نسيت CamelCasePlugin، تُعيد الاستعلامات مفاتيح snake_case بينما تتوقع الأنواع المُولَّدة camelCase. اقرن دائمًا الإضافة مع الراية --camel-case في codegen.
  • عدم اكتشاف الهجرات: يجب أن تتبع أسماء الملفات الترتيب المعجمي. استخدم بادئة تاريخ مثل 2026_01_01_ ليتم فرزها بشكل صحيح.

نصائح الأداء

  • أضف دائمًا استدعاءات select() صريحة بدلاً من selectAll() في كود الإنتاج لتجنّب جلب أعمدة غير ضرورية.
  • استخدم executeTakeFirst() بدلاً من execute() عند الحاجة إلى صف واحد — هذا يتجنب إنشاء مصفوفة.
  • غلّف الكتابات متعددة الخطوات داخل معاملات لمنع الفشل الجزئي.
  • ادمج Kysely مع pgbouncer أو تجميع اتصالات Neon لعمليات النشر serverless.

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

الآن وقد أصبحت لديك طبقة قاعدة بيانات آمنة الأنواع، فكّر في توسيعها بـ:

  • نسخ متماثلة للقراءة باستخدام عدة نسخ من Kysely
  • سياسات أمان على مستوى الصف في PostgreSQL مع withSchema() الخاصة بـ Kysely
  • مهام في الخلفية تستدعي طبقة الاستعلامات نفسها من العامل
  • إقران Kysely مع درس Drizzle ORM لمقارنة الأساليب
  • النشر إلى الإنتاج عبر دليل Coolify

الخاتمة

يحقق Kysely توازنًا فريدًا — يمنحك القوة الكاملة وشفافية SQL دون التنازل أبدًا عن أمان TypeScript. لقد كتبت استعلامات حقيقية، واستخدمت وصلات حقيقية، وأطلقت عمليات هجرة حقيقية، كل ذلك دون أن يخفي ORM السلوك خلف طبقة من السحر. بالنسبة للفرق التي تفكّر بالفعل بلغة SQL، يُعدّ Kysely في الغالب أنظف طريق نحو تطبيق Next.js قابل للصيانة وآمن الأنواع.

كلما وجدت نفسك تكافح مع تجريدات ORM أو تتساءل عن الاستعلام الذي تم تنفيذه فعليًا، تذكّر أن Kysely يتيح لك قراءة وكتابة SQL بالضبط كما قصدت — مع المُترجم يحرس ظهرك.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على أنشئ تأثيرات نصية مذهلة مع Pretext و Next.js — من تخطيطات المجلات إلى الفن التفاعلي.

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

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

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

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