diff --git a/README.md b/README.md index d3c809b..a9e7e63 100755 --- a/README.md +++ b/README.md @@ -264,6 +264,8 @@ export class ProductDto extends IntersectionType( - Apply a ! after non-optional class fields to avoid strict mode warnings (Property has no initializer and is not definitely assigned in the constructor.) - _preserveDefaultNullable_ - Determines how null fields are handled. When set to **false** (default), it turns all null fields to undefined. Otherwise, it follows Prisma generation and adds null to the type. +- _nameConvention_ + - Determines what naming convention to use for the generated classes' file names. The default value is **snake**. The other option is **pascal**, **camel**, **kebab**. ### **How it works?** diff --git a/package.json b/package.json index 1fff259..640e970 100755 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@prisma/client": "^5.5.2", "@prisma/generator-helper": "^5.5.2", "@prisma/internals": "^5.5.2", - "change-case": "^4.1.2", + "change-case": "^5.4.4", "prettier": "2.5.1" }, "devDependencies": { diff --git a/prisma/postgresql.prisma b/prisma/postgresql.prisma index 9560e8b..00bc3d0 100755 --- a/prisma/postgresql.prisma +++ b/prisma/postgresql.prisma @@ -13,6 +13,7 @@ generator prismaClassGenerator { output = "../src/_gen/prisma-class" dryRun = "false" separateRelationFields = "false" + nameConvention = "kebab" } enum ProductType { @@ -27,7 +28,7 @@ enum ProductAnotherType { CC } -model Product { +model TestProduct { id Int @id title String @db.VarChar(255) desc String @default("abc") @db.VarChar(1024) @@ -39,29 +40,29 @@ model Product { averageRating Float? categoryId Int companyId Int - category Category @relation(fields: [categoryId], references: [id]) - company Company @relation(fields: [companyId], references: [id]) + category TestCategory @relation(fields: [categoryId], references: [id]) + company TestCompany @relation(fields: [companyId], references: [id]) createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @updatedAt @db.Timestamp(6) } -model Category { - id Int @id - products Product[] +model TestCategory { + id Int @id + products TestProduct[] } -model Company { - id Int @id +model TestCompany { + id Int @id name String - totalIncome BigInt @default(100) + totalIncome BigInt @default(100) lat Decimal lng Decimal by Bytes - products Product[] + products TestProduct[] tags String[] - tagsWithEmptyDefault String[] @default([]) - tagsWithDefault String[] @default(["a", "b"]) + tagsWithEmptyDefault String[] @default([]) + tagsWithDefault String[] @default(["a", "b"]) numTags Int[] - numTagsWithEmptyDefault Int[] @default([]) - numTagsWithDefault Int[] @default([1, 2]) + numTagsWithEmptyDefault Int[] @default([]) + numTagsWithDefault Int[] @default([1, 2]) } diff --git a/src/components/file.component.ts b/src/components/file.component.ts index 3094ac2..daa6ca4 100755 --- a/src/components/file.component.ts +++ b/src/components/file.component.ts @@ -1,10 +1,11 @@ -import { pascalCase, snakeCase } from 'change-case' +import { pascalCase, snakeCase, camelCase, kebabCase } from 'change-case' import { ClassComponent } from './class.component' import * as path from 'path' import { getRelativeTSPath, prettierFormat, writeTSFile } from '../util' import { PrismaClassGenerator } from '../generator' import { Echoable } from '../interfaces/echoable' import { ImportComponent } from './import.component' +import { PrismaClassGeneratorOptions } from '../interfaces/options' export class FileComponent implements Echoable { private _dir?: string @@ -45,11 +46,28 @@ export class FileComponent implements Echoable { this._prismaClass = value } - constructor(input: { classComponent: ClassComponent; output: string }) { + constructor(input: { + classComponent: ClassComponent + output: string + case: PrismaClassGeneratorOptions['nameConvention'] + }) { const { classComponent, output } = input this._prismaClass = classComponent this.dir = path.resolve(output) - this.filename = `${snakeCase(classComponent.name)}.ts` + switch (input.case) { + case 'camel': + this.filename = `${camelCase(classComponent.name)}.ts` + break + case 'pascal': + this.filename = `${pascalCase(classComponent.name)}.ts` + break + case 'kebab': + this.filename = `${kebabCase(classComponent.name)}.ts` + break + default: + this.filename = `${snakeCase(classComponent.name)}.ts` + break + } this.resolveImports() } diff --git a/src/error-handler.ts b/src/error-handler.ts index 60c1f4b..487e787 100755 --- a/src/error-handler.ts +++ b/src/error-handler.ts @@ -1,6 +1,21 @@ import { Dictionary } from '@prisma/internals' -import { PrismaClassGeneratorOptions } from './generator' +import { DEFAULT_OPTIONS } from './generator' import { log } from './util' +import { PrismaClassGeneratorOptions } from './interfaces/options' + +const OPTIONS_DESCRIPTION: Record = { + makeIndexFile: 'make index file', + dryRun: 'dry run', + separateRelationFields: 'separate relation fields', + useSwagger: 'use swagger decorstor', + useGraphQL: 'use graphql', + useUndefinedDefault: 'use undefined default', + clientImportPath: 'set prisma import path instead `@prisma/client`', + useNonNullableAssertions: + 'applies non-nullable assertions (!) to class properties', + preserveDefaultNullable: 'preserve default nullable behavior', + nameConvention: 'name convention for generated classes file name', +} export class GeneratorFormatNotValidError extends Error { config: Dictionary @@ -14,9 +29,9 @@ export class GeneratorPathNotExists extends Error {} export const handleGenerateError = (e: Error) => { if (e instanceof GeneratorFormatNotValidError) { - const options = Object.keys(PrismaClassGeneratorOptions).map((key) => { - const value = PrismaClassGeneratorOptions[key] - return `\t${key} = (${value.defaultValue}) <- [${value.desc}]` + const options = Object.keys(DEFAULT_OPTIONS).map((key) => { + const value = DEFAULT_OPTIONS[key] + return `\t${key} = (${value}) <-- ${OPTIONS_DESCRIPTION[key]}` }) log( [ diff --git a/src/generator.ts b/src/generator.ts index 608c23f..ad72270 100755 --- a/src/generator.ts +++ b/src/generator.ts @@ -5,6 +5,7 @@ import { PrismaConvertor } from './convertor' import { getRelativeTSPath, parseBoolean, + parseNameConvention, parseNumber, prettierFormat, writeTSFile, @@ -13,53 +14,25 @@ import { INDEX_TEMPLATE } from './templates/index.template' import { ImportComponent } from './components/import.component' import * as prettier from 'prettier' import { FileComponent } from './components/file.component' +import { PrismaClassGeneratorOptions } from './interfaces/options' export const GENERATOR_NAME = 'Prisma Class Generator' -export const PrismaClassGeneratorOptions = { - makeIndexFile: { - desc: 'make index file', - defaultValue: true, - }, - dryRun: { - desc: 'dry run', - defaultValue: true, - }, - separateRelationFields: { - desc: 'separate relation fields', - defaultValue: false, - }, - useSwagger: { - desc: 'use swagger decorstor', - defaultValue: true, - }, - useGraphQL: { - desc: 'use graphql', - defaultValue: false, - }, - useUndefinedDefault: { - desc: 'use undefined default', - defaultValue: false, - }, - clientImportPath: { - desc: 'set prisma import path instead @prisma/client', - defaultValue: undefined, - }, - useNonNullableAssertions: { - desc: 'applies non-nullable assertions (!) to class properties', - defaultValue: false, - }, - preserveDefaultNullable: { - defaultValue: false, - desc: 'preserve default nullable behavior', - }, +export const DEFAULT_OPTIONS: PrismaClassGeneratorOptions = { + makeIndexFile: true, + dryRun: true, + separateRelationFields: false, + useSwagger: true, + useGraphQL: false, + useUndefinedDefault: false, + clientImportPath: undefined, + useNonNullableAssertions: false, + preserveDefaultNullable: false, + nameConvention: 'snake', } as const -export type PrismaClassGeneratorOptionsKeys = - keyof typeof PrismaClassGeneratorOptions -export type PrismaClassGeneratorConfig = Partial< - Record -> +export type PrismaClassGeneratorOptionsKeys = keyof PrismaClassGeneratorOptions +export type PrismaClassGeneratorConfig = Partial export class PrismaClassGenerator { static instance: PrismaClassGenerator @@ -139,7 +112,12 @@ export class PrismaClassGenerator { const classes = convertor.getClasses() const files = classes.map( - (classComponent) => new FileComponent({ classComponent, output }), + (classComponent) => + new FileComponent({ + classComponent, + output, + case: config.nameConvention, + }), ) const classToPath = files.reduce((result, fileRow) => { @@ -198,12 +176,15 @@ export class PrismaClassGenerator { const config = this.options.generator.config const result: PrismaClassGeneratorConfig = {} - for (const optionName in PrismaClassGeneratorOptions) { - const { defaultValue } = PrismaClassGeneratorOptions[optionName] + for (const optionName in DEFAULT_OPTIONS) { + const defaultValue = DEFAULT_OPTIONS[optionName] result[optionName] = defaultValue const value = config[optionName] if (value) { + if (optionName === 'nameConvention') { + result[optionName] = parseNameConvention(value) + } if (typeof defaultValue === 'boolean') { result[optionName] = parseBoolean(value) } else if (typeof defaultValue === 'number') { diff --git a/src/interfaces/options.ts b/src/interfaces/options.ts new file mode 100644 index 0000000..80a4d27 --- /dev/null +++ b/src/interfaces/options.ts @@ -0,0 +1,62 @@ +export interface PrismaClassGeneratorOptions { + /** + * @description make index file + * @default true + * @type boolean + */ + makeIndexFile: boolean + /** + * @description dry run + * @default true + * @type boolean + */ + dryRun: boolean + /** + * @description separate relation fields + * @default false + * @type boolean + */ + separateRelationFields: boolean + /** + * @description use swagger decorstor + * @default true + * @type boolean + */ + useSwagger: boolean + /** + * @description use graphql + * @default false + * @type boolean + */ + useGraphQL: boolean + /** + * @description use undefined default + * @default false + * @type boolean + */ + useUndefinedDefault: boolean + /** + * @description set prisma import path instead `@prisma/client` + * @default undefined + * @type string | undefined + */ + clientImportPath: string | undefined + /** + * @description applies non-nullable assertions (!) to class properties + * @default false + * @type boolean + */ + useNonNullableAssertions: boolean + /** + * @default false + * @description preserve default nullable behavior + * @type boolean + */ + preserveDefaultNullable: boolean + /** + * @description name convention for generated classes file name + * @default 'snake' + * @type 'snake' | 'camel' | 'pascal' | 'kebab' + */ + nameConvention: 'snake' | 'camel' | 'pascal' | 'kebab' +} diff --git a/src/util.ts b/src/util.ts index 9f44edc..91c5dec 100755 --- a/src/util.ts +++ b/src/util.ts @@ -5,6 +5,7 @@ import { GENERATOR_NAME } from './generator' import { GeneratorFormatNotValidError } from './error-handler' import { DMMF } from '@prisma/generator-helper' import { Options, format } from 'prettier' +import { PrismaClassGeneratorOptions } from './interfaces/options' export const capitalizeFirst = (src: string) => { return src.charAt(0).toUpperCase() + src.slice(1) @@ -65,6 +66,22 @@ export const parseNumber = (value: unknown): number => { return numbered } +export const parseNameConvention = ( + value: string | string[], +): PrismaClassGeneratorOptions['nameConvention'] => { + if (Array.isArray(value)) { + throw new GeneratorFormatNotValidError( + `parseNameConvention failed : "nameConvention" should be string type`, + ) + } + if (['snake', 'camel', 'pascal', 'kebab'].includes(value) === false) { + throw new GeneratorFormatNotValidError( + `parseNameConvention failed : "${value}" is not valid name convention ( snake | camel | pascal | kebab )`, + ) + } + return value as PrismaClassGeneratorOptions['nameConvention'] +} + export const toArray = (value: T | T[]): T[] => { return Array.isArray(value) ? value : [value] }