Skip to content

Commit

Permalink
feat: add prisma-sync generator
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Jan 16, 2024
1 parent ac32750 commit 448fe8d
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 1 deletion.
5 changes: 5 additions & 0 deletions libs/tools/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"schema": "./src/generators/prisma-model/prisma-model-schema.json",
"description": "prisma-model generator"
},
"prisma-sync": {
"factory": "./src/generators/prisma-sync/prisma-sync-generator",
"schema": "./src/generators/prisma-sync/prisma-sync-schema.json",
"description": "prisma-sync generator"
},
"rename": {
"factory": "./src/generators/rename/rename-generator",
"schema": "./src/generators/rename/rename-schema.json",
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "@pubkey-stack/tools",
"version": "0.0.1",
"dependencies": {
"@clack/prompts": "^0.7.0",
"@nx/devkit": "17.2.8",
"@clack/prompts": "^0.7.0",
"@mrleebo/prisma-ast": "^0.8.0",
"@nx/nest": "17.2.8",
"@nx/react": "17.2.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const variable = "<%= name %>";
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Tree } from '@nx/devkit'
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
import { normalizePrismaSyncSchema } from '../../lib/prisma/normalize-prisma-sync-schema'

import { prismaSyncGenerator } from './prisma-sync-generator'
import { PrismaSyncGeneratorSchema } from './prisma-sync-schema'

describe('prisma-sync generator', () => {
let tree: Tree
const options: PrismaSyncGeneratorSchema = normalizePrismaSyncSchema({ app: 'test' })

beforeEach(() => {
tree = createTreeWithEmptyWorkspace()
})

it('should run successfully', async () => {
tree.write('prisma/schema.prisma', '')
await prismaSyncGenerator(tree, options)
})
})
16 changes: 16 additions & 0 deletions libs/tools/src/generators/prisma-sync/prisma-sync-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Tree } from '@nx/devkit'
import { normalizePrismaSyncSchema } from '../../lib/prisma/normalize-prisma-sync-schema'
import { syncPrismaEntities } from '../../lib/prisma/sync-prisma-entities'
import { PrismaSyncGeneratorSchema } from './prisma-sync-schema'

export async function prismaSyncGenerator(tree: Tree, rawOptions: PrismaSyncGeneratorSchema) {
const options = normalizePrismaSyncSchema(rawOptions)
const prisma = tree.exists(options.schemaFile)
if (!prisma) {
throw new Error('Prisma schema not found')
}

syncPrismaEntities(tree, options)
}

export default prismaSyncGenerator
5 changes: 5 additions & 0 deletions libs/tools/src/generators/prisma-sync/prisma-sync-schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PrismaSyncGeneratorSchema {
app: string
schemaFile?: string
verbose?: boolean
}
24 changes: 24 additions & 0 deletions libs/tools/src/generators/prisma-sync/prisma-sync-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "PrismaSyncSchema",
"title": "",
"type": "object",
"properties": {
"app": {
"type": "string",
"description": "The name of the application to sync",
"default": "api"
},
"schemaFile": {
"type": "string",
"description": "The path to the schema file to sync",
"default": "prisma/schema.prisma"
},
"verbose": {
"type": "boolean",
"description": "Print more output",
"default": false
}
},
"required": []
}
9 changes: 9 additions & 0 deletions libs/tools/src/lib/prisma/get-domain-from-project-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function getDomainFromProjectName(project: string, app: string) {
return (
project
// Remove the data-access suffix from the path
.replace('/data-access', '')
// Remove everything with ${app}/ and before it
.replace(new RegExp(`.*${app}/`), '')
)
}
9 changes: 9 additions & 0 deletions libs/tools/src/lib/prisma/get-prisma-enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Enum } from '@mrleebo/prisma-ast'
import { Tree } from '@nx/devkit'
import { getPrismaSchema } from './get-prisma-schema'

