From 66377b15985504fde53d04dd56e99f401885b3c6 Mon Sep 17 00:00:00 2001 From: Dimi Kot Date: Sat, 28 Sep 2024 20:35:48 -0700 Subject: [PATCH] v2.10.297: Improve CLI UI --- .eslintrc.base.js | 9 + .npmignore | 2 +- .npmrc | 1 - .prettierrc | 8 + docs/interfaces/MigrateOptions.md | 127 +++++++++ docs/modules.md | 26 +- internal/build.sh | 4 + internal/clean.sh | 2 +- internal/deploy.sh | 5 +- internal/dev.sh | 4 + internal/test.sh | 5 + jest.config.base.js | 13 + jest.config.js | 13 +- package.json | 22 +- src/cli.ts | 248 +++++++++++------- src/internal/Patch.ts | 6 +- src/internal/Registry.ts | 17 +- .../helpers/__tests__/collapse.test.ts | 30 +++ .../__tests__/schemaNameMatchesPrefix.test.ts | 11 + src/internal/helpers/collapse.ts | 26 +- .../helpers/schemaNameMatchesPrefix.ts | 20 ++ src/internal/render.ts | 23 +- tsconfig.json | 5 +- 23 files changed, 459 insertions(+), 168 deletions(-) delete mode 100644 .npmrc create mode 100644 .prettierrc create mode 100644 docs/interfaces/MigrateOptions.md create mode 100644 internal/build.sh create mode 100644 internal/dev.sh create mode 100644 internal/test.sh create mode 100644 jest.config.base.js create mode 100644 src/internal/helpers/__tests__/collapse.test.ts create mode 100644 src/internal/helpers/__tests__/schemaNameMatchesPrefix.test.ts create mode 100644 src/internal/helpers/schemaNameMatchesPrefix.ts diff --git a/.eslintrc.base.js b/.eslintrc.base.js index 9684533..ebe20a4 100644 --- a/.eslintrc.base.js +++ b/.eslintrc.base.js @@ -38,6 +38,7 @@ module.exports = (projectRoot, extraRules = {}) => ({ "typescript-enum", "typescript-sort-keys", "unused-imports", + "no-only-tests", ], settings: { react: { @@ -109,6 +110,10 @@ module.exports = (projectRoot, extraRules = {}) => ({ "@typescript-eslint/no-useless-constructor": ["error"], "@typescript-eslint/prefer-optional-chain": ["error"], "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/require-array-sort-compare": [ + "error", + { ignoreStringArrays: true }, + ], eqeqeq: ["error"], "object-shorthand": ["error", "always"], "@typescript-eslint/unbound-method": ["error"], @@ -380,6 +385,7 @@ module.exports = (projectRoot, extraRules = {}) => ({ }, }, ], + "import/no-useless-path-segments": ["error", { noUselessIndex: true }], "unused-imports/no-unused-imports": "error", "no-restricted-imports": [ "error", @@ -430,6 +436,9 @@ module.exports = (projectRoot, extraRules = {}) => ({ ], quotes: ["error", "double", { avoidEscape: true }], + + "no-only-tests/no-only-tests": "error", + ...extraRules, }, }); diff --git a/.npmignore b/.npmignore index 7ec72e7..29f2edc 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ dist/__tests__ dist/**/__tests__ -dist/tsconfig.tsbuildinfo +dist/*.tsbuildinfo .npmrc # Common in both .gitignore and .npmignore diff --git a/.npmrc b/.npmrc deleted file mode 100644 index c52ad5f..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -# Published to https://www.npmjs.com diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f5688b5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "options": { "parser": "typescript" } + } + ] +} diff --git a/docs/interfaces/MigrateOptions.md b/docs/interfaces/MigrateOptions.md new file mode 100644 index 0000000..e845100 --- /dev/null +++ b/docs/interfaces/MigrateOptions.md @@ -0,0 +1,127 @@ +[@clickup/pg-mig](../README.md) / [Exports](../modules.md) / MigrateOptions + +# Interface: MigrateOptions + +Options for the migrate() function. + +## Properties + +### migDir + +• **migDir**: `string` + +The directory the migration versions are loaded from. + +#### Defined in + +[src/cli.ts:27](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L27) + +___ + +### hosts + +• **hosts**: `string`[] + +List of PostgreSQL master hostnames. The migration versions in `migDir` +will be applied to all of them. + +#### Defined in + +[src/cli.ts:30](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L30) + +___ + +### port + +• **port**: `number` + +PostgreSQL port on each hosts. + +#### Defined in + +[src/cli.ts:32](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L32) + +___ + +### user + +• **user**: `string` + +PostgreSQL user on each host. + +#### Defined in + +[src/cli.ts:34](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L34) + +___ + +### pass + +• **pass**: `string` + +PostgreSQL password on each host. + +#### Defined in + +[src/cli.ts:36](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L36) + +___ + +### db + +• **db**: `string` + +PostgreSQL database name on each host. + +#### Defined in + +[src/cli.ts:38](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L38) + +___ + +### parallelism + +• `Optional` **parallelism**: `number` + +How many schemas to process in parallel (defaults to 10). + +#### Defined in + +[src/cli.ts:40](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L40) + +___ + +### dry + +• `Optional` **dry**: `boolean` + +If true, prints what it plans to do, but doesn't change anything. + +#### Defined in + +[src/cli.ts:42](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L42) + +___ + +### ci + +• `Optional` **ci**: `boolean` + +If true, then doesn't use log-update and doesn't replace lines; instead, +prints logs to stdout line by line. + +#### Defined in + +[src/cli.ts:45](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L45) + +___ + +### action + +• **action**: \{ `type`: ``"make"`` ; `name`: `string` } \| \{ `type`: ``"list"`` } \| \{ `type`: ``"undo"`` ; `version`: `string` } \| \{ `type`: ``"apply"`` } + +What to do. + +#### Defined in + +[src/cli.ts:47](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L47) diff --git a/docs/modules.md b/docs/modules.md index b8d2051..63d7a2c 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -2,6 +2,10 @@ # @clickup/pg-mig +## Interfaces + +- [MigrateOptions](interfaces/MigrateOptions.md) + ## Functions ### main @@ -31,7 +35,7 @@ pg-mig #### Defined in -[src/cli.ts:39](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L39) +[src/cli.ts:72](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L72) ___ @@ -44,21 +48,9 @@ This function is meant to be called from other tools. #### Parameters -| Name | Type | Description | -| :------ | :------ | :------ | -| `options` | `Object` | - | -| `options.migDir` | `string` | The directory the migration versions are loaded from. | -| `options.hosts` | `string`[] | List of PostgreSQL master hostnames. The migration versions in `migDir` will be applied to all of them. | -| `options.port` | `number` | PostgreSQL port on each hosts. | -| `options.user` | `string` | PostgreSQL user on each host. | -| `options.pass` | `string` | PostgreSQL password on each host. | -| `options.db` | `string` | PostgreSQL database name on each host. | -| `options.parallelism?` | `number` | How many schemas to process in parallel (defaults to 10). | -| `options.undo?` | `string` | If passed, switches the action to undo the provided migration version. | -| `options.make?` | `string` | If passed, switches the action to create a new migration version. | -| `options.dry?` | `boolean` | If true, prints what it plans to do, but doesn't change anything. | -| `options.list?` | `boolean` | Lists all versions in `migDir`. | -| `options.ci?` | `boolean` | If true, then doesn't use logUpdate() and doesn't replace lines; instead, prints logs to stdout line by line. | +| Name | Type | +| :------ | :------ | +| `options` | [`MigrateOptions`](interfaces/MigrateOptions.md) | #### Returns @@ -66,4 +58,4 @@ This function is meant to be called from other tools. #### Defined in -[src/cli.ts:79](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L79) +[src/cli.ts:117](https://github.com/clickup/pg-mig/blob/master/src/cli.ts#L117) diff --git a/internal/build.sh b/internal/build.sh new file mode 100644 index 0000000..fbbef68 --- /dev/null +++ b/internal/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +tsc --build diff --git a/internal/clean.sh b/internal/clean.sh index f82a4a1..c50cecc 100644 --- a/internal/clean.sh +++ b/internal/clean.sh @@ -1,4 +1,4 @@ #!/bin/bash set -e -rm -rf dist yarn.lock package-lock.json pnpm-lock.yaml *.log +rm -rf dist dist.* yarn.lock package-lock.json pnpm-lock.yaml node_modules ./*.log diff --git a/internal/deploy.sh b/internal/deploy.sh index 7b12dc6..834cd67 100644 --- a/internal/deploy.sh +++ b/internal/deploy.sh @@ -1,7 +1,8 @@ #!/bin/bash set -e -npm run build -npm run lint +npm run clean +npm install npm run test +npm run lint npm publish --access=public diff --git a/internal/dev.sh b/internal/dev.sh new file mode 100644 index 0000000..c3751b6 --- /dev/null +++ b/internal/dev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +tsc --watch --preserveWatchOutput diff --git a/internal/test.sh b/internal/test.sh new file mode 100644 index 0000000..c8eb6f2 --- /dev/null +++ b/internal/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +bash internal/build.sh +jest diff --git a/jest.config.base.js b/jest.config.base.js new file mode 100644 index 0000000..cee966c --- /dev/null +++ b/jest.config.base.js @@ -0,0 +1,13 @@ +"use strict"; +module.exports = { + roots: ["/src"], + testMatch: ["**/*.test.ts"], + clearMocks: true, + restoreMocks: true, + ...(process.env.IN_JEST_PROJECT + ? {} + : { forceExit: true, testTimeout: 30000 }), + transform: { + "\\.ts$": "ts-jest", + }, +}; diff --git a/jest.config.js b/jest.config.js index c0ea465..208e748 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,2 @@ "use strict"; -module.exports = { - roots: ["/src"], - testMatch: ["**/*.test.ts"], - clearMocks: true, - restoreMocks: true, - ...(process.env.IN_JEST_PROJECT - ? {} - : { forceExit: true, testTimeout: 30000, forceExit: true }), - transform: { - "\\.ts$": "ts-jest", - }, -}; +module.exports = { ...require("./jest.config.base") }; diff --git a/package.json b/package.json index bccc46c..5dbcfdb 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,31 @@ { "name": "@clickup/pg-mig", "description": "PostgreSQL schema migration tool with micro-sharding and clustering support", - "version": "2.10.296", + "version": "2.10.297", "license": "MIT", + "keywords": [ + "postgresql", + "postgres", + "database", + "schema", + "migration", + "micro-sharding", + "clustering", + "DDL", + "psql", + "database-versioning", + "schema-versioning" + ], "main": "dist/cli.js", "types": "dist/cli.d.ts", "bin": { "pg-mig": "./dist/cli.js" }, "scripts": { - "build": "tsc", - "dev": "tsc --watch --preserveWatchOutput", + "build": "bash internal/build.sh", + "dev": "bash internal/dev.sh", "lint": "bash internal/lint.sh", - "test": "jest", + "test": "bash internal/test.sh", "docs": "bash internal/docs.sh", "clean": "bash internal/clean.sh", "copy-package-to-public-dir": "copy-package-to-public-dir.sh", @@ -44,6 +57,7 @@ "eslint-import-resolver-typescript": "^3.5.5", "eslint-plugin-import": "^2.27.5", "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react": "^7.32.2", diff --git a/src/cli.ts b/src/cli.ts index 54c159f..0fd916b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { basename } from "path"; import sortBy from "lodash/sortBy"; import throttle from "lodash/throttle"; @@ -18,6 +19,38 @@ import { renderPatchSummary, } from "./internal/render"; +/** + * Options for the migrate() function. + */ +export interface MigrateOptions { + /** The directory the migration versions are loaded from. */ + migDir: string; + /** List of PostgreSQL master hostnames. The migration versions in `migDir` + * will be applied to all of them. */ + hosts: string[]; + /** PostgreSQL port on each hosts. */ + port: number; + /** PostgreSQL user on each host. */ + user: string; + /** PostgreSQL password on each host. */ + pass: string; + /** PostgreSQL database name on each host. */ + db: string; + /** How many schemas to process in parallel (defaults to 10). */ + parallelism?: number; + /** If true, prints what it plans to do, but doesn't change anything. */ + dry?: boolean; + /** If true, then doesn't use log-update and doesn't replace lines; instead, + * prints logs to stdout line by line. */ + ci?: boolean; + /** What to do. */ + action: + | { type: "make"; name: string } + | { type: "list" } + | { type: "undo"; version: string } + | { type: "apply" }; +} + /** * CLI tool entry point. This function is run when `pg-mig` is called from the * command line. Accepts parameters from process.argv. See `migrate()` for @@ -63,12 +96,17 @@ export async function main(): Promise { user: args.get("user", process.env["PGUSER"] || ""), pass: args.get("pass", process.env["PGPASSWORD"] || ""), db: args.get("db", process.env["PGDATABASE"]), - undo: args.getOptional("undo"), - make: args.getOptional("make"), parallelism: parseInt(args.get("parallelism", "0")) || undefined, dry: args.flag("dry"), - list: args.flag("list"), ci: args.flag("ci"), + action: + args.getOptional("make") !== undefined + ? { type: "make", name: args.get("make") } + : args.flag("list") + ? { type: "list" } + : args.getOptional("undo") !== undefined + ? { type: "undo", version: args.get("undo") } + : { type: "apply" }, }); } @@ -76,112 +114,123 @@ export async function main(): Promise { * Similar to main(), but accepts options explicitly, not from process.argv. * This function is meant to be called from other tools. */ -export async function migrate(options: { - /** The directory the migration versions are loaded from. */ - migDir: string; - /** List of PostgreSQL master hostnames. The migration versions in `migDir` - * will be applied to all of them. */ - hosts: string[]; - /** PostgreSQL port on each hosts. */ - port: number; - /** PostgreSQL user on each host. */ - user: string; - /** PostgreSQL password on each host. */ - pass: string; - /** PostgreSQL database name on each host. */ - db: string; - /** How many schemas to process in parallel (defaults to 10). */ - parallelism?: number; - /** If passed, switches the action to undo the provided migration version. */ - undo?: string; - /** If passed, switches the action to create a new migration version. */ - make?: string; - /** If true, prints what it plans to do, but doesn't change anything. */ - dry?: boolean; - /** Lists all versions in `migDir`. */ - list?: boolean; - /** If true, then doesn't use logUpdate() and doesn't replace lines; instead, - * prints logs to stdout line by line. */ - ci?: boolean; -}): Promise { - const hostDests = options.hosts.map( - (host) => - new Dest( - host, - options.port, - options.user, - options.pass, - options.db, - "public", - ), - ); +export async function migrate(options: MigrateOptions): Promise { const registry = new Registry(options.migDir); printText(`Running on ${options.hosts}:${options.port} ${options.db}`); - if (options.make !== undefined) { - // example: create_table_x@sh - const [migrationName, schemaPrefix] = options.make.split("@"); - const usage = "Format: --make=migration_name@schema_prefix"; + if (options.action.type === "make") { + return actionMake(options, registry, options.action.name); + } - if (!migrationName?.match(/^[a-z0-9_]+$/)) { - printError("migration_name is missing or incorrect"); - printText(usage); - return false; - } + if (options.action.type === "list") { + return actionList(options, registry); + } - if (!schemaPrefix) { - printError("schema_prefix is missing"); - printText(usage); - return false; + while (true) { + const { success, hasMoreWork } = await actionUndoOrApply(options, registry); + if (!success || !hasMoreWork) { + return success; } + } +} - if (!registry.prefixes.includes(schemaPrefix)) { - printText( - `WARNING: schema prefix "${schemaPrefix}" wasn't found. Valid prefixes:`, - ); - for (const prefix of registry.prefixes) { - printText(`- ${prefix}`); - } - } +/** + * Makes new migration files. + */ +async function actionMake( + options: Exclude, + registry: Registry, + name: string, +): Promise { + const [migrationName, schemaPrefix] = name.split("@"); + const usage = "Format: --make=migration_name@schema_prefix"; + + if (!migrationName?.match(/^[a-z0-9_]+$/)) { + printError("migration_name is missing or incorrect"); + printText(usage); + return false; + } + + if (!schemaPrefix) { + printError("schema_prefix is missing"); + printText(usage); + return false; + } - printText("\nMaking migration files..."); - const createdFiles = await makeMigration( - options.migDir, - migrationName, - schemaPrefix, + if (!registry.prefixes.includes(schemaPrefix)) { + printText( + `WARNING: schema prefix "${schemaPrefix}" wasn't found. Valid prefixes:`, ); - for (const file of createdFiles) { - printText(file); + for (const prefix of registry.prefixes) { + printText(`- ${prefix}`); } + } - return true; + printText("\nMaking migration files..."); + const createdFiles = await makeMigration( + options.migDir, + migrationName, + schemaPrefix, + ); + for (const file of createdFiles) { + printText(file); } - if (options.list) { - printText("All versions:"); + return true; +} - for (const version of sortBy(registry.getVersions())) { - printText(` > ${version}`); - } +/** + * Prints the list of all migration versions in the registry. + */ +async function actionList( + _options: MigrateOptions, + registry: Registry, +): Promise { + printText("All versions:"); - return true; + for (const version of sortBy(registry.getVersions())) { + printText(` > ${version}`); } - if (options.undo === "") { + return true; +} + +/** + * Applies or undoes migrations. + */ +async function actionUndoOrApply( + options: MigrateOptions, + registry: Registry, +): Promise<{ success: boolean; hasMoreWork: boolean }> { + const hostDests = options.hosts.map( + (host) => + new Dest( + host, + options.port, + options.user, + options.pass, + options.db, + "public", + ), + ); + + if (options.action.type === "undo" && !options.action.version) { printText(await renderLatestVersions(hostDests, registry)); printError("Please provide a migration version to undo."); - return false; + return { success: false, hasMoreWork: false }; } - const patch = new Patch(hostDests, registry, { undo: options.undo }); + const patch = new Patch(hostDests, registry, { + undo: options.action.type === "undo" ? options.action.version : undefined, + }); const chains = await patch.getChains(); - const [summary, hasWork] = renderPatchSummary(chains); - if (!hasWork || options.dry) { + const summary = renderPatchSummary(chains); + if (chains.length === 0 || options.dry) { printText(await renderLatestVersions(hostDests, registry)); printText(summary); - return true; + return { success: true, hasMoreWork: false }; } printText(summary); @@ -219,29 +268,46 @@ export async function migrate(options: { afterChains, ); + const progress = options.ci + ? null + : logUpdate.create(process.stdout, { showCursor: true }); + const success = await grid.run( throttle(() => { - const lines = renderGrid(grid).split("\n"); - if (!options.ci) { - logUpdate(lines.slice(0, (process.stdout.rows || 20) - 1).join("\n")); + const lines = renderGrid(grid); + if (lines.length > 0) { + progress?.( + lines + .slice(0, Math.max((process.stdout.rows || 25) - 3, 3)) + .join("\n"), + ); + } else { + progress?.clear(); } }, 100), ); - if (!options.ci) { - logUpdate.clear(); - } + progress?.clear(); const errors = renderGrid(grid); - if (errors) { + if (errors.length > 0) { printText("\n" + errors); printError("Failed"); } else { printSuccess("Succeeded."); } - return success; + return { + success, + hasMoreWork: + options.action.type === "apply" && success + ? (await patch.getChains()).length > 0 + : false, + }; } +/** + * Entry point for the CLI tool. + */ if (require.main === module) { main() .then((success) => process.exit(success ? 0 : 1)) diff --git a/src/internal/Patch.ts b/src/internal/Patch.ts index 9822f9e..1f15b20 100644 --- a/src/internal/Patch.ts +++ b/src/internal/Patch.ts @@ -122,16 +122,16 @@ export class Patch { reEntries[i].name + ", although version " + dbVersions[i] + - " has already been applied" + " has already been applied. Hint: make sure that you've rebased on top of the main branch, and new migration versions are still the most recent." ); } } if (dbVersions.length > reEntries.length) { throw ( - "version " + + "Version " + dbVersions[reEntries.length] + - " exists in the DB, but is missing on disk" + " exists in the DB, but is missing on disk. Hint: make sure you've rebased on top of the main branch." ); } diff --git a/src/internal/Registry.ts b/src/internal/Registry.ts index bb69259..f086149 100644 --- a/src/internal/Registry.ts +++ b/src/internal/Registry.ts @@ -3,6 +3,7 @@ import { basename } from "path"; import sortBy from "lodash/sortBy"; import { DefaultMap } from "./helpers/DefaultMap"; import { extractVars } from "./helpers/extractVars"; +import { schemaNameMatchesPrefix } from "./helpers/schemaNameMatchesPrefix"; import { validateCreateIndexConcurrently } from "./helpers/validateCreateIndexConcurrently"; /** @@ -106,13 +107,8 @@ export class Registry { } throw ( - "Schema " + - schema + - " matches more than one migration prefix (" + - entriesBySchema.get(schema)![0].schemaPrefix + - " and " + - schemaPrefix + - ")" + `Schema ${schema} matches more than one migration prefix ` + + `(${prevPrefix} and ${schemaPrefix})` ); } @@ -137,13 +133,6 @@ export class Registry { } } -function schemaNameMatchesPrefix(schema: string, prefix: string): boolean { - return ( - schema.startsWith(prefix) && - !!schema.substring(prefix.length).match(/^(\d|$)/s) - ); -} - function buildFile(fileName: string): File { if (!existsSync(fileName)) { throw `Migration file doesn't exist: ${fileName}`; diff --git a/src/internal/helpers/__tests__/collapse.test.ts b/src/internal/helpers/__tests__/collapse.test.ts new file mode 100644 index 0000000..5babf23 --- /dev/null +++ b/src/internal/helpers/__tests__/collapse.test.ts @@ -0,0 +1,30 @@ +import { collapse } from "../collapse"; + +test("collapse", () => { + expect(collapse(["host:sh0000"])).toEqual(["host:sh0000"]); + expect(collapse(["host:sh0001", "host:sh0002", "host:sh0003"])).toEqual([ + "host:sh0001-0003", + ]); + expect(collapse(["host:sh0001", "host:sh0002", "host:sh0003"])).toEqual([ + "host:sh0001-0003", + ]); + expect(collapse(["host:sh0001", "host:sh0003"])).toEqual([ + "host:sh0001,0003", + ]); + expect( + collapse([ + "host:sh0001", + "host:sh0002", + "host:sh0003", + "host:sh0008", + "host:sh0009", + "other:01", + "other:02", + "other:03", + ]), + ).toEqual(["host:sh0001-0003,0008-0009", "other:01-03"]); + expect(collapse(["host:public", "host:some"])).toEqual([ + "host:public", + "host:some", + ]); +}); diff --git a/src/internal/helpers/__tests__/schemaNameMatchesPrefix.test.ts b/src/internal/helpers/__tests__/schemaNameMatchesPrefix.test.ts new file mode 100644 index 0000000..6e4a5d2 --- /dev/null +++ b/src/internal/helpers/__tests__/schemaNameMatchesPrefix.test.ts @@ -0,0 +1,11 @@ +import { schemaNameMatchesPrefix } from "../schemaNameMatchesPrefix"; + +test("schemaNameMatchesPrefix", () => { + expect(schemaNameMatchesPrefix("sh0001", "sh")).toBe(true); + expect(schemaNameMatchesPrefix("sharding", "sh")).toBe(false); + expect(schemaNameMatchesPrefix("public", "public")).toBe(true); + expect(schemaNameMatchesPrefix("sh0001old1234", "sh")).toBe(true); + expect(schemaNameMatchesPrefix("sh0000", "sh")).toBe(true); + expect(schemaNameMatchesPrefix("sh0000", "sh0000")).toBe(true); + expect(schemaNameMatchesPrefix("sh0000old1234", "sh0000")).toBe(true); +}); diff --git a/src/internal/helpers/collapse.ts b/src/internal/helpers/collapse.ts index 924ea6f..1c09e2e 100644 --- a/src/internal/helpers/collapse.ts +++ b/src/internal/helpers/collapse.ts @@ -3,18 +3,36 @@ import { DefaultMap } from "./DefaultMap"; export function collapse(list: string[]): string[] { const res = []; - const numberSuffixes = new DefaultMap(); + const numberSuffixes = new DefaultMap< + string, + { numbers: number[]; widths: Map } + >(); for (const s of list.sort()) { const match = s.match(/^(.*?)(\d+)$/)!; if (match) { - numberSuffixes.getOrAdd(match[1], []).push(parseInt(match[2])); + const prefix = match[1]; + const numStr = match[2]; + const n = parseInt(numStr); + const slot = numberSuffixes.getOrAdd(prefix, { + numbers: [], + widths: new Map(), + }); + slot.numbers.push(n); + slot.widths.set(n, numStr.length); } else { res.push(s); } } - for (const [prefix, numbers] of numberSuffixes.entries()) { - res.push(prefix + multirange(numbers).toString()); + for (const [prefix, { numbers, widths }] of numberSuffixes.entries()) { + res.push( + prefix + + multirange(numbers) + .toString() + .replace(/(\d+)/g, (_, n: string) => + n.padStart(widths.get(parseInt(n)) ?? 0, "0"), + ), + ); } return res; diff --git a/src/internal/helpers/schemaNameMatchesPrefix.ts b/src/internal/helpers/schemaNameMatchesPrefix.ts new file mode 100644 index 0000000..8581eaf --- /dev/null +++ b/src/internal/helpers/schemaNameMatchesPrefix.ts @@ -0,0 +1,20 @@ +/** + * See unit test for matching and non-matching examples. + */ +export function schemaNameMatchesPrefix( + schema: string, + prefix: string, +): boolean { + const schemaStartsWithPrefix = schema.startsWith(prefix); + const schemaEqualsToPrefixExactly = schema === prefix; + const schemaHasDigitAfterPrefix = schema + .substring(prefix.length) + .match(/^\d/); + const prefixItselfHasDigitSoItIsSelectiveEnough = prefix.match(/\d/); + return !!( + schemaStartsWithPrefix && + (schemaEqualsToPrefixExactly || + schemaHasDigitAfterPrefix || + prefixItselfHasDigitSoItIsSelectiveEnough) + ); +} diff --git a/src/internal/render.ts b/src/internal/render.ts index cf7092f..654cfcd 100644 --- a/src/internal/render.ts +++ b/src/internal/render.ts @@ -16,7 +16,7 @@ const TABLE_OPTIONS = { maxWidth: process.stdout.columns - 2, }; -export function renderGrid(grid: Grid): string { +export function renderGrid(grid: Grid): string[] { const activeRows: string[][] = []; const errorRows: string[][] = []; for (const worker of sortBy( @@ -73,7 +73,7 @@ export function renderGrid(grid: Grid): string { " migrations/s" : ""; - return ( + const text = (activeRows.length > 0 ? "Running: " + [ @@ -87,11 +87,11 @@ export function renderGrid(grid: Grid): string { "\n" + table1.toString() + "\n" - : "") + (errorRows.length > 0 ? table2.toString().trimEnd() + "\n" : "") - ); + : "") + (errorRows.length > 0 ? table2.toString().trimEnd() + "\n" : ""); + return text.split("\n").filter(Boolean); } -export function renderPatchSummary(chains: Chain[]): [string, boolean] { +export function renderPatchSummary(chains: Chain[]): string { const destsGrouped = new DefaultMap(); for (const chain of chains) { const key = @@ -107,15 +107,10 @@ export function renderPatchSummary(chains: Chain[]): [string, boolean] { rows.push(collapse(dests) + ": " + key); } - return [ - chalk.yellow( - "Migrations to apply:\n" + - (rows.length ? rows : [""]) - .map((s) => " * " + s) - .join("\n"), - ), - rows.length > 0, - ]; + return chalk.yellow( + "Migrations to apply:\n" + + (rows.length ? rows : [""]).map((s) => " * " + s).join("\n"), + ); } export async function renderLatestVersions( diff --git a/tsconfig.json b/tsconfig.json index 8342ac4..8b5a443 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src/**/*"] -} +{ "extends": "./tsconfig.base.json" }