diff --git a/packages/create-bison-app/template/prisma/_.env.ejs b/packages/create-bison-app/template/_.env.ejs similarity index 100% rename from packages/create-bison-app/template/prisma/_.env.ejs rename to packages/create-bison-app/template/_.env.ejs diff --git a/packages/create-bison-app/template/_templates/graphql/new/graphql.ejs b/packages/create-bison-app/template/_templates/graphql/new/graphql.ejs index 5bb213e6..1e1ffe0d 100644 --- a/packages/create-bison-app/template/_templates/graphql/new/graphql.ejs +++ b/packages/create-bison-app/template/_templates/graphql/new/graphql.ejs @@ -3,7 +3,8 @@ to: graphql/modules/<%= name %>.ts --- <% camelized = h.inflection.camelize(name) -%> <% plural = h.inflection.pluralize(camelized) -%> -import { objectType, extendType } from 'nexus'; +import { objectType, extendType, inputObjectType, stringArg, arg, nonNull, enumType } from 'nexus'; +import { Role } from '@prisma/client'; import { UserInputError, /*ForbiddenError*/ } from 'apollo-server-micro'; // import { isAdmin } from '../services/permissions'; @@ -13,47 +14,35 @@ export const <%= camelized %> = objectType({ name: '<%= camelized %>', description: 'A <%= camelized %>', definition(t) { - t.model.id(); - t.model.createdAt(); - t.model.updatedAt(); + t.nonNull.id('id'); + t.nonNull.date('createdAt'); + t.nonNull.date('updatedAt'); }, }); -/* -// Enums -export const CallPreference = enumType({ - name: 'CallPreference', - members: ['WEEKDAY', 'WEEKEND', 'WEEKNIGHT'], -}); - // Queries export const <%= camelized %>Queries = extendType({ type: 'Query', definition: (t) => { - // List <%= plural %> Query (admin only) - t.crud.<%= plural.toLowerCase() %>({ - filtering: true, - ordering: true, - // use resolve for permission checks or to remove fields - resolve: async (root, args, ctx, info, originalResolve) => { - if (!isAdmin(ctx.user)) throw new ForbiddenError('Unauthorized'); + // List <%= plural %> Query + t.list.field('<%= plural.toLowerCase() %>', { + type: '<%= camelized %>', + authorize: () => false, + args: nonNull(arg({ type: 'SomethingQueryInput' })), + description: 'Returns available <%= plural.toLowerCase() %>', + }) - return await originalResolve(root, args, ctx, info); + // single query + t.field('something', { + type: '<%= camelized %>', + description: 'Returns a specific <%= camelized %>', + // update auth rules + authorize: () => false, + args: nonNull(arg({ type: 'SomethingQueryInput' })), + resolve: (_root, args, ctx) => { + // TODO }, }); - - // Custom Query - t.field('me', { - type: 'User', - description: 'Returns the currently logged in user', - nullable: true, - resolve: (_root, _args, ctx) => ctx.user, - }); - - t.list.field('availabilityForUser', { - type: 'Event', - description: 'Returns available time slots to schedule calls with an expert', - }) }, }); @@ -61,6 +50,60 @@ export const <%= camelized %>Queries = extendType({ export const <%= camelized %>Mutations = extendType({ type: 'Mutation', definition: (t) => { + t.field('somethingMutation', { + type: 'String', + description: 'Does something', + args: { + data: nonNull(arg({ type: 'SomethingMutationInput' })), + }, + authorize: () => false, + resolve: async (_root, args, ctx) => { + console.log(args.data.hello) + return args.data.hello + } + }); }, }); -*/ \ No newline at end of file + +// Inputs +export const SomethingMutationInput = inputObjectType({ + name: 'SomethingMutationInput', + description: 'Input used to do something', + definition: (t) => { + t.nonNull.string('hello'); + }, +}) + +export const SomethingQueryInput = inputObjectType({ + name: 'SomethingQueryInput', + description: 'Input used to do something', + definition: (t) => { + t.nonNull.string('hello'); + }, +}); + +export const <%= camelized %>OrderByInput = inputObjectType({ + name: '<%= camelized %>OrderByInput', + description: 'Order <%= camelized.toLowerCase() %> by a specific field', + definition(t) { + t.field('hello', { type: 'SortOrder' }); + }, +}); + +export const UserWhereUniqueInput = inputObjectType({ + name: 'UserWhereUniqueInput', + description: 'Input to find users based on unique fields', + definition(t) { + t.id('id'); + t.email('email'); + }, +}); + +export const UserWhereInput = inputObjectType({ + name: 'UserWhereInput', + description: 'Input to find users based other fields', + definition(t) { + t.int('id'); + t.field('email', { type: 'StringFilter' }); + }, +}); \ No newline at end of file diff --git a/packages/create-bison-app/template/graphql/modules/index.ts b/packages/create-bison-app/template/graphql/modules/index.ts index 1d151a80..899cb199 100644 --- a/packages/create-bison-app/template/graphql/modules/index.ts +++ b/packages/create-bison-app/template/graphql/modules/index.ts @@ -1,3 +1,4 @@ export * from './scalars'; export * from './user'; export * from './profile'; +export * from './shared' diff --git a/packages/create-bison-app/template/graphql/modules/shared.ts b/packages/create-bison-app/template/graphql/modules/shared.ts new file mode 100644 index 00000000..0e274cb4 --- /dev/null +++ b/packages/create-bison-app/template/graphql/modules/shared.ts @@ -0,0 +1,25 @@ +import { enumType, inputObjectType } from 'nexus'; + +// the following items are migrated from the prisma plugin +export const SortOrder = enumType({ + name: 'SortOrder', + description: 'Sort direction for filtering queries (ascending or descending)', + members: ['asc', 'desc'], +}); + +export const StringFilter = inputObjectType({ + name: 'StringFilter', + description: 'A way to filter string fields. Meant to pass to prisma where clause', + definition(t) { + t.string('contains'); + t.string('endsWith'); + t.string('equals'); + t.string('gt'); + t.string('gte'); + t.list.nonNull.string('in'); + t.string('lt'); + t.string('lte'); + t.list.nonNull.string('notIn'); + t.string('startsWith'); + }, +}); diff --git a/packages/create-bison-app/template/graphql/modules/user.ts.ejs b/packages/create-bison-app/template/graphql/modules/user.ts.ejs index 2178cfec..90481447 100644 --- a/packages/create-bison-app/template/graphql/modules/user.ts.ejs +++ b/packages/create-bison-app/template/graphql/modules/user.ts.ejs @@ -1,11 +1,6 @@ import { objectType, extendType, inputObjectType, stringArg, arg } from 'nexus'; import { Role } from '@prisma/client'; -<% if (host.name === 'vercel') { -%> import { UserInputError, ForbiddenError } from 'apollo-server-micro'; -<% } -%> -<% if (host.name === 'heroku') { -%> -import { UserInputError, ForbiddenError } from 'apollo-server-express'; -<% } -%> import { hashPassword, appJwtForUser, comparePasswords } from '../../services/auth'; import { isAdmin, canAccess } from '../../services/permissions'; @@ -15,16 +10,13 @@ export const User = objectType({ name: 'User', description: 'A User', definition(t) { - t.model.id(); - t.model.email(); - t.model.roles(); - t.model.createdAt(); - t.model.updatedAt(); - t.model.profile(); + t.nonNull.id('id'); + t.nonNull.date('createdAt'); + t.nonNull.date('updatedAt'); + t.nonNull.list.nonNull.field('roles', { type: 'Role' }); // Show email as null for unauthorized users t.string('email', { - nullable: true, resolve: (profile, _args, ctx) => (canAccess(profile, ctx) ? profile.email : null), }); }, @@ -50,23 +42,8 @@ export const UserQueries = extendType({ t.field('me', { type: 'User', description: 'Returns the currently logged in user', - nullable: true, resolve: (_root, _args, ctx) => ctx.user, }); - - // List Users Query (admin only) - t.crud.users({ - filtering: true, - ordering: true, - resolve: async (root, args, ctx, info, originalResolve) => { - if (!isAdmin(ctx.user)) throw new ForbiddenError('Unauthorized'); - - return await originalResolve(root, args, ctx, info); - }, - }); - - // User Query - t.crud.user(); }, }); @@ -79,8 +56,8 @@ export const Mutations = extendType({ type: 'AuthPayload', description: 'Login to an existing account', args: { - email: stringArg({ required: true }), - password: stringArg({ required: true }), + email: nonNull(stringArg()), + password: nonNull(stringArg()), }, resolve: async (_root, args, ctx) => { const { email, password } = args; @@ -109,41 +86,14 @@ export const Mutations = extendType({ }, }); - // User Create (admin only) - t.crud.createOneUser({ - alias: 'createUser', - resolve: async (root, args, ctx, info, originalResolve) => { - if (!isAdmin(ctx.user)) { - throw new ForbiddenError('Not authorized'); - } - - const user = await ctx.db.user.findUnique({ where: { email: args.data.email } }); - - if (user) { - throw new UserInputError('Email already exists.', { - invalidArgs: { email: 'already exists' }, - }); - } - - // force role to user and hash the password - const updatedArgs = { - data: { - ...args.data, - password: hashPassword(args.data.password), - }, - }; - - return originalResolve(root, updatedArgs, ctx, info); - }, - }); - t.field('signup', { type: 'AuthPayload', description: 'Signup for an account', args: { - data: arg({ type: 'SignupInput', required: true }), + data: nonNull(arg({ type: 'SignupInput' })), }, resolve: async (_root, args, ctx) => { + let data; const existingUser = await ctx.db.user.findUnique({ where: { email: args.data.email } }); if (existingUser) { @@ -152,12 +102,20 @@ export const Mutations = extendType({ }); } + try { + data = await validateSignupParams(args.data); + } catch (e) { + throw new UserInputError('Invalid Data', { + invalidArgs: formatYupErrorsForApollo(e), + }); + } + // force role to user and hash the password const updatedArgs = { data: { - ...args.data, + ...data, roles: { set: [Role.USER] }, - password: hashPassword(args.data.password), + password: hashPassword(data.password), }, }; @@ -173,11 +131,46 @@ export const Mutations = extendType({ }, }); +// Inputs export const SignupInput = inputObjectType({ name: 'SignupInput', + description: 'Input required for a user to signup', definition: (t) => { - t.string('email', { required: true }); - t.string('password', { required: true }); - t.field('profile', { type: 'ProfileCreateNestedOneWithoutUserInput' }); + t.nonNull.string('email'); + t.nonNull.string('password'); }, }); + +// Manually added types that were previously in the prisma plugin +export const UserRole = enumType({ + name: 'Role', + members: Object.values(Role), +}); + +export const UserOrderByInput = inputObjectType({ + name: 'UserOrderByInput', + description: 'Order users by a specific field', + definition(t) { + t.field('email', { type: 'SortOrder' }); + t.field('createdAt', { type: 'SortOrder' }); + t.field('updatedAt', { type: 'SortOrder' }); + }, +}); + +export const UserWhereUniqueInput = inputObjectType({ + name: 'UserWhereUniqueInput', + description: 'Input to find users based on unique fields', + definition(t) { + t.id('id'); + t.string('email'); + }, +}); + +export const UserWhereInput = inputObjectType({ + name: 'UserWhereInput', + description: 'Input to find users based other fields', + definition(t) { + t.int('id'); + t.field('email', { type: 'StringFilter' }); + }, +}); \ No newline at end of file diff --git a/packages/create-bison-app/template/package.json.ejs b/packages/create-bison-app/template/package.json.ejs index 96af5caa..374f341f 100644 --- a/packages/create-bison-app/template/package.json.ejs +++ b/packages/create-bison-app/template/package.json.ejs @@ -12,11 +12,12 @@ "build:nexus": "NODE_ENV=development ts-node -P tsconfig.cjs.json --transpile-only graphql/schema.ts", "cypress:open": "cypress open", "cypress:run": "cypress run", - "db:migrate": "yarn -s prisma migrate dev && yarn build:prisma", - "db:deploy": "yarn -s primsa deploy && yarn build:prisma", - "db:reset": "yarn prisma migrate reset && yarn build:prisma", - "db:setup": "yarn db:migrate && yarn prisma generate", - "db:drop": "DOTENV_CONFIG_PATH=./prisma/.env ts-node -r dotenv/config ./scripts/dropDatabase", + "db:migrate": "prisma migrate dev", + "db:migrate:prod": "prisma migrate deploy", + "db:deploy": "prisma deploy", + "db:reset": "prisma migrate reset", + "db:seed": "prisma db seed --preview-feature", + "db:setup": "yarn db:reset" "dev": "concurrently -n \"WATCHERS,NEXT\" -c \"black.bgYellow.dim,black.bgCyan.dim\" \"yarn watch:all\" \"next dev\"", "dev:typecheck": "tsc --noEmit", "g:cell": "hygen cell new --name", @@ -45,7 +46,7 @@ "@apollo/client": "^3.0.2", "@chakra-ui/react": "1.3.4", "@chakra-ui/theme": "1.7.1", - "@prisma/client": "^2.19.0", + "@prisma/client": "^2.22.1", <% if (host.name === 'vercel') { -%> "apollo-server-micro": "^2.18.1", <% } -%> @@ -63,7 +64,6 @@ "jsonwebtoken": "^8.5.1", "next": "9.5.3", "nexus": "^1.0.0", - "nexus-plugin-prisma": "^0.33.0", "react": "16.13.1", "react-dom": "16.13.1", "react-hook-form": "^6.1.0", @@ -104,7 +104,7 @@ "nanoid": "^3.1.10", "pg": "^8.3.0", "prettier": "^2.0.5", - "prisma": "^2.19.0", + "prisma": "^2.22.1", "start-server-and-test": "^1.11.2", "supertest": "^4.0.2", "ts-jest": "^26.1.3", diff --git a/packages/create-bison-app/template/prisma/seed.ts b/packages/create-bison-app/template/prisma/seed.ts new file mode 100644 index 00000000..7e3c3c35 --- /dev/null +++ b/packages/create-bison-app/template/prisma/seed.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +async function main() { + // TODO: seeds + console.log('no seeds yet!'); +} + +main() + .catch((e) => console.error(e)) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/create-bison-app/template/prisma/seeds.js b/packages/create-bison-app/template/prisma/seeds.js deleted file mode 100644 index d35e2fca..00000000 --- a/packages/create-bison-app/template/prisma/seeds.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-console */ -const { PrismaClient } = require('@prisma/client'); -const dotenv = require('dotenv'); - -dotenv.config(); -const db = new PrismaClient(); - -async function main() { - // Seed data is database data that needs to exist for your app to run. - // Ideally this file should be idempotent: running it multiple times - // will result in the same database state (usually by checking for the - // existence of a record before trying to create it). For example: - // - // const existing = await db.user.findMany({ where: { email: 'admin@email.com' }}) - // if (!existing.length) { - // await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }}) - // } - - console.info('No data to seed. See prisma/seeds.js for info.'); -} - -main() - .catch((e) => console.error(e)) - .finally(async () => { - await db.$disconnect(); - }); diff --git a/packages/create-bison-app/test/tasks/copyFiles.test.js b/packages/create-bison-app/test/tasks/copyFiles.test.js index fe8fb72d..8f487e37 100644 --- a/packages/create-bison-app/test/tasks/copyFiles.test.js +++ b/packages/create-bison-app/test/tasks/copyFiles.test.js @@ -141,8 +141,8 @@ describe("copyFiles", () => { }); }); - it("copies prisma/env with the correct contents", async () => { - const target = path.join(targetFolder, "prisma", ".env"); + it("copies env with the correct contents", async () => { + const target = path.join(targetFolder, ".env"); const file = await fs.promises.readFile(target); const fileString = file.toString(); const { user, password, host, port, name } = variables.db.dev;