Skip to content

Commit

Permalink
feat(prisma): Support multi file Prisma schemas (#10869)
Browse files Browse the repository at this point in the history
feat(prisma): Support multi file Prisma schemas (#10869) by @dthyresson

Prisma's `prismaSchemaFolder`
[feature](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema)
allows you to define multiple files in a schema subdirectory of your
prisma directory.

This PR updates:

* Prisma utilities
* generators
* dbAuth setup
* documentation

to support single and multi file Prisma schemas.

If you have enabled Prisma multi file schemas, you configure your
project toml api `schemaPath` setting the directory where your
schema.prisma can be found, for example: './api/db/schema'

When [organizing your Prisma Schema into multiple
files](https://www.prisma.io/blog/organize-your-prisma-schema-with-multi-file-support),
you will need
[enable](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema)
that feature in Prisma, move your `schema.prisma` file into a new
directory such as `./api/db/schema` and then set `schemaPath` in the api
toml config.
:::

For example:

```toml title="redwood.toml"
[api]
  port = 8911  
  schemaPath = "./api/db/schema"
```

---------

Co-authored-by: Josh GM Walker <[email protected]>
  • Loading branch information
dthyresson and Josh-Walker-GM authored Jul 19, 2024
1 parent 531a144 commit cf4dc76
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 159 deletions.
25 changes: 25 additions & 0 deletions .changesets/10869.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
feat(prisma): Support multi file Prisma schemas (#10869) by @dthyresson

Prisma's `prismaSchemaFolder` [feature](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) allows you to define multiple files in a schema subdirectory of your prisma directory.

This PR updates:

* Prisma utilities
* generators
* dbAuth setup
* documentation

to support single and multi file Prisma schemas.

If you have enabled Prisma multi file schemas, you configure your project toml api `schemaPath` setting the directory where your schema.prisma can be found, for example: './api/db/schema'

When [organizing your Prisma Schema into multiple files](https://www.prisma.io/blog/organize-your-prisma-schema-with-multi-file-support), you will need [enable](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) that feature in Prisma, move your `schema.prisma` file into a new directory such as `./api/db/schema` and then set `schemaPath` in the api toml config.
:::

For example:

```toml title="redwood.toml"
[api]
port = 8911
schemaPath = "./api/db/schema"
```
17 changes: 17 additions & 0 deletions docs/docs/app-configuration-redwood-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,26 @@ Don't make secrets available to your web side. Everything in `includeEnvironment
| :------------- | :---------------------------------- | :------------------------- |
| `port` | Port for the api server to listen at | `8911` |
| `host` | Hostname for the api server to listen at | Defaults to `'0.0.0.0'` in production and `'::'` in development |
| `schemaPath` | The location of your Prisma schema. If you have [enabled Prisma multi file schemas](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema), then its value is the directory where your `schema.prisma` can be found, for example: `'./api/db/schema'` | Defaults to `'./api/db/schema.prisma'` |
| `debugPort` | Port for the debugger to listen at | `18911` |
| `serverConfig` | [Deprecated; use the [server file](./docker.md#using-the-server-file) instead] Path to the `server.config.js` file | `'./api/server.config.js'` |

### Multi File Schema

Prisma's `prismaSchemaFolder` [feature](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) allows you to define multiple files in a schema subdirectory of your prisma directory.

:::note Important
If you wish to [organize your Prisma Schema into multiple files](https://www.prisma.io/blog/organize-your-prisma-schema-with-multi-file-support), you will need [enable](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) that feature in Prisma, move your `schema.prisma` file into a new directory such as `./api/db/schema` and then set `schemaPath` in the api toml config.
:::

For example:

```toml title="redwood.toml"
[api]
port = 8911
schemaPath = "./api/db/schema"
```

## [browser]

```toml title="redwood.toml"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ jest.mock('@redwoodjs/cli-helpers', () => {
})

jest.mock('@prisma/internals', () => ({
getSchema: () => {
const fs = require('node:fs')
return fs.readFileSync(dbSchemaPath, 'utf-8')
},
getDMMF: () => {
const fs = require('node:fs')
const schema: string = fs.readFileSync(dbSchemaPath, 'utf-8')
Expand Down
39 changes: 20 additions & 19 deletions packages/auth-providers/dbAuth/setup/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs'
import path from 'node:path'

import { getDMMF } from '@prisma/internals'
import { getDMMF, getSchema } from '@prisma/internals'
import execa from 'execa'

import { getPaths } from '@redwoodjs/cli-helpers'
Expand All @@ -12,38 +13,38 @@ export const functionsPath = getPaths().api.functions.replace(
'',
)

export async function hasModel(name: string) {
export const getModelNames = async () => {
const datamodel = await getSchema(getPaths().api.dbSchema)
const schema = await getDMMF({ datamodel })

return schema.datamodel.models.map((model) => model.name)
}

export const hasModel = async (name: string) => {
if (!name) {
return false
}

// Support PascalCase, camelCase, kebab-case, UPPER_CASE, and lowercase model
// names
const modelName = name.replace(/[_-]/g, '').toLowerCase()
const modelNames = (await getModelNames()).map((name) => name.toLowerCase())

const schema = await getDMMF({ datamodelPath: getPaths().api.dbSchema })

for (const model of schema.datamodel.models) {
if (model.name.toLowerCase() === modelName) {
return true
}
if (modelNames.includes(modelName)) {
return true
}

return false
}

export async function getModelNames() {
const schema = await getDMMF({ datamodelPath: getPaths().api.dbSchema })

return schema.datamodel.models.map((model) => model.name)
}
export async function addModels(models: string) {
const isDirectory = fs.statSync(getPaths().api.dbSchema).isDirectory()

export function addModels(models: string) {
const schema = fs.readFileSync(getPaths().api.dbSchema, 'utf-8')

const schemaWithUser = schema + models

fs.writeFileSync(getPaths().api.dbSchema, schemaWithUser)
if (isDirectory) {
fs.writeFileSync(path.join(getPaths().api.dbSchema, 'user.prisma'), models)
} else {
fs.appendFileSync(getPaths().api.dbSchema, models)
}
}

export function hasAuthPages() {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/prismaHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const handler = async ({ _, $0, commands = [], ...options }) => {
commands[0],
)
) {
if (!fs.existsSync(rwjsPaths.api.dbSchema)) {
// if no schema file or directory exists
const schemaDir = path.dirname(rwjsPaths.api.dbSchema)
if (!fs.existsSync(rwjsPaths.api.dbSchema) && !fs.existsSync(schemaDir)) {
console.error()
console.error(c.error('No Prisma Schema found.'))
console.error(`Redwood searched here '${rwjsPaths.api.dbSchema}'`)
Expand Down
208 changes: 100 additions & 108 deletions packages/cli/src/commands/setup/deploy/providers/render.js
Original file line number Diff line number Diff line change
@@ -1,127 +1,119 @@
// import terminalLink from 'terminal-link'
import path from 'path'

import { getSchema, getConfig } from '@prisma/internals'
import execa from 'execa'
import fs from 'fs-extra'
import { Listr } from 'listr2'
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'
import { errorTelemetry } from '@redwoodjs/telemetry'
import { getPaths } from '@redwoodjs/project-config'

import { getPaths, writeFilesTask, printSetupNotes } from '../../../../lib'
import c from '../../../../lib/colors'
import { addFilesTask, updateApiURLTask } from '../helpers'
import {
POSTGRES_YAML,
RENDER_HEALTH_CHECK,
RENDER_YAML,
SQLITE_YAML,
} from '../templates/render'

export const command = 'render'
export const description = 'Setup Render deploy'
// It's easy for the api side to exceed Render's free-plan limit.
// Because telemetryMiddleware is added to Yargs as middleware,
// we need to set the env var here outside the handler to correctly disable it.
if (process.argv.slice(2).includes('api')) {
process.env.REDWOOD_DISABLE_TELEMETRY = 1
}

export const getRenderYamlContent = async (database) => {
if (database === 'none') {
return {
path: path.join(getPaths().base, 'render.yaml'),
content: RENDER_YAML(''),
}
}
if (!fs.existsSync('api/db/schema.prisma')) {
throw new Error("Could not find prisma schema at 'api/db/schema.prisma'")
}
export const command = 'render <side>'
export const description = 'Build, migrate, and serve command for Render deploy'

const schema = await getSchema('api/db/schema.prisma')
const config = await getConfig({ datamodel: schema })
const detectedDatabase = config.datasources[0].activeProvider
export const builder = (yargs) => {
yargs
.positional('side', {
choices: ['api', 'web'],
description: 'Side to deploy',
type: 'string',
})
.option('prisma', {
description: 'Apply database migrations',
type: 'boolean',
default: true,
})
.option('data-migrate', {
description: 'Apply data migrations',
type: 'boolean',
default: true,
alias: 'dm',
})
.epilogue(
`For more commands, options, and examples, see ${terminalLink(
'Redwood CLI Reference',
'https://redwoodjs.com/docs/cli-commands#deploy',
)}`,
)
}

if (detectedDatabase === database) {
switch (database) {
case 'postgresql':
return {
path: path.join(getPaths().base, 'render.yaml'),
content: RENDER_YAML(POSTGRES_YAML),
}
case 'sqlite':
return {
path: path.join(getPaths().base, 'render.yaml'),
content: RENDER_YAML(SQLITE_YAML),
}
default:
throw new Error(`
Unexpected datasource provider found: ${database}`)
}
} else {
throw new Error(`
Prisma datasource provider is detected to be ${detectedDatabase}.
export const handler = async ({ side, prisma, dataMigrate }) => {
recordTelemetryAttributes({
command: 'deploy render',
side,
prisma,
dataMigrate,
})

Option 1: Update your schema.prisma provider to be ${database}, then run
yarn rw prisma migrate dev
yarn rw setup deploy render --database ${database}
const rwjsPaths = getPaths()

Option 2: Rerun setup deploy command with current schema.prisma provider:
yarn rw setup deploy render --database ${detectedDatabase}`)
const execaConfig = {
cwd: rwjsPaths.base,
shell: true,
stdio: 'inherit',
}
}

export const builder = (yargs) =>
yargs.option('database', {
alias: 'd',
choices: ['none', 'postgresql', 'sqlite'],
description: 'Database deployment for Render only',
default: 'postgresql',
type: 'string',
})
async function runApiCommands() {
if (prisma) {
console.log('Running database migrations...')
execa.commandSync(
`node_modules/.bin/prisma migrate deploy --schema "${rwjsPaths.api.dbSchema}"`,
execaConfig,
)
}

// any notes to print out when the job is done
const notes = [
'You are ready to deploy to Render!\n',
'Go to https://dashboard.render.com/iacs to create your account and deploy to Render',
'Check out the deployment docs at https://render.com/docs/deploy-redwood for detailed instructions',
'Note: After first deployment to Render update the rewrite rule destination in `./render.yaml`',
]
if (dataMigrate) {
console.log('Running data migrations...')
const packageJson = fs.readJsonSync(
path.join(rwjsPaths.base, 'package.json'),
)
const hasDataMigratePackage =
!!packageJson.devDependencies['@redwoodjs/cli-data-migrate']

const additionalFiles = [
{
path: path.join(getPaths().base, 'api/src/functions/healthz.js'),
content: RENDER_HEALTH_CHECK,
},
]
if (!hasDataMigratePackage) {
console.error(
[
"Skipping data migrations; your project doesn't have the `@redwoodjs/cli-data-migrate` package as a dev dependency.",
"Without it installed, you're likely to run into memory issues during deploy.",
"If you want to run data migrations, add the package to your project's root package.json and deploy again:",
'',
'```',
'yarn add -D @redwoodjs/cli-data-migrate',
'```',
].join('\n'),
)
} else {
execa.commandSync('yarn rw dataMigrate up', execaConfig)
}
}

export const handler = async ({ force, database }) => {
recordTelemetryAttributes({
command: 'setup deploy render',
force,
database,
})
const tasks = new Listr(
[
{
title: 'Adding render.yaml',
task: async () => {
const fileData = await getRenderYamlContent(database)
let files = {}
files[fileData.path] = fileData.content
return writeFilesTask(files, { overwriteExisting: force })
},
},
updateApiURLTask('/.redwood/functions'),
// Add health check api function
addFilesTask({
files: additionalFiles,
force,
}),
printSetupNotes(notes),
],
{ rendererOptions: { collapseSubtasks: false } },
)
const serverFilePath = path.join(rwjsPaths.api.dist, 'server.js')
const hasServerFile = fs.pathExistsSync(serverFilePath)

if (hasServerFile) {
execa(`yarn node ${serverFilePath}`, execaConfig)
} else {
const { handler } = await import(
'@redwoodjs/api-server/dist/apiCLIConfigHandler.js'
)
handler()
}
}

async function runWebCommands() {
execa.commandSync('yarn install', execaConfig)
execa.commandSync('yarn rw build web --verbose', execaConfig)
}

try {
await tasks.run()
} catch (e) {
errorTelemetry(process.argv, e.message)
console.error(c.error(e.message))
process.exit(e?.exitCode || 1)
if (side === 'api') {
runApiCommands()
} else if (side === 'web') {
runWebCommands()
}
}
Loading

0 comments on commit cf4dc76

Please sign in to comment.