Галоўная / Артыкулы / Спрытнае адрабатаванне кантэнту як коду: пайплайн для запуску дадзейнаў з Git у Postgres

Спрытнае адрабатаванне кантэнту як коду: пайплайн для запуску дадзейнаў з Git у Postgres

Паказвае, як заменіць CMS на JSON, яке караўнаецца за дапамою Git, верыфікацыю Zod і аператыў upsert у Prisma, каб безпечна запрацаваць структураваны контэнт у Postgres.

2258 слоў

Уявіце стварэнне прыемкі з пытаннямі з калькуляцый, фрагментамі коду, адказамі та рангамі сложнасці. Такія матэрыялы досыта часта змінююцца, але не так, каб было неабходна жывая рэдагування у некалькі часы.

Вочыважным першым выборам можа быць CMS на кшталт Sanity або Strapi. Але перш чым выбіраць якой-небудзь з іх, корыстна спачатку узгадаць фактычныя трэбаванні:

  • Полная історыя змян для кожной правкі матэрыялу
  • Магчымасць перагляду змян прытым, якія не падаюць у продакшн
  • Праверкі, якія зупіняюць процес стварэння, а не падаюць у продакшн
  • Няхайка додатковай інфраструктуры для MVP
  • Рабочы процес, який падобаецца таму, як вы вже дастаеце код

З урахоўваннем гэтых трэбаванняй, зберагчыць матэрыялы безпасцей у Git мае больш сенс, чым дадаваць CMS.

У гэтым падходзе для зберагчыка дадзеных у час роботы выкорыстоўваюцца файлы JSON, схемы Zod, скрыпт Prisma seed і база дадзеных PostgreSQL. Працэс выглядае так:

JSON → Zod → Seed (upsert) → Postgres → API

Гэты падход навмысна не ўражае. Нудны працэс — гэта той, якому можна даверыць.

Чаму не «проста выкорыстоўваць CMS»?

Платформы CMS практычна корыстныя, калі не-тэхнічныя адносібы ўпоркідаюць контент ўжо кожны дзень, калі патрэбны станы чернавікаў і ролі дазволаў, або калі формат дадзеных зміняецца неперакладальна.

