Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

bison-app-release-test3

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bison-app-release-test3

Creates a production-ready full-stack Jamstack app

  • 1.12.0-canary.8
  • unpublished
  • latest
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

<%= name %>

CI STATUS

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

  • Run 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.
    • For more information about code generation, view the FAQ.
  • Run yarn dev to start your development 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.

  • Define an Organization table in 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?
}
  • Generate a migration with yarn g:migration.

You should see a new folder in prisma/migrations.

  • Migrate the database with yarn db:migrate

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.

  • Create a new GraphQL module using yarn g:graphql organization
  • Edit the new module to reflect what you want to expose via the API. In the following Mutation example, we alias the Mutation name, require a user to be logged in, and force the new Organization to be owned by the logged in user. All in about 10 lines of code!

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';

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

Understanding the GraphQL API and TypeScript types

  • Open 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!
}
  • Open up types.ts to see the generated TypeScript types that correspond with the GraphQL changes.

API Request Tests

Let's confirm the API changes using a request test. To do this:

  • Generate a new factory: yarn g:test:factory organization
  • Add a default value for organization name in the 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,
    };
  },
// ...
  • Generate a new API request test: yarn g:test:request createOrganization
  • Update the API request test to call the new mutation and ensure that we get an error if not logged in. If you are curious what the 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 '../../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",
        ]
      `);
  });
});
  • Add a new test to confirm that the organization user is set to the current user
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.

  • Create a new page to create organizations: yarn g:page organizations/new
  • Create an OrganizationForm component: yarn g:component OrganizationForm
  • Add a simple form with a name input. See the React Hook Form docs for detailed information.
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>
  );
}
  • Update the form to use Chakra components
  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>
  );
}
  • Add a GraphQL mutation to create an organization (use the same code from the API request test to keep it easy!)
  • Make sure you import 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
      }
    }
  }
`;
  • Save the file. You should see GraphQL Codegen pickup on the changes.
  • Open 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
  >
) {
  • Use the newly generated hook to save the results of the form:
export function OrganizationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const [createOrganization, { data, loading, error }] = useCreateOrganizationMutation();

  async function onSubmit(data) {
    createOrganization(data);
  }
}
  • Attach the mutations loading state to the button loading state
<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

  • Generate a new page: yarn g:page "organizations/[:id]". This uses the dynamic page capability of Next.js.
  • Add a new "cell" to fetch data. While not required, it keeps things clean. yarn g:cell Organization
  • Add a query to the cell that fetches organization data
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 />;
};
  • Verify we get an error about querying for organizations in the dev server console.
[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.

  • Open api.graphql to see the parameters we can pass to the organization query.
  • Copy the where parameter and use it in our cell.
type Query {
  '''
  organization(where: OrganizationWhereUniqueInput!): Organization
  '''
}
  • Update the organization query to take a parameter and use the where query
export const QUERY = gql`
  query organization($where: OrganizationWhereUniqueInput!) {
    organization(where: $where) {
      name
    }
  }
`;
  • Use the newly generated hook from types.ts to fetch data in the cell.
  • Add a prop to the cell for organizationId and pass the value to the query.
  • Udate the Success component to take the proper return type for the query
  • Only render the Success 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 />;
};
  • Add the Cell to the organization page:
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:

  • View the login and logout e2e tests

<% if (host.name === 'heroku' ) { -%>

Deploy

Heroku Setup

https://heroku.com

Keywords

FAQs

Package last updated on 14 Feb 2022

Did you know?

Socket

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc