UNPKG

prisma-docs-generator

Version:

Documentation reference generator for Prisma Schema

695 lines (657 loc) 20.8 kB
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<T> = { [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<MGModelOperationOutput>; }; interface FieldDefault { name: string; args: any[]; } let fieldDirectiveMap = new Map<string, string>([ ['isUnique', '@unique'], ['isId', '@id'], ['hasDefaultValue', '@default'], ['isUpdatedAt', '@updatedAt'], ['hasDefaultValue', '@default'], ]); export default class ModelGenerator implements Generatable<ModelGeneratorStructure> { data: ModelGeneratorStructure; constructor(d: DMMFDocument) { this.data = this.getData(d); } getModelDiretiveHTML(directive: MGModelDirective): string { return ` <tr> <td class="px-4 py-2 border"> <strong>${directive.name}</strong> </td> <td class="px-4 py-2 border"> <ul> ${directive.values.map((val) => `<li>${val}</li>`).join('')} </ul> </td> </tr> `; } getModelFieldTableRow(field: MGModelField, modelName: string): string { return ` <tr id="${`model-${modelName}-${field.name}`}"> <td class="px-4 py-2 border"> ${field.name} </td> <td class="px-4 py-2 border"> ${ isScalarType(field.bareTypeName) ? field.type : `<a href="#type-outputType-${field.bareTypeName}">${field.type}</a>` } </td> <td class="px-4 py-2 border"> <ul> ${ field.directives.length > 0 ? field.directives .map((directive) => `<li><strong>${directive}</strong></li>`) .join('') : '<li> - </li>' } </ul> </td> <td class="px-4 py-2 border"> ${field.required ? `<strong>Yes</strong>` : 'No'} </td> <td class="px-4 py-2 border"> ${field.documentation ?? '-'} </td> </tr> `; } getModelOperationMarkup( operation: MGModelOperation, modelName: string ): string { return ` <div class="mt-4"> <h4 id="${`model-${modelName}-${operation.name}`}" class="mb-2 text-lg font-bold">${ operation.name }</h4> <p>${operation.description}</p> <div class="mb-2"> <pre class="language-markup" ><code class=" language-javascript">${ operation.usage }</code></pre> </div> <h4 class="text-lg mb-2">Input</h4> <table class="table-auto mb-2"> <thead> <tr> <th class="px-4 py-2 border">Name</th> <th class="px-4 py-2 border">Type</th> <th class="px-4 py-2 border">Required</th> </tr> </thead> <tbody> ${operation.opKeys ?.map( (opK) => ` <tr> <td class="px-4 py-2 border"> ${opK.name} </td> <td class="px-4 py-2 border"> ${opK.types .map((t) => isScalarType(t.type as string) ? t.type : `<a href="#type-inputType-${t.type}">${t.type}${ t.isList ? '[]' : '' }</a>` ) .join(' | ')} </td> <td class="px-4 py-2 border"> ${opK.required ? '<strong>Yes</strong>' : 'No'} </td> </tr> ` ) .join('')} </tbody> </table> <h4 class="text-lg mb-2">Output</h4> <div><strong>Type: </strong> <a href="#type-outputType-${ operation.output.type }">${operation.output.type}</a></div> <div><strong>Required: </strong> ${operation.output.required ? `Yes` : `No`}</div> <div><strong>List: </strong> ${operation.output.list ? `Yes` : `No`}</div> </div> `; } toHTML() { return ` <div class="mb-8"> <h1 class="text-3xl text-gray-800" id="models">Models</h1> ${this.data.models .map( (model) => ` <div class="px-4 mb-4"> <h2 class="text-2xl" id="model-${model.name}">${model.name}</h2> ${ model.documentation ? `<div class="mb-2">Description: ${model.documentation}</div>` : '' } ${ model.directives.length > 0 ? ` <table class="table-auto"> <thead> <tr> <th class="px-4 py-2 border">Name</th> <th class="px-4 py-2 border">Value</th> </tr> </thead> <tbody> ${model.directives .map((directive) => this.getModelDiretiveHTML(directive)) .join('')} </tbody> </table> ` : '' } <div class="px-4 mt-4"> <h3 class="mb-2 text-xl" id="model-${ model.name }-fields">Fields</h3> <div class="px-2 mb-4"> <table class="table-auto"> <thead> <tr> <th class="px-4 py-2 border">Name</th> <th class="px-4 py-2 border">Type</th> <th class="px-4 py-2 border">Attributes</th> <th class="px-4 py-2 border">Required</th> <th class="px-4 py-2 border">Comment</th> </tr> </thead> <tbody> ${model.fields .map((field) => this.getModelFieldTableRow(field, model.name) ) .join('')} </tbody> </table> </div> </div> <hr class="my-8"> <div class="px-4 mt-4"> <h3 class="mb-2 text-xl" id="model-${ model.name }-operations">Operations</h3> <div class="px-2 mb-4"> ${model.operations .map((op) => this.getModelOperationMarkup(op, model.name)) .join(`<hr class="my-4">`)} </div> </div> </div> ` ) .join(`<hr class="my-16">`)} </div> `; } 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), }; } }