Repository: Softjey/prisma-dm
Pull request #4: Introduce --upto, optional config keys, custom migration schema name, .DS_Store typo fix
Author: login: pieterjandebruyne
Created: 2025-03-03T16:09:59Z | Merged: 2025-03-10T16:15:57Z

- introduce --upto command (fixed the logic so that when you use this to pass a specific migration name, that migration iwll be included
- Make configuration keys optional (I think this is better so that updates to default will also be implemented for users that did not want to override the default)
- allow custom migration schema name (older configuration files should still work because all keys are now optional and fallback to the default)
- fix ".DS_STORE" typo to ".DS_Store"
- update readme to reflect above changes


Diff:
diff --git a/.gitignore b/.gitignore
index d67fb29..81389de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .git
 node_modules
-dist
\ No newline at end of file
+dist
+.idea
diff --git a/README.md b/README.md
index 700cdfa..99aac54 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ Commands:
    Place the `schema.prisma` file in the corresponding migration folder alongside `migration.sql`. This file must match the schema state after applying the Prisma migration.
 
    ⚠️ **Note**: You can skip copying the `schema.prisma` file and generating types if you do not need the Prisma Client. Instead, you can simply add the post-migration script directly to the migration folder.
+   ⚠️ **Note**: You can change the default `schema.prisma` filename by using the `migrationSchemaFileName` key in the config.
 
 4. **Generate Types**:
    Run the `generate` command to create TypeScript types for your data migrations based on your schemas:
@@ -130,13 +131,22 @@ Commands:
    ```bash
    npx prisma-dm migrate
    ```
+   (by default, it migrates to `latest`)
 
-   ⚠️ **Note**: You can use the `--to` flag to migrate to a specific migration version (by default, it migrates to `latest`). Example:
+   ⚠️ **Note**: You can use the `--to` flag to migrate to a specific migration version (not including the migration 
+   passed. Example:
 
    ```bash
    npx prisma-dm migrate --to 20250108201031_add_user_name
    ```
 
+   ⚠️ **Note**: You can use the `--upto` flag to migrate to a specific migration version (including the one passed) (by
+   default, it migrates to `latest`). Example:
+
+   ```bash
+   npx prisma-dm migrate --upto 20250108201031_add_user_name
+   ```
+
 ## Configuration
 
 The configuration file (`prisma-dm.config.json`) allows customization of the library. Key fields include:
@@ -151,6 +161,9 @@ The configuration file (`prisma-dm.config.json`) allows customization of the lib
 - **`migrationsDir`**: Directory containing Prisma migrations. Default: `prisma/migrations`.
 
 - **`tempDir`**: Temporary directory for moving migration folders during execution. Default: `prisma/.temp`.
+- 
+- **`migrationSchemaFileName`**: The filename for prisma schema files within migration directories. Default: `schema.
+  prisma`.
 
 - **`log`**: Logging level (`none`, `info`, `verbose`). Default: `info`.
 
diff --git a/config.schema.json b/config.schema.json
index bcb7338..7a865ff 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -32,6 +32,12 @@
       "description": "Temporary directory for processing. Must be a valid directory path.",
       "default": "prisma/.temp"
     },
+    "migrationSchemaFileName": {
+      "type": "string",
+      "pattern": "^[^\\s]+\\.[^\\s]+$",
+      "description": "The filename for prisma schema files within migration directories. Defaults to schema.prisma",
+      "default": "schema.prisma"
+    },
     "log": {
       "type": "string",
       "enum": [
@@ -51,4 +57,4 @@
     "log"
   ],
   "additionalProperties": false
-}
\ No newline at end of file
+}
diff --git a/package-lock.json b/package-lock.json
index dc0e983..62751b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
       "license": "ISC",
       "dependencies": {
         "ajv": "^8.17.1",
-        "commander": "^13.0.0",
+        "commander": "^13.1.0",
         "dotenv": "^16.4.7",
         "fs-extra": "^11.2.0",
         "slonik": "^46.4.0"
@@ -211,9 +211,9 @@
       "dev": true
     },
     "node_modules/commander": {
-      "version": "13.0.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz",
-      "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+      "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
       "engines": {
         "node": ">=18"
       }
diff --git a/package.json b/package.json
index 80cbf5c..cd2314a 100644
--- a/package.json
+++ b/package.json
@@ -47,9 +47,9 @@
   },
   "dependencies": {
     "ajv": "^8.17.1",
-    "commander": "^13.0.0",
+    "commander": "^13.1.0",
     "dotenv": "^16.4.7",
     "fs-extra": "^11.2.0",
     "slonik": "^46.4.0"
   }
