diff --git a/lib/adapters/fileMigrationHistoryLog/acquireLock.ts b/lib/adapters/fileMigrationHistoryLog/acquireLock.ts new file mode 100644 index 0000000..76181c7 --- /dev/null +++ b/lib/adapters/fileMigrationHistoryLog/acquireLock.ts @@ -0,0 +1,110 @@ +import * as Either from 'fp-ts/Either'; +import { pipe } from 'fp-ts/lib/function'; +import * as TaskEither from 'fp-ts/TaskEither'; +import { lock } from 'proper-lockfile'; +import { isValidationErrorLike } from 'zod-validation-error'; + +import { + AcquireLockError, + InvalidMigrationHistoryLogError, + MigrationHistoryLogWriteError, + ReleaseLockError, +} from '../../errors'; +import { History } from '../../models'; +import type { MigrationHistoryLog } from '../../ports'; +import { FileSystemWriteError, writeFile } from '../../utils/fs'; +import { makeReadHistory } from './readHistory'; +import type { FileMigrationHistoryLogContext } from './types'; +import { toMigrationHistoryLogWriteError } from './utils/toMigrationHistoryLogWriteError'; + +export function makeAcquireLock( + ctx: FileMigrationHistoryLogContext +): MigrationHistoryLog['acquireLock'] { + const { filePath } = ctx; + + return function acquireLock() { + return pipe( + TaskEither.tryCatch( + () => lock(filePath.toString()), + (err) => + new AcquireLockError( + `Unable to acquire lock on "${filePath}"`, + err instanceof Error ? { cause: err } : undefined + ) + ), + TaskEither.bindTo('releaseLockAsync'), + TaskEither.bindW('defaultValue', makeReadHistory(ctx)), + TaskEither.map(({ defaultValue, releaseLockAsync }) => { + let value = defaultValue; + let isLockReleased = false; + + return { + // use getter to prevent mutation + get currentValue() { + return value; + }, + persistHistory: (history) => + pipe( + // ensure lock has not been released + isLockReleased, + Either.fromPredicate( + (flag) => flag === false, + () => + new MigrationHistoryLogWriteError( + `Unable to write to migration history-log; lock has been released` + ) + ), + // update history on disk + Either.flatMap(() => History.serialize(history)), + TaskEither.fromEither, + TaskEither.flatMap((contents) => writeFile(filePath, contents)), + // handle errors + TaskEither.mapLeft((err) => { + if (isValidationErrorLike(err)) { + return new InvalidMigrationHistoryLogError(err.message, { + cause: err, + }); + } + + if (err instanceof FileSystemWriteError) { + return toMigrationHistoryLogWriteError(err); + } + + return err; + }), + // update value with new history + TaskEither.tap(() => { + value = history; + return TaskEither.of(void 0); + }) + ), + releaseLock: () => + pipe( + // ensure lock has not been released + isLockReleased, + TaskEither.fromPredicate( + (flag) => flag === false, + () => new ReleaseLockError(`Lock has already been released`) + ), + // release lock + TaskEither.flatMap(() => + TaskEither.tryCatch( + () => releaseLockAsync(), + (err) => + new ReleaseLockError( + `Unable to release lock on "${filePath}"`, + err instanceof Error ? { cause: err } : undefined + ) + ) + ), + // set flag to true + TaskEither.tap(() => { + isLockReleased = true; + return TaskEither.of(undefined); + }) + ), + }; + }) + ); + }; +} diff --git a/lib/adapters/fileMigrationHistoryLog/addExecutedMigration.ts b/lib/adapters/fileMigrationHistoryLog/addExecutedMigration.ts deleted file mode 100644 index 4e8862b..0000000 --- a/lib/adapters/fileMigrationHistoryLog/addExecutedMigration.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as Either from 'fp-ts/Either'; -import { pipe } from 'fp-ts/lib/function'; -import * as TaskEither from 'fp-ts/TaskEither'; - -import { InvalidMigrationHistoryLogError } from '../../errors'; -import { HistoryLog } from '../../models'; -import { MigrationHistoryLog } from '../../ports'; -import { writeFile } from '../../utils/fs'; -import { makeGetExecutedMigrations } from './getExecutedMigrations'; -import type { FileMigrationHistoryLogContext } from './types'; -import { toMigrationHistoryLogWriteError } from './utils/toMigrationHistoryLogWriteError'; - -export function makeAddExecutedMigration( - ctx: FileMigrationHistoryLogContext -): MigrationHistoryLog['addExecutedMigration'] { - const { filePath } = ctx; - - return function addExecutedMigration(executedMigration) { - return pipe( - makeGetExecutedMigrations(ctx)(), - TaskEither.map((historyLog) => { - return HistoryLog.addEntry(historyLog, executedMigration); - }), - TaskEither.flatMapEither((historyLog) => { - return pipe( - HistoryLog.serialize(historyLog), - Either.mapLeft((err) => { - return new InvalidMigrationHistoryLogError(err.message, { - cause: err, - }); - }) - ); - }), - TaskEither.flatMap((contents) => - pipe( - writeFile(filePath, contents), - TaskEither.mapLeft(toMigrationHistoryLogWriteError) - ) - ) - ); - }; -} diff --git a/lib/adapters/fileMigrationHistoryLog/index.ts b/lib/adapters/fileMigrationHistoryLog/index.ts index e879dbb..41a6234 100644 --- a/lib/adapters/fileMigrationHistoryLog/index.ts +++ b/lib/adapters/fileMigrationHistoryLog/index.ts @@ -1,7 +1,7 @@ import type { MigrationHistoryLog } from '../../ports'; -import { makeAddExecutedMigration } from './addExecutedMigration'; -import { makeGetExecutedMigrations } from './getExecutedMigrations'; +import { makeAcquireLock } from './acquireLock'; import { makeInit } from './init'; +import { makeReadHistory } from './readHistory'; import type { FileMigrationHistoryLogContext } from './types'; export const fileMigrationHistoryLog: { @@ -10,6 +10,6 @@ export const fileMigrationHistoryLog: { ) => MigrationHistoryLog[K]; } = { init: makeInit, - getExecutedMigrations: makeGetExecutedMigrations, - addExecutedMigration: makeAddExecutedMigration, + readHistory: makeReadHistory, + acquireLock: makeAcquireLock, }; diff --git a/lib/adapters/fileMigrationHistoryLog/init.test.ts b/lib/adapters/fileMigrationHistoryLog/init.test.ts index 2ecc806..040bdba 100644 --- a/lib/adapters/fileMigrationHistoryLog/init.test.ts +++ b/lib/adapters/fileMigrationHistoryLog/init.test.ts @@ -1,4 +1,3 @@ -import { mkdir, rm } from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -10,7 +9,7 @@ import { } from 'jest-fp-ts-matchers'; import { MigrationHistoryLogWriteError } from '../../errors'; -import { stat, writeFile } from '../../utils/fs'; +import { mkdir, rm, stat, writeFile } from '../../utils/fs'; import { makeInit } from './init'; import type { FileMigrationHistoryLogContext } from './types'; @@ -32,7 +31,7 @@ describe('init()', () => { TaskEither.fromTask ), (resource) => TaskEither.right(resource), - () => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true })) + () => rm(ctx.filePath, { recursive: true }) )(); }); @@ -53,7 +52,7 @@ describe('init()', () => { TaskEither.fromTask ), (resource) => TaskEither.right(resource), - () => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true })) + () => rm(ctx.filePath, { recursive: true }) )(); }); @@ -64,7 +63,7 @@ describe('init()', () => { return TaskEither.bracket( pipe( - TaskEither.fromTask(() => mkdir(ctx.filePath, { recursive: true })), + mkdir(ctx.filePath, { recursive: true }), TaskEither.flatMap(makeInit(ctx)), expectLeftTaskEither((err) => { expect(err).toBeInstanceOf(MigrationHistoryLogWriteError); @@ -75,7 +74,7 @@ describe('init()', () => { TaskEither.fromTask ), (resource) => TaskEither.right(resource), - () => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true })) + () => rm(ctx.filePath, { recursive: true }) )(); }); }); diff --git a/lib/adapters/fileMigrationHistoryLog/init.ts b/lib/adapters/fileMigrationHistoryLog/init.ts index fc790d3..0570588 100644 --- a/lib/adapters/fileMigrationHistoryLog/init.ts +++ b/lib/adapters/fileMigrationHistoryLog/init.ts @@ -6,7 +6,7 @@ import { MigrationHistoryLogReadError, MigrationHistoryLogWriteError, } from '../../errors'; -import { HistoryLog } from '../../models'; +import { History } from '../../models'; import type { MigrationHistoryLog } from '../../ports'; import { FileOrDirectoryNotFoundError, @@ -50,8 +50,8 @@ export function makeInit( if (err instanceof FileOrDirectoryNotFoundError) { // initialize history-log return pipe( - HistoryLog.emptyHistoryLog, - HistoryLog.serialize, + History.emptyHistoryLog, + History.serialize, TaskEither.fromEither, TaskEither.flatMap((content) => writeFile(filePath, content)), TaskEither.mapLeft((err) => { diff --git a/lib/adapters/fileMigrationHistoryLog/mock/history-log.json b/lib/adapters/fileMigrationHistoryLog/mock/history-log.json index 441ba8b..2111cf1 100644 --- a/lib/adapters/fileMigrationHistoryLog/mock/history-log.json +++ b/lib/adapters/fileMigrationHistoryLog/mock/history-log.json @@ -1,12 +1,14 @@ -[ - { - "checksum": "f524ee4ad943b8312921906d9ef52b1b", - "id": "20240101-one", - "executedAt": "2024-01-01T00:00:00Z" - }, - { - "checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a", - "id": "20240103-two", - "executedAt": "2024-01-03T09:54:32Z" - } -] +{ + "entries": [ + { + "checksum": "f524ee4ad943b8312921906d9ef52b1b", + "id": "20240101-one", + "executedAt": "2024-01-01T00:00:00Z" + }, + { + "checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a", + "id": "20240103-two", + "executedAt": "2024-01-03T09:54:32Z" + } + ] +} diff --git a/lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.test.ts b/lib/adapters/fileMigrationHistoryLog/readHistory.test.ts similarity index 59% rename from lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.test.ts rename to lib/adapters/fileMigrationHistoryLog/readHistory.test.ts index e8e4cdb..a69b2b2 100644 --- a/lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.test.ts +++ b/lib/adapters/fileMigrationHistoryLog/readHistory.test.ts @@ -7,34 +7,36 @@ import { } from 'jest-fp-ts-matchers'; import { InvalidMigrationHistoryLogError } from '../../errors'; -import { makeGetExecutedMigrations } from './getExecutedMigrations'; +import { makeReadHistory } from './readHistory'; import type { FileMigrationHistoryLogContext } from './types'; -describe('getExecutedMigrations()', () => { - it('retrieves executed migrations from disk', async () => { +describe('readHistory()', () => { + it('retrieves history log from disk', async () => { const ctx: FileMigrationHistoryLogContext = { filePath: path.resolve(__dirname, './mock/history-log.json'), }; - const getExecutedMigrations = makeGetExecutedMigrations(ctx); + const readHistory = makeReadHistory(ctx); return pipe( - getExecutedMigrations(), - expectRightTaskEither((logEntries) => { - expect(logEntries).toHaveLength(2); - expect(logEntries).toMatchInlineSnapshot(` - [ - { - "checksum": "f524ee4ad943b8312921906d9ef52b1b", - "executedAt": 2024-01-01T00:00:00.000Z, - "id": "20240101-one", - }, - { - "checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a", - "executedAt": 2024-01-03T09:54:32.000Z, - "id": "20240103-two", - }, - ] + readHistory(), + expectRightTaskEither((history) => { + expect(history.entries).toHaveLength(2); + expect(history).toMatchInlineSnapshot(` + { + "entries": [ + { + "checksum": "f524ee4ad943b8312921906d9ef52b1b", + "executedAt": 2024-01-01T00:00:00.000Z, + "id": "20240101-one", + }, + { + "checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a", + "executedAt": 2024-01-03T09:54:32.000Z, + "id": "20240103-two", + }, + ], + } `); }) )(); @@ -45,10 +47,10 @@ describe('getExecutedMigrations()', () => { filePath: path.resolve(__dirname, './mock/invalid-history-log-1.json'), }; - const getExecutedMigrations = makeGetExecutedMigrations(ctx); + const readHistory = makeReadHistory(ctx); return pipe( - getExecutedMigrations(), + readHistory(), expectLeftTaskEither((err) => { expect(err).toBeInstanceOf(InvalidMigrationHistoryLogError); expect(err.message).toMatchInlineSnapshot( @@ -63,10 +65,10 @@ describe('getExecutedMigrations()', () => { filePath: path.resolve(__dirname, './mock/invalid-history-log-2.json'), }; - const getExecutedMigrations = makeGetExecutedMigrations(ctx); + const readHistory = makeReadHistory(ctx); return pipe( - getExecutedMigrations(), + readHistory(), expectLeftTaskEither((err) => { expect(err).toBeInstanceOf(InvalidMigrationHistoryLogError); expect(err.message).toMatchInlineSnapshot( diff --git a/lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.ts b/lib/adapters/fileMigrationHistoryLog/readHistory.ts similarity index 84% rename from lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.ts rename to lib/adapters/fileMigrationHistoryLog/readHistory.ts index 20522a0..2a20249 100644 --- a/lib/adapters/fileMigrationHistoryLog/getExecutedMigrations.ts +++ b/lib/adapters/fileMigrationHistoryLog/readHistory.ts @@ -6,7 +6,7 @@ import { MigrationHistoryLogNotFoundError, MigrationHistoryLogReadError, } from '../../errors'; -import { HistoryLog } from '../../models'; +import { History } from '../../models'; import { MigrationHistoryLog } from '../../ports'; import { FileOrDirectoryNotFoundError, @@ -16,14 +16,14 @@ import { import type { FileMigrationHistoryLogContext } from './types'; import { toInvalidMigrationHistoryLogError } from './utils/toInvalidMigrationHistoryLogError'; -export function makeGetExecutedMigrations( +export function makeReadHistory( ctx: FileMigrationHistoryLogContext -): MigrationHistoryLog['getExecutedMigrations'] { +): MigrationHistoryLog['readHistory'] { const { filePath } = ctx; - return function getExecutedMigrations() { + return function readHistory() { return pipe( - readFile(filePath), + readFile(filePath, { encoding: 'utf8' }), TaskEither.mapLeft((err) => { if (err instanceof FileOrDirectoryNotFoundError) { return new MigrationHistoryLogNotFoundError( @@ -43,7 +43,7 @@ export function makeGetExecutedMigrations( }), TaskEither.flatMapEither((contents) => { return pipe( - HistoryLog.deserialize(contents), + History.deserialize(contents), Either.mapLeft(toInvalidMigrationHistoryLogError) ); }) diff --git a/lib/adapters/fileMigrationHistoryLog/types.ts b/lib/adapters/fileMigrationHistoryLog/types.ts index 355d433..57cfa74 100644 --- a/lib/adapters/fileMigrationHistoryLog/types.ts +++ b/lib/adapters/fileMigrationHistoryLog/types.ts @@ -1,3 +1,5 @@ +import type { PathLike } from 'fs'; + export type FileMigrationHistoryLogContext = { - filePath: string; + filePath: PathLike; }; diff --git a/lib/adapters/fileMigrationRepo/createMigrationFromTemplate.test.ts b/lib/adapters/fileMigrationRepo/createEmptyMigration.test.ts similarity index 69% rename from lib/adapters/fileMigrationRepo/createMigrationFromTemplate.test.ts rename to lib/adapters/fileMigrationRepo/createEmptyMigration.test.ts index 4ca4b39..9c429a8 100644 --- a/lib/adapters/fileMigrationRepo/createMigrationFromTemplate.test.ts +++ b/lib/adapters/fileMigrationRepo/createEmptyMigration.test.ts @@ -1,4 +1,3 @@ -import { readdir, rm } from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -7,12 +6,12 @@ import * as TaskEither from 'fp-ts/TaskEither'; import { expectRightTaskEither } from 'jest-fp-ts-matchers'; import { MigrationId } from '../../models'; -import { makeCreateMigrationFromTemplate } from './createMigrationFromTemplate'; -import { makeGetMigrationTemplate } from './getMigrationTemplate'; +import { readdir, rm } from '../../utils/fs'; +import { makeCreateEmptyMigration } from './createEmptyMigration'; import { makeInit } from './init'; import type { FileMigrationRepoContext } from './types'; -describe('createMigrationFromTemplate()', () => { +describe('createEmptyMigration()', () => { it('creates migration on disk; in ts format', async () => { const ctx: FileMigrationRepoContext = { dirPath: path.join(os.tmpdir(), '/migrations-test-dir'), @@ -20,7 +19,7 @@ describe('createMigrationFromTemplate()', () => { }; const migrationIdStr = '20240103-foobar'; - const createMigrationFromTemplate = makeCreateMigrationFromTemplate(ctx); + const createEmptyMigration = makeCreateEmptyMigration(ctx); return TaskEither.bracket( pipe( @@ -29,19 +28,15 @@ describe('createMigrationFromTemplate()', () => { 'migrationId', pipe(MigrationId.parse(migrationIdStr), TaskEither.fromEither) ), - TaskEither.bindW('template', makeGetMigrationTemplate(ctx)), TaskEither.bindW('init', makeInit(ctx)), - TaskEither.bindW('response', ({ migrationId, template }) => - createMigrationFromTemplate(migrationId, template) + TaskEither.bindW('response', ({ migrationId }) => + createEmptyMigration(migrationId) ), TaskEither.bindW('dirents', () => - TaskEither.fromTask(() => - readdir(ctx.dirPath, { - encoding: 'utf8', - recursive: true, - withFileTypes: true, - }) - ) + readdir(ctx.dirPath, { + encoding: 'utf8', + recursive: true, + }) ), expectRightTaskEither(({ response, dirents }) => { expect(response).toBeUndefined(); @@ -66,7 +61,7 @@ describe('createMigrationFromTemplate()', () => { TaskEither.fromTask ), (resource) => TaskEither.right(resource), - () => TaskEither.fromTask(() => rm(ctx.dirPath, { recursive: true })) + () => rm(ctx.dirPath, { recursive: true }) )(); }); @@ -77,7 +72,7 @@ describe('createMigrationFromTemplate()', () => { }; const migrationIdStr = '20240103-foobar'; - const createMigrationFromTemplate = makeCreateMigrationFromTemplate(ctx); + const createEmptyMigration = makeCreateEmptyMigration(ctx); return TaskEither.bracket( pipe( @@ -86,19 +81,15 @@ describe('createMigrationFromTemplate()', () => { 'migrationId', pipe(MigrationId.parse(migrationIdStr), TaskEither.fromEither) ), - TaskEither.bindW('template', makeGetMigrationTemplate(ctx)), TaskEither.bindW('init', makeInit(ctx)), - TaskEither.bindW('response', ({ migrationId, template }) => - createMigrationFromTemplate(migrationId, template) + TaskEither.bindW('response', ({ migrationId }) => + createEmptyMigration(migrationId) ), TaskEither.bindW('dirents', () => - TaskEither.fromTask(() => - readdir(ctx.dirPath, { - encoding: 'utf8', - recursive: true, - withFileTypes: true, - }) - ) + readdir(ctx.dirPath, { + encoding: 'utf8', + recursive: true, + }) ), expectRightTaskEither(({ response, dirents }) => { expect(response).toBeUndefined(); @@ -123,7 +114,7 @@ describe('createMigrationFromTemplate()', () => { TaskEither.fromTask ), (resource) => TaskEither.right(resource), - () => TaskEither.fromTask(() => rm(ctx.dirPath, { recursive: true })) + () => rm(ctx.dirPath, { recursive: true }) )(); }); }); diff --git a/lib/adapters/fileMigrationRepo/createEmptyMigration.ts b/lib/adapters/fileMigrationRepo/createEmptyMigration.ts new file mode 100644 index 0000000..d2a3221 --- /dev/null +++ b/lib/adapters/fileMigrationRepo/createEmptyMigration.ts @@ -0,0 +1,64 @@ +// import { mkdir, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; + +import { constUndefined, pipe } from 'fp-ts/lib/function'; +import * as TaskEither from 'fp-ts/TaskEither'; + +import type { MigrationRepo } from '../../ports'; +import { FileSystemWriteError, mkdir, rmdir, writeFile } from '../../utils/fs'; +import { makeGetMigrationTemplate } from './getMigrationTemplate'; +import type { FileMigrationRepoContext } from './types'; +import { getLanguageExtension } from './utils/getLanguageExtension'; +import { toMigrationRepoWriteError } from './utils/toMigrationRepoWriteError'; + +export function makeCreateEmptyMigration( + ctx: FileMigrationRepoContext +): MigrationRepo['createEmptyMigration'] { + const { dirPath, language } = ctx; + const ext = getLanguageExtension(language); + const getMigrationTemplate = makeGetMigrationTemplate(ctx); + + return function createEmptyMigration(migrationId) { + return TaskEither.bracketW( + getMigrationTemplate(), + (template) => + pipe( + mkdir(path.join(dirPath, migrationId), { recursive: true }), + TaskEither.flatMap(() => + writeFile(path.join(dirPath, migrationId, `up.${ext}`), template.up) + ), + TaskEither.flatMap(() => + writeFile( + path.join(dirPath, migrationId, `down.${ext}`), + template.down + ) + ), + TaskEither.mapLeft((err) => { + if (err instanceof FileSystemWriteError) { + return toMigrationRepoWriteError(err); + } + + return err; + }), + TaskEither.map(constUndefined) + ), + (template, result) => + pipe( + result, + TaskEither.fromEither, + TaskEither.orElse(() => + pipe( + rmdir(path.join(dirPath, migrationId), { recursive: true }), + TaskEither.mapLeft((err) => { + if (err instanceof FileSystemWriteError) { + return toMigrationRepoWriteError(err); + } + + return err; + }) + ) + ) + ) + ); + }; +} diff --git a/lib/adapters/fileMigrationRepo/createMigrationFromTemplate.ts b/lib/adapters/fileMigrationRepo/createMigrationFromTemplate.ts deleted file mode 100644 index fa90b09..0000000 --- a/lib/adapters/fileMigrationRepo/createMigrationFromTemplate.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import * as path from 'node:path'; - -import { constUndefined, pipe } from 'fp-ts/lib/function'; -import * as TaskEither from 'fp-ts/TaskEither'; - -import type { MigrationRepo } from '../../ports'; -import type { FileMigrationRepoContext } from './types'; -import { getLanguageExtension } from './utils/getLanguageExtension'; -import { toMigrationRepoWriteError } from './utils/toMigrationRepoWriteError'; - -export function makeCreateMigrationFromTemplate( - ctx: FileMigrationRepoContext -): MigrationRepo['createMigrationFromTemplate'] { - const { dirPath, language } = ctx; - const ext = getLanguageExtension(language); - - return function createMigrationFromTemplate(migrationId, template) { - return pipe( - TaskEither.tryCatch( - () => mkdir(path.join(dirPath, migrationId), { recursive: true }), - toMigrationRepoWriteError - ), - TaskEither.flatMap(() => - TaskEither.tryCatch( - () => - writeFile( - path.join(dirPath, migrationId, `up.${ext}`), - template.up, - { - encoding: 'utf8', - } - ), - toMigrationRepoWriteError - ) - ), - TaskEither.flatMap(() => - TaskEither.tryCatch( - () => - writeFile( - path.join(dirPath, migrationId, `down.${ext}`), - template.down, - { - encoding: 'utf8', - } - ), - toMigrationRepoWriteError - ) - ), - TaskEither.map(constUndefined) - ); - }; -} diff --git a/lib/adapters/fileMigrationRepo/getMigrationTemplate.ts b/lib/adapters/fileMigrationRepo/getMigrationTemplate.ts index a03d746..5575f28 100644 --- a/lib/adapters/fileMigrationRepo/getMigrationTemplate.ts +++ b/lib/adapters/fileMigrationRepo/getMigrationTemplate.ts @@ -4,11 +4,7 @@ import { pipe } from 'fp-ts/lib/function'; import * as Record from 'fp-ts/Record'; import * as TaskEither from 'fp-ts/TaskEither'; -import { - MigrationRepoReadError, - MigrationTemplateNotFoundError, -} from '../../errors'; -import type { MigrationRepo } from '../../ports'; +import { MigrationRepoReadError } from '../../errors'; import { FileOrDirectoryNotFoundError, FileSystemReadError, @@ -17,15 +13,16 @@ import { import type { FileMigrationRepoContext } from './types'; import { getLanguageExtension } from './utils/getLanguageExtension'; -export function makeGetMigrationTemplate( - ctx: FileMigrationRepoContext -): MigrationRepo['getMigrationTemplate'] { +export function makeGetMigrationTemplate(ctx: FileMigrationRepoContext) { let cache: { up: string; down: string; } | null = null; - return function getMigrationTemplate() { + return function getMigrationTemplate(): TaskEither.TaskEither< + MigrationRepoReadError, + Record<'up' | 'down', string> + > { // performance optimization: memoize migration template if (cache != null) { return TaskEither.of(cache); @@ -40,10 +37,10 @@ export function makeGetMigrationTemplate( }, Record.map((filepath) => pipe( - readFile(filepath), + readFile(filepath, { encoding: 'utf8' }), TaskEither.mapLeft((err) => { if (err instanceof FileOrDirectoryNotFoundError) { - return new MigrationTemplateNotFoundError( + return new MigrationRepoReadError( `Migration template not found; unable to read "${filepath}"`, { cause: err } ); diff --git a/lib/adapters/fileMigrationRepo/index.ts b/lib/adapters/fileMigrationRepo/index.ts index e78b9a1..84dbeb1 100644 --- a/lib/adapters/fileMigrationRepo/index.ts +++ b/lib/adapters/fileMigrationRepo/index.ts @@ -1,7 +1,6 @@ import type { MigrationRepo } from '../../ports'; -import { makeCreateMigrationFromTemplate } from './createMigrationFromTemplate'; +import { makeCreateEmptyMigration } from './createEmptyMigration'; import { makeDeleteMigration } from './deleteMigration'; -import { makeGetMigrationTemplate } from './getMigrationTemplate'; import { makeInit } from './init'; import { makeListMigrations } from './listMigrations'; import { makeReadMigration } from './readMigration'; @@ -13,8 +12,7 @@ export const fileMigrationRepo: { ) => MigrationRepo[K]; } = { init: makeInit, - getMigrationTemplate: makeGetMigrationTemplate, - createMigrationFromTemplate: makeCreateMigrationFromTemplate, + createEmptyMigration: makeCreateEmptyMigration, listMigrations: makeListMigrations, deleteMigration: makeDeleteMigration, readMigration: makeReadMigration, diff --git a/lib/adapters/fileMigrationRepo/init.ts b/lib/adapters/fileMigrationRepo/init.ts index 6654f7d..5277f6c 100644 --- a/lib/adapters/fileMigrationRepo/init.ts +++ b/lib/adapters/fileMigrationRepo/init.ts @@ -42,7 +42,7 @@ export function makeInit(ctx: FileMigrationRepoContext): MigrationRepo['init'] { TaskEither.orElseW((err) => { if (err instanceof FileOrDirectoryNotFoundError) { return pipe( - mkdir(dirPath), + mkdir(dirPath, { recursive: true }), TaskEither.mapLeft((err) => { if (err instanceof FileSystemWriteError) { return new MigrationRepoWriteError( diff --git a/lib/adapters/fileMigrationRepo/listMigrations.ts b/lib/adapters/fileMigrationRepo/listMigrations.ts index 418383f..eb16bcb 100644 --- a/lib/adapters/fileMigrationRepo/listMigrations.ts +++ b/lib/adapters/fileMigrationRepo/listMigrations.ts @@ -25,7 +25,10 @@ export function makeListMigrations( ): MigrationRepo['listMigrations'] { return function getMigrations() { return pipe( - readdir(ctx.dirPath), + readdir(ctx.dirPath, { + encoding: 'utf8', + recursive: false, + }), // ensure migration repo exists TaskEither.mapLeft((err) => { if (err instanceof FileOrDirectoryNotFoundError) { diff --git a/lib/commands/collectMigrationState.ts b/lib/commands/collectMigrationState.ts index 111616a..7108c7f 100644 --- a/lib/commands/collectMigrationState.ts +++ b/lib/commands/collectMigrationState.ts @@ -15,7 +15,7 @@ import { MigrationHistoryLog, MigrationRepo } from '../ports'; export type CollectMigrationStateDeps = { listMigrations: MigrationRepo['listMigrations']; - getExecutedMigrations: MigrationHistoryLog['getExecutedMigrations']; + readHistory: MigrationHistoryLog['readHistory']; }; export function collectMigrationState(): ReaderTaskEither.ReaderTaskEither< @@ -30,26 +30,24 @@ export function collectMigrationState(): ReaderTaskEither.ReaderTaskEither< > { return pipe( ReaderTaskEither.ask(), - ReaderTaskEither.chainTaskEitherK( - ({ listMigrations, getExecutedMigrations }) => { - return pipe( - TaskEither.Do, - TaskEither.bind('migrations', () => - pipe( - listMigrations(), - TaskEither.map((migrations) => - migrations.toSorted((migration, otherMigration) => - migration.id.localeCompare(otherMigration.id) - ) + ReaderTaskEither.chainTaskEitherK(({ listMigrations, readHistory }) => { + return pipe( + TaskEither.Do, + TaskEither.bind('migrations', () => + pipe( + listMigrations(), + TaskEither.map((migrations) => + migrations.toSorted((migration, otherMigration) => + migration.id.localeCompare(otherMigration.id) ) ) - ), - TaskEither.bindW('historyLogEntries', () => getExecutedMigrations()), - TaskEither.flatMapEither(({ migrations, historyLogEntries }) => - MigrationState.create(migrations, historyLogEntries) ) - ); - } - ) + ), + TaskEither.bindW('history', () => readHistory()), + TaskEither.flatMapEither(({ migrations, history }) => + MigrationState.create(migrations, history) + ) + ); + }) ); } diff --git a/lib/commands/createEmptyMigration.ts b/lib/commands/createEmptyMigration.ts index 816efec..456a6c5 100644 --- a/lib/commands/createEmptyMigration.ts +++ b/lib/commands/createEmptyMigration.ts @@ -10,11 +10,7 @@ import { ValidationError, } from 'zod-validation-error'; -import { - MigrationRepoReadError, - MigrationRepoWriteError, - MigrationTemplateNotFoundError, -} from '../errors'; +import { MigrationRepoReadError, MigrationRepoWriteError } from '../errors'; import { MigrationId } from '../models'; import { MigrationRepo } from '../ports'; import { formatDateInUTC } from '../utils/date'; @@ -38,50 +34,41 @@ export function parseCreateEmptyMigrationInputProps( } export type CreateEmptyMigrationDeps = { - createMigrationFromTemplate: MigrationRepo['createMigrationFromTemplate']; - getMigrationTemplate: MigrationRepo['getMigrationTemplate']; + createEmptyMigration: MigrationRepo['createEmptyMigration']; }; export function createEmptyMigration( props: CreateEmptyMigrationInputProps ): ReaderTaskEither.ReaderTaskEither< CreateEmptyMigrationDeps, - | MigrationRepoWriteError - | MigrationTemplateNotFoundError - | MigrationRepoReadError, + MigrationRepoWriteError | MigrationRepoReadError, void > { return pipe( ReaderTaskEither.ask(), - ReaderTaskEither.chainTaskEitherK( - ({ createMigrationFromTemplate, getMigrationTemplate }) => { - return pipe( - Either.Do, - Either.bind('timestamp', () => - pipe(props.date, formatDateInUTC(DATE_FORMAT_PATTERN)) - ), - Either.let('description', () => kebabCase(props.description)), - Either.map(({ timestamp, description }) => - [timestamp, description].join('-') - ), - Either.flatMap(MigrationId.parse), - Either.mapLeft((err) => { - if (isValidationErrorLike(err)) { - return new MigrationRepoWriteError(`Invalid migration ID`, { - cause: err, - }); - } + ReaderTaskEither.chainTaskEitherK(({ createEmptyMigration }) => { + return pipe( + Either.Do, + Either.bind('timestamp', () => + pipe(props.date, formatDateInUTC(DATE_FORMAT_PATTERN)) + ), + Either.let('description', () => kebabCase(props.description)), + Either.map(({ timestamp, description }) => + [timestamp, description].join('-') + ), + Either.flatMap(MigrationId.parse), + Either.mapLeft((err) => { + if (isValidationErrorLike(err)) { + return new MigrationRepoWriteError(`Invalid migration ID`, { + cause: err, + }); + } - return err; - }), - TaskEither.fromEither, - TaskEither.bindTo('migrationId'), - TaskEither.bindW('template', getMigrationTemplate), - TaskEither.flatMap(({ migrationId, template }) => - createMigrationFromTemplate(migrationId, template) - ) - ); - } - ) + return err; + }), + TaskEither.fromEither, + TaskEither.flatMap(createEmptyMigration) + ); + }) ); } diff --git a/lib/commands/migrateUp.ts b/lib/commands/migrateUp.ts index 81e6b8c..ca60e37 100644 --- a/lib/commands/migrateUp.ts +++ b/lib/commands/migrateUp.ts @@ -11,6 +11,7 @@ import { } from 'zod-validation-error'; import { + AcquireLockError, InvalidMigrationHistoryLogError, InvalidMigrationStateError, MigrationHistoryLogNotFoundError, @@ -21,20 +22,17 @@ import { MigrationRepoReadError, MigrationRepoWriteError, MigrationRuntimeError, - MigrationTemplateNotFoundError, + ReleaseLockError, } from '../errors'; import { - HistoryLogEntry, + History, + HistoryEntry, Migration, MigrationId, MigrationState, } from '../models'; import { isPendingMigration } from '../models/MigrationState'; -import { MigrationHistoryLog } from '../ports'; -import { - collectMigrationState, - type CollectMigrationStateDeps, -} from './collectMigrationState'; +import type { MigrationHistoryLog, MigrationRepo } from '../ports'; const schema = zod.object({ migrationId: MigrationId.schema.optional(), @@ -48,8 +46,9 @@ export function parseMigrateUpInputProps( return Either.tryCatch(() => schema.parse(value), toValidationError()); } -export type MigrateUpDeps = CollectMigrationStateDeps & { - addExecutedMigration: MigrationHistoryLog['addExecutedMigration']; +export type MigrateUpDeps = { + listMigrations: MigrationRepo['listMigrations']; + acquireLock: MigrationHistoryLog['acquireLock']; }; export function migrateUp( @@ -66,18 +65,36 @@ export function migrateUp( | MigrationRepoReadError | MigrationRepoWriteError | MigrationRuntimeError - | MigrationTemplateNotFoundError, + | AcquireLockError + | ReleaseLockError, Array > { return pipe( - collectMigrationState(), - ReaderTaskEither.flatMapEither((migrationState) => - calculateMigrationsToApply(migrationState, props.migrationId) - ), - ReaderTaskEither.flatMap((migrationsToApply) => { + ReaderTaskEither.ask(), + ReaderTaskEither.chainTaskEitherK(({ listMigrations, acquireLock }) => { return pipe( - ReaderTaskEither.ask(), - ReaderTaskEither.chainTaskEitherK(({ addExecutedMigration }) => { + TaskEither.Do, + TaskEither.bind('migrations', () => + pipe( + listMigrations(), + TaskEither.map((migrations) => + migrations.toSorted((migration, otherMigration) => + migration.id.localeCompare(otherMigration.id) + ) + ) + ) + ), + TaskEither.bindW('historyLock', () => acquireLock()), + TaskEither.bindW('migrationsToApply', ({ migrations, historyLock }) => + pipe( + MigrationState.create(migrations, historyLock.currentValue), + Either.flatMap((migrationState) => + calculateMigrationsToApply(migrationState, props.migrationId) + ), + TaskEither.fromEither + ) + ), + TaskEither.flatMap(({ migrationsToApply, historyLock }) => { return pipe( migrationsToApply, ArrayFp.map((migration) => @@ -95,23 +112,30 @@ export function migrateUp( ) ), TaskEither.flatMapEither(() => - HistoryLogEntry.parse({ - id: migration.id, - executedAt: new Date(), - checksum: migration.checksum, - }) - ), - TaskEither.mapLeft((err) => { - if (isValidationErrorLike(err)) { - return new MigrationRuntimeError( - `Invalid migration history log entry for migration "${migration.id}"`, - { cause: err } - ); - } + pipe( + HistoryEntry.parse({ + id: migration.id, + executedAt: new Date(), + checksum: migration.checksum, + }), + Either.mapLeft((err) => { + if (isValidationErrorLike(err)) { + return new MigrationRuntimeError( + `Invalid migration history log entry for migration "${migration.id}"`, + { cause: err } + ); + } - return err; - }), - TaskEither.flatMap(addExecutedMigration), + return err; + }), + Either.map((entry) => + History.addEntry(historyLock.currentValue, entry) + ) + ) + ), + TaskEither.flatMap((nextHistory) => + historyLock.persistHistory(nextHistory) + ), TaskEither.map(() => migration.id) ) ), diff --git a/lib/errors.ts b/lib/errors.ts index 5896c61..923bf0d 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -1,7 +1,3 @@ -export class MigrationNotFoundError extends Error { - name = 'MigrationNotFoundError' as const; -} - export class MigrationRepoReadError extends Error { name = 'MigrationRepoReadError' as const; } @@ -14,8 +10,8 @@ export class MigrationRepoNotFoundError extends Error { name = 'MigrationRepoNotFoundError' as const; } -export class MigrationTemplateNotFoundError extends Error { - name = 'MigrationTemplateNotFoundError' as const; +export class MigrationNotFoundError extends Error { + name = 'MigrationNotFoundError' as const; } export class MigrationHistoryLogNotFoundError extends Error { @@ -34,6 +30,14 @@ export class InvalidMigrationHistoryLogError extends Error { name = 'InvalidMigrationHistoryLogError' as const; } +export class AcquireLockError extends Error { + name = 'AcquireLockError' as const; +} + +export class ReleaseLockError extends Error { + name = 'ReleaseLockError' as const; +} + export class InvalidMigrationStateError extends Error { name = 'InvalidMigrationStateError' as const; } diff --git a/lib/models/HistoryLog.ts b/lib/models/History.ts similarity index 60% rename from lib/models/HistoryLog.ts rename to lib/models/History.ts index 54d2fc4..7a36002 100644 --- a/lib/models/HistoryLog.ts +++ b/lib/models/History.ts @@ -4,29 +4,29 @@ import { pipe } from 'fp-ts/lib/function'; import { z as zod } from 'zod'; import { toValidationError, ValidationError } from 'zod-validation-error'; -import * as HistoryLogEntry from './HistoryLogEntry'; +import * as HistoryLogEntry from './HistoryEntry'; export const schema = zod.object({ entries: zod.array(HistoryLogEntry.schema), }); -export type HistoryLog = zod.infer; +export type History = zod.infer; export function parse( value: zod.input -): Either.Either { +): Either.Either { return Either.tryCatch(() => schema.parse(value), toValidationError()); } export function serialize( - historyLog: HistoryLog + history: History ): Either.Either { - return pipe(Json.stringify(historyLog), Either.mapLeft(toValidationError())); + return pipe(Json.stringify(history), Either.mapLeft(toValidationError())); } export function deserialize( content: string -): Either.Either { +): Either.Either { return pipe( Json.parse(content), Either.mapLeft(toValidationError()), @@ -36,15 +36,15 @@ export function deserialize( } export function addEntry( - historyLog: HistoryLog, - entry: HistoryLogEntry.HistoryLogEntry -): HistoryLog { + history: History, + entry: HistoryLogEntry.HistoryEntry +): History { return { - ...historyLog, - entries: [...historyLog.entries, entry], + ...history, + entries: [...history.entries, entry], }; } -export const emptyHistoryLog: HistoryLog = { +export const emptyHistoryLog: History = { entries: [], }; diff --git a/lib/models/HistoryLogEntry.ts b/lib/models/HistoryEntry.ts similarity index 78% rename from lib/models/HistoryLogEntry.ts rename to lib/models/HistoryEntry.ts index 9bf8766..ea501b7 100644 --- a/lib/models/HistoryLogEntry.ts +++ b/lib/models/HistoryEntry.ts @@ -11,12 +11,12 @@ export const schema = zod executedAt: zod.coerce.date(), checksum: Checksum.schema, }) - .brand<'HistoryLogEntry'>(); + .brand<'HistoryEntry'>(); -export type HistoryLogEntry = zod.infer; +export type HistoryEntry = zod.infer; export function parse( value: zod.input -): Either.Either { +): Either.Either { return Either.tryCatch(() => schema.parse(value), toValidationError()); } diff --git a/lib/models/MigrationState.ts b/lib/models/MigrationState.ts index 468a05e..67250d3 100644 --- a/lib/models/MigrationState.ts +++ b/lib/models/MigrationState.ts @@ -3,13 +3,13 @@ import * as Either from 'fp-ts/Either'; import { pipe } from 'fp-ts/lib/function'; import { InvalidMigrationStateError } from '../errors'; -import * as HistoryLog from './HistoryLog'; -import * as HistoryLogEntry from './HistoryLogEntry'; +import * as History from './History'; +import * as HistoryEntry from './HistoryEntry'; import * as Migration from './Migration'; export type ExecutedMigration = Migration.Migration & { status: 'EXECUTED'; - executedAt: HistoryLogEntry.HistoryLogEntry['executedAt']; + executedAt: HistoryEntry.HistoryEntry['executedAt']; }; export type PendingMigration = Migration.Migration & { @@ -22,9 +22,9 @@ export type MigrationState = Array; export function create( migrations: Array, - historyLog: HistoryLog.HistoryLog + history: History.History ): Either.Either { - if (migrations.length < historyLog.entries.length) { + if (migrations.length < history.entries.length) { return Either.left( new InvalidMigrationStateError( 'Invalid migration state; there atr more executed migrations than migrations' @@ -39,7 +39,7 @@ export function create( index, migration ): Either.Either => { - const historyLogEntry = historyLog.entries[index]; + const historyLogEntry = history.entries[index]; if (historyLogEntry == null) { return Either.of({ diff --git a/lib/models/index.ts b/lib/models/index.ts index c3772af..cb5d7b9 100644 --- a/lib/models/index.ts +++ b/lib/models/index.ts @@ -2,6 +2,6 @@ export * as Checksum from './Checksum'; export * as MigrateFunc from './MigrateFunc'; export * as MigrationId from './MigrationId'; export * as Migration from './Migration'; -export * as HistoryLog from './HistoryLog'; -export * as HistoryLogEntry from './HistoryLogEntry'; +export * as History from './History'; +export * as HistoryEntry from './HistoryEntry'; export * as MigrationState from './MigrationState'; diff --git a/lib/ports/MigrationHistoryLog.ts b/lib/ports/MigrationHistoryLog.ts index 4c0537e..2bc6331 100644 --- a/lib/ports/MigrationHistoryLog.ts +++ b/lib/ports/MigrationHistoryLog.ts @@ -1,31 +1,47 @@ import * as TaskEither from 'fp-ts/TaskEither'; import { + AcquireLockError, InvalidMigrationHistoryLogError, MigrationHistoryLogNotFoundError, MigrationHistoryLogReadError, MigrationHistoryLogWriteError, + ReleaseLockError, } from '../errors'; -import { HistoryLog, HistoryLogEntry } from '../models'; +import { History } from '../models'; export type MigrationHistoryLog = { init: () => TaskEither.TaskEither< MigrationHistoryLogReadError | MigrationHistoryLogWriteError, void >; - getExecutedMigrations: () => TaskEither.TaskEither< + readHistory: () => TaskEither.TaskEither< | MigrationHistoryLogNotFoundError | InvalidMigrationHistoryLogError | MigrationHistoryLogReadError, - HistoryLog.HistoryLog + History.History >; - addExecutedMigration: ( - executedMigration: HistoryLogEntry.HistoryLogEntry - ) => TaskEither.TaskEither< + acquireLock: () => TaskEither.TaskEither< + | AcquireLockError | MigrationHistoryLogNotFoundError | InvalidMigrationHistoryLogError - | MigrationHistoryLogReadError - | MigrationHistoryLogWriteError, - void + | MigrationHistoryLogReadError, + { + currentValue: History.History; + persistHistory: PersistHistoryFunc; + releaseLock: ReleaseLockFunc; + } >; }; + +export type PersistHistoryFunc = ( + history: History.History +) => TaskEither.TaskEither< + InvalidMigrationHistoryLogError | MigrationHistoryLogWriteError, + void +>; + +export type ReleaseLockFunc = () => TaskEither.TaskEither< + ReleaseLockError, + void +>; diff --git a/lib/ports/MigrationRepo.ts b/lib/ports/MigrationRepo.ts index f738178..ac86710 100644 --- a/lib/ports/MigrationRepo.ts +++ b/lib/ports/MigrationRepo.ts @@ -5,7 +5,6 @@ import { MigrationRepoNotFoundError, MigrationRepoReadError, MigrationRepoWriteError, - MigrationTemplateNotFoundError, } from '../errors'; import { Migration, MigrationId } from '../models'; @@ -14,12 +13,9 @@ export type MigrationRepo = { MigrationRepoReadError | MigrationRepoWriteError, void >; - getMigrationTemplate: () => TaskEither.TaskEither< - MigrationTemplateNotFoundError | MigrationRepoReadError, - { - up: string; - down: string; - } + listMigrations: () => TaskEither.TaskEither< + MigrationRepoNotFoundError | MigrationRepoReadError, + Array >; readMigration: ( id: MigrationId.MigrationId @@ -27,18 +23,20 @@ export type MigrationRepo = { MigrationNotFoundError | MigrationRepoReadError, Migration.Migration >; - listMigrations: () => TaskEither.TaskEither< - MigrationRepoNotFoundError | MigrationRepoReadError, - Array + createEmptyMigration: ( + id: MigrationId.MigrationId + ) => TaskEither.TaskEither< + MigrationRepoReadError | MigrationRepoWriteError, + void >; - createMigrationFromTemplate: ( - id: MigrationId.MigrationId, - template: { - up: string; - down: string; - } - ) => TaskEither.TaskEither; deleteMigration: ( id: MigrationId.MigrationId ) => TaskEither.TaskEither; + // getMigrationTemplate: () => TaskEither.TaskEither< + // MigrationTemplateNotFoundError | MigrationRepoReadError, + // { + // up: string; + // down: string; + // } + // >; }; diff --git a/lib/utils/fs/index.ts b/lib/utils/fs/index.ts index e0de4ca..a432a6f 100644 --- a/lib/utils/fs/index.ts +++ b/lib/utils/fs/index.ts @@ -6,3 +6,4 @@ export * from './readFile'; export * from './rm'; export * from './stat'; export * from './writeFile'; +export * from './rmdir'; diff --git a/lib/utils/fs/mkdir.ts b/lib/utils/fs/mkdir.ts index 0a10ed6..392ed1e 100644 --- a/lib/utils/fs/mkdir.ts +++ b/lib/utils/fs/mkdir.ts @@ -1,3 +1,4 @@ +import type { MakeDirectoryOptions, PathLike } from 'node:fs'; import { mkdir as mkdirNative } from 'node:fs/promises'; import { constUndefined, pipe } from 'fp-ts/lib/function'; @@ -7,14 +8,14 @@ import { FileSystemWriteError } from './errors'; import { toFileSystemWriteError } from './toFileSystemWriteError'; export function mkdir( - dirPath: string + dirPath: PathLike, + options: MakeDirectoryOptions & { + recursive?: boolean | undefined; + } ): TaskEither.TaskEither { return pipe( TaskEither.tryCatch( - () => - mkdirNative(dirPath, { - recursive: true, - }), + () => mkdirNative(dirPath, options), toFileSystemWriteError ), TaskEither.map(constUndefined) diff --git a/lib/utils/fs/readFile.ts b/lib/utils/fs/readFile.ts index 861d71b..99407f3 100644 --- a/lib/utils/fs/readFile.ts +++ b/lib/utils/fs/readFile.ts @@ -1,5 +1,7 @@ +import type { PathLike } from 'node:fs'; import { readFile as readFileNative } from 'node:fs/promises'; +import type { TranscodeEncoding } from 'buffer'; import * as TaskEither from 'fp-ts/TaskEither'; import { FileOrDirectoryNotFoundError, FileSystemReadError } from './errors'; @@ -7,13 +9,14 @@ import { isNodeFileSystemError } from './isNodeFileSystemError'; import { toFileSystemReadError } from './toFileSystemReadError'; export function readFile( - filePath: string + filePath: PathLike, + options: { encoding: TranscodeEncoding } ): TaskEither.TaskEither< FileOrDirectoryNotFoundError | FileSystemReadError, string > { return TaskEither.tryCatch( - () => readFileNative(filePath, { encoding: 'utf8' }), + () => readFileNative(filePath, options), (err) => { if (isNodeFileSystemError(err) && err.code === 'ENOENT') { return new FileOrDirectoryNotFoundError( diff --git a/lib/utils/fs/readdir.ts b/lib/utils/fs/readdir.ts index 443e680..a136c64 100644 --- a/lib/utils/fs/readdir.ts +++ b/lib/utils/fs/readdir.ts @@ -1,4 +1,4 @@ -import { type Dirent } from 'node:fs'; +import type { Dirent, ObjectEncodingOptions, PathLike } from 'node:fs'; import { readdir as readdirNative } from 'node:fs/promises'; import * as TaskEither from 'fp-ts/TaskEither'; @@ -8,7 +8,10 @@ import { isNodeFileSystemError } from './isNodeFileSystemError'; import { toFileSystemReadError } from './toFileSystemReadError'; export function readdir( - dirPath: string + dirPath: PathLike, + options: ObjectEncodingOptions & { + recursive?: boolean | undefined; + } ): TaskEither.TaskEither< FileOrDirectoryNotFoundError | FileSystemReadError, Array @@ -16,8 +19,7 @@ export function readdir( return TaskEither.tryCatch( () => readdirNative(dirPath, { - encoding: 'utf8', - recursive: false, + ...options, withFileTypes: true, }), (err) => { diff --git a/lib/utils/fs/rm.ts b/lib/utils/fs/rm.ts index c83483d..be0c55d 100644 --- a/lib/utils/fs/rm.ts +++ b/lib/utils/fs/rm.ts @@ -1,3 +1,4 @@ +import type { PathLike, RmOptions } from 'node:fs'; import { rm as rmNative } from 'node:fs/promises'; import * as TaskEither from 'fp-ts/TaskEither'; @@ -6,10 +7,11 @@ import { FileSystemWriteError } from './errors'; import { toFileSystemWriteError } from './toFileSystemWriteError'; export function rm( - filePath: string + filePath: PathLike, + options?: RmOptions ): TaskEither.TaskEither { return TaskEither.tryCatch( - () => rmNative(filePath, { recursive: true }), + () => rmNative(filePath, options), toFileSystemWriteError ); } diff --git a/lib/utils/fs/rmdir.ts b/lib/utils/fs/rmdir.ts new file mode 100644 index 0000000..40f91d1 --- /dev/null +++ b/lib/utils/fs/rmdir.ts @@ -0,0 +1,17 @@ +import type { PathLike, RmDirOptions } from 'node:fs'; +import { rmdir as rmdirNative } from 'node:fs/promises'; + +import * as TaskEither from 'fp-ts/TaskEither'; + +import { FileSystemWriteError } from './errors'; +import { toFileSystemWriteError } from './toFileSystemWriteError'; + +export function rmdir( + filePath: PathLike, + options?: RmDirOptions | undefined +): TaskEither.TaskEither { + return TaskEither.tryCatch( + () => rmdirNative(filePath, options), + toFileSystemWriteError + ); +} diff --git a/lib/utils/fs/stat.ts b/lib/utils/fs/stat.ts index 2221096..5daf2b4 100644 --- a/lib/utils/fs/stat.ts +++ b/lib/utils/fs/stat.ts @@ -1,4 +1,4 @@ -import { type Stats } from 'node:fs'; +import type { PathLike, Stats } from 'node:fs'; import { stat as statNative } from 'node:fs/promises'; import * as TaskEither from 'fp-ts/TaskEither'; @@ -8,7 +8,7 @@ import { isNodeFileSystemError } from './isNodeFileSystemError'; import { toFileSystemReadError } from './toFileSystemReadError'; export function stat( - dirPath: string + dirPath: PathLike ): TaskEither.TaskEither< FileOrDirectoryNotFoundError | FileSystemReadError, Stats diff --git a/lib/utils/fs/writeFile.ts b/lib/utils/fs/writeFile.ts index 2b6ac6c..caac407 100644 --- a/lib/utils/fs/writeFile.ts +++ b/lib/utils/fs/writeFile.ts @@ -1,3 +1,4 @@ +import type { PathLike } from 'node:fs'; import { writeFile as writeFileNative } from 'node:fs/promises'; import * as TaskEither from 'fp-ts/TaskEither'; @@ -6,7 +7,7 @@ import { FileSystemWriteError } from './errors'; import { toFileSystemWriteError } from './toFileSystemWriteError'; export function writeFile( - filePath: string, + filePath: PathLike, contents: string ): TaskEither.TaskEither { return TaskEither.tryCatch( diff --git a/package-lock.json b/package-lock.json index 4de6110..19f4f13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/jest": "^29.2.4", "@types/lodash": "^4.14.202", "@types/node": "^20.5.0", + "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "concurrently": "^8.2.0", @@ -2669,6 +2670,21 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/proper-lockfile": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", + "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", + "dev": true, + "dependencies": { + "@types/retry": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", diff --git a/package.json b/package.json index 03c6e05..db20e3a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/jest": "^29.2.4", "@types/lodash": "^4.14.202", "@types/node": "^20.5.0", + "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "concurrently": "^8.2.0",