
Security News
OpenGrep Restores Fingerprinting in JSON and SARIF Outputs
OpenGrep has restored fingerprint and metavariable support in JSON and SARIF outputs, making static analysis more effective for CI/CD security automation.
create-bison-app
Advanced tools
This checklist and mini-tutorial will make sure you make the most of your shiny new Bison app.
yarn setup:dev
to prep and migrate your local database, as well as generate the prisma client, nexus typings, and GraphQL typings. If this fails, make sure you have Postgres running and the generated DATABASE_URL
values are correct in your .env
files.
yarn dev
to start your development serverWhile 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.
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.
prisma/schema.prisma
.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?
}
yarn g:migration
.You should see a new folder in prisma/migrations
.
yarn db:migrate
For more on Prisma, view the docs.
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.
yarn g:graphql organization
Because Nexus is strongly typed, all of the t.
operations should autocomplete in your editor.
import { objectType, inputObjectType, queryField, mutationField, arg, list, nonNull } from 'nexus';
import { isAdmin } from '@/services/permissions';
// Organization Type
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();
},
});
},
});
// Queries
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 })
},
});
// Mutations
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 });
}
});
// Mutation Inputs
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');
},
});
// Query Inputs
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');
// add DB uniq fields here
// t.string('name');
},
});
export const OrganizationWhereInput = inputObjectType({
name: 'OrganizationWhereInput',
description: 'Input to find organizations based on other fields',
definition(t) {
t.field('name', { type: 'StringFilter' });
},
});
api.graphql
and look at our the new definitions that were generated for you:
"""
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!
}
types.ts
to see the generated TypeScript types that correspond with the GraphQL changes.Let's confirm the API changes using a request test. To do this:
yarn g:test:factory organization
build
function. You can use any of the methods from the chance
library.export const OrganizationFactory = {
build: (attrs: Partial<Prisma.OrganizationCreateInput> = {}) => {
return {
name: chance.company(), // <-- add this
...attrs,
};
},
// ...
yarn g:test:request createOrganization
Input
type should be, check api.graphql
.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 '@/tests/helpers';
import { OrganizationFactory } from '@/tests/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);
});
Now that we have the API finished, we can move to the frontend changes.
yarn g:page organizations/new
OrganizationForm
component: yarn g:component OrganizationForm
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>
);
}
gql
from @apollo/client
since we are working in the frontend.export const CREATE_ORGANIZATION_MUTATION = gql`
mutation createOrganization($data: OrganizationCreateInput!) {
createOrganization(data: $data) {
id
name
users {
id
}
}
}
`;
types.ts
. Codegen should have created a new hook called useCreateOrganizationMutation
, which we can use to get fully typed GraphQL operations!// types.ts - search for the following function:
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!
yarn g:page "organizations/[:id]"
. This uses the dynamic page capability of Next.js.yarn g:cell 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.
api.graphql
to see the parameters we can pass to the organization query.where
parameter and use it in our cell.type Query {
'''
organization(where: OrganizationWhereUniqueInput!): Organization
'''
}
export const QUERY = gql`
query organization($where: OrganizationWhereUniqueInput!) {
organization(where: $where) {
name
}
}
`;
types.ts
to fetch data in the cell.organizationId
and pass the value to the query.Success
component to take the proper return type for the querySuccess
component if data.organization
is present.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;
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' ) { -%>
1.12.0-canary.15 (2022-04-27)
Note: Version bump only for package root
FAQs
Creates a production-ready full-stack Jamstack app
The npm package create-bison-app receives a total of 146 weekly downloads. As such, create-bison-app popularity was classified as not popular.
We found that create-bison-app demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
OpenGrep has restored fingerprint and metavariable support in JSON and SARIF outputs, making static analysis more effective for CI/CD security automation.
Security News
Security experts warn that recent classification changes obscure the true scope of the NVD backlog as CVE volume hits all-time highs.
Security Fundamentals
Attackers use obfuscation to hide malware in open source packages. Learn how to spot these techniques across npm, PyPI, Maven, and more.