Skip to content

Instantly share code, notes, and snippets.

@johannesschobel
Last active November 17, 2022 07:51
Show Gist options
  • Save johannesschobel/365383f9d639d34f708c407389b93aa9 to your computer and use it in GitHub Desktop.
Save johannesschobel/365383f9d639d34f708c407389b93aa9 to your computer and use it in GitHub Desktop.
Prisma CRUD Service Generator for NestJS

This is a draft version for a generator.

As my previous attempt (described in prisma/prisma#5273) to create a "prisma crud service" failed to provide a typesafe service, i decided to take another route.

I then thought about creating a custom generator that can be invoked via the schema.prisma file. The generator, in turn, will now create a x.service.ts file, for each model x in the schema.prisma file (dmmf). This service, in turn, contains all CRUD methods that are properly typed. Note that this service than can easily extended or injected to provide this feature.

Approach

The current approach looks like this:

FILE 1) This serves as main entry-point for the generator. It then simply calls the generate method from the other file

FILE 2) This provides some basic ideas, i.e., create the output folder and then calls the function to create all the files.

FILE 3) This is a stub file, i.e., it is the blueprint for all services. Note the __Class__ and __class__ placeholders, that are then replaced when loading this file

FILE 4) This is a (simplified) version of the basic logic. It, for example, transforms all dmmf.models to a respective x.service.ts file, and saves them to the specified folder. Finally, it also creates a barrel file (i.e., the index.ts) in this folder.

Usage

Simply add a new generator to the schema.prisma file, similar to this:

generator nestjs {
  provider    = "node ./node_modules/@trackyourhealth/nestjs-restful-generator"
  output      = "./your/custom/output/path"
  serviceStub = "./path/to/custom/stub/from/root/directory.txt"
}

I have already published a version of this generator to npm, so feel free to try this sample! The package can be downloaded via @trackyourhealth/nestjs-restful-generator!

Evaluation

I have tested this approach in a playground application and it certainly works. My workflow looks like this:

  1. install package @trackyourhealth/nestjs-restful-generator
  2. add custom generator like described above and set a valid output path
  3. run prisma generate to create models and services
  4. in a nestjs module create a new user.service.ts and let it extend the autogenerated UserCrudService from this generated package
  5. Inject this service wherever needed

Limitations

This approach certainly has limitations. For now, i see the following drawbacks (compared to my first approach described in the GitHub Issue)

  • I (as the package maintainer) will decide how the x.crud.service will look like - and not you (as the developer using this package). I will try to upgrade the generator to allow for custom stub templates. Had no time for this yet :D (this has been added in v 0.0.5 of this package)
  • The developer cannot adjust the code (i.e., change one particular method of one particular service) to better fit the application scenario. Once you re-run the generator your code will be overwritten! In order achieve this, you will need to extend and overwrite!

But most importantly:

  • You will not reduce boilerplate code. You will just use a generator to do the dirty work for you.

Not sure if this the right approach, but lets discuss!

All the best and thanks for your time

