Skip to content

Commit

Permalink
feat: implement migrateUp command
Browse files Browse the repository at this point in the history
  • Loading branch information
jmike committed Jan 7, 2024
1 parent ace3fcf commit 18fe5aa
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 16 deletions.
16 changes: 14 additions & 2 deletions lib/commands/createEmptyMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import * as ReaderTaskEither from 'fp-ts/ReaderTaskEither';
import * as TaskEither from 'fp-ts/TaskEither';
import { kebabCase } from 'lodash/fp';
import { z as zod } from 'zod';
import { toValidationError, ValidationError } from 'zod-validation-error';
import {
isValidationErrorLike,
toValidationError,
ValidationError,
} from 'zod-validation-error';

import {
MigrationRepoReadError,
Expand Down Expand Up @@ -42,7 +46,6 @@ export function createEmptyMigration(
props: CreateEmptyMigrationInputProps
): ReaderTaskEither.ReaderTaskEither<
CreateEmptyMigrationDeps,
| ValidationError
| MigrationRepoWriteError
| MigrationTemplateNotFoundError
| MigrationRepoReadError,
Expand All @@ -62,6 +65,15 @@ export function createEmptyMigration(
[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),
Expand Down
1 change: 1 addition & 0 deletions lib/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './createEmptyMigration';
export * from './collectMigrationState';
export * from './migrateUp';
162 changes: 162 additions & 0 deletions lib/commands/migrateUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as ArrayFp from 'fp-ts/Array';
import * as Either from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import * as ReaderTaskEither from 'fp-ts/ReaderTaskEither';
import * as TaskEither from 'fp-ts/TaskEither';
import { z as zod } from 'zod';
import {
isValidationErrorLike,
toValidationError,
ValidationError,
} from 'zod-validation-error';

import {
InvalidMigrationHistoryLogError,
InvalidMigrationStateError,
MigrationHistoryLogNotFoundError,
MigrationHistoryLogReadError,
MigrationHistoryLogWriteError,
MigrationNotFoundError,
MigrationRepoNotFoundError,
MigrationRepoReadError,
MigrationRepoWriteError,
MigrationRuntimeError,
MigrationTemplateNotFoundError,
} from '../errors';
import {
HistoryLogEntry,
Migration,
MigrationId,
MigrationState,
} from '../models';
import { isPendingMigration } from '../models/MigrationState';
import { MigrationHistoryLog } from '../ports';
import {
collectMigrationState,
type CollectMigrationStateDeps,
} from './collectMigrationState';

const schema = zod.object({
migrationId: MigrationId.schema.optional(),
});

export type MigrateUpInputProps = zod.infer<typeof schema>;

export function parseMigrateUpInputProps(
value: zod.input<typeof schema>
): Either.Either<ValidationError, MigrateUpInputProps> {
return Either.tryCatch(() => schema.parse(value), toValidationError());
}

export type MigrateUpDeps = CollectMigrationStateDeps & {
addExecutedMigration: MigrationHistoryLog['addExecutedMigration'];
};

export function migrateUp(
props: MigrateUpInputProps
): ReaderTaskEither.ReaderTaskEither<
MigrateUpDeps,
| InvalidMigrationHistoryLogError
| InvalidMigrationStateError
| MigrationHistoryLogNotFoundError
| MigrationHistoryLogReadError
| MigrationHistoryLogWriteError
| MigrationNotFoundError
| MigrationRepoNotFoundError
| MigrationRepoReadError
| MigrationRepoWriteError
| MigrationRuntimeError
| MigrationTemplateNotFoundError,
Array<MigrationId.MigrationId>
> {
return pipe(
collectMigrationState(),
ReaderTaskEither.flatMapEither((migrationState) =>
calculateMigrationsToApply(migrationState, props.migrationId)
),
ReaderTaskEither.flatMap((migrationsToApply) => {
return pipe(
ReaderTaskEither.ask<MigrateUpDeps>(),
ReaderTaskEither.chainTaskEitherK(({ addExecutedMigration }) => {
return pipe(
migrationsToApply,
ArrayFp.map((migration) =>
pipe(
TaskEither.tryCatch(
() => migration.up(),
(err) =>
err instanceof Error
? new MigrationRuntimeError(
`Unknown error occurred while applying migration "${migration.id}"`,
{ cause: err }
)
: new MigrationRuntimeError(
`Unknown error occurred while applying migration "${migration.id}"`
)
),
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 }
);
}

return err;
}),
TaskEither.flatMap(addExecutedMigration),
TaskEither.map(() => migration.id)
)
),
ArrayFp.sequence(TaskEither.ApplicativeSeq)
);
})
);
})
);
}

