Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically pascal case model names during introspection #1934

Open
hcharley opened this issue Mar 25, 2020 · 21 comments
Open

Automatically pascal case model names during introspection #1934

hcharley opened this issue Mar 25, 2020 · 21 comments

Comments

@hcharley
Copy link

hcharley commented Mar 25, 2020

Problem

Having to manually update model names to conform to Prisma's naming conventions using the introspection tool is 1) annoying to do for many snake cased table names, and 2) prevents an introspection-only schema approach to using prisma2.

Solution

Automatically change model names from this format:

model my_table {
  id   String @id @default(cuid())
  name String
}

To this:

model MyTable {
  id   String @id @default(cuid())
  name String
  @@map("my_table")
}

Alternatives

I could write my own script.

Additional context

Started in discussion at:

#1925

Just going to dump some regex here that might help write that script:

model ([a-z_]+) {
const result = `
model ${pascalCase(name)} {
  @@map(${name})
}
`
@giautm
Copy link

giautm commented Apr 22, 2020

I also want fields must mapping to camelCase, because I introspect from a database with ~ 100 tables & 10~20 column per table. I don't want to map field by manual. I hope has a flag, when introspect used to switching between modes.

@TLadd
Copy link

TLadd commented Apr 22, 2020

I wanted to follow standard postgres naming practices (using snake case for column and field names so I don't have to quote everything when using raw sql), but still end up with a generated client that follows normal Typescript/prisma conventions (Pascal case for models, camelcase for field names, plurals for array relationships).

I wrote a script that handles all of my personal naming issues. Might be easier with access to the AST and there could be cases I'm missing since I'm starting on a new app and so my schema is fairly simple, but works for my use case and shouldn't be too hard to add additional cases as needed. Didn't bother trying to handle spacing. Saving the file in vscode with the prisma plugin fixes it. It handles:

  • change model name to pascal case in both the model declaration and when used as types for relation fields. Adds the appropriate @@Map annotation.
  • change field names from snake case to pascal case and updates references to those fields in relations and indexes. Add @Map annotation to end of field line.
  • Add and s to field names for one to many and many to many relationships.
import fs from "fs";
import path from "path";

const PRISMA_FILE_PATH = path.join(__dirname, "..", "prisma", "schema.prisma");

function snakeToCamel(str: string) {
  return str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase());
}
function snakeToPascal(str: string) {
  let camelCase = snakeToCamel(str);
  return `${camelCase[0].toUpperCase()}${camelCase.substr(1)}`;
}

const PRISMA_PRIMITIVES = ["String", "Boolean", "Int", "Float", "DateTime"];

function isPrimitiveType(typeName: string) {
  return PRISMA_PRIMITIVES.includes(typeName);
}

function fixFieldsArrayString(fields: string) {
  return fields
    .split(", ")
    .map((field) => snakeToCamel(field))
    .join(", ");
}

async function fixPrismaFile() {
  const text = fs.readFileSync(path.join(PRISMA_FILE_PATH), "utf8");

  const textAsArray = text.split("\n");

  const fixedText = [];
  let currentModelName: string | null = null;
  let hasAddedModelMap = false;

  for (let line of textAsArray) {
    // Are we at the start of a model definition
    const modelMatch = line.match(/^model (\w+) {$/);
    if (modelMatch) {
      currentModelName = modelMatch[1];
      hasAddedModelMap = false;
      const pascalModelName = snakeToPascal(currentModelName);
      fixedText.push(`model ${pascalModelName} {`);
      continue;
    }

    // We don't need to change anything if we aren't in a model body
    if (!currentModelName) {
      fixedText.push(line);
      continue;
    }

    // Add the @@map to the table name for the model
    if (!hasAddedModelMap && (line.match(/\s+@@/) || line === "}")) {
      if (line === "}") {
        fixedText.push("");
      }
      fixedText.push(`  @@map("${currentModelName}")`);
      hasAddedModelMap = true;
    }

    // Renames field and applies a @map to the field name if it is snake case
    // Adds an s to the field name if the type is an array relation
    const fieldMatch = line.match(/\s\s(\w+)\s+(\w+)(\[\])?/);
    let fixedLine = line;
    if (fieldMatch) {
      const [, currentFieldName, currentFieldType, isArrayType] = fieldMatch;

      let fixedFieldName = snakeToCamel(currentFieldName);
      if (isArrayType && !fixedFieldName.endsWith("s")) {
        fixedFieldName = `${fixedFieldName}s`;
      }

      fixedLine = fixedLine.replace(currentFieldName, fixedFieldName);

      // Add map if we needed to convert the field name and the field is not a relational type
      // If it's relational, the field type will be a non-primitive, hence the isPrimitiveType check
      if (currentFieldName.includes("_") && isPrimitiveType(currentFieldType)) {
        fixedLine = `${fixedLine} @map("${currentFieldName}")`;
      }
    }

    // Capitalizes model names in field types
    const fieldTypeMatch = fixedLine.match(/\s\s\w+\s+(\w+)/);
    if (fieldTypeMatch) {
      const currentFieldType = fieldTypeMatch[1];
      const fieldTypeIndex = fieldTypeMatch[0].lastIndexOf(currentFieldType);
      const fixedFieldType = snakeToPascal(currentFieldType);
      const startOfLine = fixedLine.substr(0, fieldTypeIndex);
      const restOfLine = fixedLine.substr(
        fieldTypeIndex + currentFieldType.length
      );
      fixedLine = `${startOfLine}${fixedFieldType}${restOfLine}`;
    }

    // Changes `fields: [relation_id]` in @relation to camel case
    const relationFieldsMatch = fixedLine.match(/fields:\s\[([\w,\s]+)\]/);
    if (relationFieldsMatch) {
      const fields = relationFieldsMatch[1];
      fixedLine = fixedLine.replace(fields, fixFieldsArrayString(fields));
    }

    // Changes fields listed in @@index or @@unique to camel case
    const indexUniqueFieldsMatch = fixedLine.match(/@@\w+\(\[([\w,\s]+)\]/);
    if (indexUniqueFieldsMatch) {
      const fields = indexUniqueFieldsMatch[1];
      fixedLine = fixedLine.replace(fields, fixFieldsArrayString(fields));
    }

    fixedText.push(fixedLine);
  }

  fs.writeFileSync(PRISMA_FILE_PATH, fixedText.join("\n"));
}

fixPrismaFile();

Edit: Added check to avoid adding @Map to relation fields.

@hcharley
Copy link
Author

Didn't bother trying to handle spacing. Saving the file in vscode with the prisma plugin fixes it

Just as an aside @TLadd, you can use this snippet for prettier:

import prettier from 'prettier';

let PRETTIER_OPTS = {};

if (!process.env._HAS_RESOLVED_PRETTIER) {
  const prettierConfigPath = prettier.resolveConfigFile.sync();
  if (prettierConfigPath) {
    const o = prettier.resolveConfig.sync(prettierConfigPath);
    if (o) {
      PRETTIER_OPTS = o;
    }
  }
  process.env._HAS_RESOLVED_PRETTIER = 'true';
}

export const formatWithPrettier = (output: string) => {
  return prettier.format(output, {
    parser: 'typescript', // or X parser
    ...PRETTIER_OPTS,
  });
};

@ryands17
Copy link

For those using knex to migrate, I have updated the above script to remove the knex models that occur in the schema.prisma during introspection.

import * as fs from 'fs'
import * as path from 'path'

const PRISMA_FILE_PATH = path.join(__dirname, 'schema.prisma')

function snakeToCamel(str: string) {
  return str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase())
}
function snakeToPascal(str: string) {
  let camelCase = snakeToCamel(str)
  return `${camelCase[0].toUpperCase()}${camelCase.substr(1)}`
}

const PRISMA_PRIMITIVES = ['String', 'Boolean', 'Int', 'Float', 'DateTime']
const KNEX_INTERNAL_MODELS = ['knex_migrations', 'knex_migrations_lock']

function isKnexInternalModel(typeName: string) {
  return KNEX_INTERNAL_MODELS.includes(typeName)
}

function isPrimitiveType(typeName: string) {
  return PRISMA_PRIMITIVES.includes(typeName)
}

function fixFieldsArrayString(fields: string) {
  return fields
    .split(', ')
    .map((field) => snakeToCamel(field))
    .join(', ')
}

async function fixPrismaFile() {
  const text = await fs.promises.readFile(PRISMA_FILE_PATH, 'utf8')

  const textAsArray = text.split('\n')

  const fixedText = []
  let currentModelName: string | null = null
  let hasAddedModelMap = false

  for (let line of textAsArray) {
    // Are we at the start of a model definition
    const modelMatch = line.match(/^model (\w+) {$/)
    if (modelMatch) {
      currentModelName = modelMatch[1]
      if (isKnexInternalModel(currentModelName)) {
        continue
      }
      hasAddedModelMap = false
      const pascalModelName = snakeToPascal(currentModelName)
      fixedText.push(`model ${pascalModelName} {`)
      continue
    }

    if (currentModelName && isKnexInternalModel(currentModelName)) {
      continue
    }

    // We don't need to change anything if we aren't in a model body
    if (!currentModelName) {
      fixedText.push(line)
      continue
    }

    // Add the @@map to the table name for the model
    if (!hasAddedModelMap && (line.match(/\s+@@/) || line === '}')) {
      if (line === '}') {
        fixedText.push('')
      }
      fixedText.push(`  @@map("${currentModelName}")`)
      hasAddedModelMap = true
    }

    // Renames field and applies a @map to the field name if it is snake case
    // Adds an s to the field name if the type is an array relation
    const fieldMatch = line.match(/\s\s(\w+)\s+(\w+)(\[\])?/)
    let fixedLine = line
    if (fieldMatch) {
      const [, currentFieldName, currentFieldType, isArrayType] = fieldMatch

      let fixedFieldName = snakeToCamel(currentFieldName)
      if (isArrayType && !fixedFieldName.endsWith('s')) {
        fixedFieldName = `${fixedFieldName}s`
      }

      fixedLine = fixedLine.replace(currentFieldName, fixedFieldName)

      // Add map if we needed to convert the field name and the field is not a relational type
      // If it's relational, the field type will be a non-primitive, hence the isPrimitiveType check
      if (currentFieldName.includes('_') && isPrimitiveType(currentFieldType)) {
        fixedLine = `${fixedLine} @map("${currentFieldName}")`
      }
    }

    // Capitalizes model names in field types
    const fieldTypeMatch = fixedLine.match(/\s\s\w+\s+(\w+)/)
    if (fieldTypeMatch) {
      const currentFieldType = fieldTypeMatch[1]
      const fieldTypeIndex = fieldTypeMatch[0].lastIndexOf(currentFieldType)
      const fixedFieldType = snakeToPascal(currentFieldType)
      const startOfLine = fixedLine.substr(0, fieldTypeIndex)
      const restOfLine = fixedLine.substr(
        fieldTypeIndex + currentFieldType.length
      )
      fixedLine = `${startOfLine}${fixedFieldType}${restOfLine}`
    }

    // Changes `fields: [relation_id]` in @relation to camel case
    const relationFieldsMatch = fixedLine.match(/fields:\s\[([\w,\s]+)\]/)
    if (relationFieldsMatch) {
      const fields = relationFieldsMatch[1]
      fixedLine = fixedLine.replace(fields, fixFieldsArrayString(fields))
    }

    // Changes fields listed in @@index or @@unique to camel case
    const indexUniqueFieldsMatch = fixedLine.match(/@@\w+\(\[([\w,\s]+)\]/)
    if (indexUniqueFieldsMatch) {
      const fields = indexUniqueFieldsMatch[1]
      fixedLine = fixedLine.replace(fields, fixFieldsArrayString(fields))
    }

    fixedText.push(fixedLine)
  }

  await fs.promises.writeFile(PRISMA_FILE_PATH, fixedText.join('\n'))
}

fixPrismaFile()

@michaellzc
Copy link

For those looking for fixing the naming convention automatically, I wrote a utility program that utilizes the Prisma schema AST, provided by the internal API getDMMF from @prisma/sdk, to automatically transform the naming of model and fields while keeping the mapping correct.

https://github.com/IBM/prisma-schema-transformer

@saiar
Copy link

saiar commented Sep 5, 2020

@ExiaSR Thanks for the utility!

I just found this that might help with the same thing https://paljs.com/cli/schema/ for anyone who is interested. It supposedly also helps generate the TypeScript interface classes for the models.

@jjangga0214
Copy link
Contributor

jjangga0214 commented Oct 30, 2021

Does resolving this take a lot of time? I think, with AST, we can easily deal with this. There are already many libraries for changing cases(e.g. snake_case to camelCase or PascalCase or vise versa)

@jedashford
Copy link

@ExiaSR Thanks for the utility!

I just found this that might help with the same thing https://paljs.com/cli/schema/ for anyone who is interested. It supposedly also helps generate the TypeScript interface classes for the models.

This is what worked for us, super simple and fast. Thanks!

@allyusd
Copy link

allyusd commented Dec 11, 2021

I'm looking https://paljs.com/cli/schema/, it's good, but it has some issues:

feat: changing case of model name of single word · Issue #241 · paljs/prisma-tools
feat: changing case of relation · Issue #240 · paljs/prisma-tools

Then I trying https://github.com/IBM/prisma-schema-transformer, it's great, but not yet supported prisma 3 now. (have committ log but not release)

So I fork a patch for it and write my solution on README.md
https://github.com/allyusd/prisma-schema-transformer

@janpio janpio changed the title Feature request: automatically pascal case model names during introspection Automatically pascal case model names during introspection Jan 15, 2022
@nikolasburk
Copy link
Member

This feature request seems to be included in this one: Introspection Configuration File (#1184 ) Should we close it and point people to #1184 in the future?

@rclmenezes
Copy link

rclmenezes commented Nov 6, 2022

I wrote my own solution because:

  • The other libraries didn't work with pnpm (they had yarn hard-coded in)
  • Enums still didn't work well

So here's my solution:
https://gist.github.com/rclmenezes/b3b382f31e9df24fca6fd709057ba73d

It works as a state machine, going over the file line by line.

  • It will convert enum names to PascalCase
  • It will convert model names to PascalCase and add an @@Map and singularize
  • It will convert field types to camelCase and add a @Map

Basically my workflow is:

  1. Migrate my DB using raw SQL with Postgrator.
  2. pnpm prisma db pull
  3. pnpm ts-node createIdiomaticPrisma.ts

Anyway, hope this helps anyone else with the same problem.

@ctsstc
Copy link

ctsstc commented Feb 1, 2023

It would be great to see options for naming conventions, like many are saying snake case for table names and for our team we use camel case on the model names.

It's kind of crazy to see all the lengths that everyone has gone to, to handle their cases 😬

@ruheni
Copy link
Contributor

ruheni commented Feb 1, 2023

Hey 👋

I recently came across prisma-case-format by @iiian which seems to be a good workaround for ensuring consistent naming conventions in your Prisma schema. 🙂

@shellscape
Copy link

@nikolasburk @janpio this is another 3 year old issue asking for the same feature. the community demand is there, your competitors already support it. this is another missing DX feature that I'm sorry to say is not a good look for Prisma. I'd hope an open source project with corporate backing would have resources enough to tackle issues with this kind of age.

@jedashford
Copy link

@nikolasburk @janpio this is another 3 year old issue asking for the same feature. the community demand is there, your competitors already support it. this is another missing DX feature that I'm sorry to say is not a good look for Prisma. I'd hope an open source project with corporate backing would have resources enough to tackle issues with this kind of age.

It's open source, submit a PR, and help the community.

@shellscape
Copy link

what's open source? that's the first I've heard of it

@kevinvdburgt
Copy link

what's open source?

Prisma itself is open source. Feel free to create a PR and get some credits :-)

@jedashford
Copy link

@shellscape you stated yourself that its open source (link to description of open source):

I'd hope an open source project with corporate backing would have resources enough to tackle issues with this kind of age.

This means the community owns it. You, me, anyone willing to help. To demean other volunteers because your feature hasn't been worked on, when you are free and enabled to build/test/deliver it yourself, feels a bit disingenuous.

@shellscape
Copy link

Prisma itself is open source. Feel free to create a PR and get some credits :-)

Credits! Oh man. That sounds awesome. What do I get to do with those credits?

Wait so like, I can see the code and I can change the code?! That's incredible. I wonder if there's a project out there that millions of developers use every day that I could maintain. Maybe they'll let me if I have enough credits?

This means the community owns it. You, me, anyone willing to help. To demean other volunteers because your feature hasn't been worked on, when you are free and enabled to build/test/deliver it yourself, feels a bit disingenuous.

A thousand apologies, sir. The next time I have a criticism I'll just keep it to myself for sure. I surely wouldn't want to demean anyone with criticism.

@janpio
Copy link
Member

janpio commented Apr 12, 2023

I'd hope an open source project with corporate backing would have resources enough to tackle issues with this kind of age.

No, we don't, sorry. We are busy with other things that do not have easy manual or automated workarounds (see previous comments).

(Please keep it civil, everyone. I don't want to have to start hiding or deleting comments. Thanks.)

@nilsm
Copy link

nilsm commented Aug 5, 2023

As mentioned above https://github.com/iiian/prisma-case-format works great. This is the script I used in package.json to pull the schema for an introspection-only workflow.

"db:pull": "prisma db pull && prisma-case-format --file prisma/schema.prisma -p && prisma generate"

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

No branches or pull requests