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

Define custom prefixes for creating new migrations #585

Open
wants to merge 2 commits into
base: main
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,25 @@ const umzug = new Umzug({
})
```

You can also customize the format of the prefix added to migration file names:

```js
const umzug = new Umzug({
migrations: ...,
create: {
prefix: (prefix) => {
const base = new Date().toISOString().replace(/[-:]/g, "");
const prefixes = {
TIMESTAMP: base.split(".")[0],
DATE: base.split("T")[0],
NONE: '',
};
return prefixes[prefix];
}
}
})
```

The create command includes some safety checks to make sure migrations aren't created with ambiguous ordering, and that they will be picked up by umzug when applying migrations. The first pair is expected to be the "up" migration file, and to be picked up by the `pending` command.

Use `node migrator create --help` for more options:
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type Promisable<T> = T | PromiseLike<T>;

export type LogFn = (message: Record<string, unknown>) => void;

export type MigrationPrefix = 'TIMESTAMP' | 'DATE' | 'NONE';

/** Constructor options for the Umzug class */
export type UmzugOptions<Ctx extends {} = Record<string, unknown>> = {
/** The migrations that the Umzug instance should perform */
Expand All @@ -34,6 +36,11 @@ export type UmzugOptions<Ctx extends {} = Record<string, unknown>> = {
* in the same folder as the last existing migration. The value here can be overriden by passing `folder` when calling `create`.
*/
folder?: string;
/**
* A function for generating custom prefixes for migration files when using `create`. If this is not specified the default date formats will
* be used ("1970.01.01T00.00.00" for TIMESTAMP, "1970.01.01" for DATE and "" for NONE)
*/
prefix?: (prefix: MigrationPrefix) => string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
prefix?: (prefix: MigrationPrefix) => string;
prefix?: (params: {name: string}) => string;

I like this approach but we can go further. The MigrationPrefix type isn't really useful anymore. Let's deprecate it and just pass the name in and let the user define a function that does whatever they need. The default can still be the dot-separated date format that's currently used.

};
};

Expand Down
24 changes: 15 additions & 9 deletions src/umzug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
RunnableMigration,
UmzugEvents,
UmzugOptions,
MigrationPrefix,
} from './types';
import { RerunBehavior } from './types';

Expand Down Expand Up @@ -332,23 +333,28 @@ export class Umzug<Ctx extends object = object> extends emittery<UmzugEvents<Ctx
});
}

private getDefaultPrefix(prefix: MigrationPrefix) {
const isoDate = new Date().toISOString();
const prefixes = {
TIMESTAMP: isoDate.replace(/\.\d{3}Z$/, '').replace(/\W/g, '.'),
DATE: isoDate.split('T')[0].replace(/\W/g, '.'),
NONE: '',
};
return prefixes[prefix];
}

async create(options: {
name: string;
folder?: string;
prefix?: 'TIMESTAMP' | 'DATE' | 'NONE';
prefix?: MigrationPrefix;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a bit confusing having two places to setup the prefix. Can we deprecate this one with a warning message?

allowExtension?: string;
allowConfusingOrdering?: boolean;
skipVerify?: boolean;
}): Promise<void> {
await this.runCommand('create', async ({ context }) => {
const isoDate = new Date().toISOString();
const prefixes = {
TIMESTAMP: isoDate.replace(/\.\d{3}Z$/, '').replace(/\W/g, '.'),
DATE: isoDate.split('T')[0].replace(/\W/g, '.'),
NONE: '',
};
const prefixType = options.prefix ?? 'TIMESTAMP';
const fileBasename = [prefixes[prefixType], options.name].filter(Boolean).join('.');
const getPrefix = this.options.create?.prefix ?? this.getDefaultPrefix;
const prefix = getPrefix(options.prefix ?? 'TIMESTAMP');
const fileBasename = [prefix, options.name].filter(Boolean).join('.');

const allowedExtensions = options.allowExtension
? [options.allowExtension]
Expand Down
25 changes: 24 additions & 1 deletion test/umzug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jest.mock('../src/storage', () => {
});

const names = (migrations: Array<{ name: string }>) => migrations.map(m => m.name);
const paths = (migrations: Array<{ path?: string }>) => migrations.map(m => m.path);

describe('basic usage', () => {
test('requires script files', async () => {
Expand Down Expand Up @@ -114,7 +115,7 @@ describe('custom context', () => {
expect(pending[0]).toContain('test.x.js');
});

test(`create doesn't cause "confusing oredering" warning when migrations are nested in folders`, async () => {
test(`create doesn't cause "confusing ordering" warning when migrations are nested in folders`, async () => {
const syncer = fsSyncer(path.join(__dirname, 'generated/create-nested-folders'), {});
syncer.sync();

Expand All @@ -141,6 +142,28 @@ describe('custom context', () => {
expect(pending[1]).toContain('test2');
});

test(`create can override default migration prefix`, async () => {
const syncer = fsSyncer(path.join(__dirname, 'generated/create-custom-prefix'), {});
syncer.sync();

const umzug = new Umzug({
migrations: {
glob: ['*.js', { cwd: syncer.baseDir }],
},
logger: undefined,
create: {
folder: syncer.baseDir,
template: filepath => [[`${filepath}.js`, `/* custom template */`]],
prefix: () => 'a-custom-prefix',
},
});

await umzug.create({ name: 'test' });
const pending = paths(await umzug.pending());
expect(pending).toHaveLength(1);
expect(path.basename(pending[0] ?? '')).toContain('a-custom-prefix.test.js');
});

describe(`resolve asynchronous context getter before the migrations run`, () => {
const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const getContext = async () => {
Expand Down