Compare commits
No commits in common. "main" and "@emigrate/reporter-pino@0.5.0" have entirely different histories.
main
...
@emigrate/
93 changed files with 5642 additions and 10263 deletions
7
.github/workflows/ci.yaml
vendored
7
.github/workflows/ci.yaml
vendored
|
|
@ -13,7 +13,6 @@ jobs:
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
DO_NOT_TRACK: 1
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
|
|
@ -21,12 +20,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4.0.0
|
- uses: pnpm/action-setup@v2.4.0
|
||||||
|
with:
|
||||||
|
version: 8.3.1
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.15.0
|
node-version: 20.9.0
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
|
||||||
8
.github/workflows/deploy.yaml
vendored
8
.github/workflows/deploy.yaml
vendored
|
|
@ -10,7 +10,6 @@ on:
|
||||||
|
|
||||||
# Allow this job to clone the repo and create a page deployment
|
# Allow this job to clone the repo and create a page deployment
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
|
||||||
contents: read
|
contents: read
|
||||||
pages: write
|
pages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
@ -30,10 +29,11 @@ jobs:
|
||||||
echo $ASTRO_SITE
|
echo $ASTRO_SITE
|
||||||
echo $ASTRO_BASE
|
echo $ASTRO_BASE
|
||||||
- name: Install, build, and upload your site output
|
- name: Install, build, and upload your site output
|
||||||
uses: withastro/action@v2
|
uses: withastro/action@v1
|
||||||
with:
|
with:
|
||||||
path: ./docs # The root location of your Astro project inside the repository. (optional)
|
path: ./docs # The root location of your Astro project inside the repository. (optional)
|
||||||
package-manager: pnpm@9.4.0 # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
|
node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 18. (optional)
|
||||||
|
package-manager: pnpm@8.10.2 # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
|
|
@ -44,4 +44,4 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v1
|
||||||
|
|
|
||||||
62
.github/workflows/integration.yaml
vendored
62
.github/workflows/integration.yaml
vendored
|
|
@ -1,62 +0,0 @@
|
||||||
name: Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main', 'changeset-release/main']
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mysql_integration:
|
|
||||||
name: Emigrate MySQL integration tests
|
|
||||||
timeout-minutes: 15
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
DO_NOT_TRACK: 1
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
env:
|
|
||||||
MYSQL_ROOT_PASSWORD: root
|
|
||||||
MYSQL_DATABASE: emigrate
|
|
||||||
MYSQL_USER: emigrate
|
|
||||||
MYSQL_PASSWORD: emigrate
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
options: --health-cmd="mysqladmin ping -h localhost" --health-interval=10s --health-timeout=5s --health-retries=5
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4.0.0
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22.15.0
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Wait for MySQL to be ready
|
|
||||||
run: |
|
|
||||||
for i in {1..30}; do
|
|
||||||
nc -z localhost 3306 && echo "MySQL is up!" && break
|
|
||||||
echo "Waiting for MySQL..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: pnpm build --filter @emigrate/mysql
|
|
||||||
|
|
||||||
- name: Integration Tests
|
|
||||||
env:
|
|
||||||
MYSQL_HOST: '127.0.0.1'
|
|
||||||
MYSQL_PORT: 3306
|
|
||||||
run: pnpm --filter @emigrate/mysql integration
|
|
||||||
34
.github/workflows/release.yaml
vendored
34
.github/workflows/release.yaml
vendored
|
|
@ -16,7 +16,6 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
actions: read
|
actions: read
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -25,48 +24,25 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4.0.0
|
- uses: pnpm/action-setup@v2.4.0
|
||||||
|
with:
|
||||||
|
version: 8.3.1
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.15.0
|
node-version: 20.9.0
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Create Release Pull Request
|
- name: Create Release Pull Request
|
||||||
id: changesets
|
uses: changesets/action@v1.4.5
|
||||||
uses: aboviq/changesets-action@v1.5.2
|
|
||||||
with:
|
with:
|
||||||
publish: pnpm run release
|
publish: pnpm run release
|
||||||
commit: 'chore(release): version packages'
|
commit: 'chore(release): version packages'
|
||||||
title: 'chore(release): version packages'
|
title: 'chore(release): version packages'
|
||||||
createGithubReleases: aggregate
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.PAT_GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Release to @next tag on npm
|
|
||||||
if: github.ref_name == 'main' && steps.changesets.outputs.published != 'true'
|
|
||||||
run: |
|
|
||||||
git checkout main
|
|
||||||
|
|
||||||
CHANGESET_FILE=$(git diff-tree --no-commit-id --name-only HEAD -r ".changeset/*-*-*.md")
|
|
||||||
if [ -z "$CHANGESET_FILE" ]; then
|
|
||||||
echo "No changesets found, skipping release to @next tag"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
AFFECTED_PACKAGES=$(sed -n '/---/,/---/p' "$CHANGESET_FILE" | sed '/---/d')
|
|
||||||
if [ -z "$AFFECTED_PACKAGES" ]; then
|
|
||||||
echo "No packages affected by changesets, skipping release to @next tag"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
pnpm changeset version --snapshot next
|
|
||||||
pnpm changeset publish --tag next
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT_GITHUB_TOKEN }}
|
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -49,35 +49,22 @@ Run all pending migrations
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
|
||||||
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
||||||
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
||||||
|
|
||||||
-s, --storage <name> The storage to use for where to store the migration history (required)
|
-s, --storage <name> The storage to use for where to store the migration history (required)
|
||||||
|
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
|
|
||||||
-r, --reporter <name> The reporter to use for reporting the migration progress
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
|
|
||||||
-l, --limit <count> Limit the number of migrations to run
|
-l, --limit <count> Limit the number of migrations to run
|
||||||
|
-f, --from <name> Start running migrations from the given migration name, the given name doesn't need to exist
|
||||||
-f, --from <name/path> Start running migrations from the given migration name or relative file path to a migration file,
|
and is compared in lexicographical order
|
||||||
the given name or path needs to exist. The same migration and those after it lexicographically will be run
|
-t, --to <name> Skip migrations after the given migration name, the given name doesn't need to exist
|
||||||
|
and is compared in lexicographical order
|
||||||
-t, --to <name/path> Skip migrations after the given migration name or relative file path to a migration file,
|
|
||||||
the given name or path needs to exist. The same migration and those before it lexicographically will be run
|
|
||||||
|
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-execution Mark the migrations as executed and successful without actually running them,
|
--no-execution Mark the migrations as executed and successful without actually running them,
|
||||||
which is useful if you want to mark migrations as successful after running them manually
|
which is useful if you want to mark migrations as successful after running them manually
|
||||||
|
|
||||||
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: 10)
|
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: 10)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,5 @@
|
||||||
# @emigrate/docs
|
# @emigrate/docs
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
### Major Changes
|
|
||||||
|
|
||||||
- 1d33d65: Rename the URL path "/commands/" to "/cli/" to make it more clear that those pages are the documentation for the CLI. This change is a BREAKING CHANGE because it changes the URL path of the pages.
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 0c597fd: Add a separate page for the Emigrate CLI itself, with all the commands as sub pages
|
|
||||||
|
|
||||||
## 0.4.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- b62c692: Add documentation for the built-in "json" reporter
|
|
||||||
- b62c692: The "default" reporter is now named "pretty"
|
|
||||||
- e7ec75d: Add note in FAQ on using Emigrate for existing databases
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- c838ffb: Add note on how to write Emigrate's config using TypeScript in a production environment without having `typescript` installed.
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- f6761fe: Document the changes to the "remove" command, specifically that it also accepts relative file paths now
|
|
||||||
- 9109238: Document the changes to the "up" command's `--from` and `--to` options, specifically that they can take relative file paths and that the given migration must exist.
|
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
@ -40,4 +11,4 @@
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
||||||
- cbc35bd: Add first version of the [Baseline guide](https://emigrate.dev/guides/baseline)
|
- cbc35bd: Add first version of the [Baseline guide](https://emigrate.dev/guides/baseline)
|
||||||
- cbc35bd: Document the new --limit, --from and --to options for the ["up" command](https://emigrate.dev/cli/up/)
|
- cbc35bd: Document the new --limit, --from and --to options for the ["up" command](https://emigrate.dev/commands/up/)
|
||||||
|
|
|
||||||
|
|
@ -78,32 +78,23 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Command Line Interface',
|
label: 'Commands',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Introduction',
|
label: 'emigrate up',
|
||||||
link: '/cli/',
|
link: '/commands/up/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Commands',
|
label: 'emigrate list',
|
||||||
items: [
|
link: '/commands/list/',
|
||||||
{
|
},
|
||||||
label: 'emigrate up',
|
{
|
||||||
link: '/cli/up/',
|
label: 'emigrate new',
|
||||||
},
|
link: '/commands/new/',
|
||||||
{
|
},
|
||||||
label: 'emigrate list',
|
{
|
||||||
link: '/cli/list/',
|
label: 'emigrate remove',
|
||||||
},
|
link: '/commands/remove/',
|
||||||
{
|
|
||||||
label: 'emigrate new',
|
|
||||||
link: '/cli/new/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'emigrate remove',
|
|
||||||
link: '/cli/remove/',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -115,7 +106,7 @@ export default defineConfig({
|
||||||
link: '/guides/typescript/',
|
link: '/guides/typescript/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Baseline existing database',
|
label: 'Baseline',
|
||||||
link: '/guides/baseline/',
|
link: '/guides/baseline/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -180,12 +171,8 @@ export default defineConfig({
|
||||||
link: '/plugins/reporters/',
|
link: '/plugins/reporters/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pretty Reporter (default)',
|
label: 'Default Reporter',
|
||||||
link: '/plugins/reporters/pretty/',
|
link: '/plugins/reporters/default/',
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'JSON Reporter',
|
|
||||||
link: '/plugins/reporters/json/',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pino Reporter',
|
label: 'Pino Reporter',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@emigrate/docs",
|
"name": "@emigrate/docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "0.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.7.0",
|
|
||||||
"@astrojs/starlight": "^0.15.0",
|
"@astrojs/starlight": "^0.15.0",
|
||||||
"@astrojs/starlight-tailwind": "2.0.1",
|
"@astrojs/starlight-tailwind": "2.0.1",
|
||||||
"@astrojs/tailwind": "^5.0.3",
|
"@astrojs/tailwind": "^5.0.3",
|
||||||
|
|
@ -21,6 +20,5 @@
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"extends": "../package.json"
|
"extends": "../package.json"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@9.4.0"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
---
|
|
||||||
title: "CLI Introduction"
|
|
||||||
description: "Some basic information about the Emigrate CLI."
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Tabs, TabItem, LinkCard } from '@astrojs/starlight/components';
|
|
||||||
import Link from '@components/Link.astro';
|
|
||||||
|
|
||||||
Emigrate comes with a CLI that you can use to manage your migrations. The CLI is a powerful tool that allows you to create, run, and manage migrations.
|
|
||||||
|
|
||||||
### Installing the Emigrate CLI
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem label="npm">
|
|
||||||
```bash
|
|
||||||
npm install @emigrate/cli
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="pnpm">
|
|
||||||
```bash
|
|
||||||
pnpm add @emigrate/cli
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="yarn">
|
|
||||||
```bash
|
|
||||||
yarn add @emigrate/cli
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="bun">
|
|
||||||
```bash
|
|
||||||
bun add @emigrate/cli
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="deno">
|
|
||||||
```json title="package.json" {3,6}
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"emigrate": "emigrate"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@emigrate/cli": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
### Existing commands
|
|
||||||
|
|
||||||
|
|
||||||
<LinkCard
|
|
||||||
href="up/"
|
|
||||||
title="emigrate up"
|
|
||||||
description="The command for executing migrations, or showing pending migrations in dry run mode."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinkCard
|
|
||||||
href="list/"
|
|
||||||
title="emigrate list"
|
|
||||||
description="The command for listing all migrations and their status."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinkCard
|
|
||||||
href="new/"
|
|
||||||
title="emigrate new"
|
|
||||||
description="The command for creating new migration files."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinkCard
|
|
||||||
href="remove/"
|
|
||||||
title="emigrate remove"
|
|
||||||
description="The command for removing migrations from the migration history."
|
|
||||||
/>
|
|
||||||
|
|
@ -86,9 +86,6 @@ In case you have both a `emigrate-storage-somedb` and a `somedb` package install
|
||||||
|
|
||||||
### `-r`, `--reporter <name>`
|
### `-r`, `--reporter <name>`
|
||||||
|
|
||||||
**type:** `"pretty" | "json" | string`
|
|
||||||
**default:** `"pretty"`
|
|
||||||
|
|
||||||
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
||||||
|
|
||||||
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
||||||
|
|
@ -101,9 +101,6 @@ In case you have both a `emigrate-plugin-someplugin` and a `someplugin` package
|
||||||
|
|
||||||
### `-r`, `--reporter <name>`
|
### `-r`, `--reporter <name>`
|
||||||
|
|
||||||
**type:** `"pretty" | "json" | string`
|
|
||||||
**default:** `"pretty"`
|
|
||||||
|
|
||||||
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
||||||
|
|
||||||
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
||||||
|
|
@ -13,22 +13,22 @@ The `remove` command is used to remove a migration from the history. This is use
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem label="npm">
|
<TabItem label="npm">
|
||||||
```bash
|
```bash
|
||||||
npx emigrate remove [options] <name/path>
|
npx emigrate remove [options] <name>
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem label="pnpm">
|
<TabItem label="pnpm">
|
||||||
```bash
|
```bash
|
||||||
pnpm emigrate remove [options] <name/path>
|
pnpm emigrate remove [options] <name>
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem label="yarn">
|
<TabItem label="yarn">
|
||||||
```bash
|
```bash
|
||||||
yarn emigrate remove [options] <name/path>
|
yarn emigrate remove [options] <name>
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem label="bun">
|
<TabItem label="bun">
|
||||||
```bash
|
```bash
|
||||||
bunx --bun emigrate remove [options] <name/path>
|
bunx --bun emigrate remove [options] <name>
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem label="deno">
|
<TabItem label="deno">
|
||||||
|
|
@ -44,18 +44,16 @@ The `remove` command is used to remove a migration from the history. This is use
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
deno task emigrate remove [options] <name/path>
|
deno task emigrate remove [options] <name>
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
### `<name/path>`
|
### `<name>`
|
||||||
|
|
||||||
The name of the migration file to remove, including the extension, e.g. `20200101000000_some_migration.js`, or a relative file path to a migration file to remove, e.g: `migrations/20200101000000_some_migration.js`.
|
The name of the migration file to remove, including the extension, e.g. `20200101000000_some_migration.js`.
|
||||||
|
|
||||||
Using relative file paths is useful in terminals that support autocomplete, and also when you copy and use the relative migration file path from the output of the <Link href="/cli/list/">`list`</Link> command.
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
|
@ -95,9 +93,6 @@ In case you have both a `emigrate-storage-somedb` and a `somedb` package install
|
||||||
|
|
||||||
### `-r`, `--reporter <name>`
|
### `-r`, `--reporter <name>`
|
||||||
|
|
||||||
**type:** `"pretty" | "json" | string`
|
|
||||||
**default:** `"pretty"`
|
|
||||||
|
|
||||||
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
The <Link href="/plugins/reporters/">reporter</Link> to use for listing the migrations.
|
||||||
|
|
||||||
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
||||||
|
|
@ -75,34 +75,20 @@ and "skipped" for the migrations that also haven't been run but won't because of
|
||||||
|
|
||||||
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
||||||
|
|
||||||
### `-f`, `--from <name/path>`
|
### `-f`, `--from <name>`
|
||||||
|
|
||||||
The name of the migration to start from. This can be used to run only a subset of the pending migrations.
|
The name of the migration to start from. This can be used to run only a subset of the pending migrations.
|
||||||
|
|
||||||
The given migration need to exist and is compared in lexicographical order with all migrations, the migration with the same name and those lexicographically after it will be migrated.
|
The given migration name does not need to exist and is compared in lexicographical order with the migration names, so it can be a prefix of a migration name or similar.
|
||||||
It's okay to use an already executed migration as the "from" migration, it won't be executed again.
|
|
||||||
|
|
||||||
The reason for why the given migration name must exist and cannot be just a prefix is to avoid accidentally running migrations that you didn't intend to run.
|
|
||||||
|
|
||||||
The given name can also be a relative path to a migration file, which makes it easier to use with terminals that support tab completion
|
|
||||||
or when copying the output from Emigrate and using it directly as the value of the `--from` option.
|
|
||||||
Relative paths are resolved relative to the current working directory.
|
|
||||||
|
|
||||||
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
||||||
and "skipped" for the migrations that also haven't been run but won't because of the set "from".
|
and "skipped" for the migrations that also haven't been run but won't because of the set "from".
|
||||||
|
|
||||||
### `-t`, `--to <name/path>`
|
### `-t`, `--to <name>`
|
||||||
|
|
||||||
The name of the migration to end at. This can be used to run only a subset of the pending migrations.
|
The name of the migration to end at. This can be used to run only a subset of the pending migrations.
|
||||||
|
|
||||||
The given migration name need to exist and is compared in lexicographical order with all migrations, the migration with the same name and those lexicographically before it will be migrated.
|
The given migration name does not need to exist and is compared in lexicographical order with the migration names, so it can be a prefix of a migration name or similar.
|
||||||
It's okay to use an already executed migration as the "to" migration, it won't be executed again.
|
|
||||||
|
|
||||||
The reason for why the given migration name must exist and cannot be just a prefix is to avoid accidentally running migrations that you didn't intend to run.
|
|
||||||
|
|
||||||
The given name can also be a relative path to a migration file, which makes it easier to use with terminals that support tab completion
|
|
||||||
or when copying the output from Emigrate and using it directly as the value of the `--to` option.
|
|
||||||
Relative paths are resolved relative to the current working directory.
|
|
||||||
|
|
||||||
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
||||||
and "skipped" for the migrations that also haven't been run but won't because of the set "to".
|
and "skipped" for the migrations that also haven't been run but won't because of the set "to".
|
||||||
|
|
@ -147,9 +133,6 @@ In case you have both a `emigrate-plugin-someplugin` and a `someplugin` package
|
||||||
|
|
||||||
### `-r`, `--reporter <name>`
|
### `-r`, `--reporter <name>`
|
||||||
|
|
||||||
**type:** `"pretty" | "json" | string`
|
|
||||||
**default:** `"pretty"`
|
|
||||||
|
|
||||||
The <Link href="/plugins/reporters/">reporter</Link> to use for reporting the migration progress.
|
The <Link href="/plugins/reporters/">reporter</Link> to use for reporting the migration progress.
|
||||||
|
|
||||||
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
The name can be either a path to a module or a package name. For package names Emigrate will automatically prefix the given name with these prefixes in order:
|
||||||
|
|
@ -113,8 +113,8 @@ CREATE TABLE public.posts (
|
||||||
|
|
||||||
### Log the baseline migration
|
### Log the baseline migration
|
||||||
|
|
||||||
For new environments this baseline migration will automatically be run when you run <Link href="/cli/up/">`emigrate up`</Link>.
|
For new environments this baseline migration will automatically be run when you run <Link href="/commands/up/">`emigrate up`</Link>.
|
||||||
For any existing environments you will need to run `emigrate up` with the <Link href="/cli/up/#--no-execution">`--no-execution`</Link> flag to prevent the migration from being executed and only log the migration:
|
For any existing environments you will need to run `emigrate up` with the <Link href="/commands/up/#--no-execution">`--no-execution`</Link> flag to prevent the migration from being executed and only log the migration:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem label="npm">
|
<TabItem label="npm">
|
||||||
|
|
@ -155,7 +155,7 @@ For any existing environments you will need to run `emigrate up` with the <Link
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
In case you have already added more migration files to your migration directory you can limit the "up" command to just log the baseline migration by specifying the <Link href="/cli/up/#-t---to-name">`--to`</Link> option:
|
In case you have already added more migration files to your migration directory you can limit the "up" command to just log the baseline migration by specifying the <Link href="/commands/up/#-t---to-name">`--to`</Link> option:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem label="npm">
|
<TabItem label="npm">
|
||||||
|
|
@ -198,7 +198,7 @@ In case you have already added more migration files to your migration directory
|
||||||
|
|
||||||
### Verify the baseline migration status
|
### Verify the baseline migration status
|
||||||
|
|
||||||
You can verify the status of the baseline migration by running the <Link href="/cli/list/">`emigrate list`</Link> command:
|
You can verify the status of the baseline migration by running the <Link href="/commands/list/">`emigrate list`</Link> command:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem label="npm">
|
<TabItem label="npm">
|
||||||
|
|
@ -251,5 +251,5 @@ Emigrate list v0.14.1 /your/project/path
|
||||||
|
|
||||||
### Happy migrating!
|
### Happy migrating!
|
||||||
|
|
||||||
You can now start adding new migrations to your migration directory and run <Link href="/cli/up/">`emigrate up`</Link> to apply them to your database.
|
You can now start adding new migrations to your migration directory and run <Link href="/commands/up/">`emigrate up`</Link> to apply them to your database.
|
||||||
Which should be part of your CD pipeline to ensure that your database schema is always up to date in each environment.
|
Which should be part of your CD pipeline to ensure that your database schema is always up to date in each environment.
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
import Link from '@components/Link.astro';
|
import Link from '@components/Link.astro';
|
||||||
|
|
||||||
:::tip[Using Bun or Deno?]
|
:::tip[Using Bun or Deno?]
|
||||||
If you are using [Bun](https://bun.sh) or [Deno](https://deno.land) you are already good to go as they both support TypeScript out of the box!
|
If you are using [Bun](https://bun.sh) or [Deno](https://deno.land) you are already good to go as they both support TypeScript out of the box.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
If you're using NodeJS you have at least the two following options to support running TypeScript migration files in NodeJS.
|
You have at least the two following options to support running TypeScript migration files in NodeJS.
|
||||||
|
|
||||||
## Using `tsx`
|
## Using `tsx`
|
||||||
|
|
||||||
If you want to be able to write and run migration files written in TypeScript an easy way is to install the [`tsx`](https://github.com/privatenumber/tsx) package.
|
If you want to be able to write and run migration files written in TypeScript the easiest way is to install the [`tsx`](https://github.com/privatenumber/tsx) package.
|
||||||
|
|
||||||
### Installing `tsx`
|
### Installing `tsx`
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ After installing `tsx` you can load it in two ways.
|
||||||
|
|
||||||
#### Via CLI
|
#### Via CLI
|
||||||
|
|
||||||
Using the <Link href="/cli/up/#-i---import-module">`--import`</Link> flag you can load `tsx` before running your migration files.
|
Using the <Link href="/commands/up/#-i---import-module">`--import`</Link> flag you can load `tsx` before running your migration files.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem label="npm">
|
<TabItem label="npm">
|
||||||
|
|
@ -67,13 +67,9 @@ Using the <Link href="/cli/up/#-i---import-module">`--import`</Link> flag you ca
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
:::note
|
|
||||||
This method is necessary if you want to write your configuration file in TypeScript without having `typescript` installed in your production environment, as `tsx` must be loaded before the configuration file is loaded.
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Via configuration file
|
#### Via configuration file
|
||||||
|
|
||||||
You can also directly import `tsx` in your configuration file (will only work if you're not using TypeScript for your configuration file).
|
You can also directly import `tsx` in your configuration file.
|
||||||
|
|
||||||
```js title="emigrate.config.js" {1}
|
```js title="emigrate.config.js" {1}
|
||||||
import 'tsx';
|
import 'tsx';
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ title: "FAQ"
|
||||||
description: "Frequently asked questions about Emigrate."
|
description: "Frequently asked questions about Emigrate."
|
||||||
---
|
---
|
||||||
|
|
||||||
import Link from '@components/Link.astro';
|
|
||||||
|
|
||||||
## Why no `down` migrations?
|
## Why no `down` migrations?
|
||||||
|
|
||||||
> Always forward never backwards.
|
> Always forward never backwards.
|
||||||
|
|
||||||
Many other migration tools support `down` (or undo) migrations, but in all the years we have been
|
Many other migration tools support `down` migrations, but in all the years we have been
|
||||||
doing migrations we have never needed to rollback a migration in production,
|
doing migrations we have never needed to rollback a migration in production,
|
||||||
in that case we would just write a new migration to fix the problem.
|
in that case we would just write a new migration to fix the problem.
|
||||||
|
|
||||||
|
|
@ -19,7 +17,3 @@ and in such case you just revert the migration manually and fix the `up` migrati
|
||||||
The benefit of this is that you don't have to worry about writing `down` migrations, and you can focus on writing the `up` migrations.
|
The benefit of this is that you don't have to worry about writing `down` migrations, and you can focus on writing the `up` migrations.
|
||||||
This way you will only ever have to write `down` migrations when they are really necessary instead of for every migration
|
This way you will only ever have to write `down` migrations when they are really necessary instead of for every migration
|
||||||
(which makes it the exception rather than the rule, which is closer to the truth).
|
(which makes it the exception rather than the rule, which is closer to the truth).
|
||||||
|
|
||||||
## Can I use Emigrate with my existing database?
|
|
||||||
|
|
||||||
Yes, you can use Emigrate with an existing database. See the <Link href="/guides/baseline/">Baseline guide</Link> for more information.
|
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,5 @@ The generator is responsible for generating migration files in a specific format
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
Instead of having to install a generator plugin, you can also use the much simpler <Link href="/cli/new/#-t---template-path">`--template`</Link> option to specify a custom template file for new migrations.
|
Instead of having to install a generator plugin, you can also use the much simpler <Link href="/commands/new/#-t---template-path">`--template`</Link> option to specify a custom template file for new migrations.
|
||||||
:::
|
:::
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,4 @@ A <Link href="/plugins/generators/">generator plugin</Link> for generating new m
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
For more information see <Link href="/cli/new/">the `new` command</Link>'s documentation.
|
For more information see <Link href="/commands/new/">the `new` command</Link>'s documentation.
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,4 @@ The MySQL generator creates new migration files with the `.sql` extension. In th
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
For more information see <Link href="/cli/new/">the `new` command</Link>'s documentation.
|
For more information see <Link href="/commands/new/">the `new` command</Link>'s documentation.
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,4 @@ The PostgreSQL generator creates new migration files with the `.sql` extension.
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
For more information see <Link href="/cli/new/">the `new` command</Link>'s documentation.
|
For more information see <Link href="/commands/new/">the `new` command</Link>'s documentation.
|
||||||
|
|
|
||||||
23
docs/src/content/docs/plugins/reporters/default.mdx
Normal file
23
docs/src/content/docs/plugins/reporters/default.mdx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: Default Reporter
|
||||||
|
---
|
||||||
|
|
||||||
|
Emigrate's default reporter. The default reporter recognizes if the current terminal is an interactive shell (or if it's a CI environment), if that's the case _no_ animations will be shown.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
By default, Emigrate uses the default reporter.
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
Emigrate up v0.10.0 /Users/joakim/dev/@aboviq/test-emigrate (dry run)
|
||||||
|
|
||||||
|
1 pending migrations to run
|
||||||
|
|
||||||
|
› migration-folder/20231218135441244_create_some_table.sql (pending)
|
||||||
|
|
||||||
|
1 pending (1 total)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -20,7 +20,6 @@ Or set it up in your configuration file, see <Link href="/reference/configuratio
|
||||||
## Available Reporters
|
## Available Reporters
|
||||||
|
|
||||||
<CardGrid>
|
<CardGrid>
|
||||||
<LinkCard title="Pretty Reporter" description="The default reporter" href="pretty/" />
|
<LinkCard title="Default Reporter" href="default/" />
|
||||||
<LinkCard title="JSON Reporter" description="A built-in reporter for outputing a JSON object" href="json/" />
|
<LinkCard title="Pino Reporter" href="pino/" />
|
||||||
<LinkCard title="Pino Reporter" description="A reporter package for outputting new line delimited JSON" href="pino/" />
|
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
---
|
|
||||||
title: JSON Reporter
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
|
||||||
import Link from '@components/Link.astro';
|
|
||||||
|
|
||||||
An Emigrate reporter that outputs a JSON object.
|
|
||||||
|
|
||||||
The reporter is included by default and does not need to be installed separately.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Via CLI
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem label="npm">
|
|
||||||
```bash
|
|
||||||
npx emigrate <command> --reporter json
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="pnpm">
|
|
||||||
```bash
|
|
||||||
pnpm emigrate <command> --reporter json
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="yarn">
|
|
||||||
```bash
|
|
||||||
yarn emigrate <command> --reporter json
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="bun">
|
|
||||||
```bash
|
|
||||||
bunx --bun emigrate <command> --reporter json
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="deno">
|
|
||||||
```json title="package.json" {3}
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"emigrate": "emigrate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deno task emigrate <command> --reporter json
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
See for instance the <Link href="/cli/up/#-r---reporter-name">Reporter Option</Link> for the `up` command for more information.
|
|
||||||
|
|
||||||
### Via configuration file
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem label="JavaScript">
|
|
||||||
```js title="emigrate.config.js"
|
|
||||||
/** @type {import('@emigrate/cli').EmigrateConfig} */
|
|
||||||
export default {
|
|
||||||
reporter: 'json',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="TypeScript">
|
|
||||||
```ts title="emigrate.config.ts"
|
|
||||||
import { type EmigrateConfig } from '@emigrate/cli';
|
|
||||||
|
|
||||||
const config: EmigrateConfig = {
|
|
||||||
reporter: 'json',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
See <Link href="/reference/configuration/#reporter">Reporter Configuration</Link> for more information.
|
|
||||||
|
|
||||||
## Example output
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "up",
|
|
||||||
"version": "0.17.2",
|
|
||||||
"numberTotalMigrations": 1,
|
|
||||||
"numberDoneMigrations": 0,
|
|
||||||
"numberSkippedMigrations": 0,
|
|
||||||
"numberFailedMigrations": 0,
|
|
||||||
"numberPendingMigrations": 1,
|
|
||||||
"success": true,
|
|
||||||
"startTime": 1707206599968,
|
|
||||||
"endTime": 1707206600005,
|
|
||||||
"migrations": [
|
|
||||||
{
|
|
||||||
"name": "/your/project/migrations/20240206075446123_some_other_table.sql",
|
|
||||||
"status": "pending",
|
|
||||||
"duration": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -87,31 +87,15 @@ The `@emigrate/reporter-` prefix is optional when using this reporter.
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
See for instance the <Link href="/cli/up/#-r---reporter-name">Reporter Option</Link> for the `up` command for more information.
|
See for instance the <Link href="/commands/up/#-r---reporter-name">Reporter Option</Link> for the `up` command for more information.
|
||||||
|
|
||||||
### Via configuration file
|
### Via configuration file
|
||||||
|
|
||||||
<Tabs>
|
```js title="emigrate.config.js" {2}
|
||||||
<TabItem label="JavaScript">
|
export default {
|
||||||
```js title="emigrate.config.js"
|
reporter: 'pino',
|
||||||
/** @type {import('@emigrate/cli').EmigrateConfig} */
|
};
|
||||||
export default {
|
```
|
||||||
reporter: 'pino',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="TypeScript">
|
|
||||||
```ts title="emigrate.config.ts"
|
|
||||||
import { type EmigrateConfig } from '@emigrate/cli';
|
|
||||||
|
|
||||||
const config: EmigrateConfig = {
|
|
||||||
reporter: 'pino',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
See <Link href="/reference/configuration/#reporter">Reporter Configuration</Link> for more information.
|
See <Link href="/reference/configuration/#reporter">Reporter Configuration</Link> for more information.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
---
|
|
||||||
title: Pretty Reporter (default)
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
|
||||||
import Link from '@components/Link.astro';
|
|
||||||
|
|
||||||
Emigrate's default reporter. It recognizes if the current terminal is an interactive shell (or if it's a CI environment), if that's the case _no_ animations will be shown.
|
|
||||||
|
|
||||||
The reporter is included by default and does not need to be installed separately.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
By default, Emigrate uses the "pretty" reporter, but it can also be explicitly set by using the <Link href="/cli/up/#-r---reporter-name">`--reporter`</Link> flag.
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem label="npm">
|
|
||||||
```bash
|
|
||||||
npx emigrate <command> --reporter pretty
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="pnpm">
|
|
||||||
```bash
|
|
||||||
pnpm emigrate <command> --reporter pretty
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="yarn">
|
|
||||||
```bash
|
|
||||||
yarn emigrate <command> --reporter pretty
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="bun">
|
|
||||||
```bash
|
|
||||||
bunx --bun emigrate <command> --reporter pretty
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="deno">
|
|
||||||
```json title="package.json" {3}
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"emigrate": "emigrate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deno task emigrate <command> --reporter pretty
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
Or by setting it in the configuration file.
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem label="JavaScript">
|
|
||||||
```js title="emigrate.config.js"
|
|
||||||
/** @type {import('@emigrate/cli').EmigrateConfig} */
|
|
||||||
export default {
|
|
||||||
reporter: 'pretty',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem label="TypeScript">
|
|
||||||
```ts title="emigrate.config.ts"
|
|
||||||
import { type EmigrateConfig } from '@emigrate/cli';
|
|
||||||
|
|
||||||
const config: EmigrateConfig = {
|
|
||||||
reporter: 'pretty',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
See <Link href="/reference/configuration/#reporter">Reporter Configuration</Link> for more information.
|
|
||||||
|
|
||||||
## Example output
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
Emigrate up v0.17.2 /your/working/directory (dry run)
|
|
||||||
|
|
||||||
1 pending migrations to run
|
|
||||||
|
|
||||||
› migration-folder/20231218135441244_create_some_table.sql (pending)
|
|
||||||
|
|
||||||
1 pending (1 total)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
@ -45,9 +45,9 @@ Set the directory where your migrations are located, relative to the project roo
|
||||||
|
|
||||||
### `reporter`
|
### `reporter`
|
||||||
|
|
||||||
**type:** `"pretty" | "json" | string | EmigrateReporter | Promise<EmigrateReporter> | (() => Promise<EmigrateReporter>)`
|
**type:** `string | EmigrateReporter | Promise<EmigrateReporter> | (() => Promise<EmigrateReporter>)`
|
||||||
|
|
||||||
**default:** `"pretty"` - the default reporter
|
**default:** `"default"` - the default reporter
|
||||||
|
|
||||||
Set the reporter to use for the different commands. Specifying a <Link href="/plugins/reporters/">reporter</Link> is most useful in a CI or production environment where you either ship logs or want to have a machine-readable format.
|
Set the reporter to use for the different commands. Specifying a <Link href="/plugins/reporters/">reporter</Link> is most useful in a CI or production environment where you either ship logs or want to have a machine-readable format.
|
||||||
|
|
||||||
|
|
@ -64,9 +64,6 @@ export default {
|
||||||
up: {
|
up: {
|
||||||
reporter: 'json',
|
reporter: 'json',
|
||||||
},
|
},
|
||||||
new: {
|
|
||||||
reporter: 'pretty', // Not really necessary, as it's the default
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
24
package.json
24
package.json
|
|
@ -37,10 +37,9 @@
|
||||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.15.0",
|
"node": "20.9.0",
|
||||||
"pnpm": "9.4.0"
|
"pnpm": "8.10.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
@ -62,31 +61,26 @@
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [
|
"files": "packages/**/*.test.ts",
|
||||||
"packages/**/*.test.ts",
|
|
||||||
"packages/**/*.integration.ts"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-floating-promises": 0,
|
"@typescript-eslint/no-floating-promises": 0
|
||||||
"max-params": 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@changesets/cli": "2.27.1",
|
"@changesets/cli": "2.27.1",
|
||||||
"@commitlint/cli": "18.6.1",
|
"@commitlint/cli": "18.4.3",
|
||||||
"@commitlint/config-conventional": "18.6.1",
|
"@commitlint/config-conventional": "18.4.3",
|
||||||
"@types/node": "20.10.4",
|
"@types/node": "20.10.4",
|
||||||
"glob": "10.3.10",
|
"glob": "10.3.10",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"lint-staged": "15.2.0",
|
"lint-staged": "15.2.0",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"testcontainers": "10.24.2",
|
"tsx": "4.7.0",
|
||||||
"tsx": "4.15.7",
|
"turbo": "1.10.16",
|
||||||
"turbo": "2.0.5",
|
"typescript": "5.3.3",
|
||||||
"typescript": "5.5.2",
|
|
||||||
"xo": "0.56.0"
|
"xo": "0.56.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,5 @@
|
||||||
# @emigrate/cli
|
# @emigrate/cli
|
||||||
|
|
||||||
## 0.18.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
- Updated dependencies [d779286]
|
|
||||||
- @emigrate/plugin-tools@0.9.8
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.18.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/plugin-tools@0.9.7
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.18.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 4152209: Handle the case where the config is returned as an object with a nested `default` property
|
|
||||||
|
|
||||||
## 0.18.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 57a0991: Cleanup AbortSignal listeners when they are not needed to avoid MaxListenersExceededWarning when migrating many migrations at once
|
|
||||||
|
|
||||||
## 0.18.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- c838ffb: Make it possible to write the Emigrate configuration file in TypeScript and load it using `tsx` in a NodeJS environment by importing packages provided using the `--import` CLI option before loading the configuration file. This makes it possible to run Emigrate in production with a configuration file written in TypeScript without having the `typescript` package installed.
|
|
||||||
- 18382ce: Add a built-in "json" reporter for outputting a single JSON object
|
|
||||||
- 18382ce: Rename the "default" reporter to "pretty" and make it possible to specify it using the `--reporter` CLI option or in the configuration file
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- c838ffb: Don't use the `typescript` package for loading an Emigrate configuration file written in TypeScript in a Bun or Deno environment
|
|
||||||
|
|
||||||
## 0.17.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 61cbcbd: Force exiting after 10 seconds should not change the exit code, i.e. if all migrations have run successfully the exit code should be 0
|
|
||||||
|
|
||||||
## 0.17.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 543b7f6: Use setTimeout/setInterval from "node:timers" so that .unref() correctly works with Bun
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/plugin-tools@0.9.6
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.17.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 0faebbe: Add support for passing the relative path to a migration file to remove from the history using the "remove" command
|
|
||||||
- 9109238: When the `--from` or `--to` CLI options are used the given migration name (or path to migration file) must exist. This is a BREAKING CHANGE from before. The reasoning is that by forcing the migrations to exist you avoid accidentally running migrations you don't intend to, because a simple typo could have the effect that many unwanted migrations is executed so it's better to show an error if that's the case.
|
|
||||||
- 1f139fd: Completely rework how the "remove" command is run, this is to make it more similar to the "up" and "list" command as now it will also use the `onMigrationStart`, `onMigrationSuccess` and `onMigrationError` reporter methods when reporting the command progress. It's also in preparation for adding `--from` and `--to` CLI options for the "remove" command, similar to how the same options work for the "up" command.
|
|
||||||
- 9109238: Add support for passing relative paths to migration files as the `--from` and `--to` CLI options. This is very useful from terminals that support autocomplete for file paths. It also makes it possible to copy the path to a migration file from Emigrate's output and use that as either `--from` and `--to` directly.
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- f1b9098: Only include files when collecting migrations, i.e. it should be possible to have folders inside your migrations folder.
|
|
||||||
- 2f6b4d2: Don't dim decimal points in durations in the default reporter
|
|
||||||
- f2d4bb3: Set Emigrate error instance names from their respective constructor's name for consistency and correct error deserialization.
|
|
||||||
- ef45be9: Show number of skipped migrations correctly in the command output
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
- @emigrate/plugin-tools@0.9.5
|
|
||||||
|
|
||||||
## 0.16.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- b56b6da: Handle migration history entries without file extensions for migration files with periods in their names that are not part of the file extension. Previously Emigrate would attempt to re-run these migrations, but now it will correctly ignore them. E.g. the migration history contains an entry for "migration.file.name" and the migration file is named "migration.file.name.js" it will not be re-run.
|
|
||||||
|
|
||||||
## 0.16.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 121492b: Sort migration files lexicographically correctly by using the default Array.sort implementation
|
|
||||||
|
|
||||||
## 0.16.0
|
## 0.16.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -20,24 +20,6 @@ bun add @emigrate/cli
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```text
|
|
||||||
Usage: emigrate <options>/<command>
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
|
||||||
-v, --version Print version number and exit
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
|
|
||||||
up Run all pending migrations (or do a dry run)
|
|
||||||
new Create a new migration file
|
|
||||||
list List all migrations and their status
|
|
||||||
remove Remove entries from the migration history
|
|
||||||
```
|
|
||||||
|
|
||||||
### `emigrate up`
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Usage: emigrate up [options]
|
Usage: emigrate up [options]
|
||||||
|
|
||||||
|
|
@ -46,35 +28,22 @@ Run all pending migrations
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
|
||||||
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
||||||
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
||||||
|
|
||||||
-s, --storage <name> The storage to use for where to store the migration history (required)
|
-s, --storage <name> The storage to use for where to store the migration history (required)
|
||||||
|
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
|
|
||||||
-r, --reporter <name> The reporter to use for reporting the migration progress
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
|
|
||||||
-l, --limit <count> Limit the number of migrations to run
|
-l, --limit <count> Limit the number of migrations to run
|
||||||
|
-f, --from <name> Start running migrations from the given migration name, the given name doesn't need to exist
|
||||||
-f, --from <name/path> Start running migrations from the given migration name or relative file path to a migration file,
|
and is compared in lexicographical order
|
||||||
the given name or path needs to exist. The same migration and those after it lexicographically will be run
|
-t, --to <name> Skip migrations after the given migration name, the given name doesn't need to exist
|
||||||
|
and is compared in lexicographical order
|
||||||
-t, --to <name/path> Skip migrations after the given migration name or relative file path to a migration file,
|
|
||||||
the given name or path needs to exist. The same migration and those before it lexicographically will be run
|
|
||||||
|
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-execution Mark the migrations as executed and successful without actually running them,
|
--no-execution Mark the migrations as executed and successful without actually running them,
|
||||||
which is useful if you want to mark migrations as successful after running them manually
|
which is useful if you want to mark migrations as successful after running them manually
|
||||||
|
|
||||||
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: 10)
|
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: 10)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/cli",
|
"name": "@emigrate/cli",
|
||||||
"version": "0.18.4",
|
"version": "0.16.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -19,8 +18,7 @@
|
||||||
"emigrate": "dist/cli.js"
|
"emigrate": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
@ -37,9 +35,7 @@
|
||||||
"immigration"
|
"immigration"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emigrate/tsconfig": "workspace:*",
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
"@types/bun": "1.0.5",
|
|
||||||
"bun-types": "1.0.26"
|
|
||||||
},
|
},
|
||||||
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
||||||
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/cli#readme",
|
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/cli#readme",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export async function* arrayMapAsync<T, U>(iterable: AsyncIterable<T>, mapper: (item: T) => U): AsyncIterable<U> {
|
|
||||||
for await (const item of iterable) {
|
|
||||||
yield mapper(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { parseArgs } from 'node:util';
|
import { parseArgs } from 'node:util';
|
||||||
import { setTimeout } from 'node:timers';
|
|
||||||
import importFromEsm from 'import-from-esm';
|
import importFromEsm from 'import-from-esm';
|
||||||
import { CommandAbortError, ShowUsageError } from './errors.js';
|
import { CommandAbortError, ShowUsageError } from './errors.js';
|
||||||
import { getConfig } from './get-config.js';
|
import { getConfig } from './get-config.js';
|
||||||
|
|
@ -24,6 +23,7 @@ const importAll = async (cwd: string, modules: string[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const up: Action = async (args, abortSignal) => {
|
const up: Action = async (args, abortSignal) => {
|
||||||
|
const config = await getConfig('up');
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -93,35 +93,22 @@ Run all pending migrations
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
|
||||||
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
||||||
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
||||||
|
|
||||||
-s, --storage <name> The storage to use for where to store the migration history (required)
|
-s, --storage <name> The storage to use for where to store the migration history (required)
|
||||||
|
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
-r, --reporter <name> The reporter to use for reporting the migration progress (default: pretty)
|
|
||||||
|
|
||||||
-l, --limit <count> Limit the number of migrations to run
|
-l, --limit <count> Limit the number of migrations to run
|
||||||
|
-f, --from <name> Start running migrations from the given migration name, the given name doesn't need to exist
|
||||||
-f, --from <name/path> Start running migrations from the given migration name or relative file path to a migration file,
|
and is compared in lexicographical order
|
||||||
the given name or path needs to exist. The same migration and those after it lexicographically will be run
|
-t, --to <name> Skip migrations after the given migration name, the given name doesn't need to exist
|
||||||
|
and is compared in lexicographical order
|
||||||
-t, --to <name/path> Skip migrations after the given migration name or relative file path to a migration file,
|
|
||||||
the given name or path needs to exist. The same migration and those before it lexicographically will be run
|
|
||||||
|
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-execution Mark the migrations as executed and successful without actually running them,
|
--no-execution Mark the migrations as executed and successful without actually running them,
|
||||||
which is useful if you want to mark migrations as successful after running them manually
|
which is useful if you want to mark migrations as successful after running them manually
|
||||||
|
|
||||||
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: ${DEFAULT_RESPITE_SECONDS})
|
--abort-respite <sec> The number of seconds to wait before abandoning running migrations after the command has been aborted (default: ${DEFAULT_RESPITE_SECONDS})
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -142,14 +129,6 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
if (values.import) {
|
|
||||||
await importAll(cwd, values.import);
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
|
|
||||||
|
|
||||||
const config = await getConfig('up', forceImportTypeScriptAsIs);
|
|
||||||
const {
|
const {
|
||||||
directory = config.directory,
|
directory = config.directory,
|
||||||
storage = config.storage,
|
storage = config.storage,
|
||||||
|
|
@ -158,6 +137,7 @@ Examples:
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
limit: limitString,
|
limit: limitString,
|
||||||
|
import: imports = [],
|
||||||
'abort-respite': abortRespiteString,
|
'abort-respite': abortRespiteString,
|
||||||
'no-execution': noExecution,
|
'no-execution': noExecution,
|
||||||
} = values;
|
} = values;
|
||||||
|
|
@ -183,6 +163,8 @@ Examples:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await importAll(cwd, imports);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { default: upCommand } = await import('./commands/up.js');
|
const { default: upCommand } = await import('./commands/up.js');
|
||||||
process.exitCode = await upCommand({
|
process.exitCode = await upCommand({
|
||||||
|
|
@ -213,6 +195,7 @@ Examples:
|
||||||
};
|
};
|
||||||
|
|
||||||
const newMigration: Action = async (args) => {
|
const newMigration: Action = async (args) => {
|
||||||
|
const config = await getConfig('new');
|
||||||
const { values, positionals } = parseArgs({
|
const { values, positionals } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -242,12 +225,6 @@ const newMigration: Action = async (args) => {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
import: {
|
|
||||||
type: 'string',
|
|
||||||
short: 'i',
|
|
||||||
multiple: true,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
color: {
|
color: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
|
@ -269,24 +246,14 @@ Arguments:
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
-r, --reporter <name> The reporter to use for reporting the migration file creation progress
|
||||||
-i, --import <module> Additional modules/packages to import before creating the migration (can be specified multiple times)
|
|
||||||
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
|
||||||
|
|
||||||
-r, --reporter <name> The reporter to use for reporting the migration file creation progress (default: pretty)
|
|
||||||
|
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
|
|
||||||
-t, --template <path> A template file to use as contents for the new migration file
|
-t, --template <path> A template file to use as contents for the new migration file
|
||||||
(if the extension option is not provided the template file's extension will be used)
|
(if the extension option is not provided the template file's extension will be used)
|
||||||
|
|
||||||
-x, --extension <ext> The extension to use for the new migration file
|
-x, --extension <ext> The extension to use for the new migration file
|
||||||
(if no template or plugin is provided an empty migration file will be created with the given extension)
|
(if no template or plugin is provided an empty migration file will be created with the given extension)
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
One of the --template, --extension or the --plugin options must be specified
|
One of the --template, --extension or the --plugin options must be specified
|
||||||
|
|
@ -306,14 +273,6 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
if (values.import) {
|
|
||||||
await importAll(cwd, values.import);
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
|
|
||||||
|
|
||||||
const config = await getConfig('new', forceImportTypeScriptAsIs);
|
|
||||||
const {
|
const {
|
||||||
directory = config.directory,
|
directory = config.directory,
|
||||||
template = config.template,
|
template = config.template,
|
||||||
|
|
@ -339,6 +298,7 @@ Examples:
|
||||||
};
|
};
|
||||||
|
|
||||||
const list: Action = async (args) => {
|
const list: Action = async (args) => {
|
||||||
|
const config = await getConfig('list');
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -381,18 +341,12 @@ List all migrations and their status. This command does not run any migrations.
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
|
||||||
-i, --import <module> Additional modules/packages to import before listing the migrations (can be specified multiple times)
|
-i, --import <module> Additional modules/packages to import before listing the migrations (can be specified multiple times)
|
||||||
For example if you want to use Dotenv to load environment variables
|
For example if you want to use Dotenv to load environment variables
|
||||||
|
-r, --reporter <name> The reporter to use for reporting the migrations
|
||||||
-r, --reporter <name> The reporter to use for reporting the migrations (default: pretty)
|
|
||||||
|
|
||||||
-s, --storage <name> The storage to use to get the migration history (required)
|
-s, --storage <name> The storage to use to get the migration history (required)
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -408,15 +362,14 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
const {
|
||||||
|
directory = config.directory,
|
||||||
|
storage = config.storage,
|
||||||
|
reporter = config.reporter,
|
||||||
|
import: imports = [],
|
||||||
|
} = values;
|
||||||
|
|
||||||
if (values.import) {
|
await importAll(cwd, imports);
|
||||||
await importAll(cwd, values.import);
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
|
|
||||||
|
|
||||||
const config = await getConfig('list', forceImportTypeScriptAsIs);
|
|
||||||
const { directory = config.directory, storage = config.storage, reporter = config.reporter } = values;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { default: listCommand } = await import('./commands/list.js');
|
const { default: listCommand } = await import('./commands/list.js');
|
||||||
|
|
@ -434,6 +387,7 @@ Examples:
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove: Action = async (args) => {
|
const remove: Action = async (args) => {
|
||||||
|
const config = await getConfig('remove');
|
||||||
const { values, positionals } = parseArgs({
|
const { values, positionals } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -473,32 +427,26 @@ const remove: Action = async (args) => {
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const usage = `Usage: emigrate remove [options] <name/path>
|
const usage = `Usage: emigrate remove [options] <name>
|
||||||
|
|
||||||
Remove entries from the migration history.
|
Remove entries from the migration history.
|
||||||
This is useful if you want to retry a migration that has failed.
|
This is useful if you want to retry a migration that has failed.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
||||||
name/path The name of or relative path to the migration file to remove from the history (required)
|
name The name of the migration file to remove from the history (required)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
-d, --directory <path> The directory where the migration files are located (required)
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
|
||||||
-i, --import <module> Additional modules/packages to import before removing the migration (can be specified multiple times)
|
-i, --import <module> Additional modules/packages to import before removing the migration (can be specified multiple times)
|
||||||
For example if you want to use Dotenv to load environment variables
|
For example if you want to use Dotenv to load environment variables
|
||||||
|
-r, --reporter <name> The reporter to use for reporting the removal process
|
||||||
-r, --reporter <name> The reporter to use for reporting the removal process (default: pretty)
|
|
||||||
|
|
||||||
-s, --storage <name> The storage to use to get the migration history (required)
|
-s, --storage <name> The storage to use to get the migration history (required)
|
||||||
|
-f, --force Force removal of the migration history entry even if the migration file does not exist
|
||||||
-f, --force Force removal of the migration history entry even if the migration is not in a failed state
|
or it's in a non-failed state
|
||||||
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -506,7 +454,6 @@ Examples:
|
||||||
emigrate remove -d migrations -s fs 20231122120529381_some_migration_file.js
|
emigrate remove -d migrations -s fs 20231122120529381_some_migration_file.js
|
||||||
emigrate remove --directory ./migrations --storage postgres 20231122120529381_some_migration_file.sql
|
emigrate remove --directory ./migrations --storage postgres 20231122120529381_some_migration_file.sql
|
||||||
emigrate remove -i dotenv/config -d ./migrations -s postgres 20231122120529381_some_migration_file.sql
|
emigrate remove -i dotenv/config -d ./migrations -s postgres 20231122120529381_some_migration_file.sql
|
||||||
emigrate remove -i dotenv/config -d ./migrations -s postgres migrations/20231122120529381_some_migration_file.sql
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (values.help) {
|
if (values.help) {
|
||||||
|
|
@ -516,15 +463,15 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
const {
|
||||||
|
directory = config.directory,
|
||||||
|
storage = config.storage,
|
||||||
|
reporter = config.reporter,
|
||||||
|
force,
|
||||||
|
import: imports = [],
|
||||||
|
} = values;
|
||||||
|
|
||||||
if (values.import) {
|
await importAll(cwd, imports);
|
||||||
await importAll(cwd, values.import);
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
|
|
||||||
|
|
||||||
const config = await getConfig('remove', forceImportTypeScriptAsIs);
|
|
||||||
const { directory = config.directory, storage = config.storage, reporter = config.reporter, force } = values;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { default: removeCommand } = await import('./commands/remove.js');
|
const { default: removeCommand } = await import('./commands/remove.js');
|
||||||
|
|
@ -641,5 +588,5 @@ await main(process.argv.slice(2), controller.signal);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.error('Process did not exit within 10 seconds, forcing exit');
|
console.error('Process did not exit within 10 seconds, forcing exit');
|
||||||
process.exit(process.exitCode);
|
process.exit(1);
|
||||||
}, 10_000).unref();
|
}, 10_000).unref();
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import { describe, it } from 'node:test';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import { collectMigrations } from './collect-migrations.js';
|
|
||||||
import { toEntries, toEntry, toMigration, toMigrations } from './test-utils.js';
|
|
||||||
import { arrayFromAsync } from './array-from-async.js';
|
|
||||||
import { MigrationHistoryError } from './errors.js';
|
|
||||||
|
|
||||||
describe('collect-migrations', () => {
|
|
||||||
it('returns all migrations from the history and all pending migrations', async () => {
|
|
||||||
const cwd = '/cwd';
|
|
||||||
const directory = 'directory';
|
|
||||||
const history = {
|
|
||||||
async *[Symbol.asyncIterator]() {
|
|
||||||
yield* toEntries(['migration1.js', 'migration2.js']);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const getMigrations = async () => toMigrations(cwd, directory, ['migration1.js', 'migration2.js', 'migration3.js']);
|
|
||||||
|
|
||||||
const result = await arrayFromAsync(collectMigrations(cwd, directory, history, getMigrations));
|
|
||||||
|
|
||||||
assert.deepStrictEqual(result, [
|
|
||||||
{
|
|
||||||
...toMigration(cwd, directory, 'migration1.js'),
|
|
||||||
duration: 0,
|
|
||||||
status: 'done',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...toMigration(cwd, directory, 'migration2.js'),
|
|
||||||
duration: 0,
|
|
||||||
status: 'done',
|
|
||||||
},
|
|
||||||
toMigration(cwd, directory, 'migration3.js'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes any errors from the history', async () => {
|
|
||||||
const entry = toEntry('migration1.js', 'failed');
|
|
||||||
const cwd = '/cwd';
|
|
||||||
const directory = 'directory';
|
|
||||||
const history = {
|
|
||||||
async *[Symbol.asyncIterator]() {
|
|
||||||
yield* [entry];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const getMigrations = async () => toMigrations(cwd, directory, ['migration1.js', 'migration2.js', 'migration3.js']);
|
|
||||||
|
|
||||||
const result = await arrayFromAsync(collectMigrations(cwd, directory, history, getMigrations));
|
|
||||||
|
|
||||||
assert.deepStrictEqual(result, [
|
|
||||||
{
|
|
||||||
...toMigration(cwd, directory, 'migration1.js'),
|
|
||||||
duration: 0,
|
|
||||||
status: 'failed',
|
|
||||||
error: MigrationHistoryError.fromHistoryEntry(entry),
|
|
||||||
},
|
|
||||||
toMigration(cwd, directory, 'migration2.js'),
|
|
||||||
toMigration(cwd, directory, 'migration3.js'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can handle a migration history without file extensions', async () => {
|
|
||||||
const cwd = '/cwd';
|
|
||||||
const directory = 'directory';
|
|
||||||
const history = {
|
|
||||||
async *[Symbol.asyncIterator]() {
|
|
||||||
yield* toEntries(['migration1']);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const getMigrations = async () => toMigrations(cwd, directory, ['migration1.js', 'migration2.js', 'migration3.js']);
|
|
||||||
|
|
||||||
const result = await arrayFromAsync(collectMigrations(cwd, directory, history, getMigrations));
|
|
||||||
|
|
||||||
assert.deepStrictEqual(result, [
|
|
||||||
{ ...toMigration(cwd, directory, 'migration1.js'), duration: 0, status: 'done' },
|
|
||||||
toMigration(cwd, directory, 'migration2.js'),
|
|
||||||
toMigration(cwd, directory, 'migration3.js'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can handle a migration history without file extensions even if the migration name contains periods', async () => {
|
|
||||||
const cwd = '/cwd';
|
|
||||||
const directory = 'directory';
|
|
||||||
const history = {
|
|
||||||
async *[Symbol.asyncIterator]() {
|
|
||||||
yield* toEntries(['mig.ration1']);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const getMigrations = async () =>
|
|
||||||
toMigrations(cwd, directory, ['mig.ration1.js', 'migration2.js', 'migration3.js']);
|
|
||||||
|
|
||||||
const result = await arrayFromAsync(collectMigrations(cwd, directory, history, getMigrations));
|
|
||||||
|
|
||||||
assert.deepStrictEqual(result, [
|
|
||||||
{ ...toMigration(cwd, directory, 'mig.ration1.js'), duration: 0, status: 'done' },
|
|
||||||
toMigration(cwd, directory, 'migration2.js'),
|
|
||||||
toMigration(cwd, directory, 'migration3.js'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,28 +1,29 @@
|
||||||
|
import { extname } from 'node:path';
|
||||||
import { type MigrationHistoryEntry, type MigrationMetadata, type MigrationMetadataFinished } from '@emigrate/types';
|
import { type MigrationHistoryEntry, type MigrationMetadata, type MigrationMetadataFinished } from '@emigrate/types';
|
||||||
import { toMigrationMetadata } from './to-migration-metadata.js';
|
import { toMigrationMetadata } from './to-migration-metadata.js';
|
||||||
import { getMigrations as getMigrationsOriginal, type GetMigrationsFunction } from './get-migrations.js';
|
import { getMigrations as getMigrationsOriginal } from './get-migrations.js';
|
||||||
|
|
||||||
export async function* collectMigrations(
|
export async function* collectMigrations(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
directory: string,
|
directory: string,
|
||||||
history: AsyncIterable<MigrationHistoryEntry>,
|
history: AsyncIterable<MigrationHistoryEntry>,
|
||||||
getMigrations: GetMigrationsFunction = getMigrationsOriginal,
|
getMigrations = getMigrationsOriginal,
|
||||||
): AsyncIterable<MigrationMetadata | MigrationMetadataFinished> {
|
): AsyncIterable<MigrationMetadata | MigrationMetadataFinished> {
|
||||||
const allMigrations = await getMigrations(cwd, directory);
|
const allMigrations = await getMigrations(cwd, directory);
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
||||||
for await (const entry of history) {
|
for await (const entry_ of history) {
|
||||||
const migration = allMigrations.find((migrationFile) => {
|
const entry = extname(entry_.name) === '' ? { ...entry_, name: `${entry_.name}.js` } : entry_;
|
||||||
return migrationFile.name === entry.name || migrationFile.name === `${entry.name}.js`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!migration) {
|
const index = allMigrations.findIndex((migrationFile) => migrationFile.name === entry.name);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield toMigrationMetadata({ ...entry, name: migration.name }, { cwd, directory });
|
yield toMigrationMetadata(entry, { cwd, directory });
|
||||||
|
|
||||||
seen.add(migration.name);
|
seen.add(entry.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* allMigrations.filter((migration) => !seen.has(migration.name));
|
yield* allMigrations.filter((migration) => !seen.has(migration.name));
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import { BadOptionError, MissingOptionError, StorageInitError, toError } from '.
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
import { exec } from '../exec.js';
|
import { exec } from '../exec.js';
|
||||||
import { migrationRunner } from '../migration-runner.js';
|
import { migrationRunner } from '../migration-runner.js';
|
||||||
|
import { arrayFromAsync } from '../array-from-async.js';
|
||||||
import { collectMigrations } from '../collect-migrations.js';
|
import { collectMigrations } from '../collect-migrations.js';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import { getStandardReporter } from '../reporters/get.js';
|
|
||||||
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
type ExtraFlags = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
@ -17,7 +19,7 @@ export default async function listCommand({
|
||||||
storage: storageConfig,
|
storage: storageConfig,
|
||||||
color,
|
color,
|
||||||
cwd,
|
cwd,
|
||||||
}: Config & ExtraFlags): Promise<number> {
|
}: Config & ExtraFlags) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw MissingOptionError.fromOption('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +30,7 @@ export default async function listCommand({
|
||||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = getStandardReporter(reporterConfig) ?? (await getOrLoadReporter([reporterConfig]));
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw BadOptionError.fromOption(
|
throw BadOptionError.fromOption(
|
||||||
|
|
@ -54,19 +56,13 @@ export default async function listCommand({
|
||||||
dry: true,
|
dry: true,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations: collectedMigrations,
|
migrations: await arrayFromAsync(collectedMigrations),
|
||||||
async validate() {
|
async validate() {
|
||||||
// No-op
|
// No-op
|
||||||
},
|
},
|
||||||
async execute() {
|
async execute() {
|
||||||
throw new Error('Unexpected execute call');
|
throw new Error('Unexpected execute call');
|
||||||
},
|
},
|
||||||
async onSuccess() {
|
|
||||||
throw new Error('Unexpected onSuccess call');
|
|
||||||
},
|
|
||||||
async onError() {
|
|
||||||
throw new Error('Unexpected onError call');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return error ? 1 : 0;
|
return error ? 1 : 0;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import { type Config } from '../types.js';
|
||||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import { getDuration } from '../get-duration.js';
|
import { getDuration } from '../get-duration.js';
|
||||||
import { getStandardReporter } from '../reporters/get.js';
|
|
||||||
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
type ExtraFlags = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
@ -24,7 +25,7 @@ type ExtraFlags = {
|
||||||
export default async function newCommand(
|
export default async function newCommand(
|
||||||
{ directory, template, reporter: reporterConfig, plugins = [], cwd, extension, color }: Config & ExtraFlags,
|
{ directory, template, reporter: reporterConfig, plugins = [], cwd, extension, color }: Config & ExtraFlags,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw MissingOptionError.fromOption('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +38,7 @@ export default async function newCommand(
|
||||||
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
|
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = getStandardReporter(reporterConfig) ?? (await getOrLoadReporter([reporterConfig]));
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw BadOptionError.fromOption(
|
throw BadOptionError.fromOption(
|
||||||
|
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
import { describe, it } from 'node:test';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import { type EmigrateReporter, type Storage, type Plugin, type MigrationMetadataFinished } from '@emigrate/types';
|
|
||||||
import { deserializeError } from 'serialize-error';
|
|
||||||
import { version } from '../get-package-info.js';
|
|
||||||
import {
|
|
||||||
BadOptionError,
|
|
||||||
MigrationNotRunError,
|
|
||||||
MigrationRemovalError,
|
|
||||||
OptionNeededError,
|
|
||||||
StorageInitError,
|
|
||||||
} from '../errors.js';
|
|
||||||
import {
|
|
||||||
assertErrorEqualEnough,
|
|
||||||
getErrorCause,
|
|
||||||
getMockedReporter,
|
|
||||||
getMockedStorage,
|
|
||||||
toEntry,
|
|
||||||
toMigrations,
|
|
||||||
type Mocked,
|
|
||||||
} from '../test-utils.js';
|
|
||||||
import removeCommand from './remove.js';
|
|
||||||
|
|
||||||
describe('remove', () => {
|
|
||||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
|
||||||
const { reporter, run } = getRemoveCommand([]);
|
|
||||||
|
|
||||||
const exitCode = await run('some_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFailed(reporter, StorageInitError.fromError(new Error('No storage configured')));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the given migration has not been executed', async () => {
|
|
||||||
const storage = getMockedStorage(['some_other_migration.js']);
|
|
||||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('some_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'some_migration.js',
|
|
||||||
status: 'failed',
|
|
||||||
error: new MigrationNotRunError('Migration "some_migration.js" is not in the migration history'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
new MigrationNotRunError('Migration "some_migration.js" is not in the migration history'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the given migration is not in a failed state in the history', async () => {
|
|
||||||
const storage = getMockedStorage(['1_old_migration.js', '2_some_migration.js', '3_new_migration.js']);
|
|
||||||
const { reporter, run } = getRemoveCommand(['2_some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('2_some_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: '2_some_migration.js',
|
|
||||||
status: 'failed',
|
|
||||||
error: OptionNeededError.fromOption(
|
|
||||||
'force',
|
|
||||||
'The migration "2_some_migration.js" is not in a failed state. Use the "force" option to force its removal',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
OptionNeededError.fromOption(
|
|
||||||
'force',
|
|
||||||
'The migration "2_some_migration.js" is not in a failed state. Use the "force" option to force its removal',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the given migration does not exist at all', async () => {
|
|
||||||
const storage = getMockedStorage(['some_migration.js']);
|
|
||||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('some_other_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[],
|
|
||||||
BadOptionError.fromOption('name', 'The migration: "migrations/some_other_migration.js" was not found'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0, removes the migration from the history and finishes without an error when the given migration is in a failed state', async () => {
|
|
||||||
const storage = getMockedStorage([toEntry('some_migration.js', 'failed')]);
|
|
||||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('some_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(reporter, storage, [{ name: 'some_migration.js', status: 'done', started: true }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0, removes the migration from the history and finishes without an error when the given migration is not in a failed state but "force" is true', async () => {
|
|
||||||
const storage = getMockedStorage(['1_old_migration.js', '2_some_migration.js', '3_new_migration.js']);
|
|
||||||
const { reporter, run } = getRemoveCommand(['2_some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('2_some_migration.js', { force: true });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(reporter, storage, [{ name: '2_some_migration.js', status: 'done', started: true }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the removal of the migration crashes', async () => {
|
|
||||||
const storage = getMockedStorage([toEntry('some_migration.js', 'failed')]);
|
|
||||||
storage.remove.mock.mockImplementation(async () => {
|
|
||||||
throw new Error('Some error');
|
|
||||||
});
|
|
||||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run('some_migration.js');
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'some_migration.js',
|
|
||||||
status: 'failed',
|
|
||||||
error: new Error('Some error'),
|
|
||||||
started: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
new MigrationRemovalError('Failed to remove migration: migrations/some_migration.js', {
|
|
||||||
cause: new Error('Some error'),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function getRemoveCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
|
||||||
const reporter = getMockedReporter();
|
|
||||||
|
|
||||||
const run = async (
|
|
||||||
name: string,
|
|
||||||
options?: Omit<Parameters<typeof removeCommand>[0], 'cwd' | 'directory' | 'storage' | 'reporter' | 'plugins'>,
|
|
||||||
) => {
|
|
||||||
return removeCommand(
|
|
||||||
{
|
|
||||||
cwd: '/emigrate',
|
|
||||||
directory: 'migrations',
|
|
||||||
storage: {
|
|
||||||
async initializeStorage() {
|
|
||||||
if (!storage) {
|
|
||||||
throw new Error('No storage configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
return storage;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reporter,
|
|
||||||
plugins: plugins ?? [],
|
|
||||||
async getMigrations(cwd, directory) {
|
|
||||||
return toMigrations(cwd, directory, migrationFiles);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
},
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
run,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertPreconditionsFailed(reporter: Mocked<Required<EmigrateReporter>>, finishedError?: Error) {
|
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'remove',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
directory: 'migrations',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0, 'Collected call');
|
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0, 'Locked call');
|
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0, 'Started migrations');
|
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0, 'Successful migrations');
|
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0, 'Failed migrations');
|
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
|
||||||
// hackety hack:
|
|
||||||
if (finishedError) {
|
|
||||||
finishedError.stack = error?.stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
|
||||||
const cause = getErrorCause(error);
|
|
||||||
const expectedCause = finishedError?.cause;
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
cause,
|
|
||||||
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
|
||||||
'Finished error cause',
|
|
||||||
);
|
|
||||||
assert.strictEqual(entries?.length, 0, 'Finished entries length');
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertPreconditionsFulfilled(
|
|
||||||
reporter: Mocked<Required<EmigrateReporter>>,
|
|
||||||
storage: Mocked<Storage>,
|
|
||||||
expected: Array<{ name: string; status: MigrationMetadataFinished['status']; started?: boolean; error?: Error }>,
|
|
||||||
finishedError?: Error,
|
|
||||||
) {
|
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
|
||||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
|
||||||
{
|
|
||||||
command: 'remove',
|
|
||||||
cwd: '/emigrate',
|
|
||||||
version,
|
|
||||||
dry: false,
|
|
||||||
color: undefined,
|
|
||||||
directory: 'migrations',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let started = 0;
|
|
||||||
let done = 0;
|
|
||||||
let failed = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
let pending = 0;
|
|
||||||
let failedAndStarted = 0;
|
|
||||||
const failedEntries: typeof expected = [];
|
|
||||||
const successfulEntries: typeof expected = [];
|
|
||||||
|
|
||||||
for (const entry of expected) {
|
|
||||||
if (entry.started) {
|
|
||||||
started++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line default-case
|
|
||||||
switch (entry.status) {
|
|
||||||
case 'done': {
|
|
||||||
done++;
|
|
||||||
|
|
||||||
if (entry.started) {
|
|
||||||
successfulEntries.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'failed': {
|
|
||||||
failed++;
|
|
||||||
failedEntries.push(entry);
|
|
||||||
|
|
||||||
if (entry.started) {
|
|
||||||
failedAndStarted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'skipped': {
|
|
||||||
skipped++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'pending': {
|
|
||||||
pending++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1, 'Collected call');
|
|
||||||
assert.strictEqual(storage.lock.mock.calls.length, 0, 'Storage lock never called');
|
|
||||||
assert.strictEqual(storage.unlock.mock.calls.length, 0, 'Storage unlock never called');
|
|
||||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0, 'Locked call');
|
|
||||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, started, 'Started migrations');
|
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, successfulEntries.length, 'Successful migrations');
|
|
||||||
assert.strictEqual(storage.remove.mock.calls.length, started, 'Storage remove called');
|
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, failedEntries.length, 'Failed migrations');
|
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
|
||||||
assertErrorEqualEnough(error, finishedError, 'Finished error');
|
|
||||||
assert.strictEqual(entries?.length, expected.length, 'Finished entries length');
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
|
||||||
expected.map((entry) => `${entry.name} (${entry.status})`),
|
|
||||||
'Finished entries',
|
|
||||||
);
|
|
||||||
assert.strictEqual(storage.end.mock.calls.length, 1, 'Storage end called once');
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +1,31 @@
|
||||||
import path from 'node:path';
|
import process from 'node:process';
|
||||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { type MigrationMetadata, isFinishedMigration } from '@emigrate/types';
|
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||||
import {
|
import {
|
||||||
BadOptionError,
|
BadOptionError,
|
||||||
MigrationNotRunError,
|
MigrationNotRunError,
|
||||||
MigrationRemovalError,
|
|
||||||
MissingArgumentsError,
|
MissingArgumentsError,
|
||||||
MissingOptionError,
|
MissingOptionError,
|
||||||
OptionNeededError,
|
OptionNeededError,
|
||||||
StorageInitError,
|
StorageInitError,
|
||||||
toError,
|
|
||||||
} from '../errors.js';
|
} from '../errors.js';
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
|
import { getMigration } from '../get-migration.js';
|
||||||
|
import { getDuration } from '../get-duration.js';
|
||||||
import { exec } from '../exec.js';
|
import { exec } from '../exec.js';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import { collectMigrations } from '../collect-migrations.js';
|
|
||||||
import { migrationRunner } from '../migration-runner.js';
|
|
||||||
import { arrayMapAsync } from '../array-map-async.js';
|
|
||||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
|
||||||
import { getStandardReporter } from '../reporters/get.js';
|
|
||||||
|
|
||||||
type ExtraFlags = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
getMigrations?: GetMigrationsFunction;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type RemovableMigrationMetadata = MigrationMetadata & { originalStatus?: 'done' | 'failed' };
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
export default async function removeCommand(
|
export default async function removeCommand(
|
||||||
{
|
{ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force = false }: Config & ExtraFlags,
|
||||||
directory,
|
|
||||||
reporter: reporterConfig,
|
|
||||||
storage: storageConfig,
|
|
||||||
color,
|
|
||||||
cwd,
|
|
||||||
force = false,
|
|
||||||
getMigrations,
|
|
||||||
}: Config & ExtraFlags,
|
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<number> {
|
) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw MissingOptionError.fromOption('directory');
|
throw MissingOptionError.fromOption('directory');
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +40,7 @@ export default async function removeCommand(
|
||||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = getStandardReporter(reporterConfig) ?? (await getOrLoadReporter([reporterConfig]));
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw BadOptionError.fromOption(
|
throw BadOptionError.fromOption(
|
||||||
|
|
@ -73,79 +59,71 @@ export default async function removeCommand(
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
|
||||||
const collectedMigrations = arrayMapAsync(
|
|
||||||
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
|
||||||
(migration) => {
|
|
||||||
if (isFinishedMigration(migration)) {
|
|
||||||
if (migration.status === 'failed') {
|
|
||||||
const { status, duration, error, ...pendingMigration } = migration;
|
|
||||||
const removableMigration: RemovableMigrationMetadata = { ...pendingMigration, originalStatus: status };
|
|
||||||
|
|
||||||
return removableMigration;
|
if (fileError) {
|
||||||
}
|
await reporter.onFinished?.([], fileError);
|
||||||
|
|
||||||
if (migration.status === 'done') {
|
await storage.end();
|
||||||
const { status, duration, ...pendingMigration } = migration;
|
|
||||||
const removableMigration: RemovableMigrationMetadata = { ...pendingMigration, originalStatus: status };
|
|
||||||
|
|
||||||
return removableMigration;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unexpected migration status: ${migration.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return migration as RemovableMigrationMetadata;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!name.includes(path.sep)) {
|
|
||||||
name = path.join(directory, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = await migrationRunner({
|
|
||||||
dry: false,
|
|
||||||
lock: false,
|
|
||||||
name,
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
migrations: collectedMigrations,
|
|
||||||
migrationFilter(migration) {
|
|
||||||
return migration.relativeFilePath === name;
|
|
||||||
},
|
|
||||||
async validate(migration) {
|
|
||||||
if (migration.originalStatus === 'done' && !force) {
|
|
||||||
throw OptionNeededError.fromOption(
|
|
||||||
'force',
|
|
||||||
`The migration "${migration.name}" is not in a failed state. Use the "force" option to force its removal`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!migration.originalStatus) {
|
|
||||||
throw MigrationNotRunError.fromMetadata(migration);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async execute(migration) {
|
|
||||||
try {
|
|
||||||
await storage.remove(migration);
|
|
||||||
} catch (error) {
|
|
||||||
throw MigrationRemovalError.fromMetadata(migration, toError(error));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onSuccess() {
|
|
||||||
// No-op
|
|
||||||
},
|
|
||||||
async onError() {
|
|
||||||
// No-op
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return error ? 1 : 0;
|
|
||||||
} catch (error) {
|
|
||||||
await reporter.onFinished?.([], toError(error));
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
} finally {
|
|
||||||
await storage.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||||
|
let historyEntry: MigrationHistoryEntry | undefined;
|
||||||
|
let removalError: Error | undefined;
|
||||||
|
|
||||||
|
for await (const migrationHistoryEntry of storage.getHistory()) {
|
||||||
|
if (migrationHistoryEntry.name !== migrationFile.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrationHistoryEntry.status === 'done' && !force) {
|
||||||
|
removalError = OptionNeededError.fromOption(
|
||||||
|
'force',
|
||||||
|
`The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
historyEntry = migrationHistoryEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await reporter.onMigrationRemoveStart?.(migrationFile);
|
||||||
|
|
||||||
|
const start = process.hrtime();
|
||||||
|
|
||||||
|
if (historyEntry) {
|
||||||
|
try {
|
||||||
|
await storage.remove(migrationFile);
|
||||||
|
|
||||||
|
const duration = getDuration(start);
|
||||||
|
const finishedMigration: MigrationMetadataFinished = { ...migrationFile, status: 'done', duration };
|
||||||
|
|
||||||
|
await reporter.onMigrationRemoveSuccess?.(finishedMigration);
|
||||||
|
|
||||||
|
finishedMigrations.push(finishedMigration);
|
||||||
|
} catch (error) {
|
||||||
|
removalError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
}
|
||||||
|
} else if (!removalError) {
|
||||||
|
removalError = MigrationNotRunError.fromMetadata(migrationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removalError) {
|
||||||
|
const duration = getDuration(start);
|
||||||
|
const finishedMigration: MigrationMetadataFinished = {
|
||||||
|
...migrationFile,
|
||||||
|
status: 'failed',
|
||||||
|
error: removalError,
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
await reporter.onMigrationRemoveError?.(finishedMigration, removalError);
|
||||||
|
finishedMigrations.push(finishedMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
await reporter.onFinished?.(finishedMigrations, removalError);
|
||||||
|
|
||||||
|
await storage.end();
|
||||||
|
|
||||||
|
return removalError ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
import { describe, it, mock } from 'node:test';
|
import { describe, it, mock, type Mock } from 'node:test';
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import { type EmigrateReporter, type Storage, type Plugin, type MigrationMetadataFinished } from '@emigrate/types';
|
import path from 'node:path';
|
||||||
|
import {
|
||||||
|
type EmigrateReporter,
|
||||||
|
type MigrationHistoryEntry,
|
||||||
|
type MigrationMetadata,
|
||||||
|
type Storage,
|
||||||
|
type Plugin,
|
||||||
|
type SerializedError,
|
||||||
|
type FailedMigrationHistoryEntry,
|
||||||
|
type NonFailedMigrationHistoryEntry,
|
||||||
|
type MigrationMetadataFinished,
|
||||||
|
} from '@emigrate/types';
|
||||||
|
import { deserializeError, serializeError } from 'serialize-error';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import {
|
import {
|
||||||
BadOptionError,
|
BadOptionError,
|
||||||
|
|
@ -10,16 +22,13 @@ import {
|
||||||
MigrationRunError,
|
MigrationRunError,
|
||||||
StorageInitError,
|
StorageInitError,
|
||||||
} from '../errors.js';
|
} from '../errors.js';
|
||||||
import {
|
|
||||||
type Mocked,
|
|
||||||
toEntry,
|
|
||||||
toMigrations,
|
|
||||||
getMockedReporter,
|
|
||||||
getMockedStorage,
|
|
||||||
assertErrorEqualEnough,
|
|
||||||
} from '../test-utils.js';
|
|
||||||
import upCommand from './up.js';
|
import upCommand from './up.js';
|
||||||
|
|
||||||
|
type Mocked<T> = {
|
||||||
|
// @ts-expect-error - This is a mock
|
||||||
|
[K in keyof T]: Mock<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
describe('up', () => {
|
describe('up', () => {
|
||||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
||||||
const { reporter, run } = getUpCommand(['some_migration.js']);
|
const { reporter, run } = getUpCommand(['some_migration.js']);
|
||||||
|
|
@ -31,43 +40,39 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when there are no migrations to run', async () => {
|
it('returns 0 and finishes without an error when there are no migrations to run', async () => {
|
||||||
const storage = getMockedStorage([]);
|
const { reporter, run } = getUpCommand([], getStorage([]));
|
||||||
const { reporter, run } = getUpCommand([], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
|
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
|
||||||
const storage = getMockedStorage(['my_migration.js']);
|
const { reporter, run } = getUpCommand(['my_migration.js'], getStorage(['my_migration.js']));
|
||||||
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
|
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
|
||||||
const storage = getMockedStorage(['my_migration']);
|
const { reporter, run } = getUpCommand(['my_migration.js'], getStorage(['my_migration']));
|
||||||
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when all pending migrations are run successfully', async () => {
|
it('returns 0 and finishes without an error when all pending migrations are run successfully', async () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
getStorage(['some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -81,7 +86,7 @@ describe('up', () => {
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: 'some_migration.js', status: 'done', started: true },
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
{ name: 'some_other_migration.js', status: 'done', started: true },
|
{ name: 'some_other_migration.js', status: 'done', started: true },
|
||||||
]);
|
]);
|
||||||
|
|
@ -89,10 +94,9 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
||||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
||||||
storage,
|
getStorage(['some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -113,7 +117,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: false },
|
{ dry: false },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{ name: 'some_migration.js', status: 'done', started: true },
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
{ name: 'fail.js', status: 'failed', started: true, error: new Error('Oh noes!') },
|
{ name: 'fail.js', status: 'failed', started: true, error: new Error('Oh noes!') },
|
||||||
|
|
@ -125,8 +128,7 @@ describe('up', () => {
|
||||||
|
|
||||||
describe('each migration file extension needs a corresponding loader plugin', () => {
|
describe('each migration file extension needs a corresponding loader plugin', () => {
|
||||||
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
|
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
|
||||||
const storage = getMockedStorage([]);
|
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], getStorage([]));
|
||||||
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
|
|
@ -134,7 +136,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: false },
|
{ dry: false },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{ name: 'some_other.js', status: 'skipped' },
|
{ name: 'some_other.js', status: 'skipped' },
|
||||||
{
|
{
|
||||||
|
|
@ -148,8 +149,7 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin in dry-run mode as well', async () => {
|
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin in dry-run mode as well', async () => {
|
||||||
const storage = getMockedStorage([]);
|
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], getStorage([]));
|
||||||
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run({ dry: true });
|
const exitCode = await run({ dry: true });
|
||||||
|
|
||||||
|
|
@ -157,7 +157,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: true },
|
{ dry: true },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{ name: 'some_other.js', status: 'skipped' },
|
{ name: 'some_other.js', status: 'skipped' },
|
||||||
{
|
{
|
||||||
|
|
@ -174,8 +173,7 @@ describe('up', () => {
|
||||||
describe('failed migrations in the history are blocking', () => {
|
describe('failed migrations in the history are blocking', () => {
|
||||||
it('returns 1 and finishes with an error when there are failed migrations in the history', async () => {
|
it('returns 1 and finishes with an error when there are failed migrations in the history', async () => {
|
||||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||||
const storage = getMockedStorage([failedEntry]);
|
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
|
||||||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
|
|
@ -183,7 +181,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: false },
|
{ dry: false },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'some_failed_migration.js',
|
name: 'some_failed_migration.js',
|
||||||
|
|
@ -204,8 +201,7 @@ describe('up', () => {
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when there are failed migrations in the history in dry-run mode as well', async () => {
|
it('returns 1 and finishes with an error when there are failed migrations in the history in dry-run mode as well', async () => {
|
||||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||||
const storage = getMockedStorage([failedEntry]);
|
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
|
||||||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
|
||||||
|
|
||||||
const exitCode = await run({ dry: true });
|
const exitCode = await run({ dry: true });
|
||||||
|
|
||||||
|
|
@ -213,7 +209,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: true },
|
{ dry: true },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'some_failed_migration.js',
|
name: 'some_failed_migration.js',
|
||||||
|
|
@ -234,13 +229,12 @@ describe('up', () => {
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when the failed migrations in the history are not part of the current set of migrations', async () => {
|
it('returns 0 and finishes without an error when the failed migrations in the history are not part of the current set of migrations', async () => {
|
||||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||||
const storage = getMockedStorage([failedEntry]);
|
const { reporter, run } = getUpCommand([], getStorage([failedEntry]));
|
||||||
const { reporter, run } = getUpCommand([], storage);
|
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, []);
|
assertPreconditionsFulfilled({ dry: false }, reporter, []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -248,10 +242,9 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
getStorage(['some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -265,7 +258,7 @@ describe('up', () => {
|
||||||
const exitCode = await run({ limit: 1 });
|
const exitCode = await run({ limit: 1 });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: 'some_migration.js', status: 'done', started: true },
|
{ name: 'some_migration.js', status: 'done', started: true },
|
||||||
{ name: 'some_other_migration.js', status: 'skipped' },
|
{ name: 'some_other_migration.js', status: 'skipped' },
|
||||||
]);
|
]);
|
||||||
|
|
@ -274,125 +267,27 @@ describe('up', () => {
|
||||||
|
|
||||||
describe('limiting which pending migrations to run', () => {
|
describe('limiting which pending migrations to run', () => {
|
||||||
it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
|
it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
|
||||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
getStorage(['some_already_run_migration.js']),
|
||||||
);
|
);
|
||||||
|
|
||||||
const exitCode = await run({ dry: true, limit: 1 });
|
const exitCode = await run({ dry: true, limit: 1 });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
{ name: 'some_migration.js', status: 'pending' },
|
{ name: 'some_migration.js', status: 'pending' },
|
||||||
{ name: 'some_other_migration.js', status: 'skipped' },
|
{ name: 'some_other_migration.js', status: 'skipped' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully', async () => {
|
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully, even when the "from" is not an existing migration', async () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
[
|
|
||||||
'1_some_already_run_migration.js',
|
|
||||||
'2_some_migration.js',
|
|
||||||
'3_existing_migration.js',
|
|
||||||
'4_some_other_migration.js',
|
|
||||||
],
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
loadableExtensions: ['.js'],
|
|
||||||
async loadMigration() {
|
|
||||||
return migration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ from: '3_existing_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
|
||||||
{ name: '3_existing_migration.js', status: 'done', started: true },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are run successfully, when the "from" parameter is a relative path', async () => {
|
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
[
|
|
||||||
'1_some_already_run_migration.js',
|
|
||||||
'2_some_migration.js',
|
|
||||||
'3_existing_migration.js',
|
|
||||||
'4_some_other_migration.js',
|
|
||||||
],
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
loadableExtensions: ['.js'],
|
|
||||||
async loadMigration() {
|
|
||||||
return migration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ from: 'migrations/3_existing_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
|
||||||
{ name: '3_existing_migration.js', status: 'done', started: true },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 and runs all pending migrations, if "from" is an already executed migration', async () => {
|
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
|
||||||
{
|
|
||||||
loadableExtensions: ['.js'],
|
|
||||||
async loadMigration() {
|
|
||||||
return migration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ from: '1_some_already_run_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
|
||||||
{ name: '2_some_migration.js', status: 'done', started: true },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the given "from" migration name does not exist', async () => {
|
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -405,81 +300,36 @@ describe('up', () => {
|
||||||
|
|
||||||
const exitCode = await run({ from: '3_non_existing_migration.js' });
|
const exitCode = await run({ from: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
{ dry: false },
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'skipped' },
|
|
||||||
],
|
|
||||||
BadOptionError.fromOption(
|
|
||||||
'from',
|
|
||||||
'The "from" migration: "migrations/3_non_existing_migration.js" was not found',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are validated and listed successfully in dry-mode', async () => {
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '3_some_other_migration.js'],
|
|
||||||
storage,
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ dry: true, from: '3_some_other_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
{ name: '3_some_other_migration.js', status: 'pending' },
|
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
||||||
]);
|
]);
|
||||||
|
assert.strictEqual(migration.mock.calls.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when pending migrations before given "to" parameter are run successfully', async () => {
|
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are validated and listed successfully in dry-mode, even when the "from" is not an existing migration', async () => {
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
[
|
|
||||||
'1_some_already_run_migration.js',
|
|
||||||
'2_some_migration.js',
|
|
||||||
'3_existing_migration.js',
|
|
||||||
'4_some_other_migration.js',
|
|
||||||
],
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
loadableExtensions: ['.js'],
|
|
||||||
async loadMigration() {
|
|
||||||
return migration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ to: '3_existing_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
|
||||||
{ name: '2_some_migration.js', status: 'done', started: true },
|
|
||||||
{ name: '3_existing_migration.js', status: 'done', started: true },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'skipped' },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when the given "to" migration name does not exist', async () => {
|
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ dry: true, from: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
|
{ name: '4_some_other_migration.js', status: 'pending' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 and finishes without an error when pending migrations before given "to" parameter are run successfully, even when the "to" is not an existing migration', async () => {
|
||||||
|
const migration = mock.fn(async () => {
|
||||||
|
// Success
|
||||||
|
});
|
||||||
|
const { reporter, run } = getUpCommand(
|
||||||
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -492,66 +342,25 @@ describe('up', () => {
|
||||||
|
|
||||||
const exitCode = await run({ to: '3_non_existing_migration.js' });
|
const exitCode = await run({ to: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
|
||||||
assertPreconditionsFulfilled(
|
|
||||||
{ dry: false },
|
|
||||||
reporter,
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'skipped' },
|
|
||||||
],
|
|
||||||
BadOptionError.fromOption('to', 'The "to" migration: "migrations/3_non_existing_migration.js" was not found'),
|
|
||||||
);
|
|
||||||
assert.strictEqual(migration.mock.calls.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 0 and runs no migrations, if "to" is an already executed migration', async () => {
|
|
||||||
const migration = mock.fn(async () => {
|
|
||||||
// Success
|
|
||||||
});
|
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
|
||||||
storage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
loadableExtensions: ['.js'],
|
|
||||||
async loadMigration() {
|
|
||||||
return migration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitCode = await run({ to: '1_some_already_run_migration.js' });
|
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
{ name: '2_some_migration.js', status: 'done', started: true },
|
||||||
{ name: '4_some_other_migration.js', status: 'skipped' },
|
{ name: '4_some_other_migration.js', status: 'skipped' },
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(migration.mock.calls.length, 0);
|
assert.strictEqual(migration.mock.calls.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 and finishes without an error when pending migrations after given "to" parameter are validated and listed successfully in dry-mode', async () => {
|
it('returns 0 and finishes without an error when pending migrations after given "to" parameter are validated and listed successfully in dry-mode, even when the "to" is not an existing migration', async () => {
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
'1_some_already_run_migration.js',
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
'2_some_migration.js',
|
|
||||||
'3_existing_migration.js',
|
|
||||||
'4_some_other_migration.js',
|
|
||||||
],
|
|
||||||
storage,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const exitCode = await run({ dry: true, to: '3_existing_migration.js' });
|
const exitCode = await run({ dry: true, to: '3_non_existing_migration.js' });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: true }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: true }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'pending' },
|
{ name: '2_some_migration.js', status: 'pending' },
|
||||||
{ name: '3_existing_migration.js', status: 'pending' },
|
|
||||||
{ name: '4_some_other_migration.js', status: 'skipped' },
|
{ name: '4_some_other_migration.js', status: 'skipped' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -560,7 +369,6 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -570,7 +378,7 @@ describe('up', () => {
|
||||||
'5_yet_another_migration.js',
|
'5_yet_another_migration.js',
|
||||||
'6_some_more_migration.js',
|
'6_some_more_migration.js',
|
||||||
],
|
],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -584,7 +392,7 @@ describe('up', () => {
|
||||||
const exitCode = await run({ from: '3_another_migration.js', to: '5_yet_another_migration.js', limit: 2 });
|
const exitCode = await run({ from: '3_another_migration.js', to: '5_yet_another_migration.js', limit: 2 });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
{ name: '3_another_migration.js', status: 'done', started: true },
|
{ name: '3_another_migration.js', status: 'done', started: true },
|
||||||
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
||||||
|
|
@ -600,7 +408,6 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -610,7 +417,7 @@ describe('up', () => {
|
||||||
'5_yet_another_migration.js',
|
'5_yet_another_migration.js',
|
||||||
'6_some_more_migration.js',
|
'6_some_more_migration.js',
|
||||||
],
|
],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -629,7 +436,7 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
{ name: '3_another_migration.js', status: 'done', started: true },
|
{ name: '3_another_migration.js', status: 'done', started: true },
|
||||||
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
{ name: '4_some_other_migration.js', status: 'done', started: true },
|
||||||
|
|
@ -644,7 +451,6 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -654,7 +460,7 @@ describe('up', () => {
|
||||||
'5_yet_another_migration.js',
|
'5_yet_another_migration.js',
|
||||||
'6_some_more_migration.js',
|
'6_some_more_migration.js',
|
||||||
],
|
],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -673,7 +479,7 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||||
assertPreconditionsFulfilled({ dry: false }, reporter, storage, [
|
assertPreconditionsFulfilled({ dry: false }, reporter, [
|
||||||
{ name: '2_some_migration.js', status: 'skipped' },
|
{ name: '2_some_migration.js', status: 'skipped' },
|
||||||
{ name: '3_another_migration.js', status: 'done', started: true },
|
{ name: '3_another_migration.js', status: 'done', started: true },
|
||||||
{ name: '4_some_other_migration.sql', status: 'done', started: true },
|
{ name: '4_some_other_migration.sql', status: 'done', started: true },
|
||||||
|
|
@ -696,7 +502,6 @@ describe('up', () => {
|
||||||
},
|
},
|
||||||
{ times: 1 },
|
{ times: 1 },
|
||||||
);
|
);
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -706,7 +511,7 @@ describe('up', () => {
|
||||||
'5_yet_another_migration.js',
|
'5_yet_another_migration.js',
|
||||||
'6_some_more_migration.js',
|
'6_some_more_migration.js',
|
||||||
],
|
],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -725,7 +530,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: false },
|
{ dry: false },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{ name: '2_some_migration.js', status: 'done', started: true },
|
{ name: '2_some_migration.js', status: 'done', started: true },
|
||||||
{ name: '3_another_migration.js', status: 'done', started: true },
|
{ name: '3_another_migration.js', status: 'done', started: true },
|
||||||
|
|
@ -755,7 +559,6 @@ describe('up', () => {
|
||||||
},
|
},
|
||||||
{ times: 1 },
|
{ times: 1 },
|
||||||
);
|
);
|
||||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -765,7 +568,7 @@ describe('up', () => {
|
||||||
'5_yet_another_migration.js',
|
'5_yet_another_migration.js',
|
||||||
'6_some_more_migration.js',
|
'6_some_more_migration.js',
|
||||||
],
|
],
|
||||||
storage,
|
getStorage(['1_some_already_run_migration.js']),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
loadableExtensions: ['.js'],
|
loadableExtensions: ['.js'],
|
||||||
|
|
@ -785,7 +588,6 @@ describe('up', () => {
|
||||||
assertPreconditionsFulfilled(
|
assertPreconditionsFulfilled(
|
||||||
{ dry: false },
|
{ dry: false },
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
|
||||||
[
|
[
|
||||||
{ name: '2_some_migration.js', status: 'done', started: true },
|
{ name: '2_some_migration.js', status: 'done', started: true },
|
||||||
{
|
{
|
||||||
|
|
@ -805,8 +607,105 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getErrorCause(error: Error | undefined): Error | SerializedError | undefined {
|
||||||
|
if (error?.cause instanceof Error) {
|
||||||
|
return error.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error?.cause === 'object' && error.cause !== null) {
|
||||||
|
return error.cause as unknown as SerializedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
filePath: `${cwd}/${directory}/${name}`,
|
||||||
|
relativeFilePath: `${directory}/${name}`,
|
||||||
|
extension: path.extname(name),
|
||||||
|
directory,
|
||||||
|
cwd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMigrations(cwd: string, directory: string, names: string[]): MigrationMetadata[] {
|
||||||
|
return names.map((name) => toMigration(cwd, directory, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntry(name: MigrationHistoryEntry): MigrationHistoryEntry;
|
||||||
|
function toEntry<S extends MigrationHistoryEntry['status']>(
|
||||||
|
name: string,
|
||||||
|
status?: S,
|
||||||
|
): S extends 'failed' ? FailedMigrationHistoryEntry : NonFailedMigrationHistoryEntry;
|
||||||
|
|
||||||
|
function toEntry(name: string | MigrationHistoryEntry, status?: 'done' | 'failed'): MigrationHistoryEntry {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
return name.status === 'failed' ? name : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
date: new Date(),
|
||||||
|
error: { name: 'Error', message: 'Failed' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
status: status ?? 'done',
|
||||||
|
date: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntries(
|
||||||
|
names: Array<string | MigrationHistoryEntry>,
|
||||||
|
status?: MigrationHistoryEntry['status'],
|
||||||
|
): MigrationHistoryEntry[] {
|
||||||
|
return names.map((name) => (typeof name === 'string' ? toEntry(name, status) : name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function noop() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStorage(historyEntries: Array<string | MigrationHistoryEntry>) {
|
||||||
|
const storage: Mocked<Storage> = {
|
||||||
|
lock: mock.fn(async (migrations) => migrations),
|
||||||
|
unlock: mock.fn(async () => {
|
||||||
|
// void
|
||||||
|
}),
|
||||||
|
getHistory: mock.fn(async function* () {
|
||||||
|
yield* toEntries(historyEntries);
|
||||||
|
}),
|
||||||
|
remove: mock.fn(),
|
||||||
|
onSuccess: mock.fn(),
|
||||||
|
onError: mock.fn(),
|
||||||
|
end: mock.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
||||||
const reporter = getMockedReporter();
|
const reporter: Mocked<Required<EmigrateReporter>> = {
|
||||||
|
onFinished: mock.fn(noop),
|
||||||
|
onInit: mock.fn(noop),
|
||||||
|
onAbort: mock.fn(noop),
|
||||||
|
onCollectedMigrations: mock.fn(noop),
|
||||||
|
onLockedMigrations: mock.fn(noop),
|
||||||
|
onNewMigration: mock.fn(noop),
|
||||||
|
onMigrationRemoveStart: mock.fn(noop),
|
||||||
|
onMigrationRemoveSuccess: mock.fn(noop),
|
||||||
|
onMigrationRemoveError: mock.fn(noop),
|
||||||
|
onMigrationStart: mock.fn(noop),
|
||||||
|
onMigrationSuccess: mock.fn(noop),
|
||||||
|
onMigrationError: mock.fn(noop),
|
||||||
|
onMigrationSkip: mock.fn(noop),
|
||||||
|
};
|
||||||
|
|
||||||
const run = async (
|
const run = async (
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
|
|
@ -845,7 +744,6 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
|
||||||
function assertPreconditionsFulfilled(
|
function assertPreconditionsFulfilled(
|
||||||
options: { dry: boolean },
|
options: { dry: boolean },
|
||||||
reporter: Mocked<Required<EmigrateReporter>>,
|
reporter: Mocked<Required<EmigrateReporter>>,
|
||||||
storage: Mocked<Storage>,
|
|
||||||
expected: Array<{ name: string; status: MigrationMetadataFinished['status']; started?: boolean; error?: Error }>,
|
expected: Array<{ name: string; status: MigrationMetadataFinished['status']; started?: boolean; error?: Error }>,
|
||||||
finishedError?: Error,
|
finishedError?: Error,
|
||||||
) {
|
) {
|
||||||
|
|
@ -866,9 +764,7 @@ function assertPreconditionsFulfilled(
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
let pending = 0;
|
let pending = 0;
|
||||||
let failedAndStarted = 0;
|
|
||||||
const failedEntries: typeof expected = [];
|
const failedEntries: typeof expected = [];
|
||||||
const successfulEntries: typeof expected = [];
|
|
||||||
|
|
||||||
for (const entry of expected) {
|
for (const entry of expected) {
|
||||||
if (entry.started) {
|
if (entry.started) {
|
||||||
|
|
@ -879,22 +775,12 @@ function assertPreconditionsFulfilled(
|
||||||
switch (entry.status) {
|
switch (entry.status) {
|
||||||
case 'done': {
|
case 'done': {
|
||||||
done++;
|
done++;
|
||||||
|
|
||||||
if (entry.started) {
|
|
||||||
successfulEntries.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'failed': {
|
case 'failed': {
|
||||||
failed++;
|
failed++;
|
||||||
failedEntries.push(entry);
|
failedEntries.push(entry);
|
||||||
|
|
||||||
if (entry.started) {
|
|
||||||
failedAndStarted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -916,41 +802,42 @@ function assertPreconditionsFulfilled(
|
||||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, done, 'Successful migrations');
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, done, 'Successful migrations');
|
||||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, failed, 'Failed migrations');
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, failed, 'Failed migrations');
|
||||||
|
|
||||||
assert.strictEqual(storage.onSuccess.mock.calls.length, successfulEntries.length, 'Storage onSuccess calls');
|
|
||||||
assert.strictEqual(storage.onError.mock.calls.length, failedAndStarted, 'Storage onError calls');
|
|
||||||
|
|
||||||
for (const [index, entry] of failedEntries.entries()) {
|
for (const [index, entry] of failedEntries.entries()) {
|
||||||
if (entry.status === 'failed') {
|
if (entry.status === 'failed') {
|
||||||
const error = reporter.onMigrationError.mock.calls[index]?.arguments[1];
|
const error = reporter.onMigrationError.mock.calls[index]?.arguments[1];
|
||||||
assertErrorEqualEnough(error, entry.error, 'Error');
|
assert.deepStrictEqual(error, entry.error, 'Error');
|
||||||
|
const cause = entry.error?.cause;
|
||||||
if (entry.started) {
|
assert.deepStrictEqual(error?.cause, cause ? deserializeError(cause) : cause, 'Error cause');
|
||||||
const [finishedMigration, error] = storage.onError.mock.calls[index]?.arguments ?? [];
|
|
||||||
assert.strictEqual(finishedMigration?.name, entry.name);
|
|
||||||
assert.strictEqual(finishedMigration?.status, entry.status);
|
|
||||||
assertErrorEqualEnough(error, entry.error, `Entry error (${entry.name})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, pending + skipped, 'Total pending and skipped');
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, pending + skipped, 'Total pending and skipped');
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
assertErrorEqualEnough(error, finishedError, 'Finished error');
|
if (finishedError instanceof DOMException || error instanceof DOMException) {
|
||||||
|
// The assert library doesn't support DOMException apparently, so ugly workaround here:
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
deserializeError(serializeError(error)),
|
||||||
|
deserializeError(serializeError(finishedError)),
|
||||||
|
'Finished error',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cause = getErrorCause(error);
|
||||||
|
const expectedCause = finishedError?.cause;
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
cause,
|
||||||
|
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||||
|
'Finished error cause',
|
||||||
|
);
|
||||||
assert.strictEqual(entries?.length, expected.length, 'Finished entries length');
|
assert.strictEqual(entries?.length, expected.length, 'Finished entries length');
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
expected.map((entry) => `${entry.name} (${entry.status})`),
|
expected.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
'Finished entries',
|
'Finished entries',
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [index, entry] of successfulEntries.entries()) {
|
|
||||||
const [finishedMigration] = storage.onSuccess.mock.calls[index]?.arguments ?? [];
|
|
||||||
assert.strictEqual(finishedMigration?.name, entry.name);
|
|
||||||
assert.strictEqual(finishedMigration?.status, entry.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.strictEqual(storage.end.mock.calls.length, 1, 'Storage end should always be called');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertPreconditionsFailed(
|
function assertPreconditionsFailed(
|
||||||
|
|
@ -977,6 +864,13 @@ function assertPreconditionsFailed(
|
||||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
||||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
assertErrorEqualEnough(error, finishedError, 'Finished error');
|
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||||
|
const cause = getErrorCause(error);
|
||||||
|
const expectedCause = finishedError?.cause;
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
cause,
|
||||||
|
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||||
|
'Finished error cause',
|
||||||
|
);
|
||||||
assert.strictEqual(entries?.length, 0, 'Finished entries length');
|
assert.strictEqual(entries?.length, 0, 'Finished entries length');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
import path from 'node:path';
|
|
||||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
||||||
import {
|
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||||
BadOptionError,
|
|
||||||
MigrationLoadError,
|
|
||||||
MissingOptionError,
|
|
||||||
StorageInitError,
|
|
||||||
toError,
|
|
||||||
toSerializedError,
|
|
||||||
} from '../errors.js';
|
|
||||||
import { type Config } from '../types.js';
|
import { type Config } from '../types.js';
|
||||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||||
import { exec } from '../exec.js';
|
import { exec } from '../exec.js';
|
||||||
import { migrationRunner } from '../migration-runner.js';
|
import { migrationRunner } from '../migration-runner.js';
|
||||||
|
import { filterAsync } from '../filter-async.js';
|
||||||
import { collectMigrations } from '../collect-migrations.js';
|
import { collectMigrations } from '../collect-migrations.js';
|
||||||
|
import { arrayFromAsync } from '../array-from-async.js';
|
||||||
import { version } from '../get-package-info.js';
|
import { version } from '../get-package-info.js';
|
||||||
import { getStandardReporter } from '../reporters/get.js';
|
|
||||||
|
|
||||||
type ExtraFlags = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
@ -30,6 +23,7 @@ type ExtraFlags = {
|
||||||
abortRespite?: number;
|
abortRespite?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
const lazyPluginLoaderJs = async () => import('../plugin-loader-js.js');
|
const lazyPluginLoaderJs = async () => import('../plugin-loader-js.js');
|
||||||
|
|
||||||
export default async function upCommand({
|
export default async function upCommand({
|
||||||
|
|
@ -58,7 +52,7 @@ export default async function upCommand({
|
||||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporter = getStandardReporter(reporterConfig) ?? (await getOrLoadReporter([reporterConfig]));
|
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||||
|
|
||||||
if (!reporter) {
|
if (!reporter) {
|
||||||
throw BadOptionError.fromOption(
|
throw BadOptionError.fromOption(
|
||||||
|
|
@ -78,7 +72,10 @@ export default async function upCommand({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory(), getMigrations);
|
const collectedMigrations = filterAsync(
|
||||||
|
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
||||||
|
(migration) => !isFinishedMigration(migration) || migration.status === 'failed',
|
||||||
|
);
|
||||||
|
|
||||||
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]);
|
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]);
|
||||||
|
|
||||||
|
|
@ -96,14 +93,6 @@ export default async function upCommand({
|
||||||
return loaderByExtension.get(extension);
|
return loaderByExtension.get(extension);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (from && !from.includes(path.sep)) {
|
|
||||||
from = path.join(directory, from);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to && !to.includes(path.sep)) {
|
|
||||||
to = path.join(directory, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = await migrationRunner({
|
const error = await migrationRunner({
|
||||||
dry,
|
dry,
|
||||||
limit,
|
limit,
|
||||||
|
|
@ -113,10 +102,7 @@ export default async function upCommand({
|
||||||
abortRespite,
|
abortRespite,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations: collectedMigrations,
|
migrations: await arrayFromAsync(collectedMigrations),
|
||||||
migrationFilter(migration) {
|
|
||||||
return !isFinishedMigration(migration) || migration.status === 'failed';
|
|
||||||
},
|
|
||||||
async validate(migration) {
|
async validate(migration) {
|
||||||
if (noExecution) {
|
if (noExecution) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -145,12 +131,6 @@ export default async function upCommand({
|
||||||
|
|
||||||
await migrationFunction();
|
await migrationFunction();
|
||||||
},
|
},
|
||||||
async onSuccess(migration) {
|
|
||||||
await storage.onSuccess(migration);
|
|
||||||
},
|
|
||||||
async onError(migration, error) {
|
|
||||||
await storage.onError(migration, toSerializedError(error));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return error ? 1 : 0;
|
return error ? 1 : 0;
|
||||||
|
|
|
||||||
6
packages/cli/src/deno.d.ts
vendored
6
packages/cli/src/deno.d.ts
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const Deno: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { serializeError, errorConstructors, deserializeError } from 'serialize-e
|
||||||
|
|
||||||
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
|
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
|
||||||
|
|
||||||
export const toError = (error: unknown): Error => (error instanceof Error ? error : new Error(String(error)));
|
export const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
||||||
|
|
||||||
export const toSerializedError = (error: unknown) => {
|
export const toSerializedError = (error: unknown) => {
|
||||||
const errorInstance = toError(error);
|
const errorInstance = toError(error);
|
||||||
|
|
@ -23,14 +23,13 @@ export class EmigrateError extends Error {
|
||||||
public code?: string,
|
public code?: string,
|
||||||
) {
|
) {
|
||||||
super(message, options);
|
super(message, options);
|
||||||
this.name = this.constructor.name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShowUsageError extends EmigrateError {}
|
export class ShowUsageError extends EmigrateError {}
|
||||||
|
|
||||||
export class MissingOptionError extends ShowUsageError {
|
export class MissingOptionError extends ShowUsageError {
|
||||||
static fromOption(option: string | string[]): MissingOptionError {
|
static fromOption(option: string | string[]) {
|
||||||
return new MissingOptionError(
|
return new MissingOptionError(
|
||||||
`Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`,
|
`Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
@ -48,7 +47,7 @@ export class MissingOptionError extends ShowUsageError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MissingArgumentsError extends ShowUsageError {
|
export class MissingArgumentsError extends ShowUsageError {
|
||||||
static fromArgument(argument: string): MissingArgumentsError {
|
static fromArgument(argument: string) {
|
||||||
return new MissingArgumentsError(`Missing required argument: ${argument}`, undefined, argument);
|
return new MissingArgumentsError(`Missing required argument: ${argument}`, undefined, argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +61,7 @@ export class MissingArgumentsError extends ShowUsageError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OptionNeededError extends ShowUsageError {
|
export class OptionNeededError extends ShowUsageError {
|
||||||
static fromOption(option: string, message: string): OptionNeededError {
|
static fromOption(option: string, message: string) {
|
||||||
return new OptionNeededError(message, undefined, option);
|
return new OptionNeededError(message, undefined, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +75,7 @@ export class OptionNeededError extends ShowUsageError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadOptionError extends ShowUsageError {
|
export class BadOptionError extends ShowUsageError {
|
||||||
static fromOption(option: string, message: string): BadOptionError {
|
static fromOption(option: string, message: string) {
|
||||||
return new BadOptionError(message, undefined, option);
|
return new BadOptionError(message, undefined, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +95,7 @@ export class UnexpectedError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationHistoryError extends EmigrateError {
|
export class MigrationHistoryError extends EmigrateError {
|
||||||
static fromHistoryEntry(entry: FailedMigrationHistoryEntry): MigrationHistoryError {
|
static fromHistoryEntry(entry: FailedMigrationHistoryEntry) {
|
||||||
return new MigrationHistoryError(`Migration ${entry.name} is in a failed state, it should be fixed and removed`, {
|
return new MigrationHistoryError(`Migration ${entry.name} is in a failed state, it should be fixed and removed`, {
|
||||||
cause: deserializeError(entry.error),
|
cause: deserializeError(entry.error),
|
||||||
});
|
});
|
||||||
|
|
@ -108,7 +107,7 @@ export class MigrationHistoryError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationLoadError extends EmigrateError {
|
export class MigrationLoadError extends EmigrateError {
|
||||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error): MigrationLoadError {
|
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||||
return new MigrationLoadError(`Failed to load migration file: ${metadata.relativeFilePath}`, { cause });
|
return new MigrationLoadError(`Failed to load migration file: ${metadata.relativeFilePath}`, { cause });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +117,7 @@ export class MigrationLoadError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationRunError extends EmigrateError {
|
export class MigrationRunError extends EmigrateError {
|
||||||
static fromMetadata(metadata: FailedMigrationMetadata): MigrationRunError {
|
static fromMetadata(metadata: FailedMigrationMetadata) {
|
||||||
return new MigrationRunError(`Failed to run migration: ${metadata.relativeFilePath}`, { cause: metadata.error });
|
return new MigrationRunError(`Failed to run migration: ${metadata.relativeFilePath}`, { cause: metadata.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +127,7 @@ export class MigrationRunError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationNotRunError extends EmigrateError {
|
export class MigrationNotRunError extends EmigrateError {
|
||||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error): MigrationNotRunError {
|
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||||
return new MigrationNotRunError(`Migration "${metadata.name}" is not in the migration history`, { cause });
|
return new MigrationNotRunError(`Migration "${metadata.name}" is not in the migration history`, { cause });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,18 +136,8 @@ export class MigrationNotRunError extends EmigrateError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MigrationRemovalError extends EmigrateError {
|
|
||||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error): MigrationRemovalError {
|
|
||||||
return new MigrationRemovalError(`Failed to remove migration: ${metadata.relativeFilePath}`, { cause });
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
|
||||||
super(message, options, 'ERR_MIGRATION_REMOVE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StorageInitError extends EmigrateError {
|
export class StorageInitError extends EmigrateError {
|
||||||
static fromError(error: Error): StorageInitError {
|
static fromError(error: Error) {
|
||||||
return new StorageInitError('Could not initialize storage', { cause: error });
|
return new StorageInitError('Could not initialize storage', { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,11 +147,11 @@ export class StorageInitError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandAbortError extends EmigrateError {
|
export class CommandAbortError extends EmigrateError {
|
||||||
static fromSignal(signal: NodeJS.Signals): CommandAbortError {
|
static fromSignal(signal: NodeJS.Signals) {
|
||||||
return new CommandAbortError(`Command aborted due to signal: ${signal}`);
|
return new CommandAbortError(`Command aborted due to signal: ${signal}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromReason(reason: string, cause?: unknown): CommandAbortError {
|
static fromReason(reason: string, cause?: unknown) {
|
||||||
return new CommandAbortError(`Command aborted: ${reason}`, { cause });
|
return new CommandAbortError(`Command aborted: ${reason}`, { cause });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +161,7 @@ export class CommandAbortError extends EmigrateError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionDesertedError extends EmigrateError {
|
export class ExecutionDesertedError extends EmigrateError {
|
||||||
static fromReason(reason: string, cause?: Error): ExecutionDesertedError {
|
static fromReason(reason: string, cause?: Error) {
|
||||||
return new ExecutionDesertedError(`Execution deserted: ${reason}`, { cause });
|
return new ExecutionDesertedError(`Execution deserted: ${reason}`, { cause });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +181,6 @@ errorConstructors.set('MigrationHistoryError', MigrationHistoryError as unknown
|
||||||
errorConstructors.set('MigrationLoadError', MigrationLoadError as unknown as ErrorConstructor);
|
errorConstructors.set('MigrationLoadError', MigrationLoadError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('MigrationNotRunError', MigrationNotRunError as unknown as ErrorConstructor);
|
errorConstructors.set('MigrationNotRunError', MigrationNotRunError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('MigrationRemovalError', MigrationRemovalError as unknown as ErrorConstructor);
|
|
||||||
errorConstructors.set('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
errorConstructors.set('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('CommandAbortError', CommandAbortError as unknown as ErrorConstructor);
|
errorConstructors.set('CommandAbortError', CommandAbortError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError as unknown as ErrorConstructor);
|
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError as unknown as ErrorConstructor);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { setTimeout } from 'node:timers';
|
|
||||||
import prettyMs from 'pretty-ms';
|
import prettyMs from 'pretty-ms';
|
||||||
import { ExecutionDesertedError, toError } from './errors.js';
|
import { ExecutionDesertedError, toError } from './errors.js';
|
||||||
import { DEFAULT_RESPITE_SECONDS } from './defaults.js';
|
import { DEFAULT_RESPITE_SECONDS } from './defaults.js';
|
||||||
|
|
@ -28,8 +27,6 @@ export const exec = async <Return extends Promise<any>>(
|
||||||
const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined;
|
const aborter = options.abortSignal ? getAborter(options.abortSignal, options.abortRespite) : undefined;
|
||||||
const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]);
|
const result = await Promise.race(aborter ? [aborter, fn()] : [fn()]);
|
||||||
|
|
||||||
aborter?.cancel();
|
|
||||||
|
|
||||||
return [result, undefined];
|
return [result, undefined];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [undefined, toError(error)];
|
return [undefined, toError(error)];
|
||||||
|
|
@ -42,44 +39,27 @@ export const exec = async <Return extends Promise<any>>(
|
||||||
* @param signal The abort signal to listen to
|
* @param signal The abort signal to listen to
|
||||||
* @param respite The time in milliseconds to wait before rejecting
|
* @param respite The time in milliseconds to wait before rejecting
|
||||||
*/
|
*/
|
||||||
const getAborter = (
|
const getAborter = async (signal: AbortSignal, respite = DEFAULT_RESPITE_SECONDS * 1000): Promise<never> => {
|
||||||
signal: AbortSignal,
|
return new Promise((_, reject) => {
|
||||||
respite = DEFAULT_RESPITE_SECONDS * 1000,
|
if (signal.aborted) {
|
||||||
): PromiseLike<never> & { cancel: () => void } => {
|
setTimeout(
|
||||||
const cleanups: Array<() => void> = [];
|
|
||||||
|
|
||||||
const aborter = new Promise<never>((_, reject) => {
|
|
||||||
const abortListener = () => {
|
|
||||||
const timer = setTimeout(
|
|
||||||
reject,
|
reject,
|
||||||
respite,
|
respite,
|
||||||
ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)),
|
ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)),
|
||||||
);
|
).unref();
|
||||||
timer.unref();
|
|
||||||
cleanups.push(() => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signal.aborted) {
|
|
||||||
abortListener();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
signal.addEventListener('abort', abortListener, { once: true });
|
signal.addEventListener(
|
||||||
|
'abort',
|
||||||
cleanups.push(() => {
|
() => {
|
||||||
signal.removeEventListener('abort', abortListener);
|
setTimeout(
|
||||||
});
|
reject,
|
||||||
|
respite,
|
||||||
|
ExecutionDesertedError.fromReason(`Deserted after ${prettyMs(respite)}`, toError(signal.reason)),
|
||||||
|
).unref();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
for (const cleanup of cleanups) {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanups.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return Object.assign(aborter, { cancel });
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
13
packages/cli/src/filter-async.ts
Normal file
13
packages/cli/src/filter-async.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function filterAsync<T, S extends T>(
|
||||||
|
iterable: AsyncIterable<T>,
|
||||||
|
filter: (item: T) => item is S,
|
||||||
|
): AsyncIterable<S>;
|
||||||
|
export function filterAsync<T>(iterable: AsyncIterable<T>, filter: (item: T) => unknown): AsyncIterable<T>;
|
||||||
|
|
||||||
|
export async function* filterAsync<T>(iterable: AsyncIterable<T>, filter: (item: T) => unknown): AsyncIterable<T> {
|
||||||
|
for await (const item of iterable) {
|
||||||
|
if (filter(item)) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,11 @@
|
||||||
import process from 'node:process';
|
import { cosmiconfig } from 'cosmiconfig';
|
||||||
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
|
|
||||||
import { type Config, type EmigrateConfig } from './types.js';
|
import { type Config, type EmigrateConfig } from './types.js';
|
||||||
|
|
||||||
const commands = ['up', 'list', 'new', 'remove'] as const;
|
const commands = ['up', 'list', 'new', 'remove'] as const;
|
||||||
type Command = (typeof commands)[number];
|
type Command = (typeof commands)[number];
|
||||||
const canImportTypeScriptAsIs = Boolean(process.isBun) || typeof Deno !== 'undefined';
|
|
||||||
|
|
||||||
const getEmigrateConfig = (config: any): EmigrateConfig => {
|
export const getConfig = async (command: Command): Promise<Config> => {
|
||||||
if ('default' in config && typeof config.default === 'object' && config.default !== null) {
|
const explorer = cosmiconfig('emigrate');
|
||||||
return config.default as EmigrateConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof config === 'object' && config !== null) {
|
|
||||||
return config as EmigrateConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getConfig = async (command: Command, forceImportTypeScriptAsIs = false): Promise<Config> => {
|
|
||||||
const explorer = cosmiconfig('emigrate', {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
loaders: forceImportTypeScriptAsIs || canImportTypeScriptAsIs ? { '.ts': defaultLoaders['.js'] } : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await explorer.search();
|
const result = await explorer.search();
|
||||||
|
|
||||||
|
|
@ -30,7 +13,7 @@ export const getConfig = async (command: Command, forceImportTypeScriptAsIs = fa
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = getEmigrateConfig(result.config);
|
const config = result.config as EmigrateConfig;
|
||||||
|
|
||||||
const commandConfig = config[command];
|
const commandConfig = config[command];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
export const getDuration = (start: [number, number]): number => {
|
export const getDuration = (start: [number, number]) => {
|
||||||
const [seconds, nanoseconds] = process.hrtime(start);
|
const [seconds, nanoseconds] = process.hrtime(start);
|
||||||
return seconds * 1000 + nanoseconds / 1_000_000;
|
return seconds * 1000 + nanoseconds / 1_000_000;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import { getMigrations } from './get-migrations.js';
|
|
||||||
|
|
||||||
const originalOpendir = fs.opendir;
|
|
||||||
const opendirMock = mock.fn(originalOpendir);
|
|
||||||
|
|
||||||
describe('get-migrations', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
fs.opendir = opendirMock;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
opendirMock.mock.restore();
|
|
||||||
fs.opendir = originalOpendir;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip files with leading periods', async () => {
|
|
||||||
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');
|
|
||||||
|
|
||||||
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 files with leading underscores', async () => {
|
|
||||||
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');
|
|
||||||
|
|
||||||
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 files without file extensions', async () => {
|
|
||||||
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');
|
|
||||||
|
|
||||||
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 sort them in lexicographical order', async () => {
|
|
||||||
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');
|
|
||||||
|
|
||||||
assert.deepStrictEqual(migrations, [
|
|
||||||
{
|
|
||||||
name: 'bar.js',
|
|
||||||
filePath: '/cwd/directory/bar.js',
|
|
||||||
relativeFilePath: 'directory/bar.js',
|
|
||||||
extension: '.js',
|
|
||||||
directory: 'directory',
|
|
||||||
cwd: '/cwd/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'bar_data.js',
|
|
||||||
filePath: '/cwd/directory/bar_data.js',
|
|
||||||
relativeFilePath: 'directory/bar_data.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/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'foo.js',
|
|
||||||
filePath: '/cwd/directory/foo.js',
|
|
||||||
relativeFilePath: 'directory/foo.js',
|
|
||||||
extension: '.js',
|
|
||||||
directory: 'directory',
|
|
||||||
cwd: '/cwd/',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,44 +1,45 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
import { type Dirent } from 'node:fs';
|
||||||
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;
|
||||||
|
|
||||||
async function* tryReadDirectory(directoryPath: string): AsyncIterable<string> {
|
const tryReadDirectory = async (directoryPath: string): Promise<Dirent[]> => {
|
||||||
try {
|
try {
|
||||||
for await (const entry of await fs.opendir(directoryPath)) {
|
return await fs.readdir(directoryPath, {
|
||||||
if (
|
withFileTypes: true,
|
||||||
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 arrayFromAsync(tryReadDirectory(directoryPath));
|
const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath);
|
||||||
|
|
||||||
return allFilesInMigrationDirectory.sort().map((name) => {
|
const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory
|
||||||
const filePath = path.join(directoryPath, name);
|
.filter(
|
||||||
|
(file) =>
|
||||||
|
file.isFile() && !file.name.startsWith('.') && !file.name.startsWith('_') && path.extname(file.name) !== '',
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return migrationFiles;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,4 @@ const getPackageInfo = async () => {
|
||||||
throw new UnexpectedError(`Could not read package info from: ${packageInfoPath}`);
|
throw new UnexpectedError(`Could not read package info from: ${packageInfoPath}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const packageInfo = await getPackageInfo();
|
export const { version } = await getPackageInfo();
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const version: string = packageInfo.version;
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
|
|
||||||
export const emigrate = (): void => {
|
export const emigrate = () => {
|
||||||
// console.log('Done!');
|
// console.log('Done!');
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,34 +9,27 @@ import {
|
||||||
type FailedMigrationMetadata,
|
type FailedMigrationMetadata,
|
||||||
type SuccessfulMigrationMetadata,
|
type SuccessfulMigrationMetadata,
|
||||||
} from '@emigrate/types';
|
} from '@emigrate/types';
|
||||||
import { toError, EmigrateError, MigrationRunError, BadOptionError } from './errors.js';
|
import { toError, EmigrateError, MigrationRunError, toSerializedError } from './errors.js';
|
||||||
import { exec } from './exec.js';
|
import { exec } from './exec.js';
|
||||||
import { getDuration } from './get-duration.js';
|
import { getDuration } from './get-duration.js';
|
||||||
|
|
||||||
type MigrationRunnerParameters<T extends MigrationMetadata | MigrationMetadataFinished> = {
|
type MigrationRunnerParameters = {
|
||||||
dry: boolean;
|
dry: boolean;
|
||||||
lock?: boolean;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
name?: string;
|
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
abortRespite?: number;
|
abortRespite?: number;
|
||||||
reporter: EmigrateReporter;
|
reporter: EmigrateReporter;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
migrations: AsyncIterable<T>;
|
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
|
||||||
migrationFilter?: (migration: T) => boolean;
|
validate: (migration: MigrationMetadata) => Promise<void>;
|
||||||
validate: (migration: T) => Promise<void>;
|
execute: (migration: MigrationMetadata) => Promise<void>;
|
||||||
execute: (migration: T) => Promise<void>;
|
|
||||||
onSuccess: (migration: SuccessfulMigrationMetadata) => Promise<void>;
|
|
||||||
onError: (migration: FailedMigrationMetadata, error: Error) => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const migrationRunner = async <T extends MigrationMetadata | MigrationMetadataFinished>({
|
export const migrationRunner = async ({
|
||||||
dry,
|
dry,
|
||||||
lock = true,
|
|
||||||
limit,
|
limit,
|
||||||
name,
|
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
|
@ -46,10 +39,9 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
migrations,
|
migrations,
|
||||||
validate,
|
validate,
|
||||||
execute,
|
execute,
|
||||||
onSuccess,
|
}: MigrationRunnerParameters): Promise<Error | undefined> => {
|
||||||
onError,
|
await reporter.onCollectedMigrations?.(migrations);
|
||||||
migrationFilter = () => true,
|
|
||||||
}: MigrationRunnerParameters<T>): Promise<Error | undefined> => {
|
|
||||||
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||||
const migrationsToLock: MigrationMetadata[] = [];
|
const migrationsToLock: MigrationMetadata[] = [];
|
||||||
|
|
||||||
|
|
@ -71,35 +63,15 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
let nameFound = false;
|
|
||||||
let fromFound = false;
|
|
||||||
let toFound = false;
|
|
||||||
|
|
||||||
for await (const migration of migrations) {
|
for await (const migration of migrations) {
|
||||||
if (name && migration.relativeFilePath === name) {
|
|
||||||
nameFound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (from && migration.relativeFilePath === from) {
|
|
||||||
fromFound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to && migration.relativeFilePath === to) {
|
|
||||||
toFound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!migrationFilter(migration)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFinishedMigration(migration)) {
|
if (isFinishedMigration(migration)) {
|
||||||
skip ||= migration.status === 'failed' || migration.status === 'skipped';
|
skip ||= migration.status === 'failed' || migration.status === 'skipped';
|
||||||
|
|
||||||
validatedMigrations.push(migration);
|
validatedMigrations.push(migration);
|
||||||
} else if (
|
} else if (
|
||||||
skip ||
|
skip ||
|
||||||
Boolean(from && migration.relativeFilePath < from) ||
|
Boolean(from && migration.name < from) ||
|
||||||
Boolean(to && migration.relativeFilePath > to) ||
|
Boolean(to && migration.name > to) ||
|
||||||
(limit && migrationsToLock.length >= limit)
|
(limit && migrationsToLock.length >= limit)
|
||||||
) {
|
) {
|
||||||
validatedMigrations.push({
|
validatedMigrations.push({
|
||||||
|
|
@ -135,38 +107,9 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await reporter.onCollectedMigrations?.(validatedMigrations);
|
const [lockedMigrations, lockError] = dry
|
||||||
|
? [migrationsToLock]
|
||||||
let optionError: Error | undefined;
|
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
||||||
|
|
||||||
if (name && !nameFound) {
|
|
||||||
optionError = BadOptionError.fromOption('name', `The migration: "${name}" was not found`);
|
|
||||||
} else if (from && !fromFound) {
|
|
||||||
optionError = BadOptionError.fromOption('from', `The "from" migration: "${from}" was not found`);
|
|
||||||
} else if (to && !toFound) {
|
|
||||||
optionError = BadOptionError.fromOption('to', `The "to" migration: "${to}" was not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (optionError) {
|
|
||||||
dry = true;
|
|
||||||
skip = true;
|
|
||||||
|
|
||||||
for (const migration of migrationsToLock) {
|
|
||||||
const validatedIndex = validatedMigrations.indexOf(migration);
|
|
||||||
|
|
||||||
validatedMigrations[validatedIndex] = {
|
|
||||||
...migration,
|
|
||||||
status: 'skipped',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
migrationsToLock.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [lockedMigrations, lockError] =
|
|
||||||
dry || !lock
|
|
||||||
? [migrationsToLock]
|
|
||||||
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
|
||||||
|
|
||||||
if (lockError) {
|
if (lockError) {
|
||||||
for (const migration of migrationsToLock) {
|
for (const migration of migrationsToLock) {
|
||||||
|
|
@ -181,7 +124,7 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
migrationsToLock.length = 0;
|
migrationsToLock.length = 0;
|
||||||
|
|
||||||
skip = true;
|
skip = true;
|
||||||
} else if (lock) {
|
} else {
|
||||||
for (const migration of migrationsToLock) {
|
for (const migration of migrationsToLock) {
|
||||||
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
||||||
|
|
||||||
|
|
@ -244,7 +187,7 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
|
|
||||||
const start = hrtime();
|
const start = hrtime();
|
||||||
|
|
||||||
const [, migrationError] = await exec(async () => execute(migration as T), { abortSignal, abortRespite });
|
const [, migrationError] = await exec(async () => execute(migration), { abortSignal, abortRespite });
|
||||||
|
|
||||||
const duration = getDuration(start);
|
const duration = getDuration(start);
|
||||||
|
|
||||||
|
|
@ -255,7 +198,7 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
duration,
|
duration,
|
||||||
error: migrationError,
|
error: migrationError,
|
||||||
};
|
};
|
||||||
await onError(finishedMigration, migrationError);
|
await storage.onError(finishedMigration, toSerializedError(migrationError));
|
||||||
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
||||||
finishedMigrations.push(finishedMigration);
|
finishedMigrations.push(finishedMigration);
|
||||||
skip = true;
|
skip = true;
|
||||||
|
|
@ -265,14 +208,15 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
status: 'done',
|
status: 'done',
|
||||||
duration,
|
duration,
|
||||||
};
|
};
|
||||||
await onSuccess(finishedMigration);
|
await storage.onSuccess(finishedMigration);
|
||||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
await reporter.onMigrationSuccess?.(finishedMigration);
|
||||||
finishedMigrations.push(finishedMigration);
|
finishedMigrations.push(finishedMigration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, unlockError] =
|
const [, unlockError] = dry
|
||||||
dry || !lock ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
? []
|
||||||
|
: await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
const firstFailed = finishedMigrations.find(isFailedMigration);
|
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||||
|
|
@ -283,11 +227,7 @@ export const migrationRunner = async <T extends MigrationMetadata | MigrationMet
|
||||||
? MigrationRunError.fromMetadata(firstFailed)
|
? MigrationRunError.fromMetadata(firstFailed)
|
||||||
: undefined;
|
: undefined;
|
||||||
const error =
|
const error =
|
||||||
optionError ??
|
unlockError ?? firstError ?? lockError ?? (abortSignal?.aborted ? toError(abortSignal.reason) : undefined);
|
||||||
unlockError ??
|
|
||||||
firstError ??
|
|
||||||
lockError ??
|
|
||||||
(abortSignal?.aborted ? toError(abortSignal.reason) : undefined);
|
|
||||||
|
|
||||||
await reporter.onFinished?.(finishedMigrations, error);
|
await reporter.onFinished?.(finishedMigrations, error);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { setInterval } from 'node:timers';
|
|
||||||
import { black, blueBright, bold, cyan, dim, faint, gray, green, red, redBright, yellow, yellowBright } from 'ansis';
|
import { black, blueBright, bold, cyan, dim, faint, gray, green, red, redBright, yellow, yellowBright } from 'ansis';
|
||||||
import logUpdate from 'log-update';
|
import logUpdate from 'log-update';
|
||||||
import elegantSpinner from 'elegant-spinner';
|
import elegantSpinner from 'elegant-spinner';
|
||||||
|
|
@ -14,7 +13,6 @@ import {
|
||||||
} from '@emigrate/types';
|
} from '@emigrate/types';
|
||||||
|
|
||||||
type Status = ReturnType<typeof getMigrationStatus>;
|
type Status = ReturnType<typeof getMigrationStatus>;
|
||||||
type Command = ReporterInitParameters['command'];
|
|
||||||
|
|
||||||
const interactive = isInteractive();
|
const interactive = isInteractive();
|
||||||
const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
||||||
|
|
@ -22,7 +20,7 @@ const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
||||||
const formatDuration = (duration: number): string => {
|
const formatDuration = (duration: number): string => {
|
||||||
const pretty = prettyMs(duration);
|
const pretty = prettyMs(duration);
|
||||||
|
|
||||||
return yellow(pretty.replaceAll(/([^\s\d.]+)/g, dim('$1')));
|
return yellow(pretty.replaceAll(/([^\s\d]+)/g, dim('$1')));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTitle = ({ command, version, dry, cwd }: ReporterInitParameters) => {
|
const getTitle = ({ command, version, dry, cwd }: ReporterInitParameters) => {
|
||||||
|
|
@ -32,16 +30,11 @@ const getTitle = ({ command, version, dry, cwd }: ReporterInitParameters) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMigrationStatus = (
|
const getMigrationStatus = (
|
||||||
command: Command,
|
|
||||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
activeMigration?: MigrationMetadata,
|
activeMigration?: MigrationMetadata,
|
||||||
) => {
|
) => {
|
||||||
if ('status' in migration) {
|
if ('status' in migration) {
|
||||||
return command === 'remove' && migration.status === 'done' ? 'removed' : migration.status;
|
return migration.status;
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'remove' && migration.name === activeMigration?.name) {
|
|
||||||
return 'removing';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return migration.name === activeMigration?.name ? 'running' : 'pending';
|
return migration.name === activeMigration?.name ? 'running' : 'pending';
|
||||||
|
|
@ -49,10 +42,6 @@ const getMigrationStatus = (
|
||||||
|
|
||||||
const getIcon = (status: Status) => {
|
const getIcon = (status: Status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'removing': {
|
|
||||||
return cyan(spinner());
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'running': {
|
case 'running': {
|
||||||
return cyan(spinner());
|
return cyan(spinner());
|
||||||
}
|
}
|
||||||
|
|
@ -61,10 +50,6 @@ const getIcon = (status: Status) => {
|
||||||
return gray(figures.pointerSmall);
|
return gray(figures.pointerSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'removed': {
|
|
||||||
return green(figures.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'done': {
|
case 'done': {
|
||||||
return green(figures.tick);
|
return green(figures.tick);
|
||||||
}
|
}
|
||||||
|
|
@ -104,19 +89,20 @@ const getName = (name: string, status?: Status) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMigrationText = (
|
const getMigrationText = (
|
||||||
command: Command,
|
|
||||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
activeMigration?: MigrationMetadata,
|
activeMigration?: MigrationMetadata,
|
||||||
) => {
|
) => {
|
||||||
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
|
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
|
||||||
const nameWithoutExtension = migration.name.slice(0, -migration.extension.length);
|
const nameWithoutExtension = migration.name.slice(0, -migration.extension.length);
|
||||||
const status = getMigrationStatus(command, migration, activeMigration);
|
const status = getMigrationStatus(migration, activeMigration);
|
||||||
const parts = [' ', getIcon(status)];
|
const parts = [' ', getIcon(status)];
|
||||||
|
|
||||||
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
|
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
|
||||||
|
|
||||||
if ('status' in migration || migration.name === activeMigration?.name) {
|
if ('status' in migration) {
|
||||||
parts.push(gray`(${status})`);
|
parts.push(gray`(${migration.status})`);
|
||||||
|
} else if (migration.name === activeMigration?.name) {
|
||||||
|
parts.push(gray`(running)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('duration' in migration && migration.duration) {
|
if ('duration' in migration && migration.duration) {
|
||||||
|
|
@ -271,6 +257,7 @@ const getHeaderMessage = (
|
||||||
|
|
||||||
let skippedCount = 0;
|
let skippedCount = 0;
|
||||||
let failedCount = 0;
|
let failedCount = 0;
|
||||||
|
let unlockableCount = 0;
|
||||||
|
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
||||||
|
|
@ -284,6 +271,8 @@ const getHeaderMessage = (
|
||||||
failedCount += 1;
|
failedCount += 1;
|
||||||
} else if (migration.status === 'skipped') {
|
} else if (migration.status === 'skipped') {
|
||||||
skippedCount += 1;
|
skippedCount += 1;
|
||||||
|
} else {
|
||||||
|
unlockableCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -291,6 +280,7 @@ const getHeaderMessage = (
|
||||||
const parts = [
|
const parts = [
|
||||||
bold(`${lockedMigrations.length} of ${migrations.length}`),
|
bold(`${lockedMigrations.length} of ${migrations.length}`),
|
||||||
dim(statusText),
|
dim(statusText),
|
||||||
|
unlockableCount > 0 ? yellow(`(${unlockableCount} locked)`) : '',
|
||||||
skippedCount > 0 ? yellowBright(`(${skippedCount} skipped)`) : '',
|
skippedCount > 0 ? yellowBright(`(${skippedCount} skipped)`) : '',
|
||||||
failedCount > 0 ? redBright(`(${failedCount} failed)`) : '',
|
failedCount > 0 ? redBright(`(${failedCount} failed)`) : '',
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
@ -329,6 +319,19 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
||||||
this.#migrations = [migration];
|
this.#migrations = [migration];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||||
|
this.#migrations = [migration];
|
||||||
|
this.#activeMigration = migration;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||||
|
this.#finishMigration(migration);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void> {
|
||||||
|
this.#finishMigration(migration);
|
||||||
|
}
|
||||||
|
|
||||||
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
||||||
this.#activeMigration = migration;
|
this.#activeMigration = migration;
|
||||||
}
|
}
|
||||||
|
|
@ -373,9 +376,7 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
||||||
const parts = [
|
const parts = [
|
||||||
getTitle(this.#parameters),
|
getTitle(this.#parameters),
|
||||||
getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations),
|
getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations),
|
||||||
this.#migrations
|
this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '',
|
||||||
?.map((migration) => getMigrationText(this.#parameters.command, migration, this.#activeMigration))
|
|
||||||
.join('\n') ?? '',
|
|
||||||
getAbortMessage(this.#abortReason),
|
getAbortMessage(this.#abortReason),
|
||||||
getSummary(this.#parameters.command, this.#migrations),
|
getSummary(this.#parameters.command, this.#migrations),
|
||||||
getError(this.#error),
|
getError(this.#error),
|
||||||
|
|
@ -440,23 +441,35 @@ class DefaultReporter implements Required<EmigrateReporter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void> {
|
onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void> {
|
||||||
console.log(getMigrationText(this.#parameters.command, migration));
|
console.log(getMigrationText(migration));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||||
|
console.log(getMigrationText(migration));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||||
|
console.log(getMigrationText(migration));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void> {
|
||||||
|
console.error(getMigrationText(migration));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
||||||
console.log(getMigrationText(this.#parameters.command, migration, migration));
|
console.log(getMigrationText(migration, migration));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
||||||
console.log(getMigrationText(this.#parameters.command, migration));
|
console.log(getMigrationText(migration));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationError(migration: MigrationMetadataFinished, _error: Error): void | PromiseLike<void> {
|
onMigrationError(migration: MigrationMetadataFinished, _error: Error): void | PromiseLike<void> {
|
||||||
console.error(getMigrationText(this.#parameters.command, migration));
|
console.error(getMigrationText(migration));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationSkip(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
onMigrationSkip(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
||||||
console.log(getMigrationText(this.#parameters.command, migration));
|
console.log(getMigrationText(migration));
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike<void> {
|
onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike<void> {
|
||||||
|
|
@ -471,6 +484,6 @@ class DefaultReporter implements Required<EmigrateReporter> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reporterDefault: EmigrateReporter = interactive ? new DefaultFancyReporter() : new DefaultReporter();
|
const reporterDefault = interactive ? new DefaultFancyReporter() : new DefaultReporter();
|
||||||
|
|
||||||
export default reporterDefault;
|
export default reporterDefault;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import type { EmigrateReporter } from '@emigrate/types';
|
|
||||||
import { type Config } from '../types.js';
|
|
||||||
import * as reporters from './index.js';
|
|
||||||
|
|
||||||
export const getStandardReporter = (reporter?: Config['reporter']): EmigrateReporter | undefined => {
|
|
||||||
if (!reporter) {
|
|
||||||
return reporters.pretty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof reporter === 'string' && reporter in reporters) {
|
|
||||||
return reporters[reporter as keyof typeof reporters];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as pretty } from './default.js';
|
|
||||||
export { default as json } from './json.js';
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { type ReporterInitParameters, type EmigrateReporter, type MigrationMetadataFinished } from '@emigrate/types';
|
|
||||||
import { toSerializedError } from '../errors.js';
|
|
||||||
|
|
||||||
class JsonReporter implements EmigrateReporter {
|
|
||||||
#parameters!: ReporterInitParameters;
|
|
||||||
#startTime!: number;
|
|
||||||
|
|
||||||
onInit(parameters: ReporterInitParameters): void {
|
|
||||||
this.#startTime = Date.now();
|
|
||||||
this.#parameters = parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void {
|
|
||||||
const { command, version } = this.#parameters;
|
|
||||||
|
|
||||||
let numberDoneMigrations = 0;
|
|
||||||
let numberSkippedMigrations = 0;
|
|
||||||
let numberFailedMigrations = 0;
|
|
||||||
let numberPendingMigrations = 0;
|
|
||||||
|
|
||||||
for (const migration of migrations) {
|
|
||||||
// eslint-disable-next-line unicorn/prefer-switch
|
|
||||||
if (migration.status === 'done') {
|
|
||||||
numberDoneMigrations++;
|
|
||||||
} else if (migration.status === 'skipped') {
|
|
||||||
numberSkippedMigrations++;
|
|
||||||
} else if (migration.status === 'failed') {
|
|
||||||
numberFailedMigrations++;
|
|
||||||
} else {
|
|
||||||
numberPendingMigrations++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
command,
|
|
||||||
version,
|
|
||||||
numberTotalMigrations: migrations.length,
|
|
||||||
numberDoneMigrations,
|
|
||||||
numberSkippedMigrations,
|
|
||||||
numberFailedMigrations,
|
|
||||||
numberPendingMigrations,
|
|
||||||
success: !error,
|
|
||||||
startTime: this.#startTime,
|
|
||||||
endTime: Date.now(),
|
|
||||||
error: error ? toSerializedError(error) : undefined,
|
|
||||||
migrations: migrations.map((migration) => ({
|
|
||||||
name: migration.filePath,
|
|
||||||
status: migration.status,
|
|
||||||
duration: 'duration' in migration ? migration.duration : 0,
|
|
||||||
error: 'error' in migration ? toSerializedError(migration.error) : undefined,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(JSON.stringify(result, undefined, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonReporter: EmigrateReporter = new JsonReporter();
|
|
||||||
|
|
||||||
export default jsonReporter;
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { mock, type Mock } from 'node:test';
|
|
||||||
import path from 'node:path';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import {
|
|
||||||
type SerializedError,
|
|
||||||
type EmigrateReporter,
|
|
||||||
type FailedMigrationHistoryEntry,
|
|
||||||
type MigrationHistoryEntry,
|
|
||||||
type MigrationMetadata,
|
|
||||||
type NonFailedMigrationHistoryEntry,
|
|
||||||
type Storage,
|
|
||||||
} from '@emigrate/types';
|
|
||||||
import { toSerializedError } from './errors.js';
|
|
||||||
|
|
||||||
export type Mocked<T> = {
|
|
||||||
// @ts-expect-error - This is a mock
|
|
||||||
[K in keyof T]: Mock<T[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function noop(): Promise<void> {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getErrorCause(error: Error | undefined): Error | SerializedError | undefined {
|
|
||||||
if (error?.cause instanceof Error) {
|
|
||||||
return error.cause;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error?.cause === 'object' && error.cause !== null) {
|
|
||||||
return error.cause as unknown as SerializedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockedStorage(historyEntries: Array<string | MigrationHistoryEntry>): Mocked<Storage> {
|
|
||||||
return {
|
|
||||||
lock: mock.fn(async (migrations) => migrations),
|
|
||||||
unlock: mock.fn(async () => {
|
|
||||||
// void
|
|
||||||
}),
|
|
||||||
getHistory: mock.fn(async function* () {
|
|
||||||
yield* toEntries(historyEntries);
|
|
||||||
}),
|
|
||||||
remove: mock.fn(),
|
|
||||||
onSuccess: mock.fn(),
|
|
||||||
onError: mock.fn(),
|
|
||||||
end: mock.fn(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockedReporter(): Mocked<Required<EmigrateReporter>> {
|
|
||||||
return {
|
|
||||||
onFinished: mock.fn(noop),
|
|
||||||
onInit: mock.fn(noop),
|
|
||||||
onAbort: mock.fn(noop),
|
|
||||||
onCollectedMigrations: mock.fn(noop),
|
|
||||||
onLockedMigrations: mock.fn(noop),
|
|
||||||
onNewMigration: mock.fn(noop),
|
|
||||||
onMigrationStart: mock.fn(noop),
|
|
||||||
onMigrationSuccess: mock.fn(noop),
|
|
||||||
onMigrationError: mock.fn(noop),
|
|
||||||
onMigrationSkip: mock.fn(noop),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
filePath: `${cwd}/${directory}/${name}`,
|
|
||||||
relativeFilePath: `${directory}/${name}`,
|
|
||||||
extension: path.extname(name),
|
|
||||||
directory,
|
|
||||||
cwd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toMigrations(cwd: string, directory: string, names: string[]): MigrationMetadata[] {
|
|
||||||
return names.map((name) => toMigration(cwd, directory, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toEntry(name: MigrationHistoryEntry): MigrationHistoryEntry;
|
|
||||||
export function toEntry<S extends MigrationHistoryEntry['status']>(
|
|
||||||
name: string,
|
|
||||||
status?: S,
|
|
||||||
): S extends 'failed' ? FailedMigrationHistoryEntry : NonFailedMigrationHistoryEntry;
|
|
||||||
|
|
||||||
export function toEntry(name: string | MigrationHistoryEntry, status?: 'done' | 'failed'): MigrationHistoryEntry {
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
return name.status === 'failed' ? name : name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'failed') {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
date: new Date(),
|
|
||||||
error: { name: 'Error', message: 'Failed' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
status: status ?? 'done',
|
|
||||||
date: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toEntries(
|
|
||||||
names: Array<string | MigrationHistoryEntry>,
|
|
||||||
status?: MigrationHistoryEntry['status'],
|
|
||||||
): MigrationHistoryEntry[] {
|
|
||||||
return names.map((name) => (typeof name === 'string' ? toEntry(name, status) : name));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertErrorEqualEnough(actual?: Error | SerializedError, expected?: Error, message?: string): void {
|
|
||||||
if (expected === undefined) {
|
|
||||||
assert.strictEqual(actual, undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
cause: actualCause,
|
|
||||||
stack: actualStack,
|
|
||||||
...actualError
|
|
||||||
} = actual instanceof Error ? toSerializedError(actual) : actual ?? {};
|
|
||||||
const { cause: expectedCause, stack: expectedStack, ...expectedError } = toSerializedError(expected);
|
|
||||||
// @ts-expect-error Ignore
|
|
||||||
const { stack: actualCauseStack, ...actualCauseRest } = actualCause ?? {};
|
|
||||||
// @ts-expect-error Ignore
|
|
||||||
const { stack: expectedCauseStack, ...expectedCauseRest } = expectedCause ?? {};
|
|
||||||
assert.deepStrictEqual(actualError, expectedError, message);
|
|
||||||
assert.deepStrictEqual(actualCauseRest, expectedCauseRest, message ? `${message} (cause)` : undefined);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/types';
|
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/types';
|
||||||
import type * as reporters from './reporters/index.js';
|
|
||||||
|
|
||||||
export type StandardReporter = keyof typeof reporters;
|
|
||||||
|
|
||||||
export type EmigratePlugin = Plugin;
|
export type EmigratePlugin = Plugin;
|
||||||
|
|
||||||
|
|
@ -9,7 +6,7 @@ type StringOrModule<T> = string | T | (() => Awaitable<T>) | (() => Awaitable<{
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
storage?: StringOrModule<EmigrateStorage>;
|
storage?: StringOrModule<EmigrateStorage>;
|
||||||
reporter?: StandardReporter | StringOrModule<EmigrateReporter>;
|
reporter?: StringOrModule<EmigrateReporter>;
|
||||||
plugins?: Array<StringOrModule<EmigratePlugin>>;
|
plugins?: Array<StringOrModule<EmigratePlugin>>;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const withLeadingPeriod = (string: string): string => (string.startsWith('.') ? string : `.${string}`);
|
export const withLeadingPeriod = (string: string) => (string.startsWith('.') ? string : `.${string}`);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,5 @@
|
||||||
# @emigrate/mysql
|
# @emigrate/mysql
|
||||||
|
|
||||||
## 0.3.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 26240f4: Make sure we can initialize multiple running instances of Emigrate using @emigrate/mysql concurrently without issues with creating the history table (for instance in a Kubernetes environment and/or with a Percona cluster).
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
- 26240f4: Either lock all or none of the migrations to run to make sure they run in order when multiple instances of Emigrate runs concurrently (for instance in a Kubernetes environment)
|
|
||||||
- Updated dependencies [d779286]
|
|
||||||
- @emigrate/plugin-tools@0.9.8
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 57498db: Unreference all connections when run using Bun, to not keep the process open unnecessarily long
|
|
||||||
|
|
||||||
## 0.3.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/plugin-tools@0.9.7
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 4442604: Automatically create the database if it doesn't exist, and the user have the permissions to do so
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- aef2d7c: Avoid "CREATE TABLE IF NOT EXISTS" as it's too locking in a clustered database when running it concurrently
|
|
||||||
|
|
||||||
## 0.2.8
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 17feb2d: Only unreference connections in a Bun environment as it crashes Node for some reason, without even throwing an error that is
|
|
||||||
|
|
||||||
## 0.2.7
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 198aa54: Unreference all connections automatically so that they don't hinder the process from exiting. This is especially needed in Bun environments as it seems to handle sockets differently regarding this matter than NodeJS.
|
|
||||||
|
|
||||||
## 0.2.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/plugin-tools@0.9.6
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.2.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- f8a5cc7: Make sure the storage initialization crashes when a database connection can't be established
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
- @emigrate/plugin-tools@0.9.5
|
|
||||||
|
|
||||||
## 0.2.4
|
## 0.2.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/mysql",
|
"name": "@emigrate/mysql",
|
||||||
"version": "0.3.3",
|
"version": "0.2.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "A MySQL plugin for Emigrate. Uses a MySQL database for storing migration history. Can load and generate .sql migration files.",
|
"description": "A MySQL plugin for Emigrate. Uses a MySQL database for storing migration history. Can load and generate .sql migration files.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,17 +15,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo",
|
|
||||||
"!dist/**/*.test.js",
|
|
||||||
"!dist/tests/*"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
"build:watch": "tsc --pretty --watch",
|
"build:watch": "tsc --pretty --watch",
|
||||||
"lint": "xo --cwd=../.. $(pwd)",
|
"lint": "xo --cwd=../.. $(pwd)"
|
||||||
"integration": "glob -c \"node --import tsx --test-reporter spec --test\" \"./src/**/*.integration.ts\"",
|
|
||||||
"integration:watch": "glob -c \"node --watch --import tsx --test-reporter spec --test\" \"./src/**/*.integration.ts\""
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"emigrate",
|
"emigrate",
|
||||||
|
|
@ -48,9 +42,7 @@
|
||||||
"mysql2": "3.6.5"
|
"mysql2": "3.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emigrate/tsconfig": "workspace:*",
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
"@types/bun": "1.1.2",
|
|
||||||
"bun-types": "1.1.8"
|
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"extends": "../../package.json"
|
"extends": "../../package.json"
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import assert from 'node:assert';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { before, after, describe, it } from 'node:test';
|
|
||||||
import type { MigrationMetadata } from '@emigrate/types';
|
|
||||||
import { startDatabase, stopDatabase } from './tests/database.js';
|
|
||||||
import { createMysqlStorage } from './index.js';
|
|
||||||
|
|
||||||
let db: { port: number; host: string };
|
|
||||||
|
|
||||||
const toEnd = new Set<{ end: () => Promise<void> }>();
|
|
||||||
|
|
||||||
describe('emigrate-mysql', async () => {
|
|
||||||
before(
|
|
||||||
async () => {
|
|
||||||
db = await startDatabase();
|
|
||||||
},
|
|
||||||
{ timeout: 60_000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
after(
|
|
||||||
async () => {
|
|
||||||
for (const storage of toEnd) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await storage.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
toEnd.clear();
|
|
||||||
await stopDatabase();
|
|
||||||
},
|
|
||||||
{ timeout: 10_000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('migration locks', async () => {
|
|
||||||
it('either locks none or all of the given migrations', async () => {
|
|
||||||
const { initializeStorage } = createMysqlStorage({
|
|
||||||
table: 'migrations',
|
|
||||||
connection: {
|
|
||||||
host: db.host,
|
|
||||||
user: 'emigrate',
|
|
||||||
password: 'emigrate',
|
|
||||||
database: 'emigrate',
|
|
||||||
port: db.port,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [storage1, storage2] = await Promise.all([initializeStorage(), initializeStorage()]);
|
|
||||||
|
|
||||||
toEnd.add(storage1);
|
|
||||||
toEnd.add(storage2);
|
|
||||||
|
|
||||||
const migrations = toMigrations('/emigrate', 'migrations', [
|
|
||||||
'2023-10-01-01-test.js',
|
|
||||||
'2023-10-01-02-test.js',
|
|
||||||
'2023-10-01-03-test.js',
|
|
||||||
'2023-10-01-04-test.js',
|
|
||||||
'2023-10-01-05-test.js',
|
|
||||||
'2023-10-01-06-test.js',
|
|
||||||
'2023-10-01-07-test.js',
|
|
||||||
'2023-10-01-08-test.js',
|
|
||||||
'2023-10-01-09-test.js',
|
|
||||||
'2023-10-01-10-test.js',
|
|
||||||
'2023-10-01-11-test.js',
|
|
||||||
'2023-10-01-12-test.js',
|
|
||||||
'2023-10-01-13-test.js',
|
|
||||||
'2023-10-01-14-test.js',
|
|
||||||
'2023-10-01-15-test.js',
|
|
||||||
'2023-10-01-16-test.js',
|
|
||||||
'2023-10-01-17-test.js',
|
|
||||||
'2023-10-01-18-test.js',
|
|
||||||
'2023-10-01-19-test.js',
|
|
||||||
'2023-10-01-20-test.js',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [locked1, locked2] = await Promise.all([storage1.lock(migrations), storage2.lock(migrations)]);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
locked1.length === 0 || locked2.length === 0,
|
|
||||||
true,
|
|
||||||
'One of the processes should have no locks',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
locked1.length === 20 || locked2.length === 20,
|
|
||||||
true,
|
|
||||||
'One of the processes should have all locks',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
filePath: `${cwd}/${directory}/${name}`,
|
|
||||||
relativeFilePath: `${directory}/${name}`,
|
|
||||||
extension: path.extname(name),
|
|
||||||
directory,
|
|
||||||
cwd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toMigrations(cwd: string, directory: string, names: string[]): MigrationMetadata[] {
|
|
||||||
return names.map((name) => toMigration(cwd, directory, name));
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import {
|
import {
|
||||||
createConnection,
|
createConnection,
|
||||||
createPool,
|
createPool,
|
||||||
|
|
@ -10,13 +9,10 @@ import {
|
||||||
type Pool,
|
type Pool,
|
||||||
type ResultSetHeader,
|
type ResultSetHeader,
|
||||||
type RowDataPacket,
|
type RowDataPacket,
|
||||||
type Connection,
|
|
||||||
} from 'mysql2/promise';
|
} from 'mysql2/promise';
|
||||||
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
|
import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
|
||||||
import {
|
import {
|
||||||
type Awaitable,
|
|
||||||
type MigrationMetadata,
|
type MigrationMetadata,
|
||||||
type MigrationFunction,
|
|
||||||
type EmigrateStorage,
|
type EmigrateStorage,
|
||||||
type LoaderPlugin,
|
type LoaderPlugin,
|
||||||
type Storage,
|
type Storage,
|
||||||
|
|
@ -44,39 +40,27 @@ export type MysqlLoaderOptions = {
|
||||||
connection: ConnectionOptions | string;
|
connection: ConnectionOptions | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConnection = async (options: ConnectionOptions | string) => {
|
const getConnection = async (connection: ConnectionOptions | string) => {
|
||||||
let connection: Connection;
|
if (typeof connection === 'string') {
|
||||||
|
const uri = new URL(connection);
|
||||||
if (typeof options === 'string') {
|
|
||||||
const uri = new URL(options);
|
|
||||||
|
|
||||||
// client side connectTimeout is unstable in mysql2 library
|
// client side connectTimeout is unstable in mysql2 library
|
||||||
// it throws an error you can't catch and crashes node
|
// it throws an error you can't catch and crashes node
|
||||||
// best to leave this at 0 (disabled)
|
// best to leave this at 0 (disabled)
|
||||||
uri.searchParams.set('connectTimeout', '0');
|
uri.searchParams.set('connectTimeout', '0');
|
||||||
uri.searchParams.set('multipleStatements', 'true');
|
uri.searchParams.set('multipleStatements', 'true');
|
||||||
uri.searchParams.set('flags', '-FOUND_ROWS');
|
|
||||||
|
|
||||||
connection = await createConnection(uri.toString());
|
return createConnection(uri.toString());
|
||||||
} else {
|
|
||||||
connection = await createConnection({
|
|
||||||
...options,
|
|
||||||
// client side connectTimeout is unstable in mysql2 library
|
|
||||||
// it throws an error you can't catch and crashes node
|
|
||||||
// best to leave this at 0 (disabled)
|
|
||||||
connectTimeout: 0,
|
|
||||||
multipleStatements: true,
|
|
||||||
flags: ['-FOUND_ROWS'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.isBun) {
|
return createConnection({
|
||||||
// @ts-expect-error the connection is not in the types but it's there
|
...connection,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// client side connectTimeout is unstable in mysql2 library
|
||||||
connection.connection.stream.unref();
|
// it throws an error you can't catch and crashes node
|
||||||
}
|
// best to leave this at 0 (disabled)
|
||||||
|
connectTimeout: 0,
|
||||||
return connection;
|
multipleStatements: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPool = (connection: PoolOptions | string) => {
|
const getPool = (connection: PoolOptions | string) => {
|
||||||
|
|
@ -87,7 +71,6 @@ const getPool = (connection: PoolOptions | string) => {
|
||||||
// it throws an error you can't catch and crashes node
|
// it throws an error you can't catch and crashes node
|
||||||
// best to leave this at 0 (disabled)
|
// best to leave this at 0 (disabled)
|
||||||
uri.searchParams.set('connectTimeout', '0');
|
uri.searchParams.set('connectTimeout', '0');
|
||||||
uri.searchParams.set('flags', '-FOUND_ROWS');
|
|
||||||
|
|
||||||
return createPool(uri.toString());
|
return createPool(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +81,6 @@ const getPool = (connection: PoolOptions | string) => {
|
||||||
// it throws an error you can't catch and crashes node
|
// it throws an error you can't catch and crashes node
|
||||||
// best to leave this at 0 (disabled)
|
// best to leave this at 0 (disabled)
|
||||||
connectTimeout: 0,
|
connectTimeout: 0,
|
||||||
flags: ['-FOUND_ROWS'],
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -109,8 +91,8 @@ type HistoryEntry = {
|
||||||
error?: SerializedError;
|
error?: SerializedError;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lockMigration = async (connection: Connection, table: string, migration: MigrationMetadata) => {
|
const lockMigration = async (pool: Pool, table: string, migration: MigrationMetadata) => {
|
||||||
const [result] = await connection.execute<ResultSetHeader>({
|
const [result] = await pool.execute<ResultSetHeader>({
|
||||||
sql: `
|
sql: `
|
||||||
INSERT INTO ${escapeId(table)} (name, status, date)
|
INSERT INTO ${escapeId(table)} (name, status, date)
|
||||||
VALUES (?, ?, NOW())
|
VALUES (?, ?, NOW())
|
||||||
|
|
@ -173,186 +155,40 @@ const deleteMigration = async (pool: Pool, table: string, migration: MigrationMe
|
||||||
return result.affectedRows === 1;
|
return result.affectedRows === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseName = (config: ConnectionOptions | string) => {
|
const initializeTable = async (pool: Pool, table: string) => {
|
||||||
if (typeof config === 'string') {
|
// This table definition is compatible with the one used by the immigration-mysql package
|
||||||
const uri = new URL(config);
|
await pool.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${escapeId(table)} (
|
||||||
return uri.pathname.replace(/^\//u, '');
|
name varchar(255) not null primary key,
|
||||||
}
|
status varchar(32),
|
||||||
|
date datetime not null
|
||||||
return config.database ?? '';
|
) Engine=InnoDB;
|
||||||
};
|
`);
|
||||||
|
|
||||||
const setDatabaseName = <T extends ConnectionOptions | string>(config: T, databaseName: string): T => {
|
|
||||||
if (typeof config === 'string') {
|
|
||||||
const uri = new URL(config);
|
|
||||||
|
|
||||||
uri.pathname = `/${databaseName}`;
|
|
||||||
|
|
||||||
return uri.toString() as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof config === 'object') {
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
database: databaseName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid connection config');
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeDatabase = async (config: ConnectionOptions | string) => {
|
|
||||||
let connection: Connection | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
connection = await getConnection(config);
|
|
||||||
await connection.query('SELECT 1');
|
|
||||||
await connection.end();
|
|
||||||
} catch (error) {
|
|
||||||
await connection?.end();
|
|
||||||
|
|
||||||
// The ER_BAD_DB_ERROR error code is thrown when the database does not exist but the user might have the permissions to create it
|
|
||||||
// Otherwise the error code is ER_DBACCESS_DENIED_ERROR
|
|
||||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ER_BAD_DB_ERROR') {
|
|
||||||
const databaseName = getDatabaseName(config);
|
|
||||||
|
|
||||||
const informationSchemaConfig = setDatabaseName(config, 'information_schema');
|
|
||||||
|
|
||||||
const informationSchemaConnection = await getConnection(informationSchemaConfig);
|
|
||||||
try {
|
|
||||||
await informationSchemaConnection.query(`CREATE DATABASE ${escapeId(databaseName)}`);
|
|
||||||
// Any database creation error here will be propagated
|
|
||||||
} finally {
|
|
||||||
await informationSchemaConnection.end();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// In this case we don't know how to handle the error, so we rethrow it
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const lockWaitTimeout = 10; // seconds
|
|
||||||
|
|
||||||
const isHistoryTableExisting = async (connection: Connection, table: string) => {
|
|
||||||
const [result] = await connection.execute<RowDataPacket[]>({
|
|
||||||
sql: `
|
|
||||||
SELECT
|
|
||||||
1 as table_exists
|
|
||||||
FROM
|
|
||||||
information_schema.tables
|
|
||||||
WHERE
|
|
||||||
table_schema = DATABASE()
|
|
||||||
AND table_name = ?
|
|
||||||
`,
|
|
||||||
values: [table],
|
|
||||||
});
|
|
||||||
|
|
||||||
return result[0]?.['table_exists'] === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeTable = async (config: ConnectionOptions | string, table: string) => {
|
|
||||||
const connection = await getConnection(config);
|
|
||||||
|
|
||||||
if (await isHistoryTableExisting(connection, table)) {
|
|
||||||
await connection.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockName = `emigrate_init_table_lock_${table}`;
|
|
||||||
|
|
||||||
const [lockResult] = await connection.query<RowDataPacket[]>(`SELECT GET_LOCK(?, ?) AS got_lock`, [
|
|
||||||
lockName,
|
|
||||||
lockWaitTimeout,
|
|
||||||
]);
|
|
||||||
const didGetLock = lockResult[0]?.['got_lock'] === 1;
|
|
||||||
|
|
||||||
if (didGetLock) {
|
|
||||||
try {
|
|
||||||
// This table definition is compatible with the one used by the immigration-mysql package
|
|
||||||
await connection.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS ${escapeId(table)} (
|
|
||||||
name varchar(255) not null primary key,
|
|
||||||
status varchar(32),
|
|
||||||
date datetime not null
|
|
||||||
) Engine=InnoDB;
|
|
||||||
`);
|
|
||||||
} finally {
|
|
||||||
await connection.query(`SELECT RELEASE_LOCK(?)`, [lockName]);
|
|
||||||
await connection.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Didn't get the lock, wait to see if the table was created by another process
|
|
||||||
const maxWait = lockWaitTimeout * 1000; // milliseconds
|
|
||||||
const checkInterval = 250; // milliseconds
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (Date.now() - start < maxWait) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
if (await isHistoryTableExisting(connection, table)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await setTimeout(checkInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Timeout waiting for table ${table} to be created by other process`);
|
|
||||||
} finally {
|
|
||||||
await connection.end();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlStorageOptions): EmigrateStorage => {
|
export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlStorageOptions): EmigrateStorage => {
|
||||||
return {
|
return {
|
||||||
async initializeStorage() {
|
async initializeStorage() {
|
||||||
await initializeDatabase(connection);
|
|
||||||
await initializeTable(connection, table);
|
|
||||||
|
|
||||||
const pool = getPool(connection);
|
const pool = getPool(connection);
|
||||||
|
|
||||||
if (process.isBun) {
|
try {
|
||||||
pool.on('connection', (connection) => {
|
await initializeTable(pool, table);
|
||||||
// @ts-expect-error stream is not in the types but it's there
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
await pool.end();
|
||||||
connection.stream.unref();
|
throw error;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage: Storage = {
|
const storage: Storage = {
|
||||||
async lock(migrations) {
|
async lock(migrations) {
|
||||||
const connection = await pool.getConnection();
|
const lockedMigrations: MigrationMetadata[] = [];
|
||||||
|
|
||||||
try {
|
for await (const migration of migrations) {
|
||||||
await connection.beginTransaction();
|
if (await lockMigration(pool, table, migration)) {
|
||||||
const lockedMigrations: MigrationMetadata[] = [];
|
lockedMigrations.push(migration);
|
||||||
|
|
||||||
for await (const migration of migrations) {
|
|
||||||
if (await lockMigration(connection, table, migration)) {
|
|
||||||
lockedMigrations.push(migration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lockedMigrations.length === migrations.length) {
|
|
||||||
await connection.commit();
|
|
||||||
|
|
||||||
return lockedMigrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection.rollback();
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
await connection.rollback();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
connection.release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return lockedMigrations;
|
||||||
},
|
},
|
||||||
async unlock(migrations) {
|
async unlock(migrations) {
|
||||||
for await (const migration of migrations) {
|
for await (const migration of migrations) {
|
||||||
|
|
@ -411,6 +247,17 @@ export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlSt
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const { initializeStorage } = createMysqlStorage({
|
||||||
|
table: process.env['MYSQL_TABLE'],
|
||||||
|
connection: process.env['MYSQL_URL'] ?? {
|
||||||
|
host: process.env['MYSQL_HOST'],
|
||||||
|
port: process.env['MYSQL_PORT'] ? Number.parseInt(process.env['MYSQL_PORT'], 10) : undefined,
|
||||||
|
user: process.env['MYSQL_USER'],
|
||||||
|
password: process.env['MYSQL_PASSWORD'],
|
||||||
|
database: process.env['MYSQL_DATABASE'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const createMysqlLoader = ({ connection }: MysqlLoaderOptions): LoaderPlugin => {
|
export const createMysqlLoader = ({ connection }: MysqlLoaderOptions): LoaderPlugin => {
|
||||||
return {
|
return {
|
||||||
loadableExtensions: ['.sql'],
|
loadableExtensions: ['.sql'],
|
||||||
|
|
@ -429,6 +276,16 @@ export const createMysqlLoader = ({ connection }: MysqlLoaderOptions): LoaderPlu
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const { loadableExtensions, loadMigration } = createMysqlLoader({
|
||||||
|
connection: process.env['MYSQL_URL'] ?? {
|
||||||
|
host: process.env['MYSQL_HOST'],
|
||||||
|
port: process.env['MYSQL_PORT'] ? Number.parseInt(process.env['MYSQL_PORT'], 10) : undefined,
|
||||||
|
user: process.env['MYSQL_USER'],
|
||||||
|
password: process.env['MYSQL_PASSWORD'],
|
||||||
|
database: process.env['MYSQL_DATABASE'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
||||||
return {
|
return {
|
||||||
filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`,
|
filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`,
|
||||||
|
|
@ -437,34 +294,6 @@ export const generateMigration: GenerateMigrationFunction = async (name) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const storage = createMysqlStorage({
|
|
||||||
table: process.env['MYSQL_TABLE'],
|
|
||||||
connection: process.env['MYSQL_URL'] ?? {
|
|
||||||
host: process.env['MYSQL_HOST'],
|
|
||||||
port: process.env['MYSQL_PORT'] ? Number.parseInt(process.env['MYSQL_PORT'], 10) : undefined,
|
|
||||||
user: process.env['MYSQL_USER'],
|
|
||||||
password: process.env['MYSQL_PASSWORD'],
|
|
||||||
database: process.env['MYSQL_DATABASE'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const loader = createMysqlLoader({
|
|
||||||
connection: process.env['MYSQL_URL'] ?? {
|
|
||||||
host: process.env['MYSQL_HOST'],
|
|
||||||
port: process.env['MYSQL_PORT'] ? Number.parseInt(process.env['MYSQL_PORT'], 10) : undefined,
|
|
||||||
user: process.env['MYSQL_USER'],
|
|
||||||
password: process.env['MYSQL_PASSWORD'],
|
|
||||||
database: process.env['MYSQL_DATABASE'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const initializeStorage: () => Promise<Storage> = storage.initializeStorage;
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const loadableExtensions: string[] = loader.loadableExtensions;
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const loadMigration: (migration: MigrationMetadata) => Awaitable<MigrationFunction> = loader.loadMigration;
|
|
||||||
|
|
||||||
const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = {
|
const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = {
|
||||||
initializeStorage,
|
initializeStorage,
|
||||||
loadableExtensions,
|
loadableExtensions,
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/* eslint @typescript-eslint/naming-convention:0, import/no-extraneous-dependencies: 0 */
|
|
||||||
import process from 'node:process';
|
|
||||||
import { GenericContainer, type StartedTestContainer } from 'testcontainers';
|
|
||||||
|
|
||||||
let container: StartedTestContainer | undefined;
|
|
||||||
|
|
||||||
export const startDatabase = async (): Promise<{ port: number; host: string }> => {
|
|
||||||
if (process.env['CI']) {
|
|
||||||
const config = {
|
|
||||||
port: process.env['MYSQL_PORT'] ? Number.parseInt(process.env['MYSQL_PORT'], 10) : 3306,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
||||||
host: process.env['MYSQL_HOST'] || 'localhost',
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`Connecting to MySQL from environment variables: ${JSON.stringify(config)}`);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.log('Starting MySQL container...');
|
|
||||||
const containerSetup = new GenericContainer('mysql:8.2')
|
|
||||||
.withEnvironment({
|
|
||||||
MYSQL_ROOT_PASSWORD: 'admin',
|
|
||||||
MYSQL_USER: 'emigrate',
|
|
||||||
MYSQL_PASSWORD: 'emigrate',
|
|
||||||
MYSQL_DATABASE: 'emigrate',
|
|
||||||
})
|
|
||||||
.withTmpFs({ '/var/lib/mysql': 'rw' })
|
|
||||||
.withCommand(['--sql-mode=NO_ENGINE_SUBSTITUTION', '--default-authentication-plugin=mysql_native_password'])
|
|
||||||
.withExposedPorts(3306)
|
|
||||||
.withReuse();
|
|
||||||
|
|
||||||
container = await containerSetup.start();
|
|
||||||
|
|
||||||
console.log('MySQL container started');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { port: container.getMappedPort(3306), host: container.getHost() };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stopDatabase = async (): Promise<void> => {
|
|
||||||
if (container) {
|
|
||||||
console.log('Stopping MySQL container...');
|
|
||||||
await container.stop();
|
|
||||||
console.log('MySQL container stopped');
|
|
||||||
container = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,5 @@
|
||||||
# @emigrate/plugin-generate-js
|
# @emigrate/plugin-generate-js
|
||||||
|
|
||||||
## 0.3.8
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [d779286]
|
|
||||||
- @emigrate/plugin-tools@0.9.8
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.7
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/plugin-tools@0.9.7
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/plugin-tools@0.9.6
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.3.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
- @emigrate/plugin-tools@0.9.5
|
|
||||||
|
|
||||||
## 0.3.4
|
## 0.3.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/plugin-generate-js",
|
"name": "@emigrate/plugin-generate-js",
|
||||||
"version": "0.3.8",
|
"version": "0.3.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,5 @@
|
||||||
# @emigrate/plugin-tools
|
# @emigrate/plugin-tools
|
||||||
|
|
||||||
## 0.9.8
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.9.7
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.9.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.9.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
|
|
||||||
## 0.9.4
|
## 0.9.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/plugin-tools",
|
"name": "@emigrate/plugin-tools",
|
||||||
"version": "0.9.8",
|
"version": "0.9.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ const load = async <T>(
|
||||||
*
|
*
|
||||||
* @returns A timestamp string in the format YYYYMMDDHHmmssmmm
|
* @returns A timestamp string in the format YYYYMMDDHHmmssmmm
|
||||||
*/
|
*/
|
||||||
export const getTimestampPrefix = (): string => new Date().toISOString().replaceAll(/[-:ZT.]/g, '');
|
export const getTimestampPrefix = () => new Date().toISOString().replaceAll(/[-:ZT.]/g, '');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility function to sanitize a migration name so that it can be used as a filename
|
* A utility function to sanitize a migration name so that it can be used as a filename
|
||||||
|
|
@ -212,7 +212,7 @@ export const getTimestampPrefix = (): string => new Date().toISOString().replace
|
||||||
* @param name A migration name to sanitize
|
* @param name A migration name to sanitize
|
||||||
* @returns A sanitized migration name that can be used as a filename
|
* @returns A sanitized migration name that can be used as a filename
|
||||||
*/
|
*/
|
||||||
export const sanitizeMigrationName = (name: string): string =>
|
export const sanitizeMigrationName = (name: string) =>
|
||||||
name
|
name
|
||||||
.replaceAll(/[\W/\\:|*?'"<>_]+/g, '_')
|
.replaceAll(/[\W/\\:|*?'"<>_]+/g, '_')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,5 @@
|
||||||
# @emigrate/postgres
|
# @emigrate/postgres
|
||||||
|
|
||||||
## 0.3.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
- Updated dependencies [d779286]
|
|
||||||
- @emigrate/plugin-tools@0.9.8
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/plugin-tools@0.9.7
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 4442604: Automatically create the database if it doesn't exist, and the user have the permissions to do so
|
|
||||||
|
|
||||||
## 0.2.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/plugin-tools@0.9.6
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.2.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- f8a5cc7: Make sure the storage initialization crashes when a database connection can't be established
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
- @emigrate/plugin-tools@0.9.5
|
|
||||||
|
|
||||||
## 0.2.4
|
## 0.2.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/postgres",
|
"name": "@emigrate/postgres",
|
||||||
"version": "0.3.2",
|
"version": "0.2.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing migration history. Can load and generate .sql migration files.",
|
"description": "A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing migration history. Can load and generate .sql migration files.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import {
|
||||||
type GeneratorPlugin,
|
type GeneratorPlugin,
|
||||||
type SerializedError,
|
type SerializedError,
|
||||||
type MigrationHistoryEntry,
|
type MigrationHistoryEntry,
|
||||||
type Awaitable,
|
|
||||||
type MigrationFunction,
|
|
||||||
} from '@emigrate/types';
|
} from '@emigrate/types';
|
||||||
|
|
||||||
const defaultTable = 'migrations';
|
const defaultTable = 'migrations';
|
||||||
|
|
@ -34,12 +32,12 @@ export type PostgresLoaderOptions = {
|
||||||
connection: ConnectionOptions | string;
|
connection: ConnectionOptions | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPool = async (connection: ConnectionOptions | string): Promise<Sql> => {
|
const getPool = (connection: ConnectionOptions | string) => {
|
||||||
const sql = typeof connection === 'string' ? postgres(connection) : postgres(connection);
|
if (typeof connection === 'string') {
|
||||||
|
return postgres(connection);
|
||||||
|
}
|
||||||
|
|
||||||
await sql`SELECT 1`;
|
return postgres(connection);
|
||||||
|
|
||||||
return sql;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const lockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => {
|
const lockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => {
|
||||||
|
|
@ -94,64 +92,6 @@ const deleteMigration = async (sql: Sql, table: string, migration: MigrationMeta
|
||||||
return result.count === 1;
|
return result.count === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseName = (config: ConnectionOptions | string) => {
|
|
||||||
if (typeof config === 'string') {
|
|
||||||
const uri = new URL(config);
|
|
||||||
|
|
||||||
return uri.pathname.replace(/^\//u, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return config.database ?? '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDatabaseName = <T extends ConnectionOptions | string>(config: T, databaseName: string): T => {
|
|
||||||
if (typeof config === 'string') {
|
|
||||||
const uri = new URL(config);
|
|
||||||
|
|
||||||
uri.pathname = `/${databaseName}`;
|
|
||||||
|
|
||||||
return uri.toString() as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof config === 'object') {
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
database: databaseName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid connection config');
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeDatabase = async (config: ConnectionOptions | string) => {
|
|
||||||
let sql: Sql | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
sql = await getPool(config);
|
|
||||||
await sql.end();
|
|
||||||
} catch (error) {
|
|
||||||
await sql?.end();
|
|
||||||
|
|
||||||
// The error code 3D000 means that the database does not exist, but the user might have the permissions to create it
|
|
||||||
if (error && typeof error === 'object' && 'code' in error && error.code === '3D000') {
|
|
||||||
const databaseName = getDatabaseName(config);
|
|
||||||
|
|
||||||
const postgresConfig = setDatabaseName(config, 'postgres');
|
|
||||||
|
|
||||||
const postgresSql = await getPool(postgresConfig);
|
|
||||||
try {
|
|
||||||
await postgresSql`CREATE DATABASE ${postgresSql(databaseName)}`;
|
|
||||||
// Any database creation error here will be propagated
|
|
||||||
} finally {
|
|
||||||
await postgresSql.end();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// In this case we don't know how to handle the error, so we rethrow it
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeTable = async (sql: Sql, table: string) => {
|
const initializeTable = async (sql: Sql, table: string) => {
|
||||||
const [row] = await sql<Array<{ exists: 1 }>>`
|
const [row] = await sql<Array<{ exists: 1 }>>`
|
||||||
SELECT 1 as exists
|
SELECT 1 as exists
|
||||||
|
|
@ -182,9 +122,7 @@ export const createPostgresStorage = ({
|
||||||
}: PostgresStorageOptions): EmigrateStorage => {
|
}: PostgresStorageOptions): EmigrateStorage => {
|
||||||
return {
|
return {
|
||||||
async initializeStorage() {
|
async initializeStorage() {
|
||||||
await initializeDatabase(connection);
|
const sql = getPool(connection);
|
||||||
|
|
||||||
const sql = await getPool(connection);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initializeTable(sql, table);
|
await initializeTable(sql, table);
|
||||||
|
|
@ -257,12 +195,23 @@ export const createPostgresStorage = ({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const { initializeStorage } = createPostgresStorage({
|
||||||
|
table: process.env['POSTGRES_TABLE'],
|
||||||
|
connection: process.env['POSTGRES_URL'] ?? {
|
||||||
|
host: process.env['POSTGRES_HOST'],
|
||||||
|
port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
|
||||||
|
user: process.env['POSTGRES_USER'],
|
||||||
|
password: process.env['POSTGRES_PASSWORD'],
|
||||||
|
database: process.env['POSTGRES_DB'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const createPostgresLoader = ({ connection }: PostgresLoaderOptions): LoaderPlugin => {
|
export const createPostgresLoader = ({ connection }: PostgresLoaderOptions): LoaderPlugin => {
|
||||||
return {
|
return {
|
||||||
loadableExtensions: ['.sql'],
|
loadableExtensions: ['.sql'],
|
||||||
async loadMigration(migration) {
|
async loadMigration(migration) {
|
||||||
return async () => {
|
return async () => {
|
||||||
const sql = await getPool(connection);
|
const sql = getPool(connection);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error The "simple" option is not documented, but it exists
|
// @ts-expect-error The "simple" option is not documented, but it exists
|
||||||
|
|
@ -275,6 +224,16 @@ export const createPostgresLoader = ({ connection }: PostgresLoaderOptions): Loa
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const { loadableExtensions, loadMigration } = createPostgresLoader({
|
||||||
|
connection: process.env['POSTGRES_URL'] ?? {
|
||||||
|
host: process.env['POSTGRES_HOST'],
|
||||||
|
port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
|
||||||
|
user: process.env['POSTGRES_USER'],
|
||||||
|
password: process.env['POSTGRES_PASSWORD'],
|
||||||
|
database: process.env['POSTGRES_DB'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
export const generateMigration: GenerateMigrationFunction = async (name) => {
|
||||||
return {
|
return {
|
||||||
filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`,
|
filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`,
|
||||||
|
|
@ -283,34 +242,6 @@ export const generateMigration: GenerateMigrationFunction = async (name) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const storage = createPostgresStorage({
|
|
||||||
table: process.env['POSTGRES_TABLE'],
|
|
||||||
connection: process.env['POSTGRES_URL'] ?? {
|
|
||||||
host: process.env['POSTGRES_HOST'],
|
|
||||||
port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
|
|
||||||
user: process.env['POSTGRES_USER'],
|
|
||||||
password: process.env['POSTGRES_PASSWORD'],
|
|
||||||
database: process.env['POSTGRES_DB'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const loader = createPostgresLoader({
|
|
||||||
connection: process.env['POSTGRES_URL'] ?? {
|
|
||||||
host: process.env['POSTGRES_HOST'],
|
|
||||||
port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
|
|
||||||
user: process.env['POSTGRES_USER'],
|
|
||||||
password: process.env['POSTGRES_PASSWORD'],
|
|
||||||
database: process.env['POSTGRES_DB'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const initializeStorage: () => Promise<Storage> = storage.initializeStorage;
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const loadableExtensions: string[] = loader.loadableExtensions;
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
export const loadMigration: (migration: MigrationMetadata) => Awaitable<MigrationFunction> = loader.loadMigration;
|
|
||||||
|
|
||||||
const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = {
|
const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = {
|
||||||
initializeStorage,
|
initializeStorage,
|
||||||
loadableExtensions,
|
loadableExtensions,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,5 @@
|
||||||
# @emigrate/reporter-pino
|
# @emigrate/reporter-pino
|
||||||
|
|
||||||
## 0.6.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.6.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.6.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 081ab34: Make sure Pino outputs logs in Bun environments
|
|
||||||
|
|
||||||
## 0.6.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 1065322: Show correct status for migrations for the "list" and "new" commands
|
|
||||||
|
|
||||||
## 0.6.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.6.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 86e0d52: Adapt to the new Reporter interface, i.e. the removal of the "remove" command related methods
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ef45be9: Show number of skipped migrations correctly in the command output
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/reporter-pino",
|
"name": "@emigrate/reporter-pino",
|
||||||
"version": "0.6.5",
|
"version": "0.5.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "A Pino reporter for Emigrate for logging the migration process.",
|
"description": "A Pino reporter for Emigrate for logging the migration process.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
@ -41,9 +39,7 @@
|
||||||
"pino": "8.16.2"
|
"pino": "8.16.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emigrate/tsconfig": "workspace:*",
|
"@emigrate/tsconfig": "workspace:*"
|
||||||
"@types/bun": "1.0.5",
|
|
||||||
"bun-types": "1.0.26"
|
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"extends": "../../package.json"
|
"extends": "../../package.json"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ class PinoReporter implements Required<EmigrateReporter> {
|
||||||
scope: command,
|
scope: command,
|
||||||
version,
|
version,
|
||||||
},
|
},
|
||||||
transport: process.isBun ? { target: 'pino/file', options: { destination: 1 } } : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#logger.info({ parameters }, `Emigrate "${command}" initialized${parameters.dry ? ' (dry-run)' : ''}`);
|
this.#logger.info({ parameters }, `Emigrate "${command}" initialized${parameters.dry ? ' (dry-run)' : ''}`);
|
||||||
|
|
@ -70,40 +69,29 @@ class PinoReporter implements Required<EmigrateReporter> {
|
||||||
const migrations = this.#migrations ?? [];
|
const migrations = this.#migrations ?? [];
|
||||||
|
|
||||||
if (migrations.length === 0) {
|
if (migrations.length === 0) {
|
||||||
this.#logger.info('No migrations found');
|
this.#logger.info('No pending migrations found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusText = this.#command === 'list' ? 'migrations are pending' : 'pending migrations to run';
|
|
||||||
|
|
||||||
if (migrations.length === lockedMigrations.length) {
|
if (migrations.length === lockedMigrations.length) {
|
||||||
this.#logger.info({ migrationCount: lockedMigrations.length }, `${lockedMigrations.length} ${statusText}`);
|
this.#logger.info(
|
||||||
|
{ migrationCount: lockedMigrations.length },
|
||||||
|
`${lockedMigrations.length} pending migrations to run`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let skippedCount = 0;
|
const nonLockedMigrations = migrations.filter(
|
||||||
let failedCount = 0;
|
(migration) => !lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name),
|
||||||
|
);
|
||||||
for (const migration of migrations) {
|
const failedMigrations = nonLockedMigrations.filter(
|
||||||
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
(migration) => 'status' in migration && migration.status === 'failed',
|
||||||
|
);
|
||||||
if (isLocked) {
|
const unlockableCount = this.#command === 'up' ? nonLockedMigrations.length - failedMigrations.length : 0;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('status' in migration) {
|
|
||||||
if (migration.status === 'failed') {
|
|
||||||
failedCount += 1;
|
|
||||||
} else if (migration.status === 'skipped') {
|
|
||||||
skippedCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = [
|
const parts = [
|
||||||
`${lockedMigrations.length} of ${migrations.length} ${statusText}`,
|
`${lockedMigrations.length} of ${migrations.length} pending migrations to run`,
|
||||||
skippedCount > 0 ? `(${skippedCount} skipped)` : '',
|
unlockableCount > 0 ? `(${unlockableCount} locked)` : '',
|
||||||
failedCount > 0 ? `(${failedCount} failed)` : '',
|
failedMigrations.length > 0 ? `(${failedMigrations.length} failed)` : '',
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
this.#logger.info({ migrationCount: lockedMigrations.length }, parts.join(' '));
|
this.#logger.info({ migrationCount: lockedMigrations.length }, parts.join(' '));
|
||||||
|
|
@ -116,28 +104,27 @@ class PinoReporter implements Required<EmigrateReporter> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||||
|
this.#logger.debug({ migration: migration.relativeFilePath }, `Removing migration: ${migration.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||||
|
this.#logger.info({ migration: migration.relativeFilePath }, `Successfully removed migration: ${migration.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMigrationRemoveError(migration: MigrationMetadataFinished, error: Error): Awaitable<void> {
|
||||||
|
this.#logger.error(
|
||||||
|
{ migration: migration.relativeFilePath, [this.errorKey]: error },
|
||||||
|
`Failed to remove migration: ${migration.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onMigrationStart(migration: MigrationMetadata): Awaitable<void> {
|
onMigrationStart(migration: MigrationMetadata): Awaitable<void> {
|
||||||
let status = 'running';
|
this.#logger.info({ migration: migration.relativeFilePath }, `${migration.name} (running)`);
|
||||||
|
|
||||||
if (this.#command === 'remove') {
|
|
||||||
status = 'removing';
|
|
||||||
} else if (this.#command === 'new') {
|
|
||||||
status = 'creating';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#logger.info({ migration: migration.relativeFilePath }, `${migration.name} (${status})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
onMigrationSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||||
let status = 'done';
|
this.#logger.info({ migration: migration.relativeFilePath }, `${migration.name} (${migration.status})`);
|
||||||
|
|
||||||
if (this.#command === 'remove') {
|
|
||||||
status = 'removed';
|
|
||||||
} else if (this.#command === 'new') {
|
|
||||||
status = 'created';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#logger.info({ migration: migration.relativeFilePath }, `${migration.name} (${status})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationError(migration: MigrationMetadataFinished, error: Error): Awaitable<void> {
|
onMigrationError(migration: MigrationMetadataFinished, error: Error): Awaitable<void> {
|
||||||
|
|
@ -187,15 +174,16 @@ class PinoReporter implements Required<EmigrateReporter> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
|
||||||
this.#command === 'remove'
|
|
||||||
? { removed: done, failed, skipped, pending, total }
|
|
||||||
: { done, failed, skipped, pending, total };
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.#logger.error({ result, [this.errorKey]: error }, `Emigrate "${this.#command}" failed`);
|
this.#logger.error(
|
||||||
|
{ result: { failed, done, skipped, pending, total }, [this.errorKey]: error },
|
||||||
|
`Emigrate "${this.#command}" failed`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.#logger.info({ result }, `Emigrate "${this.#command}" finished successfully`);
|
this.#logger.info(
|
||||||
|
{ result: { failed, done, skipped, pending, total } },
|
||||||
|
`Emigrate "${this.#command}" finished successfully`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,8 +192,6 @@ export const createPinoReporter = (options: PinoReporterOptions = {}): EmigrateR
|
||||||
return new PinoReporter(options);
|
return new PinoReporter(options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultExport: EmigrateReporter = createPinoReporter({
|
export default createPinoReporter({
|
||||||
level: process.env['LOG_LEVEL'],
|
level: process.env['LOG_LEVEL'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defaultExport;
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,5 @@
|
||||||
# @emigrate/storage-fs
|
# @emigrate/storage-fs
|
||||||
|
|
||||||
## 0.4.7
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
- Updated dependencies [ca154fa]
|
|
||||||
- @emigrate/types@0.12.2
|
|
||||||
|
|
||||||
## 0.4.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
- Updated dependencies [db656c2]
|
|
||||||
- @emigrate/types@0.12.1
|
|
||||||
|
|
||||||
## 0.4.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [94ad9fe]
|
|
||||||
- @emigrate/types@0.12.0
|
|
||||||
|
|
||||||
## 0.4.4
|
## 0.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/storage-fs",
|
"name": "@emigrate/storage-fs",
|
||||||
"version": "0.4.7",
|
"version": "0.4.4",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "A storage plugin for Emigrate for storing the migration history in a file",
|
"description": "A storage plugin for Emigrate for storing the migration history in a file",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
# @emigrate/tsconfig
|
# @emigrate/tsconfig
|
||||||
|
|
||||||
## 1.0.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- d779286: Upgrade TypeScript to v5.5 and enable [isolatedDeclarations](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations)
|
|
||||||
|
|
||||||
## 1.0.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"inlineSources": false,
|
"inlineSources": false,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"isolatedDeclarations": true,
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
|
@ -32,7 +31,5 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
||||||
},
|
}
|
||||||
"include": ["${configDir}/src"],
|
|
||||||
"exclude": ["${configDir}/dist"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
"display": "Build",
|
"display": "Build",
|
||||||
"extends": "./base.json",
|
"extends": "./base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": false,
|
"noEmit": false
|
||||||
"outDir": "${configDir}/dist"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/tsconfig",
|
"name": "@emigrate/tsconfig",
|
||||||
"version": "1.0.3",
|
"version": "1.0.1",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"base.json",
|
"base.json",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,5 @@
|
||||||
# @emigrate/types
|
# @emigrate/types
|
||||||
|
|
||||||
## 0.12.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- ca154fa: Minimize package size by excluding \*.tsbuildinfo files
|
|
||||||
|
|
||||||
## 0.12.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- db656c2: Enable NPM provenance
|
|
||||||
|
|
||||||
## 0.12.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 94ad9fe: Remove the "remove" command specific reporter methods. So instead of using `onMigrationRemoveStart`, `onMigrationRemoveSuccess` and `onMigrationRemoveError` the `onMigrationStart`, `onMigrationSuccess` and `onMigrationError` methods should be used and the reporter can still format the output differently depending on the current command (which it receives in the `onInit` method). This is a BREAKING CHANGE.
|
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@emigrate/types",
|
"name": "@emigrate/types",
|
||||||
"version": "0.12.2",
|
"version": "0.11.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public"
|
||||||
"provenance": true
|
|
||||||
},
|
},
|
||||||
"description": "Common Emigrate TypeScript types to ease plugin development.",
|
"description": "Common Emigrate TypeScript types to ease plugin development.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -16,8 +15,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"!dist/*.tsbuildinfo"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --pretty",
|
"build": "tsc --pretty",
|
||||||
|
|
|
||||||
|
|
@ -272,20 +272,36 @@ export type EmigrateReporter = Partial<{
|
||||||
* This is only called when the command is 'new'.
|
* This is only called when the command is 'new'.
|
||||||
*/
|
*/
|
||||||
onNewMigration(migration: MigrationMetadata, content: string): Awaitable<void>;
|
onNewMigration(migration: MigrationMetadata, content: string): Awaitable<void>;
|
||||||
|
/**
|
||||||
|
* Called when a migration is about to be removed from the migration history.
|
||||||
|
*
|
||||||
|
* This is only called when the command is 'remove'.
|
||||||
|
*/
|
||||||
|
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void>;
|
||||||
|
/**
|
||||||
|
* Called when a migration is successfully removed from the migration history.
|
||||||
|
*
|
||||||
|
* This is only called when the command is 'remove'.
|
||||||
|
*/
|
||||||
|
onMigrationRemoveSuccess(migration: SuccessfulMigrationMetadata): Awaitable<void>;
|
||||||
|
/**
|
||||||
|
* Called when a migration couldn't be removed from the migration history.
|
||||||
|
*
|
||||||
|
* This is only called when the command is 'remove'.
|
||||||
|
*/
|
||||||
|
onMigrationRemoveError(migration: FailedMigrationMetadata, error: Error): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration is about to be executed.
|
* Called when a migration is about to be executed.
|
||||||
*
|
*
|
||||||
* Will be called for each migration when the command is "up",
|
* Will only be called for each migration when the command is "up".
|
||||||
* or before removing each migration from the history when the command is "remove".
|
|
||||||
*
|
*
|
||||||
* @param migration Information about the migration that is about to be executed/removed.
|
* @param migration Information about the migration that is about to be executed.
|
||||||
*/
|
*/
|
||||||
onMigrationStart(migration: MigrationMetadata): Awaitable<void>;
|
onMigrationStart(migration: MigrationMetadata): Awaitable<void>;
|
||||||
/**
|
/**
|
||||||
* Called when a migration has been successfully executed.
|
* Called when a migration has been successfully executed.
|
||||||
*
|
*
|
||||||
* Will be called after a successful migration when the command is "up",
|
* Will be called after a successful migration when the command is "up"
|
||||||
* or after a successful removal of a migration from the history when the command is "remove",
|
|
||||||
* or for each successful migration from the history when the command is "list".
|
* or for each successful migration from the history when the command is "list".
|
||||||
*
|
*
|
||||||
* @param migration Information about the migration that was executed.
|
* @param migration Information about the migration that was executed.
|
||||||
|
|
@ -294,8 +310,7 @@ export type EmigrateReporter = Partial<{
|
||||||
/**
|
/**
|
||||||
* Called when a migration has failed.
|
* Called when a migration has failed.
|
||||||
*
|
*
|
||||||
* Will be called after a failed migration when the command is "up",
|
* Will be called after a failed migration when the command is "up"
|
||||||
* or after a failed removal of a migration from the history when the command is "remove",
|
|
||||||
* or for each failed migration from the history when the command is "list" (will be at most one in this case).
|
* or for each failed migration from the history when the command is "list" (will be at most one in this case).
|
||||||
*
|
*
|
||||||
* @param migration Information about the migration that failed.
|
* @param migration Information about the migration that failed.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": "@emigrate/tsconfig/build.json"
|
"extends": "@emigrate/tsconfig/build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11943
pnpm-lock.yaml
generated
11943
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://turborepo.org/schema.json",
|
"$schema": "https://turborepo.org/schema.json",
|
||||||
"ui": "stream",
|
"pipeline": {
|
||||||
"tasks": {
|
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"inputs": ["src/**/*", "!src/**/*.test.ts", "tsconfig.json", "tsconfig.build.json"],
|
"inputs": ["src/**/*", "!src/**/*.test.ts", "tsconfig.json", "tsconfig.build.json"],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue