Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Fix migration commands #219

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 105 additions & 35 deletions lib/dbms.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,46 @@ const { Model, Schema } = require('metaschema');

const dbms = {};

const cpdir = async (from, to) => {
const isUtilityFile = (file) => {
const firstCh = file[0];
return firstCh === '.' || firstCh === firstCh.toLowerCase();
};

const makeVersionName = (v, date) => `${date}--v${v}`;

const cpdir = async (from, to, filter) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for future todo: we may move this function to metautil

await fsp.mkdir(to, { recursive: true });
const files = await fsp.readdir(from, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) continue;
const basename = file.name.slice(0, file.name.lastIndexOf('.'));
if (!isUtilityFile(file.name) && filter && !filter.includes(basename)) {
continue;
}
const fromPath = path.join(from, file.name);
const toPath = path.join(to, file.name);
await fsp.copyFile(fromPath, toPath);
}
};

const shorten = (dir) => {
const pos = dir.lastIndexOf('/');
return pos !== -1 ? dir.substring(pos) : dir;
const shorten = (dir, target = null) => {
if (target) {
return path.relative(target, dir);
}
const pos = dir.lastIndexOf(path.sep);
return pos !== -1 ? dir.slice(pos) : dir;
};

const diffModels = (oldModel, newModel) => {
if (oldModel === newModel) return [];
const newSchemas = [];
for (const [name, schema] of newModel.entities) {
const oldSchema = oldModel.entities.get(name);
if (!oldSchema || JSON.stringify(oldSchema) !== JSON.stringify(schema)) {
newSchemas.push(name);
}
}
return newSchemas;
};

const loadModel = async (modelPath) => {
Expand All @@ -30,15 +57,26 @@ const loadModel = async (modelPath) => {
return model;
};

const getPreviousVersion = async (modelPath) => {
const historyPath = path.join(modelPath, 'history');
const folders = await fsp.readdir(historyPath, { withFileTypes: true });
const replaceModelVersion = async (newVersion, modelPath) => {
const databasePath = path.join(modelPath, '.database.js');
const existingFile = await fsp.readFile(databasePath, { encoding: 'utf8' });
const newFile = existingFile.replace(
/version: \d+/,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd better use string functions instead of regex

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make it much harder to check that we have exactly /version: \d+/, (checking numbers as well) and therefore possibly replacing the wrong thing in a file.

`version: ${newVersion}`
);
await fsp.writeFile(databasePath, newFile, { encoding: 'utf8' });
};

const getPreviousVersion = async (modelPath, historyPath) => {
const folders = await fsp
.readdir(historyPath, { withFileTypes: true })
.catch(() => []);
let version = 0;
let previousName = '';
for (const folder of folders) {
if (!folder.isDirectory()) continue;
const { name } = folder;
const v = parseInt(name.substring(name.indexOf('v') + 1), 10);
const v = parseInt(/v(\d+)/.exec(name)[1], 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

if (v > version) {
version = v;
previousName = folder.name;
Expand Down Expand Up @@ -73,39 +111,71 @@ const create = async (modelPath, outputPath = modelPath) => {
await model.saveTypes(dtsPath);
};

const generate = async (modelPath) => {
const model = await loadModel(modelPath);
const { name, driver, version } = model.database;
const now = new Date().toISOString().substring(0, 10);
console.log(`Generate migration: ${driver}:${name} v${version} (${now})`);
const previousPath = await getPreviousVersion(modelPath).catch(() => {});
const createMigrations = async (migrationPath, fullVersion, cwd) => {
const mig = path.join(migrationPath, fullVersion);
await fsp.mkdir(mig, { recursive: true });
const migUp = path.join(mig, `${fullVersion}-up.sql`);
await fsp.writeFile(migUp, '', { encoding: 'utf8' });
const migDn = path.join(mig, `${fullVersion}-dn.sql`);
await fsp.writeFile(migDn, '', { encoding: 'utf8' });
console.log(`Migration up: ${shorten(migUp, cwd)}`);
console.log(`Migration down: ${shorten(migDn, cwd)}`);
};

const generate = async (modelPath, cwd = process.cwd()) => {
const historyPath = path.join(modelPath, 'history');
const migrationPath = path.join(modelPath, 'migration');
const currentModel = await loadModel(modelPath);
const { name, driver, version } = currentModel.database;
const now = new Date().toISOString().replace(/:/g, '-').split('.')[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log(`Generating new migration: ${driver}:${name}`);
const previousPath = await getPreviousVersion(modelPath, historyPath);

let previousModel;
if (!previousPath) {
console.log('Previous version is not found in ../history');
return;
console.log(`Previous version not found in ${shorten(historyPath, cwd)}`);
const oldVersionPath = path.join(
modelPath,
'history',
makeVersionName(version, now)
);
console.log(
`Saving current as old version to ${shorten(oldVersionPath, cwd)}`
);
await fsp.mkdir(oldVersionPath, { recursive: true });
await cpdir(modelPath, oldVersionPath);
previousModel = currentModel;
} else {
console.log(`Previous version is found in ${shorten(previousPath, cwd)}`);
console.log(`Saving current as new version`);
previousModel = await loadModel(previousPath);
}
const ps = await loadModel(previousPath);
const folder = path.basename(previousPath);
const date = folder.substring(0, folder.lastIndexOf('v') - 1);
console.log(`Previous version: v${ps.database.version} (${date})`);
if (ps.database.version >= version) {
console.log('You have latest model version. Migration is not needed.');
return;

const newVersion =
currentModel.database.version <= previousModel.database.version
? previousModel.database.version + 1
: currentModel.database.version;

if (currentModel.database.version !== newVersion) {
currentModel.database.version = newVersion;
await replaceModelVersion(newVersion, modelPath);
}
const newFolder = path.join(modelPath, `history/${now}-v${version}`);
const mig = path.join(modelPath, `migration/${now}-v${version}`);
const migUp = mig + '-up.sql';
const migDn = mig + '-dn.sql';
console.log(`Save history: ${shorten(newFolder)}`);
await fsp.mkdir(newFolder, { recursive: true });
await cpdir(modelPath, newFolder);
console.log(`Migration up: ${shorten(migUp)}`);
console.log(`Migration down: ${shorten(migDn)}`);

const fullVersion = makeVersionName(newVersion, now);
const versionPath = path.join(historyPath, fullVersion);
console.log(
`New version: v${newVersion} (${now}) to ${shorten(versionPath, cwd)}`
);

const newSchemas = diffModels(previousModel, currentModel);
await cpdir(modelPath, versionPath, newSchemas);
await createMigrations(migrationPath, fullVersion, cwd);
};

const migrate = async (modelPath, version) => {
const migrate = async (modelPath, toVersion) => {
const model = await loadModel(modelPath);
const { execute } = dbms[model.database.driver];
if (version) console.log(`Migration to this version: ${version}`);
if (toVersion) console.log(`Migration to this version: ${toVersion}`);
else console.log('Migration to the latest version');
const migPath = path.join(modelPath, `migrations`);
const files = await fsp.readdir(migPath, { withFileTypes: true });
Expand All @@ -117,7 +187,7 @@ const migrate = async (modelPath, version) => {
const to = name.lastIndexOf('-');
const v = parseInt(name.substring(from, to), 10);
if (!name.endsWith('-up.sql')) continue;
if (!version || v <= version) {
if (!toVersion || v <= toVersion) {
const fileName = path.join(migPath, name);
const sql = await fsp.readFile(fileName, 'utf8');
console.log(`Apply script: ${name}`);
Expand Down