├── .editorconfig ├── .env ├── .env.dev ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc.js ├── .vscode └── extensions.json ├── LICENSE.md ├── Procfile ├── README.md ├── babel.config.js ├── config └── babelJestTransformer.js ├── jest.config.js ├── nodemon.json ├── package.json ├── public ├── mutation-done.PNG ├── mutation.PNG ├── query-done.PNG └── query.PNG └── src ├── api ├── resolver.js └── v1 │ ├── data.js │ └── schema.js ├── datastore ├── api │ ├── api.js │ └── redisApi.js ├── defns │ └── index.js ├── index.js └── store │ ├── redisStore.js │ └── store.js ├── env.js ├── server.js └── structures ├── definitions └── cookies.js ├── graphql └── cookies.js ├── index.js └── initialStates └── cookies.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_size = 4 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | POOKY_DATASOURCE=REDIS -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | POOKY_DATASOURCE=REDIS -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | }, 5 | extends: ['airbnb-base', 'prettier'], 6 | parserOptions: { 7 | ecmaFeatures: { jsx: false }, // to be overridden as necessary 8 | ecmaVersion: 2018, 9 | }, 10 | plugins: ['prettier', 'react', 'import', 'jsx-a11y'], 11 | rules: { 12 | 'prettier/prettier': 'error', 13 | 'no-underscore-dangle': 'off', 14 | 'camelcase': 'warn', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please make sure you check the list of open issues before posting an issue 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (if necessary) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Environment 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please search amongst the open [Pull Requests](../) before creating one. 2 | 3 | Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: 4 | For more information, see the `CONTRIBUTING` guidelines. 5 | 6 | **Summary** 7 | 8 | 9 | 10 | This PR fixes/implements the following **bugs/features** 11 | 12 | * [ ] Bug 1 13 | * [ ] Bug 2 14 | * [ ] Feature 1 15 | * [ ] Feature 2 16 | * [ ] Breaking changes 17 | 18 | 19 | 20 | Explain the **motivation** for making this change. What existing problem does the pull request solve? 21 | 22 | 23 | 24 | **Test plan (required)** 25 | 26 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. 27 | 28 | **Code formatting** 29 | 30 | 31 | 32 | **Closing issues** 33 | 34 | 35 | Fixes # -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | dist 12 | 13 | # ide 14 | .vscode/* 15 | !.vscode/extensions.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | *.lock 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "avoid", 3 | bracketSpacing: true, 4 | endOfLine: "lf", 5 | jsxBracketSameLine: false, 6 | printWidth: 80, 7 | singleQuote: true, 8 | tabWidth: 2, 9 | trailingComma: "all", 10 | useTabs: false, 11 | overrides: [ 12 | { 13 | files: ["*.js", "*.jsx", "*.es", "*.es6", "*.mjs"], 14 | options: { 15 | printWidth: 100, 16 | parser: "babel" 17 | } 18 | }, 19 | { 20 | files: ["*.json"], 21 | excludeFiles: ["package.json"], 22 | options: { 23 | parser: "json" 24 | } 25 | }, 26 | 27 | { 28 | files: ["*.css", "*.css3", "*.css4"], 29 | options: { 30 | parser: "css" 31 | } 32 | }, 33 | { 34 | files: ["*.scss"], 35 | options: { 36 | parser: "scss" 37 | } 38 | }, 39 | { 40 | files: ["*.htm", "*.html"], 41 | options: { 42 | parser: "html" 43 | } 44 | }, 45 | { 46 | files: ["*.md", "*.mdown"], 47 | options: { 48 | parser: "markdown" 49 | } 50 | } 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "2gua.rainbow-brackets", 4 | "christian-kohler.path-intellisense", 5 | "christian-kohler.npm-intellisense", 6 | "dbaeumer.vscode-eslint", 7 | "editorconfig.editorconfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Matthew Wall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a base repository for a GraphQL API. 4 | 5 | The target audience for this repository is anyone looking to reverse pooky. 6 | 7 | Looking for help? Add me on Discord: `orion#0001` 8 | 9 | ## Demonstration 10 | 11 | ##### GraphiQL Demonstration 12 | https://vimeo.com/373469096 13 | 14 | ##### Client-Side Fetch Example 15 | https://vimeo.com/373468734 16 | 17 | ## Getting Started 18 | 19 | GraphQL consists of a type system, query language and execution semantics, 20 | static validation, and type introspection, each outlined below. To guide you 21 | through each of these components, I've written an example designed to 22 | illustrate the various pieces of GraphQL. 23 | 24 | This example is not exhausxtive, but it is designed to quickly introduce 25 | the core concepts of GraphQL, to provide some context before diving into 26 | the more detailed specification or the [GraphQL.js](https://github.com/graphql/graphql-js) 27 | reference implementation. 28 | 29 | The premise of the example is that we want to use GraphQL to query for 30 | potential data (cookies, checkout form fields, etc). 31 | 32 | ### Structures 33 | 34 | At the heart of any GraphQL implementation is a description of what types 35 | of objects it can return, described in a GraphQL type system and returned 36 | in the GraphQL Schema. 37 | 38 | For this repository, I've created a package that handles all of the type 39 | definitions for you in a shared space. Browse around the [structures](https://github.com/walmat/pooky-api/tree/master/packages/structures) packages to see the definitions I've setup 40 | for the existing graphQL queries/mutations. 41 | 42 | **Important: this has been reverted to adapt to Supreme's new tactic of changing cookie names. All type checking has been loosened and both queries and mutations now accept a dynamic JSON (stringified) data structure** 43 | 44 | For example, since all of the cookies are required strings (with the exception 45 | of `pooky_use_cookie`), our type definitions for the cookies object is as follows: 46 | 47 | ```graphql 48 | type Cookies { 49 | pooky: String!, 50 | pooky_owl: String!, 51 | pooky_data: String!, 52 | pooky_electric: String!, 53 | pooky_order_allow: String!, 54 | pooky_performance: String!, 55 | pooky_recaptcha: String!, 56 | pooky_recaptcha_coherence: String!, 57 | pooky_settings: String!, 58 | pooky_telemetry: String!, 59 | pooky_use_cookie: Boolean!, 60 | updated_pooky_coherence: String!, 61 | } 62 | ``` 63 | 64 | This shorthand is convenient for describing the basic shape of a type 65 | system; the JavaScript implementation is more full-featured, and allows types 66 | and fields to be documented. It also sets up the mapping between the 67 | type system and the underlying data; for a test case in GraphQL.js, the 68 | underlying data is a [set of JavaScript objects](https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js), 69 | but in most cases the backing data will be accessed through some service, and 70 | this type system layer will be responsible for mapping from types and fields to 71 | that service. 72 | 73 | A common pattern in many APIs, and indeed in GraphQL is to give 74 | objects an ID that can be used to refetch the object. So let's add 75 | that to our cookies type as well: 76 | 77 | ```graphql 78 | type Cookies { 79 | id: String!, 80 | pooky: String!, 81 | pooky_owl: String!, 82 | pooky_data: String!, 83 | pooky_electric: String!, 84 | pooky_order_allow: String!, 85 | pooky_performance: String!, 86 | pooky_recaptcha: String!, 87 | pooky_recaptcha_coherence: String!, 88 | pooky_settings: String!, 89 | pooky_telemetry: String!, 90 | pooky_use_cookie: Boolean!, 91 | updated_pooky_coherence: String!, 92 | } 93 | ``` 94 | 95 | Any new data structures should first be defined in this namespace to then 96 | later be used by both: a) the redis/memory datastore, and b) the graphQL 97 | query/mutation APIs. 98 | 99 | One question you might ask, though, is whether any of those fields can return 100 | `null`. By default, `null` is a permitted value for any type in GraphQL, 101 | since fetching data to fulfill a GraphQL query often requires talking 102 | to different services that may or may not be available. However, if the 103 | type system can guarantee that a type is never null, then we can mark 104 | it as Non Null in the type system. We indicate that in our shorthand 105 | by adding an "!" after the type. We can update our type system to note 106 | that the `id`, and all of the cookies is never null. 107 | 108 | Note that while in our current implementation, we can guarantee that more 109 | fields are non-null (since our current implementation has hard-coded data), 110 | we didn't mark them as non-null. One can imagine we would eventually 111 | replace our hardcoded data with a backend service, which might not be 112 | perfectly reliable; by leaving these fields as nullable, we allow 113 | ourselves the flexibility to eventually return null to indicate a backend 114 | error, while also telling the client that the error occurred. 115 | 116 | ## Datastore 117 | 118 | A pretty crucial piece to any pooky api is a data storage object to store the cookies 119 | to be fetched. I decided to go with redis, and I've been utilizing it a lot recently. 120 | 121 | (Note: One could easily swap out the datastore to be any form of database if they wanted to. 122 | I left this pretty modular and basic, so it's an easy swap to Mongo/PostgreSQL/etc). 123 | 124 | By default, I've defined 2 separate storage structures: `MemoryStore`, and `RedisStore`. 125 | 126 | The `MemoryStore` is useful in development mode to quickly get up and running and doesn't require 127 | any external processes to be running. 128 | 129 | See more about the datastore [here](https://github.com/walmat/pooky-api/tree/master/packages/datastore) 130 | 131 | ## Server (API) 132 | 133 | The server package is where it all comes together. I've defined a super basic data API that can be used 134 | to query and mutate data inside the `Store`. Let's look at some examples. 135 | 136 | First, let's get the API up and running.. 137 | 138 | ```md 139 | #### install dependencies 140 | yarn 141 | 142 | #### build packages/* 143 | yarn build 144 | 145 | #### launch the api (in development mode) 146 | yarn dev 147 | ``` 148 | 149 | A neat little playground called GraphiQL is enabled (only in development mode), for this API. You can access it [here](http://localhost:5000/api/v1/data). 150 | 151 | #### Queries / Mutations 152 | 153 | If you're playing around in the GraphiQL playground, we can demonstrate how this api will work and look. 154 | 155 | Go ahead and paste the following mutation in the left-hand panel of the dashboard 156 | 157 | ```graphql 158 | mutation { 159 | addCookies(data: { 160 | pooky: "55bd0c5d-2e36-4f4f-aeb9-eaa8fe00aecf", 161 | pooky_owl: "6800974eb7b26d7408c0c617a53388d744349b640cdc34ba61f2f4829fbf7ced", 162 | pooky_data: "b4ad1771e7278cd938c0789dccf528c6935c70b50d1300ac03abbb50e857bfd0", 163 | pooky_electric: "ca5d35456efc96de36af9274269bbcbcd2c158c846c4051baaa2de5efa574e2e", 164 | pooky_order_allow: "eyJ0b2hydV9vayI6IHRydWUsImVuYWJszwiQOiB0cnVlLCJhbGxfcmVsZWFzZXMiOnRydWUsInNwbGF5X2VudiI6InByb2QiLCAibW91c2Vfc2NvcmUiOjEwMCwiYnlwYXNzIjp0cnVlfQ", 165 | pooky_performance: "90ff67084a669a673d32424c716f6f6b98a59dd83d5116e8187df230550341b892fff72ab92932237c175603d6f4fdc4", 166 | pooky_recaptcha: "e97aea62ce991cbb72af50a9d79110d6fe4b7eac7e44abbb87bebcb5095b88c1edd028289cebf2c1fb1e69d684b8595f", 167 | pooky_recaptcha_coherence: "0a48537c18a172c964265b8e0487a8e1d1f09151920859249c2f1d2a2c05fa1b57fd5c2a5766ad2545c43847ce4cc3d5", 168 | pooky_settings: "6bca6fffcbd6aa398905fb9dc2a3ca1f8c803de2eee4f45fb41a4ffc70061fa1f5d3575bf720aabc2be8efb74d850bec", 169 | pooky_telemetry: "67db023465119f3486c7aae6378e1b6eed727986fa248edbb36322217cc997baa78579a0239e472d3e1f343e2f79b318", 170 | pooky_use_cookie: true, 171 | updated_pooky_coherence: "bafd60d036cf80654d46b78341625b11ba67fe510fbbba40d71c00ed6a8f59cb", 172 | }) { 173 | pooky, 174 | pooky_owl, 175 | pooky_data, 176 | pooky_electric, 177 | pooky_order_allow, 178 | pooky_performance, 179 | pooky_recaptcha, 180 | pooky_recaptcha_coherence, 181 | pooky_settings, 182 | pooky_telemetry, 183 | pooky_use_cookie, 184 | updated_pooky_coherence, 185 | } 186 | } 187 | ``` 188 | 189 | Please note: these are dummy values just used for demonstration, do not assume these are correct. 190 | 191 | Your screen should look like the following: 192 | 193 | ![Mutation](https://github.com/walmat/pooky-api/blob/master/public/mutation.PNG) 194 | 195 | Hit the big `play` icon to perform the mutation. 196 | 197 | ![Mutation](https://github.com/walmat/pooky-api/blob/master/public/mutation-done.PNG) 198 | 199 | Congrats, you just performed a simple graphQL mutation! 200 | 201 | Now, this is all stored in memory as of now, so when we shutdown the API, we will lose all data stored in the Store. 202 | 203 | However, we can view the currently stored cookies by performing a `Query`. 204 | 205 | ```graphql 206 | { 207 | cookies { 208 | pooky 209 | pooky_owl 210 | pooky_data 211 | pooky_electric 212 | pooky_order_allow 213 | pooky_performance 214 | pooky_recaptcha 215 | pooky_recaptcha_coherence 216 | pooky_settings 217 | pooky_telemetry 218 | pooky_use_cookie 219 | updated_pooky_coherence 220 | } 221 | } 222 | ``` 223 | 224 | This should look like: 225 | 226 | ![query](https://github.com/walmat/pooky-api/blob/master/public/query.PNG) 227 | 228 | Now, hit the `play` icon to perform the query on the datastore that holds cookies. 229 | 230 | ![query-done](https://github.com/walmat/pooky-api/blob/master/public/query-done.PNG) 231 | 232 | Simple, right? 233 | 234 | ### Additional Content / Resources 235 | 236 | 1. https://github.com/schickling/timemachine - Allows you to modify the date object that a browser environment uses 237 | 2. https://github.com/paulirish/break-on-access - Allows you to set breakpoints when certain actions happen 238 | 3. https://github.com/krpar/pooky-browser (huge shoutout to [@krpar](https://github.com/krpar) for this) - Allows you to activate and step through pooky in a Puppeteer environment. 239 | 240 | This README scratches the surface on what GraphQL is capable of, to find out 241 | more about what it's used for, please refer to it's [documentation](https://graphql.org). 242 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/env', '@babel/react'], 3 | plugins: [ 4 | '@babel/transform-runtime', 5 | '@babel/proposal-class-properties', 6 | '@babel/proposal-export-default-from', 7 | '@babel/proposal-export-namespace-from', 8 | '@babel/proposal-function-bind', 9 | '@babel/proposal-json-strings', 10 | '@babel/syntax-dynamic-import', 11 | '@babel/syntax-import-meta', 12 | ], 13 | babelrcRoots: ['packages/*'], 14 | env: { 15 | production: { 16 | ignore: ['**/__tests__', '**/*.test.js'], 17 | }, 18 | development: { 19 | ignore: ['**/__tests__', '**/*.test.js'], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /config/babelJestTransformer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const babelJest = require('babel-jest'); 3 | 4 | module.exports = babelJest.createTransformer({ 5 | rootMode: 'upward', 6 | }); 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/packages/datastore/jest.config.js', 4 | '/packages/structures/jest.config.js', 5 | ], 6 | transform: { 7 | '^.+\\.jsx?$': require.resolve('babel-jest'), 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["*.test.js", "dist/"], 4 | "watch": ["src/"], 5 | "exec": "yarn run build && yarn run dev:node" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pooky-api", 3 | "description": "A GraphQL + Express Boilerplate for a dynamic Pooky API", 4 | "version": "1.0.0", 5 | "private": false, 6 | "scripts": { 7 | "build": "babel --root-mode upward --no-comments -d dist src/", 8 | "start": "yarn dev", 9 | "dev": "cross-env NODE_ENV=development yarn run dev:nodemon", 10 | "dev:nodemon": "nodemon --config nodemon.json", 11 | "dev:node": "node dist/server.js", 12 | "postinstall": "yarn run build", 13 | "lint": "eslint --ext js src/", 14 | "lint:fix": "eslint --fix --ext js src/", 15 | "clean": "concurrently \"yarn clean:node_modules\" \"yarn clean:dist\"", 16 | "clean:node_modules": "cross-env rm -rf node_modules && rm -rf packages/**/node_modules", 17 | "clean:dist": "cross-env rm -rf packages/**/dist", 18 | "test": "jest", 19 | "coverage": "jest --coverage" 20 | }, 21 | "dependencies": { 22 | "@babel/cli": "^7.2.0", 23 | "@babel/core": "^7.2.0", 24 | "@babel/node": "^7.2.0", 25 | "@babel/plugin-proposal-class-properties": "^7.2.0", 26 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 27 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 28 | "@babel/plugin-proposal-function-bind": "^7.2.0", 29 | "@babel/plugin-proposal-json-strings": "^7.2.0", 30 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 31 | "@babel/plugin-syntax-import-meta": "^7.2.0", 32 | "@babel/plugin-transform-runtime": "^7.4.3", 33 | "@babel/preset-env": "^7.2.0", 34 | "@babel/preset-react": "^7.0.0", 35 | "@babel/runtime": "^7.4.3", 36 | "babel-core": "^7.0.0-bridge.0", 37 | "babel-jest": "^23.6.0", 38 | "concurrently": "5.0.0", 39 | "cors": "^2.8.5", 40 | "cross-env": "6.0.3", 41 | "dotenv": "^7.0.0", 42 | "eslint": "5.12.0", 43 | "eslint-config-airbnb": "^17.1.0", 44 | "eslint-config-prettier": "^4.1.0", 45 | "eslint-plugin-import": "^2.16.0", 46 | "eslint-plugin-jsx-a11y": "^6.2.1", 47 | "eslint-plugin-prettier": "^3.0.0", 48 | "eslint-plugin-react": "^7.11.1", 49 | "express": "^4.16.4", 50 | "express-graphql": "^0.8.0", 51 | "graphql": "^14.2.1", 52 | "graphql-type-json": "^0.3.0", 53 | "ioredis": "^4.14.1", 54 | "jest": "^23.6.0", 55 | "nodemon": "^1.18.10", 56 | "prettier": "^1.16.4", 57 | "yarn": "^1.12.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/mutation-done.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmat/pooky-api/a46fcad95c354a9fe368d149e3297821cd011974/public/mutation-done.PNG -------------------------------------------------------------------------------- /public/mutation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmat/pooky-api/a46fcad95c354a9fe368d149e3297821cd011974/public/mutation.PNG -------------------------------------------------------------------------------- /public/query-done.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmat/pooky-api/a46fcad95c354a9fe368d149e3297821cd011974/public/query-done.PNG -------------------------------------------------------------------------------- /public/query.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walmat/pooky-api/a46fcad95c354a9fe368d149e3297821cd011974/public/query.PNG -------------------------------------------------------------------------------- /src/api/resolver.js: -------------------------------------------------------------------------------- 1 | import RedisStore from '../datastore'; 2 | 3 | class Resolver { 4 | constructor() { 5 | this._settingsId = null; 6 | this.store = new RedisStore(process.env.REDIS_URL); 7 | } 8 | 9 | async getCookies() { 10 | return this.store.cookies.pop(); 11 | } 12 | 13 | async addCookies(payload) { 14 | return this.store.cookies.push(payload); 15 | } 16 | 17 | async flushCookies() { 18 | return this.store.cookies.flush(); 19 | } 20 | } 21 | 22 | export default Resolver; 23 | -------------------------------------------------------------------------------- /src/api/v1/data.js: -------------------------------------------------------------------------------- 1 | import graphqlHTTP from 'express-graphql'; 2 | 3 | import DataSchema from './schema'; 4 | 5 | const attachGraphQLDataRoute = (app, route, root) => { 6 | app.use( 7 | route, 8 | graphqlHTTP({ 9 | schema: DataSchema, 10 | rootValue: root, 11 | graphiql: process.env.NODE_ENV === 'development', 12 | }), 13 | ); 14 | }; 15 | 16 | export default attachGraphQLDataRoute; 17 | -------------------------------------------------------------------------------- /src/api/v1/schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from 'graphql'; 2 | 3 | import * as structures from '../../structures'; 4 | 5 | const { 6 | graphql: { Cookies, CookiesInputType }, 7 | } = structures; 8 | 9 | const query = new GraphQLObjectType({ 10 | name: 'QueryAPI', 11 | description: 'Query API for Pooky API', 12 | fields: () => ({ 13 | cookies: { 14 | type: Cookies, 15 | description: 'Retrieve one set of cookies', 16 | resolve: root => root.getCookies(), 17 | }, 18 | flush: { 19 | type: GraphQLNonNull(GraphQLBoolean), 20 | description: 'Flushes all cookies from the store', 21 | resolve: root => root.flushCookies(), 22 | }, 23 | }), 24 | }); 25 | 26 | const mutation = new GraphQLObjectType({ 27 | name: 'MutationAPI', 28 | description: 'Mutation API for Pooky API', 29 | fields: () => ({ 30 | addCookies: { 31 | type: Cookies, 32 | description: 'Add a new cookies object', 33 | args: { 34 | data: { 35 | type: GraphQLNonNull(CookiesInputType), 36 | description: 'Cookies object data to store', 37 | }, 38 | }, 39 | resolve: (root, { data }) => root.addCookies(data), 40 | }, 41 | }), 42 | }); 43 | 44 | const schema = new GraphQLSchema({ 45 | query, 46 | mutation, 47 | types: [Cookies], 48 | }); 49 | 50 | export default schema; 51 | -------------------------------------------------------------------------------- /src/datastore/api/api.js: -------------------------------------------------------------------------------- 1 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["pop", "flush", "add"] }] */ 2 | /* eslint no-unused-vars: "off" */ 3 | // Standard BREAD api 4 | class Api { 5 | constructor(type) { 6 | if (new.target === Api) { 7 | throw new TypeError('Cannot construct instances of this abstract class!'); 8 | } 9 | this._type = type; 10 | } 11 | 12 | get type() { 13 | return this._type; 14 | } 15 | 16 | /** 17 | * Removes the first index of a dataset 18 | * 19 | * @returns {Object} payload 20 | * @throws when operation could not be performed 21 | */ 22 | async pop() { 23 | throw new Error('This needs to be overwritten in subclass!'); 24 | } 25 | 26 | /** 27 | * Removes all keys in a database 28 | * 29 | * @returns {Object} payload 30 | * @throws when operation could not be performed 31 | */ 32 | async flush() { 33 | throw new Error('This needs to be overwritten in subclass!'); 34 | } 35 | 36 | /** 37 | * Add the given payload with a new id 38 | * 39 | * @param {Object} payload the new payload to add 40 | * @returns {Object} payload that has been added with a new id 41 | * @throws when operation could not be performed 42 | */ 43 | async push(payload) { 44 | throw new Error('This needs to be overwritten in subclass!'); 45 | } 46 | } 47 | 48 | export default Api; 49 | -------------------------------------------------------------------------------- /src/datastore/api/redisApi.js: -------------------------------------------------------------------------------- 1 | import Api from './api'; 2 | 3 | class RedisApi extends Api { 4 | constructor(type, client) { 5 | super(type); 6 | this._client = client; 7 | } 8 | 9 | /** 10 | * @returns the first existing cookie dataset 11 | * @throws when an error occurs in retrieving the data 12 | */ 13 | async pop() { 14 | try { 15 | const reply = await this._client.lpop(`data:${this._type}`); 16 | if (!reply) { 17 | throw new Error('No cookies available!'); 18 | } 19 | return JSON.parse(reply); 20 | } catch (err) { 21 | throw new Error(`Unable to pop: ${err.message}`); 22 | } 23 | } 24 | 25 | async flush() { 26 | try { 27 | const reply = await this._client.flushdb(); 28 | 29 | if (!reply) { 30 | throw new Error('Technical error!'); 31 | } 32 | return true; 33 | } catch (err) { 34 | throw new Error(`Unable to flush: ${err.message}`); 35 | } 36 | } 37 | 38 | async push(payload) { 39 | try { 40 | const reply = await this._client.rpush(`data:${this._type}`, JSON.stringify(payload)); 41 | if (!reply) { 42 | throw new Error('Technical error!'); 43 | } 44 | return payload; 45 | } catch (err) { 46 | throw new Error(`Unable to add: ${err.message}`); 47 | } 48 | } 49 | } 50 | 51 | export default RedisApi; 52 | -------------------------------------------------------------------------------- /src/datastore/defns/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | REDIS: 'REDIS', 3 | }; 4 | -------------------------------------------------------------------------------- /src/datastore/index.js: -------------------------------------------------------------------------------- 1 | import RedisStore from './store/redisStore'; 2 | 3 | export default RedisStore; 4 | -------------------------------------------------------------------------------- /src/datastore/store/redisStore.js: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | 3 | import RedisApi from '../api/redisApi'; 4 | import Defns from '../defns'; 5 | import Store from './store'; 6 | 7 | class RedisStore extends Store { 8 | constructor(redisClientOptions = null) { 9 | super(Defns.REDIS); 10 | 11 | if (redisClientOptions) { 12 | this._client = new Redis(redisClientOptions); 13 | } else { 14 | this._client = new Redis(); 15 | } 16 | this._cookiesApi = new RedisApi('cookies', this._client); 17 | } 18 | 19 | get cookies() { 20 | return this._cookiesApi; 21 | } 22 | } 23 | 24 | export default RedisStore; 25 | -------------------------------------------------------------------------------- /src/datastore/store/store.js: -------------------------------------------------------------------------------- 1 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["cookies", "settings"] }] */ 2 | 3 | class Store { 4 | constructor(source) { 5 | if (new.target === Store) { 6 | throw new TypeError('Cannot contruct instances of this abstract class!'); 7 | } 8 | this._source = source; 9 | } 10 | 11 | /** 12 | * Source of data for the store 13 | */ 14 | get source() { 15 | return this._source; 16 | } 17 | 18 | /** 19 | * Api instance to manage cookies 20 | */ 21 | get cookies() { 22 | throw new Error('This needs to be overwritten in subclass!'); 23 | } 24 | } 25 | 26 | export default Store; 27 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const config = (environment = process.env.NODE_ENV) => { 6 | const file = environment === 'development' ? '.env.dev' : '.env'; 7 | const envPath = path.resolve(__dirname, '..', file); 8 | const parsedEnv = dotenv.parse(fs.readFileSync(envPath)); 9 | Object.keys(parsedEnv).forEach(k => { 10 | process.env[k] = parsedEnv[k]; 11 | }); 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | 4 | import attachV1DataRoute from './api/v1/data'; 5 | import Resolver from './api/resolver'; 6 | import configEnv from './env'; 7 | 8 | configEnv(); 9 | 10 | const rootResolver = new Resolver(); 11 | const app = express(); 12 | app.use(cors()); 13 | 14 | // Attach the v1 graphql data route 15 | attachV1DataRoute(app, '/api/v1/pooky', rootResolver); 16 | 17 | const port = process.env.PORT || 5000; 18 | 19 | rootResolver.store._client 20 | .ping() 21 | .then(() => { 22 | // Listen for a connection. 23 | app.listen(port, () => { 24 | console.log(`Server is listening on port ${port}`); 25 | }); 26 | }) 27 | .catch(err => { 28 | console.log(err); 29 | }); 30 | -------------------------------------------------------------------------------- /src/structures/definitions/cookies.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | // Dynamic Cookies Definition 4 | // JSON Stringified object 5 | export default PropTypes.objectOf(PropTypes.any); 6 | 7 | // Non-Dynamic Strict PropType checker.. uncomment if you don't want to have a dynamic type. 8 | // export default PropTypes.shape({ 9 | // pooky: PropTypes.string.isRequired, 10 | 11 | // pooky_owl: PropTypes.string.isRequired, 12 | 13 | // pooky_data: PropTypes.string.isRequired, 14 | 15 | // pooky_electric: PropTypes.string.isRequired, 16 | 17 | // pooky_order_allow: PropTypes.string.isRequired, 18 | 19 | // pooky_performance: PropTypes.string.isRequired, 20 | 21 | // pooky_recaptcha: PropTypes.string.isRequired, 22 | 23 | // pooky_recaptcha_coherence: PropTypes.string.isRequired, 24 | 25 | // pooky_settings: PropTypes.string.isRequired, 26 | 27 | // pooky_telemetry: PropTypes.string.isRequired, 28 | 29 | // pooky_use_cookie: PropTypes.bool.isRequired, 30 | 31 | // updated_pooky_coherence: PropTypes.string.isRequired, 32 | // }); 33 | -------------------------------------------------------------------------------- /src/structures/graphql/cookies.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLNonNull, GraphQLInputObjectType } from 'graphql'; 2 | 3 | import GraphQLJSON from 'graphql-type-json'; 4 | 5 | export const Cookies = new GraphQLObjectType({ 6 | name: 'Cookies', 7 | description: 'Cookies generated by pooky', 8 | fields: () => ({ 9 | values: { 10 | type: GraphQLNonNull(GraphQLJSON), 11 | description: 'JSON stringified Object of cookies', 12 | }, 13 | }), 14 | }); 15 | 16 | export const CookiesInputType = new GraphQLInputObjectType({ 17 | name: 'CookiesInput', 18 | description: 'Input data to create/edit a Cookies Object', 19 | fields: () => ({ 20 | values: { 21 | type: GraphQLNonNull(GraphQLJSON), 22 | description: 'JSON stringified Object of cookies', 23 | }, 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /src/structures/index.js: -------------------------------------------------------------------------------- 1 | import cookiesDefn from './definitions/cookies'; 2 | import { Cookies, CookiesInputType } from './graphql/cookies'; 3 | import cookiesState from './initialStates/cookies'; 4 | 5 | const definitions = { 6 | cookiesDefn, 7 | }; 8 | const graphql = { 9 | Cookies, 10 | CookiesInputType, 11 | }; 12 | const initialStates = { 13 | cookiesState, 14 | }; 15 | 16 | export { definitions, graphql, initialStates }; 17 | -------------------------------------------------------------------------------- /src/structures/initialStates/cookies.js: -------------------------------------------------------------------------------- 1 | // Initial State for cookies object 2 | // See definitions/cookies.js for descriptions of each field 3 | 4 | export default JSON.stringify({}); 5 | --------------------------------------------------------------------------------