#!/usr/bin/env node
import { generatorHandler } from '@prisma/generator-helper';
import { generate } from './nestjs-restful-generator';
generatorHandler({
onManifest: () => ({
defaultOutput: 'node_modules/@generated/prisma/nestjs',
prettyName: 'NestJS RESTful Service Integration',
requiresGenerators: ['prisma-client-js'],
}),
onGenerate: generate,
});
import { DMMF as PrismaDMMF } from '@prisma/client/runtime';
import { GeneratorOptions } from '@prisma/generator-helper';
import { promises as fs } from 'fs';
import * as path from 'path';
import { generateCode } from './generator/generate-code';
import { GenerateCodeOptions } from './generator/generate-code.options';
import { toUnixPath } from './generator/helpers';
import { removeDir } from './utils';
export const generate = async (options: GeneratorOptions) => {
// prepare the output folder
const outputDir = options.generator.output ?? '';
await fs.mkdir(outputDir, { recursive: true });
await removeDir(outputDir, true);
// get the original prisma client
const prismaClientPath =
options.otherGenerators.find(
(client) => client.provider === 'prisma-client-js',
)?.output ?? null;
if (prismaClientPath === null) {
throw new Error(
'NestJS REST Generator - Could not resolve `prisma-client-js` path',
);
}
// we are ready to go!
// eslint-disable-next-line @typescript-eslint/no-var-requires
const prismaClientDmmf = require(prismaClientPath)
.dmmf as PrismaDMMF.Document;
const generatorConfig = options.generator.config;
const config: GenerateCodeOptions = {
outputDirPath: outputDir,
relativePrismaOutputPath: toUnixPath(
path.relative(outputDir, prismaClientPath),
),
absolutePrismaOutputPath: prismaClientPath.includes('node_modules')
? prismaClientPath.split('node_modules/')[1]
: undefined,
};
// generate all the stuff
await generateCode(prismaClientDmmf, config);
};
export const stubContent = `import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
@Injectable()
export class __Class__CrudService {
constructor(private readonly prisma: PrismaClient) {}
async aggregate(data: Prisma.__Class__AggregateArgs) {
try {
const result = await this.prisma.__class__.aggregate(data);
return result;
} catch (exception) {
throw new Error(exception);
}
}
async count(data: Prisma.__Class__CountArgs) {
try {
const result = await this.prisma.__class__.count(data);
return result;
} catch (exception) {
throw new Error(exception);
}
}
async create(data: Prisma.__Class__CreateArgs) {
try {
const result = await this.prisma.__class__.create(data);
return result;
} catch (exception) {
throw new Error(exception);
}
}
// ...
}
`;
import { DMMF as PrismaDMMF } from '@prisma/client/runtime';
import { promises as fs } from 'fs';
import * as path from 'path';
import { CompilerOptions, ModuleKind, Project, ScriptTarget } from 'ts-morph';
import { stubContent } from '../stubs/service.stub';
import { servicesFolderName } from './config';
import { generateServicesBarrelFile } from './file.writer';
import { GenerateCodeOptions } from './generate-code.options';
const baseCompilerOptions: CompilerOptions = {
target: ScriptTarget.ES2019,
module: ModuleKind.CommonJS,
emitDecoratorMetadata: true,
experimentalDecorators: true,
};
export const generateCode = async (
dmmf: PrismaDMMF.Document,
options: GenerateCodeOptions,
) => {
const project = new Project({
compilerOptions: {
...baseCompilerOptions,
...{ declaration: true },
},
});
// we process the services
await createServicesFromModels(project, dmmf, options);
await project.save();
};
async function createServicesFromModels(
project: Project,
dmmf: PrismaDMMF.Document,
options: GenerateCodeOptions,
) {
const servicePath = path.join(options.outputDirPath, servicesFolderName);
await fs.mkdir(path.join(servicePath), { recursive: true });
const barrelFilePath = path.join(servicePath, 'index.ts');
const serviceBarrelFile = project.createSourceFile(
barrelFilePath,
undefined,
{ overwrite: true },
);
const models = dmmf.datamodel.models;
const serviceStubContent = stubContent;
const serviceNames: string[] = [];
for (const model of models) {
console.log(`Processing Model ${model.name}`);
let serviceContent = serviceStubContent;
// now we replace some placeholders
serviceContent = serviceContent.replace(/__Class__/g, model.name);
serviceContent = serviceContent.replace(
/__class__/g,
model.name.toLowerCase(),
);
// write to output
const outputFileName = `${model.name.toLowerCase()}.service`;
const outputFile = `${outputFileName}.ts`;
project.createSourceFile(
path.join(servicePath, outputFile),
serviceContent,
{ overwrite: true },
);
serviceNames.push(outputFileName);
}
generateServicesBarrelFile(serviceBarrelFile, serviceNames);
}
@FredrikBorgstrom
Copy link

FredrikBorgstrom commented Nov 16, 2022

Hi,
I managed to get your code to work with just a few minor modifications.
However, the source code for your NPM package is gone on Github. Would it be possible for you to share it again? I would really like to continue working on it.

Cheers,
Fredrik

@johannesschobel
Copy link
Author

Dear @FredrikBorgstrom , take a look at https://github.com/prisma-utils/prisma-utils and especially the prisma-crud-generator library.
Respective library is a fully fledged crud generator, developed by me:
Take a look at the documentation here https://github.com/prisma-utils/prisma-utils/tree/main/libs/prisma-crud-generator

The library can be found and installed via npm:
https://www.npmjs.com/package/@prisma-utils/prisma-crud-generator

All the best

@FredrikBorgstrom
Copy link

Great, thanks a lot for sharing!

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