Skip to content

Commit

Permalink
Implemented createBundle
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor-podzigun committed Jun 10, 2024
1 parent a17ac2a commit c243255
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 7 deletions.
70 changes: 63 additions & 7 deletions src/bundler.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/**
* @typedef {import("../index.mjs").MigrationBundle} MigrationBundle
*/
import fs from "fs";
import path from "path";

const bundleFileName = "bundle.json";

/**
* @param {string[]} args
Expand All @@ -11,15 +17,65 @@ export async function createBundle(args) {
}

const migrationsDir = args[0];
try {
if (!fs.lstatSync(migrationsDir).isDirectory()) {
console.error(`"Error: ${migrationsDir}" is not a directory`);
return;
}
} catch (_) {
const dirStats = getFileStats(migrationsDir);
if (!dirStats) {
console.error(`Error: Migrations folder "${migrationsDir}" doesn't exist`);
return;
}
if (!dirStats.isDirectory()) {
console.error(`Error: "${migrationsDir}" is not a directory`);
return;
}

const allFiles = fs.readdirSync(migrationsDir);
let lastModified = 0;
let sqlFiles = /** @type {string[]} */ ([]);
allFiles.sort(strCompare).forEach((f) => {
if (f.endsWith(".sql") || f.endsWith(".SQL")) {
const stats = fs.lstatSync(path.join(migrationsDir, f));
if (lastModified < stats.mtimeMs) {
lastModified = stats.mtimeMs;
}
sqlFiles.push(f);
}
});

console.log(`migrationsDir: "${migrationsDir}"`);
const migrationsBundle = path.join(migrationsDir, bundleFileName);
const bundleStats = getFileStats(migrationsBundle);
if (!bundleStats || bundleStats.mtimeMs < lastModified) {
/** @type {MigrationBundle} */
const bundleObj = sqlFiles.map((file) => {
return {
file,
content: fs.readFileSync(path.join(migrationsDir, file)).toString(),
};
});

fs.writeFileSync(migrationsBundle, JSON.stringify(bundleObj, undefined, 2));
console.log(`Generated SQL bundle file: ${migrationsBundle}`);
return;
}

console.log("Nothing to generate, SQL bundle is up to date!");
}

/**
* @param {string} file
* @returns {fs.Stats | undefined}
*/
function getFileStats(file) {
try {
return fs.lstatSync(file);
} catch (_) {
return undefined;
}
}

/**
* @param {string} a
* @param {string} b
* @returns {number}
*/
function strCompare(a, b) {
return a === b ? 0 : a < b ? -1 : 1;
}
170 changes: 170 additions & 0 deletions test/bundler.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from "fs";
import path from "path";
import assert from "node:assert/strict";
import mockFunction from "mock-fn";
import { createBundle } from "../index.mjs";
Expand All @@ -11,6 +13,10 @@ const { describe, it } = await (async () => {
: import(module);
})();

const bundleFileName = "bundle.json";
const migrationsDir = "./test/migrations";
const migrationsBundle = path.join(migrationsDir, bundleFileName);

