<%= name %>
Getting Started Tutorial
This checklist and mini-tutorial will make sure you make the most of your shiny new Bison app.
Migrate your database, generate typings, and start the dev server
Complete a Bison workflow
While not a requirement, Bison works best when you start development with the database and API layer. We will illustrate how to use this by adding the concept of an organization to our app. The workflow below assumes you already have yarn dev
running.
The Database
Bison uses Prisma for database operations. We've added a few conveniences around the default Prisma setup, but if you're familiar with Prisma, you're familiar with databases in Bison.
We suggest copying the id
, createdAt
and updatedAt
fields from the User
model.
model Organization {
id String @id @default(cuid())
name String
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
If you use VSCode and have the Prisma extension installed, saving the file should automatically add the inverse relationship to the User
model!
model User {
id String @id @default(cuid())
email String @unique
password String
roles Role[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization? @relation(fields: [organizationId], references: [id])
organizationId String?
}
You should see a new folder in prisma/migrations
.
For more on Prisma, view the docs.
The GraphQL API
With the database changes complete, we need to decide what types, queries, and mutations to expose in our GraphQL API.
Bison uses Nexus Schema to create the GraphQL API. Nexus provides a strongly-typed, concise way of defining GraphQL types and operations.
Because Nexus is strongly typed, all of the t.
operations should autocomplete in your editor.
File: ./graphql/modules/organization.ts
import { objectType, inputObjectType, queryField, mutationField, arg, list, nonNull } from 'nexus';
import { isAdmin } from '../../services/permissions';
export const Organization = objectType({
name: 'Organization',
description: 'A Organization',
definition(t) {
t.nonNull.id('id');
t.nonNull.date('createdAt');
t.nonNull.date('updatedAt');
t.nonNull.string('name');
t.nonNull.list.nonNull.field('users', {
type: 'User',
resolve: async (parent, _, context) => {
return context.prisma.organization
.findUnique({
where: { id: parent.id },
})
.users();
},
});
},
});
export const findOrganizationsQuery = queryField('organizations', {
type: list('Organization'),
authorize: (_root, _args, ctx) => !!ctx.user,
args: {
where: arg({ type: 'OrganizationWhereInput' }),
orderBy: arg({ type: 'OrganizationOrderByInput', list: true }),
},
description: 'Returns found organizations',
resolve: async (_root, args, ctx) => {
const { where = {}, orderBy = [] } = args;
return await ctx.db.organization.findMany({ where, orderBy });
}
});
export const findUniqueOrganizationQuery = queryField('organization', {
type: 'Organization',
description: 'Returns a specific Organization',
authorize: (_root, _args, ctx) => !!ctx.user,
args: {
where: nonNull(arg({ type: 'OrganizationWhereUniqueInput' }))
},
resolve: (_root, args, ctx) => {
const { where } = args;
return ctx.prisma.organization.findUnique({ where })
},
});
export const createOrganizationMutation = mutationField('createOrganization', {
type: 'Organization',
description: 'Creates a Organization',
authorize: (_root, _args, ctx) => isAdmin(ctx.user),
args: {
data: nonNull(arg({ type: 'CreateOrganizationInput' })),
},
resolve: async (_root, args, ctx) => {
return await ctx.db.organization.create(args);
}
});
export const updateOrganizationMutation = mutationField('updateOrganization', {
type: 'Organization',
description: 'Updates a Organization',
authorize: (_root, _args, ctx) => isAdmin(ctx.user),
args: {
where: nonNull(arg({ type: 'OrganizationWhereUniqueInput'})),
data: nonNull(arg({ type: 'UpdateOrganizationInput' })),
},
resolve: async (_root, args, ctx) => {
const { where, data } = args;
return await ctx.db.organization.update({ where, data });
}
});
export const CreateOrganizationInput = inputObjectType({
name: 'CreateOrganizationInput',
description: 'Input used to create a organization',
definition: (t) => {
t.nonNull.string('name');
},
});
export const UpdateOrganizationInput = inputObjectType({
name: 'UpdateOrganizationInput',
description: 'Input used to update a organization',
definition: (t) => {
t.nonNull.string('name');
},
});
export const OrganizationOrderByInput = inputObjectType({
name: 'OrganizationOrderByInput',
description: 'Order organization by a specific field',
definition(t) {
t.field('name', { type: 'SortOrder' });
},
});
export const OrganizationWhereUniqueInput = inputObjectType({
name: 'OrganizationWhereUniqueInput',
description: 'Input to find organizations based on unique fields',
definition(t) {
t.id('id');
},
});
export const OrganizationWhereInput = inputObjectType({
name: 'OrganizationWhereInput',
description: 'Input to find organizations based on other fields',
definition(t) {
t.field('name', { type: 'StringFilter' });
},
});
Understanding the GraphQL API and TypeScript types
"""
A Organization
"""
type Organization {
createdAt: DateTime!
id: ID!
name: String!
updatedAt: DateTime!
}
"""
Order organization by a specific field
"""
input OrganizationOrderByInput {
name: SortOrder
}
"""
Input to find organizations based on other fields
"""
input OrganizationWhereInput {
name: StringFilter
}
"""
Input to find organizations based on unique fields
"""
input OrganizationWhereUniqueInput {
id: ID
}
"""
Input used to create a organization
"""
input CreateOrganizationInput {
name: String!
}
"""
Input used to update a organization
"""
input UpdateOrganizationInput {
name: String!
}
API Request Tests
Let's confirm the API changes using a request test. To do this:
export const OrganizationFactory = {
build: (attrs: Partial<Prisma.OrganizationCreateInput> = {}) => {
return {
name: chance.company(),
...attrs,
};
},
Here we use inline snapshots to confirm the error message content, but you can also manually assert the content.
import { graphQLRequest, graphQLRequestAsUser, resetDB, disconnect } from '../../helpers';
import { OrganizationFactory } from '../factories/organization';
beforeEach(async () => resetDB());
afterAll(async () => disconnect());
describe('createOrganization mutation', () => {
it('returns an error if not logged in', async () => {
const query = `
mutation createOrganization($data: OrganizationCreateInput!) {
createOrganization(data: $data) {
id
name
users {
email
}
}
}
`;
const variables = { data: { name: 'Cool Company' } };
const response = await graphQLRequest({ query, variables });
const errorMessages = response.body.errors.map((e) => e.message);
expect(errorMessages).toMatchInlineSnapshot(`
Array [
"Not authorized",
]
`);
});
});
it('sets the user to the logged in user', async () => {
const query = `
mutation createOrganization($data: OrganizationCreateInput!) {
createOrganization(data: $data) {
id
name
users {
id
}
}
}
`;
const user = await UserFactory.create();
const variables = { data: { name: 'Cool Company', users: { connect: [{ id: 'notmyid' }] } } };
const response = await graphQLRequestAsUser(user, { query, variables });
const organization = response.body.data.createOrganization;
const [organizationUser] = organization.users;
expect(organizationUser.id).toEqual(user.id);
});
Add a Frontend page and form that creates an organization
Now that we have the API finished, we can move to the frontend changes.
import React from 'react';
import { useForm } from 'react-hook-form';
export function OrganizationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
async function onSubmit(data) {
console.log(data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: true })} />
{errors.name && <span>This field is required</span>}
<input type="submit" />
</form>
);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl id="name">
<FormLabel htmlFor="name">Name</FormLabel>
<Input type="text" {...register('name', { required: true })} isInvalid={errors.name} />
<ErrorText>{errors.name && errors.name.message}</ErrorText>
</FormControl>
<Button type="submit" marginTop={8} width="full">
Submit
</Button>
</form>
);
}
export const CREATE_ORGANIZATION_MUTATION = gql`
mutation createOrganization($data: OrganizationCreateInput!) {
createOrganization(data: $data) {
id
name
users {
id
}
}
}
`;
export function useCreateOrganizationMutation(
baseOptions?: Apollo.MutationHookOptions<
CreateOrganizationMutation,
CreateOrganizationMutationVariables
>
) {
export function OrganizationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const [createOrganization, { data, loading, error }] = useCreateOrganizationMutation();
async function onSubmit(data) {
createOrganization(data);
}
}
<Button type="submit" marginTop={8} width="full" isLoading={loading}>
Submit
</Button>
You should now have a fully working form that creates a new database entry on submit!
Adding a new page that shows the organization
import React from 'react';
import gql from 'graphql-tag';
import { Spinner, Text } from '@chakra-ui/react';
export const QUERY = gql`
query organization {
organization {
name
}
}
`;
export const Loading = () => <Spinner />;
export const Error = () => <Text>Error. See dev tools.</Text>;
export const Empty = () => <Text>No data.</Text>;
export const Success = () => {
return <Text>Awesome!</Text>;
};
export const OrganizationCell = () => {
return <Empty />;
};
[WATCHERS] [GQLCODEGEN] GraphQLDocumentError: Field "organization" argument "where" of type "OrganizationWhereUniqueInput!" is required, but it was not provided.
We forgot to add a where clause to our organization query that's in the cell. Let's do that now.
type Query {
'''
organization(where: OrganizationWhereUniqueInput!): Organization
'''
}
export const QUERY = gql`
query organization($where: OrganizationWhereUniqueInput!) {
organization(where: $where) {
name
}
}
`;
import React from 'react';
import gql from 'graphql-tag';
import { Spinner, Text } from '@chakra-ui/react';
import { OrganizationQuery, useOrganizationQuery } from '../types';
export const QUERY = gql`
query organization($where: OrganizationWhereUniqueInput!) {
organization(where: $where) {
name
}
}
`;
export const Loading = () => <Spinner />;
export const Error = () => <Text>Error. See dev tools.</Text>;
export const Empty = () => <Text>No data.</Text>;
export const Success = ({ organization }: OrganizationQuery) => {
return <Text>Awesome! {organization.name}</Text>;
};
export const OrganizationCell = ({ organizationId }) => {
const { data, loading, error } = useOrganizationQuery({
variables: {
where: { id: organizationId },
},
});
if (loading) return <Loading />;
if (error) return <Error />;
if (data.organization) return <Success {...data} />;
return <Empty />;
};
import React from 'react';
import Head from 'next/head';
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { OrganizationCell } from '../../cells/Organization';
function OrganizationPage() {
const router = useRouter();
const { id } = router.query;
return (
<>
<Head>
<title>An organization</title>
</Head>
<Flex direction={{ base: 'column', lg: 'row' }}>
<OrganizationCell organizationId={id} />
</Flex>
</>
);
}
export default OrganizationPage;
Congrats!
Outside of e2e tests, you've used just about every feature in Bison. But don't worry. We've got your back there too.
Bonus:
<% if (host.name === 'heroku' ) { -%>
Deploy
Heroku Setup
https://heroku.com