Skip to content

Commit

Permalink
feat: Enable TS strict (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
DominicSherman committed Jan 14, 2022
1 parent d7de8ba commit abc7075
Show file tree
Hide file tree
Showing 18 changed files with 131 additions and 35 deletions.
7 changes: 6 additions & 1 deletion packages/create-bison-app/template/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ export function LoginForm() {
try {
setIsLoading(true);
const { data } = await login({ variables: formData });

if (!data?.login?.token) {
throw new Error('Login failed.');
}

await loginUser(data.login.token);
await router.replace('/');
} catch (e) {
} catch (e: any) {
setErrorsFromGraphQLErrors(setError, e.graphQLErrors);
setIsLoading(false);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/create-bison-app/template/components/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ export function SignupForm() {

const { data } = await signup({ variables });

if (!data?.signup?.token) {
throw new Error('Signup failed.');
}

await login(data.signup.token);

router.replace('/');
} catch (e) {
} catch (e: any) {
setErrorsFromGraphQLErrors(setError, e.graphQLErrors);
} finally {
setIsLoading(false);
Expand Down
14 changes: 9 additions & 5 deletions packages/create-bison-app/template/context/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ const now = new Date();
const timeValidInMs = 365 * 24 * 60 * 60 * 1000;
const COOKIE_EXPIRE_DATE = new Date(now.getTime() + timeValidInMs);

const AuthContext = createContext<AuthContextObject>({});
const AuthContext = createContext<AuthContextObject>({
login: () => ({}),
logout: () => ({}),
});

AuthContext.displayName = 'AuthContext';

export const ME_QUERY = gql`
Expand Down Expand Up @@ -74,12 +78,12 @@ const useAuth = () => useContext(AuthContext);
export { AuthProvider, useAuth };

interface Props {
loggedInUser?: Partial<User>;
loggedInUser?: Partial<User> | null;
children: ReactNode;
}

export interface AuthContextObject {
user?: Partial<User>;
login?: (token?: string) => any;
logout?: () => any;
user?: Partial<User> | null;
login: (token: string) => any;
logout: () => any;
}
2 changes: 1 addition & 1 deletion packages/create-bison-app/template/graphql/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ type ApolloApiContext = ApolloContext<{ req: IncomingMessage }>;
export type Context = {
db: PrismaClient;
prisma: PrismaClient;
user: User;
user: User | null;
};
9 changes: 9 additions & 0 deletions packages/create-bison-app/template/graphql/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApolloError } from 'apollo-server-errors';

export class NotFoundError extends ApolloError {
constructor(message: string) {
super(message, 'NOT_FOUND');

Object.defineProperty(this, 'name', { value: 'NotFoundError' });
}
}
46 changes: 46 additions & 0 deletions packages/create-bison-app/template/graphql/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */

/** Removes nullability from a type
* @example foo: string | null | undefined => foo: string | undefined
*/
export const prismaArg = <T>(field: T | undefined | null): T | undefined => {
if (field === undefined || field === null) {
return undefined;
}

return field;
};

/** Recursively removes nullability from nested object values */
type ObjectWithoutNulls<T> = {
[K in keyof T]: T[K] extends string | number | undefined | null
? Exclude<T[K], null>
: ObjectWithoutNulls<Exclude<T[K], null>>;
};

/** Removes nullability from the values of an object
* @example foo: {bar: string | null | undefined} => foo: {bar: string | undefined}
*/
export const prismaArgObject = <T extends Partial<Record<keyof T, T[keyof T]>> | undefined>(
field: T | null
): ObjectWithoutNulls<T> => {
const newObject: T | null = field;

if (!newObject || !field) {
// @ts-ignore
return undefined;
}

Object.entries(field).forEach(([key, value]) => {
if (typeof value === 'object') {
// @ts-ignore
newObject[key] = prismaArgObject(value);
} else {
// @ts-ignore
newObject[key] = prismaArg(value);
}
});

// @ts-ignore
return newObject;
};
14 changes: 11 additions & 3 deletions packages/create-bison-app/template/graphql/modules/profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { objectType, inputObjectType } from 'nexus';
import { inputObjectType, objectType } from 'nexus';

import { NotFoundError } from '../errors';

// Profile Type
export const Profile = objectType({
Expand All @@ -12,12 +14,18 @@ export const Profile = objectType({
t.nonNull.string('lastName');
t.nonNull.field('user', {
type: 'User',
resolve: (parent, _, context) => {
return context.prisma.profile
resolve: async (parent, _, context) => {
const user = await context.prisma.profile
.findUnique({
where: { id: parent.id },
})
.user();

if (!user) {
throw new NotFoundError('User not found');
}

return user;
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Role } from '@prisma/client';
import { UserInputError } from 'apollo-server-micro';

import { prismaArgObject } from '../helpers';
import { hashPassword, appJwtForUser, comparePasswords } from '../../services/auth';
import { canAccess, isAdmin } from '../../services/permissions';

Expand Down Expand Up @@ -76,7 +77,7 @@ export const findUniqueUserQuery = queryField('user', {
where: nonNull(arg({ type: 'UserWhereUniqueInput' })),
},
resolve: async (_root, args, ctx) => {
return await ctx.db.user.findUnique({ where: args.where });
return await ctx.db.user.findUnique({ where: prismaArgObject(args.where) });
},
});

Expand Down Expand Up @@ -170,7 +171,7 @@ export const createUserMutation = mutationField('createUser', {
// force role to user and hash the password
const updatedArgs = {
data: {
...data,
...prismaArgObject(data),
password: hashPassword(data.password),
},
};
Expand Down
4 changes: 2 additions & 2 deletions packages/create-bison-app/template/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { useAuth } from '../context/auth';
* Dynamically load layouts. This codesplits and prevents code from the logged in layout from being
* included in the bundle if we're rendering the logged out layout.
*/
const LoggedInLayout = dynamic(() =>
const LoggedInLayout = dynamic<{ children: React.ReactNode }>(() =>
import('../layouts/LoggedIn').then((mod) => mod.LoggedInLayout)
);

const LoggedOutLayout = dynamic(() =>
const LoggedOutLayout = dynamic<{ children: React.ReactNode }>(() =>
import('../layouts/LoggedOut').then((mod) => mod.LoggedOutLayout)
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { hashPassword } from '../../services/auth';
import { Role, UserCreateInput } from '../../types';
import { Role } from '../../types';

import { seedUsers } from '../seeds/users';
import { prisma } from '../../lib/prisma';
import { Prisma } from '@prisma/client';

// HR: Hey, we've had a few more employees join -- can you create an account for them?!

const INITIAL_PASSWORD = 'test1234';

const main = async () => {
const newEmployees: UserCreateInput[] = [
const newEmployees: Prisma.UserCreateInput[] = [
{
profile: {
create: {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-bison-app/template/prisma/scripts/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const run = () => {
});

child.on('exit', function (code) {
process.exit(code);
process.exit(code || 1);
});
};

Expand Down
10 changes: 5 additions & 5 deletions packages/create-bison-app/template/prisma/seeds/users/data.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { hashPassword } from '../../../services/auth';
import { Role, UserCreateInput } from '../../../types';
import { Prisma, Role } from '@prisma/client';

import { hashPassword } from '../../../services/auth';
// *********************************************
// ** DEVELOPMENT DATA SET
// *********************************************

const INITIAL_PASSWORD = 'test1234';

const initialDevUsers: UserCreateInput[] = [
const initialDevUsers: Prisma.UserCreateInput[] = [
{
email: 'barry.allen@speedforce.net',
password: hashPassword(INITIAL_PASSWORD),
Expand All @@ -27,7 +27,7 @@ const initialDevUsers: UserCreateInput[] = [

const INITIAL_PROD_PASSWORD = 'strong@password';

const initialProdUsers: UserCreateInput[] = [
const initialProdUsers: Prisma.UserCreateInput[] = [
{
email: 'apps@echobind.com',
password: hashPassword(INITIAL_PROD_PASSWORD),
Expand All @@ -47,5 +47,5 @@ const initialProdUsers: UserCreateInput[] = [

const appEnv = process.env.APP_ENV || 'development';

export const userSeedData: UserCreateInput[] =
export const userSeedData: Prisma.UserCreateInput[] =
appEnv === 'production' ? initialProdUsers : initialDevUsers;
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { User, UserCreateInput } from '../../../types';
import { Prisma } from '@prisma/client';

import { prisma } from '../../../lib/prisma';
import { User } from '../../../types';

type SeedUserResult = Pick<User, 'id' | 'email'>;

export const seedUsers = async (users: UserCreateInput[]): Promise<SeedUserResult[]> => {
export const seedUsers = async (users: Prisma.UserCreateInput[]): Promise<SeedUserResult[]> => {
const userPromiseArray = users.map(
async (user): Promise<SeedUserResult> =>
prisma.user.upsert({
Expand Down
14 changes: 12 additions & 2 deletions packages/create-bison-app/template/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ export function comparePasswords(password: string, hashedPassword: string): bool
return bcrypt.compareSync(password, hashedPassword);
}

const getAppSecret = (): string => {
const appSecret = process.env.APP_SECRET;

if (!appSecret) {
throw new Error('APP_SECRET is not set');
}

return appSecret;
};

/**
* Signs a JWT for the provided user
* @param user The user to return a JWT for
*/
export const appJwtForUser = (user: Partial<User>): string => {
return jwt.sign({ userId: user.id }, process.env.APP_SECRET);
return jwt.sign({ userId: user.id }, getAppSecret());
};

/**
Expand All @@ -40,7 +50,7 @@ export const verifyAuthHeader = (header?: string): JWT | undefined => {
const token = header.replace('Bearer ', '');

try {
return jwt.verify(token, process.env.APP_SECRET) as JWT;
return jwt.verify(token, getAppSecret()) as JWT;
} catch (e) {
return;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/create-bison-app/template/services/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import { Context } from '../graphql/context';
* Returns true if the user has a role of admin
* @param user The user to check the role for
*/
export const isAdmin = (user: Partial<User>): boolean => {
return user?.roles.includes(Role.ADMIN);
export const isAdmin = (user: Partial<User> | null): boolean => {
if (!user?.roles) {
return false;
}

return user.roles.includes(Role.ADMIN);
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/create-bison-app/template/tests/helpers/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const exec = util.promisify(childProcess.exec);
export const resetDB = async (): Promise<boolean> => {
if (process.env.NODE_ENV === 'production') return Promise.resolve(false);

const match = process.env.DATABASE_URL.match(/schema=(.*)(&.*)*$/);
const match = process.env.DATABASE_URL?.match(/schema=(.*)(&.*)*$/);
const schema = match ? match[1] : 'public';

// NOTE: the prisma client does not handle this query well, use pg instead
Expand Down
2 changes: 1 addition & 1 deletion packages/create-bison-app/template/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
Expand Down
9 changes: 5 additions & 4 deletions packages/create-bison-app/template/utils/setErrors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ErrorOption } from 'react-hook-form';
import { UseFormSetError } from 'react-hook-form';

/**
* Sets errors on the frontend from a GraphQL Response. Assumes react-hook-form.
*/
export function setErrorsFromGraphQLErrors(setError: SetErrorFn, errors: ErrorResponse[]) {
export function setErrorsFromGraphQLErrors(
setError: UseFormSetError<any>,
errors: ErrorResponse[]
) {
return (errors || []).forEach((e) => {
const errorObjects = e.extensions.invalidArgs || {};
Object.keys(errorObjects).forEach((key) => {
Expand All @@ -12,8 +15,6 @@ export function setErrorsFromGraphQLErrors(setError: SetErrorFn, errors: ErrorRe
});
}

type SetErrorFn = (e: string, obj: ErrorOption) => void;

interface ErrorResponse {
extensions: {
code: string;
Expand Down

0 comments on commit abc7075

Please sign in to comment.