summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--finished-application/backend/src/resolvers/Mutation.js1
-rw-r--r--finished-application/frontend/components/Nav.js2
-rw-r--r--finished-application/frontend/components/Pagination.js27
-rw-r--r--finished-application/frontend/components/styles/PaginationStyles.js27
-rw-r--r--sick-fits/frontend/components/styles/PaginationStyles.js27
-rwxr-xr-xstepped-solutions/22/frontend/components/SingleItem.js69
-rwxr-xr-xstepped-solutions/22/frontend/pages/item.js9
-rwxr-xr-xstepped-solutions/24/backend/src/resolvers/Query.js14
-rwxr-xr-xstepped-solutions/24/backend/src/schema.graphql13
-rwxr-xr-xstepped-solutions/24/frontend/components/Items.js61
-rwxr-xr-xstepped-solutions/24/frontend/components/Pagination.js65
-rwxr-xr-xstepped-solutions/24/frontend/pages/index.js9
-rwxr-xr-xstepped-solutions/24/frontend/pages/items.js3
-rwxr-xr-xstepped-solutions/25/backend/datamodel.graphql30
-rwxr-xr-xstepped-solutions/25/backend/src/generated/prisma.graphql819
-rwxr-xr-xstepped-solutions/25/backend/src/index.js22
-rwxr-xr-xstepped-solutions/25/backend/src/resolvers/Mutation.js74
-rwxr-xr-xstepped-solutions/25/backend/src/schema.graphql14
-rwxr-xr-xstepped-solutions/26/frontend/components/Signup.js81
-rwxr-xr-xstepped-solutions/26/frontend/pages/signup.js18
-rwxr-xr-xstepped-solutions/27/backend/src/index.js33
-rwxr-xr-xstepped-solutions/27/backend/src/resolvers/Query.js21
-rwxr-xr-xstepped-solutions/27/backend/src/schema.graphql15
-rwxr-xr-xstepped-solutions/27/frontend/components/Nav.js32
-rwxr-xr-xstepped-solutions/27/frontend/components/User.js27
-rwxr-xr-xstepped-solutions/28/backend/src/resolvers/Mutation.js95
-rwxr-xr-xstepped-solutions/28/backend/src/schema.graphql17
-rwxr-xr-xstepped-solutions/28/frontend/components/Nav.js36
-rwxr-xr-xstepped-solutions/28/frontend/components/Signin.js76
-rwxr-xr-xstepped-solutions/28/frontend/components/Signup.js86
-rwxr-xr-xstepped-solutions/28/frontend/components/User.js27
-rwxr-xr-xstepped-solutions/28/frontend/pages/signup.js18
-rwxr-xr-xstepped-solutions/29/backend/src/resolvers/Mutation.js99
-rwxr-xr-xstepped-solutions/29/backend/src/schema.graphql22
-rwxr-xr-xstepped-solutions/29/frontend/components/Nav.js38
-rwxr-xr-xstepped-solutions/29/frontend/components/Signout.js19
-rwxr-xr-xstepped-solutions/29/frontend/components/styles/NavStyles.js66
-rwxr-xr-xstepped-solutions/30/src/resolvers/Mutation.js156
-rwxr-xr-xstepped-solutions/30/src/schema.graphql30
39 files changed, 2273 insertions, 25 deletions
diff --git a/finished-application/backend/src/resolvers/Mutation.js b/finished-application/backend/src/resolvers/Mutation.js
index 80ff3f2..2027494 100644
--- a/finished-application/backend/src/resolvers/Mutation.js
+++ b/finished-application/backend/src/resolvers/Mutation.js
@@ -140,6 +140,7 @@ const mutations = {
.FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to reset</a>`
),
});
+
return res.updateUser;
},
diff --git a/finished-application/frontend/components/Nav.js b/finished-application/frontend/components/Nav.js
index b483376..5f3495a 100644
--- a/finished-application/frontend/components/Nav.js
+++ b/finished-application/frontend/components/Nav.js
@@ -12,7 +12,7 @@ class Nav extends React.Component {
// below we set the fetchPolicy to network only so it forces re-fetch on the server
return (
<User>
- {({ data: { me }, error }) => (
+ {({ data: { me } }) => (
<NavStyles data-test="nav">
<Link href="/items">
<a>Shop</a>
diff --git a/finished-application/frontend/components/Pagination.js b/finished-application/frontend/components/Pagination.js
index b77f8f6..f684840 100644
--- a/finished-application/frontend/components/Pagination.js
+++ b/finished-application/frontend/components/Pagination.js
@@ -6,6 +6,9 @@ import PropTypes from 'prop-types';
import gql from 'graphql-tag';
import Head from 'next/head';
import { perPage } from '../config';
+import PaginationStyles from './styles/PaginationStyles';
+
+
const PAGINATION_QUERY = gql`
query itemsConnection($skip: Int = 0, $first: Int = 4) {
@@ -17,30 +20,6 @@ const PAGINATION_QUERY = gql`
}
`;
-const PaginationStyles = styled.div`
- text-align: center;
- display: inline-grid;
- grid-template-columns: repeat(4, auto);
- align-items: stretch;
- justify-content: center;
- align-content: center;
- margin: 2rem 0;
- border: 1px solid ${props => props.theme.lightgrey};
- border-radius: 10px;
- & > * {
- margin: 0;
- padding: 15px 30px;
- border-right: 1px solid ${props => props.theme.lightgrey};
- &:last-child {
- border-right: 0;
- }
- }
- a[aria-disabled='true'] {
- color: grey;
- pointer-events: none;
- }
-`;
-
const Pagination = props => (
<Query query={PAGINATION_QUERY}>
{({ data, loading, error }) => {
diff --git a/finished-application/frontend/components/styles/PaginationStyles.js b/finished-application/frontend/components/styles/PaginationStyles.js
new file mode 100644
index 0000000..30753af
--- /dev/null
+++ b/finished-application/frontend/components/styles/PaginationStyles.js
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+
+const PaginationStyles = styled.div`
+ text-align: center;
+ display: inline-grid;
+ grid-template-columns: repeat(4, auto);
+ align-items: stretch;
+ justify-content: center;
+ align-content: center;
+ margin: 2rem 0;
+ border: 1px solid ${props => props.theme.lightgrey};
+ border-radius: 10px;
+ & > * {
+ margin: 0;
+ padding: 15px 30px;
+ border-right: 1px solid ${props => props.theme.lightgrey};
+ &:last-child {
+ border-right: 0;
+ }
+ }
+ a[aria-disabled='true'] {
+ color: grey;
+ pointer-events: none;
+ }
+`;
+
+export default PaginationStyles;
diff --git a/sick-fits/frontend/components/styles/PaginationStyles.js b/sick-fits/frontend/components/styles/PaginationStyles.js
new file mode 100644
index 0000000..30753af
--- /dev/null
+++ b/sick-fits/frontend/components/styles/PaginationStyles.js
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+
+const PaginationStyles = styled.div`
+ text-align: center;
+ display: inline-grid;
+ grid-template-columns: repeat(4, auto);
+ align-items: stretch;
+ justify-content: center;
+ align-content: center;
+ margin: 2rem 0;
+ border: 1px solid ${props => props.theme.lightgrey};
+ border-radius: 10px;
+ & > * {
+ margin: 0;
+ padding: 15px 30px;
+ border-right: 1px solid ${props => props.theme.lightgrey};
+ &:last-child {
+ border-right: 0;
+ }
+ }
+ a[aria-disabled='true'] {
+ color: grey;
+ pointer-events: none;
+ }
+`;
+
+export default PaginationStyles;
diff --git a/stepped-solutions/22/frontend/components/SingleItem.js b/stepped-solutions/22/frontend/components/SingleItem.js
new file mode 100755
index 0000000..6b07cbc
--- /dev/null
+++ b/stepped-solutions/22/frontend/components/SingleItem.js
@@ -0,0 +1,69 @@
+import React, { Component } from 'react';
+import gql from 'graphql-tag';
+import { Query } from 'react-apollo';
+import Error from './ErrorMessage';
+import styled from 'styled-components';
+import Head from 'next/head';
+
+const SingleItemStyles = styled.div`
+ max-width: 1200px;
+ margin: 2rem auto;
+ box-shadow: ${props => props.theme.bs};
+ display: grid;
+ grid-auto-columns: 1fr;
+ grid-auto-flow: column;
+ min-height: 800px;
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ .details {
+ margin: 3rem;
+ font-size: 2rem;
+ }
+`;
+
+const SINGLE_ITEM_QUERY = gql`
+ query SINGLE_ITEM_QUERY($id: ID!) {
+ item(where: { id: $id }) {
+ id
+ title
+ description
+ largeImage
+ }
+ }
+`;
+class SingleItem extends Component {
+ render() {
+ return (
+ <Query
+ query={SINGLE_ITEM_QUERY}
+ variables={{
+ id: this.props.id,
+ }}
+ >
+ {({ error, loading, data }) => {
+ if (error) return <Error error={error} />;
+ if (loading) return <p>Loading...</p>;
+ if (!data.item) return <p>No Item Found for {this.props.id}</p>;
+ const item = data.item;
+ return (
+ <SingleItemStyles>
+ <Head>
+ <title>Sick Fits | {item.title}</title>
+ </Head>
+ <img src={item.largeImage} alt={item.title} />
+ <div className="details">
+ <h2>Viewing {item.title}</h2>
+ <p>{item.description}</p>
+ </div>
+ </SingleItemStyles>
+ );
+ }}
+ </Query>
+ );
+ }
+}
+
+export default SingleItem;
diff --git a/stepped-solutions/22/frontend/pages/item.js b/stepped-solutions/22/frontend/pages/item.js
new file mode 100755
index 0000000..e58b220
--- /dev/null
+++ b/stepped-solutions/22/frontend/pages/item.js
@@ -0,0 +1,9 @@
+import SingleItem from '../components/SingleItem';
+
+const Item = props => (
+ <div>
+ <SingleItem id={props.query.id} />
+ </div>
+);
+
+export default Item;
diff --git a/stepped-solutions/24/backend/src/resolvers/Query.js b/stepped-solutions/24/backend/src/resolvers/Query.js
new file mode 100755
index 0000000..821387e
--- /dev/null
+++ b/stepped-solutions/24/backend/src/resolvers/Query.js
@@ -0,0 +1,14 @@
+const { forwardTo } = require('prisma-binding');
+
+const Query = {
+ items: forwardTo('db'),
+ item: forwardTo('db'),
+ itemsConnection: forwardTo('db'),
+ // async items(parent, args, ctx, info) {
+ // console.log('Getting Items!!');
+ // const items = await ctx.db.query.items();
+ // return items;
+ // },
+};
+
+module.exports = Query;
diff --git a/stepped-solutions/24/backend/src/schema.graphql b/stepped-solutions/24/backend/src/schema.graphql
new file mode 100755
index 0000000..be73f71
--- /dev/null
+++ b/stepped-solutions/24/backend/src/schema.graphql
@@ -0,0 +1,13 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+}
diff --git a/stepped-solutions/24/frontend/components/Items.js b/stepped-solutions/24/frontend/components/Items.js
new file mode 100755
index 0000000..9b5426c
--- /dev/null
+++ b/stepped-solutions/24/frontend/components/Items.js
@@ -0,0 +1,61 @@
+import React, { Component } from 'react';
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import styled from 'styled-components';
+import Item from './Item';
+import Pagination from './Pagination';
+import { perPage } from '../config';
+
+const ALL_ITEMS_QUERY = gql`
+ query ALL_ITEMS_QUERY($skip: Int = 0, $first: Int = ${perPage}) {
+ items(first: $first, skip: $skip, orderBy: createdAt_DESC) {
+ id
+ title
+ price
+ description
+ image
+ largeImage
+ }
+ }
+`;
+
+const Center = styled.div`
+ text-align: center;
+`;
+
+const ItemsList = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 60px;
+ max-width: ${props => props.theme.maxWidth};
+ margin: 0 auto;
+`;
+
+class Items extends Component {
+ render() {
+ return (
+ <Center>
+ <Pagination page={this.props.page} />
+ <Query
+ query={ALL_ITEMS_QUERY}
+ // fetchPolicy="network-only"
+ variables={{
+ skip: this.props.page * perPage - perPage,
+ }}
+ >
+ {({ data, error, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <p>Error: {error.message}</p>;
+ return (
+ <ItemsList>{data.items.map(item => <Item item={item} key={item.id} />)}</ItemsList>
+ );
+ }}
+ </Query>
+ <Pagination page={this.props.page} />
+ </Center>
+ );
+ }
+}
+
+export default Items;
+export { ALL_ITEMS_QUERY };
diff --git a/stepped-solutions/24/frontend/components/Pagination.js b/stepped-solutions/24/frontend/components/Pagination.js
new file mode 100755
index 0000000..639f0d5
--- /dev/null
+++ b/stepped-solutions/24/frontend/components/Pagination.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import gql from 'graphql-tag';
+import { Query } from 'react-apollo';
+import Head from 'next/head';
+import Link from 'next/link';
+import PaginationStyles from './styles/PaginationStyles';
+import { perPage } from '../config';
+
+const PAGINATION_QUERY = gql`
+ query PAGINATION_QUERY {
+ itemsConnection {
+ aggregate {
+ count
+ }
+ }
+ }
+`;
+
+const Pagination = props => (
+ <Query query={PAGINATION_QUERY}>
+ {({ data, loading, error }) => {
+ if (loading) return <p>Loading...</p>;
+ const count = data.itemsConnection.aggregate.count;
+ const pages = Math.ceil(count / perPage);
+ const page = props.page;
+ return (
+ <PaginationStyles>
+ <Head>
+ <title>
+ Sick Fits! — Page {page} of {pages}
+ </title>
+ </Head>
+ <Link
+ prefetch
+ href={{
+ pathname: 'items',
+ query: { page: page - 1 },
+ }}
+ >
+ <a className="prev" aria-disabled={page <= 1}>
+ ← Prev
+ </a>
+ </Link>
+ <p>
+ Page {props.page} of {pages}!
+ </p>
+ <p>{count} Items Total</p>
+ <Link
+ prefetch
+ href={{
+ pathname: 'items',
+ query: { page: page + 1 },
+ }}
+ >
+ <a className="prev" aria-disabled={page >= pages}>
+ Next →
+ </a>
+ </Link>
+ </PaginationStyles>
+ );
+ }}
+ </Query>
+);
+
+export default Pagination;
diff --git a/stepped-solutions/24/frontend/pages/index.js b/stepped-solutions/24/frontend/pages/index.js
new file mode 100755
index 0000000..edb45ab
--- /dev/null
+++ b/stepped-solutions/24/frontend/pages/index.js
@@ -0,0 +1,9 @@
+import Items from '../components/Items';
+
+const Home = props => (
+ <div>
+ <Items page={parseFloat(props.query.page) || 1} />
+ </div>
+);
+
+export default Home;
diff --git a/stepped-solutions/24/frontend/pages/items.js b/stepped-solutions/24/frontend/pages/items.js
new file mode 100755
index 0000000..c95ff45
--- /dev/null
+++ b/stepped-solutions/24/frontend/pages/items.js
@@ -0,0 +1,3 @@
+import Items from './index';
+
+export default Items;
diff --git a/stepped-solutions/25/backend/datamodel.graphql b/stepped-solutions/25/backend/datamodel.graphql
new file mode 100755
index 0000000..e1b05ec
--- /dev/null
+++ b/stepped-solutions/25/backend/datamodel.graphql
@@ -0,0 +1,30 @@
+enum Permission {
+ ADMIN
+ USER
+ ITEMCREATE
+ ITEMUPDATE
+ ITEMDELETE
+ PERMISSIONUPDATE
+}
+
+type User {
+ id: ID! @unique
+ name: String!
+ email: String! @unique
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: [Permission]
+}
+
+type Item {
+ id: ID! @unique
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+ # createAt: DateTime!
+ # updatedAt: DateTime!
+ # user: User!
+}
diff --git a/stepped-solutions/25/backend/src/generated/prisma.graphql b/stepped-solutions/25/backend/src/generated/prisma.graphql
new file mode 100755
index 0000000..3d27a97
--- /dev/null
+++ b/stepped-solutions/25/backend/src/generated/prisma.graphql
@@ -0,0 +1,819 @@
+# source: https://us1.prisma.sh/wesbos/siccccccccck-fits/dev
+# timestamp: Wed Jul 11 2018 09:57:06 GMT-0400 (EDT)
+
+type AggregateItem {
+ count: Int!
+}
+
+type AggregateUser {
+ count: Int!
+}
+
+type BatchPayload {
+ """The number of nodes that have been affected by the Batch operation."""
+ count: Long!
+}
+
+type Item implements Node {
+ id: ID!
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+"""A connection to a list of items."""
+type ItemConnection {
+ """Information to aid in pagination."""
+ pageInfo: PageInfo!
+
+ """A list of edges."""
+ edges: [ItemEdge]!
+ aggregate: AggregateItem!
+}
+
+input ItemCreateInput {
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+"""An edge in a connection."""
+type ItemEdge {
+ """The item at the end of the edge."""
+ node: Item!
+
+ """A cursor for use in pagination."""
+ cursor: String!
+}
+
+enum ItemOrderByInput {
+ id_ASC
+ id_DESC
+ title_ASC
+ title_DESC
+ description_ASC
+ description_DESC
+ image_ASC
+ image_DESC
+ largeImage_ASC
+ largeImage_DESC
+ price_ASC
+ price_DESC
+ updatedAt_ASC
+ updatedAt_DESC
+ createdAt_ASC
+ createdAt_DESC
+}
+
+type ItemPreviousValues {
+ id: ID!
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+type ItemSubscriptionPayload {
+ mutation: MutationType!
+ node: Item
+ updatedFields: [String!]
+ previousValues: ItemPreviousValues
+}
+
+input ItemSubscriptionWhereInput {
+ """Logical AND on all given filters."""
+ AND: [ItemSubscriptionWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [ItemSubscriptionWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [ItemSubscriptionWhereInput!]
+
+ """
+ The subscription event gets dispatched when it's listed in mutation_in
+ """
+ mutation_in: [MutationType!]
+
+ """
+ The subscription event gets only dispatched when one of the updated fields names is included in this list
+ """
+ updatedFields_contains: String
+
+ """
+ The subscription event gets only dispatched when all of the field names included in this list have been updated
+ """
+ updatedFields_contains_every: [String!]
+
+ """
+ The subscription event gets only dispatched when some of the field names included in this list have been updated
+ """
+ updatedFields_contains_some: [String!]
+ node: ItemWhereInput
+}
+
+input ItemUpdateInput {
+ title: String
+ description: String
+ image: String
+ largeImage: String
+ price: Int
+}
+
+input ItemWhereInput {
+ """Logical AND on all given filters."""
+ AND: [ItemWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [ItemWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [ItemWhereInput!]
+ id: ID
+
+ """All values that are not equal to given value."""
+ id_not: ID
+
+ """All values that are contained in given list."""
+ id_in: [ID!]
+
+ """All values that are not contained in given list."""
+ id_not_in: [ID!]
+
+ """All values less than the given value."""
+ id_lt: ID
+
+ """All values less than or equal the given value."""
+ id_lte: ID
+
+ """All values greater than the given value."""
+ id_gt: ID
+
+ """All values greater than or equal the given value."""
+ id_gte: ID
+
+ """All values containing the given string."""
+ id_contains: ID
+
+ """All values not containing the given string."""
+ id_not_contains: ID
+
+ """All values starting with the given string."""
+ id_starts_with: ID
+
+ """All values not starting with the given string."""
+ id_not_starts_with: ID
+
+ """All values ending with the given string."""
+ id_ends_with: ID
+
+ """All values not ending with the given string."""
+ id_not_ends_with: ID
+ title: String
+
+ """All values that are not equal to given value."""
+ title_not: String
+
+ """All values that are contained in given list."""
+ title_in: [String!]
+
+ """All values that are not contained in given list."""
+ title_not_in: [String!]
+
+ """All values less than the given value."""
+ title_lt: String
+
+ """All values less than or equal the given value."""
+ title_lte: String
+
+ """All values greater than the given value."""
+ title_gt: String
+
+ """All values greater than or equal the given value."""
+ title_gte: String
+
+ """All values containing the given string."""
+ title_contains: String
+
+ """All values not containing the given string."""
+ title_not_contains: String
+
+ """All values starting with the given string."""
+ title_starts_with: String
+
+ """All values not starting with the given string."""
+ title_not_starts_with: String
+
+ """All values ending with the given string."""
+ title_ends_with: String
+
+ """All values not ending with the given string."""
+ title_not_ends_with: String
+ description: String
+
+ """All values that are not equal to given value."""
+ description_not: String
+
+ """All values that are contained in given list."""
+ description_in: [String!]
+
+ """All values that are not contained in given list."""
+ description_not_in: [String!]
+
+ """All values less than the given value."""
+ description_lt: String
+
+ """All values less than or equal the given value."""
+ description_lte: String
+
+ """All values greater than the given value."""
+ description_gt: String
+
+ """All values greater than or equal the given value."""
+ description_gte: String
+
+ """All values containing the given string."""
+ description_contains: String
+
+ """All values not containing the given string."""
+ description_not_contains: String
+
+ """All values starting with the given string."""
+ description_starts_with: String
+
+ """All values not starting with the given string."""
+ description_not_starts_with: String
+
+ """All values ending with the given string."""
+ description_ends_with: String
+
+ """All values not ending with the given string."""
+ description_not_ends_with: String
+ image: String
+
+ """All values that are not equal to given value."""
+ image_not: String
+
+ """All values that are contained in given list."""
+ image_in: [String!]
+
+ """All values that are not contained in given list."""
+ image_not_in: [String!]
+
+ """All values less than the given value."""
+ image_lt: String
+
+ """All values less than or equal the given value."""
+ image_lte: String
+
+ """All values greater than the given value."""
+ image_gt: String
+
+ """All values greater than or equal the given value."""
+ image_gte: String
+
+ """All values containing the given string."""
+ image_contains: String
+
+ """All values not containing the given string."""
+ image_not_contains: String
+
+ """All values starting with the given string."""
+ image_starts_with: String
+
+ """All values not starting with the given string."""
+ image_not_starts_with: String
+
+ """All values ending with the given string."""
+ image_ends_with: String
+
+ """All values not ending with the given string."""
+ image_not_ends_with: String
+ largeImage: String
+
+ """All values that are not equal to given value."""
+ largeImage_not: String
+
+ """All values that are contained in given list."""
+ largeImage_in: [String!]
+
+ """All values that are not contained in given list."""
+ largeImage_not_in: [String!]
+
+ """All values less than the given value."""
+ largeImage_lt: String
+
+ """All values less than or equal the given value."""
+ largeImage_lte: String
+
+ """All values greater than the given value."""
+ largeImage_gt: String
+
+ """All values greater than or equal the given value."""
+ largeImage_gte: String
+
+ """All values containing the given string."""
+ largeImage_contains: String
+
+ """All values not containing the given string."""
+ largeImage_not_contains: String
+
+ """All values starting with the given string."""
+ largeImage_starts_with: String
+
+ """All values not starting with the given string."""
+ largeImage_not_starts_with: String
+
+ """All values ending with the given string."""
+ largeImage_ends_with: String
+
+ """All values not ending with the given string."""
+ largeImage_not_ends_with: String
+ price: Int
+
+ """All values that are not equal to given value."""
+ price_not: Int
+
+ """All values that are contained in given list."""
+ price_in: [Int!]
+
+ """All values that are not contained in given list."""
+ price_not_in: [Int!]
+
+ """All values less than the given value."""
+ price_lt: Int
+
+ """All values less than or equal the given value."""
+ price_lte: Int
+
+ """All values greater than the given value."""
+ price_gt: Int
+
+ """All values greater than or equal the given value."""
+ price_gte: Int
+}
+
+input ItemWhereUniqueInput {
+ id: ID
+}
+
+"""
+The `Long` scalar type represents non-fractional signed whole numeric values.
+Long can represent values between -(2^63) and 2^63 - 1.
+"""
+scalar Long
+
+type Mutation {
+ createUser(data: UserCreateInput!): User!
+ createItem(data: ItemCreateInput!): Item!
+ updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
+ updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item
+ deleteUser(where: UserWhereUniqueInput!): User
+ deleteItem(where: ItemWhereUniqueInput!): Item
+ upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
+ upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item!
+ updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload!
+ updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput): BatchPayload!
+ deleteManyUsers(where: UserWhereInput): BatchPayload!
+ deleteManyItems(where: ItemWhereInput): BatchPayload!
+}
+
+enum MutationType {
+ CREATED
+ UPDATED
+ DELETED
+}
+
+"""An object with an ID"""
+interface Node {
+ """The id of the object."""
+ id: ID!
+}
+
+"""Information about pagination in a connection."""
+type PageInfo {
+ """When paginating forwards, are there more items?"""
+ hasNextPage: Boolean!
+
+ """When paginating backwards, are there more items?"""
+ hasPreviousPage: Boolean!
+
+ """When paginating backwards, the cursor to continue."""
+ startCursor: String
+
+ """When paginating forwards, the cursor to continue."""
+ endCursor: String
+}
+
+enum Permission {
+ ADMIN
+ USER
+ ITEMCREATE
+ ITEMUPDATE
+ ITEMDELETE
+ PERMISSIONUPDATE
+}
+
+type Query {
+ users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]!
+ user(where: UserWhereUniqueInput!): User
+ item(where: ItemWhereUniqueInput!): Item
+ usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
+ itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection!
+
+ """Fetches an object given its ID"""
+ node(
+ """The ID of an object"""
+ id: ID!
+ ): Node
+}
+
+type Subscription {
+ user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
+ item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload
+}
+
+type User implements Node {
+ id: ID!
+ name: String!
+ email: String!
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: [Permission!]!
+}
+
+"""A connection to a list of items."""
+type UserConnection {
+ """Information to aid in pagination."""
+ pageInfo: PageInfo!
+
+ """A list of edges."""
+ edges: [UserEdge]!
+ aggregate: AggregateUser!
+}
+
+input UserCreateInput {
+ name: String!
+ email: String!
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserCreatepermissionsInput
+}
+
+input UserCreatepermissionsInput {
+ set: [Permission!]
+}
+
+"""An edge in a connection."""
+type UserEdge {
+ """The item at the end of the edge."""
+ node: User!
+
+ """A cursor for use in pagination."""
+ cursor: String!
+}
+
+enum UserOrderByInput {
+ id_ASC
+ id_DESC
+ name_ASC
+ name_DESC
+ email_ASC
+ email_DESC
+ password_ASC
+ password_DESC
+ resetToken_ASC
+ resetToken_DESC
+ resetTokenExpiry_ASC
+ resetTokenExpiry_DESC
+ updatedAt_ASC
+ updatedAt_DESC
+ createdAt_ASC
+ createdAt_DESC
+}
+
+type UserPreviousValues {
+ id: ID!
+ name: String!
+ email: String!
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: [Permission!]!
+}
+
+type UserSubscriptionPayload {
+ mutation: MutationType!
+ node: User
+ updatedFields: [String!]
+ previousValues: UserPreviousValues
+}
+
+input UserSubscriptionWhereInput {
+ """Logical AND on all given filters."""
+ AND: [UserSubscriptionWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [UserSubscriptionWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [UserSubscriptionWhereInput!]
+
+ """
+ The subscription event gets dispatched when it's listed in mutation_in
+ """
+ mutation_in: [MutationType!]
+
+ """
+ The subscription event gets only dispatched when one of the updated fields names is included in this list
+ """
+ updatedFields_contains: String
+
+ """
+ The subscription event gets only dispatched when all of the field names included in this list have been updated
+ """
+ updatedFields_contains_every: [String!]
+
+ """
+ The subscription event gets only dispatched when some of the field names included in this list have been updated
+ """
+ updatedFields_contains_some: [String!]
+ node: UserWhereInput
+}
+
+input UserUpdateInput {
+ name: String
+ email: String
+ password: String
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserUpdatepermissionsInput
+}
+
+input UserUpdatepermissionsInput {
+ set: [Permission!]
+}
+
+input UserWhereInput {
+ """Logical AND on all given filters."""
+ AND: [UserWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [UserWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [UserWhereInput!]
+ id: ID
+
+ """All values that are not equal to given value."""
+ id_not: ID
+
+ """All values that are contained in given list."""
+ id_in: [ID!]
+
+ """All values that are not contained in given list."""
+ id_not_in: [ID!]
+
+ """All values less than the given value."""
+ id_lt: ID
+
+ """All values less than or equal the given value."""
+ id_lte: ID
+
+ """All values greater than the given value."""
+ id_gt: ID
+
+ """All values greater than or equal the given value."""
+ id_gte: ID
+
+ """All values containing the given string."""
+ id_contains: ID
+
+ """All values not containing the given string."""
+ id_not_contains: ID
+
+ """All values starting with the given string."""
+ id_starts_with: ID
+
+ """All values not starting with the given string."""
+ id_not_starts_with: ID
+
+ """All values ending with the given string."""
+ id_ends_with: ID
+
+ """All values not ending with the given string."""
+ id_not_ends_with: ID
+ name: String
+
+ """All values that are not equal to given value."""
+ name_not: String
+
+ """All values that are contained in given list."""
+ name_in: [String!]
+
+ """All values that are not contained in given list."""
+ name_not_in: [String!]
+
+ """All values less than the given value."""
+ name_lt: String
+
+ """All values less than or equal the given value."""
+ name_lte: String
+
+ """All values greater than the given value."""
+ name_gt: String
+
+ """All values greater than or equal the given value."""
+ name_gte: String
+
+ """All values containing the given string."""
+ name_contains: String
+
+ """All values not containing the given string."""
+ name_not_contains: String
+
+ """All values starting with the given string."""
+ name_starts_with: String
+
+ """All values not starting with the given string."""
+ name_not_starts_with: String
+
+ """All values ending with the given string."""
+ name_ends_with: String
+
+ """All values not ending with the given string."""
+ name_not_ends_with: String
+ email: String
+
+ """All values that are not equal to given value."""
+ email_not: String
+
+ """All values that are contained in given list."""
+ email_in: [String!]
+
+ """All values that are not contained in given list."""
+ email_not_in: [String!]
+
+ """All values less than the given value."""
+ email_lt: String
+
+ """All values less than or equal the given value."""
+ email_lte: String
+
+ """All values greater than the given value."""
+ email_gt: String
+
+ """All values greater than or equal the given value."""
+ email_gte: String
+
+ """All values containing the given string."""
+ email_contains: String
+
+ """All values not containing the given string."""
+ email_not_contains: String
+
+ """All values starting with the given string."""
+ email_starts_with: String
+
+ """All values not starting with the given string."""
+ email_not_starts_with: String
+
+ """All values ending with the given string."""
+ email_ends_with: String
+
+ """All values not ending with the given string."""
+ email_not_ends_with: String
+ password: String
+
+ """All values that are not equal to given value."""
+ password_not: String
+
+ """All values that are contained in given list."""
+ password_in: [String!]
+
+ """All values that are not contained in given list."""
+ password_not_in: [String!]
+
+ """All values less than the given value."""
+ password_lt: String
+
+ """All values less than or equal the given value."""
+ password_lte: String
+
+ """All values greater than the given value."""
+ password_gt: String
+
+ """All values greater than or equal the given value."""
+ password_gte: String
+
+ """All values containing the given string."""
+ password_contains: String
+
+ """All values not containing the given string."""
+ password_not_contains: String
+
+ """All values starting with the given string."""
+ password_starts_with: String
+
+ """All values not starting with the given string."""
+ password_not_starts_with: String
+
+ """All values ending with the given string."""
+ password_ends_with: String
+
+ """All values not ending with the given string."""
+ password_not_ends_with: String
+ resetToken: String
+
+ """All values that are not equal to given value."""
+ resetToken_not: String
+
+ """All values that are contained in given list."""
+ resetToken_in: [String!]
+
+ """All values that are not contained in given list."""
+ resetToken_not_in: [String!]
+
+ """All values less than the given value."""
+ resetToken_lt: String
+
+ """All values less than or equal the given value."""
+ resetToken_lte: String
+
+ """All values greater than the given value."""
+ resetToken_gt: String
+
+ """All values greater than or equal the given value."""
+ resetToken_gte: String
+
+ """All values containing the given string."""
+ resetToken_contains: String
+
+ """All values not containing the given string."""
+ resetToken_not_contains: String
+
+ """All values starting with the given string."""
+ resetToken_starts_with: String
+
+ """All values not starting with the given string."""
+ resetToken_not_starts_with: String
+
+ """All values ending with the given string."""
+ resetToken_ends_with: String
+
+ """All values not ending with the given string."""
+ resetToken_not_ends_with: String
+ resetTokenExpiry: String
+
+ """All values that are not equal to given value."""
+ resetTokenExpiry_not: String
+
+ """All values that are contained in given list."""
+ resetTokenExpiry_in: [String!]
+
+ """All values that are not contained in given list."""
+ resetTokenExpiry_not_in: [String!]
+
+ """All values less than the given value."""
+ resetTokenExpiry_lt: String
+
+ """All values less than or equal the given value."""
+ resetTokenExpiry_lte: String
+
+ """All values greater than the given value."""
+ resetTokenExpiry_gt: String
+
+ """All values greater than or equal the given value."""
+ resetTokenExpiry_gte: String
+
+ """All values containing the given string."""
+ resetTokenExpiry_contains: String
+
+ """All values not containing the given string."""
+ resetTokenExpiry_not_contains: String
+
+ """All values starting with the given string."""
+ resetTokenExpiry_starts_with: String
+
+ """All values not starting with the given string."""
+ resetTokenExpiry_not_starts_with: String
+
+ """All values ending with the given string."""
+ resetTokenExpiry_ends_with: String
+
+ """All values not ending with the given string."""
+ resetTokenExpiry_not_ends_with: String
+}
+
+input UserWhereUniqueInput {
+ id: ID
+ email: String
+}
diff --git a/stepped-solutions/25/backend/src/index.js b/stepped-solutions/25/backend/src/index.js
new file mode 100755
index 0000000..2edee7e
--- /dev/null
+++ b/stepped-solutions/25/backend/src/index.js
@@ -0,0 +1,22 @@
+const cookieParser = require('cookie-parser');
+
+require('dotenv').config({ path: 'variables.env' });
+const createServer = require('./createServer');
+const db = require('./db');
+
+const server = createServer();
+
+server.express.use(cookieParser());
+// TODO Use express middlware to populate current user
+
+server.start(
+ {
+ cors: {
+ credentials: true,
+ origin: process.env.FRONTEND_URL,
+ },
+ },
+ deets => {
+ console.log(`Server is now running on port http://localhost:${deets.port}`);
+ }
+);
diff --git a/stepped-solutions/25/backend/src/resolvers/Mutation.js b/stepped-solutions/25/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..efaa5be
--- /dev/null
+++ b/stepped-solutions/25/backend/src/resolvers/Mutation.js
@@ -0,0 +1,74 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/25/backend/src/schema.graphql b/stepped-solutions/25/backend/src/schema.graphql
new file mode 100755
index 0000000..f3c6d3f
--- /dev/null
+++ b/stepped-solutions/25/backend/src/schema.graphql
@@ -0,0 +1,14 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+}
diff --git a/stepped-solutions/26/frontend/components/Signup.js b/stepped-solutions/26/frontend/components/Signup.js
new file mode 100755
index 0000000..974bc62
--- /dev/null
+++ b/stepped-solutions/26/frontend/components/Signup.js
@@ -0,0 +1,81 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+
+const SIGNUP_MUTATION = gql`
+ mutation SIGNUP_MUTATION($email: String!, $name: String!, $password: String!) {
+ signup(email: $email, name: $name, password: $password) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Signup extends Component {
+ state = {
+ name: '',
+ password: '',
+ email: '',
+ };
+ saveToState = e => {
+ this.setState({ [e.target.name]: e.target.value });
+ };
+ render() {
+ return (
+ <Mutation mutation={SIGNUP_MUTATION} variables={this.state}>
+ {(signup, { error, loading }) => (
+ <Form
+ method="post"
+ onSubmit={async e => {
+ e.preventDefault();
+ await signup();
+ this.setState({ name: '', email: '', password: '' });
+ }}
+ >
+ <fieldset disabled={loading} aria-busy={loading}>
+ <h2>Sign Up for An Account</h2>
+ <Error error={error} />
+ <label htmlFor="email">
+ Email
+ <input
+ type="email"
+ name="email"
+ placeholder="email"
+ value={this.state.email}
+ onChange={this.saveToState}
+ />
+ </label>
+ <label htmlFor="name">
+ Name
+ <input
+ type="text"
+ name="name"
+ placeholder="name"
+ value={this.state.name}
+ onChange={this.saveToState}
+ />
+ </label>
+ <label htmlFor="password">
+ Password
+ <input
+ type="password"
+ name="password"
+ placeholder="password"
+ value={this.state.password}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <button type="submit">Sign Up!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Signup;
diff --git a/stepped-solutions/26/frontend/pages/signup.js b/stepped-solutions/26/frontend/pages/signup.js
new file mode 100755
index 0000000..dd90349
--- /dev/null
+++ b/stepped-solutions/26/frontend/pages/signup.js
@@ -0,0 +1,18 @@
+import Signup from '../components/Signup';
+import styled from 'styled-components';
+
+const Columns = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ grid-gap: 20px;
+`;
+
+const SignupPage = props => (
+ <Columns>
+ <Signup />
+ <Signup />
+ <Signup />
+ </Columns>
+);
+
+export default SignupPage;
diff --git a/stepped-solutions/27/backend/src/index.js b/stepped-solutions/27/backend/src/index.js
new file mode 100755
index 0000000..d95ec58
--- /dev/null
+++ b/stepped-solutions/27/backend/src/index.js
@@ -0,0 +1,33 @@
+const cookieParser = require('cookie-parser');
+const jwt = require('jsonwebtoken');
+
+require('dotenv').config({ path: 'variables.env' });
+const createServer = require('./createServer');
+const db = require('./db');
+
+const server = createServer();
+
+server.express.use(cookieParser());
+
+// decode the JWT so we can get the user Id on each request
+server.express.use((req, res, next) => {
+ const { token } = req.cookies;
+ if (token) {
+ const { userId } = jwt.verify(token, process.env.APP_SECRET);
+ // put the userId onto the req for future requests to access
+ req.userId = userId;
+ }
+ next();
+});
+
+server.start(
+ {
+ cors: {
+ credentials: true,
+ origin: process.env.FRONTEND_URL,
+ },
+ },
+ deets => {
+ console.log(`Server is now running on port http://localhost:${deets.port}`);
+ }
+);
diff --git a/stepped-solutions/27/backend/src/resolvers/Query.js b/stepped-solutions/27/backend/src/resolvers/Query.js
new file mode 100755
index 0000000..8055122
--- /dev/null
+++ b/stepped-solutions/27/backend/src/resolvers/Query.js
@@ -0,0 +1,21 @@
+const { forwardTo } = require('prisma-binding');
+
+const Query = {
+ items: forwardTo('db'),
+ item: forwardTo('db'),
+ itemsConnection: forwardTo('db'),
+ me(parent, args, ctx, info) {
+ // check if there is a current user ID
+ if (!ctx.request.userId) {
+ return null;
+ }
+ return ctx.db.query.user(
+ {
+ where: { id: ctx.request.userId },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Query;
diff --git a/stepped-solutions/27/backend/src/schema.graphql b/stepped-solutions/27/backend/src/schema.graphql
new file mode 100755
index 0000000..f2cc283
--- /dev/null
+++ b/stepped-solutions/27/backend/src/schema.graphql
@@ -0,0 +1,15 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+}
diff --git a/stepped-solutions/27/frontend/components/Nav.js b/stepped-solutions/27/frontend/components/Nav.js
new file mode 100755
index 0000000..df6e47f
--- /dev/null
+++ b/stepped-solutions/27/frontend/components/Nav.js
@@ -0,0 +1,32 @@
+import Link from 'next/link';
+import NavStyles from './styles/NavStyles';
+import User from './User';
+
+const Nav = () => (
+ <NavStyles>
+ <User>
+ {({ data: { me } }) => {
+ console.log(me);
+ if (me) return <p>{me.name}</p>;
+ return null;
+ }}
+ </User>
+ <Link href="/items">
+ <a>Shop</a>
+ </Link>
+ <Link href="/sell">
+ <a>Sell</a>
+ </Link>
+ <Link href="/signup">
+ <a>Signup</a>
+ </Link>
+ <Link href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link href="/me">
+ <a>Account</a>
+ </Link>
+ </NavStyles>
+);
+
+export default Nav;
diff --git a/stepped-solutions/27/frontend/components/User.js b/stepped-solutions/27/frontend/components/User.js
new file mode 100755
index 0000000..addd79d
--- /dev/null
+++ b/stepped-solutions/27/frontend/components/User.js
@@ -0,0 +1,27 @@
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import PropTypes from 'prop-types';
+
+const CURRENT_USER_QUERY = gql`
+ query {
+ me {
+ id
+ email
+ name
+ permissions
+ }
+ }
+`;
+
+const User = props => (
+ <Query {...props} query={CURRENT_USER_QUERY}>
+ {payload => props.children(payload)}
+ </Query>
+);
+
+User.PropTypes = {
+ children: PropTypes.func.isRequired,
+};
+
+export default User;
+export { CURRENT_USER_QUERY };
diff --git a/stepped-solutions/28/backend/src/resolvers/Mutation.js b/stepped-solutions/28/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..7bcbb9c
--- /dev/null
+++ b/stepped-solutions/28/backend/src/resolvers/Mutation.js
@@ -0,0 +1,95 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+ async signin(parent, { email, password }, ctx, info) {
+ // 1. check if there is a user with that email
+ const user = await ctx.db.query.user({ where: { email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${email}`);
+ }
+ // 2. Check if their password is correct
+ const valid = await bcrypt.compare(password, user.password);
+ if (!valid) {
+ throw new Error('Invalid Password!');
+ }
+ // 3. generate the JWT Token
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // 4. Set the cookie with the token
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 5. Return the user
+ return user;
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/28/backend/src/schema.graphql b/stepped-solutions/28/backend/src/schema.graphql
new file mode 100755
index 0000000..a8ad765
--- /dev/null
+++ b/stepped-solutions/28/backend/src/schema.graphql
@@ -0,0 +1,17 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+ signin(email: String!, password: String!): User!
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+
+}
diff --git a/stepped-solutions/28/frontend/components/Nav.js b/stepped-solutions/28/frontend/components/Nav.js
new file mode 100755
index 0000000..28e94d3
--- /dev/null
+++ b/stepped-solutions/28/frontend/components/Nav.js
@@ -0,0 +1,36 @@
+import Link from 'next/link';
+import NavStyles from './styles/NavStyles';
+import User from './User';
+
+const Nav = () => (
+ <User>
+ {({ data: { me } }) => (
+ <NavStyles>
+ <Link href="/items">
+ <a>Shop</a>
+ </Link>
+ {me && (
+ <>
+ <Link href="/sell">
+ <a>Sell</a>
+ </Link>
+ <Link href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link href="/me">
+ <a>Account</a>
+ </Link>
+ </>
+ )}
+ {!me && (
+ <Link href="/signup">
+ <a>Sign In</a>
+ </Link>
+
+ )}
+ </NavStyles>
+ )}
+ </User>
+);
+
+export default Nav;
diff --git a/stepped-solutions/28/frontend/components/Signin.js b/stepped-solutions/28/frontend/components/Signin.js
new file mode 100755
index 0000000..4fd3013
--- /dev/null
+++ b/stepped-solutions/28/frontend/components/Signin.js
@@ -0,0 +1,76 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+import { CURRENT_USER_QUERY } from './User';
+
+const SIGNIN_MUTATION = gql`
+ mutation SIGNIN_MUTATION($email: String!, $password: String!) {
+ signin(email: $email, password: $password) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Signin extends Component {
+ state = {
+ name: '',
+ password: '',
+ email: '',
+ };
+ saveToState = e => {
+ this.setState({ [e.target.name]: e.target.value });
+ };
+ render() {
+ return (
+ <Mutation
+ mutation={SIGNIN_MUTATION}
+ variables={this.state}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(signup, { error, loading }) => (
+ <Form
+ method="post"
+ onSubmit={async e => {
+ e.preventDefault();
+ await signup();
+ this.setState({ name: '', email: '', password: '' });
+ }}
+ >
+ <fieldset disabled={loading} aria-busy={loading}>
+ <h2>Sign into your account</h2>
+ <Error error={error} />
+ <label htmlFor="email">
+ Email
+ <input
+ type="email"
+ name="email"
+ placeholder="email"
+ value={this.state.email}
+ onChange={this.saveToState}
+ />
+ </label>
+ <label htmlFor="password">
+ Password
+ <input
+ type="password"
+ name="password"
+ placeholder="password"
+ value={this.state.password}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <button type="submit">Sign In!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Signin;
diff --git a/stepped-solutions/28/frontend/components/Signup.js b/stepped-solutions/28/frontend/components/Signup.js
new file mode 100755
index 0000000..3e59a2b
--- /dev/null
+++ b/stepped-solutions/28/frontend/components/Signup.js
@@ -0,0 +1,86 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+import { CURRENT_USER_QUERY } from './User';
+
+const SIGNUP_MUTATION = gql`
+ mutation SIGNUP_MUTATION($email: String!, $name: String!, $password: String!) {
+ signup(email: $email, name: $name, password: $password) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Signup extends Component {
+ state = {
+ name: '',
+ password: '',
+ email: '',
+ };
+ saveToState = e => {
+ this.setState({ [e.target.name]: e.target.value });
+ };
+ render() {
+ return (
+ <Mutation
+ mutation={SIGNUP_MUTATION}
+ variables={this.state}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(signup, { error, loading }) => (
+ <Form
+ method="post"
+ onSubmit={async e => {
+ e.preventDefault();
+ await signup();
+ this.setState({ name: '', email: '', password: '' });
+ }}
+ >
+ <fieldset disabled={loading} aria-busy={loading}>
+ <h2>Sign Up for An Account</h2>
+ <Error error={error} />
+ <label htmlFor="email">
+ Email
+ <input
+ type="email"
+ name="email"
+ placeholder="email"
+ value={this.state.email}
+ onChange={this.saveToState}
+ />
+ </label>
+ <label htmlFor="name">
+ Name
+ <input
+ type="text"
+ name="name"
+ placeholder="name"
+ value={this.state.name}
+ onChange={this.saveToState}
+ />
+ </label>
+ <label htmlFor="password">
+ Password
+ <input
+ type="password"
+ name="password"
+ placeholder="password"
+ value={this.state.password}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <button type="submit">Sign Up!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Signup;
diff --git a/stepped-solutions/28/frontend/components/User.js b/stepped-solutions/28/frontend/components/User.js
new file mode 100755
index 0000000..af074c0
--- /dev/null
+++ b/stepped-solutions/28/frontend/components/User.js
@@ -0,0 +1,27 @@
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import PropTypes from 'prop-types';
+
+const CURRENT_USER_QUERY = gql`
+ query {
+ me {
+ id
+ email
+ name
+ permissions
+ }
+ }
+`;
+
+const User = props => (
+ <Query {...props} query={CURRENT_USER_QUERY}>
+ {payload => console.log(payload) || props.children(payload)}
+ </Query>
+);
+
+User.PropTypes = {
+ children: PropTypes.func.isRequired,
+};
+
+export default User;
+export { CURRENT_USER_QUERY };
diff --git a/stepped-solutions/28/frontend/pages/signup.js b/stepped-solutions/28/frontend/pages/signup.js
new file mode 100755
index 0000000..28ed2f4
--- /dev/null
+++ b/stepped-solutions/28/frontend/pages/signup.js
@@ -0,0 +1,18 @@
+import Signup from '../components/Signup';
+import Signin from '../components/Signin';
+import styled from 'styled-components';
+
+const Columns = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ grid-gap: 20px;
+`;
+
+const SignupPage = props => (
+ <Columns>
+ <Signup />
+ <Signin />
+ </Columns>
+);
+
+export default SignupPage;
diff --git a/stepped-solutions/29/backend/src/resolvers/Mutation.js b/stepped-solutions/29/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..6c34b8c
--- /dev/null
+++ b/stepped-solutions/29/backend/src/resolvers/Mutation.js
@@ -0,0 +1,99 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+ async signin(parent, { email, password }, ctx, info) {
+ // 1. check if there is a user with that email
+ const user = await ctx.db.query.user({ where: { email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${email}`);
+ }
+ // 2. Check if their password is correct
+ const valid = await bcrypt.compare(password, user.password);
+ if (!valid) {
+ throw new Error('Invalid Password!');
+ }
+ // 3. generate the JWT Token
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // 4. Set the cookie with the token
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 5. Return the user
+ return user;
+ },
+ signout(parent, args, ctx, info) {
+ ctx.response.clearCookie('token');
+ return { message: 'Goodbye!' };
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/29/backend/src/schema.graphql b/stepped-solutions/29/backend/src/schema.graphql
new file mode 100755
index 0000000..40916a1
--- /dev/null
+++ b/stepped-solutions/29/backend/src/schema.graphql
@@ -0,0 +1,22 @@
+# import * from './generated/prisma.graphql'
+
+type SuccessMessage {
+ message: String
+}
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+ signin(email: String!, password: String!): User!
+ signout: SuccessMessage
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+
+}
diff --git a/stepped-solutions/29/frontend/components/Nav.js b/stepped-solutions/29/frontend/components/Nav.js
new file mode 100755
index 0000000..12abde5
--- /dev/null
+++ b/stepped-solutions/29/frontend/components/Nav.js
@@ -0,0 +1,38 @@
+import Link from 'next/link';
+import NavStyles from './styles/NavStyles';
+import User from './User';
+import Signout from './Signout';
+
+const Nav = () => (
+ <User>
+ {({ data: { me } }) => (
+ <NavStyles>
+ <Link href="/items">
+ <a>Shop</a>
+ </Link>
+ {me && (
+ <>
+ <Link href="/sell">
+ <a>Sell</a>
+ </Link>
+ <Link href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link href="/me">
+ <a>Account</a>
+ </Link>
+ <Signout />
+ </>
+ )}
+ {!me && (
+ <Link href="/signup">
+ <a>Sign In</a>
+ </Link>
+
+ )}
+ </NavStyles>
+ )}
+ </User>
+);
+
+export default Nav;
diff --git a/stepped-solutions/29/frontend/components/Signout.js b/stepped-solutions/29/frontend/components/Signout.js
new file mode 100755
index 0000000..f852531
--- /dev/null
+++ b/stepped-solutions/29/frontend/components/Signout.js
@@ -0,0 +1,19 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+
+const SIGN_OUT_MUTATION = gql`
+ mutation SIGN_OUT_MUTATION {
+ signout {
+ message
+ }
+ }
+`;
+
+const Signout = props => (
+ <Mutation mutation={SIGN_OUT_MUTATION} refetchQueries={[{ query: CURRENT_USER_QUERY }]}>
+ {signout => <button onClick={signout}>Sign Out</button>}
+ </Mutation>
+);
+export default Signout;
diff --git a/stepped-solutions/29/frontend/components/styles/NavStyles.js b/stepped-solutions/29/frontend/components/styles/NavStyles.js
new file mode 100755
index 0000000..fe4abda
--- /dev/null
+++ b/stepped-solutions/29/frontend/components/styles/NavStyles.js
@@ -0,0 +1,66 @@
+import styled from 'styled-components';
+
+const NavStyles = styled.ul`
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-self: end;
+ font-size: 2rem;
+ a,
+ button {
+ padding: 1rem 3rem;
+ display: flex;
+ align-items: center;
+ position: relative;
+ text-transform: uppercase;
+ font-weight: 900;
+ font-size: 1em;
+ background: none;
+ border: 0;
+ cursor: pointer;
+ color: ${props => props.theme.black};
+ font-weight: 800;
+ @media (max-width: 700px) {
+ font-size: 10px;
+ padding: 0 10px;
+ }
+ &:before {
+ content: '';
+ width: 2px;
+ background: ${props => props.theme.lightgrey};
+ height: 100%;
+ left: 0;
+ position: absolute;
+ transform: skew(-20deg);
+ top: 0;
+ bottom: 0;
+ }
+ &:after {
+ height: 2px;
+ background: red;
+ content: '';
+ width: 0;
+ position: absolute;
+ transform: translateX(-50%);
+ transition: width 0.4s;
+ transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
+ left: 50%;
+ margin-top: 2rem;
+ }
+ &:hover,
+ &:focus {
+ outline: none;
+ &:after {
+ width: calc(100% - 60px);
+ }
+ }
+ }
+ @media (max-width: 1300px) {
+ border-top: 1px solid ${props => props.theme.lightgrey};
+ width: 100%;
+ justify-content: center;
+ font-size: 1.5rem;
+ }
+`;
+
+export default NavStyles;
diff --git a/stepped-solutions/30/src/resolvers/Mutation.js b/stepped-solutions/30/src/resolvers/Mutation.js
new file mode 100755
index 0000000..7f43b1a
--- /dev/null
+++ b/stepped-solutions/30/src/resolvers/Mutation.js
@@ -0,0 +1,156 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const { randomBytes } = require('crypto');
+const { promisify } = require('util');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+ async signin(parent, { email, password }, ctx, info) {
+ // 1. check if there is a user with that email
+ const user = await ctx.db.query.user({ where: { email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${email}`);
+ }
+ // 2. Check if their password is correct
+ const valid = await bcrypt.compare(password, user.password);
+ if (!valid) {
+ throw new Error('Invalid Password!');
+ }
+ // 3. generate the JWT Token
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // 4. Set the cookie with the token
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 5. Return the user
+ return user;
+ },
+ signout(parent, args, ctx, info) {
+ ctx.response.clearCookie('token');
+ return { message: 'Goodbye!' };
+ },
+ async requestReset(parent, args, ctx, info) {
+ // 1. Check if this is a real user
+ const user = await ctx.db.query.user({ where: { email: args.email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${args.email}`);
+ }
+ // 2. Set a reset token and expiry on that user
+ const randomBytesPromiseified = promisify(randomBytes);
+ const resetToken = (await randomBytesPromiseified(20)).toString('hex');
+ const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now
+ const res = await ctx.db.mutation.updateUser({
+ where: { email: args.email },
+ data: { resetToken, resetTokenExpiry },
+ });
+ console.log(res);
+ return { message: 'Thanks!' };
+ // 3. Email them that reset token
+ },
+ async resetPassword(parent, args, ctx, info) {
+ // 1. check if the passwords match
+ if (args.password !== args.confirmPassword) {
+ throw new Error("Yo Passwords don't match!");
+ }
+ // 2. check if its a legit reset token
+ // 3. Check if its expired
+ const [user] = await ctx.db.query.users({
+ where: {
+ resetToken: args.resetToken,
+ resetTokenExpiry_gte: Date.now() - 3600000,
+ },
+ });
+ if (!user) {
+ throw new Error('This token is either invalid or expired!');
+ }
+ // 4. Hash their new password
+ const password = await bcrypt.hash(args.password, 10);
+ // 5. Save the new password to the user and remove old resetToken fields
+ const updatedUser = await ctx.db.mutation.updateUser({
+ where: { email: user.email },
+ data: {
+ password,
+ resetToken: null,
+ resetTokenExpiry: null,
+ },
+ });
+ // 6. Generate JWT
+ const token = jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET);
+ // 7. Set the JWT cookie
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 8. return the new user
+ return updatedUser;
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/30/src/schema.graphql b/stepped-solutions/30/src/schema.graphql
new file mode 100755
index 0000000..d1db1ba
--- /dev/null
+++ b/stepped-solutions/30/src/schema.graphql
@@ -0,0 +1,30 @@
+# import * from './generated/prisma.graphql'
+
+type SuccessMessage {
+ message: String
+}
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+ signin(email: String!, password: String!): User!
+ signout: SuccessMessage
+ requestReset(email: String!): SuccessMessage
+ resetPassword(resetToken: String!, password: String!, confirmPassword: String!): User!
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+}
+
+type User{
+ id: ID!
+ name: String!
+ email: String!
+ permissions: [Permission!]!
+}