Quiero tener una herramienta de migración de bases de datos, que tenga las siguientes propiedades:
Muchos de estos puntos surgieron de mi experiencia con esta increíble herramienta llamada tern. ¡Me entristeció que JavaScript no tuviera lo mismo! (O tal vez soy un desastre buscando en Google...). Así que decidí que este podría ser un buen ejercicio de codificación para mí y una historia que podría resultar interesante para otra persona :)
¡Vamos a robar diseñar la herramienta CLI!
Entonces la sintaxis de la herramienta sería la siguiente: martlet up --database-url
Donde "arriba" debería aplicar todas las migraciones que aún no se han aplicado y abajo debería revertirse a la versión especificada.
Las opciones tienen el siguiente significado y valores predeterminados:
Como puede ver, comencé a descubrir cómo invocaría la herramienta antes de escribir cualquier código real. Esta es una buena práctica, ayuda a cumplir los requisitos y reducir los ciclos de desarrollo.
Ok, ¡lo primero es lo primero! Creemos un archivo index.js y enviemos el mensaje de ayuda. Se vería así:
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();
Ahora analizaremos las opciones:
export function parseOptions(args) { const options = { dir: "migrations", driver: "pg", databaseUrl: process.env.DATABASE_URL, }; for (let idx = 0; idxComo puedes ver, no uso ninguna biblioteca para analizar; Simplemente repito la lista de argumentos y proceso cada opción. Entonces, si tengo una opción booleana, cambiaría el índice de iteración en 1, y si tengo una opción con un valor, lo cambiaría en 2.
2.2 Implementación del adaptador de controlador
Para admitir múltiples controladores, necesitamos tener alguna interfaz universal para acceder a una base de datos; así es como puede verse:
interface Adapter { connect(url: string): Promise; transact(query: (fn: (text) => Promise )): Promise ; close(): Promise ; } Creo que conectar y cerrar son funciones bastante obvias, déjame explicarte el método de transacción. Debería aceptar una función que sería llamada con una función que acepta un texto de consulta y devuelve una promesa con un resultado intermedio. Esta complejidad es necesaria para tener una interfaz general que brinde la capacidad de ejecutar múltiples consultas dentro de una transacción. Es más fácil de entender mirando el ejemplo de uso.
Así es como el adaptador busca el controlador 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(); } }Y el ejemplo de uso podría ser:
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 Instalación del controlador bajo demanda
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", }); };Al principio intentamos instalar el controlador con Yarn, pero no queremos generar ninguna diferencia en el directorio, por lo que conservamos los archivos Yarn.lock y Package.json. Si el hilo no está disponible, recurriremos a npm.
Cuando nos aseguramos de que el controlador esté instalado, podemos crear un adaptador y usarlo:
export async function loadAdapter(driver) { await downloadDriver(driver); return import(PACKAGES[driver].split("@")[0]).then( (m) => new PGAdapter(m.default), );2.4 Implementación de la lógica de migración
Comenzamos conectándonos a la base de datos y obteniendo la versión actual:
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}`);Luego, leemos el directorio de migraciones y las ordenamos por versión. Después de eso, aplicamos cada migración que tenga una versión mayor a la actual. Simplemente presentaré la migración real en el siguiente fragmento:
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}`); });La migración de reversión es similar, pero clasificamos las migraciones en orden inverso y las aplicamos hasta llegar a la versión deseada.
3. Pruebas
Decidí no utilizar ningún marco de prueba específico, sino utilizar las capacidades de prueba integradas de Nodejs. Incluyen el ejecutor de pruebas y el paquete de aserción.
import { it, before, after, describe } from "node:test"; import assert from "node:assert";Y para ejecutar pruebas ejecutaría node --test --test-concurrency=1.
En realidad, estaba escribiendo el código en una especie de forma TDD. No validé que mi código de migraciones funcionara a mano, pero lo fui escribiendo junto con las pruebas. Por eso decidí que las pruebas de un extremo a otro serían la mejor opción para esta herramienta.
Para tal enfoque, las pruebas necesitarían iniciar una base de datos vacía, aplicar algunas migraciones, verificar que el contenido de la base de datos sea correcto y luego volver al estado inicial y validar que la base de datos esté vacía.
Para ejecutar una base de datos, utilicé la biblioteca "testcontainers", que proporciona un buen complemento para la ventana acoplable.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(); });Escribí algunas migraciones simples y probé que funcionaban como se esperaba. A continuación se muestra un ejemplo de validación del estado de una base de datos:
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. Conclusión
Este fue un ejemplo de cómo abordaría el desarrollo de una herramienta CLI simple en el ecosistema de JavaScript. Quiero señalar que el ecosistema moderno de JavaScript es bastante potente y potente, y logré implementar la herramienta con un mínimo de dependencias externas. Utilicé un controlador Postgres que se descargaría a pedido y contenedores de prueba para las pruebas. Creo que ese enfoque ofrece a los desarrolladores la mayor flexibilidad y control sobre la aplicación.
5. Referencias
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3