fix(migrations): don't include folders when collecting migrations

It should be possible to have folders inside your migrations folder
This commit is contained in:
Joakim Carlstein 2024-01-26 09:17:47 +01:00 committed by Joakim Carlstein
parent 9109238b86
commit f1b9098750
3 changed files with 95 additions and 29 deletions

View file

@ -0,0 +1,5 @@
---
'@emigrate/cli': patch
---
Only include files when collecting migrations, i.e. it should be possible to have folders inside your migrations folder.

View file

@ -3,21 +3,27 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import assert from 'node:assert'; import assert from 'node:assert';
import { getMigrations } from './get-migrations.js'; import { getMigrations } from './get-migrations.js';
const originalReaddir = fs.readdir; const originalOpendir = fs.opendir;
const readdirMock = mock.fn(originalReaddir); const opendirMock = mock.fn(originalOpendir);
describe('get-migrations', () => { describe('get-migrations', () => {
beforeEach(() => { beforeEach(() => {
fs.readdir = readdirMock; fs.opendir = opendirMock;
}); });
afterEach(() => { afterEach(() => {
readdirMock.mock.restore(); opendirMock.mock.restore();
fs.readdir = originalReaddir; fs.opendir = originalOpendir;
}); });
it('should skip files with leading periods', async () => { it('should skip files with leading periods', async () => {
readdirMock.mock.mockImplementation(async () => ['.foo.js', 'bar.js', 'baz.js']); opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: '.foo.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory'); const migrations = await getMigrations('/cwd/', 'directory');
@ -42,7 +48,13 @@ describe('get-migrations', () => {
}); });
it('should skip files with leading underscores', async () => { it('should skip files with leading underscores', async () => {
readdirMock.mock.mockImplementation(async () => ['_foo.js', 'bar.js', 'baz.js']); opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: '_foo.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory'); const migrations = await getMigrations('/cwd/', 'directory');
@ -67,7 +79,44 @@ describe('get-migrations', () => {
}); });
it('should skip files without file extensions', async () => { it('should skip files without file extensions', async () => {
readdirMock.mock.mockImplementation(async () => ['foo', 'bar.js', 'baz.js']); opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory');
assert.deepStrictEqual(migrations, [
{
name: 'bar.js',
filePath: '/cwd/directory/bar.js',
relativeFilePath: 'directory/bar.js',
extension: '.js',
directory: 'directory',
cwd: '/cwd/',
},
{
name: 'baz.js',
filePath: '/cwd/directory/baz.js',
relativeFilePath: 'directory/baz.js',
extension: '.js',
directory: 'directory',
cwd: '/cwd/',
},
]);
});
it('should skip non-files', async () => {
opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo.js', isFile: () => false },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory'); const migrations = await getMigrations('/cwd/', 'directory');
@ -92,7 +141,14 @@ describe('get-migrations', () => {
}); });
it('should sort them in lexicographical order', async () => { it('should sort them in lexicographical order', async () => {
readdirMock.mock.mockImplementation(async () => ['foo.js', 'bar_data.js', 'bar.js', 'baz.js']); opendirMock.mock.mockImplementation(async function* () {
yield* [
{ name: 'foo.js', isFile: () => true },
{ name: 'bar_data.js', isFile: () => true },
{ name: 'bar.js', isFile: () => true },
{ name: 'baz.js', isFile: () => true },
];
});
const migrations = await getMigrations('/cwd/', 'directory'); const migrations = await getMigrations('/cwd/', 'directory');

View file

@ -3,37 +3,42 @@ import fs from 'node:fs/promises';
import { type MigrationMetadata } from '@emigrate/types'; import { type MigrationMetadata } from '@emigrate/types';
import { withLeadingPeriod } from './with-leading-period.js'; import { withLeadingPeriod } from './with-leading-period.js';
import { BadOptionError } from './errors.js'; import { BadOptionError } from './errors.js';
import { arrayFromAsync } from './array-from-async.js';
export type GetMigrationsFunction = typeof getMigrations; export type GetMigrationsFunction = typeof getMigrations;
const tryReadDirectory = async (directoryPath: string): Promise<string[]> => { async function* tryReadDirectory(directoryPath: string): AsyncIterable<string> {
try { try {
return await fs.readdir(directoryPath); for await (const entry of await fs.opendir(directoryPath)) {
if (
entry.isFile() &&
!entry.name.startsWith('.') &&
!entry.name.startsWith('_') &&
path.extname(entry.name) !== ''
) {
yield entry.name;
}
}
} catch { } catch {
throw BadOptionError.fromOption('directory', `Couldn't read directory: ${directoryPath}`); throw BadOptionError.fromOption('directory', `Couldn't read directory: ${directoryPath}`);
} }
}; }
export const getMigrations = async (cwd: string, directory: string): Promise<MigrationMetadata[]> => { export const getMigrations = async (cwd: string, directory: string): Promise<MigrationMetadata[]> => {
const directoryPath = path.resolve(cwd, directory); const directoryPath = path.resolve(cwd, directory);
const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath); const allFilesInMigrationDirectory = await arrayFromAsync(tryReadDirectory(directoryPath));
const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory return allFilesInMigrationDirectory.sort().map((name) => {
.filter((name) => !name.startsWith('.') && !name.startsWith('_') && path.extname(name) !== '') const filePath = path.join(directoryPath, name);
.sort()
.map((name) => {
const filePath = path.join(directoryPath, name);
return { return {
name, name,
filePath, filePath,
relativeFilePath: path.relative(cwd, filePath), relativeFilePath: path.relative(cwd, filePath),
extension: withLeadingPeriod(path.extname(name)), extension: withLeadingPeriod(path.extname(name)),
directory, directory,
cwd, cwd,
}; } satisfies MigrationMetadata;
}); });
return migrationFiles;
}; };