export function getPrismaEnums(tree: Tree, schemaPath = 'prisma/schema.prisma'): Enum[] {
const schema = getPrismaSchema(tree, schemaPath)

return schema.list.filter((item) => item.type === 'enum') as Enum[]
}
1 change: 1 addition & 0 deletions libs/tools/src/lib/prisma/get-prisma-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { getPrismaSchema } from './get-prisma-schema'

export function getPrismaModels(tree: Tree, schemaPath = 'prisma/schema.prisma'): Model[] {
const schema = getPrismaSchema(tree, schemaPath)

return schema.list.filter((item) => item.type === 'model') as Model[]
}
70 changes: 70 additions & 0 deletions libs/tools/src/lib/prisma/get-project-entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { getProjects, names, Tree } from '@nx/devkit'
import { getDomainFromProjectName } from './get-domain-from-project-name'

export const suffixEnum = '.enum.ts'
export const suffixModel = '.entity.ts'
const filterModels = ['app-config', 'paging-meta', 'paging-response', '-paging']

// Get an array of all the data-access projects with their entity folder and entities
export function getProjectEntities(tree: Tree, app: string) {
// Get an array of all the projects
const projects = Array.from(getProjects(tree).values())

// The path to the entity folder inside a data-access project
const entityPath = 'src/lib/entity'

// Get an array of all the data-access projects with their entity folder
return (
projects
// Filter to only include data-access projects
.filter((project) => project.name.includes('data-access'))
// Map the root path to the entity folder path
.map(({ name, root }) => ({
name,
root,
entityRoot: `${root}/${entityPath}`,
domain: getDomainFromProjectName(root, app),
}))
// Filter to only include projects that have an entity folder
.filter((root) => tree.exists(root.entityRoot))
.map((project) => ({
...project,
enums: getProjectEnums(tree, project.entityRoot),
models: getProjectModels(tree, project.entityRoot),
}))
// Filter out projects that don't have models or enums
.filter((project) => project.enums.length > 0 || project.models.length > 0)
)
}

// Get an array of all the enums in the entity folder
function getProjectEnums(tree: Tree, entityRoot: string) {
return (
tree
// Get all the files in the entity folder
.children(entityRoot)
// Filter to only include files that match the enum suffix
.filter((item) => item.endsWith(suffixEnum))
// Remove the suffix from the file name
.map((item) => item.replace(`${suffixEnum}`, ''))
// Convert the file name to the class name
.map((item) => names(item).className)
)
}

// Get an array of all the models in the entity folder
function getProjectModels(tree: Tree, entityRoot: string) {
return (
tree
// Get all the files in the entity folder
.children(entityRoot)
// Filter to only include files that match the model suffix
.filter((child) => child.endsWith(suffixModel))
// Filter to remove the -paging.${suffixModel} files
.filter((child) => !filterModels.some((filter) => child.includes(filter)))
// Remove the suffix from the file name
.map((child) => child.replace(`${suffixModel}`, ''))
// Convert the file name to the class name
.map((child) => names(child).className)
)
}
9 changes: 9 additions & 0 deletions libs/tools/src/lib/prisma/normalize-prisma-sync-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PrismaSyncGeneratorSchema } from '../../generators/prisma-sync/prisma-sync-schema'

export function normalizePrismaSyncSchema(schema: PrismaSyncGeneratorSchema): PrismaSyncGeneratorSchema {
return {
app: schema.app ?? 'api',
schemaFile: schema.schemaFile ?? 'prisma/schema.prisma',
verbose: schema.verbose ?? false,
}
}
130 changes: 130 additions & 0 deletions libs/tools/src/lib/prisma/sync-prisma-entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { names, Tree } from '@nx/devkit'
import { PrismaSyncGeneratorSchema } from '../../generators/prisma-sync/prisma-sync-schema'
import { getPrismaEnums } from './get-prisma-enums'
import { getPrismaModels } from './get-prisma-models'
import { getProjectEntities, suffixEnum, suffixModel } from './get-project-entities'