describe("bundler.test.mjs", () => {
it("should fail if migrations dir is not specified", async () => {
//given
Expand Down Expand Up @@ -38,4 +44,168 @@ describe("bundler.test.mjs", () => {
);
assert.deepEqual(errorMock.times, 1);
});

it("should fail if migrations dir doesn't exist", async () => {
//given
let capturedError = "";
const errorMock = mockFunction((msg) => {
capturedError = msg;
});
const savedError = console.error;
console.error = errorMock;

//when
let resError = null;
try {
await createBundle(["1234"]);
} catch (error) {
resError = error;
}

//then
console.error = savedError;
assert.deepEqual(resError, null);
assert.deepEqual(
capturedError,
`Error: Migrations folder "1234" doesn't exist`
);
assert.deepEqual(errorMock.times, 1);
});

it("should fail if migrations dir is not a directory", async () => {
//given
let capturedError = "";
const errorMock = mockFunction((msg) => {
capturedError = msg;
});
const savedError = console.error;
console.error = errorMock;

//when
let resError = null;
try {
await createBundle(["package.json"]);
} catch (error) {
resError = error;
}

//then
console.error = savedError;
assert.deepEqual(resError, null);
assert.deepEqual(capturedError, `Error: "package.json" is not a directory`);
assert.deepEqual(errorMock.times, 1);
});

it("should generate new bundle file", async () => {
//given
if (fs.existsSync(migrationsBundle)) {
fs.unlinkSync(migrationsBundle);
}
const logs = /** @type {string[]} */ ([]);
const logMock = mockFunction((msg) => {
logs.push(msg);
});
const savedLog = console.log;
console.log = logMock;

//when
await createBundle([migrationsDir]);

//then
console.log = savedLog;
assert.deepEqual(logMock.times, 1);
assert.deepEqual(logs, [`Generated SQL bundle file: ${migrationsBundle}`]);
assertBundleFile(migrationsBundle);
});

it("should not generate bundle file if it's up to date", async () => {
//given
if (fs.existsSync(migrationsBundle)) {
fs.unlinkSync(migrationsBundle);
}
const logs = /** @type {string[]} */ ([]);
const logMock = mockFunction((msg) => {
logs.push(msg);
});
const savedLog = console.log;
console.log = logMock;

//when & then
await createBundle([migrationsDir]);
assert.deepEqual(logMock.times, 1);
assert.deepEqual(logs, [`Generated SQL bundle file: ${migrationsBundle}`]);
assertBundleFile(migrationsBundle);

//when
await createBundle([migrationsDir]);

//then
console.log = savedLog;
assert.deepEqual(logMock.times, 2);
assert.deepEqual(logs, [
`Generated SQL bundle file: ${migrationsBundle}`,
`Nothing to generate, SQL bundle is up to date!`,
]);
assertBundleFile(migrationsBundle);
});

it("should re-generate bundle file if it's outdated", async () => {
//given
if (fs.existsSync(migrationsBundle)) {
fs.unlinkSync(migrationsBundle);
}
const logs = /** @type {string[]} */ ([]);
const logMock = mockFunction((msg) => {
logs.push(msg);
});
const savedLog = console.log;
console.log = logMock;

//when & then
await createBundle([migrationsDir]);
assert.deepEqual(logMock.times, 1);
assert.deepEqual(logs, [`Generated SQL bundle file: ${migrationsBundle}`]);
assertBundleFile(migrationsBundle);

const sqlStats = fs.lstatSync(
path.join(migrationsDir, "V001__initial_db_structure.sql")
);
const bundleStats = fs.lstatSync(migrationsBundle);
fs.utimesSync(
migrationsBundle,
bundleStats.atimeMs / 1000,
sqlStats.mtimeMs / 1000 - 60 // set bundle time to minus 60 sec.
);

//when
await createBundle([migrationsDir]);

//then
console.log = savedLog;
assert.deepEqual(logMock.times, 2);
assert.deepEqual(logs, [
`Generated SQL bundle file: ${migrationsBundle}`,
`Generated SQL bundle file: ${migrationsBundle}`,
]);
assertBundleFile(migrationsBundle);
});
});

/**
* @param {string} bundleFile
*/
function assertBundleFile(bundleFile) {
assert.deepEqual(
fs.readFileSync(bundleFile).toString(),
`[
{
"file": "V001__initial_db_structure.sql",
"content": "\\n-- non-transactional\\nPRAGMA foreign_keys = ON;\\n\\n-- comment 1\\n-- comment 2\\ncreate table test_migrations (\\n id integer primary key, -- inline comment\\n original_name text\\n);\\n\\ninsert into test_migrations (original_name) values ('test 1');\\n"
},
{
"file": "V002__rename_db_field.sql",
"content": "\\n/*\\n * multi-line comment\\n */\\n\\nalter table test_migrations rename column original_name to new_name;\\n\\ninsert into test_migrations (new_name) values ('test 2');\\n"
}
]`
);
}

0 comments on commit c243255

Please sign in to comment.