Skip to content

Commit

Permalink
feat: migrate away from prisma-nexus-plugin.
Browse files Browse the repository at this point in the history
* updates generators
* removes nexus-prisma-plugin dependency
* updates package.json scripts
* adds graphql shared module
* updates user graphql module
* renames and updates seed file
  • Loading branch information
cball committed May 11, 2021
1 parent e015ba2 commit 36b57ed
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 132 deletions.
109 changes: 76 additions & 33 deletions packages/create-bison-app/template/_templates/graphql/new/graphql.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -13,54 +14,96 @@ 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',
})
},
});

// Mutations
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
}
});
},
});
*/

// 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' });
},
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './scalars';
export * from './user';
export * from './profile';
export * from './shared'
25 changes: 25 additions & 0 deletions packages/create-bison-app/template/graphql/modules/shared.ts
Original file line number Diff line number Diff line change
@@ -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');
},
});
119 changes: 56 additions & 63 deletions packages/create-bison-app/template/graphql/modules/user.ts.ejs
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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),
});
},
Expand All @@ -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();
},
});

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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),
},
};

Expand All @@ -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' });
},
});
Loading

0 comments on commit 36b57ed

Please sign in to comment.