export function syncPrismaEntities(tree: Tree, { app, verbose }: PrismaSyncGeneratorSchema) {
// Get all the projects with entities
const projects = getProjectEntities(tree, app)
// Get a list of all the domains in the workspace
const domains = projects.map((i) => i.domain)

// Loop through all the enums in the prisma schema
for (const prismaEnum of getPrismaEnums(tree)) {
// Check if the enum exists in the workspace

const found = projects
// Loop through all the projects in the workspace and find the one that has the enum
.find((entity) => entity.enums.find((entityEnum) => entityEnum === prismaEnum.name))

if (found) {
log(`SKIP: Enum ${prismaEnum.name} => found in project ${found.name}`)
continue
}

const domain = domains.find((domain) => prismaEnum.name.toLowerCase().startsWith(domain.toLowerCase()))

if (!domain) {
log(`SKIP: Enum ${prismaEnum.name} => can't find domain`)
continue
}
// Get the project to add the enum to
const project = projects.find((project) => project.domain === domain)

// Get the file name for the enum
const filename = names(prismaEnum.name + suffixEnum).fileName

// Get the target file path
const file = `${project.entityRoot}/${filename}`

// Get the template for the enum
const content = getEnumTemplate(prismaEnum.name)

tree.write(file, content)
if (verbose) {
log(`ADD Enum ${prismaEnum.name} to project ${project.name}`)
}

const exportLine = `export * from './lib/entity/${filename}'`.replace('.ts', '')
const indexFile = `${project.root}/src/index.ts`
const indexContent = tree.read(indexFile).toString()

if (!indexContent.includes(exportLine)) {
tree.write(indexFile, `${indexContent}\n${exportLine}`)
}
}

// Loop through all the models in the prisma schema
for (const prismaModel of getPrismaModels(tree)) {
// Check if the model exists in the workspace
const found = projects
// Loop through all the projects in the workspace and find the one that has the model
.find((entity) => entity.models.find((entityModel) => entityModel === prismaModel.name))

if (found) {
log(`SKIP: Model ${prismaModel.name} => found in project ${found.name}`)
continue
}

const domain = domains.find((domain) => prismaModel.name.toLowerCase().startsWith(domain.toLowerCase()))

if (!domain) {
log(`SKIP: Model ${prismaModel.name} => can't find domain`)
continue
}

// Get the project to add the model to
const project = projects.find((project) => project.domain === domain)

// Get the file name for the model
const filename = names(prismaModel.name + suffixModel).fileName

// Get the target file path
const file = `${project.entityRoot}/${filename}`

// Get the template for the model
const content = getModelTemplate(prismaModel.name)

tree.write(file, content)
log(`ADD Model ${prismaModel.name} to project ${project.name}`)

const exportLine = `export * from './lib/entity/${filename}'`.replace('.ts', '')
const indexFile = `${project.root}/src/index.ts`
const indexContent = tree.read(indexFile).toString()

if (!indexContent.includes(exportLine)) {
tree.write(indexFile, `${indexContent}\n${exportLine}`)
}
}

function log(...args: string[]) {
if (!verbose) return
console.log(...args)
}
}

function getEnumTemplate(name: string): string {
return [
`import { registerEnumType } from '@nestjs/graphql'`,
`import { ${name} } from '@prisma/client'`,
`export { ${name} }`,
``,
`registerEnumType(${name}, { name: '${name}' })`,
].join('\n')
}

function getModelTemplate(name: string): string {
return [
`import { Field, HideField, ObjectType } from '@nestjs/graphql'`,
``,
`@ObjectType()`,
`export class ${name} {`,
`@Field()`,
`id!: string`,
`@Field({ nullable: true })`,
`createdAt?: Date`,
`@Field({ nullable: true })`,
`updatedAt?: Date`,
].join('\n')
}

0 comments on commit 448fe8d

Please sign in to comment.