214
loading...
This website collects cookies to deliver better user experience
If you aren't familiar with Nx, check it out!
Nx
. In this tutorial we'll be working off of an empty
project, essentially the most basic setup. You can use whichever preset you'd like though.prisma-clients
that will provide our Prisma Clients.import { DbOneClient, DbTwoClient } from '@nx-prisma/prisma-clients'
// Or individually, so we can pull out generated types
import { DbTwoClient, SomeType } from '@nx-prisma/prisma-clients/db-two'
const Client1 = new DbOneClient()
const Client2 = new DbTwoClient()
workpace-generators
. These are generators that we can easily build into our projects to automate tasks that are repetitive or tedious (like setting up a new prisma project...).Nx
provides this command that will create the base files we'll need:nx generate @nrwl/workspace:workspace-generator prisma-generator
tools/generators
folder inside of a folder named whatever name you provided the command (in our case prisma-generator
)libs
folder. If you'd like to see what would be generated by this generator (or any generator) without actually creating files, you can pass the --dry-run
flag.nx workspace-generator prisma-generator --name=Test --dry-run
index.ts
: This is the file where we build out our generator functionality and will use Nrwl's devkit to build the Prisma clientschema.json
: This is where we configure the options and descriptions of our generator. We'll be setting up inputs in this file so we can configure our clientindex.ts
file at tools/generators/prisma-generator/index.ts
file you should see the code for the default generator.import { Tree, formatFiles, installPackagesTask } from '@nrwl/devkit';
import { libraryGenerator } from '@nrwl/workspace/generators';
export default async function (tree: Tree, schema: any) {
await libraryGenerator(tree, { name: schema.name });
await formatFiles(tree);
return () => {
installPackagesTask(tree);
};
}
console.log
the schema argument. This is going to hold the input options we give it via the terminal.import { Tree } from '@nrwl/devkit';
export default async function (tree: Tree, schema: any) {
console.log(schema)
}
test
, you should see the following output:// nx workspace-generator prisma-generator --name=test --dry-run
{ "name": "test" }
name
: The name of the prisma project, which we will use to set up the proper names for the files, imports, and exports we will be generatingprovider
: The name of the provider so we can correctly set up the schema's datasource block. (See a full list of providers here)
connectionString
: Connection string that will be added to a generated variable in a .env
file that all prisma schemas will share.schema.json
. Inside that file there is a properties
object where we configure them. Currently it should have one default input."properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
}
}
}
name
flag with the generatornx workspace-generator prisma-generator --name=Test
name
so let's just modify this one. All we really need to do is change its description (Which will display nicely in the Nx extension view). Also we'll remove the $default
value configuration because we won't need this and add an x-prompt
so we'll get a nice prompt when running it via the terminal."name": {
"type": "string",
"description": "Prisma Project Name",
"x-prompt": "What do you want to call the project?"
},
provider
. To give this a nice UI, we'll go ahead and make this a radio option with a list of values to choose from.x-prompt
of the type list
."provider": {
"type": "string",
"description": "Database Type",
"x-prompt": {
"message": "Which type of database are you connecting to?",
"type": "list",
"items": [
{ "value": "sqlserver", "label": "MSSQL" },
{ "value": "postgresql", "label": "Postgres" },
{ "value": "mysql", "label": "MySQL" },
{ "value": "sqlite", "label": "SQLite" },
{ "value": "mongodb", "label": "MongoDB" }
]
}
}
provider
to the list of required fields, using the required array at the bottom. It should now read:"required": ["name", "provider"]
connectionString
. This one will be almost exactly like the name
field, a simple text input. We'll also add it to the array of required
fields."connectionString": {
"type": "string",
"description": "Connection String",
"x-prompt": "What is the connection string you want to use?"
},
...
"required": ["name", "provider", "connectionString"]
We are going to create some files and folders that have strange names like __name__
. These will be eventually replaced by Nrwl's devkit with a meaningful value
template
and another folder within that one called __name__
. This is where we will hold our template files.__name__
folder, let's initialize Prisma to give us a starting point for our template.ejs
allows us to use variables in file and folder names using this syntax: __variable_name__
prisma init
.env
file that was generated here. We'll be using a shared .env
file that is auto-generated so we can configure the environment variables all in one place.schema.prisma
file and add some variables into the template that will get hydrated when the generator runs.generator client {
provider = "prisma-client-js"
output = "<%= outputLocation %>"
}
datasource db {
provider = "<%= dbType %>"
url = env("<%= constantName %>_SOURCE_URL")
}
ejs
syntax, which is used by the devkit under the hood.An ejs
variable is set up using this syntax: <%= variable_name %>
schema.prisma
file. That's because, as you may expect, prisma doesn't know about ejs
and thinks it’s just invalid syntax. schema.prisma__tmpl__
as we will be setting up something later on to strip out __tmpl__
from file names.schema.prisma
file is ready to be hydrated by a generator. The next thing we'll want to add is an index.ts
file that will export our generated Prisma Client
so we can access it as a library. Add that file into the template/__name__
folder.Prisma Client
itself with a custom name to match the project name.export { PrismaClient as <%= className %>Client } from '.prisma/<%= name %>-client';
export * from '.prisma/<%= name %>-client'
Note we used className
instead of name
for the renamed export. We'll create a version of the name
key that is PascalCase
to match common formatting.
index.ts__tmpl__
so that the compiler does not recognize it as a TypeScript
file, otherwise the compiler will pick the file up and try to compile it. This would cause a failure because of the ejs
.console.log
the terminal input.import { Tree } from '@nrwl/devkit';
interface GeneratorOptions {
name: string;
provider: string;
connectionString: string;
}
export default async function (tree: Tree, schema: GeneratorOptions) {
console.log(schema)
}
tree
variable is. This is a variable that gets passed to a generator that represents the file system. We can perform certain operations like reading files and writing files with that function.@nrwl/devkit
also provides more functions we'll be using in this generator. The first one is names
.import { Tree, names } from '@nrwl/devkit';
interface GeneratorOptions {
name: string;
provider: string;
connectionString: string;
}
export default async function (tree: Tree, schema: GeneratorOptions) {
const nameFormats = names(schema.name)
}
test_name
to the function, we would get this object back:{
name: "test_name",
className: "TestName",
propertyName: "testName",
constantName: "TEST_NAME",
fileName: "test-name"
}
generateFiles
function. This function takes in four parameters:Parameter | Description |
---|---|
tree | This will be the tree variable that represents the filesystem |
srcFolder | Path to the template folder |
target | Output Path |
substitutions | An object that sets up the variables we will use to hydrate the template where we set up ejs variables |
import {
Tree,
names,
generateFiles,
joinPathFragments
} from '@nrwl/devkit';
interface GeneratorOptions {
name: string;
provider: string;
connectionString: string;
}
export default async function (tree: Tree, schema: GeneratorOptions) {
const nameFormats = names(schema.name)
generateFiles(
tree,
joinPathFragments(__dirname, './template'),
'libs/prisma-clients',
{}
)
}
generateFiles
function and a helper function named joinPathFragments
so that we can use __dirname
to get to the current directory. libs/prisma-clients
folder (it will get created if it does not exist). The only problem is we haven't replaced the ejs
variables with meaningful values yet! We can fill in the substitutions argument with our data to get that to work.dbType
: Our providertmpl
: A variable we want to replace with ''
to strip __tmpl__
out of the file namesname
: The name of the prisma project we are generatingclassName
: The class-name format of the project nameconstantName
: All-caps version of our project nameoutputLocation
: The output location of the generated client
const { name, className, constantName } = names(schema.name)
generateFiles(
tree,
joinPathFragments(__dirname, './template'),
'libs/prisma-clients',
{
dbType: schema.provider,
tmpl: '',
name,
className,
constantName,
outputLocation: `../../../../node_modules/.prisma/${name}-client`
}
)
name
and className
out of the object the names
function returns. Then in the substitutions object in generateFiles
we added all of the variables the template is expecting.Note the output location: Prisma has the ability to be generated anywhere, however there is a known issue with Prisma and Nx not playing nicely together when the Prisma client is inside of the Nx
project and the project is built. We are specifying that we want it to go to the default .prisma
folder, but in a separate client folder with our project's name.
Nx
project! .env
file to hold our connection strings. To do this we'll make use of the file tree's exists
, read
and write
functions.generateFiles
function, add the following code:import {
formatFiles,
generateFiles,
joinPathFragments,
names,
Tree
} from '@nrwl/devkit';
// ...
// Write .env
if ( !tree.exists('.env') ) {
tree.write('.env', '')
}
let contents = tree.read('.env').toString()
contents += `${constantName}_SOURCE_URL=${schema.connectionString}\n`
tree.write('.env', contents)
await formatFiles(tree)
.env
file exists in the root project folder. If not, it creates one with no content.index.ts
file that exports each client in one location.// Write export
if ( !tree.exists('libs/prisma-clients/index.ts') ) {
tree.write('libs/prisma-clients/index.ts', '')
}
let exportsConents = tree.read('libs/prisma-clients/index.ts').toString()
exportsConents += `export { ${className}Client } from './${name}';\n`
tree.write('libs/prisma-clients/index.ts', exportsConents)
await formatFiles(tree)
formatFiles
function from the devkit to format the files we added and modified in this generator function.import {
formatFiles,
generateFiles,
joinPathFragments,
names,
Tree
} from '@nrwl/devkit';
interface GeneratorOptions {
name: string;
provider: string;
connectionString: string;
}
export default async function (tree: Tree, schema: GeneratorOptions) {
const { name, className, constantName } = names(schema.name)
generateFiles(
tree,
joinPathFragments(__dirname, './template'),
'libs/prisma-clients',
{
dbType: schema.provider,
tmpl: '',
name,
className,
constantName,
outputLocation: `../../../../node_modules/.prisma/${name}-client`
}
)
// Write .env
if ( !tree.exists('.env') ) {
tree.write('.env', '')
}
let envContents = tree.read('.env').toString()
envContents += `${constantName}_SOURCE_URL=${schema.connectionString}\n`
tree.write('.env', envContents)
// Write export
if ( !tree.exists('libs/prisma-clients/index.ts') ) {
tree.write('libs/prisma-clients/index.ts', '')
}
let exportsConents = tree.read('libs/prisma-clients/index.ts').toString()
exportsConents += `export { ${className}Client } from './${name}';\n`
tree.write('libs/prisma-clients/index.ts', exportsConents)
await formatFiles(tree)
}
SQLite
database...ejs
variables were filled in with the values we provided.schema.prisma
file and add a model:generator client {
provider = "prisma-client-js"
output = "../../../../node_modules/.prisma/test-client"
}
datasource db {
provider = "sqlite"
url = env("TEST_SOURCE_URL")
}
model User {
id Int @id
}
prisma db push --schema="./libs/prisma-clients/sqlite-test/prisma/schema.prisma"
prisma generate --schema="./libs/prisma-clients/sqlite-test/prisma/schema.prisma"
tsconfig.base.json
we’ll create a pathing configuration that allows easy access to our prisma clients by adding two records to the paths
object:"paths": {
"@nx-prisma/prisma-clients": [
"libs/prisma-clients/index.ts"
],
"@nx-prisma/prisma-clients/*": [
"libs/prisma-clients/*"
]
}
npm install -D @nrwl/nest
nx generate @nrwl/nest:application nest-app
apps
folder.apps/nest-app/src/app/app.service.ts
, import the client and add a function to create and read some data:import { Injectable } from '@nestjs/common';
import { SqliteTestClient } from '@nx-prisma/prisma-clients'
import { User } from '@nx-prisma/prisma-clients/sqlite-test'
@Injectable()
export class AppService {
private prisma: SqliteTestClient;
constructor() {
this.prisma = new SqliteTestClient()
}
async getData(): Promise<User[]> {
this.prisma.$connect()
await this.prisma.user.create({ data: { id: Math.floor(Math.random() * 1000) + 1}})
const users = await this.prisma.user.findMany()
this.prisma.$disconnect()
return users
}
}
NOTE: This is not production-ready code, only a quick sample to show data being read and returned. Check out Prisma's docs detailing how to handle prisma connections in Nest.
nx serve nest-app
, it should start up the server at http://localhost:3333
and have an /api
endpoint. http://localhost:3333/api
and refresh the page a few times. You should see that it creates a new record each time and returns the data.