├── .gitignore ├── README.md ├── package.json ├── packages ├── frontend │ ├── .nowignore │ ├── README.md │ ├── apollo.config.js │ ├── codegen.yml │ ├── now.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.tsx │ │ ├── Router.tsx │ │ ├── components │ │ │ ├── AttributesTable │ │ │ │ └── AttributesTable.tsx │ │ │ ├── BuyButton │ │ │ │ └── BuyButton.tsx │ │ │ ├── CardProduct │ │ │ │ └── CardProduct.tsx │ │ │ ├── NavBar │ │ │ │ └── NavBar.tsx │ │ │ ├── QuantitySelector │ │ │ │ └── QuantitySelector.tsx │ │ │ ├── Select │ │ │ │ └── Select.tsx │ │ │ └── VariantSelector │ │ │ │ └── VariantSelector.tsx │ │ ├── generated-types.tsx │ │ ├── helpers │ │ │ └── variants.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── Collection │ │ │ │ ├── Collection.graphql │ │ │ │ ├── Collection.tsx │ │ │ │ └── CollectionContainer.ts │ │ │ ├── Product │ │ │ │ ├── Product.graphql │ │ │ │ ├── Product.tsx │ │ │ │ └── ProductContainer.ts │ │ │ └── Welcome │ │ │ │ ├── Welcome.graphql │ │ │ │ ├── Welcome.tsx │ │ │ │ └── WelcomeContainer.ts │ │ ├── react-app-env.d.ts │ │ └── serviceWorker.ts │ └── tsconfig.json └── server │ ├── .nowignore │ ├── .vscode │ └── settings.json │ ├── now.json │ ├── output.json │ ├── package.json │ ├── prisma │ ├── db │ │ └── next.db │ ├── migrations │ │ ├── 20190618213953-init │ │ │ ├── README.md │ │ │ ├── datamodel.prisma │ │ │ └── steps.json │ │ ├── dev │ │ │ └── watch-20190618214934 │ │ │ │ ├── README.md │ │ │ │ ├── datamodel.prisma │ │ │ │ └── steps.json │ │ └── lift.lock │ ├── project.prisma │ └── seed.ts │ ├── src │ ├── context.ts │ ├── fragments │ │ ├── ProductVariant.ts │ │ └── index.ts │ ├── graphql │ │ ├── Attributes.ts │ │ ├── Brand.ts │ │ ├── Collection.ts │ │ ├── CollectionRule.ts │ │ ├── Image.ts │ │ ├── Option.ts │ │ ├── OptionValue.ts │ │ ├── Product.ts │ │ ├── Query.ts │ │ ├── Variant.ts │ │ ├── common.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── index.ts │ ├── nexus-typegen.ts │ ├── schema.graphql │ └── utils │ │ ├── attributes.ts │ │ ├── collection.ts │ │ ├── ids.ts │ │ └── variants.ts │ └── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .yalc 3 | yalc.lock 4 | .idea 5 | 6 | # CRA 7 | /src/front/.pnp 8 | /src/front/front.pnp.js 9 | # testing 10 | /src/front/coverage 11 | # production 12 | /src/front/build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | dist 25 | build 26 | .env* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Archive Notice 2 | 3 | Please note that this repo is no longer actively maintained and has been archived. 4 | 5 | We recommend checking out the following full stack frameworks which accomplish similar goals to Fluidstack: 6 | 7 | - [**RedwoodJS**](https://redwoodjs.com/) - JAMStack apps with React, GraphQL, and Prisma 8 | - [**Blitz**](https://github.com/blitz-js/blitz) - Zero-API data layer built on Next.js and inspired by Ruby on Rails 9 | 10 | # FluidStack (2019 edition) 11 | 12 | The constantly evolving modern web stack. 13 | 14 | ## Description 15 | 16 | FluidStack is an production-ready e-commerce app designed as a reference implementation of best practices to help developers build better products. It serves as a central place to find examples of common patterns needed across web development, built by the community for the community. 17 | 18 | ## Motivation 19 | 20 | Building web apps can be challenging considering the amount of technologies that are available. While there are tons of services out there to help you build your product, each of them create examples based on their business activity, making it often hard to connect the dots. 21 | 22 | That's why at Prisma, Stripe, ZEIT and Algolia, we're dedicated to unify our efforts to provide a reference implementation to showcase many best practices of each layers in your app in one single place. 23 | 24 | ## Why e-commerce ? 25 | 26 | Most applications rely on a common set of challenges that can be hard to get right. We think e-commerce is one of many domains that gather the most of these challenges while also being friendly to everyone. 27 | 28 | ## Stack 29 | 30 | - 💨 Data-layer for database access, CI/CD migrations and data visualisation with Prisma 31 | - ⭐ One-click serverless deploy with Now (ZEIT) 32 | - 🤗 SEO-friendly & server-side rendered with NextJS (ZEIT) 33 | - 💸 Ready-to-sell Stripe integration 34 | - 🔍 Blazing-fast search capabilities with Algolia 35 | 36 | ## Features 37 | 38 | - Fully customisable products 39 | - Multi-authentication support 40 | - Internationalisation 41 | - Role permissions support 42 | - Text-search capabilities 43 | - Multi-currencies 44 | - Multi-criteria filtering 45 | - Handcrafted backoffice to manage your shop 46 | 47 | ## Design goals 48 | 49 | ### End-to-end type-safety 50 | 51 | End-to-end type-safe means that your entire application, from the database accesses to your frontend is type-safe. 52 | 53 | It incredibly enhance the developer experience by providing auto-completion everywhere across your app, compile-time errors, higher confidence in code, better refactoring capabilities, and faster iterations. 54 | 55 | - Type-safe database access (Prisma) 56 | - Type-safe API layer (Nexus) 57 | - Type-safe front-end (GraphQL code generation) 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "prettier": { 7 | "semi": false, 8 | "trailingComma": "all", 9 | "singleQuote": true 10 | }, 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /packages/frontend/ .nowignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* -------------------------------------------------------------------------------- /packages/frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /packages/frontend/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | name: 'ecommerce-project', 4 | service: { 5 | url: 'http://localhost:4000', 6 | }, 7 | includes: ['src/**/*.{ts,tsx,graphql}'], 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/frontend/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | watch: true 3 | schema: http://localhost:4000 4 | documents: ./src/**/*.graphql 5 | generates: 6 | src/generated-types.tsx: 7 | plugins: 8 | - typescript 9 | - typescript-operations 10 | - typescript-react-apollo 11 | config: 12 | avoidOptional: true 13 | typesPrefix: I 14 | -------------------------------------------------------------------------------- /packages/frontend/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "webshop", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "build" } 9 | } 10 | ], 11 | "build": { 12 | "env": { 13 | "REACT_APP_PRISMA_ENDPOINT": "@webshop-graphql-staging" 14 | } 15 | }, 16 | "routes": [ 17 | { 18 | "src": "/static/(.*)", 19 | "headers": { "cache-control": "s-maxage=31536000,immutable" }, 20 | "dest": "/static/$1" 21 | }, 22 | { "src": "/favicon.ico", "dest": "/favicon.ico" }, 23 | { "src": "/asset-manifest.json", "dest": "/asset-manifest.json" }, 24 | { "src": "/manifest.json", "dest": "/manifest.json" }, 25 | { "src": "/precache-manifest.(.*)", "dest": "/precache-manifest.$1" }, 26 | { 27 | "src": "/service-worker.js", 28 | "headers": { "cache-control": "s-maxage=0" }, 29 | "dest": "/service-worker.js" 30 | }, 31 | { 32 | "src": "/(.*)", 33 | "headers": { "cache-control": "s-maxage=0" }, 34 | "dest": "/index.html" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webshop-front", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-boost": "^0.1.23", 7 | "graphql": "^14.0.2", 8 | "graphql-tag": "^2.10.0", 9 | "lodash": "^4.17.11", 10 | "react": "^16.8.4", 11 | "react-apollo": "^2.3.3", 12 | "react-dom": "^16.8.4", 13 | "react-router-dom": "^4.3.1", 14 | "react-scripts": "2.1.1", 15 | "styled-components": "^4.1.3", 16 | "styled-icons": "^7.7.0", 17 | "typescript": "^3.2.2" 18 | }, 19 | "devDependencies": { 20 | "@graphql-codegen/cli": "^1.2.1", 21 | "@graphql-codegen/typescript": "^1.2.1", 22 | "@graphql-codegen/typescript-operations": "^1.2.1", 23 | "@graphql-codegen/typescript-react-apollo": "^1.2.1", 24 | "@types/jest": "^24.0.15", 25 | "@types/node": "^12.0.8", 26 | "@types/react": "^16.7.17", 27 | "@types/react-dom": "^16.0.11", 28 | "@types/react-router-dom": "^4.3.1", 29 | "@types/styled-components": "^4.1.12", 30 | "concurrently": "^4.1.0" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject", 37 | "generate": "gql-gen", 38 | "deploy": "now", 39 | "now-build": "react-scripts build" 40 | }, 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": [ 45 | ">0.2%", 46 | "not dead", 47 | "not ie <= 11", 48 | "not op_mini all" 49 | ], 50 | "prettier": { 51 | "semi": false, 52 | "singleQuote": true, 53 | "trailingComma": "es5" 54 | }, 55 | "moduleFileExtensions": [ 56 | "ts", 57 | "tsx", 58 | "js", 59 | "jsx", 60 | "json", 61 | "node" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /packages/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluidstackdev/fluidstack/c9b47df985d70fb5134d252723dc087638b1de69/packages/frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ApolloClient from 'apollo-boost' 3 | import { ApolloProvider } from 'react-apollo' 4 | 5 | import Router from './Router' 6 | 7 | const client = new ApolloClient({ 8 | uri: process.env.REACT_APP_PRISMA_ENDPOINT, 9 | }) 10 | 11 | class App extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /packages/frontend/src/Router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom' 3 | import Welcome from './pages/Welcome/WelcomeContainer' 4 | import CollectionContainer from './pages/Collection/CollectionContainer' 5 | import ProductContainer from './pages/Product/ProductContainer' 6 | 7 | const AppRouter = () => ( 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | ) 19 | 20 | export default AppRouter 21 | -------------------------------------------------------------------------------- /packages/frontend/src/components/AttributesTable/AttributesTable.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const AttributesTable = styled.table` 4 | border-collapse: collapse; 5 | border-spacing: 0; 6 | width: 100%; 7 | border: 1px solid #ddd; 8 | text-align: left; 9 | 10 | tr:nth-child(odd) { 11 | background-color: #f2f2f2; 12 | } 13 | ` 14 | 15 | export const AttributeName = styled.th` 16 | padding: 0.75rem; 17 | vertical-align: top; 18 | border-top: 1px solid #dee2e6; 19 | letter-spacing: 0.8px; 20 | ` 21 | 22 | export const AttributeValue = styled.td` 23 | padding: 0.75rem; 24 | vertical-align: top; 25 | border-top: 1px solid #dee2e6; 26 | letter-spacing: 0.8px; 27 | ` 28 | -------------------------------------------------------------------------------- /packages/frontend/src/components/BuyButton/BuyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | export const BuyButton = styled.a` 5 | cursor: pointer; 6 | text-decoration: none; 7 | color: #fff; 8 | letter-spacing: 0.05em; 9 | border: 2px solid #ff4c3b; 10 | background-image: linear-gradient(30deg, #ff4c3b 50%, transparent 0); 11 | background-size: 540px; 12 | background-repeat: no-repeat; 13 | background-position: 0; 14 | transition: background 0.3s ease-in-out; 15 | line-height: 20px; 16 | text-transform: uppercase; 17 | font-size: 14px; 18 | font-weight: 700; 19 | border-radius: 0; 20 | text-align: center; 21 | white-space: nowrap; 22 | vertical-align: middle; 23 | padding: 7px 25px; 24 | margin-right: 8px; 25 | 26 | &:hover { 27 | background-position: 100%; 28 | color: #000; 29 | background-color: #fff; 30 | } 31 | ` 32 | -------------------------------------------------------------------------------- /packages/frontend/src/components/CardProduct/CardProduct.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { ICollectionProductFragment, IOptionValue } from '../../generated-types' 4 | import { 5 | getAvailableOptionValues, 6 | getDisplayPrice, 7 | getIterableVariants, 8 | isOptionValueAvailable, 9 | sameOptionValue, 10 | } from '../../helpers/variants' 11 | import { Link } from 'react-router-dom' 12 | 13 | interface Props { 14 | product: ICollectionProductFragment 15 | } 16 | 17 | export const CardProduct: React.SFC = props => { 18 | const [optionValue, setOptionValue] = useState(null) 19 | const iterableVariants = getIterableVariants(props.product.variants!) 20 | const availableOptionValues = optionValue 21 | ? getAvailableOptionValues([optionValue], props.product.variants!) 22 | : null 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {props.product.brand.name} 33 | {props.product.name} 34 | 35 | {getDisplayPrice(props.product) / 100} € 36 | 37 | 38 | {iterableVariants.map(([optionName, values]) => ( 39 | 40 | {optionName} 41 |
42 | {values.map(v => { 43 | if (v.isColor) { 44 | return ( 45 | 49 | setOptionValue( 50 | sameOptionValue(v, optionValue) ? null : v 51 | ) 52 | } 53 | available={isOptionValueAvailable( 54 | v, 55 | [], 56 | availableOptionValues 57 | )} 58 | /> 59 | ) 60 | } else { 61 | return ( 62 | 70 | setOptionValue( 71 | sameOptionValue(v, optionValue) ? null : v 72 | ) 73 | } 74 | > 75 | {v.name} 76 | 77 | ) 78 | } 79 | })} 80 |
81 |
82 | ))} 83 |
84 |
85 |
86 | ) 87 | } 88 | 89 | const Container = styled.div` 90 | padding-right: 16px; 91 | padding-bottom: 16px; 92 | flex-grow: 1; 93 | flex-basis: 20%; 94 | display: flex; /* so child elements can use flexbox stuff too! */ 95 | flex-direction: column; 96 | transition: all 0.2s ease-in-out; 97 | 98 | &:hover { 99 | transform: scale(1.1); 100 | } 101 | ` 102 | 103 | const ImageContainer = styled(Link)` 104 | box-sizing: border-box; 105 | text-decoration: none; 106 | color: black; 107 | ` 108 | 109 | const Image = styled.img` 110 | display: block; 111 | max-width: 100%; 112 | object-fit: cover; 113 | margin-bottom: 4px; 114 | ` 115 | 116 | const DetailContainer = styled.div` 117 | display: flex; 118 | flex-direction: column; 119 | background-color: white; 120 | padding: 8px 12px; 121 | ` 122 | 123 | const TopDetailContainer = styled(Link)` 124 | display: flex; 125 | flex-direction: row; 126 | justify-content: space-between; 127 | margin-bottom: 8px; 128 | box-sizing: border-box; 129 | text-decoration: none; 130 | color: black; 131 | ` 132 | 133 | const LabelsContainer = styled.div` 134 | display: flex; 135 | flex-direction: column; 136 | ` 137 | const BrandName = styled.span` 138 | color: #1a1a1a; 139 | font-weight: 700; 140 | margin-bottom: 2px; 141 | font-size: 12px; 142 | ` 143 | const ProductName = styled.span` 144 | color: #1a1a1a; 145 | margin-bottom: 2px; 146 | font-size: 12px; 147 | ` 148 | 149 | const Price = styled.span` 150 | color: #1a1a1a; 151 | margin-bottom: 2px; 152 | font-size: 12px; 153 | ` 154 | 155 | const OptionValuesContainer = styled.div`` 156 | 157 | const OptionValuesGroup = styled.div` 158 | display: flex; 159 | flex-direction: row; 160 | align-items: center; 161 | justify-content: space-between; 162 | margin-bottom: 4px; 163 | ` 164 | 165 | const OptionValuesGroupTitle = styled.span` 166 | font-size: 12px; 167 | font-weight: 600; 168 | margin-right: 5px; 169 | ` 170 | 171 | const OptionValue = styled.span<{ available: boolean }>` 172 | font-size: 12px; 173 | padding: 3px; 174 | border-radius: 2px; 175 | margin-right: 2px; 176 | min-width: 20px; 177 | text-align: center; 178 | 179 | ${props => `background-color: ${props.available ? '#0000000d' : 'white'};`} 180 | ${props => `color: ${props.available ? 'black' : '#00000070'};`} 181 | 182 | &:hover { 183 | cursor: pointer; 184 | background-color: #00000014; 185 | } 186 | ` 187 | 188 | const colors: Record = { 189 | red: '#ff7979', 190 | blue: '#686de0', 191 | green: '#badc58', 192 | violet: '#e056fd', 193 | } 194 | 195 | const ColorValue = styled.div<{ color: string; available: boolean }>` 196 | width: 15px; 197 | height: 15px; 198 | margin-right: 4px; 199 | ${props => 200 | `background-color: ${ 201 | props.available ? colors[props.color.toLowerCase()] : '#e0e0e0' 202 | }`} 203 | 204 | &:hover { 205 | cursor: pointer; 206 | } 207 | ` 208 | -------------------------------------------------------------------------------- /packages/frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, FunctionComponent } from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | export interface NavItem { 5 | label: string 6 | link: string 7 | } 8 | 9 | interface Props { 10 | items: NavItem[] 11 | } 12 | 13 | export const NavBar: React.SFC = props => ( 14 | 23 | ) 24 | -------------------------------------------------------------------------------- /packages/frontend/src/components/QuantitySelector/QuantitySelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import * as Fa from 'styled-icons/fa-solid' 4 | 5 | interface Props { 6 | title: string 7 | onQuantityChange: (quantity: number) => void 8 | } 9 | 10 | function tryParseInt(str: string, defaultValue: number) { 11 | if (/^\d+$/g.test(str) === true) return parseInt(str) 12 | 13 | return defaultValue 14 | } 15 | 16 | export const QuantitySelector: React.SFC = props => { 17 | const [quantity, setQuantity] = useState(1) 18 | 19 | const setQuantityValue = (quantity: number) => { 20 | const newQuantity = Math.max(1, quantity) 21 | setQuantity(newQuantity) 22 | props.onQuantityChange(newQuantity) 23 | } 24 | 25 | return ( 26 | 27 | {props.title} 28 | 29 | setQuantityValue(quantity - 1)}> 30 | 31 | 32 | 37 | setQuantityValue( 38 | tryParseInt(event.target.value.replace(/\D/, ''), 1), 39 | ) 40 | } 41 | /> 42 | setQuantityValue(quantity + 1)}> 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | const Container = styled.div` 51 | display: flex; 52 | flex-direction: column; 53 | ` 54 | 55 | const QuantityContainer = styled.div` 56 | display: flex; 57 | flex-direction: row; 58 | ` 59 | 60 | const Title = styled.span` 61 | font-size: 16px; 62 | font-weight: 500; 63 | margin-top: 16px; 64 | margin-bottom: 8px; 65 | ` 66 | 67 | const ValueSelector = styled.button` 68 | background: #fff !important; 69 | border: 1px solid #ced4da; 70 | padding-left: 12px; 71 | font-size: 12px; 72 | font-weight: 900; 73 | line-height: 1; 74 | text-align: center; 75 | white-space: nowrap; 76 | vertical-align: middle; 77 | user-select: none; 78 | outline: 0; 79 | ` 80 | 81 | const NumericInput = styled.input` 82 | padding: 0.375rem 0.75rem; 83 | font-size: 1rem; 84 | line-height: 1.5; 85 | color: #495057; 86 | background-color: #fff; 87 | background-clip: padding-box; 88 | border: 1px solid #ced4da; 89 | text-align: center; 90 | width: 80px; 91 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 92 | z-index: 2; 93 | border-right: none; 94 | border-left: none; 95 | 96 | &:focus { 97 | color: #495057; 98 | background-color: #fff; 99 | border-color: #80bdff; 100 | outline: 0; 101 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 102 | } 103 | ` 104 | -------------------------------------------------------------------------------- /packages/frontend/src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' 2 | import styled from 'styled-components' 3 | 4 | type Option = { label: string; value: string } 5 | 6 | type Props = { 7 | title: string 8 | name: string 9 | setFilter: (name: string, values: any) => void 10 | state: any 11 | options: Option[] 12 | } 13 | 14 | const optionValue = (o: Option) => o.value 15 | const optionLabel = (o: Option) => o.label 16 | 17 | const addOrRemoveOption = (selected: Option[], option: Option) => { 18 | if (isSelected(option, selected)) { 19 | return selected.filter(s => optionValue(s) !== optionValue(option)) 20 | } 21 | 22 | return [...selected, option] 23 | } 24 | 25 | const isSelected = (option: Option, selected: Option[]) => { 26 | return ( 27 | selected.find(s => optionValue(s) === optionValue(option)) !== undefined 28 | ) 29 | } 30 | 31 | function useComponentVisible( 32 | initialIsOpened: boolean, 33 | onVisibilityChange: (visibility: boolean) => void, 34 | ) { 35 | const [opened, setOpened] = useState(initialIsOpened) 36 | const ref = useRef(null) as any 37 | 38 | const handleHideDropdown = (event: KeyboardEvent) => { 39 | if (event.key === 'Escape') { 40 | onVisibilityChange(false) 41 | } 42 | } 43 | 44 | const handleClickOutside = (event: any) => { 45 | if (ref.current && !ref.current.contains(event.target)) { 46 | onVisibilityChange(false) 47 | } 48 | } 49 | 50 | useEffect(() => { 51 | document.addEventListener('keydown', handleHideDropdown, true) 52 | document.addEventListener('click', handleClickOutside, true) 53 | return () => { 54 | document.removeEventListener('keydown', handleHideDropdown, true) 55 | document.removeEventListener('click', handleClickOutside, true) 56 | } 57 | }) 58 | 59 | return { ref, opened, setOpened } 60 | } 61 | 62 | function useOuterClickNotifier(innerRef: any, onOuterClick: () => void) { 63 | useLayoutEffect(() => { 64 | function handleClick(e: any) { 65 | if (innerRef.current && !innerRef.current.contains(e.target)) { 66 | onOuterClick() 67 | } 68 | } 69 | document.addEventListener('click', handleClick) 70 | // called for each previous layout effect 71 | return () => document.removeEventListener('click', handleClick) 72 | }, [onOuterClick, innerRef]) // invoke again, if inputs have changed 73 | } 74 | 75 | export const FilterSelect: React.SFC = props => { 76 | const [selected, setSelected] = useState( 77 | props.state.filters[props.name] || [], 78 | ) 79 | const { ref, opened, setOpened } = useComponentVisible( 80 | false, 81 | newVisibility => { 82 | if (opened && !newVisibility) { 83 | setOpened(false) 84 | 85 | props.setFilter(props.name, selected) 86 | } 87 | }, 88 | ) 89 | 90 | const onChange = (option: { label: string; value: string }) => { 91 | const items = addOrRemoveOption(selected, option) 92 | setSelected(items) 93 | } 94 | 95 | return ( 96 |
97 | 0} 100 | onClick={() => setOpened(!opened)} 101 | > 102 | {props.title} 103 | 104 | {opened && ( 105 | 106 | 107 | {selected.length} selected 108 | 109 | 110 | 111 | {props.options.map(o => ( 112 | 119 | ))} 120 | 121 | 122 | 123 | )} 124 |
125 | ) 126 | } 127 | 128 | const Toggler = styled.div<{ hasSelectedElements: boolean; opened: boolean }>` 129 | padding: 5px; 130 | cursor: pointer; 131 | padding: 7px 12px; 132 | line-height: 1; 133 | letter-spacing: 0.5px; 134 | background: #f3f3f3; 135 | color: #333; 136 | border-radius: 20px; 137 | margin-right: 8px; 138 | 139 | ${props => 140 | props.hasSelectedElements || props.opened 141 | ? `background: #1a1a1a; color: #fff;` 142 | : ''} 143 | 144 | &:hover { 145 | ${props => 146 | props.hasSelectedElements || props.opened 147 | ? 'color: #fff; background-color: #4b4b4b;' 148 | : 'background: #ddd; color: #1a1a1a;'} 149 | } 150 | ` 151 | 152 | const Box = styled.div` 153 | position: absolute; 154 | width: 252px; 155 | padding-top: 11px; 156 | border: 1px solid #ddd; 157 | box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.15); 158 | margin-top: 5px; 159 | background: #fff; 160 | ` 161 | 162 | const ActionPane = styled.div` 163 | color: #1a1a1a; 164 | padding-bottom: 12px; 165 | border-bottom: 1px solid #ddd; 166 | line-height: 21px; 167 | ` 168 | 169 | const AmountSelected = styled.span` 170 | display: inline; 171 | text-align: left; 172 | padding-left: 10px; 173 | ` 174 | 175 | const ActionRight = styled.span` 176 | display: inline; 177 | text-align: right; 178 | display: none; 179 | font-weight: 700; 180 | padding-right: 17px; 181 | cursor: pointer; 182 | ` 183 | 184 | const SelectContainer = styled.div` 185 | overflow-y: auto; 186 | overflow-x: hidden; 187 | min-height: 180px; 188 | max-height: 300px; 189 | height: 40vh; 190 | margin-right: 12px; 191 | color: #1a1a1a; 192 | transform: translateZ(0); 193 | ` 194 | 195 | const List = styled.ul` 196 | list-style: none; 197 | padding: 0; 198 | margin: 0; 199 | outline: none; 200 | overflow-x: hidden; 201 | ` 202 | 203 | const Option = styled.li` 204 | padding: 8px 12px; 205 | cursor: pointer; 206 | list-style-type: none; 207 | display: flex; 208 | align-items: center; 209 | 210 | &:hover { 211 | background-color: #f3f3f3; 212 | } 213 | ` 214 | const CheckBox = styled.span<{ checked: boolean }>` 215 | position: relative; 216 | display: inline-block; 217 | flex-shrink: 0; 218 | width: 24px; 219 | height: 24px; 220 | margin-right: 10px; 221 | text-align: center; 222 | vertical-align: middle; 223 | -webkit-appearance: none; 224 | -moz-appearance: none; 225 | appearance: none; 226 | border-radius: 2px; 227 | border: 1px solid #333; 228 | 229 | ${props => `border: 1px solid ${props.checked ? '#0062b4' : '#333'};}`} 230 | 231 | &:after { 232 | content: ''; 233 | opacity: 0; 234 | position: absolute; 235 | width: 11px; 236 | height: 5px; 237 | top: 8px; 238 | left: 6px; 239 | border: 2px solid #1a1a1a; 240 | border-top: none; 241 | border-right: none; 242 | transform: rotate(-45deg); 243 | transition: opacity 0.1s ease-in-out; 244 | ${props => `opacity: ${props.checked ? 1 : 0};`} 245 | } 246 | ` 247 | 248 | const Label = styled.span` 249 | color: #1a1a1a; 250 | text-decoration: none; 251 | font-size: 14px; 252 | ` 253 | 254 | /** 255 | * opened 256 | * 257 | * background-color: #1a1a1a; 258 | color: #fff; 259 | text-shadow: 0 0 1px #fff; 260 | */ 261 | -------------------------------------------------------------------------------- /packages/frontend/src/components/VariantSelector/VariantSelector.tsx: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import React, { useState } from 'react' 3 | import { 4 | IVariant, 5 | IProductDetailFragment, 6 | IOption, 7 | IOptionValue, 8 | } from '../../generated-types' 9 | import styled from 'styled-components' 10 | import { 11 | getIterableVariants, 12 | getAvailableOptionValues, 13 | sameOptionValue, 14 | isOptionValueAvailable, 15 | OptionValueWithColor, 16 | } from '../../helpers/variants' 17 | 18 | function variantsFromOptionValue( 19 | optionValue: IOptionValue, 20 | variants: IVariant[], 21 | ) { 22 | return variants.filter(variant => { 23 | return variant.optionValues!.some(oValue => oValue.id === optionValue.id) 24 | }) 25 | } 26 | 27 | /** 28 | * Display all options of the product, and return a variant when one is selected 29 | */ 30 | export const VariantSelector: React.SFC<{ 31 | product: IProductDetailFragment 32 | onVariantChange?: (variant?: IVariant) => void 33 | onOptionValueEnter?: (variants: IVariant[]) => void 34 | onOptionValueLeave?: () => void 35 | }> = props => { 36 | const [ 37 | hoveredOptionValue, 38 | setHoveredOptionValue, 39 | ] = useState(null) 40 | const [selectedOptionValues, setSelectedOptionValues] = useState< 41 | IOptionValue[] 42 | >([]) 43 | const iterableVariants = getIterableVariants(props.product.variants!) 44 | const optionValues = 45 | selectedOptionValues.length > 0 46 | ? selectedOptionValues 47 | : hoveredOptionValue !== null 48 | ? [hoveredOptionValue] 49 | : [] 50 | const availableOptionValues = getAvailableOptionValues( 51 | optionValues, 52 | props.product.variants!, 53 | ) 54 | 55 | const clickOnOptionValue = (optionValue: IOptionValue) => { 56 | if (selectedOptionValues.find(s => s.id === optionValue.id)) { 57 | setSelectedOptionValues( 58 | selectedOptionValues.filter(s => s.id !== optionValue.id), 59 | ) 60 | if (props.onVariantChange) { 61 | props.onVariantChange(undefined) 62 | } 63 | } else { 64 | if ( 65 | !selectedOptionValues.find(s => s.option.id === optionValue.option.id) 66 | ) { 67 | const newSelectedOptionValues = [...selectedOptionValues, optionValue] 68 | setSelectedOptionValues(newSelectedOptionValues) 69 | 70 | if ( 71 | newSelectedOptionValues.length === 72 | props.product.variants![0].optionValues!.length 73 | ) { 74 | const selectedOptionsValuesIds = newSelectedOptionValues.map( 75 | s => s.id, 76 | ) 77 | const selectedVariant = props.product.variants!.find(v => 78 | v.optionValues!.every(o => selectedOptionsValuesIds.includes(o.id)), 79 | ) 80 | 81 | if (selectedVariant && props.onVariantChange) { 82 | props.onVariantChange(selectedVariant) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | const onOptionValueEntered = (optionValue: OptionValueWithColor) => { 90 | setHoveredOptionValue(optionValue) 91 | const variants = variantsFromOptionValue( 92 | optionValue, 93 | props.product.variants!, 94 | ) 95 | 96 | if (props.onOptionValueEnter) { 97 | props.onOptionValueEnter(variants) 98 | } 99 | } 100 | 101 | const onOptionValueLeave = () => { 102 | setHoveredOptionValue(null) 103 | 104 | if (props.onOptionValueLeave) { 105 | props.onOptionValueLeave() 106 | } 107 | } 108 | 109 | return ( 110 | 111 | onOptionValueLeave()}> 112 | {iterableVariants.map(([optionName, values]) => ( 113 | 114 | {optionName} 115 |
116 | {values.map(value => { 117 | const isAvailable = isOptionValueAvailable( 118 | value, 119 | optionValues, 120 | availableOptionValues, 121 | ) 122 | const isSelected = 123 | selectedOptionValues.find(s => s.id === value.id) !== 124 | undefined 125 | if (value.isColor) { 126 | return ( 127 | onOptionValueEntered(value)} 131 | available={isAvailable} 132 | selected={isSelected} 133 | onClick={() => isAvailable && clickOnOptionValue(value)} 134 | /> 135 | ) 136 | } else if (optionName === 'Size') { 137 | return ( 138 | onOptionValueEntered(value)} 143 | onClick={() => isAvailable && clickOnOptionValue(value)} 144 | > 145 | {value.name.toUpperCase()} 146 | 147 | ) 148 | } else { 149 | return ( 150 | onOptionValueEntered(value)} 154 | onClick={() => isAvailable && clickOnOptionValue(value)} 155 | > 156 | {value.name} 157 | 158 | ) 159 | } 160 | })} 161 |
162 |
163 | ))} 164 |
165 |
166 | ) 167 | } 168 | 169 | const Container = styled.div`` 170 | 171 | const OptionValuesContainer = styled.div`` 172 | 173 | const OptionValuesGroup = styled.div` 174 | display: flex; 175 | flex-direction: row; 176 | align-items: center; 177 | margin-bottom: 4px; 178 | ` 179 | 180 | const OptionValuesGroupTitle = styled.span` 181 | font-size: 20px; 182 | font-weight: 600; 183 | margin-right: 32px; 184 | ` 185 | 186 | const OptionValue = styled.span<{ available: boolean }>` 187 | font-size: 12px; 188 | padding: 3px; 189 | border-radius: 2px; 190 | margin-right: 2px; 191 | min-width: 20px; 192 | text-align: center; 193 | 194 | ${props => `background-color: ${props.available ? '#0000000d' : 'white'};`} 195 | ${props => `color: ${props.available ? 'black' : '#00000070'};`} 196 | 197 | &:hover { 198 | cursor: pointer; 199 | background-color: #00000014; 200 | } 201 | ` 202 | 203 | const colors: Record = { 204 | red: '#ff7979', 205 | blue: '#686de0', 206 | green: '#badc58', 207 | violet: '#e056fd', 208 | } 209 | 210 | const ColorValue = styled.div<{ 211 | color: string 212 | available: boolean 213 | selected: boolean 214 | }>` 215 | width: 30px; 216 | height: 30px; 217 | border-radius: 100%; 218 | margin-right: 10px; 219 | cursor: pointer; 220 | ${props => 221 | `background-color: ${ 222 | props.available ? colors[props.color.toLowerCase()] : '#e0e0e0' 223 | }`} 224 | 225 | ${props => `border: 1px solid ${props.selected ? 'blue' : '#f7f7f7'};`} 226 | ` 227 | 228 | const SizeValue = styled.div<{ available: boolean; selected: boolean }>` 229 | font-size: 18px; 230 | display: flex; 231 | align-items: center; 232 | justify-content: center; 233 | height: 30px; 234 | width: 30px; 235 | border-radius: 50%; 236 | margin-right: 10px; 237 | cursor: pointer; 238 | text-align: center; 239 | 240 | ${props => (props.available ? 'color: #222;' : 'color: #f7f7f7;')} 241 | ${props => `border: 1px solid ${props.selected ? 'blue' : '#f7f7f7'};`} 242 | ` 243 | -------------------------------------------------------------------------------- /packages/frontend/src/generated-types.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import * as React from 'react' 3 | import * as ReactApollo from 'react-apollo' 4 | export type Maybe = T | null 5 | export type Omit = Pick> 6 | /** All built-in and custom scalars, mapped to their actual values */ 7 | export type Scalars = { 8 | ID: string 9 | String: string 10 | Boolean: boolean 11 | Int: number 12 | Float: number 13 | } 14 | 15 | export type IAttribute = { 16 | __typename?: 'Attribute' 17 | id: Scalars['ID'] 18 | key: Scalars['String'] 19 | products?: Maybe> 20 | value: Scalars['String'] 21 | } 22 | 23 | export type IAttributeProductsArgs = { 24 | skip?: Maybe 25 | after?: Maybe 26 | before?: Maybe 27 | first?: Maybe 28 | last?: Maybe 29 | } 30 | 31 | export type IAttributeCreateManyWithoutAttributesInput = { 32 | create?: Maybe> 33 | connect?: Maybe> 34 | } 35 | 36 | export type IAttributeCreateWithoutProductsInput = { 37 | id?: Maybe 38 | key: Scalars['String'] 39 | value: Scalars['String'] 40 | } 41 | 42 | export type IAttributePayload = { 43 | __typename?: 'AttributePayload' 44 | name: Scalars['String'] 45 | values: Array 46 | } 47 | 48 | export type IAttributeValue = { 49 | __typename?: 'AttributeValue' 50 | id: Scalars['ID'] 51 | value: Scalars['String'] 52 | } 53 | 54 | export type IAttributeWhereUniqueInput = { 55 | id?: Maybe 56 | } 57 | 58 | export type IBrand = { 59 | __typename?: 'Brand' 60 | id: Scalars['ID'] 61 | name: Scalars['String'] 62 | products?: Maybe> 63 | } 64 | 65 | export type IBrandProductsArgs = { 66 | skip?: Maybe 67 | after?: Maybe 68 | before?: Maybe 69 | first?: Maybe 70 | last?: Maybe 71 | } 72 | 73 | export type IBrandCreateOneWithoutBrandInput = { 74 | create?: Maybe 75 | connect?: Maybe 76 | } 77 | 78 | export type IBrandCreateWithoutProductsInput = { 79 | id?: Maybe 80 | name: Scalars['String'] 81 | } 82 | 83 | export type IBrandWhereUniqueInput = { 84 | id?: Maybe 85 | } 86 | 87 | export type ICollection = { 88 | __typename?: 'Collection' 89 | id: Scalars['ID'] 90 | name: Scalars['String'] 91 | products: Array 92 | options: Array 93 | brands: Array 94 | attributes: Array 95 | } 96 | 97 | export type ICollectionProductsArgs = { 98 | optionsValuesIds?: Maybe> 99 | brandsIds?: Maybe> 100 | attributesIds?: Maybe> 101 | first?: Maybe 102 | last?: Maybe 103 | } 104 | 105 | export type ICollectionCreateManyWithoutCollectionsInput = { 106 | create?: Maybe> 107 | connect?: Maybe> 108 | } 109 | 110 | export type ICollectionCreateWithoutProductsInput = { 111 | id?: Maybe 112 | name: Scalars['String'] 113 | rules?: Maybe 114 | } 115 | 116 | export type ICollectionInput = { 117 | name: Scalars['String'] 118 | ruleSet?: Maybe 119 | productsIds?: Maybe> 120 | } 121 | 122 | export type ICollectionRuleCreateManyWithoutRulesInput = { 123 | create?: Maybe> 124 | connect?: Maybe> 125 | } 126 | 127 | export type ICollectionRuleCreateWithoutCollectionRuleSetInput = { 128 | id?: Maybe 129 | field: ICollectionRuleField 130 | relation: ICollectionRuleRelation 131 | value: Scalars['String'] 132 | } 133 | 134 | export enum ICollectionRuleField { 135 | Type = 'TYPE', 136 | Title = 'TITLE', 137 | Price = 'PRICE', 138 | } 139 | 140 | export enum ICollectionRuleRelation { 141 | Contains = 'CONTAINS', 142 | EndsWith = 'ENDS_WITH', 143 | Equals = 'EQUALS', 144 | GreaterThan = 'GREATER_THAN', 145 | LessThan = 'LESS_THAN', 146 | NotContains = 'NOT_CONTAINS', 147 | NotEquals = 'NOT_EQUALS', 148 | StartsWith = 'STARTS_WITH', 149 | } 150 | 151 | export type ICollectionRuleSetCreateOneWithoutRulesInput = { 152 | create?: Maybe 153 | connect?: Maybe 154 | } 155 | 156 | export type ICollectionRuleSetCreateWithoutCollectionInput = { 157 | id?: Maybe 158 | appliesDisjunctively: Scalars['Boolean'] 159 | rules?: Maybe 160 | } 161 | 162 | export type ICollectionRuleSetInput = { 163 | applyDisjunctively: Scalars['Boolean'] 164 | rules: Array 165 | } 166 | 167 | export type ICollectionRuleSetWhereUniqueInput = { 168 | id?: Maybe 169 | } 170 | 171 | export type ICollectionRuleWhereUniqueInput = { 172 | id?: Maybe 173 | } 174 | 175 | export type ICollectionWhereUniqueInput = { 176 | id?: Maybe 177 | } 178 | 179 | export type ICreateProductInput = { 180 | name: Scalars['String'] 181 | slug: Scalars['String'] 182 | brand: IUniqueInput 183 | attributesIds: Array 184 | variants: Array 185 | } 186 | 187 | export type ICreateVariantInput = { 188 | availableForSale: Scalars['Boolean'] 189 | price: Scalars['Int'] 190 | optionsValueIds: Array 191 | } 192 | 193 | export type IImage = { 194 | __typename?: 'Image' 195 | id: Scalars['ID'] 196 | url: Scalars['String'] 197 | } 198 | 199 | export type IImageCreateManyWithoutImagesInput = { 200 | create?: Maybe> 201 | connect?: Maybe> 202 | } 203 | 204 | export type IImageCreateOneWithoutThumbnailInput = { 205 | create?: Maybe 206 | connect?: Maybe 207 | } 208 | 209 | export type IImageCreateWithoutProductInput = { 210 | id?: Maybe 211 | url: Scalars['String'] 212 | variant?: Maybe 213 | } 214 | 215 | export type IImageCreateWithoutVariantInput = { 216 | id?: Maybe 217 | url: Scalars['String'] 218 | product?: Maybe 219 | } 220 | 221 | export type IImageWhereUniqueInput = { 222 | id?: Maybe 223 | } 224 | 225 | export type IMutation = { 226 | __typename?: 'Mutation' 227 | createOneProduct: IProduct 228 | productCreate: IProduct 229 | productDelete: IProduct 230 | productUpdate: IProduct 231 | collectionCreate: ICollection 232 | collectionAddProducts: ICollection 233 | collectionRemoveProducts: ICollection 234 | collectionUpdate: ICollection 235 | } 236 | 237 | export type IMutationCreateOneProductArgs = { 238 | data: IProductCreateInput 239 | } 240 | 241 | export type IMutationProductCreateArgs = { 242 | data: ICreateProductInput 243 | } 244 | 245 | export type IMutationProductDeleteArgs = { 246 | productId: Scalars['ID'] 247 | } 248 | 249 | export type IMutationProductUpdateArgs = { 250 | data: IUpdateProductInput 251 | } 252 | 253 | export type IMutationCollectionCreateArgs = { 254 | collection: ICollectionInput 255 | } 256 | 257 | export type IMutationCollectionAddProductsArgs = { 258 | productIds: Array 259 | collectionId: Scalars['ID'] 260 | } 261 | 262 | export type IMutationCollectionRemoveProductsArgs = { 263 | productIds: Array 264 | collectionId: Scalars['ID'] 265 | } 266 | 267 | export type IMutationCollectionUpdateArgs = { 268 | id: Scalars['ID'] 269 | collection: ICollectionInput 270 | } 271 | 272 | export type IOption = { 273 | __typename?: 'Option' 274 | id: Scalars['ID'] 275 | name: Scalars['String'] 276 | values?: Maybe> 277 | isColor?: Maybe 278 | } 279 | 280 | export type IOptionValuesArgs = { 281 | skip?: Maybe 282 | after?: Maybe 283 | before?: Maybe 284 | first?: Maybe 285 | last?: Maybe 286 | } 287 | 288 | export type IOptionCreateOneWithoutOptionInput = { 289 | create?: Maybe 290 | connect?: Maybe 291 | } 292 | 293 | export type IOptionCreateWithoutValuesInput = { 294 | id?: Maybe 295 | name: Scalars['String'] 296 | isColor?: Maybe 297 | } 298 | 299 | export type IOptionValue = { 300 | __typename?: 'OptionValue' 301 | id: Scalars['ID'] 302 | name: Scalars['String'] 303 | option: IOption 304 | variant?: Maybe 305 | } 306 | 307 | export type IOptionValueCreateManyWithoutOptionValuesInput = { 308 | create?: Maybe> 309 | connect?: Maybe> 310 | } 311 | 312 | export type IOptionValueCreateWithoutVariantInput = { 313 | id?: Maybe 314 | name: Scalars['String'] 315 | option: IOptionCreateOneWithoutOptionInput 316 | } 317 | 318 | export type IOptionValueWhereUniqueInput = { 319 | id?: Maybe 320 | } 321 | 322 | export type IOptionWhereUniqueInput = { 323 | id?: Maybe 324 | } 325 | 326 | export type IProduct = { 327 | __typename?: 'Product' 328 | id: Scalars['ID'] 329 | brand: IBrand 330 | thumbnail?: Maybe 331 | name: Scalars['String'] 332 | variants?: Maybe> 333 | slug: Scalars['String'] 334 | attributes?: Maybe> 335 | } 336 | 337 | export type IProductVariantsArgs = { 338 | skip?: Maybe 339 | after?: Maybe 340 | before?: Maybe 341 | first?: Maybe 342 | last?: Maybe 343 | } 344 | 345 | export type IProductAttributesArgs = { 346 | skip?: Maybe 347 | after?: Maybe 348 | before?: Maybe 349 | first?: Maybe 350 | last?: Maybe 351 | } 352 | 353 | export type IProductCreateInput = { 354 | id?: Maybe 355 | name: Scalars['String'] 356 | slug: Scalars['String'] 357 | description: Scalars['String'] 358 | brand: IBrandCreateOneWithoutBrandInput 359 | thumbnail?: Maybe 360 | type?: Maybe 361 | variants?: Maybe 362 | collections?: Maybe 363 | attributes?: Maybe 364 | } 365 | 366 | export type IProductCreateOneWithoutProductInput = { 367 | create?: Maybe 368 | connect?: Maybe 369 | } 370 | 371 | export type IProductCreateWithoutVariantsInput = { 372 | id?: Maybe 373 | name: Scalars['String'] 374 | slug: Scalars['String'] 375 | description: Scalars['String'] 376 | brand: IBrandCreateOneWithoutBrandInput 377 | thumbnail?: Maybe 378 | type?: Maybe 379 | collections?: Maybe 380 | attributes?: Maybe 381 | } 382 | 383 | export type IProductTypeCreateOneWithoutTypeInput = { 384 | create?: Maybe 385 | connect?: Maybe 386 | } 387 | 388 | export type IProductTypeCreateWithoutProductInput = { 389 | id?: Maybe 390 | name: Scalars['String'] 391 | } 392 | 393 | export type IProductTypeWhereUniqueInput = { 394 | id?: Maybe 395 | } 396 | 397 | export type IProductWhereUniqueInput = { 398 | id?: Maybe 399 | slug?: Maybe 400 | } 401 | 402 | export type IQuery = { 403 | __typename?: 'Query' 404 | products?: Maybe> 405 | product?: Maybe 406 | options?: Maybe> 407 | brands?: Maybe> 408 | collections?: Maybe> 409 | collection: ICollection 410 | } 411 | 412 | export type IQueryProductsArgs = { 413 | skip?: Maybe 414 | after?: Maybe 415 | before?: Maybe 416 | first?: Maybe 417 | last?: Maybe 418 | } 419 | 420 | export type IQueryProductArgs = { 421 | where: IProductWhereUniqueInput 422 | } 423 | 424 | export type IQueryOptionsArgs = { 425 | skip?: Maybe 426 | after?: Maybe 427 | before?: Maybe 428 | first?: Maybe 429 | last?: Maybe 430 | } 431 | 432 | export type IQueryBrandsArgs = { 433 | skip?: Maybe 434 | after?: Maybe 435 | before?: Maybe 436 | first?: Maybe 437 | last?: Maybe 438 | } 439 | 440 | export type IQueryCollectionsArgs = { 441 | skip?: Maybe 442 | after?: Maybe 443 | before?: Maybe 444 | first?: Maybe 445 | last?: Maybe 446 | } 447 | 448 | export type IQueryCollectionArgs = { 449 | collectionId: Scalars['ID'] 450 | } 451 | 452 | export type IRulesInput = { 453 | field: ICollectionRuleField 454 | relation: ICollectionRuleRelation 455 | value: Scalars['String'] 456 | } 457 | 458 | export type IUniqueInput = { 459 | id: Scalars['ID'] 460 | } 461 | 462 | export type IUpdateProductInput = { 463 | id: Scalars['ID'] 464 | name: Scalars['String'] 465 | brand: IUniqueInput 466 | attributesIds: Array 467 | variants: Array 468 | } 469 | 470 | export type IUpdateVariantInput = { 471 | id: Scalars['ID'] 472 | availableForSale: Scalars['Boolean'] 473 | price: Scalars['Int'] 474 | optionsValueIds: Array 475 | } 476 | 477 | export type IVariant = { 478 | __typename?: 'Variant' 479 | id: Scalars['ID'] 480 | availableForSale?: Maybe 481 | images?: Maybe> 482 | optionValues?: Maybe> 483 | price: Scalars['Int'] 484 | product?: Maybe 485 | sku?: Maybe 486 | } 487 | 488 | export type IVariantImagesArgs = { 489 | skip?: Maybe 490 | after?: Maybe 491 | before?: Maybe 492 | first?: Maybe 493 | last?: Maybe 494 | } 495 | 496 | export type IVariantOptionValuesArgs = { 497 | skip?: Maybe 498 | after?: Maybe 499 | before?: Maybe 500 | first?: Maybe 501 | last?: Maybe 502 | } 503 | 504 | export type IVariantCreateManyWithoutVariantsInput = { 505 | create?: Maybe> 506 | connect?: Maybe> 507 | } 508 | 509 | export type IVariantCreateOneWithoutVariantInput = { 510 | create?: Maybe 511 | connect?: Maybe 512 | } 513 | 514 | export type IVariantCreateWithoutImagesInput = { 515 | id?: Maybe 516 | price: Scalars['Int'] 517 | availableForSale?: Maybe 518 | sku?: Maybe 519 | optionValues?: Maybe 520 | product?: Maybe 521 | } 522 | 523 | export type IVariantCreateWithoutProductInput = { 524 | id?: Maybe 525 | price: Scalars['Int'] 526 | availableForSale?: Maybe 527 | sku?: Maybe 528 | optionValues?: Maybe 529 | images?: Maybe 530 | } 531 | 532 | export type IVariantWhereUniqueInput = { 533 | id?: Maybe 534 | } 535 | export type ICollectionQueryVariables = { 536 | collectionId: Scalars['ID'] 537 | brandsIds?: Maybe> 538 | optionsValuesIds?: Maybe> 539 | attributesIds?: Maybe> 540 | } 541 | 542 | export type ICollectionQuery = { __typename?: 'Query' } & { 543 | collection: { __typename?: 'Collection' } & Pick< 544 | ICollection, 545 | 'id' | 'name' 546 | > & { 547 | products: Array<{ __typename?: 'Product' } & ICollectionProductFragment> 548 | attributes: Array< 549 | { __typename?: 'AttributePayload' } & Pick< 550 | IAttributePayload, 551 | 'name' 552 | > & { 553 | values: Array< 554 | { __typename?: 'AttributeValue' } & Pick< 555 | IAttributeValue, 556 | 'id' | 'value' 557 | > 558 | > 559 | } 560 | > 561 | options: Array< 562 | { __typename?: 'Option' } & Pick & { 563 | values: Maybe< 564 | Array< 565 | { __typename?: 'OptionValue' } & Pick< 566 | IOptionValue, 567 | 'id' | 'name' 568 | > 569 | > 570 | > 571 | } 572 | > 573 | brands: Array<{ __typename?: 'Brand' } & Pick> 574 | } 575 | } 576 | 577 | export type ICollectionProductFragment = { __typename?: 'Product' } & Pick< 578 | IProduct, 579 | 'id' | 'name' | 'slug' 580 | > & { 581 | thumbnail: Maybe<{ __typename?: 'Image' } & Pick> 582 | brand: { __typename?: 'Brand' } & Pick 583 | variants: Maybe< 584 | Array< 585 | { __typename?: 'Variant' } & Pick & { 586 | images: Maybe< 587 | Array<{ __typename?: 'Image' } & Pick> 588 | > 589 | optionValues: Maybe< 590 | Array< 591 | { __typename?: 'OptionValue' } & Pick< 592 | IOptionValue, 593 | 'id' | 'name' 594 | > & { 595 | option: { __typename?: 'Option' } & Pick< 596 | IOption, 597 | 'id' | 'name' | 'isColor' 598 | > 599 | } 600 | > 601 | > 602 | } 603 | > 604 | > 605 | } 606 | 607 | export type IProductQueryVariables = { 608 | slug: Scalars['String'] 609 | } 610 | 611 | export type IProductQuery = { __typename?: 'Query' } & { 612 | product: Maybe<{ __typename?: 'Product' } & IProductDetailFragment> 613 | } 614 | 615 | export type IProductDetailFragment = { __typename?: 'Product' } & Pick< 616 | IProduct, 617 | 'id' | 'name' | 'slug' 618 | > & { 619 | thumbnail: Maybe<{ __typename?: 'Image' } & Pick> 620 | brand: { __typename?: 'Brand' } & Pick 621 | attributes: Maybe< 622 | Array< 623 | { __typename?: 'Attribute' } & Pick 624 | > 625 | > 626 | variants: Maybe< 627 | Array< 628 | { __typename?: 'Variant' } & Pick & { 629 | images: Maybe< 630 | Array<{ __typename?: 'Image' } & Pick> 631 | > 632 | optionValues: Maybe< 633 | Array< 634 | { __typename?: 'OptionValue' } & Pick< 635 | IOptionValue, 636 | 'id' | 'name' 637 | > & { 638 | option: { __typename?: 'Option' } & Pick< 639 | IOption, 640 | 'id' | 'name' | 'isColor' 641 | > 642 | } 643 | > 644 | > 645 | } 646 | > 647 | > 648 | } 649 | 650 | export type ICollectionsQueryVariables = {} 651 | 652 | export type ICollectionsQuery = { __typename?: 'Query' } & { 653 | collections: Maybe< 654 | Array<{ __typename?: 'Collection' } & Pick> 655 | > 656 | } 657 | export const CollectionProductFragmentDoc = gql` 658 | fragment CollectionProduct on Product { 659 | id 660 | name 661 | slug 662 | thumbnail { 663 | url 664 | } 665 | brand { 666 | id 667 | name 668 | } 669 | variants { 670 | id 671 | price 672 | images { 673 | id 674 | url 675 | } 676 | optionValues { 677 | id 678 | name 679 | option { 680 | id 681 | name 682 | isColor 683 | } 684 | } 685 | } 686 | } 687 | ` 688 | export const ProductDetailFragmentDoc = gql` 689 | fragment ProductDetail on Product { 690 | id 691 | name 692 | slug 693 | thumbnail { 694 | url 695 | } 696 | brand { 697 | id 698 | name 699 | } 700 | attributes { 701 | id 702 | key 703 | value 704 | } 705 | variants { 706 | id 707 | price 708 | images { 709 | id 710 | url 711 | } 712 | optionValues { 713 | id 714 | name 715 | option { 716 | id 717 | name 718 | isColor 719 | } 720 | } 721 | } 722 | } 723 | ` 724 | export const CollectionDocument = gql` 725 | query collection( 726 | $collectionId: ID! 727 | $brandsIds: [ID!] 728 | $optionsValuesIds: [ID!] 729 | $attributesIds: [ID!] 730 | ) { 731 | collection(collectionId: $collectionId) { 732 | id 733 | name 734 | products( 735 | brandsIds: $brandsIds 736 | optionsValuesIds: $optionsValuesIds 737 | attributesIds: $attributesIds 738 | ) { 739 | ...CollectionProduct 740 | } 741 | attributes { 742 | name 743 | values { 744 | id 745 | value 746 | } 747 | } 748 | options { 749 | id 750 | name 751 | values { 752 | id 753 | name 754 | } 755 | } 756 | brands { 757 | id 758 | name 759 | } 760 | } 761 | } 762 | ${CollectionProductFragmentDoc} 763 | ` 764 | export type CollectionComponentProps = Omit< 765 | ReactApollo.QueryProps, 766 | 'query' 767 | > & 768 | ({ variables: ICollectionQueryVariables; skip?: false } | { skip: true }) 769 | 770 | export const CollectionComponent = (props: CollectionComponentProps) => ( 771 | 772 | query={CollectionDocument} 773 | {...props} 774 | /> 775 | ) 776 | 777 | export type ICollectionProps = Partial< 778 | ReactApollo.DataProps 779 | > & 780 | TChildProps 781 | export function withCollection( 782 | operationOptions?: ReactApollo.OperationOption< 783 | TProps, 784 | ICollectionQuery, 785 | ICollectionQueryVariables, 786 | ICollectionProps 787 | >, 788 | ) { 789 | return ReactApollo.withQuery< 790 | TProps, 791 | ICollectionQuery, 792 | ICollectionQueryVariables, 793 | ICollectionProps 794 | >(CollectionDocument, { 795 | alias: 'withCollection', 796 | ...operationOptions, 797 | }) 798 | } 799 | export const ProductDocument = gql` 800 | query product($slug: String!) { 801 | product(where: { slug: $slug }) { 802 | ...ProductDetail 803 | } 804 | } 805 | ${ProductDetailFragmentDoc} 806 | ` 807 | export type ProductComponentProps = Omit< 808 | ReactApollo.QueryProps, 809 | 'query' 810 | > & 811 | ({ variables: IProductQueryVariables; skip?: false } | { skip: true }) 812 | 813 | export const ProductComponent = (props: ProductComponentProps) => ( 814 | 815 | query={ProductDocument} 816 | {...props} 817 | /> 818 | ) 819 | 820 | export type IProductProps = Partial< 821 | ReactApollo.DataProps 822 | > & 823 | TChildProps 824 | export function withProduct( 825 | operationOptions?: ReactApollo.OperationOption< 826 | TProps, 827 | IProductQuery, 828 | IProductQueryVariables, 829 | IProductProps 830 | >, 831 | ) { 832 | return ReactApollo.withQuery< 833 | TProps, 834 | IProductQuery, 835 | IProductQueryVariables, 836 | IProductProps 837 | >(ProductDocument, { 838 | alias: 'withProduct', 839 | ...operationOptions, 840 | }) 841 | } 842 | export const CollectionsDocument = gql` 843 | query collections { 844 | collections { 845 | id 846 | name 847 | } 848 | } 849 | ` 850 | export type CollectionsComponentProps = Omit< 851 | ReactApollo.QueryProps, 852 | 'query' 853 | > 854 | 855 | export const CollectionsComponent = (props: CollectionsComponentProps) => ( 856 | 857 | query={CollectionsDocument} 858 | {...props} 859 | /> 860 | ) 861 | 862 | export type ICollectionsProps = Partial< 863 | ReactApollo.DataProps 864 | > & 865 | TChildProps 866 | export function withCollections( 867 | operationOptions?: ReactApollo.OperationOption< 868 | TProps, 869 | ICollectionsQuery, 870 | ICollectionsQueryVariables, 871 | ICollectionsProps 872 | >, 873 | ) { 874 | return ReactApollo.withQuery< 875 | TProps, 876 | ICollectionsQuery, 877 | ICollectionsQueryVariables, 878 | ICollectionsProps 879 | >(CollectionsDocument, { 880 | alias: 'withCollections', 881 | ...operationOptions, 882 | }) 883 | } 884 | -------------------------------------------------------------------------------- /packages/frontend/src/helpers/variants.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { 3 | ICollectionProductFragment, 4 | IOptionValue, 5 | IVariant, 6 | } from '../generated-types' 7 | 8 | export type OptionValueWithColor = IOptionValue & { isColor: boolean } 9 | 10 | export function getDisplayPrice(product: ICollectionProductFragment) { 11 | const variant = _.minBy(product.variants, v => v.price) 12 | 13 | return (variant && variant.price) || 1180 14 | } 15 | 16 | export function sameOptionValue( 17 | a: IOptionValue | null, 18 | b: IOptionValue | null 19 | ) { 20 | if (!a || !b) { 21 | return false 22 | } 23 | 24 | return a.id === b.id 25 | } 26 | 27 | export function getAvailableOptionValues( 28 | optionValues: IOptionValue[], 29 | variants: ReadonlyArray 30 | ) { 31 | const output: IOptionValue[] = [] 32 | 33 | variants.forEach(variant => { 34 | const optionsValuesAreInVariant = variant.optionValues!.find(o => 35 | optionValues.every(inputOptionValue => 36 | sameOptionValue(o, inputOptionValue) 37 | ) 38 | ) 39 | 40 | if (optionsValuesAreInVariant) { 41 | output.push(...variant.optionValues!.flatMap(o => o)) 42 | } 43 | }) 44 | 45 | return _.uniqBy(output, o => o.id) 46 | } 47 | 48 | export function getIterableVariants(variants: ReadonlyArray) { 49 | let output: Record = {} 50 | 51 | variants.forEach(variant => { 52 | variant.optionValues!.forEach(v => { 53 | const identifier = v.option.name 54 | 55 | if (!output[identifier]) { 56 | output[identifier] = [] 57 | } 58 | 59 | if (!output[identifier].find(value => value.id === v.id)) { 60 | output[identifier].push({ 61 | ...v, 62 | isColor: v.option.isColor ? true : false, 63 | }) 64 | } 65 | }) 66 | }) 67 | 68 | return _.orderBy(Object.entries(output), ([optionName]) => optionName) 69 | } 70 | 71 | export function isOptionValueAvailable( 72 | optionValue: IOptionValue, 73 | selectedOptionValues: IOptionValue[], 74 | availableOptionValues: IOptionValue[] | null 75 | ) { 76 | if (!availableOptionValues || selectedOptionValues.length === 0) { 77 | return true 78 | } 79 | 80 | if (selectedOptionValues.find(s => s.id === optionValue.id)) { 81 | return true 82 | } 83 | 84 | return ( 85 | availableOptionValues.find(o => sameOptionValue(o, optionValue)) !== 86 | undefined 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /packages/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import * as serviceWorker from './serviceWorker' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister() 13 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Collection/Collection.graphql: -------------------------------------------------------------------------------- 1 | query collection( 2 | $collectionId: ID! 3 | $brandsIds: [ID!] 4 | $optionsValuesIds: [ID!] 5 | $attributesIds: [ID!] 6 | ) { 7 | collection(collectionId: $collectionId) { 8 | id 9 | name 10 | products( 11 | brandsIds: $brandsIds 12 | optionsValuesIds: $optionsValuesIds 13 | attributesIds: $attributesIds 14 | ) { 15 | ...CollectionProduct 16 | } 17 | attributes { 18 | name 19 | values { 20 | id 21 | value 22 | } 23 | } 24 | options { 25 | id 26 | name 27 | values { 28 | id 29 | name 30 | } 31 | } 32 | brands { 33 | id 34 | name 35 | } 36 | } 37 | } 38 | 39 | fragment CollectionProduct on Product { 40 | id 41 | name 42 | slug 43 | thumbnail { 44 | url 45 | } 46 | brand { 47 | id 48 | name 49 | } 50 | variants { 51 | id 52 | price 53 | images { 54 | id 55 | url 56 | } 57 | optionValues { 58 | id 59 | name 60 | option { 61 | id 62 | name 63 | isColor 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Collection/Collection.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { DataProps } from 'react-apollo' 3 | import { RouteComponentProps } from 'react-router' 4 | import { Link } from 'react-router-dom' 5 | import styled from 'styled-components' 6 | import { CardProduct } from '../../components/CardProduct/CardProduct' 7 | import { FilterSelect } from '../../components/Select/Select' 8 | import { 9 | ICollectionQuery, 10 | ICollectionQueryVariables, 11 | } from '../../generated-types' 12 | 13 | interface RouterProps { 14 | collectionId: string 15 | } 16 | 17 | export type CollectionProps = DataProps< 18 | ICollectionQuery, 19 | ICollectionQueryVariables 20 | > & 21 | RouteComponentProps 22 | 23 | interface CollectionState { 24 | filters: Record 25 | visible: boolean 26 | } 27 | 28 | const idFromValue = (o: { label: string; value: any }) => o.value 29 | 30 | class Collection extends Component { 31 | constructor(props: Readonly) { 32 | super(props) 33 | 34 | this.state = { 35 | filters: { 36 | brands: [], 37 | attributes: [], 38 | }, 39 | visible: false, 40 | } 41 | 42 | this.setFilter = this.setFilter.bind(this) 43 | } 44 | 45 | async setFilter(name: string, values: any) { 46 | const newFilters = { ...this.state.filters, [name]: values } 47 | 48 | this.setState({ filters: newFilters }) 49 | 50 | const optionsValuesIds = this.props.data 51 | .collection!.options.flatMap(o => newFilters[o.name] || []) 52 | .map(idFromValue) 53 | const attributesIds = this.props.data 54 | .collection!.attributes.flatMap(a => newFilters[a.name] || []) 55 | .map(idFromValue) 56 | 57 | await this.props.data.refetch({ 58 | collectionId: this.props.match.params.collectionId, 59 | brandsIds: newFilters['brands'].map(idFromValue), 60 | optionsValuesIds: optionsValuesIds, 61 | attributesIds, 62 | }) 63 | } 64 | 65 | render() { 66 | if (this.props.data.loading) { 67 | return
Loading...
68 | } 69 | 70 | if (!this.props.data.collection) { 71 | return
Collection not found
72 | } 73 | 74 | const brands = this.props.data.collection!.brands.map(b => ({ 75 | label: b.name, 76 | value: b.id, 77 | })) 78 | 79 | return ( 80 | 81 | {this.props.data.collection.name} 82 | 83 | 90 | {this.props.data.collection!.options.map(o => { 91 | const options = o.values!.map(v => ({ 92 | label: v.name, 93 | value: v.id, 94 | })) 95 | 96 | return ( 97 | 105 | ) 106 | })} 107 | {this.props.data.collection.attributes.map(a => { 108 | const values = a.values.map(v => ({ label: v.value, value: v.id })) 109 | return ( 110 | 118 | ) 119 | })} 120 | 121 | 122 | {this.props.data.collection!.products.map(product => ( 123 | 124 | ))} 125 | 126 | 127 | ) 128 | } 129 | } 130 | 131 | export default Collection 132 | 133 | const CollectionContainer = styled.div` 134 | display: flex; 135 | flex-direction: column; 136 | margin: 20px; 137 | ` 138 | 139 | const CollectionTitle = styled.h1` 140 | font-size: 30px; 141 | ` 142 | 143 | const CollectionFilterContainer = styled.div` 144 | display: flex; 145 | flex-direction: row; 146 | align-items: center; 147 | margin-bottom: 16px; 148 | ` 149 | 150 | const ProductsContainer = styled.section` 151 | display: grid; 152 | grid-template-columns: repeat(auto-fill, 300px); 153 | grid-gap: 1rem; 154 | justify-content: space-between; 155 | 156 | /* boring properties */ 157 | list-style: none; 158 | padding: 1rem; 159 | width: 100%; 160 | margin: 0 auto; 161 | ` 162 | 163 | const ProductContainer = styled(Link)` 164 | box-sizing: border-box; 165 | text-decoration: none; 166 | color: black; 167 | ` 168 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Collection/CollectionContainer.ts: -------------------------------------------------------------------------------- 1 | import { withCollection } from '../../generated-types' 2 | import Collection, { CollectionProps } from './Collection' 3 | 4 | export default withCollection({ 5 | options: props => ({ 6 | variables: { 7 | collectionId: props.match.params.collectionId, 8 | }, 9 | }), 10 | })(Collection) 11 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Product/Product.graphql: -------------------------------------------------------------------------------- 1 | query product($slug: String!) { 2 | product(where: { slug: $slug }) { 3 | ...ProductDetail 4 | } 5 | } 6 | 7 | fragment ProductDetail on Product { 8 | id 9 | name 10 | slug 11 | thumbnail { 12 | url 13 | } 14 | brand { 15 | id 16 | name 17 | } 18 | attributes { 19 | id 20 | key 21 | value 22 | } 23 | variants { 24 | id 25 | price 26 | images { 27 | id 28 | url 29 | } 30 | optionValues { 31 | id 32 | name 33 | option { 34 | id 35 | name 36 | isColor 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Product/Product.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import React, { useState } from 'react' 3 | import { DataProps } from 'react-apollo' 4 | import { RouteComponentProps } from 'react-router' 5 | import styled from 'styled-components' 6 | import { 7 | AttributeName, 8 | AttributesTable, 9 | AttributeValue, 10 | } from '../../components/AttributesTable/AttributesTable' 11 | import { BuyButton } from '../../components/BuyButton/BuyButton' 12 | import { QuantitySelector } from '../../components/QuantitySelector/QuantitySelector' 13 | import { VariantSelector } from '../../components/VariantSelector/VariantSelector' 14 | import { 15 | IProductQuery, 16 | IProductQueryVariables, 17 | IVariant, 18 | } from '../../generated-types' 19 | 20 | interface RouterProps { 21 | slug: string 22 | } 23 | 24 | export type ProductProps = DataProps & 25 | RouteComponentProps 26 | 27 | export const Product: React.SFC = props => { 28 | if (props.data.loading) { 29 | return
Loading...
30 | } 31 | 32 | if (!props.data.product) { 33 | return
Product not found
34 | } 35 | 36 | const [selectedVariant, setSelectedVariant] = useState( 37 | props.data.product.variants![0] 38 | ) 39 | const [locked, setLocked] = useState(false) 40 | const [bigThumbnail, setBigThumbnail] = useState(selectedVariant.images![0]) 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | {selectedVariant.images!.map(image => ( 48 | { 51 | setBigThumbnail(image) 52 | }} 53 | > 54 | 55 | 56 | ))} 57 | 58 |
59 | 60 |
61 |
62 | 63 |

64 | {props.data.product.brand.name} 65 |

66 |

74 | {props.data.product.name} 75 |

76 | 77 | {selectedVariant.price / 100} €{' '} 78 | 79 | { 82 | const hasOnlyOneColorVariant = 83 | _(variants!) 84 | .flatMap(v => v.optionValues!) 85 | .uniqBy(v => v.id) 86 | .filter(v => !!v.option.isColor) 87 | .value().length === 1 88 | 89 | if (hasOnlyOneColorVariant && !locked) { 90 | setSelectedVariant(variants[0]) 91 | setBigThumbnail(variants[0].images![0]) 92 | } 93 | }} 94 | onVariantChange={variant => { 95 | if (variant) { 96 | setSelectedVariant(variant) 97 | setLocked(true) 98 | } else { 99 | setLocked(false) 100 | } 101 | }} 102 | /> 103 | 104 | {}} 107 | /> 108 | 109 |
110 | Add to cart 111 | Buy now 112 |
113 | 114 | Product description 115 | 116 | Sed ut perspiciatis, unde omnis iste natus error sit voluptatem 117 | accusantium doloremque laudantium, totam rem aperiam eaque ipsa, 118 | quae ab illo inventore veritatis et quasi architecto beatae vitae 119 | dicta sunt, explicabo. Nemo enim ipsam voluptatem, 120 | 121 | 122 |
123 |
124 | 125 | 126 | Attributes 127 | 128 | 129 | {props.data.product.attributes!.map(a => ( 130 | 131 | {a.key} 132 | {a.value} 133 | 134 | ))} 135 | 136 | 137 | 138 | 139 |
140 | ) 141 | } 142 | 143 | const Container = styled.div` 144 | display: flex; 145 | flex: 1; 146 | flex-direction: column; 147 | padding: 32px; 148 | ` 149 | 150 | const TopContainer = styled.div` 151 | display: flex; 152 | flex: 1; 153 | flex-direction: row; 154 | margin-bottom: 40px; 155 | ` 156 | 157 | const BottomContainer = styled.div` 158 | display: flex; 159 | flex: 1; 160 | flex-direction: row; 161 | ` 162 | 163 | const ImageContainer = styled.div` 164 | display: flex; 165 | flex-direction: row; 166 | flex-basis: 50vh; 167 | margin-right: 32px; 168 | ` 169 | 170 | const ThumbnailListContainer = styled.div` 171 | display: flex; 172 | flex-direction: column; 173 | margin-right: 8px; 174 | ` 175 | 176 | const ThumbnailContainer = styled.div` 177 | width: 44px; 178 | margin-bottom: 8px; 179 | cursor: pointer; 180 | ` 181 | 182 | const Thumbnail = styled.img` 183 | width: 100%; 184 | height: 100%; 185 | ` 186 | 187 | const BigThumnail = styled.img` 188 | width: 100%; 189 | min-width: 320px; 190 | object-fit: contain; 191 | ` 192 | 193 | const DetailsContainer = styled.div` 194 | display: flex; 195 | flex-direction: column; 196 | ` 197 | 198 | const QuantityContainer = styled.div` 199 | margin-bottom: 32px; 200 | ` 201 | 202 | const DescriptionContainer = styled.div` 203 | padding-top: 15px; 204 | padding-bottom: 20px; 205 | border-top: 1px dashed #ddd; 206 | ` 207 | 208 | const DescriptionTitle = styled.h6` 209 | color: #222; 210 | font-weight: 700; 211 | margin: 0; 212 | ` 213 | 214 | const DescriptionContent = styled.p` 215 | color: #777; 216 | ` 217 | 218 | const AttributesTableContainer = styled.div` 219 | width: 50%; 220 | ` 221 | 222 | const AttributesTitle = styled.h6` 223 | text-transform: uppercase; 224 | border: 0; 225 | font-size: 18px; 226 | font-weight: 600; 227 | margin: 0; 228 | margin-bottom: 16px; 229 | ` 230 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Product/ProductContainer.ts: -------------------------------------------------------------------------------- 1 | import { withProduct } from '../../generated-types' 2 | import { Product, ProductProps } from './Product' 3 | 4 | export default withProduct({ 5 | options: props => ({ 6 | variables: { 7 | slug: props.match.params.slug, 8 | }, 9 | }), 10 | })(Product) 11 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Welcome/Welcome.graphql: -------------------------------------------------------------------------------- 1 | query collections { 2 | collections { 3 | id 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { DataProps } from 'react-apollo' 3 | import { 4 | ICollectionsQuery, 5 | ICollectionsQueryVariables, 6 | } from '../../generated-types' 7 | import { NavItem, NavBar } from '../../components/NavBar/NavBar' 8 | 9 | export interface WelcomeProps 10 | extends DataProps {} 11 | 12 | class Welcome extends Component { 13 | render() { 14 | if (this.props.data.loading) { 15 | return
Loading...
16 | } 17 | 18 | if (!this.props.data.collections) { 19 | return
No collections found
20 | } 21 | 22 | const items: NavItem[] = this.props.data.collections.map( 23 | collection => 24 | ({ 25 | label: collection.name, 26 | link: `/collection/${collection.id}`, 27 | }), 28 | ) 29 | 30 | return 31 | } 32 | } 33 | 34 | export default Welcome 35 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/Welcome/WelcomeContainer.ts: -------------------------------------------------------------------------------- 1 | import { withCollections } from '../../generated-types' 2 | import Welcome, { WelcomeProps } from './Welcome' 3 | 4 | export default withCollections({})(Welcome) 5 | -------------------------------------------------------------------------------- /packages/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | type Config = { 14 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 15 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 16 | }; 17 | 18 | const isLocalhost = Boolean( 19 | window.location.hostname === 'localhost' || 20 | // [::1] is the IPv6 localhost address. 21 | window.location.hostname === '[::1]' || 22 | // 127.0.0.1/8 is considered localhost for IPv4. 23 | window.location.hostname.match( 24 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 25 | ) 26 | ); 27 | 28 | export function register(config: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 32 | if (publicUrl.origin !== window.location.origin) { 33 | // Our service worker won't work if PUBLIC_URL is on a different origin 34 | // from what our page is served on. This might happen if a CDN is used to 35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 36 | return; 37 | } 38 | 39 | window.addEventListener('load', () => { 40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 41 | 42 | if (isLocalhost) { 43 | // This is running on localhost. Let's check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl, config); 45 | 46 | // Add some additional logging to localhost, pointing developers to the 47 | // service worker/PWA documentation. 48 | navigator.serviceWorker.ready.then(() => { 49 | console.log( 50 | 'This web app is being served cache-first by a service ' + 51 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 52 | ); 53 | }); 54 | } else { 55 | // Is not localhost. Just register service worker 56 | registerValidSW(swUrl, config); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | function registerValidSW(swUrl: string, config?: Config) { 63 | navigator.serviceWorker 64 | .register(swUrl) 65 | .then(registration => { 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing; 68 | if (installingWorker == null) { 69 | return; 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === 'installed') { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | 'New content is available and will be used when all ' + 79 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 80 | ); 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration); 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It's the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log('Content is cached for offline use.'); 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration); 95 | } 96 | } 97 | } 98 | }; 99 | }; 100 | }) 101 | .catch(error => { 102 | console.error('Error during service worker registration:', error); 103 | }); 104 | } 105 | 106 | function checkValidServiceWorker(swUrl: string, config?: Config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl) 109 | .then(response => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get('content-type'); 112 | if ( 113 | response.status === 404 || 114 | (contentType != null && contentType.indexOf('javascript') === -1) 115 | ) { 116 | // No service worker found. Probably a different app. Reload the page. 117 | navigator.serviceWorker.ready.then(registration => { 118 | registration.unregister().then(() => { 119 | window.location.reload(); 120 | }); 121 | }); 122 | } else { 123 | // Service worker found. Proceed as normal. 124 | registerValidSW(swUrl, config); 125 | } 126 | }) 127 | .catch(() => { 128 | console.log( 129 | 'No internet connection found. App is running in offline mode.' 130 | ); 131 | }); 132 | } 133 | 134 | export function unregister() { 135 | if ('serviceWorker' in navigator) { 136 | navigator.serviceWorker.ready.then(registration => { 137 | registration.unregister(); 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "lib": ["esnext", "dom"], 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/ .nowignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /packages/server/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "webshop-server", 4 | "builds": [ 5 | { 6 | "src": "dist/src/index.js", 7 | "use": "@now/node-server" 8 | } 9 | ], 10 | "env": { 11 | "PRISMA_ENDPOINT": "https://eu1.prisma.sh/flavian/webshop-server/staging" 12 | }, 13 | "routes": [{ "src": "/.*", "dest": "dist/src/index.js" }] 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/output.json: -------------------------------------------------------------------------------- 1 | yarn run v1.13.0 2 | $ /Users/flavian/projects/prisma/webshop/packages/server/node_modules/.bin/ts-node-dev ./prisma/seed.ts 3 | Using ts-node version 8.3.0, typescript version 3.5.2 4 | [ 5 | { 6 | "variants": [ 7 | { 8 | "id": "cjx27zorc0060qvxuw7f3gaav", 9 | "optionValues": [] 10 | }, 11 | { 12 | "id": "cjx27zors0063qvxunq8r7870", 13 | "optionValues": [] 14 | }, 15 | { 16 | "id": "cjx27zos50070qvxufxnbrems", 17 | "optionValues": [] 18 | }, 19 | { 20 | "id": "cjx27zosh0079qvxu6m6denb9", 21 | "optionValues": [] 22 | }, 23 | { 24 | "id": "cjx27zost0086qvxubdxcxk8e", 25 | "optionValues": [] 26 | }, 27 | { 28 | "id": "cjx27zot40095qvxuadimq9ll", 29 | "optionValues": [] 30 | }, 31 | { 32 | "id": "cjx27zotg0098qvxuk0ops8w2", 33 | "optionValues": [] 34 | }, 35 | { 36 | "id": "cjx27zotq0105qvxuuhamziky", 37 | "optionValues": [] 38 | }, 39 | { 40 | "id": "cjx27zou10107qvxuzvepwer2", 41 | "optionValues": [] 42 | }, 43 | { 44 | "id": "cjx27zoua0111qvxulvodrz85", 45 | "optionValues": [] 46 | } 47 | ] 48 | }, 49 | { 50 | "variants": [ 51 | { 52 | "id": "cjx281rp60002sexue3d3pzsm", 53 | "optionValues": [] 54 | }, 55 | { 56 | "id": "cjx281rq40004sexut4io4kep", 57 | "optionValues": [] 58 | }, 59 | { 60 | "id": "cjx281rqm0013sexuyeewc4nz", 61 | "optionValues": [] 62 | }, 63 | { 64 | "id": "cjx281rqw0022sexui0gfth15", 65 | "optionValues": [] 66 | }, 67 | { 68 | "id": "cjx281rrf0024sexuu0nulzyi", 69 | "optionValues": [] 70 | }, 71 | { 72 | "id": "cjx281rry0028sexuoxhtfnad", 73 | "optionValues": [] 74 | }, 75 | { 76 | "id": "cjx281rsg0036sexu0hsqxyl5", 77 | "optionValues": [] 78 | }, 79 | { 80 | "id": "cjx281rvb0043sexub40gk2ew", 81 | "optionValues": [] 82 | }, 83 | { 84 | "id": "cjx281rwj0050sexuacsuxu9x", 85 | "optionValues": [] 86 | }, 87 | { 88 | "id": "cjx281rwz0057sexu6dvfiozs", 89 | "optionValues": [] 90 | }, 91 | { 92 | "id": "cjx281rxi0063sexu1zmqk2aj", 93 | "optionValues": [] 94 | }, 95 | { 96 | "id": "cjx281rxu0071sexu1aig7yyo", 97 | "optionValues": [] 98 | }, 99 | { 100 | "id": "cjx281ryp0073sexugf5bqdvd", 101 | "optionValues": [] 102 | }, 103 | { 104 | "id": "cjx281rz70080sexuxzm0y33s", 105 | "optionValues": [] 106 | }, 107 | { 108 | "id": "cjx281rzf0087sexuy9louz5u", 109 | "optionValues": [] 110 | }, 111 | { 112 | "id": "cjx281rzs0089sexumkbgtur9", 113 | "optionValues": [] 114 | } 115 | ] 116 | }, 117 | { 118 | "variants": [ 119 | { 120 | "id": "cjx284x9w0002yhxuzsdppufy", 121 | "optionValues": [] 122 | }, 123 | { 124 | "id": "cjx284xa80011yhxu7pov0y6s", 125 | "optionValues": [ 126 | { 127 | "id": "cjx27zoqn0052qvxupfnktzx5", 128 | "name": "Violet" 129 | } 130 | ] 131 | }, 132 | { 133 | "id": "cjx284xai0017yhxusib40u4e", 134 | "optionValues": [] 135 | }, 136 | { 137 | "id": "cjx284xar0024yhxuwdfvy0jb", 138 | "optionValues": [] 139 | }, 140 | { 141 | "id": "cjx284xb10029yhxu1zilm2ld", 142 | "optionValues": [] 143 | }, 144 | { 145 | "id": "cjx284xba0036yhxu6n392ls9", 146 | "optionValues": [ 147 | { 148 | "id": "cjx27zoqn0051qvxu84tmx7g4", 149 | "name": "Green" 150 | } 151 | ] 152 | }, 153 | { 154 | "id": "cjx284xbk0042yhxuxj9pgqlh", 155 | "optionValues": [] 156 | }, 157 | { 158 | "id": "cjx284xbx0045yhxuzc02zhoq", 159 | "optionValues": [ 160 | { 161 | "id": "cjx27zoqp0054qvxus4grtzdq", 162 | "name": "S" 163 | } 164 | ] 165 | }, 166 | { 167 | "id": "cjx284xc70054yhxuswdjmufm", 168 | "optionValues": [ 169 | { 170 | "id": "cjx27zoqq0056qvxubksmfax1", 171 | "name": "L" 172 | } 173 | ] 174 | }, 175 | { 176 | "id": "cjx284xci0062yhxuoi8d21im", 177 | "optionValues": [] 178 | }, 179 | { 180 | "id": "cjx284xcp0064yhxuhsqc5si3", 181 | "optionValues": [ 182 | { 183 | "id": "cjx27zoqn0049qvxu739dgw95", 184 | "name": "Red" 185 | }, 186 | { 187 | "id": "cjx27zoqq0057qvxulhg5h80y", 188 | "name": "XL" 189 | } 190 | ] 191 | }, 192 | { 193 | "id": "cjx284xd20067yhxu7i5p744o", 194 | "optionValues": [ 195 | { 196 | "id": "cjx27zoqn0050qvxul46ksvho", 197 | "name": "Blue" 198 | }, 199 | { 200 | "id": "cjx27zoqq0055qvxu2nfo5nfg", 201 | "name": "M" 202 | } 203 | ] 204 | } 205 | ] 206 | } 207 | ] 208 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webshop-server", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "ts-node-dev --respawn --no-notify --transpileOnly ./src/index.ts", 7 | "dev": "yoga dev", 8 | "build": "yoga build", 9 | "seed": "yarn ts-node ./prisma/seed.ts", 10 | "generate": "prisma2 generate", 11 | "install": "yarn generate", 12 | "uninstall": "yarn generate" 13 | }, 14 | "dependencies": { 15 | "@prisma/nexus": "^0.0.1", 16 | "apollo-server": "^2.6.3", 17 | "graphql": "^14.3.1", 18 | "lodash": "^4.17.11", 19 | "typescript": "^3.5.2" 20 | }, 21 | "devDependencies": { 22 | "@types/faker": "^4.1.4", 23 | "@types/graphql": "14.0.3", 24 | "@types/lodash": "^4.14.118", 25 | "@types/node": "12.0.8", 26 | "faker": "^4.1.0", 27 | "prisma2": "^0.0.83", 28 | "ts-node-dev": "^1.0.0-pre.40", 29 | "typescript": "^3.5.2" 30 | }, 31 | "prettier": { 32 | "singleQuote": true, 33 | "bracketSpacing": true, 34 | "printWidth": 80, 35 | "trailingComma": "all", 36 | "semi": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/prisma/db/next.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluidstackdev/fluidstack/c9b47df985d70fb5134d252723dc087638b1de69/packages/server/prisma/db/next.db -------------------------------------------------------------------------------- /packages/server/prisma/migrations/20190618213953-init/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20190618213953-init` 2 | 3 | This migration has been generated by Flavian DESVERNE at 6/18/2019, 9:39:53 PM. 4 | You can check out the [state of the datamodel](./datamodel.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "next"."Collection"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,"rules" TEXT REFERENCES CollectionRuleSet(id),PRIMARY KEY ("id")); 10 | 11 | CREATE TABLE "next"."CollectionRuleSet"("id" TEXT NOT NULL ,"appliesDisjunctively" BOOLEAN NOT NULL DEFAULT false ,PRIMARY KEY ("id")); 12 | 13 | CREATE TABLE "next"."CollectionRule"("id" TEXT NOT NULL ,"field" TEXT NOT NULL DEFAULT 'TYPE' ,"relation" TEXT NOT NULL DEFAULT 'CONTAINS' ,"value" TEXT NOT NULL DEFAULT '' ,"collectionRuleSet" TEXT REFERENCES CollectionRuleSet(id),PRIMARY KEY ("id")); 14 | 15 | CREATE TABLE "next"."ProductType"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,PRIMARY KEY ("id")); 16 | 17 | CREATE TABLE "next"."Product"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,"slug" TEXT NOT NULL DEFAULT '' ,"description" TEXT NOT NULL DEFAULT '' ,"type" TEXT REFERENCES ProductType(id),"brand" TEXT NOT NULL REFERENCES Brand(id),"thumbnail" TEXT REFERENCES Image(id),PRIMARY KEY ("id")); 18 | 19 | CREATE TABLE "next"."Brand"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,PRIMARY KEY ("id")); 20 | 21 | CREATE TABLE "next"."Attribute"("id" TEXT NOT NULL ,"key" TEXT NOT NULL DEFAULT '' ,"value" TEXT NOT NULL DEFAULT '' ,PRIMARY KEY ("id")); 22 | 23 | CREATE TABLE "next"."OptionValue"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,"option" TEXT NOT NULL REFERENCES Option(id),"variant" TEXT REFERENCES Variant(id),PRIMARY KEY ("id")); 24 | 25 | CREATE TABLE "next"."Option"("id" TEXT NOT NULL ,"name" TEXT NOT NULL DEFAULT '' ,"isColor" BOOLEAN ,PRIMARY KEY ("id")); 26 | 27 | CREATE TABLE "next"."Variant"("id" TEXT NOT NULL ,"price" INTEGER NOT NULL DEFAULT 0 ,"availableForSale" BOOLEAN ,"sku" TEXT ,"product" TEXT REFERENCES Product(id),PRIMARY KEY ("id")); 28 | 29 | CREATE TABLE "next"."Image"("id" TEXT NOT NULL ,"url" TEXT NOT NULL DEFAULT '' ,"variant" TEXT REFERENCES Variant(id),PRIMARY KEY ("id")); 30 | 31 | CREATE TABLE "next"."_CollectionToProduct"("A" TEXT NOT NULL REFERENCES Collection(id),"B" TEXT NOT NULL REFERENCES Product(id)); 32 | 33 | CREATE TABLE "next"."_AttributeToProduct"("A" TEXT NOT NULL REFERENCES Attribute(id),"B" TEXT NOT NULL REFERENCES Product(id)); 34 | ``` 35 | 36 | ## Changes 37 | 38 | ```diff 39 | diff --git datamodel.mdl datamodel.mdl 40 | migration ..20190618213953-init 41 | --- datamodel.dml 42 | +++ datamodel.dml 43 | @@ -1,0 +1,110 @@ 44 | +datasource db { 45 | + provider = "sqlite" 46 | + url = "file:db/next.db" 47 | + default = true 48 | +} 49 | + 50 | +generator photon { 51 | + provider = "photonjs" 52 | +} 53 | + 54 | +generator nexus_prisma { 55 | + provider = "nexus-prisma" 56 | +} 57 | + 58 | +type ID = String @id @default(cuid()) 59 | + 60 | +model Collection { 61 | + id ID 62 | + name String 63 | + rules CollectionRuleSet? 64 | + products Product[] 65 | +} 66 | + 67 | +model CollectionRuleSet { 68 | + id ID 69 | + rules CollectionRule[] 70 | + appliesDisjunctively Boolean 71 | +} 72 | + 73 | +model CollectionRule { 74 | + id ID 75 | + field CollectionRuleField 76 | + relation CollectionRuleRelation 77 | + value String 78 | +} 79 | + 80 | +enum CollectionRuleField { 81 | + TYPE 82 | + TITLE 83 | + PRICE 84 | +} 85 | + 86 | +enum CollectionRuleRelation { 87 | + CONTAINS 88 | + ENDS_WITH 89 | + EQUALS 90 | + GREATER_THAN 91 | + LESS_THAN 92 | + NOT_CONTAINS 93 | + NOT_EQUALS 94 | + STARTS_WITH 95 | +} 96 | + 97 | +model ProductType { 98 | + id ID 99 | + name String 100 | +} 101 | + 102 | +model Product { 103 | + id ID 104 | + name String 105 | + brand Brand 106 | + slug String @unique 107 | + thumbnail Image? 108 | + description String 109 | + type ProductType? 110 | + variants Variant[] 111 | + collections Collection[] 112 | + attributes Attribute[] 113 | +} 114 | + 115 | +model Brand { 116 | + id ID 117 | + name String 118 | + products Product[] 119 | +} 120 | + 121 | +model Attribute { 122 | + id ID 123 | + key String 124 | + value String 125 | + products Product[] 126 | +} 127 | + 128 | +model OptionValue { 129 | + id ID 130 | + name String 131 | + option Option 132 | +} 133 | + 134 | +model Option { 135 | + id ID 136 | + name String 137 | + values OptionValue[] 138 | + isColor Boolean? 139 | +} 140 | + 141 | +model Variant { 142 | + id ID 143 | + optionValues OptionValue[] 144 | + price Int 145 | + availableForSale Boolean? @default(false) 146 | + sku String? 147 | + images Image[] 148 | +} 149 | + 150 | +model Image { 151 | + id ID 152 | + url String 153 | +} 154 | ``` 155 | 156 | ## Photon Usage 157 | 158 | You can use a specific Photon built for this migration (20190618213953-init) 159 | in your `before` or `after` migration script like this: 160 | 161 | ```ts 162 | import Photon from '@generated/photon/20190618213953-init' 163 | 164 | const photon = new Photon() 165 | 166 | async function main() { 167 | const result = await photon.users() 168 | console.dir(result, { depth: null }) 169 | } 170 | 171 | main() 172 | 173 | ``` 174 | -------------------------------------------------------------------------------- /packages/server/prisma/migrations/20190618213953-init/datamodel.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:db/next.db" 4 | default = true 5 | } 6 | 7 | generator photon { 8 | provider = "photonjs" 9 | } 10 | 11 | generator nexus_prisma { 12 | provider = "nexus-prisma" 13 | } 14 | 15 | type ID = String @id @default(cuid()) 16 | 17 | model Collection { 18 | id ID 19 | name String 20 | rules CollectionRuleSet? 21 | products Product[] 22 | } 23 | 24 | model CollectionRuleSet { 25 | id ID 26 | rules CollectionRule[] 27 | appliesDisjunctively Boolean 28 | } 29 | 30 | model CollectionRule { 31 | id ID 32 | field CollectionRuleField 33 | relation CollectionRuleRelation 34 | value String 35 | } 36 | 37 | enum CollectionRuleField { 38 | TYPE 39 | TITLE 40 | PRICE 41 | } 42 | 43 | enum CollectionRuleRelation { 44 | CONTAINS 45 | ENDS_WITH 46 | EQUALS 47 | GREATER_THAN 48 | LESS_THAN 49 | NOT_CONTAINS 50 | NOT_EQUALS 51 | STARTS_WITH 52 | } 53 | 54 | model ProductType { 55 | id ID 56 | name String 57 | } 58 | 59 | model Product { 60 | id ID 61 | name String 62 | brand Brand 63 | slug String @unique 64 | thumbnail Image? 65 | description String 66 | type ProductType? 67 | variants Variant[] 68 | collections Collection[] 69 | attributes Attribute[] 70 | } 71 | 72 | model Brand { 73 | id ID 74 | name String 75 | products Product[] 76 | } 77 | 78 | model Attribute { 79 | id ID 80 | key String 81 | value String 82 | products Product[] 83 | } 84 | 85 | model OptionValue { 86 | id ID 87 | name String 88 | option Option 89 | } 90 | 91 | model Option { 92 | id ID 93 | name String 94 | values OptionValue[] 95 | isColor Boolean? 96 | } 97 | 98 | model Variant { 99 | id ID 100 | optionValues OptionValue[] 101 | price Int 102 | availableForSale Boolean? @default(false) 103 | sku String? 104 | images Image[] 105 | } 106 | 107 | model Image { 108 | id ID 109 | url String 110 | } -------------------------------------------------------------------------------- /packages/server/prisma/migrations/20190618213953-init/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.106", 3 | "steps": [ 4 | { 5 | "stepType": "CreateModel", 6 | "name": "Collection", 7 | "embedded": false 8 | }, 9 | { 10 | "stepType": "CreateModel", 11 | "name": "CollectionRuleSet", 12 | "embedded": false 13 | }, 14 | { 15 | "stepType": "CreateModel", 16 | "name": "CollectionRule", 17 | "embedded": false 18 | }, 19 | { 20 | "stepType": "CreateModel", 21 | "name": "ProductType", 22 | "embedded": false 23 | }, 24 | { 25 | "stepType": "CreateModel", 26 | "name": "Product", 27 | "embedded": false 28 | }, 29 | { 30 | "stepType": "CreateModel", 31 | "name": "Brand", 32 | "embedded": false 33 | }, 34 | { 35 | "stepType": "CreateModel", 36 | "name": "Attribute", 37 | "embedded": false 38 | }, 39 | { 40 | "stepType": "CreateModel", 41 | "name": "OptionValue", 42 | "embedded": false 43 | }, 44 | { 45 | "stepType": "CreateModel", 46 | "name": "Option", 47 | "embedded": false 48 | }, 49 | { 50 | "stepType": "CreateModel", 51 | "name": "Variant", 52 | "embedded": false 53 | }, 54 | { 55 | "stepType": "CreateModel", 56 | "name": "Image", 57 | "embedded": false 58 | }, 59 | { 60 | "stepType": "CreateField", 61 | "model": "Collection", 62 | "name": "id", 63 | "type": { 64 | "Base": "String" 65 | }, 66 | "arity": "required", 67 | "isUnique": false, 68 | "id": { 69 | "strategy": "Auto", 70 | "sequence": null 71 | }, 72 | "default": { 73 | "Expression": [ 74 | "cuid", 75 | "String", 76 | [] 77 | ] 78 | } 79 | }, 80 | { 81 | "stepType": "CreateField", 82 | "model": "Collection", 83 | "name": "name", 84 | "type": { 85 | "Base": "String" 86 | }, 87 | "arity": "required", 88 | "isUnique": false 89 | }, 90 | { 91 | "stepType": "CreateField", 92 | "model": "Collection", 93 | "name": "rules", 94 | "type": { 95 | "Relation": { 96 | "to": "CollectionRuleSet", 97 | "to_fields": [ 98 | "id" 99 | ], 100 | "name": "CollectionToCollectionRuleSet", 101 | "on_delete": "None" 102 | } 103 | }, 104 | "arity": "optional", 105 | "isUnique": false 106 | }, 107 | { 108 | "stepType": "CreateField", 109 | "model": "Collection", 110 | "name": "products", 111 | "type": { 112 | "Relation": { 113 | "to": "Product", 114 | "to_fields": [ 115 | "id" 116 | ], 117 | "name": "CollectionToProduct", 118 | "on_delete": "None" 119 | } 120 | }, 121 | "arity": "list", 122 | "isUnique": false 123 | }, 124 | { 125 | "stepType": "CreateField", 126 | "model": "CollectionRuleSet", 127 | "name": "id", 128 | "type": { 129 | "Base": "String" 130 | }, 131 | "arity": "required", 132 | "isUnique": false, 133 | "id": { 134 | "strategy": "Auto", 135 | "sequence": null 136 | }, 137 | "default": { 138 | "Expression": [ 139 | "cuid", 140 | "String", 141 | [] 142 | ] 143 | } 144 | }, 145 | { 146 | "stepType": "CreateField", 147 | "model": "CollectionRuleSet", 148 | "name": "rules", 149 | "type": { 150 | "Relation": { 151 | "to": "CollectionRule", 152 | "to_fields": [], 153 | "name": "CollectionRuleToCollectionRuleSet", 154 | "on_delete": "None" 155 | } 156 | }, 157 | "arity": "list", 158 | "isUnique": false 159 | }, 160 | { 161 | "stepType": "CreateField", 162 | "model": "CollectionRuleSet", 163 | "name": "appliesDisjunctively", 164 | "type": { 165 | "Base": "Boolean" 166 | }, 167 | "arity": "required", 168 | "isUnique": false 169 | }, 170 | { 171 | "stepType": "CreateField", 172 | "model": "CollectionRuleSet", 173 | "name": "collection", 174 | "type": { 175 | "Relation": { 176 | "to": "Collection", 177 | "to_fields": [], 178 | "name": "CollectionToCollectionRuleSet", 179 | "on_delete": "None" 180 | } 181 | }, 182 | "arity": "optional", 183 | "isUnique": false 184 | }, 185 | { 186 | "stepType": "CreateField", 187 | "model": "CollectionRule", 188 | "name": "id", 189 | "type": { 190 | "Base": "String" 191 | }, 192 | "arity": "required", 193 | "isUnique": false, 194 | "id": { 195 | "strategy": "Auto", 196 | "sequence": null 197 | }, 198 | "default": { 199 | "Expression": [ 200 | "cuid", 201 | "String", 202 | [] 203 | ] 204 | } 205 | }, 206 | { 207 | "stepType": "CreateField", 208 | "model": "CollectionRule", 209 | "name": "field", 210 | "type": { 211 | "Enum": "CollectionRuleField" 212 | }, 213 | "arity": "required", 214 | "isUnique": false 215 | }, 216 | { 217 | "stepType": "CreateField", 218 | "model": "CollectionRule", 219 | "name": "relation", 220 | "type": { 221 | "Enum": "CollectionRuleRelation" 222 | }, 223 | "arity": "required", 224 | "isUnique": false 225 | }, 226 | { 227 | "stepType": "CreateField", 228 | "model": "CollectionRule", 229 | "name": "value", 230 | "type": { 231 | "Base": "String" 232 | }, 233 | "arity": "required", 234 | "isUnique": false 235 | }, 236 | { 237 | "stepType": "CreateField", 238 | "model": "CollectionRule", 239 | "name": "collectionRuleSet", 240 | "type": { 241 | "Relation": { 242 | "to": "CollectionRuleSet", 243 | "to_fields": [ 244 | "id" 245 | ], 246 | "name": "CollectionRuleToCollectionRuleSet", 247 | "on_delete": "None" 248 | } 249 | }, 250 | "arity": "optional", 251 | "isUnique": false 252 | }, 253 | { 254 | "stepType": "CreateField", 255 | "model": "ProductType", 256 | "name": "id", 257 | "type": { 258 | "Base": "String" 259 | }, 260 | "arity": "required", 261 | "isUnique": false, 262 | "id": { 263 | "strategy": "Auto", 264 | "sequence": null 265 | }, 266 | "default": { 267 | "Expression": [ 268 | "cuid", 269 | "String", 270 | [] 271 | ] 272 | } 273 | }, 274 | { 275 | "stepType": "CreateField", 276 | "model": "ProductType", 277 | "name": "name", 278 | "type": { 279 | "Base": "String" 280 | }, 281 | "arity": "required", 282 | "isUnique": false 283 | }, 284 | { 285 | "stepType": "CreateField", 286 | "model": "ProductType", 287 | "name": "product", 288 | "type": { 289 | "Relation": { 290 | "to": "Product", 291 | "to_fields": [], 292 | "name": "ProductToProductType", 293 | "on_delete": "None" 294 | } 295 | }, 296 | "arity": "optional", 297 | "isUnique": false 298 | }, 299 | { 300 | "stepType": "CreateField", 301 | "model": "Product", 302 | "name": "id", 303 | "type": { 304 | "Base": "String" 305 | }, 306 | "arity": "required", 307 | "isUnique": false, 308 | "id": { 309 | "strategy": "Auto", 310 | "sequence": null 311 | }, 312 | "default": { 313 | "Expression": [ 314 | "cuid", 315 | "String", 316 | [] 317 | ] 318 | } 319 | }, 320 | { 321 | "stepType": "CreateField", 322 | "model": "Product", 323 | "name": "name", 324 | "type": { 325 | "Base": "String" 326 | }, 327 | "arity": "required", 328 | "isUnique": false 329 | }, 330 | { 331 | "stepType": "CreateField", 332 | "model": "Product", 333 | "name": "brand", 334 | "type": { 335 | "Relation": { 336 | "to": "Brand", 337 | "to_fields": [ 338 | "id" 339 | ], 340 | "name": "BrandToProduct", 341 | "on_delete": "None" 342 | } 343 | }, 344 | "arity": "required", 345 | "isUnique": false 346 | }, 347 | { 348 | "stepType": "CreateField", 349 | "model": "Product", 350 | "name": "slug", 351 | "type": { 352 | "Base": "String" 353 | }, 354 | "arity": "required", 355 | "isUnique": true 356 | }, 357 | { 358 | "stepType": "CreateField", 359 | "model": "Product", 360 | "name": "thumbnail", 361 | "type": { 362 | "Relation": { 363 | "to": "Image", 364 | "to_fields": [ 365 | "id" 366 | ], 367 | "name": "ImageToProduct", 368 | "on_delete": "None" 369 | } 370 | }, 371 | "arity": "optional", 372 | "isUnique": false 373 | }, 374 | { 375 | "stepType": "CreateField", 376 | "model": "Product", 377 | "name": "description", 378 | "type": { 379 | "Base": "String" 380 | }, 381 | "arity": "required", 382 | "isUnique": false 383 | }, 384 | { 385 | "stepType": "CreateField", 386 | "model": "Product", 387 | "name": "type", 388 | "type": { 389 | "Relation": { 390 | "to": "ProductType", 391 | "to_fields": [ 392 | "id" 393 | ], 394 | "name": "ProductToProductType", 395 | "on_delete": "None" 396 | } 397 | }, 398 | "arity": "optional", 399 | "isUnique": false 400 | }, 401 | { 402 | "stepType": "CreateField", 403 | "model": "Product", 404 | "name": "variants", 405 | "type": { 406 | "Relation": { 407 | "to": "Variant", 408 | "to_fields": [], 409 | "name": "ProductToVariant", 410 | "on_delete": "None" 411 | } 412 | }, 413 | "arity": "list", 414 | "isUnique": false 415 | }, 416 | { 417 | "stepType": "CreateField", 418 | "model": "Product", 419 | "name": "collections", 420 | "type": { 421 | "Relation": { 422 | "to": "Collection", 423 | "to_fields": [ 424 | "id" 425 | ], 426 | "name": "CollectionToProduct", 427 | "on_delete": "None" 428 | } 429 | }, 430 | "arity": "list", 431 | "isUnique": false 432 | }, 433 | { 434 | "stepType": "CreateField", 435 | "model": "Product", 436 | "name": "attributes", 437 | "type": { 438 | "Relation": { 439 | "to": "Attribute", 440 | "to_fields": [ 441 | "id" 442 | ], 443 | "name": "AttributeToProduct", 444 | "on_delete": "None" 445 | } 446 | }, 447 | "arity": "list", 448 | "isUnique": false 449 | }, 450 | { 451 | "stepType": "CreateField", 452 | "model": "Brand", 453 | "name": "id", 454 | "type": { 455 | "Base": "String" 456 | }, 457 | "arity": "required", 458 | "isUnique": false, 459 | "id": { 460 | "strategy": "Auto", 461 | "sequence": null 462 | }, 463 | "default": { 464 | "Expression": [ 465 | "cuid", 466 | "String", 467 | [] 468 | ] 469 | } 470 | }, 471 | { 472 | "stepType": "CreateField", 473 | "model": "Brand", 474 | "name": "name", 475 | "type": { 476 | "Base": "String" 477 | }, 478 | "arity": "required", 479 | "isUnique": false 480 | }, 481 | { 482 | "stepType": "CreateField", 483 | "model": "Brand", 484 | "name": "products", 485 | "type": { 486 | "Relation": { 487 | "to": "Product", 488 | "to_fields": [], 489 | "name": "BrandToProduct", 490 | "on_delete": "None" 491 | } 492 | }, 493 | "arity": "list", 494 | "isUnique": false 495 | }, 496 | { 497 | "stepType": "CreateField", 498 | "model": "Attribute", 499 | "name": "id", 500 | "type": { 501 | "Base": "String" 502 | }, 503 | "arity": "required", 504 | "isUnique": false, 505 | "id": { 506 | "strategy": "Auto", 507 | "sequence": null 508 | }, 509 | "default": { 510 | "Expression": [ 511 | "cuid", 512 | "String", 513 | [] 514 | ] 515 | } 516 | }, 517 | { 518 | "stepType": "CreateField", 519 | "model": "Attribute", 520 | "name": "key", 521 | "type": { 522 | "Base": "String" 523 | }, 524 | "arity": "required", 525 | "isUnique": false 526 | }, 527 | { 528 | "stepType": "CreateField", 529 | "model": "Attribute", 530 | "name": "value", 531 | "type": { 532 | "Base": "String" 533 | }, 534 | "arity": "required", 535 | "isUnique": false 536 | }, 537 | { 538 | "stepType": "CreateField", 539 | "model": "Attribute", 540 | "name": "products", 541 | "type": { 542 | "Relation": { 543 | "to": "Product", 544 | "to_fields": [ 545 | "id" 546 | ], 547 | "name": "AttributeToProduct", 548 | "on_delete": "None" 549 | } 550 | }, 551 | "arity": "list", 552 | "isUnique": false 553 | }, 554 | { 555 | "stepType": "CreateField", 556 | "model": "OptionValue", 557 | "name": "id", 558 | "type": { 559 | "Base": "String" 560 | }, 561 | "arity": "required", 562 | "isUnique": false, 563 | "id": { 564 | "strategy": "Auto", 565 | "sequence": null 566 | }, 567 | "default": { 568 | "Expression": [ 569 | "cuid", 570 | "String", 571 | [] 572 | ] 573 | } 574 | }, 575 | { 576 | "stepType": "CreateField", 577 | "model": "OptionValue", 578 | "name": "name", 579 | "type": { 580 | "Base": "String" 581 | }, 582 | "arity": "required", 583 | "isUnique": false 584 | }, 585 | { 586 | "stepType": "CreateField", 587 | "model": "OptionValue", 588 | "name": "option", 589 | "type": { 590 | "Relation": { 591 | "to": "Option", 592 | "to_fields": [ 593 | "id" 594 | ], 595 | "name": "OptionToOptionValue", 596 | "on_delete": "None" 597 | } 598 | }, 599 | "arity": "required", 600 | "isUnique": false 601 | }, 602 | { 603 | "stepType": "CreateField", 604 | "model": "OptionValue", 605 | "name": "variant", 606 | "type": { 607 | "Relation": { 608 | "to": "Variant", 609 | "to_fields": [ 610 | "id" 611 | ], 612 | "name": "OptionValueToVariant", 613 | "on_delete": "None" 614 | } 615 | }, 616 | "arity": "optional", 617 | "isUnique": false 618 | }, 619 | { 620 | "stepType": "CreateField", 621 | "model": "Option", 622 | "name": "id", 623 | "type": { 624 | "Base": "String" 625 | }, 626 | "arity": "required", 627 | "isUnique": false, 628 | "id": { 629 | "strategy": "Auto", 630 | "sequence": null 631 | }, 632 | "default": { 633 | "Expression": [ 634 | "cuid", 635 | "String", 636 | [] 637 | ] 638 | } 639 | }, 640 | { 641 | "stepType": "CreateField", 642 | "model": "Option", 643 | "name": "name", 644 | "type": { 645 | "Base": "String" 646 | }, 647 | "arity": "required", 648 | "isUnique": false 649 | }, 650 | { 651 | "stepType": "CreateField", 652 | "model": "Option", 653 | "name": "values", 654 | "type": { 655 | "Relation": { 656 | "to": "OptionValue", 657 | "to_fields": [], 658 | "name": "OptionToOptionValue", 659 | "on_delete": "None" 660 | } 661 | }, 662 | "arity": "list", 663 | "isUnique": false 664 | }, 665 | { 666 | "stepType": "CreateField", 667 | "model": "Option", 668 | "name": "isColor", 669 | "type": { 670 | "Base": "Boolean" 671 | }, 672 | "arity": "optional", 673 | "isUnique": false 674 | }, 675 | { 676 | "stepType": "CreateField", 677 | "model": "Variant", 678 | "name": "id", 679 | "type": { 680 | "Base": "String" 681 | }, 682 | "arity": "required", 683 | "isUnique": false, 684 | "id": { 685 | "strategy": "Auto", 686 | "sequence": null 687 | }, 688 | "default": { 689 | "Expression": [ 690 | "cuid", 691 | "String", 692 | [] 693 | ] 694 | } 695 | }, 696 | { 697 | "stepType": "CreateField", 698 | "model": "Variant", 699 | "name": "optionValues", 700 | "type": { 701 | "Relation": { 702 | "to": "OptionValue", 703 | "to_fields": [], 704 | "name": "OptionValueToVariant", 705 | "on_delete": "None" 706 | } 707 | }, 708 | "arity": "list", 709 | "isUnique": false 710 | }, 711 | { 712 | "stepType": "CreateField", 713 | "model": "Variant", 714 | "name": "price", 715 | "type": { 716 | "Base": "Int" 717 | }, 718 | "arity": "required", 719 | "isUnique": false 720 | }, 721 | { 722 | "stepType": "CreateField", 723 | "model": "Variant", 724 | "name": "availableForSale", 725 | "type": { 726 | "Base": "Boolean" 727 | }, 728 | "arity": "optional", 729 | "isUnique": false, 730 | "default": { 731 | "Boolean": false 732 | } 733 | }, 734 | { 735 | "stepType": "CreateField", 736 | "model": "Variant", 737 | "name": "sku", 738 | "type": { 739 | "Base": "String" 740 | }, 741 | "arity": "optional", 742 | "isUnique": false 743 | }, 744 | { 745 | "stepType": "CreateField", 746 | "model": "Variant", 747 | "name": "images", 748 | "type": { 749 | "Relation": { 750 | "to": "Image", 751 | "to_fields": [], 752 | "name": "ImageToVariant", 753 | "on_delete": "None" 754 | } 755 | }, 756 | "arity": "list", 757 | "isUnique": false 758 | }, 759 | { 760 | "stepType": "CreateField", 761 | "model": "Variant", 762 | "name": "product", 763 | "type": { 764 | "Relation": { 765 | "to": "Product", 766 | "to_fields": [ 767 | "id" 768 | ], 769 | "name": "ProductToVariant", 770 | "on_delete": "None" 771 | } 772 | }, 773 | "arity": "optional", 774 | "isUnique": false 775 | }, 776 | { 777 | "stepType": "CreateField", 778 | "model": "Image", 779 | "name": "id", 780 | "type": { 781 | "Base": "String" 782 | }, 783 | "arity": "required", 784 | "isUnique": false, 785 | "id": { 786 | "strategy": "Auto", 787 | "sequence": null 788 | }, 789 | "default": { 790 | "Expression": [ 791 | "cuid", 792 | "String", 793 | [] 794 | ] 795 | } 796 | }, 797 | { 798 | "stepType": "CreateField", 799 | "model": "Image", 800 | "name": "url", 801 | "type": { 802 | "Base": "String" 803 | }, 804 | "arity": "required", 805 | "isUnique": false 806 | }, 807 | { 808 | "stepType": "CreateField", 809 | "model": "Image", 810 | "name": "product", 811 | "type": { 812 | "Relation": { 813 | "to": "Product", 814 | "to_fields": [], 815 | "name": "ImageToProduct", 816 | "on_delete": "None" 817 | } 818 | }, 819 | "arity": "optional", 820 | "isUnique": false 821 | }, 822 | { 823 | "stepType": "CreateField", 824 | "model": "Image", 825 | "name": "variant", 826 | "type": { 827 | "Relation": { 828 | "to": "Variant", 829 | "to_fields": [ 830 | "id" 831 | ], 832 | "name": "ImageToVariant", 833 | "on_delete": "None" 834 | } 835 | }, 836 | "arity": "optional", 837 | "isUnique": false 838 | }, 839 | { 840 | "stepType": "CreateEnum", 841 | "name": "CollectionRuleField", 842 | "values": [ 843 | "TYPE", 844 | "TITLE", 845 | "PRICE" 846 | ] 847 | }, 848 | { 849 | "stepType": "CreateEnum", 850 | "name": "CollectionRuleRelation", 851 | "values": [ 852 | "CONTAINS", 853 | "ENDS_WITH", 854 | "EQUALS", 855 | "GREATER_THAN", 856 | "LESS_THAN", 857 | "NOT_CONTAINS", 858 | "NOT_EQUALS", 859 | "STARTS_WITH" 860 | ] 861 | } 862 | ] 863 | } -------------------------------------------------------------------------------- /packages/server/prisma/migrations/dev/watch-20190618214934/README.md: -------------------------------------------------------------------------------- 1 | # Migration `watch-20190618214934` 2 | 3 | This migration has been generated by Flavian DESVERNE at 6/18/2019, 9:49:34 PM. 4 | You can check out the [state of the datamodel](./datamodel.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | 10 | ``` 11 | 12 | ## Changes 13 | 14 | ```diff 15 | diff --git datamodel.mdl datamodel.mdl 16 | migration ..watch-20190618214934 17 | --- datamodel.dml 18 | +++ datamodel.dml 19 | @@ -1,0 +1,110 @@ 20 | +datasource db { 21 | + provider = "sqlite" 22 | + url = "file:db/next.db" 23 | + default = true 24 | +} 25 | + 26 | +generator photon { 27 | + provider = "photonjs" 28 | +} 29 | + 30 | +generator nexus_prisma { 31 | + provider = "nexus-prisma" 32 | +} 33 | + 34 | +type ID = String @id @default(cuid()) 35 | + 36 | +model Collection { 37 | + id ID 38 | + name String 39 | + rules CollectionRuleSet? 40 | + products Product[] 41 | +} 42 | + 43 | +model CollectionRuleSet { 44 | + id ID 45 | + rules CollectionRule[] 46 | + appliesDisjunctively Boolean 47 | +} 48 | + 49 | +model CollectionRule { 50 | + id ID 51 | + field CollectionRuleField 52 | + relation CollectionRuleRelation 53 | + value String 54 | +} 55 | + 56 | +enum CollectionRuleField { 57 | + TYPE 58 | + TITLE 59 | + PRICE 60 | +} 61 | + 62 | +enum CollectionRuleRelation { 63 | + CONTAINS 64 | + ENDS_WITH 65 | + EQUALS 66 | + GREATER_THAN 67 | + LESS_THAN 68 | + NOT_CONTAINS 69 | + NOT_EQUALS 70 | + STARTS_WITH 71 | +} 72 | + 73 | +model ProductType { 74 | + id ID 75 | + name String 76 | +} 77 | + 78 | +model Product { 79 | + id ID 80 | + name String 81 | + brand Brand 82 | + slug String @unique 83 | + thumbnail Image? 84 | + description String 85 | + type ProductType? 86 | + variants Variant[] 87 | + collections Collection[] 88 | + attributes Attribute[] 89 | +} 90 | + 91 | +model Brand { 92 | + id ID 93 | + name String 94 | + products Product[] 95 | +} 96 | + 97 | +model Attribute { 98 | + id ID 99 | + key String 100 | + value String 101 | + products Product[] 102 | +} 103 | + 104 | +model OptionValue { 105 | + id ID 106 | + name String 107 | + option Option 108 | +} 109 | + 110 | +model Option { 111 | + id ID 112 | + name String 113 | + values OptionValue[] 114 | + isColor Boolean? 115 | +} 116 | + 117 | +model Variant { 118 | + id ID 119 | + optionValues OptionValue[] 120 | + price Int 121 | + availableForSale Boolean? @default(false) 122 | + sku String? 123 | + images Image[] 124 | +} 125 | + 126 | +model Image { 127 | + id ID 128 | + url String 129 | +} 130 | ``` 131 | 132 | ## Photon Usage 133 | 134 | You can use a specific Photon built for this migration (watch-20190618214934) 135 | in your `before` or `after` migration script like this: 136 | 137 | ```ts 138 | import Photon from '@generated/photon/watch-20190618214934' 139 | 140 | const photon = new Photon() 141 | 142 | async function main() { 143 | const result = await photon.users() 144 | console.dir(result, { depth: null }) 145 | } 146 | 147 | main() 148 | 149 | ``` 150 | -------------------------------------------------------------------------------- /packages/server/prisma/migrations/dev/watch-20190618214934/datamodel.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:db/next.db" 4 | default = true 5 | } 6 | 7 | generator photon { 8 | provider = "photonjs" 9 | } 10 | 11 | generator nexus_prisma { 12 | provider = "nexus-prisma" 13 | } 14 | 15 | type ID = String @id @default(cuid()) 16 | 17 | model Collection { 18 | id ID 19 | name String 20 | rules CollectionRuleSet? 21 | products Product[] 22 | } 23 | 24 | model CollectionRuleSet { 25 | id ID 26 | rules CollectionRule[] 27 | appliesDisjunctively Boolean 28 | } 29 | 30 | model CollectionRule { 31 | id ID 32 | field CollectionRuleField 33 | relation CollectionRuleRelation 34 | value String 35 | } 36 | 37 | enum CollectionRuleField { 38 | TYPE 39 | TITLE 40 | PRICE 41 | } 42 | 43 | enum CollectionRuleRelation { 44 | CONTAINS 45 | ENDS_WITH 46 | EQUALS 47 | GREATER_THAN 48 | LESS_THAN 49 | NOT_CONTAINS 50 | NOT_EQUALS 51 | STARTS_WITH 52 | } 53 | 54 | model ProductType { 55 | id ID 56 | name String 57 | } 58 | 59 | model Product { 60 | id ID 61 | name String 62 | brand Brand 63 | slug String @unique 64 | thumbnail Image? 65 | description String 66 | type ProductType? 67 | variants Variant[] 68 | collections Collection[] 69 | attributes Attribute[] 70 | } 71 | 72 | model Brand { 73 | id ID 74 | name String 75 | products Product[] 76 | } 77 | 78 | model Attribute { 79 | id ID 80 | key String 81 | value String 82 | products Product[] 83 | } 84 | 85 | model OptionValue { 86 | id ID 87 | name String 88 | option Option 89 | } 90 | 91 | model Option { 92 | id ID 93 | name String 94 | values OptionValue[] 95 | isColor Boolean? 96 | } 97 | 98 | model Variant { 99 | id ID 100 | optionValues OptionValue[] 101 | price Int 102 | availableForSale Boolean? @default(false) 103 | sku String? 104 | images Image[] 105 | } 106 | 107 | model Image { 108 | id ID 109 | url String 110 | } -------------------------------------------------------------------------------- /packages/server/prisma/migrations/dev/watch-20190618214934/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.106", 3 | "steps": [ 4 | { 5 | "stepType": "UpdateField", 6 | "model": "Collection", 7 | "name": "rules", 8 | "type": { 9 | "Relation": { 10 | "to": "CollectionRuleSet", 11 | "to_fields": [ 12 | "id" 13 | ], 14 | "name": "CollectionToCollectionRuleSet", 15 | "on_delete": "None" 16 | } 17 | } 18 | }, 19 | { 20 | "stepType": "UpdateField", 21 | "model": "CollectionRuleSet", 22 | "name": "collection", 23 | "type": { 24 | "Relation": { 25 | "to": "Collection", 26 | "to_fields": [], 27 | "name": "CollectionToCollectionRuleSet", 28 | "on_delete": "None" 29 | } 30 | } 31 | }, 32 | { 33 | "stepType": "UpdateField", 34 | "model": "ProductType", 35 | "name": "product", 36 | "type": { 37 | "Relation": { 38 | "to": "Product", 39 | "to_fields": [], 40 | "name": "ProductToProductType", 41 | "on_delete": "None" 42 | } 43 | } 44 | }, 45 | { 46 | "stepType": "UpdateField", 47 | "model": "Product", 48 | "name": "thumbnail", 49 | "type": { 50 | "Relation": { 51 | "to": "Image", 52 | "to_fields": [ 53 | "id" 54 | ], 55 | "name": "ImageToProduct", 56 | "on_delete": "None" 57 | } 58 | } 59 | }, 60 | { 61 | "stepType": "UpdateField", 62 | "model": "Product", 63 | "name": "type", 64 | "type": { 65 | "Relation": { 66 | "to": "ProductType", 67 | "to_fields": [ 68 | "id" 69 | ], 70 | "name": "ProductToProductType", 71 | "on_delete": "None" 72 | } 73 | } 74 | }, 75 | { 76 | "stepType": "UpdateField", 77 | "model": "Image", 78 | "name": "product", 79 | "type": { 80 | "Relation": { 81 | "to": "Product", 82 | "to_fields": [], 83 | "name": "ImageToProduct", 84 | "on_delete": "None" 85 | } 86 | } 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /packages/server/prisma/migrations/lift.lock: -------------------------------------------------------------------------------- 1 | # IF THERE'S A GIT CONFLICT IN THIS FILE, DON'T SOLVE IT MANUALLY! 2 | # INSTEAD EXECUTE `prisma lift fix` 3 | # lift lockfile v1 4 | # Read more about conflict resolution here: TODO 5 | 6 | 20190618213953-init -------------------------------------------------------------------------------- /packages/server/prisma/project.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:db/next.db" 4 | default = true 5 | } 6 | 7 | generator photon { 8 | provider = "photonjs" 9 | } 10 | 11 | generator nexus_prisma { 12 | provider = "nexus-prisma" 13 | } 14 | 15 | type ID = String @id @default(cuid()) 16 | 17 | model Collection { 18 | id ID 19 | name String 20 | rules CollectionRuleSet? 21 | products Product[] 22 | } 23 | 24 | model CollectionRuleSet { 25 | id ID 26 | rules CollectionRule[] 27 | appliesDisjunctively Boolean 28 | } 29 | 30 | model CollectionRule { 31 | id ID 32 | field CollectionRuleField 33 | relation CollectionRuleRelation 34 | value String 35 | } 36 | 37 | enum CollectionRuleField { 38 | TYPE 39 | TITLE 40 | PRICE 41 | } 42 | 43 | enum CollectionRuleRelation { 44 | CONTAINS 45 | ENDS_WITH 46 | EQUALS 47 | GREATER_THAN 48 | LESS_THAN 49 | NOT_CONTAINS 50 | NOT_EQUALS 51 | STARTS_WITH 52 | } 53 | 54 | model ProductType { 55 | id ID 56 | name String 57 | } 58 | 59 | model Product { 60 | id ID 61 | name String 62 | brand Brand 63 | slug String @unique 64 | thumbnail Image? 65 | description String 66 | type ProductType? 67 | variants Variant[] 68 | collections Collection[] 69 | attributes Attribute[] 70 | } 71 | 72 | model Brand { 73 | id ID 74 | name String 75 | products Product[] 76 | } 77 | 78 | model Attribute { 79 | id ID 80 | key String 81 | value String 82 | products Product[] 83 | } 84 | 85 | model OptionValue { 86 | id ID 87 | name String 88 | option Option 89 | } 90 | 91 | model Option { 92 | id ID 93 | name String 94 | values OptionValue[] 95 | isColor Boolean? 96 | } 97 | 98 | model Variant { 99 | id ID 100 | optionValues OptionValue[] 101 | price Int 102 | availableForSale Boolean? @default(false) 103 | sku String? 104 | images Image[] 105 | } 106 | 107 | model Image { 108 | id ID 109 | url String 110 | } -------------------------------------------------------------------------------- /packages/server/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import * as faker from 'faker' 3 | import * as _ from 'lodash' 4 | import Photon, { 5 | Attribute, 6 | AttributeCreateInput, 7 | Brand, 8 | OptionCreateInput, 9 | Product, 10 | ProductType, 11 | VariantCreateInput, 12 | } from '@generated/photon' 13 | import console = require('console') 14 | 15 | const photon = new Photon() 16 | 17 | interface Option { 18 | id: string 19 | name: string 20 | values: { id: string; name: string }[] 21 | } 22 | 23 | const IMAGES = [ 24 | 'https://tinypng.com/web/output/e8hxgepvepcmw5u5y87yk49j3zdgdnzw/mb06-gray-0.jpg', 25 | 'https://tinypng.com/web/output/ukcxgwh688aw7n21q88j3wy5feka15ey/mb05-black-0.jpg', 26 | 'https://tinypng.com/web/output/zjhmkqazg8hmmtdkb9zzt4fjm1pdt6uv/mb04-black-0.jpg', 27 | 'https://tinypng.com/web/output/8zfn072e0yhkwv1qbewh55kp24mygn8g/mb04-black-0_alt1.jpg', 28 | 'https://tinypng.com/web/output/dpm6h6pj2zngd952hnuxtuyrruz1hjb7/mb03-black-0.jpg', 29 | 'https://tinypng.com/web/output/ycb012zebun79axbnfva6189anpehr4k/mb03-black-0_alt1.jpg', 30 | 'https://tinypng.com/web/output/mx658qjw8u5kgz0wtf43puywy12rhrz7/mb02-gray-0.jpg', 31 | 'https://tinypng.com/web/output/zxzqmxpb4kpj4pzk60d258ka7vhnhna2/mb02-blue-0.jpg', 32 | 'https://tinypng.com/web/output/y1gxvvyx30rthteepm0qj59rmhh01f9w/mb01-blue-0.jpg', 33 | ] 34 | 35 | main() 36 | 37 | async function main() { 38 | // const someProducts = await photon.products.findMany({ 39 | // select: { 40 | // variants: { 41 | // select: { 42 | // id: true, 43 | // optionValues: { 44 | // select: { 45 | // id: true, 46 | // name: true, 47 | // }, 48 | // }, 49 | // }, 50 | // }, 51 | // }, 52 | // }) 53 | 54 | //console.log(JSON.stringify(someProducts, null, 2)) 55 | 56 | // const allOptions = await options(20) 57 | 58 | // const variant = await photon.variants.create({ 59 | // data: { 60 | // price: 100, 61 | // optionValues: { 62 | // connect: [ 63 | // { id: allOptions[0].values[0].id }, 64 | // { id: allOptions[1].values[0].id }, 65 | // ], 66 | // }, 67 | // }, 68 | // }) 69 | 70 | //console.log(updatedVariant) 71 | 72 | await products(1) 73 | // console.log('done') 74 | 75 | //await deleteProducts() 76 | //await brands(10) 77 | } 78 | 79 | // @ts-ignore 80 | async function productTypes(n: number): Promise { 81 | return job(n, () => { 82 | return photon.productTypes.create({ 83 | data: { 84 | name: faker.commerce.product(), 85 | }, 86 | }) 87 | }) 88 | } 89 | 90 | // @ts-ignore 91 | async function products(n: number): Promise { 92 | let allBrands = await photon.brands.findMany() 93 | let allAttributes = await photon.attributes.findMany() 94 | let allOptions: Option[] = await photon.options.findMany({ 95 | select: { 96 | id: true, 97 | name: true, 98 | values: { 99 | select: { 100 | id: true, 101 | name: true, 102 | }, 103 | }, 104 | }, 105 | }) 106 | let allProductTypes = await photon.productTypes.findMany() 107 | 108 | if (allBrands.length === 0) { 109 | allBrands = await brands(20) 110 | } 111 | if (allAttributes.length === 0) { 112 | allAttributes = await attributes(20) 113 | } 114 | if (allProductTypes.length === 0) { 115 | allProductTypes = await productTypes(20) 116 | } 117 | if (allOptions.length === 0) { 118 | allOptions = await options(20) 119 | } 120 | 121 | const materials = allAttributes.filter(a => a.key === 'Material') 122 | const length = allAttributes.filter(a => a.key === 'Length') 123 | 124 | return job(n, async () => { 125 | const productName = faker.commerce.productName() 126 | 127 | const product = await photon.products.create({ 128 | data: { 129 | name: productName, 130 | description: faker.random.words(), 131 | slug: faker.helpers.slugify(productName), 132 | brand: { 133 | connect: { id: pickRnd(allBrands).id }, 134 | }, 135 | thumbnail: { 136 | create: { url: _.sample(IMAGES)! }, 137 | }, 138 | attributes: { 139 | connect: [{ id: pickRnd(materials).id }, { id: pickRnd(length).id }], 140 | }, 141 | type: { 142 | connect: { id: pickRnd(allProductTypes).id }, 143 | }, 144 | }, 145 | }) 146 | 147 | const variants = await generateVariant(allOptions) 148 | 149 | await photon.products.update({ 150 | where: { id: product.id }, 151 | data: { 152 | variants: { 153 | connect: variants.map(v => ({ id: v.id })), 154 | }, 155 | }, 156 | }) 157 | }) 158 | } 159 | 160 | // @ts-ignore 161 | async function attributes(n: number): Promise { 162 | const attributesToCreate: AttributeCreateInput[] = [ 163 | { 164 | key: 'Material', 165 | value: 'Leather', 166 | }, 167 | { 168 | key: 'Material', 169 | value: 'Tissue', 170 | }, 171 | { 172 | key: 'Material', 173 | value: 'Cotton', 174 | }, 175 | { 176 | key: 'Length', 177 | value: 'Short', 178 | }, 179 | { 180 | key: 'Length', 181 | value: 'Normal', 182 | }, 183 | { 184 | key: 'Length', 185 | value: 'Mid-length', 186 | }, 187 | { 188 | key: 'Length', 189 | value: 'Long', 190 | }, 191 | { 192 | key: 'Length', 193 | value: 'Extra-Long', 194 | }, 195 | ] 196 | 197 | return Promise.all( 198 | attributesToCreate.map(attribute => 199 | photon.attributes.create({ data: attribute }), 200 | ), 201 | ) 202 | } 203 | 204 | // @ts-ignore 205 | async function options(n: number): Promise { 206 | const optionsToCreate: OptionCreateInput[] = [ 207 | { 208 | name: 'Color', 209 | values: { 210 | create: [ 211 | { name: 'Red' }, 212 | { name: 'Blue' }, 213 | { name: 'Green' }, 214 | { name: 'Violet' }, 215 | ], 216 | }, 217 | isColor: true, 218 | }, 219 | { 220 | name: 'Size', 221 | values: { 222 | create: [{ name: 'S' }, { name: 'M' }, { name: 'L' }, { name: 'XL' }], 223 | }, 224 | isColor: false, 225 | }, 226 | ] 227 | 228 | return Promise.all( 229 | optionsToCreate.map(option => 230 | photon.options.create({ 231 | data: option, 232 | select: { 233 | id: true, 234 | name: true, 235 | values: { 236 | select: { 237 | id: true, 238 | name: true, 239 | }, 240 | }, 241 | }, 242 | }), 243 | ), 244 | ) as Promise 245 | } 246 | 247 | // @ts-ignore 248 | async function deleteOptions(): Promise<{}> { 249 | const options = await photon.options() 250 | 251 | return Promise.all( 252 | options.map(o => photon.options.delete({ where: { id: o.id } })), 253 | ) 254 | } 255 | // @ts-ignore 256 | async function deleteProducts(): Promise<{}> { 257 | const products = await photon.products() 258 | 259 | return Promise.all( 260 | products.map(p => photon.products.delete({ where: { id: p.id } })), 261 | ) 262 | } 263 | 264 | // @ts-ignore 265 | async function brands(n: number): Promise { 266 | return job(n, () => { 267 | return photon.brands.create({ 268 | data: { 269 | name: faker.company.companyName(), 270 | }, 271 | }) 272 | }) 273 | } 274 | 275 | // @ts-ignore 276 | async function generateVariant(options: Option[]): Promise { 277 | const colorOption = options.find(o => o.name === 'Color')! 278 | const sizeOption = options.find(o => o.name === 'Size')! 279 | 280 | const variants: Array<[{ id: string }, { id: string }]> = cartesianProduct([ 281 | colorOption!.values, 282 | sizeOption!.values, 283 | ]) 284 | const randomVariants = _.sampleSize(variants, _.random(1, variants.length)) 285 | const dbVariants: any[] = [] 286 | 287 | for (const __ of randomVariants) { 288 | const tmpVariant = await photon.variants.create({ 289 | data: { 290 | images: { 291 | create: _.sampleSize(IMAGES, _.random(1, IMAGES.length - 1)).map( 292 | url => ({ url }), 293 | ), 294 | }, 295 | price: _.random(10, 100) * 100, 296 | availableForSale: true, 297 | } as VariantCreateInput, 298 | }) 299 | 300 | dbVariants.push(tmpVariant) 301 | } 302 | 303 | const optionsValuesToCreate = randomVariants.map(randomVariant => ({ 304 | connect: randomVariant.map(optionValue => ({ id: optionValue.id })), 305 | })) 306 | 307 | console.log({ optionsValuesToCreate }) 308 | 309 | for (let i = 0; i < dbVariants.length; i++) { 310 | await photon.variants.update({ 311 | where: { id: dbVariants[i].id }, 312 | data: { 313 | optionValues: optionsValuesToCreate[i], 314 | }, 315 | }) 316 | } 317 | 318 | return dbVariants 319 | } 320 | 321 | function pickRnd(arr: T[]): T { 322 | return _.sample(arr) as T 323 | } 324 | 325 | function arr(n: number) { 326 | return new Array(n).fill(0) 327 | } 328 | 329 | async function job(n: number, fn: () => any) { 330 | return Promise.all( 331 | arr(n).map(() => { 332 | return fn() 333 | }), 334 | ) 335 | } 336 | 337 | function cartesianProduct(arr: Array) { 338 | return arr.reduce( 339 | (a, b) => 340 | a.map(x => b.map(y => x.concat(y))).reduce((a, b) => a.concat(b), []), 341 | [[]], 342 | ) 343 | } 344 | -------------------------------------------------------------------------------- /packages/server/src/context.ts: -------------------------------------------------------------------------------- 1 | import Photon from '@generated/photon' 2 | 3 | export interface Context { 4 | photon: Photon 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/src/fragments/ProductVariant.ts: -------------------------------------------------------------------------------- 1 | import { Option } from '@generated/photon' 2 | 3 | export const fragment = `fragment ProductVariant on Product { 4 | variants { 5 | optionValues { 6 | option { id name } 7 | } 8 | } 9 | }` 10 | 11 | export interface Type { 12 | variants: { 13 | optionValues: { option: Option }[] 14 | }[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/fragments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProductVariant' 2 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Attributes.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const AttributePayload = objectType({ 4 | name: 'AttributePayload', 5 | definition(t) { 6 | t.string('name') 7 | t.list.field('values', { type: AttributeValue }) 8 | }, 9 | }) 10 | 11 | export const AttributeValue = objectType({ 12 | name: 'AttributeValue', 13 | definition(t) { 14 | t.id('id') 15 | t.string('value') 16 | }, 17 | }) 18 | 19 | export const Attribute = objectType({ 20 | name: 'Attribute', 21 | definition(t) { 22 | t.model.id() 23 | t.model.key() 24 | t.model.products() 25 | t.model.value() 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Brand.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const Brand = objectType({ 4 | name: 'Brand', 5 | definition(t) { 6 | t.model.id() 7 | t.model.name() 8 | t.model.products() 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Collection.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { 3 | arg, 4 | extendType, 5 | idArg, 6 | inputObjectType, 7 | objectType, 8 | intArg, 9 | } from '@prisma/nexus' 10 | import { 11 | createManualCollection, 12 | recomputeCollection, 13 | throwIfManualAndAutomatic, 14 | throwIfMissingCollectionInput, 15 | } from '../utils/collection' 16 | import { optionsFromVariants } from './utils' 17 | import { ProductWhereInput } from '@generated/photon' 18 | import { transformAttributes } from '../utils/attributes' 19 | import { AttributePayload } from './Attributes' 20 | 21 | export const Collection = objectType({ 22 | name: 'Collection', 23 | definition(t) { 24 | // TODO: Fix 'rules' 25 | t.model.id() 26 | t.model.name() 27 | 28 | t.list.field('products', { 29 | type: 'Product', 30 | args: { 31 | optionsValuesIds: idArg({ list: true, required: false }), 32 | brandsIds: idArg({ list: true, required: false }), 33 | attributesIds: idArg({ list: true, required: false }), 34 | first: intArg({ required: false }), 35 | last: intArg({ required: false }), 36 | }, 37 | resolve(root, args, ctx) { 38 | const where: ProductWhereInput = { 39 | collections: { 40 | some: { id: root.id }, 41 | }, 42 | } 43 | 44 | if (args.brandsIds && args.brandsIds.length > 0) { 45 | where.brand = { id: { in: args.brandsIds } } 46 | } 47 | 48 | if (args.attributesIds && args.attributesIds.length > 0) { 49 | where.attributes = { some: { id: { in: args.attributesIds } } } 50 | } 51 | 52 | if (args.optionsValuesIds && args.optionsValuesIds.length > 0) { 53 | where.variants = { 54 | some: { 55 | optionValues: { 56 | some: { 57 | id: { in: args.optionsValuesIds }, 58 | }, 59 | }, 60 | }, 61 | } 62 | } 63 | 64 | return ctx.photon.products.findMany({ where }) 65 | }, 66 | }) 67 | 68 | t.field('options', { 69 | type: 'Option', 70 | list: true, 71 | resolve: async (root, _args, ctx) => { 72 | const { products } = await ctx.photon.collections.findOne({ 73 | where: { id: root.id }, 74 | select: { 75 | products: { 76 | select: { 77 | variants: { 78 | select: { 79 | optionValues: { 80 | select: { 81 | option: { 82 | select: { 83 | id: true, 84 | name: true, 85 | isColor: true, 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }) 96 | 97 | /** 98 | * { products { variants { optionsValues { options { } } } } } 99 | */ 100 | 101 | const variants = _.flatMap(products, p => p.variants) 102 | 103 | return optionsFromVariants(variants) 104 | }, 105 | }) 106 | 107 | t.field('brands', { 108 | type: 'Brand', 109 | list: true, 110 | resolve: (parent, _args, ctx) => { 111 | return ctx.photon.brands.findMany({ 112 | where: { 113 | products: { 114 | some: { 115 | collections: { 116 | some: { 117 | id: parent.id, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }) 124 | }, 125 | }) 126 | 127 | t.field('attributes', { 128 | type: AttributePayload, 129 | list: true, 130 | resolve: async (parent, _args, ctx) => { 131 | const attributes = await ctx.photon.attributes.findMany({ 132 | where: { 133 | products: { 134 | some: { 135 | collections: { 136 | some: { 137 | id: parent.id, 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }) 144 | 145 | return transformAttributes(attributes) 146 | }, 147 | }) 148 | }, 149 | }) 150 | 151 | export const CollectionInput = inputObjectType({ 152 | name: 'CollectionInput', 153 | definition(t) { 154 | t.string('name') 155 | t.field('ruleSet', { 156 | type: 'CollectionRuleSetInput', 157 | required: false, 158 | }) 159 | t.string('productsIds', { list: true, required: false }) 160 | }, 161 | }) 162 | 163 | export const CollectionRuleSetInput = inputObjectType({ 164 | name: 'CollectionRuleSetInput', 165 | definition(t) { 166 | t.boolean('applyDisjunctively') 167 | t.field('rules', { 168 | type: 'RulesInput', 169 | list: true, 170 | }) 171 | }, 172 | }) 173 | 174 | export const RulesInput = inputObjectType({ 175 | name: 'RulesInput', 176 | definition(t) { 177 | t.field('field', { type: 'CollectionRuleField' }) 178 | t.field('relation', { type: 'CollectionRuleRelation' }) 179 | t.string('value') 180 | }, 181 | }) 182 | 183 | export const CollectionMutation = extendType({ 184 | type: 'Mutation', 185 | definition(t) { 186 | t.field('collectionCreate', { 187 | type: 'Collection', 188 | args: { 189 | collection: arg({ type: CollectionInput }), 190 | }, 191 | resolve: async (_root, { collection }, ctx) => { 192 | throwIfMissingCollectionInput(collection) 193 | throwIfManualAndAutomatic(collection) 194 | 195 | if (collection.ruleSet && collection.ruleSet.rules.length > 0) { 196 | return recomputeCollection(collection, ctx.photon) 197 | } else { 198 | return createManualCollection(collection, ctx.photon) 199 | } 200 | }, 201 | }) 202 | 203 | t.field('collectionAddProducts', { 204 | type: 'Collection', 205 | args: { 206 | productIds: idArg({ list: true }), 207 | collectionId: idArg(), 208 | }, 209 | resolve: async (_, args, ctx) => { 210 | const collectionRules = await ctx.photon.collections 211 | .findOne({ where: { id: args.collectionId } }) 212 | .rules() 213 | .rules() 214 | 215 | if (collectionRules.length > 0) { 216 | throw new Error('Cannot add products to an automatic collection') 217 | } 218 | 219 | const collection = await ctx.photon.collections.update({ 220 | where: { id: args.collectionId }, 221 | data: { 222 | products: { connect: args.productIds.map(id => ({ id })) }, 223 | }, 224 | }) 225 | 226 | return collection 227 | }, 228 | }) 229 | 230 | t.field('collectionRemoveProducts', { 231 | type: 'Collection', 232 | args: { 233 | productIds: idArg({ list: true }), 234 | collectionId: idArg(), 235 | }, 236 | resolve: async (_, args, ctx) => { 237 | const collectionRules = await ctx.photon.collections 238 | .findOne({ where: { id: args.collectionId } }) 239 | .rules() 240 | .rules() 241 | 242 | if (collectionRules.length > 0) { 243 | throw new Error('Cannot remove products from an automatic collection') 244 | } 245 | 246 | const collection = await ctx.photon.collections.update({ 247 | where: { id: args.collectionId }, 248 | data: { 249 | products: { disconnect: args.productIds.map(id => ({ id })) }, 250 | }, 251 | }) 252 | 253 | return collection 254 | }, 255 | }) 256 | 257 | t.field('collectionUpdate', { 258 | type: 'Collection', 259 | args: { 260 | id: idArg(), 261 | collection: arg({ type: CollectionInput }), 262 | }, 263 | resolve: async (_root, { id, collection }, ctx) => { 264 | throwIfMissingCollectionInput(collection) 265 | throwIfManualAndAutomatic(collection) 266 | 267 | let outputCollection = null 268 | 269 | if (collection.ruleSet && collection.ruleSet.rules.length > 0) { 270 | outputCollection = await recomputeCollection(collection, ctx.photon) 271 | } 272 | 273 | if (collection.productsIds && collection.productsIds.length > 0) { 274 | outputCollection = await createManualCollection( 275 | collection, 276 | ctx.photon, 277 | ) 278 | } 279 | 280 | await ctx.photon.collections.delete({ where: { id } }) 281 | 282 | return outputCollection! 283 | }, 284 | }) 285 | }, 286 | }) 287 | -------------------------------------------------------------------------------- /packages/server/src/graphql/CollectionRule.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from '@prisma/nexus' 2 | 3 | // export const CollectionRuleField = enumType({ 4 | // name: 'CollectionRuleField', 5 | // members: ['TYPE', 'TITLE', 'PRICE'], 6 | // }) 7 | 8 | // export const CollectionRuleRelation = enumType({ 9 | // name: 'CollectionRuleRelation', 10 | // members: [ 11 | // 'CONTAINS', 12 | // 'ENDS_WITH', 13 | // 'EQUALS', 14 | // 'GREATER_THAN', 15 | // 'LESS_THAN', 16 | // 'NOT_CONTAINS', 17 | // 'NOT_EQUALS', 18 | // 'STARTS_WITH', 19 | // ], 20 | // }) 21 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Image.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const Image = objectType({ 4 | name: 'Image', 5 | definition(t) { 6 | t.model.id() 7 | t.model.url() 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Option.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const Option = objectType({ 4 | name: 'Option', 5 | definition(t) { 6 | t.model.id() 7 | t.model.name() 8 | t.model.values() 9 | t.model.isColor() 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/server/src/graphql/OptionValue.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const OptionValue = objectType({ 4 | name: 'OptionValue', 5 | definition(t) { 6 | t.model.id() 7 | t.model.name() 8 | t.model.option() 9 | t.model.variant() 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Product.ts: -------------------------------------------------------------------------------- 1 | import { 2 | objectType, 3 | extendType, 4 | arg, 5 | idArg, 6 | inputObjectType, 7 | } from '@prisma/nexus' 8 | import { fetchAllCollections, productMatchRules } from '../utils/collection' 9 | import { VariantCreateInput } from '@generated/photon' 10 | import { UniqueInput } from './common' 11 | 12 | /** 13 | * type Product { 14 | * id: ID! 15 | * name: String! 16 | * brand: Brand! 17 | * options: [Option!]! 18 | * } 19 | */ 20 | export const Product = objectType({ 21 | name: 'Product', 22 | definition(t) { 23 | t.model.id() 24 | t.model.brand() 25 | t.model.thumbnail() 26 | t.model.name() 27 | t.model.variants() 28 | t.model.slug() 29 | t.model.attributes({ pagination: false }) 30 | 31 | // t.field('attributes', { 32 | // ...t.prismaType.attributes, 33 | // args: {}, 34 | // nullable: false, 35 | // }) 36 | }, 37 | }) 38 | 39 | export const CreateVariantInput = inputObjectType({ 40 | name: 'CreateVariantInput', 41 | definition(t) { 42 | t.boolean('availableForSale') 43 | t.int('price') 44 | t.field('optionsValueIds', { 45 | type: UniqueInput, 46 | list: true, 47 | }) 48 | }, 49 | }) 50 | 51 | export const CreateProductInput = inputObjectType({ 52 | name: 'CreateProductInput', 53 | definition(t) { 54 | t.string('name') 55 | t.string('slug') 56 | t.field('brand', { type: UniqueInput }) 57 | t.field('attributesIds', { type: UniqueInput, list: true }) 58 | t.field('variants', { type: 'CreateVariantInput', list: true }) 59 | }, 60 | }) 61 | 62 | export const UpdateVariantInput = inputObjectType({ 63 | name: 'UpdateVariantInput', 64 | definition(t) { 65 | t.id('id') 66 | t.boolean('availableForSale') 67 | t.int('price') 68 | t.field('optionsValueIds', { 69 | type: UniqueInput, 70 | list: true, 71 | }) 72 | }, 73 | }) 74 | 75 | export const UpdateProductInput = inputObjectType({ 76 | name: 'UpdateProductInput', 77 | definition(t) { 78 | t.id('id') 79 | t.string('name') 80 | t.field('brand', { type: UniqueInput }) 81 | t.field('attributesIds', { type: UniqueInput, list: true }) 82 | t.field('variants', { type: 'UpdateVariantInput', list: true }) 83 | }, 84 | }) 85 | 86 | export const ProductMutation = extendType({ 87 | type: 'Mutation', 88 | definition(t) { 89 | t.crud.createOneProduct() 90 | 91 | t.field('productCreate', { 92 | type: 'Product', 93 | args: { 94 | data: arg({ type: CreateProductInput }), 95 | }, 96 | resolve: async (_parent, { data }, ctx) => { 97 | const collections = await fetchAllCollections(ctx.photon) 98 | const collectionsToConnect = collections 99 | .filter( 100 | c => 101 | c.rules && 102 | productMatchRules( 103 | { id: '', name: data.name, type: { id: '', name: 'SHOES' } }, 104 | c.rules.rules, 105 | ), 106 | ) 107 | .map(c => ({ id: c.id })) 108 | 109 | return ctx.photon.products.create({ 110 | data: { 111 | name: data.name, 112 | slug: '', 113 | description: '', 114 | brand: { connect: { id: data.brand.id } }, 115 | attributes: { 116 | connect: data.attributesIds, 117 | }, 118 | variants: { 119 | create: data.variants.map( 120 | variant => 121 | ({ 122 | optionValues: { 123 | connect: variant.optionsValueIds, 124 | }, 125 | } as VariantCreateInput), 126 | ), 127 | }, 128 | collections: { connect: collectionsToConnect }, 129 | }, 130 | }) 131 | }, 132 | }) 133 | 134 | t.field('productDelete', { 135 | type: 'Product', 136 | args: { 137 | productId: idArg(), 138 | }, 139 | resolve: async (_root, args, ctx) => { 140 | return ctx.photon.products.delete({ where: { id: args.productId } }) 141 | }, 142 | }) 143 | 144 | t.field('productUpdate', { 145 | type: 'Product', 146 | args: { 147 | data: arg({ type: 'UpdateProductInput' }), 148 | }, 149 | resolve: async () => { 150 | throw new Error('updateProduct resolve not implemented yet') 151 | }, 152 | }) 153 | }, 154 | }) 155 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Query.ts: -------------------------------------------------------------------------------- 1 | import { idArg, objectType } from '@prisma/nexus' 2 | 3 | /** 4 | * type Query { 5 | * products(...): [Product!]! 6 | * options(...): [Option!]! 7 | * brands(...): [Brand!]! 8 | * collection(...): Collection! 9 | * } 10 | */ 11 | export const Query = objectType({ 12 | name: 'Query', 13 | definition(t) { 14 | t.crud.findManyVariant({ filtering: true }) 15 | t.crud.findOneVariant() 16 | t.crud.findManyProduct({ alias: 'products' }) 17 | t.crud.findOneProduct({ alias: 'product' }) 18 | t.crud.findManyOption({ alias: 'options' }) 19 | t.crud.findManyBrand({ alias: 'brands' }) 20 | t.crud.findManyCollection({ alias: 'collections' }) 21 | 22 | t.field('collection', { 23 | type: 'Collection', 24 | args: { 25 | collectionId: idArg(), 26 | }, 27 | resolve: (_root, args, ctx) => { 28 | return ctx.photon.collections.findOne({ 29 | where: { id: args.collectionId }, 30 | }) 31 | }, 32 | }) 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /packages/server/src/graphql/Variant.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@prisma/nexus' 2 | 3 | export const Variant = objectType({ 4 | name: 'Variant', 5 | definition(t) { 6 | t.model.id() 7 | t.model.availableForSale() 8 | t.model.images() 9 | t.model.optionValues() 10 | t.model.price() 11 | t.model.product() 12 | t.model.sku() 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/server/src/graphql/common.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from '@prisma/nexus'; 2 | 3 | export const UniqueInput = inputObjectType({ 4 | name: 'UniqueInput', 5 | definition(t) { 6 | t.id('id') 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/server/src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Query' 2 | export * from './Product' 3 | export * from './Collection' 4 | export * from './Attributes' 5 | export * from './Brand' 6 | export * from './Option' 7 | export * from './OptionValue' 8 | export * from './Variant' 9 | export * from './Image' 10 | export * from './CollectionRule' 11 | -------------------------------------------------------------------------------- /packages/server/src/graphql/utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { Option } from '@generated/photon' 3 | 4 | interface Variant { 5 | optionValues: { option: Option }[] 6 | } 7 | 8 | export function optionsFromVariants(variants: Variant[]): Option[] { 9 | return _(variants) 10 | .flatMap(v => v.optionValues) 11 | .map(v => v.option) 12 | .uniqBy(v => v.id) 13 | .value() 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server' 2 | import { makeSchema } from '@prisma/nexus' 3 | import { join } from 'path' 4 | import { nexusPrismaMethod } from '@generated/nexus-prisma' 5 | import Photon from '@generated/photon' 6 | import { Context } from './context' 7 | import * as allTypes from './graphql' 8 | 9 | const photon = new Photon({ debug: true }) 10 | 11 | const nexusPrisma = nexusPrismaMethod({ 12 | photon: (ctx: Context) => ctx.photon, 13 | }) 14 | 15 | const schema = makeSchema({ 16 | types: [allTypes, nexusPrisma], 17 | outputs: { 18 | typegen: join(__dirname, 'nexus-typegen.ts'), 19 | schema: join(__dirname, 'schema.graphql'), 20 | }, 21 | nonNullDefaults: { 22 | input: true, 23 | output: true, 24 | }, 25 | typegenAutoConfig: { 26 | sources: [ 27 | { 28 | source: '@generated/photon', 29 | alias: 'photon', 30 | }, 31 | { 32 | source: join(__dirname, 'context.ts'), 33 | alias: 'ctx', 34 | }, 35 | ], 36 | contextType: 'ctx.Context', 37 | }, 38 | }) 39 | 40 | const server = new ApolloServer({ 41 | schema, 42 | context: { photon }, 43 | }) 44 | 45 | server 46 | .listen({ port: 4000 }, () => 47 | console.log(`🚀 Server ready at http://localhost:4000`), 48 | ) 49 | .then(serverInfo => { 50 | async function cleanup() { 51 | serverInfo.server.close() 52 | //await photon.disconnect() 53 | } 54 | 55 | process.on('SIGINT', cleanup) 56 | process.on('SIGTERM', cleanup) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/server/src/nexus-typegen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by GraphQL Nexus 3 | * Do not make changes to this file directly 4 | */ 5 | 6 | import * as ctx from "./context" 7 | import * as photon from "@generated/photon" 8 | import { core } from "nexus" 9 | 10 | declare global { 11 | interface NexusGenCustomOutputMethods { 12 | crud: NexusPrisma 13 | model: NexusPrisma 14 | } 15 | } 16 | 17 | declare global { 18 | interface NexusGen extends NexusGenTypes {} 19 | } 20 | 21 | export interface NexusGenInputs { 22 | AttributeCreateManyWithoutAttributesInput: { // input type 23 | connect?: NexusGenInputs['AttributeWhereUniqueInput'][] | null; // [AttributeWhereUniqueInput!] 24 | create?: NexusGenInputs['AttributeCreateWithoutProductsInput'][] | null; // [AttributeCreateWithoutProductsInput!] 25 | } 26 | AttributeCreateWithoutProductsInput: { // input type 27 | id?: string | null; // ID 28 | key: string; // String! 29 | value: string; // String! 30 | } 31 | AttributeWhereUniqueInput: { // input type 32 | id?: string | null; // ID 33 | } 34 | BrandCreateOneWithoutBrandInput: { // input type 35 | connect?: NexusGenInputs['BrandWhereUniqueInput'] | null; // BrandWhereUniqueInput 36 | create?: NexusGenInputs['BrandCreateWithoutProductsInput'] | null; // BrandCreateWithoutProductsInput 37 | } 38 | BrandCreateWithoutProductsInput: { // input type 39 | id?: string | null; // ID 40 | name: string; // String! 41 | } 42 | BrandWhereUniqueInput: { // input type 43 | id?: string | null; // ID 44 | } 45 | CollectionCreateManyWithoutCollectionsInput: { // input type 46 | connect?: NexusGenInputs['CollectionWhereUniqueInput'][] | null; // [CollectionWhereUniqueInput!] 47 | create?: NexusGenInputs['CollectionCreateWithoutProductsInput'][] | null; // [CollectionCreateWithoutProductsInput!] 48 | } 49 | CollectionCreateWithoutProductsInput: { // input type 50 | id?: string | null; // ID 51 | name: string; // String! 52 | rules?: NexusGenInputs['CollectionRuleSetCreateOneWithoutRulesInput'] | null; // CollectionRuleSetCreateOneWithoutRulesInput 53 | } 54 | CollectionInput: { // input type 55 | name: string; // String! 56 | productsIds?: string[] | null; // [String!] 57 | ruleSet?: NexusGenInputs['CollectionRuleSetInput'] | null; // CollectionRuleSetInput 58 | } 59 | CollectionRuleCreateManyWithoutRulesInput: { // input type 60 | connect?: NexusGenInputs['CollectionRuleWhereUniqueInput'][] | null; // [CollectionRuleWhereUniqueInput!] 61 | create?: NexusGenInputs['CollectionRuleCreateWithoutCollectionRuleSetInput'][] | null; // [CollectionRuleCreateWithoutCollectionRuleSetInput!] 62 | } 63 | CollectionRuleCreateWithoutCollectionRuleSetInput: { // input type 64 | field: NexusGenEnums['CollectionRuleField']; // CollectionRuleField! 65 | id?: string | null; // ID 66 | relation: NexusGenEnums['CollectionRuleRelation']; // CollectionRuleRelation! 67 | value: string; // String! 68 | } 69 | CollectionRuleSetCreateOneWithoutRulesInput: { // input type 70 | connect?: NexusGenInputs['CollectionRuleSetWhereUniqueInput'] | null; // CollectionRuleSetWhereUniqueInput 71 | create?: NexusGenInputs['CollectionRuleSetCreateWithoutCollectionInput'] | null; // CollectionRuleSetCreateWithoutCollectionInput 72 | } 73 | CollectionRuleSetCreateWithoutCollectionInput: { // input type 74 | appliesDisjunctively: boolean; // Boolean! 75 | id?: string | null; // ID 76 | rules?: NexusGenInputs['CollectionRuleCreateManyWithoutRulesInput'] | null; // CollectionRuleCreateManyWithoutRulesInput 77 | } 78 | CollectionRuleSetInput: { // input type 79 | applyDisjunctively: boolean; // Boolean! 80 | rules: NexusGenInputs['RulesInput'][]; // [RulesInput!]! 81 | } 82 | CollectionRuleSetWhereUniqueInput: { // input type 83 | id?: string | null; // ID 84 | } 85 | CollectionRuleWhereUniqueInput: { // input type 86 | id?: string | null; // ID 87 | } 88 | CollectionWhereUniqueInput: { // input type 89 | id?: string | null; // ID 90 | } 91 | CreateProductInput: { // input type 92 | attributesIds: NexusGenInputs['UniqueInput'][]; // [UniqueInput!]! 93 | brand: NexusGenInputs['UniqueInput']; // UniqueInput! 94 | name: string; // String! 95 | slug: string; // String! 96 | variants: NexusGenInputs['CreateVariantInput'][]; // [CreateVariantInput!]! 97 | } 98 | CreateVariantInput: { // input type 99 | availableForSale: boolean; // Boolean! 100 | optionsValueIds: NexusGenInputs['UniqueInput'][]; // [UniqueInput!]! 101 | price: number; // Int! 102 | } 103 | ImageCreateManyWithoutImagesInput: { // input type 104 | connect?: NexusGenInputs['ImageWhereUniqueInput'][] | null; // [ImageWhereUniqueInput!] 105 | create?: NexusGenInputs['ImageCreateWithoutVariantInput'][] | null; // [ImageCreateWithoutVariantInput!] 106 | } 107 | ImageCreateOneWithoutThumbnailInput: { // input type 108 | connect?: NexusGenInputs['ImageWhereUniqueInput'] | null; // ImageWhereUniqueInput 109 | create?: NexusGenInputs['ImageCreateWithoutProductInput'] | null; // ImageCreateWithoutProductInput 110 | } 111 | ImageCreateWithoutProductInput: { // input type 112 | id?: string | null; // ID 113 | url: string; // String! 114 | variant?: NexusGenInputs['VariantCreateOneWithoutVariantInput'] | null; // VariantCreateOneWithoutVariantInput 115 | } 116 | ImageCreateWithoutVariantInput: { // input type 117 | id?: string | null; // ID 118 | product?: NexusGenInputs['ProductCreateOneWithoutProductInput'] | null; // ProductCreateOneWithoutProductInput 119 | url: string; // String! 120 | } 121 | ImageWhereUniqueInput: { // input type 122 | id?: string | null; // ID 123 | } 124 | IntFilter: { // input type 125 | equals?: number | null; // Int 126 | gt?: number | null; // Int 127 | gte?: number | null; // Int 128 | in?: number[] | null; // [Int!] 129 | lt?: number | null; // Int 130 | lte?: number | null; // Int 131 | not?: number | null; // Int 132 | notIn?: number[] | null; // [Int!] 133 | } 134 | NullableStringFilter: { // input type 135 | contains?: string | null; // String 136 | endsWith?: string | null; // String 137 | equals?: string | null; // String 138 | gt?: string | null; // String 139 | gte?: string | null; // String 140 | in?: string[] | null; // [String!] 141 | lt?: string | null; // String 142 | lte?: string | null; // String 143 | not?: string | null; // String 144 | notIn?: string[] | null; // [String!] 145 | startsWith?: string | null; // String 146 | } 147 | OptionCreateOneWithoutOptionInput: { // input type 148 | connect?: NexusGenInputs['OptionWhereUniqueInput'] | null; // OptionWhereUniqueInput 149 | create?: NexusGenInputs['OptionCreateWithoutValuesInput'] | null; // OptionCreateWithoutValuesInput 150 | } 151 | OptionCreateWithoutValuesInput: { // input type 152 | id?: string | null; // ID 153 | isColor?: boolean | null; // Boolean 154 | name: string; // String! 155 | } 156 | OptionValueCreateManyWithoutOptionValuesInput: { // input type 157 | connect?: NexusGenInputs['OptionValueWhereUniqueInput'][] | null; // [OptionValueWhereUniqueInput!] 158 | create?: NexusGenInputs['OptionValueCreateWithoutVariantInput'][] | null; // [OptionValueCreateWithoutVariantInput!] 159 | } 160 | OptionValueCreateWithoutVariantInput: { // input type 161 | id?: string | null; // ID 162 | name: string; // String! 163 | option: NexusGenInputs['OptionCreateOneWithoutOptionInput']; // OptionCreateOneWithoutOptionInput! 164 | } 165 | OptionValueWhereUniqueInput: { // input type 166 | id?: string | null; // ID 167 | } 168 | OptionWhereUniqueInput: { // input type 169 | id?: string | null; // ID 170 | } 171 | ProductCreateInput: { // input type 172 | attributes?: NexusGenInputs['AttributeCreateManyWithoutAttributesInput'] | null; // AttributeCreateManyWithoutAttributesInput 173 | brand: NexusGenInputs['BrandCreateOneWithoutBrandInput']; // BrandCreateOneWithoutBrandInput! 174 | collections?: NexusGenInputs['CollectionCreateManyWithoutCollectionsInput'] | null; // CollectionCreateManyWithoutCollectionsInput 175 | description: string; // String! 176 | id?: string | null; // ID 177 | name: string; // String! 178 | slug: string; // String! 179 | thumbnail?: NexusGenInputs['ImageCreateOneWithoutThumbnailInput'] | null; // ImageCreateOneWithoutThumbnailInput 180 | type?: NexusGenInputs['ProductTypeCreateOneWithoutTypeInput'] | null; // ProductTypeCreateOneWithoutTypeInput 181 | variants?: NexusGenInputs['VariantCreateManyWithoutVariantsInput'] | null; // VariantCreateManyWithoutVariantsInput 182 | } 183 | ProductCreateOneWithoutProductInput: { // input type 184 | connect?: NexusGenInputs['ProductWhereUniqueInput'] | null; // ProductWhereUniqueInput 185 | create?: NexusGenInputs['ProductCreateWithoutVariantsInput'] | null; // ProductCreateWithoutVariantsInput 186 | } 187 | ProductCreateWithoutVariantsInput: { // input type 188 | attributes?: NexusGenInputs['AttributeCreateManyWithoutAttributesInput'] | null; // AttributeCreateManyWithoutAttributesInput 189 | brand: NexusGenInputs['BrandCreateOneWithoutBrandInput']; // BrandCreateOneWithoutBrandInput! 190 | collections?: NexusGenInputs['CollectionCreateManyWithoutCollectionsInput'] | null; // CollectionCreateManyWithoutCollectionsInput 191 | description: string; // String! 192 | id?: string | null; // ID 193 | name: string; // String! 194 | slug: string; // String! 195 | thumbnail?: NexusGenInputs['ImageCreateOneWithoutThumbnailInput'] | null; // ImageCreateOneWithoutThumbnailInput 196 | type?: NexusGenInputs['ProductTypeCreateOneWithoutTypeInput'] | null; // ProductTypeCreateOneWithoutTypeInput 197 | } 198 | ProductTypeCreateOneWithoutTypeInput: { // input type 199 | connect?: NexusGenInputs['ProductTypeWhereUniqueInput'] | null; // ProductTypeWhereUniqueInput 200 | create?: NexusGenInputs['ProductTypeCreateWithoutProductInput'] | null; // ProductTypeCreateWithoutProductInput 201 | } 202 | ProductTypeCreateWithoutProductInput: { // input type 203 | id?: string | null; // ID 204 | name: string; // String! 205 | } 206 | ProductTypeWhereUniqueInput: { // input type 207 | id?: string | null; // ID 208 | } 209 | ProductWhereUniqueInput: { // input type 210 | id?: string | null; // ID 211 | slug?: string | null; // String 212 | } 213 | QueryFindManyVariantFilter: { // input type 214 | every?: NexusGenInputs['QueryFindManyVariantWhereInput'] | null; // QueryFindManyVariantWhereInput 215 | none?: NexusGenInputs['QueryFindManyVariantWhereInput'] | null; // QueryFindManyVariantWhereInput 216 | some?: NexusGenInputs['QueryFindManyVariantWhereInput'] | null; // QueryFindManyVariantWhereInput 217 | } 218 | QueryFindManyVariantWhereInput: { // input type 219 | AND?: NexusGenInputs['QueryFindManyVariantWhereInput'][] | null; // [QueryFindManyVariantWhereInput!] 220 | availableForSale?: NexusGenInputs['QueryFindManyVariantFilter'] | null; // QueryFindManyVariantFilter 221 | id?: NexusGenInputs['StringFilter'] | null; // StringFilter 222 | images?: NexusGenInputs['QueryFindManyVariantFilter'] | null; // QueryFindManyVariantFilter 223 | NOT?: NexusGenInputs['QueryFindManyVariantWhereInput'][] | null; // [QueryFindManyVariantWhereInput!] 224 | optionValues?: NexusGenInputs['QueryFindManyVariantFilter'] | null; // QueryFindManyVariantFilter 225 | OR?: NexusGenInputs['QueryFindManyVariantWhereInput'][] | null; // [QueryFindManyVariantWhereInput!] 226 | price?: NexusGenInputs['IntFilter'] | null; // IntFilter 227 | product?: NexusGenInputs['QueryFindManyVariantWhereInput'] | null; // QueryFindManyVariantWhereInput 228 | sku?: NexusGenInputs['NullableStringFilter'] | null; // NullableStringFilter 229 | } 230 | RulesInput: { // input type 231 | field: NexusGenEnums['CollectionRuleField']; // CollectionRuleField! 232 | relation: NexusGenEnums['CollectionRuleRelation']; // CollectionRuleRelation! 233 | value: string; // String! 234 | } 235 | StringFilter: { // input type 236 | contains?: string | null; // String 237 | endsWith?: string | null; // String 238 | equals?: string | null; // String 239 | gt?: string | null; // String 240 | gte?: string | null; // String 241 | in?: string[] | null; // [String!] 242 | lt?: string | null; // String 243 | lte?: string | null; // String 244 | not?: string | null; // String 245 | notIn?: string[] | null; // [String!] 246 | startsWith?: string | null; // String 247 | } 248 | UniqueInput: { // input type 249 | id: string; // ID! 250 | } 251 | UpdateProductInput: { // input type 252 | attributesIds: NexusGenInputs['UniqueInput'][]; // [UniqueInput!]! 253 | brand: NexusGenInputs['UniqueInput']; // UniqueInput! 254 | id: string; // ID! 255 | name: string; // String! 256 | variants: NexusGenInputs['UpdateVariantInput'][]; // [UpdateVariantInput!]! 257 | } 258 | UpdateVariantInput: { // input type 259 | availableForSale: boolean; // Boolean! 260 | id: string; // ID! 261 | optionsValueIds: NexusGenInputs['UniqueInput'][]; // [UniqueInput!]! 262 | price: number; // Int! 263 | } 264 | VariantCreateManyWithoutVariantsInput: { // input type 265 | connect?: NexusGenInputs['VariantWhereUniqueInput'][] | null; // [VariantWhereUniqueInput!] 266 | create?: NexusGenInputs['VariantCreateWithoutProductInput'][] | null; // [VariantCreateWithoutProductInput!] 267 | } 268 | VariantCreateOneWithoutVariantInput: { // input type 269 | connect?: NexusGenInputs['VariantWhereUniqueInput'] | null; // VariantWhereUniqueInput 270 | create?: NexusGenInputs['VariantCreateWithoutImagesInput'] | null; // VariantCreateWithoutImagesInput 271 | } 272 | VariantCreateWithoutImagesInput: { // input type 273 | availableForSale?: boolean | null; // Boolean 274 | id?: string | null; // ID 275 | optionValues?: NexusGenInputs['OptionValueCreateManyWithoutOptionValuesInput'] | null; // OptionValueCreateManyWithoutOptionValuesInput 276 | price: number; // Int! 277 | product?: NexusGenInputs['ProductCreateOneWithoutProductInput'] | null; // ProductCreateOneWithoutProductInput 278 | sku?: string | null; // String 279 | } 280 | VariantCreateWithoutProductInput: { // input type 281 | availableForSale?: boolean | null; // Boolean 282 | id?: string | null; // ID 283 | images?: NexusGenInputs['ImageCreateManyWithoutImagesInput'] | null; // ImageCreateManyWithoutImagesInput 284 | optionValues?: NexusGenInputs['OptionValueCreateManyWithoutOptionValuesInput'] | null; // OptionValueCreateManyWithoutOptionValuesInput 285 | price: number; // Int! 286 | sku?: string | null; // String 287 | } 288 | VariantWhereUniqueInput: { // input type 289 | id?: string | null; // ID 290 | } 291 | } 292 | 293 | export interface NexusGenEnums { 294 | CollectionRuleField: photon.CollectionRuleField 295 | CollectionRuleRelation: photon.CollectionRuleRelation 296 | } 297 | 298 | export interface NexusGenRootTypes { 299 | Attribute: photon.Attribute; 300 | AttributePayload: { // root type 301 | name: string; // String! 302 | values: NexusGenRootTypes['AttributeValue'][]; // [AttributeValue!]! 303 | } 304 | AttributeValue: { // root type 305 | id: string; // ID! 306 | value: string; // String! 307 | } 308 | Brand: photon.Brand; 309 | Collection: photon.Collection; 310 | Image: photon.Image; 311 | Mutation: {}; 312 | Option: photon.Option; 313 | OptionValue: photon.OptionValue; 314 | Product: photon.Product; 315 | Query: {}; 316 | Variant: photon.Variant; 317 | String: string; 318 | Int: number; 319 | Float: number; 320 | Boolean: boolean; 321 | ID: string; 322 | } 323 | 324 | export interface NexusGenAllTypes extends NexusGenRootTypes { 325 | AttributeCreateManyWithoutAttributesInput: NexusGenInputs['AttributeCreateManyWithoutAttributesInput']; 326 | AttributeCreateWithoutProductsInput: NexusGenInputs['AttributeCreateWithoutProductsInput']; 327 | AttributeWhereUniqueInput: NexusGenInputs['AttributeWhereUniqueInput']; 328 | BrandCreateOneWithoutBrandInput: NexusGenInputs['BrandCreateOneWithoutBrandInput']; 329 | BrandCreateWithoutProductsInput: NexusGenInputs['BrandCreateWithoutProductsInput']; 330 | BrandWhereUniqueInput: NexusGenInputs['BrandWhereUniqueInput']; 331 | CollectionCreateManyWithoutCollectionsInput: NexusGenInputs['CollectionCreateManyWithoutCollectionsInput']; 332 | CollectionCreateWithoutProductsInput: NexusGenInputs['CollectionCreateWithoutProductsInput']; 333 | CollectionInput: NexusGenInputs['CollectionInput']; 334 | CollectionRuleCreateManyWithoutRulesInput: NexusGenInputs['CollectionRuleCreateManyWithoutRulesInput']; 335 | CollectionRuleCreateWithoutCollectionRuleSetInput: NexusGenInputs['CollectionRuleCreateWithoutCollectionRuleSetInput']; 336 | CollectionRuleSetCreateOneWithoutRulesInput: NexusGenInputs['CollectionRuleSetCreateOneWithoutRulesInput']; 337 | CollectionRuleSetCreateWithoutCollectionInput: NexusGenInputs['CollectionRuleSetCreateWithoutCollectionInput']; 338 | CollectionRuleSetInput: NexusGenInputs['CollectionRuleSetInput']; 339 | CollectionRuleSetWhereUniqueInput: NexusGenInputs['CollectionRuleSetWhereUniqueInput']; 340 | CollectionRuleWhereUniqueInput: NexusGenInputs['CollectionRuleWhereUniqueInput']; 341 | CollectionWhereUniqueInput: NexusGenInputs['CollectionWhereUniqueInput']; 342 | CreateProductInput: NexusGenInputs['CreateProductInput']; 343 | CreateVariantInput: NexusGenInputs['CreateVariantInput']; 344 | ImageCreateManyWithoutImagesInput: NexusGenInputs['ImageCreateManyWithoutImagesInput']; 345 | ImageCreateOneWithoutThumbnailInput: NexusGenInputs['ImageCreateOneWithoutThumbnailInput']; 346 | ImageCreateWithoutProductInput: NexusGenInputs['ImageCreateWithoutProductInput']; 347 | ImageCreateWithoutVariantInput: NexusGenInputs['ImageCreateWithoutVariantInput']; 348 | ImageWhereUniqueInput: NexusGenInputs['ImageWhereUniqueInput']; 349 | IntFilter: NexusGenInputs['IntFilter']; 350 | NullableStringFilter: NexusGenInputs['NullableStringFilter']; 351 | OptionCreateOneWithoutOptionInput: NexusGenInputs['OptionCreateOneWithoutOptionInput']; 352 | OptionCreateWithoutValuesInput: NexusGenInputs['OptionCreateWithoutValuesInput']; 353 | OptionValueCreateManyWithoutOptionValuesInput: NexusGenInputs['OptionValueCreateManyWithoutOptionValuesInput']; 354 | OptionValueCreateWithoutVariantInput: NexusGenInputs['OptionValueCreateWithoutVariantInput']; 355 | OptionValueWhereUniqueInput: NexusGenInputs['OptionValueWhereUniqueInput']; 356 | OptionWhereUniqueInput: NexusGenInputs['OptionWhereUniqueInput']; 357 | ProductCreateInput: NexusGenInputs['ProductCreateInput']; 358 | ProductCreateOneWithoutProductInput: NexusGenInputs['ProductCreateOneWithoutProductInput']; 359 | ProductCreateWithoutVariantsInput: NexusGenInputs['ProductCreateWithoutVariantsInput']; 360 | ProductTypeCreateOneWithoutTypeInput: NexusGenInputs['ProductTypeCreateOneWithoutTypeInput']; 361 | ProductTypeCreateWithoutProductInput: NexusGenInputs['ProductTypeCreateWithoutProductInput']; 362 | ProductTypeWhereUniqueInput: NexusGenInputs['ProductTypeWhereUniqueInput']; 363 | ProductWhereUniqueInput: NexusGenInputs['ProductWhereUniqueInput']; 364 | QueryFindManyVariantFilter: NexusGenInputs['QueryFindManyVariantFilter']; 365 | QueryFindManyVariantWhereInput: NexusGenInputs['QueryFindManyVariantWhereInput']; 366 | RulesInput: NexusGenInputs['RulesInput']; 367 | StringFilter: NexusGenInputs['StringFilter']; 368 | UniqueInput: NexusGenInputs['UniqueInput']; 369 | UpdateProductInput: NexusGenInputs['UpdateProductInput']; 370 | UpdateVariantInput: NexusGenInputs['UpdateVariantInput']; 371 | VariantCreateManyWithoutVariantsInput: NexusGenInputs['VariantCreateManyWithoutVariantsInput']; 372 | VariantCreateOneWithoutVariantInput: NexusGenInputs['VariantCreateOneWithoutVariantInput']; 373 | VariantCreateWithoutImagesInput: NexusGenInputs['VariantCreateWithoutImagesInput']; 374 | VariantCreateWithoutProductInput: NexusGenInputs['VariantCreateWithoutProductInput']; 375 | VariantWhereUniqueInput: NexusGenInputs['VariantWhereUniqueInput']; 376 | CollectionRuleField: NexusGenEnums['CollectionRuleField']; 377 | CollectionRuleRelation: NexusGenEnums['CollectionRuleRelation']; 378 | } 379 | 380 | export interface NexusGenFieldTypes { 381 | Attribute: { // field return type 382 | id: string; // ID! 383 | key: string; // String! 384 | products: NexusGenRootTypes['Product'][] | null; // [Product!] 385 | value: string; // String! 386 | } 387 | AttributePayload: { // field return type 388 | name: string; // String! 389 | values: NexusGenRootTypes['AttributeValue'][]; // [AttributeValue!]! 390 | } 391 | AttributeValue: { // field return type 392 | id: string; // ID! 393 | value: string; // String! 394 | } 395 | Brand: { // field return type 396 | id: string; // ID! 397 | name: string; // String! 398 | products: NexusGenRootTypes['Product'][] | null; // [Product!] 399 | } 400 | Collection: { // field return type 401 | attributes: NexusGenRootTypes['AttributePayload'][]; // [AttributePayload!]! 402 | brands: NexusGenRootTypes['Brand'][]; // [Brand!]! 403 | id: string; // ID! 404 | name: string; // String! 405 | options: NexusGenRootTypes['Option'][]; // [Option!]! 406 | products: NexusGenRootTypes['Product'][]; // [Product!]! 407 | } 408 | Image: { // field return type 409 | id: string; // ID! 410 | url: string; // String! 411 | } 412 | Mutation: { // field return type 413 | collectionAddProducts: NexusGenRootTypes['Collection']; // Collection! 414 | collectionCreate: NexusGenRootTypes['Collection']; // Collection! 415 | collectionRemoveProducts: NexusGenRootTypes['Collection']; // Collection! 416 | collectionUpdate: NexusGenRootTypes['Collection']; // Collection! 417 | createOneProduct: NexusGenRootTypes['Product']; // Product! 418 | productCreate: NexusGenRootTypes['Product']; // Product! 419 | productDelete: NexusGenRootTypes['Product']; // Product! 420 | productUpdate: NexusGenRootTypes['Product']; // Product! 421 | } 422 | Option: { // field return type 423 | id: string; // ID! 424 | isColor: boolean | null; // Boolean 425 | name: string; // String! 426 | values: NexusGenRootTypes['OptionValue'][] | null; // [OptionValue!] 427 | } 428 | OptionValue: { // field return type 429 | id: string; // ID! 430 | name: string; // String! 431 | option: NexusGenRootTypes['Option']; // Option! 432 | variant: NexusGenRootTypes['Variant'] | null; // Variant 433 | } 434 | Product: { // field return type 435 | attributes: NexusGenRootTypes['Attribute'][] | null; // [Attribute!] 436 | brand: NexusGenRootTypes['Brand']; // Brand! 437 | id: string; // ID! 438 | name: string; // String! 439 | slug: string; // String! 440 | thumbnail: NexusGenRootTypes['Image'] | null; // Image 441 | variants: NexusGenRootTypes['Variant'][] | null; // [Variant!] 442 | } 443 | Query: { // field return type 444 | brands: NexusGenRootTypes['Brand'][] | null; // [Brand!] 445 | collection: NexusGenRootTypes['Collection']; // Collection! 446 | collections: NexusGenRootTypes['Collection'][] | null; // [Collection!] 447 | findManyVariant: NexusGenRootTypes['Variant'][] | null; // [Variant!] 448 | findOneVariant: NexusGenRootTypes['Variant'] | null; // Variant 449 | options: NexusGenRootTypes['Option'][] | null; // [Option!] 450 | product: NexusGenRootTypes['Product'] | null; // Product 451 | products: NexusGenRootTypes['Product'][] | null; // [Product!] 452 | } 453 | Variant: { // field return type 454 | availableForSale: boolean | null; // Boolean 455 | id: string; // ID! 456 | images: NexusGenRootTypes['Image'][] | null; // [Image!] 457 | optionValues: NexusGenRootTypes['OptionValue'][] | null; // [OptionValue!] 458 | price: number; // Int! 459 | product: NexusGenRootTypes['Product'] | null; // Product 460 | sku: string | null; // String 461 | } 462 | } 463 | 464 | export interface NexusGenArgTypes { 465 | Attribute: { 466 | products: { // args 467 | after?: string | null; // String 468 | before?: string | null; // String 469 | first?: number | null; // Int 470 | last?: number | null; // Int 471 | skip?: number | null; // Int 472 | } 473 | } 474 | Brand: { 475 | products: { // args 476 | after?: string | null; // String 477 | before?: string | null; // String 478 | first?: number | null; // Int 479 | last?: number | null; // Int 480 | skip?: number | null; // Int 481 | } 482 | } 483 | Collection: { 484 | products: { // args 485 | attributesIds?: string[] | null; // [ID!] 486 | brandsIds?: string[] | null; // [ID!] 487 | first?: number | null; // Int 488 | last?: number | null; // Int 489 | optionsValuesIds?: string[] | null; // [ID!] 490 | } 491 | } 492 | Mutation: { 493 | collectionAddProducts: { // args 494 | collectionId: string; // ID! 495 | productIds: string[]; // [ID!]! 496 | } 497 | collectionCreate: { // args 498 | collection: NexusGenInputs['CollectionInput']; // CollectionInput! 499 | } 500 | collectionRemoveProducts: { // args 501 | collectionId: string; // ID! 502 | productIds: string[]; // [ID!]! 503 | } 504 | collectionUpdate: { // args 505 | collection: NexusGenInputs['CollectionInput']; // CollectionInput! 506 | id: string; // ID! 507 | } 508 | createOneProduct: { // args 509 | data: NexusGenInputs['ProductCreateInput']; // ProductCreateInput! 510 | } 511 | productCreate: { // args 512 | data: NexusGenInputs['CreateProductInput']; // CreateProductInput! 513 | } 514 | productDelete: { // args 515 | productId: string; // ID! 516 | } 517 | productUpdate: { // args 518 | data: NexusGenInputs['UpdateProductInput']; // UpdateProductInput! 519 | } 520 | } 521 | Option: { 522 | values: { // args 523 | after?: string | null; // String 524 | before?: string | null; // String 525 | first?: number | null; // Int 526 | last?: number | null; // Int 527 | skip?: number | null; // Int 528 | } 529 | } 530 | Product: { 531 | attributes: { // args 532 | after?: string | null; // String 533 | before?: string | null; // String 534 | first?: number | null; // Int 535 | last?: number | null; // Int 536 | skip?: number | null; // Int 537 | } 538 | variants: { // args 539 | after?: string | null; // String 540 | before?: string | null; // String 541 | first?: number | null; // Int 542 | last?: number | null; // Int 543 | skip?: number | null; // Int 544 | } 545 | } 546 | Query: { 547 | brands: { // args 548 | after?: string | null; // String 549 | before?: string | null; // String 550 | first?: number | null; // Int 551 | last?: number | null; // Int 552 | skip?: number | null; // Int 553 | } 554 | collection: { // args 555 | collectionId: string; // ID! 556 | } 557 | collections: { // args 558 | after?: string | null; // String 559 | before?: string | null; // String 560 | first?: number | null; // Int 561 | last?: number | null; // Int 562 | skip?: number | null; // Int 563 | } 564 | findManyVariant: { // args 565 | after?: string | null; // String 566 | before?: string | null; // String 567 | first?: number | null; // Int 568 | last?: number | null; // Int 569 | skip?: number | null; // Int 570 | where?: NexusGenInputs['QueryFindManyVariantWhereInput'] | null; // QueryFindManyVariantWhereInput 571 | } 572 | findOneVariant: { // args 573 | where: NexusGenInputs['VariantWhereUniqueInput']; // VariantWhereUniqueInput! 574 | } 575 | options: { // args 576 | after?: string | null; // String 577 | before?: string | null; // String 578 | first?: number | null; // Int 579 | last?: number | null; // Int 580 | skip?: number | null; // Int 581 | } 582 | product: { // args 583 | where: NexusGenInputs['ProductWhereUniqueInput']; // ProductWhereUniqueInput! 584 | } 585 | products: { // args 586 | after?: string | null; // String 587 | before?: string | null; // String 588 | first?: number | null; // Int 589 | last?: number | null; // Int 590 | skip?: number | null; // Int 591 | } 592 | } 593 | Variant: { 594 | images: { // args 595 | after?: string | null; // String 596 | before?: string | null; // String 597 | first?: number | null; // Int 598 | last?: number | null; // Int 599 | skip?: number | null; // Int 600 | } 601 | optionValues: { // args 602 | after?: string | null; // String 603 | before?: string | null; // String 604 | first?: number | null; // Int 605 | last?: number | null; // Int 606 | skip?: number | null; // Int 607 | } 608 | } 609 | } 610 | 611 | export interface NexusGenAbstractResolveReturnTypes { 612 | } 613 | 614 | export interface NexusGenInheritedFields {} 615 | 616 | export type NexusGenObjectNames = "Attribute" | "AttributePayload" | "AttributeValue" | "Brand" | "Collection" | "Image" | "Mutation" | "Option" | "OptionValue" | "Product" | "Query" | "Variant"; 617 | 618 | export type NexusGenInputNames = "AttributeCreateManyWithoutAttributesInput" | "AttributeCreateWithoutProductsInput" | "AttributeWhereUniqueInput" | "BrandCreateOneWithoutBrandInput" | "BrandCreateWithoutProductsInput" | "BrandWhereUniqueInput" | "CollectionCreateManyWithoutCollectionsInput" | "CollectionCreateWithoutProductsInput" | "CollectionInput" | "CollectionRuleCreateManyWithoutRulesInput" | "CollectionRuleCreateWithoutCollectionRuleSetInput" | "CollectionRuleSetCreateOneWithoutRulesInput" | "CollectionRuleSetCreateWithoutCollectionInput" | "CollectionRuleSetInput" | "CollectionRuleSetWhereUniqueInput" | "CollectionRuleWhereUniqueInput" | "CollectionWhereUniqueInput" | "CreateProductInput" | "CreateVariantInput" | "ImageCreateManyWithoutImagesInput" | "ImageCreateOneWithoutThumbnailInput" | "ImageCreateWithoutProductInput" | "ImageCreateWithoutVariantInput" | "ImageWhereUniqueInput" | "IntFilter" | "NullableStringFilter" | "OptionCreateOneWithoutOptionInput" | "OptionCreateWithoutValuesInput" | "OptionValueCreateManyWithoutOptionValuesInput" | "OptionValueCreateWithoutVariantInput" | "OptionValueWhereUniqueInput" | "OptionWhereUniqueInput" | "ProductCreateInput" | "ProductCreateOneWithoutProductInput" | "ProductCreateWithoutVariantsInput" | "ProductTypeCreateOneWithoutTypeInput" | "ProductTypeCreateWithoutProductInput" | "ProductTypeWhereUniqueInput" | "ProductWhereUniqueInput" | "QueryFindManyVariantFilter" | "QueryFindManyVariantWhereInput" | "RulesInput" | "StringFilter" | "UniqueInput" | "UpdateProductInput" | "UpdateVariantInput" | "VariantCreateManyWithoutVariantsInput" | "VariantCreateOneWithoutVariantInput" | "VariantCreateWithoutImagesInput" | "VariantCreateWithoutProductInput" | "VariantWhereUniqueInput"; 619 | 620 | export type NexusGenEnumNames = "CollectionRuleField" | "CollectionRuleRelation"; 621 | 622 | export type NexusGenInterfaceNames = never; 623 | 624 | export type NexusGenScalarNames = "Boolean" | "Float" | "ID" | "Int" | "String"; 625 | 626 | export type NexusGenUnionNames = never; 627 | 628 | export interface NexusGenTypes { 629 | context: ctx.Context; 630 | inputTypes: NexusGenInputs; 631 | rootTypes: NexusGenRootTypes; 632 | argTypes: NexusGenArgTypes; 633 | fieldTypes: NexusGenFieldTypes; 634 | allTypes: NexusGenAllTypes; 635 | inheritedFields: NexusGenInheritedFields; 636 | objectNames: NexusGenObjectNames; 637 | inputNames: NexusGenInputNames; 638 | enumNames: NexusGenEnumNames; 639 | interfaceNames: NexusGenInterfaceNames; 640 | scalarNames: NexusGenScalarNames; 641 | unionNames: NexusGenUnionNames; 642 | allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; 643 | allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; 644 | allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] 645 | abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; 646 | abstractResolveReturn: NexusGenAbstractResolveReturnTypes; 647 | } -------------------------------------------------------------------------------- /packages/server/src/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was autogenerated by GraphQL Nexus 2 | ### Do not make changes to this file directly 3 | 4 | 5 | type Attribute { 6 | id: ID! 7 | key: String! 8 | products(after: String, before: String, first: Int, last: Int, skip: Int): [Product!] 9 | value: String! 10 | } 11 | 12 | input AttributeCreateManyWithoutAttributesInput { 13 | connect: [AttributeWhereUniqueInput!] 14 | create: [AttributeCreateWithoutProductsInput!] 15 | } 16 | 17 | input AttributeCreateWithoutProductsInput { 18 | id: ID 19 | key: String! 20 | value: String! 21 | } 22 | 23 | type AttributePayload { 24 | name: String! 25 | values: [AttributeValue!]! 26 | } 27 | 28 | type AttributeValue { 29 | id: ID! 30 | value: String! 31 | } 32 | 33 | input AttributeWhereUniqueInput { 34 | id: ID 35 | } 36 | 37 | type Brand { 38 | id: ID! 39 | name: String! 40 | products(after: String, before: String, first: Int, last: Int, skip: Int): [Product!] 41 | } 42 | 43 | input BrandCreateOneWithoutBrandInput { 44 | connect: BrandWhereUniqueInput 45 | create: BrandCreateWithoutProductsInput 46 | } 47 | 48 | input BrandCreateWithoutProductsInput { 49 | id: ID 50 | name: String! 51 | } 52 | 53 | input BrandWhereUniqueInput { 54 | id: ID 55 | } 56 | 57 | type Collection { 58 | attributes: [AttributePayload!]! 59 | brands: [Brand!]! 60 | id: ID! 61 | name: String! 62 | options: [Option!]! 63 | products(attributesIds: [ID!], brandsIds: [ID!], first: Int, last: Int, optionsValuesIds: [ID!]): [Product!]! 64 | } 65 | 66 | input CollectionCreateManyWithoutCollectionsInput { 67 | connect: [CollectionWhereUniqueInput!] 68 | create: [CollectionCreateWithoutProductsInput!] 69 | } 70 | 71 | input CollectionCreateWithoutProductsInput { 72 | id: ID 73 | name: String! 74 | rules: CollectionRuleSetCreateOneWithoutRulesInput 75 | } 76 | 77 | input CollectionInput { 78 | name: String! 79 | productsIds: [String!] 80 | ruleSet: CollectionRuleSetInput 81 | } 82 | 83 | input CollectionRuleCreateManyWithoutRulesInput { 84 | connect: [CollectionRuleWhereUniqueInput!] 85 | create: [CollectionRuleCreateWithoutCollectionRuleSetInput!] 86 | } 87 | 88 | input CollectionRuleCreateWithoutCollectionRuleSetInput { 89 | field: CollectionRuleField! 90 | id: ID 91 | relation: CollectionRuleRelation! 92 | value: String! 93 | } 94 | 95 | enum CollectionRuleField { 96 | PRICE 97 | TITLE 98 | TYPE 99 | } 100 | 101 | enum CollectionRuleRelation { 102 | CONTAINS 103 | ENDS_WITH 104 | EQUALS 105 | GREATER_THAN 106 | LESS_THAN 107 | NOT_CONTAINS 108 | NOT_EQUALS 109 | STARTS_WITH 110 | } 111 | 112 | input CollectionRuleSetCreateOneWithoutRulesInput { 113 | connect: CollectionRuleSetWhereUniqueInput 114 | create: CollectionRuleSetCreateWithoutCollectionInput 115 | } 116 | 117 | input CollectionRuleSetCreateWithoutCollectionInput { 118 | appliesDisjunctively: Boolean! 119 | id: ID 120 | rules: CollectionRuleCreateManyWithoutRulesInput 121 | } 122 | 123 | input CollectionRuleSetInput { 124 | applyDisjunctively: Boolean! 125 | rules: [RulesInput!]! 126 | } 127 | 128 | input CollectionRuleSetWhereUniqueInput { 129 | id: ID 130 | } 131 | 132 | input CollectionRuleWhereUniqueInput { 133 | id: ID 134 | } 135 | 136 | input CollectionWhereUniqueInput { 137 | id: ID 138 | } 139 | 140 | input CreateProductInput { 141 | attributesIds: [UniqueInput!]! 142 | brand: UniqueInput! 143 | name: String! 144 | slug: String! 145 | variants: [CreateVariantInput!]! 146 | } 147 | 148 | input CreateVariantInput { 149 | availableForSale: Boolean! 150 | optionsValueIds: [UniqueInput!]! 151 | price: Int! 152 | } 153 | 154 | type Image { 155 | id: ID! 156 | url: String! 157 | } 158 | 159 | input ImageCreateManyWithoutImagesInput { 160 | connect: [ImageWhereUniqueInput!] 161 | create: [ImageCreateWithoutVariantInput!] 162 | } 163 | 164 | input ImageCreateOneWithoutThumbnailInput { 165 | connect: ImageWhereUniqueInput 166 | create: ImageCreateWithoutProductInput 167 | } 168 | 169 | input ImageCreateWithoutProductInput { 170 | id: ID 171 | url: String! 172 | variant: VariantCreateOneWithoutVariantInput 173 | } 174 | 175 | input ImageCreateWithoutVariantInput { 176 | id: ID 177 | product: ProductCreateOneWithoutProductInput 178 | url: String! 179 | } 180 | 181 | input ImageWhereUniqueInput { 182 | id: ID 183 | } 184 | 185 | input IntFilter { 186 | equals: Int 187 | gt: Int 188 | gte: Int 189 | in: [Int!] 190 | lt: Int 191 | lte: Int 192 | not: Int 193 | notIn: [Int!] 194 | } 195 | 196 | type Mutation { 197 | collectionAddProducts(collectionId: ID!, productIds: [ID!]!): Collection! 198 | collectionCreate(collection: CollectionInput!): Collection! 199 | collectionRemoveProducts(collectionId: ID!, productIds: [ID!]!): Collection! 200 | collectionUpdate(collection: CollectionInput!, id: ID!): Collection! 201 | createOneProduct(data: ProductCreateInput!): Product! 202 | productCreate(data: CreateProductInput!): Product! 203 | productDelete(productId: ID!): Product! 204 | productUpdate(data: UpdateProductInput!): Product! 205 | } 206 | 207 | input NullableStringFilter { 208 | contains: String 209 | endsWith: String 210 | equals: String 211 | gt: String 212 | gte: String 213 | in: [String!] 214 | lt: String 215 | lte: String 216 | not: String 217 | notIn: [String!] 218 | startsWith: String 219 | } 220 | 221 | type Option { 222 | id: ID! 223 | isColor: Boolean 224 | name: String! 225 | values(after: String, before: String, first: Int, last: Int, skip: Int): [OptionValue!] 226 | } 227 | 228 | input OptionCreateOneWithoutOptionInput { 229 | connect: OptionWhereUniqueInput 230 | create: OptionCreateWithoutValuesInput 231 | } 232 | 233 | input OptionCreateWithoutValuesInput { 234 | id: ID 235 | isColor: Boolean 236 | name: String! 237 | } 238 | 239 | type OptionValue { 240 | id: ID! 241 | name: String! 242 | option: Option! 243 | variant: Variant 244 | } 245 | 246 | input OptionValueCreateManyWithoutOptionValuesInput { 247 | connect: [OptionValueWhereUniqueInput!] 248 | create: [OptionValueCreateWithoutVariantInput!] 249 | } 250 | 251 | input OptionValueCreateWithoutVariantInput { 252 | id: ID 253 | name: String! 254 | option: OptionCreateOneWithoutOptionInput! 255 | } 256 | 257 | input OptionValueWhereUniqueInput { 258 | id: ID 259 | } 260 | 261 | input OptionWhereUniqueInput { 262 | id: ID 263 | } 264 | 265 | type Product { 266 | attributes(after: String, before: String, first: Int, last: Int, skip: Int): [Attribute!] 267 | brand: Brand! 268 | id: ID! 269 | name: String! 270 | slug: String! 271 | thumbnail: Image 272 | variants(after: String, before: String, first: Int, last: Int, skip: Int): [Variant!] 273 | } 274 | 275 | input ProductCreateInput { 276 | attributes: AttributeCreateManyWithoutAttributesInput 277 | brand: BrandCreateOneWithoutBrandInput! 278 | collections: CollectionCreateManyWithoutCollectionsInput 279 | description: String! 280 | id: ID 281 | name: String! 282 | slug: String! 283 | thumbnail: ImageCreateOneWithoutThumbnailInput 284 | type: ProductTypeCreateOneWithoutTypeInput 285 | variants: VariantCreateManyWithoutVariantsInput 286 | } 287 | 288 | input ProductCreateOneWithoutProductInput { 289 | connect: ProductWhereUniqueInput 290 | create: ProductCreateWithoutVariantsInput 291 | } 292 | 293 | input ProductCreateWithoutVariantsInput { 294 | attributes: AttributeCreateManyWithoutAttributesInput 295 | brand: BrandCreateOneWithoutBrandInput! 296 | collections: CollectionCreateManyWithoutCollectionsInput 297 | description: String! 298 | id: ID 299 | name: String! 300 | slug: String! 301 | thumbnail: ImageCreateOneWithoutThumbnailInput 302 | type: ProductTypeCreateOneWithoutTypeInput 303 | } 304 | 305 | input ProductTypeCreateOneWithoutTypeInput { 306 | connect: ProductTypeWhereUniqueInput 307 | create: ProductTypeCreateWithoutProductInput 308 | } 309 | 310 | input ProductTypeCreateWithoutProductInput { 311 | id: ID 312 | name: String! 313 | } 314 | 315 | input ProductTypeWhereUniqueInput { 316 | id: ID 317 | } 318 | 319 | input ProductWhereUniqueInput { 320 | id: ID 321 | slug: String 322 | } 323 | 324 | type Query { 325 | brands(after: String, before: String, first: Int, last: Int, skip: Int): [Brand!] 326 | collection(collectionId: ID!): Collection! 327 | collections(after: String, before: String, first: Int, last: Int, skip: Int): [Collection!] 328 | findManyVariant(after: String, before: String, first: Int, last: Int, skip: Int, where: QueryFindManyVariantWhereInput): [Variant!] 329 | findOneVariant(where: VariantWhereUniqueInput!): Variant 330 | options(after: String, before: String, first: Int, last: Int, skip: Int): [Option!] 331 | product(where: ProductWhereUniqueInput!): Product 332 | products(after: String, before: String, first: Int, last: Int, skip: Int): [Product!] 333 | } 334 | 335 | input QueryFindManyVariantFilter { 336 | every: QueryFindManyVariantWhereInput 337 | none: QueryFindManyVariantWhereInput 338 | some: QueryFindManyVariantWhereInput 339 | } 340 | 341 | input QueryFindManyVariantWhereInput { 342 | AND: [QueryFindManyVariantWhereInput!] 343 | availableForSale: QueryFindManyVariantFilter 344 | id: StringFilter 345 | images: QueryFindManyVariantFilter 346 | NOT: [QueryFindManyVariantWhereInput!] 347 | optionValues: QueryFindManyVariantFilter 348 | OR: [QueryFindManyVariantWhereInput!] 349 | price: IntFilter 350 | product: QueryFindManyVariantWhereInput 351 | sku: NullableStringFilter 352 | } 353 | 354 | input RulesInput { 355 | field: CollectionRuleField! 356 | relation: CollectionRuleRelation! 357 | value: String! 358 | } 359 | 360 | input StringFilter { 361 | contains: String 362 | endsWith: String 363 | equals: String 364 | gt: String 365 | gte: String 366 | in: [String!] 367 | lt: String 368 | lte: String 369 | not: String 370 | notIn: [String!] 371 | startsWith: String 372 | } 373 | 374 | input UniqueInput { 375 | id: ID! 376 | } 377 | 378 | input UpdateProductInput { 379 | attributesIds: [UniqueInput!]! 380 | brand: UniqueInput! 381 | id: ID! 382 | name: String! 383 | variants: [UpdateVariantInput!]! 384 | } 385 | 386 | input UpdateVariantInput { 387 | availableForSale: Boolean! 388 | id: ID! 389 | optionsValueIds: [UniqueInput!]! 390 | price: Int! 391 | } 392 | 393 | type Variant { 394 | availableForSale: Boolean 395 | id: ID! 396 | images(after: String, before: String, first: Int, last: Int, skip: Int): [Image!] 397 | optionValues(after: String, before: String, first: Int, last: Int, skip: Int): [OptionValue!] 398 | price: Int! 399 | product: Product 400 | sku: String 401 | } 402 | 403 | input VariantCreateManyWithoutVariantsInput { 404 | connect: [VariantWhereUniqueInput!] 405 | create: [VariantCreateWithoutProductInput!] 406 | } 407 | 408 | input VariantCreateOneWithoutVariantInput { 409 | connect: VariantWhereUniqueInput 410 | create: VariantCreateWithoutImagesInput 411 | } 412 | 413 | input VariantCreateWithoutImagesInput { 414 | availableForSale: Boolean 415 | id: ID 416 | optionValues: OptionValueCreateManyWithoutOptionValuesInput 417 | price: Int! 418 | product: ProductCreateOneWithoutProductInput 419 | sku: String 420 | } 421 | 422 | input VariantCreateWithoutProductInput { 423 | availableForSale: Boolean 424 | id: ID 425 | images: ImageCreateManyWithoutImagesInput 426 | optionValues: OptionValueCreateManyWithoutOptionValuesInput 427 | price: Int! 428 | sku: String 429 | } 430 | 431 | input VariantWhereUniqueInput { 432 | id: ID 433 | } 434 | -------------------------------------------------------------------------------- /packages/server/src/utils/attributes.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { Attribute } from '@generated/photon' 3 | 4 | export function transformAttributes(attributes: Attribute[]) { 5 | return _(attributes) 6 | .groupBy(a => a.key) 7 | .toPairs() 8 | .map(([name, values]) => ({ 9 | name, 10 | values: values.map(v => ({ id: v.id, value: v.value })), 11 | })) 12 | .value() 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/utils/collection.ts: -------------------------------------------------------------------------------- 1 | import Photon, { CollectionRule, CollectionRuleField } from '@generated/photon' 2 | import { get } from 'lodash' 3 | import { NexusGenInputs } from '../nexus-typegen' 4 | import { core } from '@prisma/nexus' 5 | 6 | interface Product { 7 | id: string 8 | name: string 9 | type: { id: string; name: string } 10 | } 11 | 12 | type CollectionInput = NexusGenInputs['CollectionInput'] 13 | 14 | function fieldPath(field: CollectionRuleField): string { 15 | switch (field) { 16 | case 'TITLE': 17 | return 'name' 18 | case 'TYPE': 19 | return 'type.name' 20 | default: 21 | throw new Error('Field not implemented yet') 22 | } 23 | } 24 | 25 | export function productMatchRule( 26 | product: Product, 27 | rule: core.Omit, 28 | ): boolean { 29 | const { field, relation, value } = rule 30 | const productField: string = get(product, fieldPath(field)).toString() 31 | 32 | switch (relation) { 33 | case 'CONTAINS': 34 | return productField.includes(value) 35 | case 'NOT_CONTAINS': 36 | return !productField.includes(value) 37 | case 'STARTS_WITH': 38 | return productField.startsWith(value) 39 | case 'ENDS_WITH': 40 | return productField.endsWith(value) 41 | case 'GREATER_THAN': 42 | return productField > value 43 | case 'LESS_THAN': 44 | return productField < value 45 | case 'EQUALS': 46 | return productField === value 47 | case 'NOT_EQUALS': 48 | return productField !== value 49 | } 50 | } 51 | 52 | export async function fetchAllCollections( 53 | photon: Photon, 54 | ): Promise< 55 | { 56 | id: string 57 | name: string 58 | rules: { rules: CollectionRule[] } | null 59 | }[] 60 | > { 61 | return photon.collections.findMany({ 62 | select: { 63 | id: true, 64 | name: true, 65 | rules: { 66 | select: { 67 | rules: { 68 | select: { 69 | id: true, 70 | field: true, 71 | relation: true, 72 | value: true, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }) 79 | } 80 | 81 | async function fetchAllProducts(photon: Photon): Promise { 82 | return photon.products.findMany({ 83 | select: { 84 | id: true, 85 | name: true, 86 | type: { 87 | select: { 88 | id: true, 89 | name: true, 90 | }, 91 | }, 92 | }, 93 | }) 94 | } 95 | 96 | export function productMatchRules( 97 | product: Product, 98 | rules: core.Omit[], 99 | ) { 100 | if (!rules) { 101 | return false 102 | } 103 | // FIXME: act as OR (but should only be the case when `applyDisjunctively` is true) 104 | return rules.some(rule => productMatchRule(product, rule)) 105 | } 106 | 107 | async function productsMatchRules( 108 | rules: core.Omit[], 109 | photon: Photon, 110 | ) { 111 | const products = await fetchAllProducts(photon) 112 | 113 | return products.filter(p => productMatchRules(p, rules)) 114 | } 115 | 116 | export async function recomputeCollection( 117 | collection: CollectionInput, 118 | prisma: Photon, 119 | ) { 120 | if (!collection.ruleSet) { 121 | throw new Error('Rule set is empty') 122 | } 123 | 124 | const products = await productsMatchRules(collection.ruleSet.rules, prisma) 125 | 126 | return prisma.collections.create({ 127 | data: { 128 | name: collection.name, 129 | rules: { 130 | create: { 131 | appliesDisjunctively: collection.ruleSet.applyDisjunctively, 132 | rules: { 133 | create: collection.ruleSet.rules, 134 | }, 135 | }, 136 | }, 137 | products: { 138 | connect: products.map(p => ({ id: p.id })), 139 | }, 140 | }, 141 | }) 142 | } 143 | 144 | export function createManualCollection( 145 | collection: CollectionInput, 146 | photon: Photon, 147 | ) { 148 | return photon.collections.create({ 149 | data: { 150 | name: collection.name, 151 | products: { 152 | connect: collection.productsIds!.map(id => ({ id })), 153 | }, 154 | }, 155 | }) 156 | } 157 | 158 | export function throwIfManualAndAutomatic(collection: CollectionInput): void { 159 | if ( 160 | collection.productsIds && 161 | collection.productsIds.length > 0 && 162 | collection.ruleSet && 163 | collection.ruleSet.rules.length > 0 164 | ) { 165 | throw new Error('Collection can either be manual or automatic') 166 | } 167 | } 168 | 169 | export function throwIfMissingCollectionInput(collection: CollectionInput) { 170 | if ( 171 | (!collection.ruleSet || collection.ruleSet.rules.length === 0) && 172 | (!collection.productsIds || collection.productsIds.length === 0) 173 | ) { 174 | throw new Error( 175 | 'Missing params: you need to fill either ruleSet or productIds param', 176 | ) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /packages/server/src/utils/ids.ts: -------------------------------------------------------------------------------- 1 | import { differenceBy, intersectionBy } from 'lodash' 2 | 3 | export interface BaseField { 4 | id: string 5 | [otherField: string]: any 6 | } 7 | 8 | export function fieldsToAdd( 9 | oldIds: BaseField[], 10 | newIds: BaseField[], 11 | ) { 12 | return differenceBy(newIds, oldIds, 'id') as T[] 13 | } 14 | 15 | export function fieldsToRemove( 16 | oldIds: BaseField[], 17 | newIds: BaseField[], 18 | ): T[] { 19 | return differenceBy(oldIds, newIds, 'id') as T[] 20 | } 21 | 22 | export function fieldsToUpdate( 23 | oldValues: T[], 24 | newValues: BaseField[], 25 | ): T[] { 26 | return intersectionBy(newValues, oldValues, v => v.id) as T[] 27 | } 28 | 29 | export function fieldsToAddRemove( 30 | oldIds: T[], 31 | newIds: U[], 32 | ): [U[], T[]] { 33 | return [fieldsToAdd(oldIds, newIds), fieldsToRemove(oldIds, newIds)] 34 | } 35 | -------------------------------------------------------------------------------- /packages/server/src/utils/variants.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { Option } from '@generated/photon' 3 | 4 | interface Variant { 5 | optionValues: { option: Option }[] 6 | } 7 | 8 | export function optionsFromVariants(variants: Variant[]) { 9 | return _(variants) 10 | .flatMap(v => v.optionValues) 11 | .map(v => v.option) 12 | .uniqBy(v => v.id) 13 | .value() 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "esnext", "esnext.asynciterable"], 4 | "sourceMap": true, 5 | "rootDir": "./", 6 | "outDir": "dist", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true 11 | }, 12 | "include": ["./src/**/*.ts", "prisma/seed.ts"], 13 | "exclude": ["node_modules", "generated", "example", "examples", "scripts"] 14 | } 15 | --------------------------------------------------------------------------------