import { Generatable, capitalize } from './helpers'; import { DMMF } from '@prisma/generator-helper'; import { DMMFDocument, DMMFMapping } from './transformDMMF'; import * as Prism from 'prismjs'; import { lowerCase, isScalarType } from './helpers'; type ModelGeneratorStructure = { models: MGModel[]; }; type MGModel = { documentation?: string; name: string; directives: MGModelDirective[]; fields: MGModelField[]; operations: MGModelOperation[]; }; type MGModelDirective = { name: string; values: any[]; }; type MGModelField = { name: string; type: string; bareTypeName: string; directives: string[]; documentation?: string; required: boolean; }; type MGModelOperationKeys = { name: string; types: DMMF.SchemaArgInputType[]; required: boolean; }; type MGModelOperationOutput = { type: string; required: boolean; list: boolean; }; type MakeOptionallyUndefined = { [k in keyof T]: T[k] | undefined; }; // TODO: Select and include keys are generated by the Prisma Client // It might need some transformation type MGModelOperation = { name: string; description: string; opKeys: MGModelOperationKeys[] | undefined; usage: string; output: MakeOptionallyUndefined; }; interface FieldDefault { name: string; args: any[]; } let fieldDirectiveMap = new Map([ ['isUnique', '@unique'], ['isId', '@id'], ['hasDefaultValue', '@default'], ['isUpdatedAt', '@updatedAt'], ['hasDefaultValue', '@default'], ]); export default class ModelGenerator implements Generatable { data: ModelGeneratorStructure; constructor(d: DMMFDocument) { this.data = this.getData(d); } getModelDiretiveHTML(directive: MGModelDirective): string { return ` ${directive.name}
    ${directive.values.map((val) => `
  • ${val}
  • `).join('')}
`; } getModelFieldTableRow(field: MGModelField, modelName: string): string { return ` ${field.name} ${ isScalarType(field.bareTypeName) ? field.type : `${field.type}` }
    ${ field.directives.length > 0 ? field.directives .map((directive) => `
  • ${directive}
  • `) .join('') : '
  • -
  • ' }
${field.required ? `Yes` : 'No'} ${field.documentation ?? '-'} `; } getModelOperationMarkup( operation: MGModelOperation, modelName: string ): string { return `

${ operation.name }

${operation.description}

${
                      operation.usage
                    }

Input

${operation.opKeys ?.map( (opK) => ` ` ) .join('')}
Name Type Required
${opK.name} ${opK.types .map((t) => isScalarType(t.type as string) ? t.type : `${t.type}${ t.isList ? '[]' : '' }` ) .join(' | ')} ${opK.required ? 'Yes' : 'No'}

Output

Required: ${operation.output.required ? `Yes` : `No`}
List: ${operation.output.list ? `Yes` : `No`}
`; } toHTML() { return `

Models

${this.data.models .map( (model) => `

${model.name}

${ model.documentation ? `
Description: ${model.documentation}
` : '' } ${ model.directives.length > 0 ? ` ${model.directives .map((directive) => this.getModelDiretiveHTML(directive)) .join('')}
Name Value
` : '' }

Fields

${model.fields .map((field) => this.getModelFieldTableRow(field, model.name) ) .join('')}
Name Type Attributes Required Comment

Operations

${model.operations .map((op) => this.getModelOperationMarkup(op, model.name)) .join(`
`)}
` ) .join(`
`)}
`; } getModelDirective(model: DMMF.Model): MGModelDirective[] { let directiveValue: MGModelDirective[] = []; if (model.primaryKey) directiveValue.push({ name: '@@id', values: model.primaryKey.fields }); if (model.uniqueFields.length > 0) { model.uniqueFields.forEach((uniqueField) => { directiveValue.push({ name: '@@unique', values: uniqueField, }); }); } if (model.uniqueIndexes.length > 0) { model.uniqueIndexes.forEach((uniqueIndex) => { directiveValue.push({ name: '@@index', values: uniqueIndex.fields }); }); } return directiveValue; } getModelFields(model: DMMF.Model): MGModelField[] { return model.fields.map((field) => { return { name: field.name, type: this.getFieldType(field), bareTypeName: field.type, documentation: (field as any).documentation, directives: this.getFieldDirectives(field), required: field.isRequired, }; }); } getFieldType(field: DMMF.Field): string { let name = field.type; if (!field.isRequired && !field.isList) { name += '?'; } if (field.isList) { name += '[]'; } return name; } getFieldDirectives(field: DMMF.Field): string[] { const filteredEntries = Object.entries(field).filter(([_, v]) => Boolean(v) ); let directives: string[] = []; filteredEntries.forEach(([k]) => { const mappedDirectiveValue = fieldDirectiveMap.get(k); if (mappedDirectiveValue) { // default needs separate treatment right now as it can be a fn or any other type really if (k === 'hasDefaultValue' && field.default !== undefined) { if ( typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean' ) { directives.push(`${mappedDirectiveValue}(${field.default})`); } if(Array.isArray(field.default)) { directives.push(`${mappedDirectiveValue}([${field.default.toString()}])`) } else if (typeof field.default === 'object') { // Output of this template is, for example, @default(now()) directives.push( `${mappedDirectiveValue}(${ (field.default as FieldDefault).name }(${(field.default as FieldDefault).args.join(',')}))` ); } } else { directives.push(mappedDirectiveValue); } } }); return directives; } getModelOperations( model: DMMF.Model, mappings: DMMFMapping | undefined, schema: DMMF.Schema ): MGModelOperation[] { if (!mappings) { throw new Error(`No operation mapping found for model: ${model.name}`); } const modelOps = Object.entries(mappings).filter( ([map, _val]) => map !== 'model' ); let ops: MGModelOperation[] = []; modelOps.forEach(([op, val]) => { const singular = capitalize(model.name); const plural = capitalize(singular); const method = `prisma.${lowerCase(model.name)}.${op}`; switch (op) { case DMMF.ModelAction.create: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Create one ${singular}`, usage: Prism.highlight( `// Create one ${singular} const ${singular} = await ${method}({ data: { // ... data to create a ${singular} } }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.deleteMany: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Delete zero or more ${singular}`, usage: Prism.highlight( `// Delete a few ${plural} const { count } = await ${method}({ where: { // ... provide filter here } }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.delete: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Delete one ${singular}`, usage: Prism.highlight( `// Delete one ${singular} const ${singular} = await ${method}({ where: { // ... filter to delete one ${singular} } })`, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.findMany: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Query') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Find zero or more ${plural}`, usage: Prism.highlight( `// Get all ${plural} const ${plural} = await ${method}() // Get first 10 ${plural} const ${plural} = await ${method}({ take: 10 }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.findUnique: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Query') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Find zero or one ${plural}`, usage: Prism.highlight( `// Get one ${singular} const ${lowerCase(singular)} = await ${method}({ where: { // ... provide filter here } }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.findFirst: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Query') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Find first ${plural}`, usage: Prism.highlight( `// Get one ${singular} const ${lowerCase(singular)} = await ${method}({ where: { // ... provide filter here } }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.update: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Update one ${singular}`, usage: Prism.highlight( `// Update one ${singular} const ${lowerCase(singular)} = await ${method}({ where: { // ... provide filter here }, data: { // ... provide data here } }) `, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.updateMany: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Update zero or one ${plural}`, usage: Prism.highlight( `const { count } = await ${method}({ where: { // ... provide filter here }, data: { // ... provide data here } })`, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } case DMMF.ModelAction.upsert: { const field = schema.outputObjectTypes.prisma .find((t) => t.name === 'Mutation') ?.fields.find((f) => f.name === val); ops.push({ name: op, description: `Create or update one ${plural}`, usage: Prism.highlight( `// Update or create a ${singular} const ${lowerCase(singular)} = await ${method}({ create: { // ... data to create a ${singular} }, update: { // ... in case it already exists, update }, where: { // ... the filter for the ${singular} we want to update } })`, Prism.languages.javascript, 'javascript' ), opKeys: field?.args.map((a) => ({ name: a.name, types: a.inputTypes, required: a.isRequired, })), output: { type: field?.outputType.type as string, required: !field?.isNullable, list: field?.outputType.isList, }, }); break; } } }); return ops; } getModels(dmmf: DMMFDocument): MGModel[] { return dmmf.datamodel.models.map((model) => { return { name: model.name, documentation: (model as any).documentation as string, // TODO: Open issue for generator helper directives: this.getModelDirective(model), fields: this.getModelFields(model), operations: this.getModelOperations( model, dmmf.mappings.find((map) => map.model === model.name), dmmf.schema ), }; }); } getData(d: DMMFDocument) { return { models: this.getModels(d), }; } }