-}
\ No newline at end of file
+}
diff --git a/src/cli.ts b/src/cli.ts
index d47c16a..89ef2e2 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,6 +1,6 @@
 #!/usr/bin/env node
 
-import { getConfig } from "./config/getConfig";
+import {configLoader} from "./config/getConfig";
 import { CLI } from "./services/CLI";
 import { Logger } from "./services/Logger";
 import { DB } from "./services/DB";
@@ -16,7 +16,7 @@ dotenv.config();
 const program = new Command();
 
 function createCLI() {
-  const config = getConfig();
+  const config = configLoader.getConfig();
   const logger = new Logger(config);
   const prisma = new DB();
   const validator = new Validator(config);
@@ -68,12 +68,16 @@ program
 program
   .command("migrate")
   .description("Migrate to target migration with post scripts execution")
-  .option("--to <value>", "Target migration", "latest")
+  .option("--to <value>", "Target migration")
+  .option("--upto <value>", "Target migration = Run all migrations up to (and including) this one")
   .action(async (options) => {
-    const toOption = options.to as string | undefined;
-    const to = toOption === "latest" ? undefined : toOption;
+    if(options.to && options.upto) {
+      throw new Error('options "to" and "upto" can not be used together');
+    }
+    const toOption: string | undefined = options.to ?? options.upto;
+    const targetMigration = toOption === "latest" ? undefined : toOption;
 
-    await createCLI().migrate({ to });
+    await createCLI().migrate({ targetMigration, includeTargetMigration: !options.to });
   });
 
 program.on("command:*", () => {
diff --git a/src/config/config.type.ts b/src/config/config.type.ts
index ffdbbe2..387a442 100644
--- a/src/config/config.type.ts
+++ b/src/config/config.type.ts
@@ -26,6 +26,10 @@ export interface ConfigSchema {
    * Temporary directory for processing. Must be a valid directory path.
    */
   tempDir: string;
+  /**
+   * The filename for prisma schema files within migration directories.
+   */
+  migrationSchemaFileName: string;
   /**
    * Log level for the script, must be one of 'info', 'debug', or 'error'.
    */
diff --git a/src/config/getConfig.ts b/src/config/getConfig.ts
index 25559b0..f5edaa6 100644
--- a/src/config/getConfig.ts
+++ b/src/config/getConfig.ts
@@ -4,12 +4,35 @@ import { ConfigSchema } from "./config.type";
 import { CONFIG_FILE_NAME } from "./CONFIG_FILE_NAME";
 import schema from "../../config.schema.json";
 import Ajv from "ajv";
+import {DEFAULT_CONFIG} from "./DEFAULT_CONFIG";
 
-export function getConfig(): ConfigSchema {
-  try {
+
+class ConfigLoader {
+  private readonly config: ConfigSchema;
+
+  constructor() {
     const configFilePath = path.join(process.cwd(), CONFIG_FILE_NAME);
-    const file = fs.readFileSync(configFilePath, "utf-8");
-    const config = JSON.parse(file);
+    let parsedConfig = {};
+    let file: string;
+    try {
+      file = fs.readFileSync(configFilePath, "utf-8");
+    } catch (error) {
+      console.info(`No config file found, falling back to default config.`);
+    }
+
+    if(file !== undefined) {
+      try {
+        parsedConfig = JSON.parse(file);
+      } catch (error) {
+        console.error(`Failed to parse config file: ${error.message}, falling back to default config.`);
+        throw new Error('Failed to parse config file')
+      }
+    }
+
+    const config: ConfigSchema = {
+      ...DEFAULT_CONFIG,
+      ...parsedConfig
+    };
 
     const ajv = new Ajv();
     const validate = ajv.compile(schema);
@@ -19,20 +42,16 @@ export function getConfig(): ConfigSchema {
       throw new Error("Configuration validation failed.");
     }
 
-    const typedConfig = config as ConfigSchema;
-
-    return {
-      migrationsDir: path.join(process.cwd(), typedConfig.migrationsDir),
-      tempDir: path.join(process.cwd(), typedConfig.tempDir),
-      ...typedConfig,
+    this.config = {
+      migrationsDir: path.join(process.cwd(), config.migrationsDir),
+      tempDir: path.join(process.cwd(), config.tempDir),
+      ...config,
     };
-  } catch (error) {
-    if (error.code === "ENOENT") {
-      throw new Error(
-        "Config file not found. Run `npx prisma-dm init` to create one."
-      );
-    }
+  }
 
-    throw error;
+  getConfig(): ConfigSchema {
+    return this.config;
   }
 }
+
+export const configLoader = new ConfigLoader()
diff --git a/src/services/CLI.ts b/src/services/CLI.ts
index 96b764a..e9e9aae 100644
--- a/src/services/CLI.ts
+++ b/src/services/CLI.ts
@@ -1,7 +1,7 @@
 import path from "path";
 import fs from "fs-extra";
 import { CONFIG_FILE_NAME } from "../config/CONFIG_FILE_NAME";
-import { getConfig } from "../config/getConfig";
+import {configLoader} from "../config/getConfig";
 import { DEFAULT_CONFIG } from "../config/DEFAULT_CONFIG";
 import { Validator } from "./Validator";
 import { PrismaCLI } from "../utils/classes/PrismaCLI";
@@ -32,14 +32,14 @@ export class CLI<T extends string> {
   }
 
   generate() {
-    const config = getConfig();
+    const config = configLoader.getConfig();
     const migrationsDirPath = path.join(process.cwd(), config.migrationsDir);
     const migrationsDir = fs.readdirSync(migrationsDirPath);
 
     for (const migrationName of migrationsDir) {
       if (this.validator.isMigrationWithPrismaSchema(migrationName)) {
         const migrationPath = path.join(migrationsDirPath, migrationName);
-        const schemaPath = path.join(migrationPath, "schema.prisma");
+        const schemaPath = path.join(migrationPath, config.migrationSchemaFileName);
         const outputPath = `${config.outputDir}/${migrationName}`;
 
         this.logger.logInfo(`Generating types for migration: ${migrationName}`);
@@ -83,20 +83,21 @@ export class CLI<T extends string> {
     this.logger.logInfo("Schema files merged");
   }
 
-  async migrate({ to }: { to?: T | undefined } = {}) {
-    if (to) {
-      this.validator.validateMigrationName(to);
+  async migrate({ targetMigration, includeTargetMigration }: { targetMigration?: T | undefined, includeTargetMigration: boolean }) {
+    if (targetMigration) {
+      this.validator.validateMigrationName(targetMigration);
     }
 
-    const config = getConfig();
+    const config = configLoader.getConfig();
     const migrationsDirPath = path.join(process.cwd(), config.migrationsDir);
     const rawMigrations = fs
       .readdirSync(migrationsDirPath)
       .filter((m) => this.validator.isMigration(m));
-    const lastMigrationIndex = to
-      ? rawMigrations.indexOf(to)
+    const lastMigrationIndex = targetMigration
+      ? rawMigrations.indexOf(targetMigration)
       : rawMigrations.length;
-    const migrations = rawMigrations.slice(0, lastMigrationIndex);
+
+    const migrations = rawMigrations.slice(0, lastMigrationIndex + (includeTargetMigration ? 1 : 0));
     const dataMigrations = migrations.filter((m) =>
       this.validator.isMigrationWithPostScript(m)
     );
diff --git a/src/services/TargetedPrismaMigrator.ts b/src/services/TargetedPrismaMigrator.ts
index 43c1606..66a0aae 100644
--- a/src/services/TargetedPrismaMigrator.ts
+++ b/src/services/TargetedPrismaMigrator.ts
@@ -1,7 +1,7 @@
 import path from "path";
 import fs from "fs-extra";
 import { ConfigSchema } from "../config/config.type";
-import { getConfig } from "../config/getConfig";
+import {configLoader} from "../config/getConfig";
 import { PrismaCLI } from "../utils/classes/PrismaCLI";
 import { Logger } from "./Logger";
 
@@ -11,7 +11,7 @@ export class TargetedPrismaMigrator<T extends string> {
   private readonly config: ConfigSchema;
 
   constructor(private readonly logger: Logger) {
-    this.config = getConfig();
+    this.config = configLoader.getConfig();
   }
 
   private createTempDir() {
diff --git a/src/services/Validator.ts b/src/services/Validator.ts
index b82e017..403f7a6 100644
--- a/src/services/Validator.ts
+++ b/src/services/Validator.ts
@@ -8,7 +8,7 @@ export class Validator {
   isMigrationWithPrismaSchema(migrationName: string) {
     const migrationPath = path.join(this.config.migrationsDir, migrationName);
     const hasPrismaSchema = fs.existsSync(
-      path.join(migrationPath, "schema.prisma")
+      path.join(migrationPath, this.config.migrationSchemaFileName)
     );
 
     return this.isMigration(migrationName) && hasPrismaSchema;
@@ -31,7 +31,7 @@ export class Validator {
     );
     const migrationsDir = fs.readdirSync(migrationsDirPath);
 
-    return name !== "migration_lock.toml" && name !== ".DS_STORE" && migrationsDir.includes(name);
+    return name !== "migration_lock.toml" && name !== ".DS_Store" && migrationsDir.includes(name);
   }
 
   validateMigrationName(name: string) {


Reviews:
user: login: Softjey
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-04T14:32:27Z
user: login: pieterjandebruyne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:18:54Z
user: login: Softjey
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-10T16:14:57Z

Review comments:
diff_hunk: @@ -4,12 +4,28 @@ import { ConfigSchema } from "./config.type";
 import { CONFIG_FILE_NAME } from "./CONFIG_FILE_NAME";
 import schema from "../../config.schema.json";
 import Ajv from "ajv";
+import {DEFAULT_CONFIG} from "./DEFAULT_CONFIG";
 
 export function getConfig(): ConfigSchema {
   try {
     const configFilePath = path.join(process.cwd(), CONFIG_FILE_NAME);
-    const file = fs.readFileSync(configFilePath, "utf-8");
-    const config = JSON.parse(file);
+    let parsedConfig = {};
+    try {
+      const file = fs.readFileSync(configFilePath, "utf-8");
+      parsedConfig = JSON.parse(file);
+    } catch (error) {
+      if (error.code === "ENOENT") {
+        console.info(`No config file ${CONFIG_FILE_NAME} found, using defaults`)
+      }else {
+        throw new Error(
+          "Could not read config file"
path: src/config/getConfig.ts
user: login: Softjey
body: Could you also show the initial error to the user? This will help avoid situations where the user doesn't know the cause of the error. You could try to use throw new Error("Could not read config file", { cause: error }); for this.
created_at: 2025-03-04T14:21:13Z
author_association: OWNER
diff_hunk: @@ -4,12 +4,28 @@ import { ConfigSchema } from "./config.type";
 import { CONFIG_FILE_NAME } from "./CONFIG_FILE_NAME";
 import schema from "../../config.schema.json";
 import Ajv from "ajv";
+import {DEFAULT_CONFIG} from "./DEFAULT_CONFIG";
 
 export function getConfig(): ConfigSchema {
   try {
     const configFilePath = path.join(process.cwd(), CONFIG_FILE_NAME);
-    const file = fs.readFileSync(configFilePath, "utf-8");
-    const config = JSON.parse(file);
+    let parsedConfig = {};
+    try {
+      const file = fs.readFileSync(configFilePath, "utf-8");
+      parsedConfig = JSON.parse(file);
+    } catch (error) {
+      if (error.code === "ENOENT") {
+        console.info(`No config file ${CONFIG_FILE_NAME} found, using defaults`)
+      }else {
+        throw new Error(
+          "Could not read config file"
path: src/config/getConfig.ts
user: login: pieterjandebruyne
body: not supported in your target es2018... so none of the error currently have this. I would suggest a targetbump and align all errors if you want this. But would do it in separate PR...
created_at: 2025-03-10T14:18:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: Softjey
created_at: 2025-03-04T14:34:32Z
body: Thanks for the great work! In the evening, I'll try to pull the branch locally, check if everything is fine, and publish a new version on NPM
author_association: OWNER
user: login: Softjey
created_at: 2025-03-09T10:01:59Z
body: Sorry for the delay.  
I pulled the repository locally and noticed one more small thing. It would be better to display message about the missing config file only once, instead of doing it every time it's needed.

Other than that, everything looks okay. Let me know once you've resolved these two issues, or let me know if you'd like me to fix them, and I'll publish the new version on NPM.  

Once again, thank you for your contribution!
author_association: OWNER
user: login: pieterjandebruyne
created_at: 2025-03-10T14:17:34Z
body: Updated the PR,
I refactored the getConfig to be a singleton of a class so we only fetch / parse the file once.
- If we don't find a file, post info message and use default
- if we found a file but it is invalid (either json or field validation fails) we throw and exit

I did not implement the cause inside the throw new Error since we don't do it anywhere and it would require your tsconfig target to be >es2022 since that function overload is only available since that version... 

Feel free to make any changes to the logic I added if you would like to have something changed..

If you are fine with it, please release it on npm :) thx.
author_association: CONTRIBUTOR
user: login: Softjey
created_at: 2025-03-10T16:15:46Z
body: Nice work! Thank you!
author_association: OWNER