Але для структураванага контэнта, які самі інжынеры ствараюць — банкі квызаў, пачатковыя даны, процесы адкалічэння, рангі цэнаў — введэнне CMS зазвычай прыносіць:

  • Ішчо адну службу, якую трэба хоставаць і захоўваць
  • Ішчо адну схему, якую трэба падтрымваць у супараднасці з вашым прыкладнам
  • Ішчо адну праслойку, чераз якую могуць праскочыць некоректныя даны
  • Ішо адзін перыяход з вашага рэдагара.
  • На самай працоўны час не была патрэбна платформа для публікацыі — была патрэбна лінія обработкі контента:

    автар → аправарэнне → перагляд → размешчэнне → запуск → аддача → выдача

    Git вядома адрабатвае першыя чатыры крокі. Едынай нехватнае часткай быў надзеяны спосаб прыімання контента ў базу дадзеных.

    Архітэктура

    content/
      questions/
        javascript/
          easy.json
          medium.json
          hard.json
        html/
          easy.json
    packages/db/
      prisma/schema.prisma
      src/seed.ts              ← read, validate, upsert
    packages/shared/
      schemas/question.ts      ← Zod contract
    scripts/
      validate-content.ts      ← CI, no DB required
    
    | Layer        | Responsibility                      |
    |--------------|-------------------------------------|
    | JSON         | Human-editable source of truth      |
    | Zod          | Runtime validation + inferred types |
    | Prisma seed  | Idempotent import into the database |
    

    Непрыемлівыя правіла: ваша прыкладна програма ніколі не чытае файлы JSON пад час роботы ў працоўнай сітцы. JSON існуе толькі як вхідны даны пад час размешчэння. Postgres застаецца слоем, які фактычна выдае запыты.

    Это дае вам перавагі рабочага процесу Git без таго, каб ваша база дадзеных стала проста проксі для файлоў.

    Крок 1: Пачніце з Zod, а не з JSON

    Перш чым пісаць будзь-які контэнт, з’явіце кантракт, які ён павінен выпалняць.

    import { z } from 'zod';
    
    export enum Topic {
      JavaScript = 'JAVASCRIPT',
      HTML = 'HTML',
      TypeScript = 'TYPESCRIPT',
    }
    
    export enum Difficulty {
      Easy = 'EASY',
      Medium = 'MEDIUM',
      Hard = 'HARD',
    }
    
    export const questionSchema = z
      .object({
        id: z.string().min(1), // stable slug: js-closures-loop-001
        topic: z.nativeEnum(Topic),
        subtopic: z.string().min(1),
        difficulty: z.nativeEnum(Difficulty),
        text: z.string().min(1),
        codeSnippet: z.string().nullable().optional(),
        options: z.array(z.string().min(1)).min(2),
        correctOptionIndex: z.number().int().min(0),
        explanation: z.string().min(1),
      })
      .refine((q) => q.correctOptionIndex < q.options.length, {
        message: 'correctOptionIndex must point to a valid option',
      });
    
    export const questionsFileSchema = z.array(questionSchema);
    export type QuestionContent = z.infer<typeof questionSchema>;
    

    Выдзеляеця калькольныя выборы пад час проектавання:

    • Поле id знаходзіцца ў самым файле з контэнтам — гэта і робіць перыявленне безпечным. Першыя клучы, створаныя базай дадзеных, ўсьмо толькі деталі рэалізацыі; стабільны ідентыфікатор на кшталт js-closures-loop-001 — гэта тое, ад чаго на самай справе залежыць захаваны працэз развіяння пользователя.
    • Уместо сырых стрэлок выкарыстоўваюцься enum-ы, што не дазволяе некансистэнтным форматам, такім як js, JS чы javascript, прасачвацца ў разныя файлы.
    • Функцыя .refine() керуе правіламі верыфікацыі, якія стосуюцца калькоў полей — гэта тое, чаго не можа выражыць простая абмежэння min(), напрыклад, падтрымка індэкса адпаведзі на пярэдзеўаныя значэння.
    • Кожны шэма верыфікуе цэлы файл JSON як адна масава, а не кожную запіс окрема.

    Рэзультатам яе, што вашы контэнт мае ўзгодны з законам дакумент, а не проста якісь угодзіце, задокументаваныя гдзей-небудзь, якія ніхто не чытае.

    Шаг 2: Стварэнне JSON

    [
      {
        "id": "js-closures-loop-001",
        "topic": "JAVASCRIPT",
        "subtopic": "closures",
        "difficulty": "MEDIUM",
        "text": "What will this code log?",
        "codeSnippet": "for (var i = 0; i < 3; i++) {\n  setTimeout(() => console.log(i), 0);\n}",
        "options": ["0 1 2", "3 3 3", "undefined undefined undefined", "0 0 0"],
        "correctOptionIndex": 1,
        "explanation": "`var` is function-scoped, so by the time the timeouts run, `i` is 3."
      }
    ]
    

    Формат спецыяльна просты: одназначныя типы, чыстыя разлікі, і жадных суперашчотаў па аспектам парсінгу. Якщо автары контэнту згодам захочуць пісаць у форматах Markdown або YAML, вы можете стварыць JSON з гэтых форматаў на этапе прадзействавання — сам скрыпт-основа должен заставацца простым і прыведамым.

    Для тэксту з рознымі форматамі неабходна хаваць сырой строковы ісхіднік у базе дадзеных — у формате Markdown, простага тэксту або яком-небудзь іншым, з якім комфортна працаваюць автары — і адрасаваць яго там, дзе програма яго паказвае. Адрасаванне ў HTML на этапе стварэння прыкрепляе вас да адной конкретнай бібліятэкі для адрасавання і створвае проблемы пад час міграцыі, якщо вы калі-небудзь змяніце ёю. Хавайце ісхіднык такім, які ён є, і адрасавайце яго толькі там, дзе ён практычна неабходны.

    Шаг 3: Заполненне дадзенымі за допамогай upsert-аў, а не чысткі

    Калі у вас яшчэ няма рэальных выкарыстоўначаў, дазволена чыстка табелі за допамогай deleteMany і ўтварэнне яе занова за допамогай createMany. Калі записы выкарыстоўначаў пачнуць абсылавацца да рэкордаў кантэнту, перайдзіце на upsert-ы, базаваныя на стабільным ідэнтыфікаторы.

    Спрасцаваны модэль Prisma:

    model Question {
      id                 String   @id @default(cuid())
      externalId         String   @unique
      topic              Topic
      subtopic           String
      difficulty         Difficulty
      text               String
      codeSnippet        String?
      options            String[]
      correctOptionIndex Int
      explanation        String
    }
    

    Скрыпт заполнення дадзенымі:

    import fs from 'node:fs/promises';
    import path from 'node:path';
    import { PrismaClient } from '@prisma/client';
    import { questionsFileSchema, type QuestionContent } from '@myapp/shared';
    
    const prisma = new PrismaClient();
    const CONTENT_DIR = path.resolve(__dirname, '../../../content/questions');
    
    async function loadQuestionsFromDisk(): Promise<QuestionContent[]> {
      const rows: QuestionContent[] = [];
      const topicDirs = await fs.readdir(CONTENT_DIR, { withFileTypes: true });
    
      for (const topicDir of topicDirs) {
        if (!topicDir.isDirectory()) continue;
    
        const dirPath = path.join(CONTENT_DIR, topicDir.name);
        const files = (await fs.readdir(dirPath)).filter((f) => f.endsWith('.json'));
    
        for (const file of files) {
          const raw = await fs.readFile(path.join(dirPath, file), 'utf8');
          const questions = questionsFileSchema.parse(JSON.parse(raw));
          rows.push(...questions);
        }
      }
    
      return rows;
    }
    
    async function main() {
      const questions = await loadQuestionsFromDisk();
      let created = 0;
      let updated = 0;
    
      for (const q of questions) {
        const data = {
          topic: q.topic,
          subtopic: q.subtopic,
          difficulty: q.difficulty,
          text: q.text,
          codeSnippet: q.codeSnippet ?? null,
          options: q.options,
          correctOptionIndex: q.correctOptionIndex,
          explanation: q.explanation,
        };
    
        const existing = await prisma.question.findUnique({
          where: { externalId: q.id },
        });
    
        if (existing) {
          await prisma.question.update({ where: { externalId: q.id }, data });
          updated++;
        } else {
          await prisma.question.create({ data: { externalId: q.id, ...data } });
          created++;
        }
      }
    
      console.log(`Seeded: ${created} created, ${updated} updated`);
    }
    
    main()
      .catch((err) => {
        console.error(err);
        process.exit(1);
      })
      .finally(() => prisma.$disconnect());
    

    Тут важныя тры деталі:

    1. questionsFileSchema.parse(...) абэрнюе некоректны JSON ад падачы ў базу дадзеных.
    2. findUnique({ where: { externalId: q.id } }) адбывае падбор па сабэй ID кантэнту, а не па внутранім первычным ключы базы дадзеных.
    3. Адкалі вы ведаеце змены, а не чысткаце, вы захоўвайце всі адпаведзі чы ўжо здабыты прагрэс выкарыстоўнача, якія ёсць прыязваныя да таго рэкорда.

    Можаце запытацца, чаму проста не вызваць createMany({ skipDuplicates: true }). Гэты варыянт запобегае дублікацыі рэядкоў, але не прабывае змяніць застарэлы тэкст, калі змieniaецца ўжо існуючы контэнт. Для сінхронізаціі контэнту пад час развяртання болей прозрачным є спосаб з выразнымі операцыямі upsert, які працуе з усіма дварчымі механізмамі. Актуальны варыянт INSERT ... ON CONFLICT DO UPDATE слід выкарыстоўваць толькі пасля таго, як аналіз пакажае, што вам справды трэба такая скорасць.

    Крок 4: Швайная адмова ў CI

    Для пераканання ў правільнасці контэнту не трэба двару. Выконайце гэта для кожнага pull request, який зміняе папку content/:

    // scripts/validate-content.ts
    import fs from 'node:fs/promises';
    import { glob } from 'glob';
    import { questionsFileSchema } from '@myapp/shared';
    
    const files = await glob('content/**/*.json');
    let failed = 0;
    
    for (const file of files) {
      try {
        const raw = await fs.readFile(file, 'utf8');
        questionsFileSchema.parse(JSON.parse(raw));
        console.log(`✓ ${file}`);
      } catch (err) {
        console.error(`✗ ${file}`, err);
        failed++;
      }
    }
    
    process.exit(failed > 0 ? 1 : 0);
    
    # .github/workflows/validate-content.yml
    name: Validate content
    on:
      pull_request:
        paths: ['content/**']
    
    jobs:
      validate:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: pnpm/action-setup@v2
          - run: pnpm install
          - run: pnpm tsx scripts/validate-content.ts
    

    Якщо контэнт несправны, pull request адмовляецца — ён ніколі не выдаўаецца без прызначэння.

    Процес развяртання выглядае так:

    npx prisma migrate deploy
    npx prisma db seed
    

    Апдэйты контэнту стаюць звычным выпускам: з’еднанне, міграцыя, заполнення.

    Крок 5: Калі змieniaецца ваша схема

    Раней чым пазней ваша схема Zod будзе развівацца. Можа, вы дадзеце поле hint. Можа, перайменаваеце codeSnippet на code. Можа, options зменіцца з масі страк на масу об’ектаў.

    Якім бы не было змена, вашы існуючыя файлы JSON больш не будуць падходзіць да схемы. Скрыпт seed выдае памылку. Не панікуйце — ставіцеся да змян схемы для кантэнту так сама, як і да міграцыйных скрыптав для базы дадзеных.

    Пачніце з версіявання самай схемы:

    // schemas/question.v1.ts — old shape
    // schemas/question.v2.ts — new shape
    // schemas/question.ts   — export latest as `questionSchema`
    

    Потым запрацаваце кодамод над файламі на дыску адной раз:

    // scripts/codemod-questions-v2.ts
    import fs from 'node:fs/promises';
    import { glob } from 'glob';
    import { z } from 'zod';
    import { questionSchemaV1 } from '@myapp/shared/schemas/question.v1';
    
    const v1File = z.array(questionSchemaV1);
    
    for (const file of await glob('content/**/*.json')) {
      const old = v1File.parse(JSON.parse(await fs.readFile(file, 'utf8')));
    
      const next = old.map((q) => ({
        ...q,
        hint: null,
        code: q.codeSnippet,
        codeSnippet: undefined,
      }));
    
      await fs.writeFile(file, JSON.stringify(next, null, 2));
    }
    

    Запрацаваце кодамод, зафіксуйце апдэйтаваны JSON, наставьце скрыпт seed на схему v2 і разверніце. Гэта весь процес: кодамод, зафіксаванне, апдэйт, розповсюджэнне.

    Это тая ж сама дысціпліна, якую вы вжо прыменяеце з prisma migrate, і яна дае тыя ж рэзультаты.

    Што вы отрымаеце

    Адзінаковая перагляд зместу стаецца адзінаковай перагляд коду. Запытанне «Чы гэтаяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяя

    Масавыя змены выконваюцца за дапамою скрыптав, а не праз ручныя клікі. Переканалаванне чатырохдзесят пытанняў з MEDIUM на HARD адбываецца за дапамою адной лініі каманды sed чыра скрыпта, а не праз чатырохдзесят окалічных правак у адмінистрацыйным інтерфейсе.

    Прагрэс ужоў застаёцца пасля запуску. Будучы выкарыстанымі стабільныя ID у поўнанні з функцыяй upsert, можна выправіць памылку ў пытанні без перарыву звязку з адказамі, якія ужо падалі ужоў.

    Што вы адмовляецеся

    Варта чытрачна паведаміць пра абмянкі:

    • Людзі, якія не ўжоў-інжынеры, зазвычай не будуць задоволеныя работай у Git. Якщо не-тэхнічныя рэдагувальнікі павінны дапамагаць, вам знадобыцца шлях для імпорту CSV, внутрашняе адзінство чыра CMS без галавы, які экспортуе ў JSON.
    • Няма вбудованага процесу работы з чернавікамі і публікуемымі версіямі. Тое, што знаходзится на main, і ўтварае базу дадзеных. Якщо вам патрэбны чернавікі, даведзіце ўвагу на викорыстанне галоўкі.
  • Медыя-рэсурсы не павінны знаходзіцца ў файлах JSON. Зображэння і відэа трэба хаваць у сховішчы об’ектаў, да якога ступаюцца праз URL.
  • Конфлікты падчас з’еднання є рэальным рызыкам, калі колькасць людзей правяць контэнт. Ёго можна зменшыць, раздзеліўшы контэнт на маленькія файлы — арганізаваныя па тэме чы сложнасці — замест аднаго вялікага questions.json.
  • Будь-яя змена контэнту ў працоўным режыме выклікае неабходнасць аплыву. Якщо ваша ситуацыя выкалікае патрэбу ў апдэйтах контэнту без перапрыяву, гэты падход не падходзіць.
  • Калі яго вжываць

    Гэты патэрн добра падходзіць, калі ваш контэнт структураваны і павторны, ваша інжынерная каманда мала, вы знаходзіцеся ў прыбліжнасці межы MVP і ранньага працоўнага режыму, змены контэнту трэба можна бачыць, і вы вже вжываеце Prisma.

    Гэта методыка паслужае слаба, калі ўсёлкі дзень публікуюць матэрыялы неспецыялісты-редактары, існуюць складныя ланцюгі затверджэння, велика залежнасць ад медыя, або ёсць патрэба рэдагаваць жывы контент безпосередна пад час його стварэння.

    Вывад

    На самай працэ не была патрэбна CMS — трэбавалася такая структура контэнта, якая будзе адпаваць тым жа стандартам, чым код: з версіямі, пераканальванымі даннымі, працэю над іх пераглядам і развяртанням через вялікаверыдную інфраструктуру.

    JSON у Git ёсць аднаковым джэрелам правды. Zod выступае як контролер. Процэс seed у Prisma служыць для завантажэння данных. Stable IDs абараняюць ціласць дадзейнаў корыстніка, калі контэнт пад імі зменяецца.

    Якщо ўсё еща няма корыстнікаў, пачніце проста з процэсу вычышэння і практычнага завантажэння данных. Перейдзіце на методы upsert, калі дадзеныя пра прагрэс стануць значнымі. Дадзіце пераканальванне данных у рамках CI, каб ніякія памылкі ў enum не дасяглі стадіі тэставання. Створыце версіі схемы контэнта ўжо да таго часу, калі не будзе другойя значныя змяна.

    Усе це спецыяльна не прывабліва – і самэўсёлкава гэта і є мета. Зберагце захопленне для самога продукту, а не для системы, якая адпаведна зберагае вашы запитанні.

    Спадневае чытанне

  • Замена any у TypeScript: Шас типавыя модэлі для частых ситуацый — Дзеўяць практычных, типавых альтэрнатыў any у TypeScript — укладаючы неканфірмаваныя дадзеныя, гэнерыкі, дискримінаваныя аюніі і выключныя пераконтроўкі — для обработкі неперапрацаваных дадзеных.
  • Міграцыя з Prisma на Drizzle: Апталогія за шас месцаў — Разработчык дзеліцься рэальнымі показнікамі і компромісамі, якія з’явіліся пад час пераходу стака PostgreSQL з TypeScript ад Prisma да Drizzle ORM.