diff options
| author | Wes Bos <wesbos@gmail.com> | 2018-09-07 12:15:53 -0400 |
|---|---|---|
| committer | Wes Bos <wesbos@gmail.com> | 2018-09-07 12:15:53 -0400 |
| commit | 093c172d16732ba8d01faa313e0af99d26902205 (patch) | |
| tree | 84838c4f6410b43f42b56219190a62e14e072ef3 /stepped-solutions | |
| parent | 9876e7dfca6fa2bbf6ac383eeb8e2c82e05c2164 (diff) | |
getting there
Diffstat (limited to 'stepped-solutions')
24 files changed, 3014 insertions, 0 deletions
diff --git a/stepped-solutions/53/backend/datamodel.graphql b/stepped-solutions/53/backend/datamodel.graphql new file mode 100755 index 0000000..ce9ae38 --- /dev/null +++ b/stepped-solutions/53/backend/datamodel.graphql @@ -0,0 +1,57 @@ +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 +} + +type OrderItem { + id: ID! @unique + title: String! + description: String! + image: String! + largeImage: String! + price: Int! + quantity: Int! @default(value: 1) + user: User +} + +type Order { + id: ID! @unique + items: [OrderItem!]! + total: Int! + user: User! + charge: String! + createdAt: DateTime! + updatedAt: DateTime! +} diff --git a/stepped-solutions/53/backend/src/generated/prisma.graphql b/stepped-solutions/53/backend/src/generated/prisma.graphql new file mode 100755 index 0000000..067c87e --- /dev/null +++ b/stepped-solutions/53/backend/src/generated/prisma.graphql @@ -0,0 +1,1855 @@ +# source: https://us1.prisma.sh/wesbos/siccccccccck-fits/dev +# timestamp: Fri Aug 24 2018 14:33:54 GMT-0400 (EDT) + +type AggregateCartItem { + count: Int! +} + +type AggregateItem { + count: Int! +} + +type AggregateOrder { + count: Int! +} + +type AggregateOrderItem { + 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 +} + +scalar DateTime + +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 + disconnect: Boolean + 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! + createOrder(data: OrderCreateInput!): Order! + createItem(data: ItemCreateInput!): Item! + createOrderItem(data: OrderItemCreateInput!): OrderItem! + createUser(data: UserCreateInput!): User! + updateCartItem(data: CartItemUpdateInput!, where: CartItemWhereUniqueInput!): CartItem + updateOrder(data: OrderUpdateInput!, where: OrderWhereUniqueInput!): Order + updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item + updateOrderItem(data: OrderItemUpdateInput!, where: OrderItemWhereUniqueInput!): OrderItem + updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User + deleteCartItem(where: CartItemWhereUniqueInput!): CartItem + deleteOrder(where: OrderWhereUniqueInput!): Order + deleteItem(where: ItemWhereUniqueInput!): Item + deleteOrderItem(where: OrderItemWhereUniqueInput!): OrderItem + deleteUser(where: UserWhereUniqueInput!): User + upsertCartItem(where: CartItemWhereUniqueInput!, create: CartItemCreateInput!, update: CartItemUpdateInput!): CartItem! + upsertOrder(where: OrderWhereUniqueInput!, create: OrderCreateInput!, update: OrderUpdateInput!): Order! + upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item! + upsertOrderItem(where: OrderItemWhereUniqueInput!, create: OrderItemCreateInput!, update: OrderItemUpdateInput!): OrderItem! + upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! + updateManyCartItems(data: CartItemUpdateInput!, where: CartItemWhereInput): BatchPayload! + updateManyOrders(data: OrderUpdateInput!, where: OrderWhereInput): BatchPayload! + updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput): BatchPayload! + updateManyOrderItems(data: OrderItemUpdateInput!, where: OrderItemWhereInput): BatchPayload! + updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload! + deleteManyCartItems(where: CartItemWhereInput): BatchPayload! + deleteManyOrders(where: OrderWhereInput): BatchPayload! + deleteManyItems(where: ItemWhereInput): BatchPayload! + deleteManyOrderItems(where: OrderItemWhereInput): BatchPayload! + deleteManyUsers(where: UserWhereInput): BatchPayload! +} + +enum MutationType { + CREATED + UPDATED + DELETED +} + +"""An object with an ID""" +interface Node { + """The id of the object.""" + id: ID! +} + +type Order implements Node { + id: ID! + items(where: OrderItemWhereInput, orderBy: OrderItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [OrderItem!] + total: Int! + user(where: UserWhereInput): User! + charge: String! + createdAt: DateTime! + updatedAt: DateTime! +} + +"""A connection to a list of items.""" +type OrderConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [OrderEdge]! + aggregate: AggregateOrder! +} + +input OrderCreateInput { + total: Int! + charge: String! + items: OrderItemCreateManyInput + user: UserCreateOneInput! +} + +"""An edge in a connection.""" +type OrderEdge { + """The item at the end of the edge.""" + node: Order! + + """A cursor for use in pagination.""" + cursor: String! +} + +type OrderItem implements Node { + id: ID! + title: String! + description: String! + image: String! + largeImage: String! + price: Int! + quantity: Int! + user(where: UserWhereInput): User +} + +"""A connection to a list of items.""" +type OrderItemConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [OrderItemEdge]! + aggregate: AggregateOrderItem! +} + +input OrderItemCreateInput { + title: String! + description: String! + image: String! + largeImage: String! + price: Int! + quantity: Int + user: UserCreateOneInput +} + +input OrderItemCreateManyInput { + create: [OrderItemCreateInput!] + connect: [OrderItemWhereUniqueInput!] +} + +"""An edge in a connection.""" +type OrderItemEdge { + """The item at the end of the edge.""" + node: OrderItem! + + """A cursor for use in pagination.""" + cursor: String! +} + +enum OrderItemOrderByInput { + id_ASC + id_DESC + title_ASC + title_DESC + description_ASC + description_DESC + image_ASC + image_DESC + largeImage_ASC + largeImage_DESC + price_ASC + price_DESC + quantity_ASC + quantity_DESC + updatedAt_ASC + updatedAt_DESC + createdAt_ASC + createdAt_DESC +} + +type OrderItemPreviousValues { + id: ID! + title: String! + description: String! + image: String! + largeImage: String! + price: Int! + quantity: Int! +} + +type OrderItemSubscriptionPayload { + mutation: MutationType! + node: OrderItem + updatedFields: [String!] + previousValues: OrderItemPreviousValues +} + +input OrderItemSubscriptionWhereInput { + """Logical AND on all given filters.""" + AND: [OrderItemSubscriptionWhereInput!] + + """Logical OR on all given filters.""" + OR: [OrderItemSubscriptionWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [OrderItemSubscriptionWhereInput!] + + """ + 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: OrderItemWhereInput +} + +input OrderItemUpdateDataInput { + title: String + description: String + image: String + largeImage: String + price: Int + quantity: Int + user: UserUpdateOneInput +} + +input OrderItemUpdateInput { + title: String + description: String + image: String + largeImage: String + price: Int + quantity: Int + user: UserUpdateOneInput +} + +input OrderItemUpdateManyInput { + create: [OrderItemCreateInput!] + connect: [OrderItemWhereUniqueInput!] + disconnect: [OrderItemWhereUniqueInput!] + delete: [OrderItemWhereUniqueInput!] + update: [OrderItemUpdateWithWhereUniqueNestedInput!] + upsert: [OrderItemUpsertWithWhereUniqueNestedInput!] +} + +input OrderItemUpdateWithWhereUniqueNestedInput { + where: OrderItemWhereUniqueInput! + data: OrderItemUpdateDataInput! +} + +input OrderItemUpsertWithWhereUniqueNestedInput { + where: OrderItemWhereUniqueInput! + update: OrderItemUpdateDataInput! + create: OrderItemCreateInput! +} + +input OrderItemWhereInput { + """Logical AND on all given filters.""" + AND: [OrderItemWhereInput!] + + """Logical OR on all given filters.""" + OR: [OrderItemWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [OrderItemWhereInput!] + 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 + 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 + user: UserWhereInput +} + +input OrderItemWhereUniqueInput { + id: ID +} + +enum OrderOrderByInput { + id_ASC + id_DESC + total_ASC + total_DESC + charge_ASC + charge_DESC + createdAt_ASC + createdAt_DESC + updatedAt_ASC + updatedAt_DESC +} + +type OrderPreviousValues { + id: ID! + total: Int! + charge: String! + createdAt: DateTime! + updatedAt: DateTime! +} + +type OrderSubscriptionPayload { + mutation: MutationType! + node: Order + updatedFields: [String!] + previousValues: OrderPreviousValues +} + +input OrderSubscriptionWhereInput { + """Logical AND on all given filters.""" + AND: [OrderSubscriptionWhereInput!] + + """Logical OR on all given filters.""" + OR: [OrderSubscriptionWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [OrderSubscriptionWhereInput!] + + """ + 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: OrderWhereInput +} + +input OrderUpdateInput { + total: Int + charge: String + items: OrderItemUpdateManyInput + user: UserUpdateOneInput +} + +input OrderWhereInput { + """Logical AND on all given filters.""" + AND: [OrderWhereInput!] + + """Logical OR on all given filters.""" + OR: [OrderWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [OrderWhereInput!] + 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 + total: Int + + """All values that are not equal to given value.""" + total_not: Int + + """All values that are contained in given list.""" + total_in: [Int!] + + """All values that are not contained in given list.""" + total_not_in: [Int!] + + """All values less than the given value.""" + total_lt: Int + + """All values less than or equal the given value.""" + total_lte: Int + + """All values greater than the given value.""" + total_gt: Int + + """All values greater than or equal the given value.""" + total_gte: Int + charge: String + + """All values that are not equal to given value.""" + charge_not: String + + """All values that are contained in given list.""" + charge_in: [String!] + + """All values that are not contained in given list.""" + charge_not_in: [String!] + + """All values less than the given value.""" + charge_lt: String + + """All values less than or equal the given value.""" + charge_lte: String + + """All values greater than the given value.""" + charge_gt: String + + """All values greater than or equal the given value.""" + charge_gte: String + + """All values containing the given string.""" + charge_contains: String + + """All values not containing the given string.""" + charge_not_contains: String + + """All values starting with the given string.""" + charge_starts_with: String + + """All values not starting with the given string.""" + charge_not_starts_with: String + + """All values ending with the given string.""" + charge_ends_with: String + + """All values not ending with the given string.""" + charge_not_ends_with: String + createdAt: DateTime + + """All values that are not equal to given value.""" + createdAt_not: DateTime + + """All values that are contained in given list.""" + createdAt_in: [DateTime!] + + """All values that are not contained in given list.""" + createdAt_not_in: [DateTime!] + + """All values less than the given value.""" + createdAt_lt: DateTime + + """All values less than or equal the given value.""" + createdAt_lte: DateTime + + """All values greater than the given value.""" + createdAt_gt: DateTime + + """All values greater than or equal the given value.""" + createdAt_gte: DateTime + updatedAt: DateTime + + """All values that are not equal to given value.""" + updatedAt_not: DateTime + + """All values that are contained in given list.""" + updatedAt_in: [DateTime!] + + """All values that are not contained in given list.""" + updatedAt_not_in: [DateTime!] + + """All values less than the given value.""" + updatedAt_lt: DateTime + + """All values less than or equal the given value.""" + updatedAt_lte: DateTime + + """All values greater than the given value.""" + updatedAt_gt: DateTime + + """All values greater than or equal the given value.""" + updatedAt_gte: DateTime + items_every: OrderItemWhereInput + items_some: OrderItemWhereInput + items_none: OrderItemWhereInput + user: UserWhereInput +} + +input OrderWhereUniqueInput { + 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]! + orders(where: OrderWhereInput, orderBy: OrderOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Order]! + items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]! + orderItems(where: OrderItemWhereInput, orderBy: OrderItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [OrderItem]! + users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! + cartItem(where: CartItemWhereUniqueInput!): CartItem + order(where: OrderWhereUniqueInput!): Order + item(where: ItemWhereUniqueInput!): Item + orderItem(where: OrderItemWhereUniqueInput!): OrderItem + user(where: UserWhereUniqueInput!): User + cartItemsConnection(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): CartItemConnection! + ordersConnection(where: OrderWhereInput, orderBy: OrderOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): OrderConnection! + itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection! + orderItemsConnection(where: OrderItemWhereInput, orderBy: OrderItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): OrderItemConnection! + 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 + order(where: OrderSubscriptionWhereInput): OrderSubscriptionPayload + item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload + orderItem(where: OrderItemSubscriptionWhereInput): OrderItemSubscriptionPayload + 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/53/backend/src/resolvers/Query.js b/stepped-solutions/53/backend/src/resolvers/Query.js new file mode 100755 index 0000000..8e0f169 --- /dev/null +++ b/stepped-solutions/53/backend/src/resolvers/Query.js @@ -0,0 +1,55 @@ +const { forwardTo } = require('prisma-binding'); +const { hasPermission } = require('../utils'); + +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 + ); + }, + async users(parent, args, ctx, info) { + // 1. Check if they are logged in + if (!ctx.request.userId) { + throw new Error('You must be logged in!'); + } + console.log(ctx.request.userId); + // 2. Check if the user has the permissions to query all the users + hasPermission(ctx.request.user, ['ADMIN', 'PERMISSIONUPDATE']); + + // 2. if they do, query all the users! + return ctx.db.query.users({}, info); + }, + async order(parent, args, ctx, info) { + // 1. Make sure they are logged in + if (!ctx.request.userId) { + throw new Error('You arent logged in!'); + } + // 2. Query the current order + const order = await ctx.db.query.order( + { + where: { id: args.id }, + }, + info + ); + // 3. Check if the have the permissions to see this order + const ownsOrder = order.user.id === ctx.request.userId; + const hasPermissionToSeeOrder = ctx.request.user.permissions.includes('ADMIN'); + if (!ownsOrder || !hasPermission) { + throw new Error('You cant see this buddd'); + } + // 4. Return the order + return order; + }, +}; + +module.exports = Query; diff --git a/stepped-solutions/53/backend/src/schema.graphql b/stepped-solutions/53/backend/src/schema.graphql new file mode 100755 index 0000000..15d119b --- /dev/null +++ b/stepped-solutions/53/backend/src/schema.graphql @@ -0,0 +1,37 @@ +# 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 + createOrder(token: String!): Order! +} + +type Query { + items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]! + item(where: ItemWhereUniqueInput!): Item + itemsConnection(where: ItemWhereInput): ItemConnection! + me: User + users: [User]! + order(id: ID!): Order +} + +type User{ + id: ID! + name: String! + email: String! + permissions: [Permission!]! + cart: [CartItem!]! +} diff --git a/stepped-solutions/53/frontend/components/Cart.js b/stepped-solutions/53/frontend/components/Cart.js new file mode 100755 index 0000000..2a72b38 --- /dev/null +++ b/stepped-solutions/53/frontend/components/Cart.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { Query, Mutation } from 'react-apollo'; +import gql from 'graphql-tag'; +import { adopt } from 'react-adopt'; +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'; +import TakeMyMoney from './TakeMyMoney'; + +const LOCAL_STATE_QUERY = gql` + query { + cartOpen @client + } +`; + +const TOGGLE_CART_MUTATION = gql` + mutation { + toggleCart @client + } +`; +/* eslint-disable */ +const Composed = adopt({ + user: ({ render }) => <User>{render}</User>, + toggleCart: ({ render }) => <Mutation mutation={TOGGLE_CART_MUTATION}>{render}</Mutation>, + localState: ({ render }) => <Query query={LOCAL_STATE_QUERY}>{render}</Query>, +}); +/* eslint-enable */ + +const Cart = () => ( + <Composed> + {({ user, toggleCart, localState }) => { + const me = user.data.me; + if (!me) return null; + return ( + <CartStyles open={localState.data.cartOpen}> + <header> + <CloseButton onClick={toggleCart} title="close"> + × + </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> + {me.cart.length && ( + <TakeMyMoney> + <SickButton>Checkout</SickButton> + </TakeMyMoney> + )} + </footer> + </CartStyles> + ); + }} + </Composed> +); + +export default Cart; +export { LOCAL_STATE_QUERY, TOGGLE_CART_MUTATION }; diff --git a/stepped-solutions/53/frontend/components/Order.js b/stepped-solutions/53/frontend/components/Order.js new file mode 100755 index 0000000..f2a2caf --- /dev/null +++ b/stepped-solutions/53/frontend/components/Order.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Query } from 'react-apollo'; +import { format } from 'date-fns'; +import Head from 'next/head'; +import gql from 'graphql-tag'; +import formatMoney from '../lib/formatMoney'; +import Error from './ErrorMessage'; +import OrderStyles from './styles/OrderStyles'; + +const SINGLE_ORDER_QUERY = gql` + query SINGLE_ORDER_QUERY($id: ID!) { + order(id: $id) { + id + charge + total + createdAt + user { + id + } + items { + id + title + description + price + image + quantity + } + } + } +`; + +class Order extends React.Component { + static propTypes = { + id: PropTypes.string.isRequired, + }; + render() { + return ( + <Query query={SINGLE_ORDER_QUERY} variables={{ id: this.props.id }}> + {({ data, error, loading }) => { + if (error) return <Error error={error} />; + if (loading) return <p>Loading...</p>; + const order = data.order; + return ( + <OrderStyles> + <Head> + <title>Sick Fits - Order {order.id}</title> + </Head> + <p> + <span>Order ID:</span> + <span>{this.props.id}</span> + </p> + <p> + <span>Charge</span> + <span>{order.charge}</span> + </p> + <p> + <span>Date</span> + <span>{format(order.createdAt, 'MMMM d, YYYY h:mm a')}</span> + </p> + <p> + <span>Order Total</span> + <span>{formatMoney(order.total)}</span> + </p> + <p> + <span>Item Count</span> + <span>{order.items.length}</span> + </p> + <div className="items"> + {order.items.map(item => ( + <div className="order-item" key={item.id}> + <img src={item.image} alt={item.title} /> + <div className="item-details"> + <h2>{item.title}</h2> + <p>Qty: {item.quantity}</p> + <p>Each: {formatMoney(item.price)}</p> + <p>SubTotal: {formatMoney(item.price * item.quantity)}</p> + <p>{item.description}</p> + </div> + </div> + ))} + </div> + </OrderStyles> + ); + }} + </Query> + ); + } +} + +export default Order; diff --git a/stepped-solutions/53/frontend/components/Search.js b/stepped-solutions/53/frontend/components/Search.js new file mode 100755 index 0000000..c29f93e --- /dev/null +++ b/stepped-solutions/53/frontend/components/Search.js @@ -0,0 +1,94 @@ +import React from 'react'; +import Downshift, { resetIdCounter } from 'downshift'; +import Router from 'next/router'; +import { ApolloConsumer } from 'react-apollo'; +import gql from 'graphql-tag'; +import debounce from 'lodash.debounce'; +import { DropDown, DropDownItem, SearchStyles } from './styles/DropDown'; + +const SEARCH_ITEMS_QUERY = gql` + query SEARCH_ITEMS_QUERY($searchTerm: String!) { + items(where: { OR: [{ title_contains: $searchTerm }, { description_contains: $searchTerm }] }) { + id + image + title + } + } +`; + +function routeToItem(item) { + Router.push({ + pathname: '/item', + query: { + id: item.id, + }, + }); +} + +class AutoComplete extends React.Component { + state = { + items: [], + loading: false, + }; + onChange = debounce(async (e, client) => { + console.log('Searching...'); + // turn loading on + this.setState({ loading: true }); + // Manually query apollo client + const res = await client.query({ + query: SEARCH_ITEMS_QUERY, + variables: { searchTerm: e.target.value }, + }); + this.setState({ + items: res.data.items, + loading: false, + }); + }, 350); + render() { + resetIdCounter(); + return ( + <SearchStyles> + <Downshift onChange={routeToItem} itemToString={item => (item === null ? '' : item.title)}> + {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex }) => ( + <div> + <ApolloConsumer> + {client => ( + <input + {...getInputProps({ + type: 'search', + placeholder: 'Search For An Item', + id: 'search', + className: this.state.loading ? 'loading' : '', + onChange: e => { + e.persist(); + this.onChange(e, client); + }, + })} + /> + )} + </ApolloConsumer> + {isOpen && ( + <DropDown> + {this.state.items.map((item, index) => ( + <DropDownItem + {...getItemProps({ item })} + key={item.id} + highlighted={index === highlightedIndex} + > + <img width="50" src={item.image} alt={item.title} /> + {item.title} + </DropDownItem> + ))} + {!this.state.items.length && + !this.state.loading && <DropDownItem> Nothing Found {inputValue}</DropDownItem>} + </DropDown> + )} + </div> + )} + </Downshift> + </SearchStyles> + ); + } +} + +export default AutoComplete; diff --git a/stepped-solutions/53/frontend/components/TakeMyMoney.js b/stepped-solutions/53/frontend/components/TakeMyMoney.js new file mode 100755 index 0000000..9d6614e --- /dev/null +++ b/stepped-solutions/53/frontend/components/TakeMyMoney.js @@ -0,0 +1,77 @@ +import React from 'react'; +import StripeCheckout from 'react-stripe-checkout'; +import { Mutation } from 'react-apollo'; +import Router from 'next/router'; +import NProgress from 'nprogress'; +import PropTypes from 'prop-types'; +import gql from 'graphql-tag'; +import calcTotalPrice from '../lib/calcTotalPrice'; +import Error from './ErrorMessage'; +import User, { CURRENT_USER_QUERY } from './User'; + +const CREATE_ORDER_MUTATION = gql` + mutation createOrder($token: String!) { + createOrder(token: $token) { + id + charge + total + items { + id + title + } + } + } +`; + +function totalItems(cart) { + return cart.reduce((tally, cartItem) => tally + cartItem.quantity, 0); +} + +class TakeMyMoney extends React.Component { + onToken = async (res, createOrder) => { + NProgress.start(); + console.log('On Token Called!'); + console.log(res.id); + // manually call the mutation once we have the stripe token + const order = await createOrder({ + variables: { + token: res.id, + }, + }).catch(err => { + alert(err.message); + }); + Router.push({ + pathname: '/order', + query: { id: order.data.createOrder.id }, + }); + }; + render() { + return ( + <User> + {({ data: { me } }) => ( + <Mutation + mutation={CREATE_ORDER_MUTATION} + refetchQueries={[{ query: CURRENT_USER_QUERY }]} + > + {createOrder => ( + <StripeCheckout + amount={calcTotalPrice(me.cart)} + name="Sick Fits" + description={`Order of ${totalItems(me.cart)} items!`} + image={me.cart.length && me.cart[0].item && me.cart[0].item.image} + stripeKey="pk_test_Vtknn6vSdcZWSG2JWvEiWSqC" + currency="USD" + email={me.email} + token={res => this.onToken(res, createOrder)} + > + {this.props.children} + </StripeCheckout> + )} + </Mutation> + )} + </User> + ); + } +} + +export default TakeMyMoney; diff --git a/stepped-solutions/53/frontend/components/styles/OrderStyles.js b/stepped-solutions/53/frontend/components/styles/OrderStyles.js new file mode 100755 index 0000000..f461b70 --- /dev/null +++ b/stepped-solutions/53/frontend/components/styles/OrderStyles.js @@ -0,0 +1,37 @@ +import styled from 'styled-components'; + +const OrderStyles = styled.div` + max-width: 1000px; + margin: 0 auto; + border: 1px solid ${props => props.theme.offWhite}; + box-shadow: ${props => props.theme.bs}; + padding: 2rem; + border-top: 10px solid red; + & > p { + display: grid; + grid-template-columns: 1fr 5fr; + margin: 0; + border-bottom: 1px solid ${props => props.theme.offWhite}; + span { + padding: 1rem; + &:first-child { + font-weight: 900; + text-align: right; + } + } + } + .order-item { + border-bottom: 1px solid ${props => props.theme.offWhite}; + display: grid; + grid-template-columns: 300px 1fr; + align-items: center; + grid-gap: 2rem; + margin: 2rem 0; + padding-bottom: 2rem; + img { + width: 100%; + object-fit: cover; + } + } +`; +export default OrderStyles; diff --git a/stepped-solutions/53/frontend/pages/order.js b/stepped-solutions/53/frontend/pages/order.js new file mode 100755 index 0000000..0fea0c6 --- /dev/null +++ b/stepped-solutions/53/frontend/pages/order.js @@ -0,0 +1,12 @@ +import PleaseSignIn from '../components/PleaseSignIn'; +import Order from '../components/Order'; + +const OrderPage = props => ( + <div> + <PleaseSignIn> + <Order id={props.query.id} /> + </PleaseSignIn> + </div> +); + +export default OrderPage; diff --git a/stepped-solutions/54/backend/src/resolvers/Query.js b/stepped-solutions/54/backend/src/resolvers/Query.js new file mode 100755 index 0000000..22a6414 --- /dev/null +++ b/stepped-solutions/54/backend/src/resolvers/Query.js @@ -0,0 +1,69 @@ +const { forwardTo } = require('prisma-binding'); +const { hasPermission } = require('../utils'); + +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 + ); + }, + async users(parent, args, ctx, info) { + // 1. Check if they are logged in + if (!ctx.request.userId) { + throw new Error('You must be logged in!'); + } + console.log(ctx.request.userId); + // 2. Check if the user has the permissions to query all the users + hasPermission(ctx.request.user, ['ADMIN', 'PERMISSIONUPDATE']); + + // 2. if they do, query all the users! + return ctx.db.query.users({}, info); + }, + async order(parent, args, ctx, info) { + // 1. Make sure they are logged in + if (!ctx.request.userId) { + throw new Error('You arent logged in!'); + } + // 2. Query the current order + const order = await ctx.db.query.order( + { + where: { id: args.id }, + }, + info + ); + // 3. Check if the have the permissions to see this order + const ownsOrder = order.user.id === ctx.request.userId; + const hasPermissionToSeeOrder = ctx.request.user.permissions.includes('ADMIN'); + if (!ownsOrder || !hasPermission) { + throw new Error('You cant see this buddd'); + } + // 4. Return the order + return order; + }, + async orders(parent, args, ctx, info) { + const { userId } = ctx.request; + if (!userId) { + throw new Error('you must be signed in!'); + } + return ctx.db.query.orders( + { + where: { + user: { id: userId }, + }, + }, + info + ); + }, +}; + +module.exports = Query; diff --git a/stepped-solutions/54/backend/src/schema.graphql b/stepped-solutions/54/backend/src/schema.graphql new file mode 100755 index 0000000..d9ab917 --- /dev/null +++ b/stepped-solutions/54/backend/src/schema.graphql @@ -0,0 +1,38 @@ +# 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 + createOrder(token: String!): Order! +} + +type Query { + items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]! + item(where: ItemWhereUniqueInput!): Item + itemsConnection(where: ItemWhereInput): ItemConnection! + me: User + users: [User]! + order(id: ID!): Order + orders(orderBy: OrderOrderByInput): [Order]! +} + +type User{ + id: ID! + name: String! + email: String! + permissions: [Permission!]! + cart: [CartItem!]! +} diff --git a/stepped-solutions/54/frontend/components/OrderList.js b/stepped-solutions/54/frontend/components/OrderList.js new file mode 100755 index 0000000..e2e22ff --- /dev/null +++ b/stepped-solutions/54/frontend/components/OrderList.js @@ -0,0 +1,80 @@ +import React from 'react'; +import { Query } from 'react-apollo'; +import { formatDistance } from 'date-fns'; +import Link from 'next/link'; +import styled from 'styled-components'; +import gql from 'graphql-tag'; +import Error from './ErrorMessage'; +import formatMoney from '../lib/formatMoney'; +import OrderItemStyles from './styles/OrderItemStyles'; + +const USER_ORDERS_QUERY = gql` + query USER_ORDERS_QUERY { + orders(orderBy: createdAt_DESC) { + id + total + createdAt + items { + id + title + price + description + quantity + image + } + } + } +`; + +const orderUl = styled.ul` + display: grid; + grid-gap: 4rem; + grid-template-columns: repeat(auto-fit, minmax(40%, 1fr)); +`; + +class OrderList extends React.Component { + render() { + return ( + <Query query={USER_ORDERS_QUERY}> + {({ data: { orders }, loading, error }) => { + if (loading) return <p>loading...</p>; + if (error) return <Error erorr={error} />; + console.log(orders); + return ( + <div> + <h2>You have {orders.length} orders</h2> + <orderUl> + {orders.map(order => ( + <OrderItemStyles key={order.id}> + <Link + href={{ + pathname: '/order', + query: { id: order.id }, + }} + > + <a> + <div className="order-meta"> + <p>{order.items.reduce((a, b) => a + b.quantity, 0)} Items</p> + <p>{order.items.length} Products</p> + <p>{formatDistance(order.createdAt, new Date())}</p> + <p>{formatMoney(order.total)}</p> + </div> + <div className="images"> + {order.items.map(item => ( + <img key={item.id} src={item.image} alt={item.title} /> + ))} + </div> + </a> + </Link> + </OrderItemStyles> + ))} + </orderUl> + </div> + ); + }} + </Query> + ); + } +} + +export default OrderList; diff --git a/stepped-solutions/54/frontend/pages/orders.js b/stepped-solutions/54/frontend/pages/orders.js new file mode 100755 index 0000000..c65d80d --- /dev/null +++ b/stepped-solutions/54/frontend/pages/orders.js @@ -0,0 +1,12 @@ +import PleaseSignIn from '../components/PleaseSignIn'; +import OrderList from '../components/OrderList'; + +const OrderPage = props => ( + <div> + <PleaseSignIn> + <OrderList /> + </PleaseSignIn> + </div> +); + +export default OrderPage; diff --git a/stepped-solutions/56/frontend/__tests__/formatMoney.test.js b/stepped-solutions/56/frontend/__tests__/formatMoney.test.js new file mode 100755 index 0000000..23d8183 --- /dev/null +++ b/stepped-solutions/56/frontend/__tests__/formatMoney.test.js @@ -0,0 +1,23 @@ +import formatMoney from '../lib/formatMoney'; + +describe('formatMoney Function', () => { + it('works with fractional dollars', () => { + expect(formatMoney(1)).toEqual('$0.01'); + expect(formatMoney(10)).toEqual('$0.10'); + expect(formatMoney(9)).toEqual('$0.09'); + expect(formatMoney(40)).toEqual('$0.40'); + }); + + it('leaves cents off for whole dollars', () => { + expect(formatMoney(5000)).toEqual('$50'); + expect(formatMoney(100)).toEqual('$1'); + expect(formatMoney(50000000)).toEqual('$500,000'); + }); + + it('works with whole and fractional dollars', () => { + expect(formatMoney(5012)).toEqual('$50.12'); + expect(formatMoney(101)).toEqual('$1.01'); + expect(formatMoney(110)).toEqual('$1.10'); + expect(formatMoney(20893749823749823749)).toEqual('$208,937,498,237,498,240.00'); + }); +}); diff --git a/stepped-solutions/56/frontend/__tests__/sample.test.js b/stepped-solutions/56/frontend/__tests__/sample.test.js new file mode 100755 index 0000000..dbed94f --- /dev/null +++ b/stepped-solutions/56/frontend/__tests__/sample.test.js @@ -0,0 +1,19 @@ +describe('sample test 101', () => { + it('works as expected', () => { + const age = 100; + expect(1).toEqual(1); + expect(age).toEqual(100); + }); + + it('handles ranges just fine', () => { + const age = 200; + expect(age).toBeGreaterThan(100); + }); + + it('makes a list of dog names', () => { + const dogs = ['snickers', 'hugo']; + expect(dogs).toEqual(dogs); + expect(dogs).toContain('snickers'); + expect(dogs).toContain('snickers'); + }); +}); diff --git a/stepped-solutions/57/frontend/__tests__/mocking.test.js b/stepped-solutions/57/frontend/__tests__/mocking.test.js new file mode 100755 index 0000000..d0a05bf --- /dev/null +++ b/stepped-solutions/57/frontend/__tests__/mocking.test.js @@ -0,0 +1,36 @@ +function Person(name, foods) { + this.name = name; + this.foods = foods; +} + +Person.prototype.fetchFavFoods = function() { + return new Promise((resolve, reject) => { + // Simulate an API + setTimeout(() => resolve(this.foods), 2000); + }); +}; + +describe('mocking learning', () => { + it('mocks a reg function', () => { + const fetchDogs = jest.fn(); + fetchDogs('snickers'); + expect(fetchDogs).toHaveBeenCalled(); + expect(fetchDogs).toHaveBeenCalledWith('snickers'); + fetchDogs('hugo'); + expect(fetchDogs).toHaveBeenCalledTimes(2); + }); + + it('can create a person', () => { + const me = new Person('Wes', ['pizza', 'burgs']); + expect(me.name).toBe('Wes'); + }); + + it('can fetch foods', async () => { + const me = new Person('Wes', ['pizza', 'burgs']); + // mock the favFoods function + me.fetchFavFoods = jest.fn().mockResolvedValue(['sushi', 'ramen']); + const favFoods = await me.fetchFavFoods(); + console.log(favFoods); + expect(favFoods).toContain('sushi'); + }); +}); diff --git a/stepped-solutions/59/frontend/__tests__/CartCount.test.js b/stepped-solutions/59/frontend/__tests__/CartCount.test.js new file mode 100755 index 0000000..8ace3da --- /dev/null +++ b/stepped-solutions/59/frontend/__tests__/CartCount.test.js @@ -0,0 +1,21 @@ +import { shallow, mount } from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import CartCount from '../components/CartCount'; + +describe('<CartCount/>', () => { + it('renders', () => { + shallow(<CartCount count={10} />); + }); + + it('matches the snapshot', () => { + const wrapper = shallow(<CartCount count={11} />); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); + + it('updates via props', () => { + const wrapper = shallow(<CartCount count={50} />); + expect(toJSON(wrapper)).toMatchSnapshot(); + wrapper.setProps({ count: 10 }); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/stepped-solutions/59/frontend/__tests__/Item.test.js b/stepped-solutions/59/frontend/__tests__/Item.test.js new file mode 100755 index 0000000..692b913 --- /dev/null +++ b/stepped-solutions/59/frontend/__tests__/Item.test.js @@ -0,0 +1,39 @@ +import ItemComponent from '../components/Item'; +import { shallow, mount } from 'enzyme'; +import toJSON from 'enzyme-to-json'; + +const fakeItem = { + id: 'ABC123', + title: 'A Cool Item', + price: 4000, + description: 'This item is really cool!', + image: 'dog.jpg', + largeImage: 'largedog.jpg', +}; + +describe('<Item/>', () => { + it('renders and matches the snapshot', () => { + const wrapper = shallow(<ItemComponent item={fakeItem} />); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); + // it('renders the image properly', () => { + // const wrapper = shallow(<ItemComponent item={fakeItem} />); + // const img = wrapper.find('img'); + // expect(img.props().src).toBe(fakeItem.image); + // expect(img.props().alt).toBe(fakeItem.title); + // }); + // it('renders the pricetag and title', () => { + // const wrapper = shallow(<ItemComponent item={fakeItem} />); + // const PriceTag = wrapper.find('PriceTag'); + // expect(PriceTag.children().text()).toBe('$50'); + // expect(wrapper.find('Title a').text()).toBe(fakeItem.title); + // }); + // it('renders out the buttons properly', () => { + // const wrapper = shallow(<ItemComponent item={fakeItem} />); + // const buttonList = wrapper.find('.buttonList'); + // expect(buttonList.children()).toHaveLength(3); + // expect(buttonList.find('Link')).toHaveLength(1); + // expect(buttonList.find('AddToCart').exists()).toBe(true); + // expect(buttonList.find('DeleteItem').exists()).toBe(true); + // }); +}); diff --git a/stepped-solutions/59/frontend/__tests__/__snapshots__/CartCount.test.js.snap b/stepped-solutions/59/frontend/__tests__/__snapshots__/CartCount.test.js.snap new file mode 100755 index 0000000..92ee89a --- /dev/null +++ b/stepped-solutions/59/frontend/__tests__/__snapshots__/CartCount.test.js.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<CartCount/> matches the snapshot 1`] = ` +<CartCount__AnimationStyles> + <TransitionGroup + childFactory={[Function]} + component="div" + > + <CSSTransition + className="count" + classNames="count" + key="11" + timeout={ + Object { + "enter": 400, + "exit": 400, + } + } + unmountOnExit={true} + > + <CartCount__Dot> + 11 + </CartCount__Dot> + </CSSTransition> + </TransitionGroup> +</CartCount__AnimationStyles> +`; + +exports[`<CartCount/> updates via props 1`] = ` +<CartCount__AnimationStyles> + <TransitionGroup + childFactory={[Function]} + component="div" + > + <CSSTransition + className="count" + classNames="count" + key="50" + timeout={ + Object { + "enter": 400, + "exit": 400, + } + } + unmountOnExit={true} + > + <CartCount__Dot> + 50 + </CartCount__Dot> + </CSSTransition> + </TransitionGroup> +</CartCount__AnimationStyles> +`; + +exports[`<CartCount/> updates via props 2`] = ` +<CartCount__AnimationStyles> + <TransitionGroup + childFactory={[Function]} + component="div" + > + <CSSTransition + className="count" + classNames="count" + key="10" + timeout={ + Object { + "enter": 400, + "exit": 400, + } + } + unmountOnExit={true} + > + <CartCount__Dot> + 10 + </CartCount__Dot> + </CSSTransition> + </TransitionGroup> +</CartCount__AnimationStyles> +`; diff --git a/stepped-solutions/59/frontend/__tests__/__snapshots__/Item.test.js.snap b/stepped-solutions/59/frontend/__tests__/__snapshots__/Item.test.js.snap new file mode 100755 index 0000000..70e4aa8 --- /dev/null +++ b/stepped-solutions/59/frontend/__tests__/__snapshots__/Item.test.js.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<Item/> renders and matches the snapshot 1`] = ` +<ItemStyles__Item> + <img + alt="A Cool Item" + src="dog.jpg" + /> + <Title> + <Link + href={ + Object { + "pathname": "/item", + "query": Object { + "id": "ABC123", + }, + } + } + > + <a> + A Cool Item + </a> + </Link> + </Title> + <PriceTag> + $40 + </PriceTag> + <p> + This item is really cool! + </p> + <div + className="buttonList" + > + <Link + href={ + Object { + "pathname": "update", + "query": Object { + "id": "ABC123", + }, + } + } + > + <a> + Edit ✏️ + </a> + </Link> + <AddToCart + id="ABC123" + /> + <DeleteItem + id="ABC123" + > + Delete This Item + </DeleteItem> + </div> +</ItemStyles__Item> +`; diff --git a/stepped-solutions/60/frontend/__tests__/SingleItem.test.js b/stepped-solutions/60/frontend/__tests__/SingleItem.test.js new file mode 100755 index 0000000..2963bde --- /dev/null +++ b/stepped-solutions/60/frontend/__tests__/SingleItem.test.js @@ -0,0 +1,57 @@ +import { mount } from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import wait from 'waait'; +import SingleItem, { SINGLE_ITEM_QUERY } from '../components/SingleItem'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { fakeItem } from '../lib/testUtils'; + +describe('<SingleItem/>', () => { + it('renders with proper data', async () => { + const mocks = [ + { + // when someone makes a request with this query and variable combo + request: { query: SINGLE_ITEM_QUERY, variables: { id: '123' } }, + // return this fake data (mocked data) + result: { + data: { + item: fakeItem(), + }, + }, + }, + ]; + const wrapper = mount( + <MockedProvider mocks={mocks}> + <SingleItem id="123" /> + </MockedProvider> + ); + expect(wrapper.text()).toContain('Loading...'); + await wait(); + wrapper.update(); + // console.log(wrapper.debug()); + expect(toJSON(wrapper.find('h2'))).toMatchSnapshot(); + expect(toJSON(wrapper.find('img'))).toMatchSnapshot(); + expect(toJSON(wrapper.find('p'))).toMatchSnapshot(); + }); + + it('Errors with a not found item', async () => { + const mocks = [ + { + request: { query: SINGLE_ITEM_QUERY, variables: { id: '123' } }, + result: { + errors: [{ message: 'Items Not Found!' }], + }, + }, + ]; + const wrapper = mount( + <MockedProvider mocks={mocks}> + <SingleItem id="123" /> + </MockedProvider> + ); + await wait(); + wrapper.update(); + console.log(wrapper.debug()); + const item = wrapper.find('[data-test="graphql-error"]'); + expect(item.text()).toContain('Items Not Found!'); + expect(toJSON(item)).toMatchSnapshot(); + }); +}); diff --git a/stepped-solutions/60/frontend/__tests__/__snapshots__/SingleItem.test.js.snap b/stepped-solutions/60/frontend/__tests__/__snapshots__/SingleItem.test.js.snap new file mode 100755 index 0000000..2f41d4c --- /dev/null +++ b/stepped-solutions/60/frontend/__tests__/__snapshots__/SingleItem.test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<SingleItem/> Errors with a not found item 1`] = ` +<p + data-test="graphql-error" +> + <strong> + Shoot! + </strong> + Items Not Found! +</p> +`; + +exports[`<SingleItem/> renders with proper data 1`] = ` +<h2> + Viewing + dogs are best +</h2> +`; + +exports[`<SingleItem/> renders with proper data 2`] = ` +<img + alt="dogs are best" + src="dog.jpg" +/> +`; + +exports[`<SingleItem/> renders with proper data 3`] = ` +<p> + dogs +</p> +`; diff --git a/stepped-solutions/60/frontend/components/SingleItem.js b/stepped-solutions/60/frontend/components/SingleItem.js new file mode 100755 index 0000000..145b5ae --- /dev/null +++ b/stepped-solutions/60/frontend/components/SingleItem.js @@ -0,0 +1,70 @@ +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; +export { SINGLE_ITEM_QUERY }; |
