أريد الحصول على أداة ترحيل قاعدة البيانات، التي تحتوي على الخصائص التالية:
نشأت الكثير من هذه النقاط من تجربتي مع هذه الأداة الرائعة التي تسمى tern. كنت حزينًا لأن جافا سكريبت ليس لديه نفس الشيء! (أو ربما أسيء استخدام Google...). لذلك قررت أن هذا يمكن أن يكون تدريبًا جيدًا على البرمجة لنفسي وقصة قد تكون مثيرة للاهتمام لشخص آخر :)
دعونا نسرق تصميم أداة CLI!
لذلك سيكون بناء جملة الأداة كما يلي: martlet up --database-url
حيث يجب أن يطبق "up" جميع عمليات الترحيل التي لم يتم تطبيقها بعد، ويجب أن يتراجع "down" إلى الإصدار المحدد.
الخيارات لها المعنى والافتراضيات التالية:
كما ترون، لقد بدأت بمعرفة كيفية استدعاء الأداة قبل كتابة أي كود فعلي. هذه ممارسة جيدة، فهي تساعد على تحقيق المتطلبات وتقليل دورات التطوير.
حسنًا، أول الأشياء أولاً! لنقم بإنشاء ملف Index.js وإخراج رسالة المساعدة. سيبدو الأمر كالتالي:
function printHelp() { console.log( "Usage: martlet up --driver--dir --database-url ", ); console.log( " martlet down --driver --dir --database-url ", ); console.log( " is a number that specifies the version to migrate down to", ); console.log("Options:"); console.log(' --driver Driver to use, default is "pg"'); console.log(' --dir Directory to use, default is "migrations"'); console.log( " --database-url Database URL to use, default is DATABASE_URL environment variable", ); } printHelp();
الآن سنقوم بتحليل الخيارات:
export function parseOptions(args) { const options = { dir: "migrations", driver: "pg", databaseUrl: process.env.DATABASE_URL, }; for (let idx = 0; idxكما ترى، لا أستخدم أي مكتبة للتحليل؛ أنا فقط أكرر قائمة الوسائط وأعالج كل خيار. لذا، إذا كان لدي خيار منطقي، فسوف أقوم بإزاحة مؤشر التكرار بمقدار 1، وإذا كان لدي خيار ذو قيمة، فسوف أقوم بإزاحته بمقدار 2.
2.2 تنفيذ محول برنامج التشغيل
لدعم برامج تشغيل متعددة، نحتاج إلى واجهة عالمية للوصول إلى قاعدة البيانات؛ هنا كيف قد يبدو:
interface Adapter { connect(url: string): Promise; transact(query: (fn: (text) => Promise )): Promise ; close(): Promise ; } أعتقد أن الاتصال والإغلاق وظيفتان واضحتان جدًا، واسمحوا لي أن أشرح طريقة التعامل. يجب أن تقبل دالة سيتم استدعاؤها بوظيفة تقبل نص استعلام وترجع وعدًا بنتيجة متوسطة. يتطلب هذا التعقيد وجود واجهة عامة توفر القدرة على تشغيل استعلامات متعددة داخل المعاملة. من الأسهل فهمه من خلال النظر إلى مثال الاستخدام.
إذًا هذه هي الطريقة التي يبحث بها المحول عن برنامج تشغيل postgres:
class PGAdapter { constructor(driver) { this.driver = driver; } async connect(url) { this.sql = this.driver(url); } async transact(query) { return this.sql.begin((sql) => ( query((text) => sql.unsafe(text)) )); } async close() { await this.sql.end(); } }ومثال الاستخدام يمكن أن يكون:
import postgres from "postgres"; const adapter = new PGAdapter(postgres); await adapter.connect(url); await adapter.transact(async (sql) => { const rows = await sql("SELECT * FROM table1"); await sql(`INSERT INTO table2 (id) VALUES (${rows[0].id})`); });2.3 تثبيت برنامج التشغيل عند الطلب
const PACKAGES = { pg: "[email protected]", }; const downloadDriver = async (driver) => { const pkg = PACKAGES[driver]; if (!pkg) { throw new Error(`Unknown driver: ${driver}`); } try { await stat(join(process.cwd(), "yarn.lock")); const lockfile = await readFile(join(process.cwd(), "yarn.lock")); const packagejson = await readFile(join(process.cwd(), "package.json")); spawnSync("yarn", ["add", pkg], { stdio: "inherit", }); await writeFile(join(process.cwd(), "yarn.lock"), lockfile); await writeFile(join(process.cwd(), "package.json"), packagejson); return; } catch {} spawnSync("npm", ["install", "--no-save", "--legacy-peer-deps", pkg], { stdio: "inherit", }); };نحاول تثبيت برنامج التشغيل باستخدام الغزل في البداية، لكننا لا نريد إنشاء أي اختلافات في الدليل، لذلك نحتفظ بملفات Yarn.lock وpackage.json. إذا لم يكن الغزل متاحًا، فسنعود إلى npm.
عندما نتأكد من تثبيت برنامج التشغيل، يمكننا إنشاء محول واستخدامه:
export async function loadAdapter(driver) { await downloadDriver(driver); return import(PACKAGES[driver].split("@")[0]).then( (m) => new PGAdapter(m.default), );2.4 تنفيذ منطق الهجرة
نبدأ بالاتصال بقاعدة البيانات والحصول على الإصدار الحالي:
await adapter.connect(options.databaseUrl); console.log("Connected to database"); const currentVersion = await adapter.transact(async (sql) => { await sql(`create table if not exists schema_migrations ( version integer primary key )`); const result = await sql(`select version from schema_migrations limit 1`); return result[0]?.version || 0; }); console.log(`Current version: ${currentVersion}`);ثم نقرأ دليل الترحيلات ونصنفها حسب الإصدار. بعد ذلك، نقوم بتطبيق كل عملية ترحيل تحتوي على إصدار أكبر من الإصدار الحالي. سأقدم فقط الترحيل الفعلي في المقتطف التالي:
await adapter.transact(async (sql) => { await sql(upMigration); await sql( `insert into schema_migrations (version) values (${version})` ); await sql(`delete from schema_migrations where version != ${version}`); });تشبه عملية الترحيل إلى الحالة السابقة، ولكننا نقوم بفرز عمليات الترحيل بترتيب عكسي ونطبقها حتى نصل إلى الإصدار المطلوب.
3. الاختبار
قررت عدم استخدام أي إطار اختبار محدد ولكن استخدام إمكانات اختبار العقدة المضمنة. وهي تشمل مشغل الاختبار وحزمة التأكيد.
import { it, before, after, describe } from "node:test"; import assert from "node:assert";ولتنفيذ الاختبارات سأقوم بتشغيل العقدة --test --test-concurrency=1.
في الواقع، كنت أكتب الكود بطريقة TDD. لم أتحقق من أن كود الترحيل الخاص بي يعمل يدويًا، لكنني كنت أكتبه مع الاختبارات. ولهذا السبب قررت أن الاختبارات الشاملة ستكون الأنسب لهذه الأداة.
لمثل هذا النهج، ستحتاج الاختبارات إلى تمهيد قاعدة بيانات فارغة، وتطبيق بعض عمليات الترحيل، والتحقق من صحة محتويات قاعدة البيانات، ثم العودة إلى الحالة الأولية والتحقق من أن قاعدة البيانات فارغة.
لتشغيل قاعدة بيانات، استخدمت مكتبة "testcontainers"، والتي توفر غلافًا لطيفًا حول عامل الإرساء.before(async () => { console.log("Starting container"); container = await new GenericContainer("postgres:16-alpine") .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: "password" }) .start(); }); after(async () => { await container.stop(); });لقد كتبت بعض عمليات الترحيل البسيطة واختبرت أنها تعمل كما هو متوقع. فيما يلي مثال للتحقق من صحة حالة قاعدة البيانات:
const sql = pg(`postgres://postgres:password@localhost:${port}/postgres`); const result = await sql`select * from schema_migrations`; assert.deepEqual(result, [{ version: 2 }]); const tables = await sql`select table_name from information_schema.tables where table_schema = 'public'`; assert.deepEqual(tables, [ { table_name: "schema_migrations" }, { table_name: "test" }, ]);4. الاستنتاج
كان هذا مثالاً لكيفية التعامل مع تطوير أداة CLI بسيطة في نظام جافا سكريبت البيئي. أريد أن أشير إلى أن نظام جافا سكريبت البيئي الحديث مشحون وقوي للغاية، وتمكنت من تنفيذ الأداة مع الحد الأدنى من التبعيات الخارجية. لقد استخدمت برنامج تشغيل postgres الذي يمكن تنزيله عند الطلب وحاويات الاختبار للاختبارات. أعتقد أن هذا الأسلوب يمنح المطورين أكبر قدر من المرونة والتحكم في التطبيق.
5. المراجع
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3