Skip to content

Commit

Permalink
feat(dbAuth): Automatically create User model in fresh projects (#10871)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jun 22, 2024
1 parent 7085042 commit 88190cf
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 78 deletions.
4 changes: 4 additions & 0 deletions .changesets/10871.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- feat(dbAuth): Automatically create User model in fresh projects (#10871) by @Tobbe

Automatically create a `User` model in the project's `schema.prisma` when
setting up dbAuth in a new project.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jest.mock('../shared', () => ({
return require('fs').existsSync(mockLoginPagePath)
},
generateAuthPagesTask: () => undefined,
getModelNames: () => ['ExampleUser'],
}))

jest.mock('@redwoodjs/cli-helpers', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import * as fs from 'node:fs'
import * as path from 'node:path'

import { vol } from 'memfs'
import prompts from 'prompts'

import type { AuthHandlerArgs } from '@redwoodjs/cli-helpers'
import type { AuthGeneratorCtx } from '@redwoodjs/cli-helpers/src/auth/authTasks'

import { createUserModelTask } from '../setupData'
import { handler } from '../setupHandler'

const RWJS_CWD = process.env.RWJS_CWD
const redwoodProjectPath = '/redwood-app'
Expand Down Expand Up @@ -36,6 +42,26 @@ jest.mock('@redwoodjs/cli-helpers', () => {
underline: (str: string) => str,
},
addEnvVarTask: () => {},
// I wish I could have used something like
// jest.requireActual(@redwoodjs/cli-helpers) here, but I couldn't because
// jest doesn't support ESM
standardAuthHandler: async (args: AuthHandlerArgs) => {
if (args.extraTasks) {
const ctx: AuthGeneratorCtx = {
force: args.forceArg,
setupMode: 'UNKNOWN',
provider: 'dbAuth',
}

for (const task of args.extraTasks) {
if (task && task.task) {
await task.task(ctx, undefined)
}
}
}

args.notes && console.log(`\n ${args.notes.join('\n ')}\n`)
},
}
})

Expand All @@ -58,6 +84,17 @@ jest.mock('@prisma/internals', () => ({
},
}))

jest.mock('prompts', () => {
return {
__esModule: true,
default: jest.fn(async (args: any) => {
return {
[args.name]: false,
}
}),
}
})

beforeAll(() => {
process.env.RWJS_CWD = redwoodProjectPath
})
Expand All @@ -66,6 +103,15 @@ afterAll(() => {
process.env.RWJS_CWD = RWJS_CWD
})

beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {})
})

afterEach(() => {
jest.mocked(console).log.mockRestore?.()
jest.mocked(prompts).mockClear?.()
})

describe('setupData createUserModelTask', () => {
it('adds a User model to schema.prisma', async () => {
vol.fromJSON(
Expand Down Expand Up @@ -176,4 +222,159 @@ model Post {

expect(schema.match(/^model User {/gm)).toHaveLength(1)
})

it('automatically adds a User model in fresh projects', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
'api/src/functions/graphql.ts': `
import { createGraphQLHandler } from "@redwoodjs/graphql-server"
import { getCurrentUser } from 'src/lib/auth'
`,
'api/db/schema.prisma': `
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
// Define your own data models here and run 'yarn redwood prisma migrate dev'
// to create migrations for them and apply to your dev DB.
model UserExample {
id Int @id @default(autoincrement())
email String @unique
name String?
}
`,
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: null,
generateAuthPages: false,
force: false,
})

expect(jest.mocked(prompts)).not.toHaveBeenCalled()

const schema = fs.readFileSync(dbSchemaPath, 'utf-8')
expect(schema).toMatch(/^model User {$/m)
expect(jest.mocked(console).log).toHaveBeenCalledWith(
expect.stringContaining('Done! But you have a little more work to do:'),
)
expect(jest.mocked(console).log).not.toHaveBeenCalledWith(
expect.stringContaining('resetTokenExpiresAt DateTime? // <─'),
)
})

it('automatically adds a User model given the rwjs template schema.prisma', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
'api/src/functions/graphql.ts': `
import { createGraphQLHandler } from "@redwoodjs/graphql-server"
import { getCurrentUser } from 'src/lib/auth'
`,
'api/db/schema.prisma': jest
.requireActual('fs')
.readFileSync(
path.resolve(
__dirname +
'/../../../../../create-redwood-app/templates/ts/api/db/schema.prisma',
),
'utf-8',
),
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: null,
generateAuthPages: false,
force: false,
})

expect(jest.mocked(prompts)).not.toHaveBeenCalled()

const schema = fs.readFileSync(dbSchemaPath, 'utf-8')
// Check for UserExample just to make sure we're reading the actual
// template file and that it looks like we expect. So we're not just
// getting an empty file or something
expect(schema).toMatch(/^model UserExample {$/m)
expect(schema).toMatch(/^model User {$/m)
expect(jest.mocked(console).log).toHaveBeenCalledWith(
expect.stringContaining('Done! But you have a little more work to do:'),
)
expect(jest.mocked(console).log).not.toHaveBeenCalledWith(
expect.stringContaining('resetTokenExpiresAt DateTime? // <─'),
)
})

it('does not automatically add a User model in projects with custom db models', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
'api/src/functions/graphql.ts': `
import { createGraphQLHandler } from "@redwoodjs/graphql-server"
import { getCurrentUser } from 'src/lib/auth'
`,
'api/db/schema.prisma': `
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model ExampleModel {
id Int @id @default(autoincrement())
email String @unique
name String?
}
`,
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: null,
generateAuthPages: false,
force: false,
})

expect(jest.mocked(prompts)).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Create User model?'),
}),
)

const schema = fs.readFileSync(dbSchemaPath, 'utf-8')
expect(schema).not.toMatch(/^model User {$/m)
expect(jest.mocked(console).log).toHaveBeenCalledWith(
expect.stringContaining('Done! But you have a little more work to do:'),
)
expect(jest.mocked(console).log).toHaveBeenCalledWith(
expect.stringContaining('resetTokenExpiresAt DateTime? // <─'),
)
})
})
17 changes: 15 additions & 2 deletions packages/auth-providers/dbAuth/setup/src/setupHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
extraTask,
createUserModelTask,
} from './setupData'
import { generateAuthPagesTask, hasAuthPages, hasModel } from './shared'
import {
generateAuthPagesTask,
getModelNames,
hasAuthPages,
hasModel,
} from './shared'
import {
notes as webAuthnNotes,
noteGenerate as webAuthnNoteGenerate,
Expand Down Expand Up @@ -72,7 +77,7 @@ export async function handler({
}
}

standardAuthHandler({
await standardAuthHandler({
basedir: __dirname,
forceArg,
provider: 'dbAuth',
Expand Down Expand Up @@ -125,6 +130,14 @@ async function shouldIncludeWebAuthn(webauthn: boolean | null) {
async function shouldCreateUserModel(createUserModel: boolean | null) {
const hasUserModel = await hasModel('User')

const modelNames = await getModelNames()
const isNewProject =
modelNames.length === 1 && modelNames[0] === 'UserExample'

if (isNewProject) {
return true
}

if (createUserModel === null && !hasUserModel) {
const createModelResponse = await prompts({
type: 'confirm',
Expand Down
6 changes: 6 additions & 0 deletions packages/auth-providers/dbAuth/setup/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export async function hasModel(name: string) {
return false
}

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

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

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

Expand Down
Loading

0 comments on commit 88190cf

Please sign in to comment.