Skip to content

Instantly share code, notes, and snippets.

@stevenaanen
Last active November 7, 2023 14:52
Show Gist options
  • Save stevenaanen/51788591ca8fc38fd40f54f8a2e6373f to your computer and use it in GitHub Desktop.
Save stevenaanen/51788591ca8fc38fd40f54f8a2e6373f to your computer and use it in GitHub Desktop.
Transform Prisma Schema to js-friendly names
// @ts-check
/*
* Run using `node convert-casing-after-prisma-pull.mjs` after installing `change-case` in your workspace.
* Then pipe your schema into it.
*
* Script will normalize the model names, field names and enums.
* It should not affect your database schema, only the generated js client from Prisma.
*
* Tip: after processing, run `prisma format` on the file.
*/
import { camelCase, pascalCase } from "change-case";
import fs from "node:fs";
// read command line input stream and store in variable
const input = fs.readFileSync(0, "utf-8");
// multiline split code blocks
const blocks = input.matchAll(/^.+\{\n[\S\s]*?\}/gm);
const PrismaScalars = [
"String",
"Boolean",
"Int",
"BigInt",
"Float",
"Decimal",
"DateTime",
"Json",
"Bytes",
"Unsupported",
"String?",
"Boolean?",
"Int?",
"BigInt?",
"Float?",
"Decimal?",
"DateTime?",
"Json?",
"Bytes?",
"Unsupported?",
];
// loop over iterator
for (const blockMatch of blocks) {
const block = blockMatch[0];
const modelReg = /(?<type>model|enum) (?<modelName>\S+) \{/;
const modelName = modelReg.exec(block)?.groups?.modelName;
const lines = block.split("\n");
const updatedLines = lines.map((line) => {
return (
line
// Replace model name
.replace(modelReg, (_, modelType, modelName) => {
return `${modelType} ${pascalCase(modelName)} {`;
})
// Replace field names and handle @map
.replace(
/^\s{2}(?<field>\w+_?[\w_]+)\s+(?<fieldType>[\w?]+)/,
(_, field, fieldType) => {
if (!PrismaScalars.includes(fieldType)) {
// these are relations
const isOptional = fieldType.endsWith("?");
return ` ${pascalCase(field)} ${pascalCase(fieldType)}${
isOptional ? "?" : ""
}`;
}
return ` ${camelCase(field)} ${fieldType} @map("${field}")`;
}
)
// Handle @relations
.replace(
/^\s{2}(?<field>\w+)\s+(?<relationName>[\w?]+)\s+@relation/,
(_, field, relationName) => {
const isOptional = relationName.endsWith("?");
return ` ${pascalCase(field)} ${pascalCase(relationName)}${
isOptional ? "?" : ""
} @relation`;
}
)
.replace(
/fields: \[(?<relationField>\w_?\w+)\]/,
(_, relationField) => {
return `fields: [${camelCase(relationField)}]`;
}
)
.replace(
/references: \[(?<referencesField>\w_?\w+)\]/,
(_, referencesField) => {
return `references: [${camelCase(referencesField)}]`;
}
)
.replace(
/references: \[(?<referencesField>\w_?\w+)\]/,
(_, referencesField) => {
return `references: [${camelCase(referencesField)}]`;
}
)
.replace(
/@@(?<type>id|index|unique)\(\[(?<relationships>[^\]]*)/,
(_, type, relationships) => {
const relations = relationships
.split(",")
.map((/** @type {string} */ r) => {
const relation = r.trim();
if (relation.includes("(length: ")) {
return relation.replace(
/(?<key>\w+)(?<params>\([^\)]*)\)/,
(_, key, params) => {
return `${camelCase(key)} ${params})`;
}
);
} else {
return camelCase(relation);
}
});
return `@@${type}([${relations.join(", ")}`;
}
)
// Handle constants
.replace(/^\s{2}(?<constant>[\w_]+)$/, (_, constant) => {
return ` ${pascalCase(constant)} @map("${constant}")`;
})
.replace(/^\}/, "")
);
});
const suffix = modelName ? `\n @@map("${modelName}")\n` : "";
process.stdout.write(`${updatedLines.join("\n")}${suffix}}\n\n`);
}
@DannyStreur
Copy link

Nice! This helps a lot when pulling the schema from an old database.

I added a few things so that relationships are always CamelCase'd and relations and indexes keep working correctly.

