summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--finished-application/backend/src/resolvers/Mutation.js3
-rw-r--r--finished-application/frontend/components/RemoveFromCart.js2
-rwxr-xr-xstepped-solutions/40/frontend/components/Cart.js47
-rwxr-xr-xstepped-solutions/40/frontend/components/Header.js74
-rwxr-xr-xstepped-solutions/40/frontend/components/Nav.js45
-rwxr-xr-xstepped-solutions/40/frontend/lib/withData.js42
-rwxr-xr-xstepped-solutions/41/backend/datamodel.graphql37
-rwxr-xr-xstepped-solutions/41/backend/src/generated/prisma.graphql1141
-rwxr-xr-xstepped-solutions/41/backend/src/resolvers/Mutation.js254
-rwxr-xr-xstepped-solutions/41/backend/src/schema.graphql34
-rwxr-xr-xstepped-solutions/41/frontend/components/AddToCart.js29
-rwxr-xr-xstepped-solutions/41/frontend/components/Item.js50
-rwxr-xr-xstepped-solutions/42/frontend/components/AddToCart.js35
-rwxr-xr-xstepped-solutions/42/frontend/components/Cart.js63
-rwxr-xr-xstepped-solutions/42/frontend/components/CartItem.js41
-rwxr-xr-xstepped-solutions/42/frontend/components/User.js38
-rwxr-xr-xstepped-solutions/43/backend/src/resolvers/Mutation.js278
-rwxr-xr-xstepped-solutions/43/backend/src/schema.graphql35
-rwxr-xr-xstepped-solutions/43/frontend/components/CartItem.js43
-rwxr-xr-xstepped-solutions/43/frontend/components/RemoveFromCart.js49
20 files changed, 2338 insertions, 2 deletions
diff --git a/finished-application/backend/src/resolvers/Mutation.js b/finished-application/backend/src/resolvers/Mutation.js
index 2027494..3c11245 100644
--- a/finished-application/backend/src/resolvers/Mutation.js
+++ b/finished-application/backend/src/resolvers/Mutation.js
@@ -204,6 +204,7 @@ const mutations = {
},
});
+ // 2. If there is a cart item, increment it by 1
if (existingCartItem) {
return ctx.db.mutation.updateCartItem(
{
@@ -214,7 +215,7 @@ const mutations = {
);
}
- // Otherwise create a new cartItem
+ // 3. Otherwise create a new cartItem
return ctx.db.mutation.createCartItem(
{
data: {
diff --git a/finished-application/frontend/components/RemoveFromCart.js b/finished-application/frontend/components/RemoveFromCart.js
index d29cbf1..cf5b6fd 100644
--- a/finished-application/frontend/components/RemoveFromCart.js
+++ b/finished-application/frontend/components/RemoveFromCart.js
@@ -44,7 +44,7 @@ class RemoveFromCart extends Component {
update={this.update}
>
{(removeFromCart, { loading }) => (
- <BigButton disabled={loading} title="Remove From Cart" onClick={() => removeFromCart}>
+ <BigButton disabled={loading} title="Remove From Cart" onClick={removeFromCart}>
×
</BigButton>
)}
diff --git a/stepped-solutions/40/frontend/components/Cart.js b/stepped-solutions/40/frontend/components/Cart.js
new file mode 100755
index 0000000..a273b39
--- /dev/null
+++ b/stepped-solutions/40/frontend/components/Cart.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { Query, Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import CartStyles from './styles/CartStyles';
+import Supreme from './styles/Supreme';
+import CloseButton from './styles/CloseButton';
+import SickButton from './styles/SickButton';
+
+const LOCAL_STATE_QUERY = gql`
+ query {
+ cartOpen @client
+ }
+`;
+
+const TOGGLE_CART_MUTATION = gql`
+ mutation {
+ toggleCart @client
+ }
+`;
+
+const Cart = () => (
+ <Mutation mutation={TOGGLE_CART_MUTATION}>
+ {toggleCart => (
+ <Query query={LOCAL_STATE_QUERY}>
+ {({ data }) => (
+ <CartStyles open={data.cartOpen}>
+ <header>
+ <CloseButton onClick={toggleCart} title="close">
+ &times;
+ </CloseButton>
+ <Supreme>Your Cart</Supreme>
+ <p>You Have __ Items in your cart.</p>
+ </header>
+
+ <footer>
+ <p>$10.10</p>
+ <SickButton>Checkout</SickButton>
+ </footer>
+ </CartStyles>
+ )}
+ </Query>
+ )}
+ </Mutation>
+);
+
+export default Cart;
+export { LOCAL_STATE_QUERY, TOGGLE_CART_MUTATION };
diff --git a/stepped-solutions/40/frontend/components/Header.js b/stepped-solutions/40/frontend/components/Header.js
new file mode 100755
index 0000000..5910309
--- /dev/null
+++ b/stepped-solutions/40/frontend/components/Header.js
@@ -0,0 +1,74 @@
+import Link from 'next/link';
+import styled from 'styled-components';
+import NProgress from 'nprogress';
+import Router from 'next/router';
+import Nav from './Nav';
+import Cart from './Cart';
+
+Router.onRouteChangeStart = () => {
+ NProgress.start();
+};
+Router.onRouteChangeComplete = () => {
+ NProgress.done();
+};
+
+Router.onRouteChangeError = () => {
+ NProgress.done();
+};
+
+const Logo = styled.h1`
+ font-size: 4rem;
+ margin-left: 2rem;
+ position: relative;
+ z-index: 2;
+ transform: skew(-7deg);
+ a {
+ padding: 0.5rem 1rem;
+ background: ${props => props.theme.red};
+ color: white;
+ text-transform: uppercase;
+ text-decoration: none;
+ }
+ @media (max-width: 1300px) {
+ margin: 0;
+ text-align: center;
+ }
+`;
+
+const StyledHeader = styled.header`
+ .bar {
+ border-bottom: 10px solid ${props => props.theme.black};
+ display: grid;
+ grid-template-columns: auto 1fr;
+ justify-content: space-between;
+ align-items: stretch;
+ @media (max-width: 1300px) {
+ grid-template-columns: 1fr;
+ justify-content: center;
+ }
+ }
+ .sub-bar {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ }
+`;
+
+const Header = () => (
+ <StyledHeader>
+ <div className="bar">
+ <Logo>
+ <Link href="/">
+ <a>Sick Fits</a>
+ </Link>
+ </Logo>
+ <Nav />
+ </div>
+ <div className="sub-bar">
+ <p>Search</p>
+ </div>
+ <Cart />
+ </StyledHeader>
+);
+
+export default Header;
diff --git a/stepped-solutions/40/frontend/components/Nav.js b/stepped-solutions/40/frontend/components/Nav.js
new file mode 100755
index 0000000..aebbcb2
--- /dev/null
+++ b/stepped-solutions/40/frontend/components/Nav.js
@@ -0,0 +1,45 @@
+import Link from 'next/link';
+import { Mutation } from 'react-apollo';
+import { TOGGLE_CART_MUTATION } from './Cart';
+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 />
+ <Mutation mutation={TOGGLE_CART_MUTATION}>
+ {(toggleCart) => (
+ <button onClick={toggleCart}>My Cart</button>
+ )}
+ </Mutation>
+ </>
+ )}
+ {!me && (
+ <Link href="/signup">
+ <a>Sign In</a>
+ </Link>
+
+ )}
+ </NavStyles>
+ )}
+ </User>
+);
+
+export default Nav;
diff --git a/stepped-solutions/40/frontend/lib/withData.js b/stepped-solutions/40/frontend/lib/withData.js
new file mode 100755
index 0000000..33908d6
--- /dev/null
+++ b/stepped-solutions/40/frontend/lib/withData.js
@@ -0,0 +1,42 @@
+import withApollo from 'next-with-apollo';
+import ApolloClient from 'apollo-boost';
+import { endpoint } from '../config';
+import { LOCAL_STATE_QUERY } from '../components/Cart';
+
+function createClient({ headers }) {
+ return new ApolloClient({
+ uri: process.env.NODE_ENV === 'development' ? endpoint : endpoint,
+ request: operation => {
+ operation.setContext({
+ fetchOptions: {
+ credentials: 'include',
+ },
+ headers,
+ });
+ },
+ // local data
+ clientState: {
+ resolvers: {
+ Mutation: {
+ toggleCart(_, variables, { cache }) {
+ // read the cartOpen value from the cache
+ const { cartOpen } = cache.readQuery({
+ query: LOCAL_STATE_QUERY,
+ });
+ // Write the cart State to the opposite
+ const data = {
+ data: { cartOpen: !cartOpen },
+ };
+ cache.writeData(data);
+ return data;
+ },
+ },
+ },
+ defaults: {
+ cartOpen: true,
+ },
+ },
+ });
+}
+
+export default withApollo(createClient);
diff --git a/stepped-solutions/41/backend/datamodel.graphql b/stepped-solutions/41/backend/datamodel.graphql
new file mode 100755
index 0000000..9d7a8b4
--- /dev/null
+++ b/stepped-solutions/41/backend/datamodel.graphql
@@ -0,0 +1,37 @@
+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]
+ cart: [CartItem!]!
+}
+
+type Item {
+ id: ID! @unique
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+ user: User!
+}
+
+
+type CartItem {
+ id: ID! @unique
+ quantity: Int! @default(value: 1)
+ item: Item! # relationship to Item
+ user: User! # relationship to User
+}
diff --git a/stepped-solutions/41/backend/src/generated/prisma.graphql b/stepped-solutions/41/backend/src/generated/prisma.graphql
new file mode 100755
index 0000000..c9a523f
--- /dev/null
+++ b/stepped-solutions/41/backend/src/generated/prisma.graphql
@@ -0,0 +1,1141 @@
+# source: https://us1.prisma.sh/wesbos/siccccccccck-fits/dev
+# timestamp: Thu Aug 16 2018 12:51:33 GMT-0400 (EDT)
+
+type AggregateCartItem {
+ count: Int!
+}
+
+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 CartItem implements Node {
+ id: ID!
+ quantity: Int!
+ item(where: ItemWhereInput): Item!
+ user(where: UserWhereInput): User!
+}
+
+"""A connection to a list of items."""
+type CartItemConnection {
+ """Information to aid in pagination."""
+ pageInfo: PageInfo!
+
+ """A list of edges."""
+ edges: [CartItemEdge]!
+ aggregate: AggregateCartItem!
+}
+
+input CartItemCreateInput {
+ quantity: Int
+ item: ItemCreateOneInput!
+ user: UserCreateOneWithoutCartInput!
+}
+
+input CartItemCreateManyWithoutUserInput {
+ create: [CartItemCreateWithoutUserInput!]
+ connect: [CartItemWhereUniqueInput!]
+}
+
+input CartItemCreateWithoutUserInput {
+ quantity: Int
+ item: ItemCreateOneInput!
+}
+
+"""An edge in a connection."""
+type CartItemEdge {
+ """The item at the end of the edge."""
+ node: CartItem!
+
+ """A cursor for use in pagination."""
+ cursor: String!
+}
+
+enum CartItemOrderByInput {
+ id_ASC
+ id_DESC
+ quantity_ASC
+ quantity_DESC
+ updatedAt_ASC
+ updatedAt_DESC
+ createdAt_ASC
+ createdAt_DESC
+}
+
+type CartItemPreviousValues {
+ id: ID!
+ quantity: Int!
+}
+
+type CartItemSubscriptionPayload {
+ mutation: MutationType!
+ node: CartItem
+ updatedFields: [String!]
+ previousValues: CartItemPreviousValues
+}
+
+input CartItemSubscriptionWhereInput {
+ """Logical AND on all given filters."""
+ AND: [CartItemSubscriptionWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [CartItemSubscriptionWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [CartItemSubscriptionWhereInput!]
+
+ """
+ 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: CartItemWhereInput
+}
+
+input CartItemUpdateInput {
+ quantity: Int
+ item: ItemUpdateOneInput
+ user: UserUpdateOneWithoutCartInput
+}
+
+input CartItemUpdateManyWithoutUserInput {
+ create: [CartItemCreateWithoutUserInput!]
+ connect: [CartItemWhereUniqueInput!]
+ disconnect: [CartItemWhereUniqueInput!]
+ delete: [CartItemWhereUniqueInput!]
+ update: [CartItemUpdateWithWhereUniqueWithoutUserInput!]
+ upsert: [CartItemUpsertWithWhereUniqueWithoutUserInput!]
+}
+
+input CartItemUpdateWithoutUserDataInput {
+ quantity: Int
+ item: ItemUpdateOneInput
+}
+
+input CartItemUpdateWithWhereUniqueWithoutUserInput {
+ where: CartItemWhereUniqueInput!
+ data: CartItemUpdateWithoutUserDataInput!
+}
+
+input CartItemUpsertWithWhereUniqueWithoutUserInput {
+ where: CartItemWhereUniqueInput!
+ update: CartItemUpdateWithoutUserDataInput!
+ create: CartItemCreateWithoutUserInput!
+}
+
+input CartItemWhereInput {
+ """Logical AND on all given filters."""
+ AND: [CartItemWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [CartItemWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [CartItemWhereInput!]
+ 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
+ quantity: Int
+
+ """All values that are not equal to given value."""
+ quantity_not: Int
+
+ """All values that are contained in given list."""
+ quantity_in: [Int!]
+
+ """All values that are not contained in given list."""
+ quantity_not_in: [Int!]
+
+ """All values less than the given value."""
+ quantity_lt: Int
+
+ """All values less than or equal the given value."""
+ quantity_lte: Int
+
+ """All values greater than the given value."""
+ quantity_gt: Int
+
+ """All values greater than or equal the given value."""
+ quantity_gte: Int
+ item: ItemWhereInput
+ user: UserWhereInput
+}
+
+input CartItemWhereUniqueInput {
+ id: ID
+}
+
+type Item implements Node {
+ id: ID!
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+ user(where: UserWhereInput): User!
+}
+
+"""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!
+ user: UserCreateOneInput!
+}
+
+input ItemCreateOneInput {
+ create: ItemCreateInput
+ connect: ItemWhereUniqueInput
+}
+
+"""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 ItemUpdateDataInput {
+ title: String
+ description: String
+ image: String
+ largeImage: String
+ price: Int
+ user: UserUpdateOneInput
+}
+
+input ItemUpdateInput {
+ title: String
+ description: String
+ image: String
+ largeImage: String
+ price: Int
+ user: UserUpdateOneInput
+}
+
+input ItemUpdateOneInput {
+ create: ItemCreateInput
+ connect: ItemWhereUniqueInput
+ delete: Boolean
+ update: ItemUpdateDataInput
+ upsert: ItemUpsertNestedInput
+}
+
+input ItemUpsertNestedInput {
+ update: ItemUpdateDataInput!
+ create: ItemCreateInput!
+}
+
+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
+ user: UserWhereInput
+}
+
+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 {
+ createCartItem(data: CartItemCreateInput!): CartItem!
+ createItem(data: ItemCreateInput!): Item!
+ createUser(data: UserCreateInput!): User!
+ updateCartItem(data: CartItemUpdateInput!, where: CartItemWhereUniqueInput!): CartItem
+ updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item
+ updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
+ deleteCartItem(where: CartItemWhereUniqueInput!): CartItem
+ deleteItem(where: ItemWhereUniqueInput!): Item
+ deleteUser(where: UserWhereUniqueInput!): User
+ upsertCartItem(where: CartItemWhereUniqueInput!, create: CartItemCreateInput!, update: CartItemUpdateInput!): CartItem!
+ upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item!
+ upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
+ updateManyCartItems(data: CartItemUpdateInput!, where: CartItemWhereInput): BatchPayload!
+ updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput): BatchPayload!
+ updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload!
+ deleteManyCartItems(where: CartItemWhereInput): BatchPayload!
+ deleteManyItems(where: ItemWhereInput): BatchPayload!
+ deleteManyUsers(where: UserWhereInput): 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 {
+ cartItems(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [CartItem]!
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]!
+ users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
+ cartItem(where: CartItemWhereUniqueInput!): CartItem
+ item(where: ItemWhereUniqueInput!): Item
+ user(where: UserWhereUniqueInput!): User
+ cartItemsConnection(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): CartItemConnection!
+ itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection!
+ usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
+
+ """Fetches an object given its ID"""
+ node(
+ """The ID of an object"""
+ id: ID!
+ ): Node
+}
+
+type Subscription {
+ cartItem(where: CartItemSubscriptionWhereInput): CartItemSubscriptionPayload
+ item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload
+ user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
+}
+
+type User implements Node {
+ id: ID!
+ name: String!
+ email: String!
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: [Permission!]!
+ cart(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [CartItem!]
+}
+
+"""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
+ cart: CartItemCreateManyWithoutUserInput
+}
+
+input UserCreateOneInput {
+ create: UserCreateInput
+ connect: UserWhereUniqueInput
+}
+
+input UserCreateOneWithoutCartInput {
+ create: UserCreateWithoutCartInput
+ connect: UserWhereUniqueInput
+}
+
+input UserCreatepermissionsInput {
+ set: [Permission!]
+}
+
+input UserCreateWithoutCartInput {
+ name: String!
+ email: String!
+ password: String!
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserCreatepermissionsInput
+}
+
+"""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 UserUpdateDataInput {
+ name: String
+ email: String
+ password: String
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserUpdatepermissionsInput
+ cart: CartItemUpdateManyWithoutUserInput
+}
+
+input UserUpdateInput {
+ name: String
+ email: String
+ password: String
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserUpdatepermissionsInput
+ cart: CartItemUpdateManyWithoutUserInput
+}
+
+input UserUpdateOneInput {
+ create: UserCreateInput
+ connect: UserWhereUniqueInput
+ delete: Boolean
+ update: UserUpdateDataInput
+ upsert: UserUpsertNestedInput
+}
+
+input UserUpdateOneWithoutCartInput {
+ create: UserCreateWithoutCartInput
+ connect: UserWhereUniqueInput
+ delete: Boolean
+ update: UserUpdateWithoutCartDataInput
+ upsert: UserUpsertWithoutCartInput
+}
+
+input UserUpdatepermissionsInput {
+ set: [Permission!]
+}
+
+input UserUpdateWithoutCartDataInput {
+ name: String
+ email: String
+ password: String
+ resetToken: String
+ resetTokenExpiry: String
+ permissions: UserUpdatepermissionsInput
+}
+
+input UserUpsertNestedInput {
+ update: UserUpdateDataInput!
+ create: UserCreateInput!
+}
+
+input UserUpsertWithoutCartInput {
+ update: UserUpdateWithoutCartDataInput!
+ create: UserCreateWithoutCartInput!
+}
+
+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
+ cart_every: CartItemWhereInput
+ cart_some: CartItemWhereInput
+ cart_none: CartItemWhereInput
+}
+
+input UserWhereUniqueInput {
+ id: ID
+ email: String
+}
diff --git a/stepped-solutions/41/backend/src/resolvers/Mutation.js b/stepped-solutions/41/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..5c16172
--- /dev/null
+++ b/stepped-solutions/41/backend/src/resolvers/Mutation.js
@@ -0,0 +1,254 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const { randomBytes } = require('crypto');
+const { promisify } = require('util');
+const { transport, makeANiceEmail } = require('../mail');
+const { hasPermission } = require('../utils');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in to do that!');
+ }
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ // This is how to create a relationship between the Item and the User
+ user: {
+ connect: {
+ id: ctx.request.userId,
+ },
+ },
+ ...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 user { id }}`);
+ // 2. Check if they own that item, or have the permissions
+ const ownsItem = item.user.id === ctx.request.userId;
+ const hasPermissions = ctx.request.user.permissions.some(permission =>
+ ['ADMIN', 'ITEMDELETE'].includes(permission)
+ );
+
+ if (!ownsItem && hasPermissions) {
+ throw new Error("You don't have permission to do that!");
+ }
+
+ // 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 },
+ });
+ // 3. Email them that reset token
+ const mailRes = await transport.sendMail({
+ from: 'wes@wesbos.com',
+ to: user.email,
+ subject: 'Your Password Reset Token',
+ html: makeANiceEmail(`Your Password Reset Token is here!
+ \n\n
+ <a href="${process.env
+ .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`),
+ });
+
+ // 4. Return the message
+ return { message: 'Thanks!' };
+ },
+ 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;
+ },
+ async updatePermissions(parent, args, ctx, info) {
+ // 1. Check if they are logged in
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in!');
+ }
+ // 2. Query the current user
+ const currentUser = await ctx.db.query.user(
+ {
+ where: {
+ id: ctx.request.userId,
+ },
+ },
+ info
+ );
+ // 3. Check if they have permissions to do this
+ hasPermission(currentUser, ['ADMIN', 'PERMISSIONUPDATE']);
+ // 4. Update the permissions
+ return ctx.db.mutation.updateUser(
+ {
+ data: {
+ permissions: {
+ set: args.permissions,
+ },
+ },
+ where: {
+ id: args.userId,
+ },
+ },
+ info
+ );
+ },
+ async addToCart(parent, args, ctx, info) {
+ // 1. Make sure they are signed in
+ const { userId } = ctx.request;
+ if (!userId) {
+ throw new Error('You must be signed in soooon');
+ }
+ // 2. Query the users current cart
+ const [existingCartItem] = await ctx.db.query.cartItems({
+ where: {
+ user: { id: userId },
+ item: { id: args.id },
+ },
+ });
+ // 3. Check if that item is already in their cart and increment by 1 if it is
+ if (existingCartItem) {
+ console.log('This item is already in their cart');
+ return ctx.db.mutation.updateCartItem(
+ {
+ where: { id: existingCartItem.id },
+ data: { quantity: existingCartItem.quantity + 1 },
+ },
+ info
+ );
+ }
+ // 4. If its not, create a fresh CartItem for that user!
+ return ctx.db.mutation.createCartItem(
+ {
+ data: {
+ user: {
+ connect: { id: userId },
+ },
+ item: {
+ connect: { id: args.id },
+ },
+ },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/41/backend/src/schema.graphql b/stepped-solutions/41/backend/src/schema.graphql
new file mode 100755
index 0000000..864396c
--- /dev/null
+++ b/stepped-solutions/41/backend/src/schema.graphql
@@ -0,0 +1,34 @@
+# 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!
+ updatePermissions(permissions: [Permission], userId: ID!): User
+ addToCart(id: ID!): CartItem
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+ users: [User]!
+}
+
+type User{
+ id: ID!
+ name: String!
+ email: String!
+ permissions: [Permission!]!
+ cart: [CartItem!]!
+}
diff --git a/stepped-solutions/41/frontend/components/AddToCart.js b/stepped-solutions/41/frontend/components/AddToCart.js
new file mode 100755
index 0000000..0625a9c
--- /dev/null
+++ b/stepped-solutions/41/frontend/components/AddToCart.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+
+const ADD_TO_CART_MUTATION = gql`
+ mutation addToCart($id: ID!) {
+ addToCart(id: $id) {
+ id
+ quantity
+ }
+ }
+`;
+
+class AddToCart extends React.Component {
+ render() {
+ const { id } = this.props;
+ return (
+ <Mutation
+ mutation={ADD_TO_CART_MUTATION}
+ variables={{
+ id,
+ }}
+ >
+ {addToCart => <button onClick={addToCart}>Add To Cart 🛒</button>}
+ </Mutation>
+ );
+ }
+}
+export default AddToCart;
diff --git a/stepped-solutions/41/frontend/components/Item.js b/stepped-solutions/41/frontend/components/Item.js
new file mode 100755
index 0000000..2741dcf
--- /dev/null
+++ b/stepped-solutions/41/frontend/components/Item.js
@@ -0,0 +1,50 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import Title from './styles/Title';
+import ItemStyles from './styles/ItemStyles';
+import PriceTag from './styles/PriceTag';
+import formatMoney from '../lib/formatMoney';
+import DeleteItem from './DeleteItem';
+import AddToCart from './AddToCart';
+
+export default class Item extends Component {
+ static propTypes = {
+ item: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const { item } = this.props;
+ return (
+ <ItemStyles>
+ {item.image && <img src={item.image} alt={item.title} />}
+
+ <Title>
+ <Link
+ href={{
+ pathname: '/item',
+ query: { id: item.id },
+ }}
+ >
+ <a>{item.title}</a>
+ </Link>
+ </Title>
+ <PriceTag>{formatMoney(item.price)}</PriceTag>
+ <p>{item.description}</p>
+
+ <div className="buttonList">
+ <Link
+ href={{
+ pathname: 'update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+ <AddToCart id={item.id} />
+ <DeleteItem id={item.id}>Delete This Item</DeleteItem>
+ </div>
+ </ItemStyles>
+ );
+ }
+}
diff --git a/stepped-solutions/42/frontend/components/AddToCart.js b/stepped-solutions/42/frontend/components/AddToCart.js
new file mode 100755
index 0000000..3e2842b
--- /dev/null
+++ b/stepped-solutions/42/frontend/components/AddToCart.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+
+const ADD_TO_CART_MUTATION = gql`
+ mutation addToCart($id: ID!) {
+ addToCart(id: $id) {
+ id
+ quantity
+ }
+ }
+`;
+
+class AddToCart extends React.Component {
+ render() {
+ const { id } = this.props;
+ return (
+ <Mutation
+ mutation={ADD_TO_CART_MUTATION}
+ variables={{
+ id,
+ }}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(addToCart, { loading }) => (
+ <button disabled={loading} onClick={addToCart}>
+ Add{loading && 'ing'} To Cart 🛒
+ </button>
+ )}
+ </Mutation>
+ );
+ }
+}
+export default AddToCart;
diff --git a/stepped-solutions/42/frontend/components/Cart.js b/stepped-solutions/42/frontend/components/Cart.js
new file mode 100755
index 0000000..1895235
--- /dev/null
+++ b/stepped-solutions/42/frontend/components/Cart.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import { Query, Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import User from './User';
+import CartStyles from './styles/CartStyles';
+import Supreme from './styles/Supreme';
+import CloseButton from './styles/CloseButton';
+import SickButton from './styles/SickButton';
+import CartItem from './CartItem';
+import calcTotalPrice from '../lib/calcTotalPrice';
+import formatMoney from '../lib/formatMoney';
+
+const LOCAL_STATE_QUERY = gql`
+ query {
+ cartOpen @client
+ }
+`;
+
+const TOGGLE_CART_MUTATION = gql`
+ mutation {
+ toggleCart @client
+ }
+`;
+
+const Cart = () => (
+ <User>
+ {({ data: { me } }) => {
+ if (!me) return null;
+ console.log(me);
+ return (
+ <Mutation mutation={TOGGLE_CART_MUTATION}>
+ {toggleCart => (
+ <Query query={LOCAL_STATE_QUERY}>
+ {({ data }) => (
+ <CartStyles open={data.cartOpen}>
+ <header>
+ <CloseButton onClick={toggleCart} title="close">
+ &times;
+ </CloseButton>
+ <Supreme>{me.name}'s Cart</Supreme>
+ <p>
+ You Have {me.cart.length} Item{me.cart.length === 1 ? '' : 's'} in your cart.
+ </p>
+ </header>
+ <ul>
+ {me.cart.map(cartItem => <CartItem key={cartItem.id} cartItem={cartItem} />)}
+ </ul>
+ <footer>
+ <p>{formatMoney(calcTotalPrice(me.cart))}</p>
+ <SickButton>Checkout</SickButton>
+ </footer>
+ </CartStyles>
+ )}
+ </Query>
+ )}
+ </Mutation>
+ );
+ }}
+ </User>
+);
+
+export default Cart;
+export { LOCAL_STATE_QUERY, TOGGLE_CART_MUTATION };
diff --git a/stepped-solutions/42/frontend/components/CartItem.js b/stepped-solutions/42/frontend/components/CartItem.js
new file mode 100755
index 0000000..8f6a099
--- /dev/null
+++ b/stepped-solutions/42/frontend/components/CartItem.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import formatMoney from '../lib/formatMoney';
+
+const CartItemStyles = styled.li`
+ padding: 1rem 0;
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ display: grid;
+ align-items: center;
+ grid-template-columns: auto 1fr auto;
+ img {
+ margin-right: 10px;
+ }
+ h3,
+ p {
+ margin: 0;
+ }
+`;
+
+const CartItem = ({ cartItem }) => (
+ <CartItemStyles>
+ <img width="100" src={cartItem.item.image} alt={cartItem.item.title} />
+ <div className="cart-item-details">
+ <h3>{cartItem.item.title}</h3>
+ <p>
+ {formatMoney(cartItem.item.price * cartItem.quantity)}
+ {' - '}
+ <em>
+ {cartItem.quantity} &times; {formatMoney(cartItem.item.price)} each
+ </em>
+ </p>
+ </div>
+ </CartItemStyles>
+);
+
+CartItem.propTypes = {
+ cartItem: PropTypes.object.isRequired,
+};
+
+export default CartItem;
diff --git a/stepped-solutions/42/frontend/components/User.js b/stepped-solutions/42/frontend/components/User.js
new file mode 100755
index 0000000..429bb25
--- /dev/null
+++ b/stepped-solutions/42/frontend/components/User.js
@@ -0,0 +1,38 @@
+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
+ cart {
+ id
+ quantity
+ item {
+ id
+ price
+ image
+ title
+ description
+ }
+ }
+ }
+ }
+`;
+
+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/43/backend/src/resolvers/Mutation.js b/stepped-solutions/43/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..5586edf
--- /dev/null
+++ b/stepped-solutions/43/backend/src/resolvers/Mutation.js
@@ -0,0 +1,278 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const { randomBytes } = require('crypto');
+const { promisify } = require('util');
+const { transport, makeANiceEmail } = require('../mail');
+const { hasPermission } = require('../utils');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in to do that!');
+ }
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ // This is how to create a relationship between the Item and the User
+ user: {
+ connect: {
+ id: ctx.request.userId,
+ },
+ },
+ ...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 user { id }}`);
+ // 2. Check if they own that item, or have the permissions
+ const ownsItem = item.user.id === ctx.request.userId;
+ const hasPermissions = ctx.request.user.permissions.some(permission =>
+ ['ADMIN', 'ITEMDELETE'].includes(permission)
+ );
+
+ if (!ownsItem && hasPermissions) {
+ throw new Error("You don't have permission to do that!");
+ }
+
+ // 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 },
+ });
+ // 3. Email them that reset token
+ const mailRes = await transport.sendMail({
+ from: 'wes@wesbos.com',
+ to: user.email,
+ subject: 'Your Password Reset Token',
+ html: makeANiceEmail(`Your Password Reset Token is here!
+ \n\n
+ <a href="${process.env
+ .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`),
+ });
+
+ // 4. Return the message
+ return { message: 'Thanks!' };
+ },
+ 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;
+ },
+ async updatePermissions(parent, args, ctx, info) {
+ // 1. Check if they are logged in
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in!');
+ }
+ // 2. Query the current user
+ const currentUser = await ctx.db.query.user(
+ {
+ where: {
+ id: ctx.request.userId,
+ },
+ },
+ info
+ );
+ // 3. Check if they have permissions to do this
+ hasPermission(currentUser, ['ADMIN', 'PERMISSIONUPDATE']);
+ // 4. Update the permissions
+ return ctx.db.mutation.updateUser(
+ {
+ data: {
+ permissions: {
+ set: args.permissions,
+ },
+ },
+ where: {
+ id: args.userId,
+ },
+ },
+ info
+ );
+ },
+ async addToCart(parent, args, ctx, info) {
+ // 1. Make sure they are signed in
+ const { userId } = ctx.request;
+ if (!userId) {
+ throw new Error('You must be signed in soooon');
+ }
+ // 2. Query the users current cart
+ const [existingCartItem] = await ctx.db.query.cartItems({
+ where: {
+ user: { id: userId },
+ item: { id: args.id },
+ },
+ });
+ // 3. Check if that item is already in their cart and increment by 1 if it is
+ if (existingCartItem) {
+ console.log('This item is already in their cart');
+ return ctx.db.mutation.updateCartItem(
+ {
+ where: { id: existingCartItem.id },
+ data: { quantity: existingCartItem.quantity + 1 },
+ },
+ info
+ );
+ }
+ // 4. If its not, create a fresh CartItem for that user!
+ return ctx.db.mutation.createCartItem(
+ {
+ data: {
+ user: {
+ connect: { id: userId },
+ },
+ item: {
+ connect: { id: args.id },
+ },
+ },
+ },
+ info
+ );
+ },
+ async removeFromCart(parent, args, ctx, info) {
+ // 1. Find the cart item
+ const cartItem = await ctx.db.query.cartItem(
+ {
+ where: {
+ id: args.id,
+ },
+ },
+ `{ id, user { id }}`
+ );
+ // 1.5 Make sure we found an item
+ if (!cartItem) throw new Error('No CartItem Found!');
+ // 2. Make sure they own that cart item
+ if (cartItem.user.id !== ctx.request.userId) {
+ throw new Error('Cheatin huhhhh');
+ }
+ // 3. Delete that cart item
+ return ctx.db.mutation.deleteCartItem(
+ {
+ where: { id: args.id },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/43/backend/src/schema.graphql b/stepped-solutions/43/backend/src/schema.graphql
new file mode 100755
index 0000000..4d5efb4
--- /dev/null
+++ b/stepped-solutions/43/backend/src/schema.graphql
@@ -0,0 +1,35 @@
+# 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!
+ updatePermissions(permissions: [Permission], userId: ID!): User
+ addToCart(id: ID!): CartItem
+ removeFromCart(id: ID!): CartItem
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+ users: [User]!
+}
+
+type User{
+ id: ID!
+ name: String!
+ email: String!
+ permissions: [Permission!]!
+ cart: [CartItem!]!
+}
diff --git a/stepped-solutions/43/frontend/components/CartItem.js b/stepped-solutions/43/frontend/components/CartItem.js
new file mode 100755
index 0000000..1be2128
--- /dev/null
+++ b/stepped-solutions/43/frontend/components/CartItem.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import formatMoney from '../lib/formatMoney';
+import RemoveFromCart from './RemoveFromCart';
+
+const CartItemStyles = styled.li`
+ padding: 1rem 0;
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ display: grid;
+ align-items: center;
+ grid-template-columns: auto 1fr auto;
+ img {
+ margin-right: 10px;
+ }
+ h3,
+ p {
+ margin: 0;
+ }
+`;
+
+const CartItem = ({ cartItem }) => (
+ <CartItemStyles>
+ <img width="100" src={cartItem.item.image} alt={cartItem.item.title} />
+ <div className="cart-item-details">
+ <h3>{cartItem.item.title}</h3>
+ <p>
+ {formatMoney(cartItem.item.price * cartItem.quantity)}
+ {' - '}
+ <em>
+ {cartItem.quantity} &times; {formatMoney(cartItem.item.price)} each
+ </em>
+ </p>
+ </div>
+ <RemoveFromCart id={cartItem.id} />
+ </CartItemStyles>
+);
+
+CartItem.propTypes = {
+ cartItem: PropTypes.object.isRequired,
+};
+
+export default CartItem;
diff --git a/stepped-solutions/43/frontend/components/RemoveFromCart.js b/stepped-solutions/43/frontend/components/RemoveFromCart.js
new file mode 100755
index 0000000..b716e92
--- /dev/null
+++ b/stepped-solutions/43/frontend/components/RemoveFromCart.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+
+const REMOVE_FROM_CART_MUTATION = gql`
+ mutation removeFromCart($id: ID!) {
+ removeFromCart(id: $id) {
+ id
+ }
+ }
+`;
+
+const BigButton = styled.button`
+ font-size: 3rem;
+ background: none;
+ border: 0;
+ &:hover {
+ color: ${props => props.theme.red};
+ cursor: pointer;
+ }
+`;
+
+class RemoveFromCart extends React.Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+ render() {
+ return (
+ <Mutation mutation={REMOVE_FROM_CART_MUTATION} variables={{ id: this.props.id }}>
+ {(removeFromCart, { loading, error }) => (
+ <BigButton
+ disabled={loading}
+ onClick={() => {
+ removeFromCart().catch(err => alert(err.message));
+ }}
+ title="Delete Item"
+ >
+ &times;
+ </BigButton>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default RemoveFromCart;