function calculateMigrationsToApply(
migrationState: MigrationState.MigrationState,
targetMigrationId?: MigrationId.MigrationId
): Either.Either<
MigrationNotFoundError | MigrationRuntimeError,
Array<Migration.Migration>
> {
if (targetMigrationId == null) {
return Either.of(migrationState.filter(isPendingMigration));
}

const targetIndex = migrationState.findLastIndex(
(migration) => migration.id === targetMigrationId
);

if (targetIndex === -1) {
return Either.left(
new MigrationNotFoundError(
`Target migration "${targetMigrationId}" not found`
)
);
}

const migrationRecord = migrationState[targetIndex];

if (migrationRecord.status === 'EXECUTED') {
return Either.left(
new MigrationRuntimeError(
`Target migration "${targetMigrationId}" is already applied`
)
);
}

return Either.of(
migrationState.slice(0, targetIndex + 1).filter(isPendingMigration)
);
}
4 changes: 4 additions & 0 deletions lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ export class InvalidMigrationHistoryLogError extends Error {
export class InvalidMigrationStateError extends Error {
name = 'InvalidMigrationStateError' as const;
}

export class MigrationRuntimeError extends Error {
name = 'MigrationRuntimeError' as const;
}
32 changes: 26 additions & 6 deletions lib/models/MigrationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ import { pipe } from 'fp-ts/lib/function';

import { InvalidMigrationStateError } from '../errors';
import * as HistoryLog from './HistoryLog';
import * as HistoryLogEntry from './HistoryLogEntry';
import * as Migration from './Migration';
import * as MigrationStateRecord from './MigrationStateRecord';

export type MigrationState = Array<MigrationStateRecord.MigrationStateRecord>;
export type ExecutedMigration = Migration.Migration & {
status: 'EXECUTED';
executedAt: HistoryLogEntry.HistoryLogEntry['executedAt'];
};

export type PendingMigration = Migration.Migration & {
status: 'PENDING';
executedAt: null;
};

export type MigrationStateRecord = ExecutedMigration | PendingMigration;
export type MigrationState = Array<MigrationStateRecord>;

export function create(
migrations: Array<Migration.Migration>,
Expand All @@ -27,10 +38,7 @@ export function create(
(
index,
migration
): Either.Either<
InvalidMigrationStateError,
MigrationStateRecord.MigrationStateRecord
> => {
): Either.Either<InvalidMigrationStateError, MigrationStateRecord> => {
const historyLogEntry = historyLog.entries[index];

if (historyLogEntry == null) {
Expand Down Expand Up @@ -67,3 +75,15 @@ export function create(
ArrayFp.sequence(Either.Applicative)
);
}

export function isPendingMigration(
record: MigrationStateRecord
): record is PendingMigration {
return record.status === 'PENDING';
}

export function isExecutedMigration(
record: MigrationStateRecord
): record is ExecutedMigration {
return record.status === 'EXECUTED';
}
7 changes: 0 additions & 7 deletions lib/models/MigrationStateRecord.ts

This file was deleted.

1 change: 0 additions & 1 deletion lib/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ export * as Migration from './Migration';
export * as HistoryLog from './HistoryLog';
export * as HistoryLogEntry from './HistoryLogEntry';
export * as MigrationState from './MigrationState';
export * as MigrationStateRecord from './MigrationStateRecord';

0 comments on commit 18fe5aa

Please sign in to comment.