// @ts-check
/*
 * Run using `node convert-casing-after-prisma-pull.mjs` after installing `change-case` in your workspace.
 * Then pipe your schema into it.
 *
 * Script will normalize the model names, field names and enums.
 * It should not affect your database schema, only the generated js client from Prisma.
 *
 * Tip: after processing, run `prisma format` on the file.
 */
import { camelCase, pascalCase } from "change-case";
import fs from "node:fs";

// read command line input stream and store in variable
const input = fs.readFileSync(0, "utf-8");

// multiline split code blocks
const blocks = input.matchAll(/^.+\{\n[\S\s]*?\}/gm);

const PrismaScalars = [
  "String",
  "Boolean",
  "Int",
  "BigInt",
  "Float",
  "Decimal",
  "DateTime",
  "Json",
  "Bytes",
  "Unsupported",
  "String?",
  "Boolean?",
  "Int?",
  "BigInt?",
  "Float?",
  "Decimal?",
  "DateTime?",
  "Json?",
  "Bytes?",
  "Unsupported?",
];

// loop over iterator
for (const blockMatch of blocks) {
  const block = blockMatch[0];
  const modelReg = /(?<type>model|enum) (?<modelName>\S+) \{/;
  const modelName = modelReg.exec(block)?.groups?.modelName;

  const lines = block.split("\n");

  const updatedLines = lines.map((line) => {
    return (
      line
        // Replace model name
        .replace(modelReg, (_, modelType, modelName) => {
          return `${modelType} ${pascalCase(modelName)} {`;
        })
        // Replace field names and handle @map
        .replace(
          /^\s{2}(?<field>\w+_?[\w_]+)\s+(?<fieldType>[\w?]+)/,
          (_, field, fieldType) => {
            if (!PrismaScalars.includes(fieldType)) {
              // these are relations
              const isOptional = fieldType.endsWith("?");
              return `  ${pascalCase(field)}  ${pascalCase(fieldType)}${
                isOptional ? "?" : ""
              }`;
            }

            return `  ${camelCase(field)}  ${fieldType}  @map("${field}")`;
          }
        )
        // Handle @relations
        .replace(
          /^\s{2}(?<field>\w+)\s+(?<relationName>[\w?]+)\s+@relation/,
          (_, field, relationName) => {
            const isOptional = relationName.endsWith("?");

            return `  ${pascalCase(field)}  ${pascalCase(relationName)}${
              isOptional ? "?" : ""
            }  @relation`;
          }
        )
        .replace(
          /fields: \[(?<relationField>\w_?\w+)\]/,
          (_, relationField) => {
            return `fields: [${camelCase(relationField)}]`;
          }
        )
        .replace(
          /references: \[(?<referencesField>\w_?\w+)\]/,
          (_, referencesField) => {
            return `references: [${camelCase(referencesField)}]`;
          }
        )
        .replace(
          /references: \[(?<referencesField>\w_?\w+)\]/,
          (_, referencesField) => {
            return `references: [${camelCase(referencesField)}]`;
          }
        )
        .replace(
          /@@(?<type>id|index|unique)\(\[(?<relationships>[^\]]*)/,
          (_, type, relationships) => {
            const relations = relationships
              .split(",")
              .map((/** @type {string} */ r) => {
                const relation = r.trim();

                if (relation.includes("(length: ")) {
                  return relation.replace(
                    /(?<key>\w+)(?<params>\([^\)]*)\)/,
                    (_, key, params) => {
                      return `${camelCase(key)} ${params})`;
                    }
                  );
                } else {
                  return camelCase(relation);
                }
              });

            return `@@${type}([${relations.join(", ")}`;
          }
        )
        // Handle constants
        .replace(/^\s{2}(?<constant>[\w_]+)$/, (_, constant) => {
          return `  ${pascalCase(constant)}  @map("${constant}")`;
        })
        .replace(/^\}/, "")
    );
  });

  const suffix = modelName ? `\n  @@map("${modelName}")\n` : "";

  process.stdout.write(`${updatedLines.join("\n")}${suffix}}\n\n`);
}

@stevenaanen
Copy link
Author

Nice! This helps a lot when pulling the schema from an old database.

I added a few things so that relationships are always CamelCase'd and relations and indexes keep working correctly.

Great improvements @DannyStreur! Updated the gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment