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

feat: save relative migrations path and add command to fix existing absolute paths #123

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ npm install -g mongo-migrate-ts
Usage: mongo-migrate [options] [command]

Options:
-h, --help output usage information
-h, --help display help for command
Copy link
Author

Choose a reason for hiding this comment

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

this change came from the actual output of running the command


Commands:
init Creates the migrations directory and configuration file
new [options] Create a new migration file under migrations directory
up Run all pending migrations
down [options] Undo migrations
status Show the status of the migrations
init Creates the migrations directory and configuration file
new [options] Create a new migration file under migrations directory
up Run all pending migrations
down [options] Undo migrations
status Show the status of the migrations
fix-paths [options] Convert absolute migration file paths into relative paths
help [command] display help for command
```

Create a directory for your migrations.
Expand Down Expand Up @@ -175,4 +177,15 @@ export class Transaction1691171075957 implements MigrationInterface {
}
}
}
```
```

## Fixing absolute migration file paths

Old versions of this package used to store migration file paths as absolute paths which meant a rollback could only be performed from the same machine that executed the migration, or one with a similar directory structure.

To fix these migration entries in your database you can use the `fix-paths` command:

```
ts-node migrations/index.ts fix-paths --base-path "[PATH_TO_MIGRATIONS_DIR_AS_SAVED_IN_DATABASE]" --dry-run
```

2 changes: 1 addition & 1 deletion __tests__/down.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('down command', () => {
new Promise((resolve) => resolve(fakeMigrations))
);

(loadMigrationFile as jest.Mock).mockImplementation((file: string) => [
(loadMigrationFile as jest.Mock).mockImplementation((_migrationsDir:string, file: string) => [
{
...fakeMigrations.find((m: MigrationModel) => m.file === file),
instance: fakeMigrationInstance,
Expand Down
83 changes: 83 additions & 0 deletions __tests__/fixPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
jest.mock('../lib/database');
jest.mock('../lib/migrations');

import { oraMock } from './__mocks__/ora.mock';
jest.mock('ora', () => {
return jest.fn().mockImplementation(oraMock);
});

import { MigrationModel, mongoConnect } from '../lib/database';
import { configMock } from './__mocks__/config.mock';
import { connectionMock } from './__mocks__/connection.mock';
import { fixPaths } from '../lib/commands/fixPaths';

describe('fixPaths command', () => {
const numberOfMigrations = 10;
const fakeMigrations: MigrationModel[] = Array(numberOfMigrations)
.fill(undefined)
.map((v: MigrationModel, index: number) => ({
_id: `${index}`,
className: `MigrationTest${index}`,
file: `${
index % 3 === 0 ? '/home/runner/migrations/' : '/home/user/project/'
}MigrationTest${index}.ts`,
timestamp: +new Date(),
}));

(mongoConnect as jest.Mock).mockReturnValue(
new Promise((resolve) => resolve(connectionMock))
);

const createMockCursor = () => {
let i = 0;
return {
hasNext: () => i < fakeMigrations.length,
next: () => {
const item = fakeMigrations[i];
i += 1;
return item ?? null;
},
};
};

const mockUpdateOne = jest.fn();

beforeEach(() => {
(connectionMock.client.close as jest.Mock).mockReset();
connectionMock.getMigrationsCollection.mockReset();
mockUpdateOne.mockReset();

connectionMock.getMigrationsCollection.mockReturnValue({
updateOne: mockUpdateOne,
find: jest.fn().mockImplementation(createMockCursor),
});
});

it('should convert absolute paths only of matching migrations', async () => {
const basePath = '/home/runner/migrations';
await fixPaths({ config: configMock, basePath });

fakeMigrations.forEach((fakeMigration, index) => {
(fakeMigration.file.startsWith(basePath)
? expect(mockUpdateOne)
: expect(mockUpdateOne).not
).toHaveBeenCalledWith(
{ _id: `${index}` },
{ $set: { file: `MigrationTest${index}.ts` } }
);
});

expect(connectionMock.client.close).toBeCalled();
});

it('should not alter the database in dry run mode', async () => {
await fixPaths({
config: configMock,
dryRun: true,
basePath: '/home/runner/migrations',
});

expect(mockUpdateOne).not.toHaveBeenCalled();
expect(connectionMock.client.close).toBeCalled();
});
});
17 changes: 17 additions & 0 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { init } from './commands/init';
import { newCommand } from './commands/new';
import { status } from './commands/status';
import { up } from './commands/up';
import { fixPaths } from './commands/fixPaths';
import { Config, processConfig } from './config';

export const cli = (config?: Config): void => {
Expand Down Expand Up @@ -86,6 +87,22 @@ export const cli = (config?: Config): void => {
.action(async () => {
await status({ config });
});

program
.command('fix-paths')
.description('Convert absolute migration file paths into relative paths')
.option(
'-p, --base-path <path>',
'Override the base path. The absolute migration dir on the current machine is used as default'
)
.option('-d, --dry-run', "Show updates but don't apply them")
.action(async (opts) => {
await fixPaths({
config,
basePath: opts.basePath,
dryRun: !!opts.dryRun,
});
});
}

program.parse();
Expand Down
16 changes: 9 additions & 7 deletions lib/commands/down.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ interface CommandDownOptions {

const downLastAppliedMigration = async (
connection: DatabaseConnection,
collection: Collection<MigrationModel>
collection: Collection<MigrationModel>,
migrationsDir: string
): Promise<void> => {
const spinner = ora(`Undoing last migration`).start();
const lastApplied = await getLastAppliedMigration(collection);
Expand All @@ -31,7 +32,7 @@ const downLastAppliedMigration = async (
}

spinner.text = `Undoing migration ${lastApplied.className}`;
const migrationFile = await loadMigrationFile(lastApplied.file);
const migrationFile = await loadMigrationFile(migrationsDir, lastApplied.file);
const migration = migrationFile.find(
(m: MigrationObject) => m.className === lastApplied.className
);
Expand All @@ -51,7 +52,8 @@ const downLastAppliedMigration = async (

const downAll = async (
connection: DatabaseConnection,
collection: Collection<MigrationModel>
collection: Collection<MigrationModel>,
migrationsDir: string
): Promise<void> => {
const spinner = ora(`Undoing all migrations`).start();
const appliedMigrations = await getAppliedMigrations(collection);
Expand All @@ -63,7 +65,7 @@ const downAll = async (

const migrationsToUndo = await Promise.all(
appliedMigrations.map(async (migration: MigrationModel) => {
const m = await loadMigrationFile(migration.file);
const m = await loadMigrationFile(migrationsDir, migration.file);
if (m && m.length === 0) {
throw new Error(
`Can undo migration ${migration.className}, no class found`
Expand Down Expand Up @@ -95,17 +97,17 @@ export const down = async ({
mode,
config,
}: CommandDownOptions): Promise<void> => {
const { uri, database, options, migrationsCollection } =
const { uri, database, options, migrationsCollection, migrationsDir } =
processConfig(config);
const connection = await mongoConnect(uri, database, options);
const collection = connection.getMigrationsCollection(migrationsCollection);
try {
switch (mode) {
case 'all':
await downAll(connection, collection);
await downAll(connection, collection, migrationsDir);
break;
case 'last':
await downLastAppliedMigration(connection, collection);
await downLastAppliedMigration(connection, collection, migrationsDir);
break;
}
} finally {
Expand Down
53 changes: 53 additions & 0 deletions lib/commands/fixPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as path from 'path';
import { Config, processConfig } from '../config';
import { mongoConnect } from '../database';

interface CommandFixPathsOptions {
config: Config;
basePath?: string;
dryRun?: boolean;
}

export const fixPaths = async ({
basePath,
config,
dryRun,
}: CommandFixPathsOptions): Promise<void> => {
const { uri, database, options, migrationsCollection, migrationsDir } =
processConfig(config);
const connection = await mongoConnect(uri, database, options);
const collection = connection.getMigrationsCollection(migrationsCollection);

const basePathWithMigrationsDirDefault =
basePath ?? path.resolve(migrationsDir);


try {
const cursor = collection.find({
file: new RegExp(`^${basePathWithMigrationsDirDefault}.*`),
});

while (await cursor.hasNext()) {
const migration = await cursor.next();
if (!migration) break;

const newFilePath = path.relative(
basePathWithMigrationsDirDefault,
migration.file
);

console.log(
`Updating migration path "${migration.file}" to "${newFilePath}"`
);

if (!dryRun) {
await collection.updateOne(
{ _id: migration._id },
{ $set: { file: newFilePath } }
);
}
}
} finally {
await connection.client.close();
}
};
14 changes: 9 additions & 5 deletions lib/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ const isMigration = (obj: any): boolean => {
};

export const loadMigrationFile = async (
filePath: string
migrationsDir: string,
filePath: string,
): Promise<MigrationObject[]> => {
if (!fs.existsSync(filePath)) {
throw new Error(`File ${filePath} not exists.`);

const absoluteFilePath = path.resolve(migrationsDir, filePath);

if (!fs.existsSync(absoluteFilePath)) {
throw new Error(`File ${absoluteFilePath} does not exist.`);
}

const classes = await import(path.resolve(filePath));
const classes = await import(path.resolve(absoluteFilePath));

return Object.keys(classes)
.filter((key: string) => typeof classes[key] === 'function')
Expand All @@ -51,7 +55,7 @@ export const loadMigrations = async (
const migrations = await Promise.all(
paths
.map((path) => (typeof path === 'string' ? path : path.fullpath()))
.map((path) => loadMigrationFile(`${migrationsDir}/${path}`))
.map((path) => loadMigrationFile(migrationsDir, path))
);

// flat migrations because in one file can be more than one migration
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"mongodb": "^6.2.0",
"prettier": "^2.5.1",
"rimraf": "^4.4.0",
"rollup": "^2.66.1",
"rollup": "^4.24.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
Expand All @@ -58,16 +58,17 @@
"cli-table": "^0.3.11",
"commander": "^9.0.0",
"connection-string": "^4.3.5",
"glob": "^10.3.12",
"date-fns": "^2.30.0",
"glob": "^10.3.12",
"ora": "5.4.1"
},
"peerDependencies": {
"mongodb": "4.x.x || 5.x.x || 6.x.x"
},
"resolutions": {
"string-width": "4.2.3",
"strip-ansi": "6.0.1"
"strip-ansi": "6.0.1",
"micromatch": ">=4.0.8"
},
"lint-staged": {
"*.{ts,js}": [
Expand Down
Loading