diff --git a/package.json b/package.json index 8dc9554..bbdfa14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-typebox-generator", - "version": "2.0.2", + "version": "3.0.0", "main": "dist/index.js", "license": "MIT", "files": [ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 87477e6..a8b680f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,6 +11,7 @@ datasource db { url = env("DATABASE_URL") } +/// model description model User { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @@ -21,14 +22,27 @@ model User { successorId Int? role Role @default(USER) posts Post[] + /// @docs.opt minLength: 3 + /// @docs.listopt maxItems: 10 keywords String[] + /// field description biography Json + /// ignored description + /// @docs.opt description: "used description" decimal Decimal + /// multiline + /// description biginteger BigInt + /// @docs.opt minimum: 0 + /// @docs.type Integer + unsigned Int + /// @docs.hide + hidden Int } model Post { id Int @id @default(autoincrement()) + /// @docs.hide user User? @relation(fields: [userId], references: [id]) userId Int? } diff --git a/prisma/typebox/Post.ts b/prisma/typebox/Post.ts index a46e9a8..b7c923d 100644 --- a/prisma/typebox/Post.ts +++ b/prisma/typebox/Post.ts @@ -1,24 +1,7 @@ import { Type, Static } from "@sinclair/typebox"; -import { Role } from "./Role"; export const Post = Type.Object({ id: Type.Number(), - user: Type.Optional( - Type.Object({ - id: Type.Number(), - createdAt: Type.Optional(Type.String()), - email: Type.String(), - weight: Type.Optional(Type.Number()), - is18: Type.Optional(Type.Boolean()), - name: Type.Optional(Type.String()), - successorId: Type.Optional(Type.Number()), - role: Type.Optional(Role), - keywords: Type.Array(Type.String()), - biography: Type.String(), - decimal: Type.Number(), - biginteger: Type.Integer(), - }) - ), userId: Type.Optional(Type.Number()), }); diff --git a/prisma/typebox/PostInput.ts b/prisma/typebox/PostInput.ts index 43e96d1..7e7647a 100644 --- a/prisma/typebox/PostInput.ts +++ b/prisma/typebox/PostInput.ts @@ -1,24 +1,7 @@ import { Type, Static } from "@sinclair/typebox"; -import { Role } from "./Role"; export const PostInput = Type.Object({ id: Type.Optional(Type.Number()), - user: Type.Optional( - Type.Object({ - id: Type.Optional(Type.Number()), - createdAt: Type.Optional(Type.String()), - email: Type.String(), - weight: Type.Optional(Type.Number()), - is18: Type.Optional(Type.Boolean()), - name: Type.Optional(Type.String()), - successorId: Type.Optional(Type.Number()), - role: Type.Optional(Role), - keywords: Type.Array(Type.String()), - biography: Type.String(), - decimal: Type.Number(), - biginteger: Type.Integer(), - }) - ), userId: Type.Optional(Type.Number()), }); diff --git a/prisma/typebox/User.ts b/prisma/typebox/User.ts index 4b56c55..73327de 100644 --- a/prisma/typebox/User.ts +++ b/prisma/typebox/User.ts @@ -1,25 +1,29 @@ import { Type, Static } from "@sinclair/typebox"; import { Role } from "./Role"; -export const User = Type.Object({ - id: Type.Number(), - createdAt: Type.Optional(Type.String()), - email: Type.String(), - weight: Type.Optional(Type.Number()), - is18: Type.Optional(Type.Boolean()), - name: Type.Optional(Type.String()), - successorId: Type.Optional(Type.Number()), - role: Type.Optional(Role), - posts: Type.Array( - Type.Object({ - id: Type.Number(), - userId: Type.Optional(Type.Number()), - }) - ), - keywords: Type.Array(Type.String()), - biography: Type.String(), - decimal: Type.Number(), - biginteger: Type.Integer(), -}); +export const User = Type.Object( + { + id: Type.Number(), + createdAt: Type.Optional(Type.String()), + email: Type.String(), + weight: Type.Optional(Type.Number()), + is18: Type.Optional(Type.Boolean()), + name: Type.Optional(Type.String()), + successorId: Type.Optional(Type.Number()), + role: Type.Optional(Role), + posts: Type.Array( + Type.Object({ + id: Type.Number(), + userId: Type.Optional(Type.Number()), + }) + ), + keywords: Type.Array(Type.String({ minLength: 3 }), { maxItems: 10 }), + biography: Type.String({ description: "field description" }), + decimal: Type.Number({ description: "used description" }), + biginteger: Type.Integer({ description: "multiline\ndescription" }), + unsigned: Type.Integer({ minimum: 0 }), + }, + { description: "model description" } +); export type UserType = Static; diff --git a/prisma/typebox/UserInput.ts b/prisma/typebox/UserInput.ts index 702b3bd..6fa3823 100644 --- a/prisma/typebox/UserInput.ts +++ b/prisma/typebox/UserInput.ts @@ -1,25 +1,29 @@ import { Type, Static } from "@sinclair/typebox"; import { Role } from "./Role"; -export const UserInput = Type.Object({ - id: Type.Optional(Type.Number()), - createdAt: Type.Optional(Type.String()), - email: Type.String(), - weight: Type.Optional(Type.Number()), - is18: Type.Optional(Type.Boolean()), - name: Type.Optional(Type.String()), - successorId: Type.Optional(Type.Number()), - role: Type.Optional(Role), - posts: Type.Array( - Type.Object({ - id: Type.Optional(Type.Number()), - userId: Type.Optional(Type.Number()), - }) - ), - keywords: Type.Array(Type.String()), - biography: Type.String(), - decimal: Type.Number(), - biginteger: Type.Integer(), -}); +export const UserInput = Type.Object( + { + id: Type.Optional(Type.Number()), + createdAt: Type.Optional(Type.String()), + email: Type.String(), + weight: Type.Optional(Type.Number()), + is18: Type.Optional(Type.Boolean()), + name: Type.Optional(Type.String()), + successorId: Type.Optional(Type.Number()), + role: Type.Optional(Role), + posts: Type.Array( + Type.Object({ + id: Type.Optional(Type.Number()), + userId: Type.Optional(Type.Number()), + }) + ), + keywords: Type.Array(Type.String({ minLength: 3 }), { maxItems: 10 }), + biography: Type.String({ description: "field description" }), + decimal: Type.Number({ description: "used description" }), + biginteger: Type.Integer({ description: "multiline\ndescription" }), + unsigned: Type.Integer({ minimum: 0 }), + }, + { description: "model description" } +); export type UserInputType = Static; diff --git a/prisma/typebox/index.ts b/prisma/typebox/index.ts new file mode 100644 index 0000000..60eba8a --- /dev/null +++ b/prisma/typebox/index.ts @@ -0,0 +1,5 @@ +export * from './User'; +export * from './UserInput'; +export * from './Post'; +export * from './PostInput'; +export * from './Role'; diff --git a/src/generator/transformDMMF.ts b/src/generator/transformDMMF.ts index 919516c..ba88423 100644 --- a/src/generator/transformDMMF.ts +++ b/src/generator/transformDMMF.ts @@ -1,164 +1,230 @@ import type { DMMF } from '@prisma/generator-helper'; -const transformField = (field: DMMF.Field) => { - const tokens = [field.name + ':']; - let inputTokens = []; - const deps = new Set(); - - if (['Int', 'Float', 'Decimal'].includes(field.type)) { - tokens.push('Type.Number()'); - } else if (['BigInt'].includes(field.type)) { - tokens.push('Type.Integer()'); - } else if (['String', 'DateTime', 'Json', 'Date'].includes(field.type)) { - tokens.push('Type.String()'); - } else if (field.type === 'Boolean') { - tokens.push('Type.Boolean()'); - } else { - tokens.push(`::${field.type}::`); - deps.add(field.type); - } - - if (field.isList) { - tokens.splice(1, 0, 'Type.Array('); - tokens.splice(tokens.length, 0, ')'); - } - - inputTokens = [...tokens]; +export function createTransformer(generatorName: string) { + const transformField = (field: DMMF.Field) => { + const lineRegex = new RegExp(`^@${generatorName}\\.([a-z]+) (.+)`); + + const tokens = [field.name + ':']; + let inputTokens = []; + const deps = new Set(); + + let overrideType; + const options = []; + const listOptions = []; + const description = []; + if (field.documentation) { + const lines = field.documentation.split('\n'); + for (let line of lines) { + line = line.trim(); + const match = line.match(lineRegex); + if (match) { + switch (match[1]) { + case 'type': + overrideType = match[2]; + break; + case 'opt': + options.push(match[2]); + break; + case 'listopt': + listOptions.push(match[2]); + break; + default: + throw new Error( + `${field.name}(${field.type}): uknown hint '@${generatorName}.${match[1]}'`, + ); + } + } else if (line === `@${generatorName}.hide`) { + return { + str: '', + strInput: '', + deps: [], + }; + } else if (!line.startsWith('@')) { + description.push(line); + } + } + } + + if (description.length) { + const opts = field.isList ? listOptions : options; + if (!opts.some((opt) => opt.indexOf('description') >= 0)) { + opts.push( + 'description: ' + JSON.stringify(description.join('\n').trim()), + ); + } + } + + const optionsStr = options.length ? `{ ${options.join(', ')} }` : ''; + + let typeStr; + if (['Int', 'Float', 'Decimal'].includes(field.type)) { + typeStr = `Type.${overrideType || 'Number'}(${optionsStr})`; + } else if (['BigInt'].includes(field.type)) { + typeStr = `Type.${overrideType || 'Integer'}(${optionsStr})`; + } else if (['String', 'DateTime', 'Json', 'Date'].includes(field.type)) { + typeStr = `Type.${overrideType || 'String'}(${optionsStr})`; + } else if (field.type === 'Boolean') { + typeStr = `Type.${overrideType || 'Boolean'}(${optionsStr})`; + } else { + typeStr = `::${field.type}::`; + deps.add(field.type); + } + + if (field.isList) { + const listOptionsStr = listOptions.length + ? `, { ${listOptions.join(', ')} }` + : ''; + typeStr = `Type.Array(${typeStr}${listOptionsStr})`; + } + + tokens.push(typeStr); + + inputTokens = [...tokens]; + + // @id cannot be optional except for input if it's auto increment + if (field.isId && (field?.default as any)?.name === 'autoincrement') { + inputTokens.splice(1, 0, 'Type.Optional('); + inputTokens.splice(inputTokens.length, 0, ')'); + } + + if ((!field.isRequired || field.hasDefaultValue) && !field.isId) { + tokens.splice(1, 0, 'Type.Optional('); + tokens.splice(tokens.length, 0, ')'); + inputTokens.splice(1, 0, 'Type.Optional('); + inputTokens.splice(inputTokens.length, 0, ')'); + } + + return { + str: tokens.join(' ').concat('\n'), + strInput: inputTokens.join(' ').concat('\n'), + deps, + }; + }; - // @id cannot be optional except for input if it's auto increment - if (field.isId && (field?.default as any)?.name === 'autoincrement') { - inputTokens.splice(1, 0, 'Type.Optional('); - inputTokens.splice(inputTokens.length, 0, ')'); - } + const transformFields = (fields: DMMF.Field[]) => { + let dependencies = new Set(); + const _fields: string[] = []; + const _inputFields: string[] = []; - if ((!field.isRequired || field.hasDefaultValue) && !field.isId) { - tokens.splice(1, 0, 'Type.Optional('); - tokens.splice(tokens.length, 0, ')'); - inputTokens.splice(1, 0, 'Type.Optional('); - inputTokens.splice(inputTokens.length, 0, ')'); - } + fields.map(transformField).forEach((field) => { + _fields.push(field.str); + _inputFields.push(field.strInput); + [...field.deps].forEach((d) => { + dependencies.add(d); + }); + }); - return { - str: tokens.join(' ').concat('\n'), - strInput: inputTokens.join(' ').concat('\n'), - deps, + return { + dependencies, + rawString: _fields.filter((f) => !!f).join(','), + rawInputString: _inputFields.filter((f) => !!f).join(','), + }; }; -}; - -const transformFields = (fields: DMMF.Field[]) => { - let dependencies = new Set(); - const _fields: string[] = []; - const _inputFields: string[] = []; - - fields.map(transformField).forEach((field) => { - _fields.push(field.str); - _inputFields.push(field.strInput); - [...field.deps].forEach((d) => { - dependencies.add(d); - }); - }); - return { - dependencies, - rawString: _fields.filter((f) => !!f).join(','), - rawInputString: _inputFields.filter((f) => !!f).join(','), + const transformModel = (model: DMMF.Model, models?: DMMF.Model[]) => { + const description = model.documentation + ?.split('\n') + .filter((line) => !line.startsWith('@')) + .join('\n') + .trim(); + const optionsStr = description?.length + ? `, { description: ${JSON.stringify(description)} }` + : ''; + const fields = transformFields(model.fields); + let raw = [ + `${models ? '' : `export const ${model.name} = `}Type.Object({\n\t`, + fields.rawString, + `}${optionsStr})`, + ].join('\n'); + let inputRaw = [ + `${models ? '' : `export const ${model.name}Input = `}Type.Object({\n\t`, + fields.rawInputString, + `}${optionsStr})`, + ].join('\n'); + + if (Array.isArray(models)) { + models.forEach((md) => { + const re = new RegExp(`.+::${md.name}.+\n`, 'gm'); + const inputRe = new RegExp(`.+::${md.name}.+\n`, 'gm'); + raw = raw.replace(re, ''); + inputRaw = inputRaw.replace(inputRe, ''); + }); + } + + return { + raw, + inputRaw, + deps: fields.dependencies, + }; }; -}; - -const transformModel = (model: DMMF.Model, models?: DMMF.Model[]) => { - const fields = transformFields(model.fields); - let raw = [ - `${models ? '' : `export const ${model.name} = `}Type.Object({\n\t`, - fields.rawString, - '})', - ].join('\n'); - let inputRaw = [ - `${models ? '' : `export const ${model.name}Input = `}Type.Object({\n\t`, - fields.rawInputString, - '})', - ].join('\n'); - - if (Array.isArray(models)) { - models.forEach((md) => { - const re = new RegExp(`.+::${md.name}.+\n`, 'gm'); - const inputRe = new RegExp(`.+::${md.name}.+\n`, 'gm'); - raw = raw.replace(re, ''); - inputRaw = inputRaw.replace(inputRe, ''); - }); - } - return { - raw, - inputRaw, - deps: fields.dependencies, + const transformEnum = (enm: DMMF.DatamodelEnum) => { + const values = enm.values + .map((v) => `${v.name}: Type.Literal('${v.name}'),\n`) + .join(''); + + return [ + `export const ${enm.name}Const = {`, + values, + '}\n', + `export const ${enm.name} = Type.KeyOf(Type.Object(${enm.name}Const))\n`, + `export type ${enm.name}Type = Static`, + ].join('\n'); }; -}; - -export const transformEnum = (enm: DMMF.DatamodelEnum) => { - const values = enm.values - .map((v) => `${v.name}: Type.Literal('${v.name}'),\n`) - .join(''); - - return [ - `export const ${enm.name}Const = {`, - values, - '}\n', - `export const ${enm.name} = Type.KeyOf(Type.Object(${enm.name}Const))\n`, - `export type ${enm.name}Type = Static`, - ].join('\n'); -}; - -export function transformDMMF(dmmf: DMMF.Document) { - const { models, enums } = dmmf.datamodel; - const importStatements = new Set([ - 'import {Type, Static} from "@sinclair/typebox"', - ]); - - return [ - ...models.map((model) => { - let { raw, inputRaw, deps } = transformModel(model); - - [...deps].forEach((d) => { - const depsModel = models.find((m) => m.name === d) as DMMF.Model; - if (depsModel) { - const replacer = transformModel(depsModel, models); - const re = new RegExp(`::${d}::`, 'gm'); - raw = raw.replace(re, replacer.raw); - inputRaw = inputRaw.replace(re, replacer.inputRaw); - } - }); - enums.forEach((enm) => { - const re = new RegExp(`::${enm.name}::`, 'gm'); - if (raw.match(re)) { - raw = raw.replace(re, enm.name); - inputRaw = inputRaw.replace(re, enm.name); - importStatements.add(`import { ${enm.name} } from './${enm.name}'`); - } - }); + function transformDMMF(dmmf: DMMF.Document) { + const { models, enums } = dmmf.datamodel; + const mainImport = 'import {Type, Static} from "@sinclair/typebox"'; + + return [ + ...models.map((model) => { + let { raw, inputRaw, deps } = transformModel(model); + + [...deps].forEach((d) => { + const depsModel = models.find((m) => m.name === d) as DMMF.Model; + if (depsModel) { + const replacer = transformModel(depsModel, models); + const re = new RegExp(`::${d}::`, 'gm'); + raw = raw.replace(re, replacer.raw); + inputRaw = inputRaw.replace(re, replacer.inputRaw); + } + }); + + const importStatements = new Set(); + enums.forEach((enm) => { + const re = new RegExp(`::${enm.name}::`, 'gm'); + if (raw.match(re)) { + raw = raw.replace(re, enm.name); + inputRaw = inputRaw.replace(re, enm.name); + importStatements.add(`import { ${enm.name} } from './${enm.name}'`); + } + }); + + return { + name: model.name, + rawString: [ + [mainImport, ...importStatements].join('\n'), + raw, + `export type ${model.name}Type = Static`, + ].join('\n\n'), + inputRawString: [ + [mainImport, ...importStatements].join('\n'), + inputRaw, + `export type ${model.name}InputType = Static`, + ].join('\n\n'), + }; + }), + ...enums.map((enm) => { + return { + name: enm.name, + inputRawString: null, + rawString: + 'import {Type, Static} from "@sinclair/typebox"\n\n' + + transformEnum(enm), + }; + }), + ]; + } - return { - name: model.name, - rawString: [ - [...importStatements].join('\n'), - raw, - `export type ${model.name}Type = Static`, - ].join('\n\n'), - inputRawString: [ - [...importStatements].join('\n'), - inputRaw, - `export type ${model.name}InputType = Static`, - ].join('\n\n'), - }; - }), - ...enums.map((enm) => { - return { - name: enm.name, - inputRawString: null, - rawString: - 'import {Type, Static} from "@sinclair/typebox"\n\n' + - transformEnum(enm), - }; - }), - ]; + return transformDMMF; } diff --git a/src/index.ts b/src/index.ts index 4a7e2e5..85f62d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { generatorHandler } from '@prisma/generator-helper'; -import { transformDMMF } from './generator/transformDMMF'; +import { createTransformer } from './generator/transformDMMF'; import * as fs from 'fs'; import * as path from 'path'; import { parseEnvValue } from '@prisma/sdk'; @@ -13,6 +13,7 @@ generatorHandler({ }; }, async onGenerate(options) { + const transformDMMF = createTransformer(options.generator.name); const payload = transformDMMF(options.dmmf); if (options.generator.output) { const outputDir =