Skip to content

Commit

Permalink
feat(migrate) Allows for currentVersion to be async function
Browse files Browse the repository at this point in the history
  • Loading branch information
karlludwigweise committed Feb 16, 2021
1 parent b58e0b3 commit 56bbaca
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 65 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A database migration runner that
- will run your migration functions
- will find and run migrations based on your settings (defaults to latest version and a new database).
- stops running migrations, when one fails
- runs all remaining migrations, if `curVersion` is `null`.
- runs all remaining migrations, if `currentVersion` is `null`.

## Usage

Expand All @@ -18,7 +18,7 @@ yarn add @klw/node-database-migration
```
import { migrate } from "@klw/node-database-migration";
const curVersion = null;
const currentVersion = null;
const migrations = {
one: {
version: "1",
Expand All @@ -37,7 +37,7 @@ const migrations = {
},
};
const result = await migrate([migrations.one, migrations.two, migrations.three], curVersion);
const result = await migrate([migrations.one, migrations.two, migrations.three], currentVersion);
```

### Your Migration Functions
Expand All @@ -56,11 +56,19 @@ You may add `options` to your migrations.

```
const options = { targetVersion: "5" }
const result = await migrate(migrations, curVersion, options);
const result = await migrate(migrations, currentVersion, options);
```

Currently only the `targetVersion` option is available. This will enable you to downgrade.

You may pass an async function as `currentVersion`.

```
const result = await migrate(migrations, async () => Promise.resolve("X"));
```

This way you can avoid top level `await`, if needed.

## Return value

The migration runner will stop running migrations, if one fails. In either case you will get a resolved `Promise`.
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ test("should run migrations UP, with current version supplied", async () => {
expect(received).toEqual(expected);
});

test("should get version from async function", async () => {
const expected = {
success: true,
targetVersion: "3",
direction: "UP",
started: ["2", "3"],
fulfilled: ["2", "3"],
};
const received = await migrate([migrations.one.success, migrations.two.success, migrations.three.success], async () =>
Promise.resolve("1"),
);
expect(received).toEqual(expected);
});

test("should run migrations DOWN", async () => {
const expected = {
success: true,
Expand Down
64 changes: 3 additions & 61 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,4 @@
import { runSequence } from "@klw/node-sequential-promises";
import { Migration, MigrationOptions, MigrationResult, MigrationVersion } from "./types";
import { migrate } from "./migrate";
import { Migration, MigrationOptions, MigrationResult, MigrationVersion, MigrationError } from "./types";

/**
* Runs your database migrations up and down.
*
* @param migrations - Array of all Migrations. See docs/types for details.
* @param currentVersion - The current version (or null) if the database is not set up yet.
* @param options - Set the target version with {targetVersion: string}
*
* Will fallback to latest version, if target version is not specified.
*/
export const migrate = async (
migrations: Migration[],
currentVersion: MigrationVersion | null,
options?: MigrationOptions,
): Promise<MigrationResult | undefined> => {
const latestVersion = migrations[migrations.length - 1].version;
const targetVersion = options?.targetVersion || "latest";
const newVersion = targetVersion === "latest" ? latestVersion : targetVersion || latestVersion;
const curVersionIndex = migrations.findIndex((x) => x.version === currentVersion);
const newVersionIndex = migrations.findIndex((x) => x.version === newVersion);

// Don't know what to do with not-matching version
if (currentVersion && curVersionIndex === -1) {
return Promise.reject(`No matching currentVersion (${currentVersion}) found.`);
}

// Define actual migrations to run
const migrateUp = newVersionIndex >= curVersionIndex;
const steps = migrateUp
? migrations.slice(curVersionIndex + 1, newVersionIndex + 1)
: migrations.slice(newVersionIndex + 1, curVersionIndex + 1).reverse();

// Target version number
const returnTargetVersion = migrations[newVersionIndex].version;

// No migrations to run
if (steps.length === 0) {
return Promise.resolve({
success: true,
started: [],
fulfilled: [],
direction: "UP",
targetVersion: returnTargetVersion,
});
}

// Actually run migrations
const result = await runSequence(steps.map((step) => (migrateUp ? step.up : step.down)));

const started = result.started.map((index) => steps[index].version);
const fulfilled = result.fulfilled.map((index) => steps[index].version);
return {
targetVersion: returnTargetVersion,
direction: migrateUp ? "UP" : "DOWN",
success: result.success,
started,
fulfilled,
errorMessage: result.error,
};
};
export { migrate, Migration, MigrationOptions, MigrationResult, MigrationVersion, MigrationError };
63 changes: 63 additions & 0 deletions src/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { runSequence } from "@klw/node-sequential-promises";
import { Migration, MigrationOptions, MigrationResult, MigrationVersion } from "./types";

/**
* Runs your database migrations up and down.
*
* @param migrations - Array of all Migrations. See docs/types for details.
* @param currentVersion - The current version (or null) if the database is not set up yet.
* @param options - Set the target version with {targetVersion: string}
*
* Will fallback to latest version, if target version is not specified.
*/
export const migrate = async (
migrations: Migration[],
currentVersion: MigrationVersion | null | (() => Promise<MigrationVersion | null>),
options?: MigrationOptions,
): Promise<MigrationResult | undefined> => {
const curVersion = currentVersion instanceof Function ? await currentVersion() : currentVersion;
const latestVersion = migrations[migrations.length - 1].version;
const targetVersion = options?.targetVersion || "latest";
const newVersion = targetVersion === "latest" ? latestVersion : targetVersion || latestVersion;
const curVersionIndex = migrations.findIndex((x) => x.version === curVersion);
const newVersionIndex = migrations.findIndex((x) => x.version === newVersion);

// Don't know what to do with not-matching version
if (curVersion && curVersionIndex === -1) {
return Promise.reject(`No matching currentVersion (${curVersion}) found.`);
}

// Define actual migrations to run
const migrateUp = newVersionIndex >= curVersionIndex;
const steps = migrateUp
? migrations.slice(curVersionIndex + 1, newVersionIndex + 1)
: migrations.slice(newVersionIndex + 1, curVersionIndex + 1).reverse();

// Target version number
const returnTargetVersion = migrations[newVersionIndex].version;

// No migrations to run
if (steps.length === 0) {
return Promise.resolve({
success: true,
started: [],
fulfilled: [],
direction: "UP",
targetVersion: returnTargetVersion,
});
}

// Actually run migrations
const result = await runSequence(steps.map((step) => (migrateUp ? step.up : step.down)));

const started = result.started.map((index) => steps[index].version);
const fulfilled = result.fulfilled.map((index) => steps[index].version);
return {
targetVersion: returnTargetVersion,
direction: migrateUp ? "UP" : "DOWN",
success: result.success,
started,
fulfilled,
errorMessage: result.error,
};
};

0 comments on commit 56bbaca

Please sign in to comment.