├── app
├── src
│ ├── components
│ │ ├── QuestionList.js
│ │ ├── Home.js
│ │ ├── SessionItem.js
│ │ ├── App.js
│ │ ├── SessionList.js
│ │ ├── LinkList.js
│ │ ├── Header.js
│ │ ├── CreateLink.js
│ │ ├── Link.js
│ │ ├── CreateSession.js
│ │ ├── CreateQuestion.js
│ │ ├── Session.js
│ │ ├── QuestionItem.js
│ │ └── Login.js
│ ├── constants.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── styles
│ │ ├── App.css
│ │ └── index.css
│ ├── index.js
│ ├── logo.svg
│ └── serviceWorker.js
├── .env
├── .env.production
├── public
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── robots.txt
│ ├── manifest.json
│ └── index.html
├── README.md
└── package.json
├── .gitignore
├── README.md
└── api
├── src
├── resolver
│ ├── Question.js
│ ├── User.js
│ ├── Vote.js
│ ├── Query.js
│ ├── Session.js
│ └── Mutation.js
├── repository
│ ├── dynamodb
│ │ ├── DynamoDbClient.js
│ │ ├── DynamoDbRepository.js
│ │ ├── DynamoDbUserRepository.js
│ │ ├── DynamoDbVoteRepository.js
│ │ ├── DynamoDbQuestionRepository.js
│ │ ├── DynamoDbSessionRepository.js
│ │ └── DynamoDbBaseRepository.js
│ ├── UserRepository.js
│ ├── QuestionRepository.js
│ ├── SessionRepository.js
│ └── VoteRepository.js
├── data
│ └── dynamodb
│ │ ├── samples
│ │ ├── ListTables.js
│ │ ├── docker-compose.yml
│ │ ├── DynamoDB.md
│ │ ├── DeleteItem.js
│ │ ├── AtomicUpdateItem.js
│ │ ├── UpdateItem.js
│ │ ├── CreateTables.js
│ │ ├── ReadItem.js
│ │ └── CreateItem.js
│ │ └── InitTables.js
├── __test__
│ └── resolver
│ │ ├── Question.test.js
│ │ ├── User.test.js
│ │ ├── Vote.test.js
│ │ ├── Query.test.js
│ │ ├── Session.test.js
│ │ └── Mutation.test.js
├── authentication.js
├── handler.js
├── local.js
├── schema.graphql
└── serverless.yml
├── README.md
└── package.json
/app/src/components/QuestionList.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:4000/dev/api
--------------------------------------------------------------------------------
/app/src/constants.js:
--------------------------------------------------------------------------------
1 | export const AUTH_TOKEN = 'auth-token'
2 | export const EMAIL = 'email'
3 |
--------------------------------------------------------------------------------
/app/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://sz4llai3tg.execute-api.eu-central-1.amazonaws.com/dev/api
--------------------------------------------------------------------------------
/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilhan-mstf/Kuestion/master/app/public/favicon.ico
--------------------------------------------------------------------------------
/app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilhan-mstf/Kuestion/master/app/public/logo192.png
--------------------------------------------------------------------------------
/app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilhan-mstf/Kuestion/master/app/public/logo512.png
--------------------------------------------------------------------------------
/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # Kuestion web app
2 | - React
3 | - Apollo GraphQL Client
4 |
5 | ## To run
6 | `yarn start`
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .history
2 | node_modules
3 | **/dynamodb/instance
4 | **/build
5 | **/layer
6 | **/.serverless
7 | .env*.local
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kuestion
2 | Question collecting and voting app for live Q&A sessions.
3 |
4 | ## Tech
5 | - GraphQL
6 | - DynamoDb
7 | - Nodejs
8 | - React
9 |
10 | ## Css lib
11 | - http://tachyons.io/
--------------------------------------------------------------------------------
/api/src/resolver/Question.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | id: (parent) => parent.id,
3 | createdAt: (parent) => parent.createdAt + '',
4 | text: (parent) => parent.text,
5 | voteCount: (parent) => parent.voteCount
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/api/src/resolver/User.js:
--------------------------------------------------------------------------------
1 | const SessionRepository = require('../repository/SessionRepository')
2 |
3 | module.exports = {
4 | name: (parent) => parent.name,
5 | email: (parent) => parent.email,
6 | sessions: (parent, args, context) => SessionRepository.getSessionsOfUser(context.repo, parent.email)
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbClient.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk')
2 |
3 | if (process.env.IS_OFFLINE) {
4 | AWS.config.update({
5 | region: process.env.REGION,
6 | endpoint: process.env.DYNAMODB_ENDPOINT
7 | })
8 | }
9 |
10 | const docClient = new AWS.DynamoDB.DocumentClient()
11 |
12 | module.exports = {
13 | docClient
14 | }
--------------------------------------------------------------------------------
/api/README.md:
--------------------------------------------------------------------------------
1 | # Kuestion api
2 | - DynamoDb
3 | - GraphQL
4 | - Nodejs
5 |
6 | ## Init Db
7 | `node src/data/dynamodb/InitTables.js`
8 |
9 | ## Serverless
10 |
11 | ## Test
12 | curl \
13 | -X POST \
14 | -H "Content-Type: application/json" \
15 | --data '{ "query": "{ info }" }' \
16 | https://18nkkf0lz0.execute-api.eu-central-1.amazonaws.com/dev/api
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbRepository.js:
--------------------------------------------------------------------------------
1 | const User = require('./DynamoDbUserRepository')
2 | const Session = require('./DynamoDbSessionRepository')
3 | const Question = require('./DynamoDbQuestionRepository')
4 | const Vote = require('./DynamoDbVoteRepository')
5 |
6 | module.exports = {
7 | User,
8 | Session,
9 | Question,
10 | Vote
11 | }
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/ListTables.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const dynamodb = new AWS.DynamoDB();
9 |
10 | dynamodb.listTables({Limit: 10}, function(err, data) {
11 | if (err) {
12 | console.log("Error", err.code);
13 | } else {
14 | console.log("Table names are ", data.TableNames);
15 | }
16 | });
--------------------------------------------------------------------------------
/api/src/resolver/Vote.js:
--------------------------------------------------------------------------------
1 | const QuestionRepository = require('../repository/QuestionRepository')
2 | const UserRepository = require('../repository/UserRepository')
3 |
4 | module.exports = {
5 | id: (parent) => parent.questionIdEmail,
6 | question: (parent, args, context) => QuestionRepository.getQuestion(context.repo, parent.questionId),
7 | user: (parent, args, context) => UserRepository.getUser(context.repo, parent.email)
8 | }
9 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hackernews-node",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "bcryptjs": "^2.4.3",
8 | "graphql-yoga": "^1.18.3",
9 | "jsonwebtoken": "^8.5.1",
10 | "uuid": "^8.0.0"
11 | },
12 | "devDependencies": {
13 | "aws-sdk": "^2.678.0",
14 | "jest": "^26.0.1",
15 | "serverless-offline": "^6.1.7"
16 | },
17 | "scripts": {
18 | "test": "jest --maxWorkers=1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbUserRepository.js:
--------------------------------------------------------------------------------
1 | const Base = require('./DynamoDbBaseRepository')
2 |
3 | function persist (user) {
4 | const params = {
5 | TableName: "User",
6 | Item: user
7 | }
8 | return Base.persist(params)
9 | }
10 |
11 | function get (email) {
12 | const params = {
13 | TableName: "User",
14 | Key: {
15 | email: email
16 | }
17 | }
18 | return Base.get(params)
19 | }
20 |
21 | module.exports = {
22 | persist,
23 | get
24 | }
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | dynamodb:
4 | container_name: dynamodb-kuestion
5 | image: amazon/dynamodb-local
6 | entrypoint: java
7 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal/data/"
8 | restart: always
9 | volumes:
10 | - dynamodb-kuestion:/home/dynamodblocal/data/
11 | ports:
12 | - "8000:8000"
13 |
14 | volumes:
15 | dynamodb-kuestion:
16 | external: true
--------------------------------------------------------------------------------
/app/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/api/src/__test__/resolver/Question.test.js:
--------------------------------------------------------------------------------
1 | const Question = require('../../resolver/Question')
2 |
3 | describe('Question type resolver', () => {
4 | it('Should resolve question fields correctly', async () => {
5 | const question = {
6 | id: 'question-0',
7 | createdAt: new Date().getTime(),
8 | text: 'Where is m mind?',
9 | voteCount: 5
10 | }
11 |
12 | expect(Question.id(question)).toBe(question.id)
13 | expect(Question.createdAt(question)).toBe(question.createdAt + "")
14 | expect(Question.text(question)).toBe(question.text)
15 | expect(Question.voteCount(question)).toEqual(question.voteCount)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/api/src/resolver/Query.js:
--------------------------------------------------------------------------------
1 | const SessionRepository = require('../repository/SessionRepository')
2 | const UserRepository = require('../repository/UserRepository')
3 | const Authentication = require('../authentication')
4 |
5 | function info () {
6 | return `This is the API of a Kuestion App`
7 | }
8 |
9 | function session (parent, { id }, context) {
10 | return SessionRepository.getSession(context.repo, id)
11 | }
12 |
13 | function user (parent, args, context) {
14 | const email = Authentication.getEmail(context)
15 | return UserRepository.getUser(context.repo, email)
16 | }
17 |
18 | module.exports = {
19 | info,
20 | session,
21 | user
22 | }
23 |
--------------------------------------------------------------------------------
/api/src/authentication.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const APP_SECRET = 'Arise, arise, Riders of Théoden! ... Ride now, ride now! Ride to Gondor!'
3 |
4 | function getEmail (context) {
5 | //console.log(context)
6 | const headers = context.request || context.event.headers
7 | const authorization = headers.authorization || headers.get('Authorization')
8 | if (authorization) {
9 | const token = authorization.replace('Bearer ', '')
10 | const { email } = jwt.verify(token, APP_SECRET)
11 | return email
12 | }
13 |
14 | throw new Error('Not authenticated')
15 | }
16 |
17 | module.exports = {
18 | APP_SECRET,
19 | getEmail
20 | }
21 |
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/DynamoDB.md:
--------------------------------------------------------------------------------
1 | # Commands
2 | ## Jar command
3 | `java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb`
4 |
5 | ## Docker
6 | `sudo docker pull amazon/dynamodb-local`
7 | `sudo docker run -p 8000:8000 amazon/dynamodb-local`
8 | `sudo docker run -p 8000:8000 -v $(pwd)/local/dynamodb:/data/ amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb -dbPath /data`
9 |
10 | ## Connect with AWS CLI
11 | `aws dynamodb list-tables --endpoint-url http://localhost:8000 --region local`
12 |
13 | ## NoSQL Workbench
14 |
15 | ## AWS node sdk
16 | https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.02.html
17 |
--------------------------------------------------------------------------------
/api/src/__test__/resolver/User.test.js:
--------------------------------------------------------------------------------
1 | const User = require('../../resolver/User')
2 | const SessionRepository = require('../../repository/SessionRepository')
3 |
4 | describe('User type resolver', () => {
5 | it('Should resolve user fields correctly', async () => {
6 | const sessions = [{ id: 'session-0' }]
7 | SessionRepository.getSessionsOfUser = jest.fn().mockImplementation(() => sessions)
8 |
9 | const user = {
10 | name: 'alice',
11 | email: 'alice@google.com'
12 | }
13 |
14 | expect(User.name(user)).toBe(user.name)
15 | expect(User.email(user)).toBe(user.email)
16 | expect(User.sessions(user, {}, {repo:{}})).toEqual(sessions)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/api/src/repository/UserRepository.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs')
2 |
3 | async function createUser (repo, name, email, password) {
4 | // TODO email validation
5 | const hasUser = await getUser(repo, email)
6 | if (hasUser) {
7 | throw new Error('createUser - User already exists')
8 | }
9 | const hashedPassword = await bcrypt.hash(password, 10)
10 | const user = {
11 | createdAt: new Date().getTime(),
12 | name: name,
13 | email: email,
14 | password: hashedPassword
15 | }
16 | return repo.User.persist(user)
17 | }
18 |
19 | function getUser (repo, email) {
20 | return repo.User.get(email)
21 | }
22 |
23 | module.exports = {
24 | createUser,
25 | getUser
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/styles/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Verdana, Geneva, sans-serif;
5 | }
6 |
7 | input {
8 | max-width: 500px;
9 | }
10 |
11 | .gray {
12 | color: #828282;
13 | }
14 |
15 | .orange {
16 | background-color: #ff6600;
17 | }
18 |
19 | .background-gray {
20 | background-color: rgb(246,246,239);
21 | }
22 |
23 | .f11 {
24 | font-size: 11px;
25 | }
26 |
27 | .w85 {
28 | width: 85%;
29 | }
30 |
31 | .button {
32 | font-family: monospace;
33 | font-size: 10pt;
34 | color: black;
35 | background-color: buttonface;
36 | text-align: center;
37 | padding: 2px 6px 3px;
38 | border-width: 2px;
39 | border-style: outset;
40 | border-color: buttonface;
41 | cursor: pointer;
42 | max-width: 250px;
43 | }
--------------------------------------------------------------------------------
/api/src/handler.js:
--------------------------------------------------------------------------------
1 | const { GraphQLServerLambda } = require('graphql-yoga')
2 | const Query = require('./resolver/Query')
3 | const Mutation = require('./resolver/Mutation')
4 | const Session = require('./resolver/Session')
5 | const User = require('./resolver/User')
6 | const Question = require('./resolver/Question')
7 | const Vote = require('./resolver/Vote')
8 | const repo = require('./repository/dynamodb/DynamoDbRepository')
9 |
10 | const resolvers = {
11 | Query,
12 | Mutation,
13 | Session,
14 | User,
15 | Question,
16 | Vote
17 | }
18 |
19 | const lambda = new GraphQLServerLambda({
20 | typeDefs: './schema.graphql',
21 | resolvers,
22 | context: request => {
23 | return { ...request, repo }
24 | }
25 | })
26 |
27 | exports.server = lambda.graphqlHandler
--------------------------------------------------------------------------------
/api/src/local.js:
--------------------------------------------------------------------------------
1 | const { GraphQLServer } = require('graphql-yoga')
2 | const Query = require('./resolver/Query')
3 | const Mutation = require('./resolver/Mutation')
4 | const Session = require('./resolver/Session')
5 | const User = require('./resolver/User')
6 | const Question = require('./resolver/Question')
7 | const Vote = require('./resolver/Vote')
8 | const repo = require('./repository/dynamodb/DynamoDbRepository')
9 |
10 | const resolvers = {
11 | Query,
12 | Mutation,
13 | Session,
14 | User,
15 | Question,
16 | Vote
17 | }
18 |
19 | const server = new GraphQLServer({
20 | typeDefs: './src/schema.graphql',
21 | resolvers,
22 | context: request => {
23 | return { ...request, repo }
24 | }
25 | })
26 | server.start(() => console.log(`Server is running on http://localhost:4000`))
27 |
--------------------------------------------------------------------------------
/app/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { AUTH_TOKEN } from '../constants'
3 | import SessionList from './SessionList'
4 |
5 | class Home extends Component {
6 | render () {
7 | const authToken = localStorage.getItem(AUTH_TOKEN)
8 | return (
9 |
10 | {authToken ? (
11 |
12 | ) : (
13 |
14 | Collect Questions
15 | For Your Live Question & Anwser Sessions
16 |
17 | - Create a session
18 | - Share it
19 | - Add questions
20 | - Upvote questions
21 |
22 |
23 | )}
24 |
25 | )
26 | }
27 | }
28 |
29 | export default Home
30 |
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/DeleteItem.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const docClient = new AWS.DynamoDB.DocumentClient();
9 |
10 | const params = {
11 | TableName: "Test",
12 | Key:{
13 | "year": 2015,
14 | "title": "The Big New Movie"
15 | },
16 | ConditionExpression:"info.rating <= :val",
17 | ExpressionAttributeValues: {
18 | ":val": 5.0
19 | }
20 | };
21 |
22 | console.log("Attempting a conditional delete...");
23 | docClient.delete(params, function(err, data) {
24 | if (err) {
25 | console.error("Unable to delete item. Error JSON:", JSON.stringify(err, null, 2));
26 | } else {
27 | console.log("DeleteItem succeeded:", JSON.stringify(data, null, 2));
28 | }
29 | });
--------------------------------------------------------------------------------
/api/src/__test__/resolver/Vote.test.js:
--------------------------------------------------------------------------------
1 | const Vote = require('../../resolver/Vote')
2 | const QuestionRepository = require('../../repository/QuestionRepository')
3 | const UserRepository = require('../../repository/UserRepository')
4 |
5 | describe('Vote type resolver', () => {
6 | it('Should resolve vote fields correctly', async () => {
7 | const question = { id: 'question-0' }
8 | QuestionRepository.getQuestion = jest.fn().mockImplementation(() => question)
9 |
10 | const user = { email: 'user@google.com' }
11 | UserRepository.getUser = jest.fn().mockImplementation(() => user)
12 |
13 | const vote = {
14 | questionId: 'question-0',
15 | email: 'user@google.com'
16 | }
17 |
18 | expect(Vote.question(vote, {}, {repo:{}})).toEqual(question)
19 | expect(Vote.user(vote, {}, {repo:{}})).toEqual(user)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/app/src/components/SessionItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | class SessionItem extends Component {
5 | render () {
6 | return (
7 |
8 |
9 | {this.props.index + 1}.
10 |
11 |
12 |
13 |
14 | {this.props.item.title}
15 |
16 |
17 |
18 | {this.props.item.totalQuestionCount} questions | {this.props.item.totalVoteCount} votes
19 |
20 |
21 |
22 | )
23 | }
24 | }
25 |
26 | export default SessionItem
27 |
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/AtomicUpdateItem.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const docClient = new AWS.DynamoDB.DocumentClient();
9 |
10 | const params = {
11 | TableName: "Test",
12 | Key:{
13 | "year": 2015,
14 | "title": "The Big New Movie"
15 | },
16 | UpdateExpression: "set info.rating = info.rating + :val",
17 | ExpressionAttributeValues:{
18 | ":val": 1
19 | },
20 | ReturnValues:"UPDATED_NEW"
21 | };
22 |
23 | console.log("Updating the item...");
24 | docClient.update(params, function(err, data) {
25 | if (err) {
26 | console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
27 | } else {
28 | console.log("UpdateItem succeeded:", JSON.stringify(data, null, 2));
29 | }
30 | });
31 |
32 |
--------------------------------------------------------------------------------
/app/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import CreateSession from './CreateSession'
3 | import Home from './Home'
4 | import Header from './Header'
5 | import Login from './Login'
6 | import Session from './Session'
7 | import { Switch, Route } from 'react-router-dom'
8 |
9 | class App extends Component {
10 | render () {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | }
26 |
27 | export default App
28 |
--------------------------------------------------------------------------------
/api/src/resolver/Session.js:
--------------------------------------------------------------------------------
1 | const UserRepository = require('../repository/UserRepository')
2 | const QuestionRepository = require('../repository/QuestionRepository')
3 | const VoteRepository = require('../repository/VoteRepository')
4 |
5 | module.exports = {
6 | id: (parent) => parent.id,
7 | createdAt: (parent) => parent.createdAt + "",
8 | title: (parent) => parent.title,
9 | description: (parent) => parent.description,
10 | postedBy: (parent, args, context) => {
11 | return UserRepository.getUser(context.repo, parent.email)
12 | },
13 | questions: (parent, args, context) => {
14 | return QuestionRepository.getQuestionsOfSession(context.repo, parent.id)
15 | },
16 | votesOfCurrentUser: (parent, args, context) => {
17 | return VoteRepository.getSessionVotesOfCurrentUser(context.repo, parent.id, parent.email)
18 | },
19 | totalQuestionCount: (parent) => parent.totalQuestionCount,
20 | totalVoteCount: (parent) => parent.totalVoteCount
21 | }
22 |
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/UpdateItem.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const docClient = new AWS.DynamoDB.DocumentClient();
9 |
10 | const params = {
11 | TableName: "Test",
12 | Key:{
13 | "year": 2015,
14 | "title": "The Big New Movie"
15 | },
16 | UpdateExpression: "set info.rating = :r, info.plot=:p, info.actors=:a",
17 | ExpressionAttributeValues:{
18 | ":r":5.5,
19 | ":p":"Everything happens all at once.",
20 | ":a":["Larry", "Moe", "Curly"]
21 | },
22 | ReturnValues:"UPDATED_NEW"
23 | };
24 |
25 | console.log("Updating the item...");
26 | docClient.update(params, function(err, data) {
27 | if (err) {
28 | console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
29 | } else {
30 | console.log("UpdateItem succeeded:", JSON.stringify(data, null, 2));
31 | }
32 | });
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/CreateTables.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const dynamodb = new AWS.DynamoDB();
9 |
10 | const params = {
11 | TableName : "Test",
12 | KeySchema: [
13 | { AttributeName: "year", KeyType: "HASH"}, //Partition key
14 | { AttributeName: "title", KeyType: "RANGE" } //Sort key
15 | ],
16 | AttributeDefinitions: [
17 | { AttributeName: "year", AttributeType: "N" },
18 | { AttributeName: "title", AttributeType: "S" }
19 | ],
20 | ProvisionedThroughput: {
21 | ReadCapacityUnits: 2,
22 | WriteCapacityUnits: 1
23 | }
24 | };
25 |
26 | dynamodb.createTable(params, function(err, data) {
27 | if (err) {
28 | console.error("Unable to create table. Error JSON:", JSON.stringify(err, null, 2));
29 | } else {
30 | console.log("Created table. Table description JSON:", JSON.stringify(data, null, 2));
31 | }
32 | });
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/ReadItem.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const docClient = new AWS.DynamoDB.DocumentClient();
9 |
10 | const params = {
11 | TableName: "Test",
12 | Key:{
13 | "year": 2015,
14 | "title": "The Big New Movie"
15 | }
16 | };
17 |
18 | async function get(params) {
19 | try {
20 | const data = await docClient.get(params).promise();
21 | console.log("GetItem succeeded:", JSON.stringify(data, null, 2));
22 | return data
23 | } catch (err) {
24 | console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));
25 | }
26 | }
27 |
28 | get(params);
29 |
30 | /*
31 | docClient.get(params, function(err, data) {
32 | if (err) {
33 | console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));
34 | } else {
35 | console.log("GetItem succeeded:", JSON.stringify(data, null, 2));
36 | }
37 | });
38 | */
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbVoteRepository.js:
--------------------------------------------------------------------------------
1 | const Base = require('./DynamoDbBaseRepository')
2 |
3 | function persist (vote) {
4 | vote.questionIdEmail = `${vote.questionId}-${vote.email}`
5 | vote.sessionIdEmail = `${vote.sessionId}-${vote.email}`
6 |
7 | const params = {
8 | TableName: "Vote",
9 | Item: vote
10 | }
11 | return Base.persist(params)
12 | }
13 |
14 | function get (questionId, email) {
15 | const params = {
16 | TableName: "Vote",
17 | Key: {
18 | questionIdEmail: `${questionId}-${email}`
19 | }
20 | }
21 | return Base.get(params)
22 | }
23 |
24 | function getSessionVotesOfCurrentUser (sessionId, userId) {
25 | const sessionIdEmail = `${vote.sessionId}-${vote.email}`
26 |
27 | const params = {
28 | TableName: "Vote",
29 | IndexName: "SessionIdEmailIndex",
30 | KeyConditionExpression: "sessionIdEmail = :sie",
31 | ExpressionAttributeValues: {
32 | ":sie": sessionIdEmail
33 | }
34 | }
35 | return Base.query(params)
36 | }
37 |
38 | module.exports = {
39 | persist,
40 | get,
41 | getSessionVotesOfCurrentUser
42 | }
--------------------------------------------------------------------------------
/api/src/__test__/resolver/Query.test.js:
--------------------------------------------------------------------------------
1 | const Query = require('../../resolver/Query')
2 | const SessionRepository = require('../../repository/SessionRepository')
3 | const UserRepository = require('../../repository/UserRepository')
4 | const Authentication = require('../../authentication')
5 |
6 | describe('Query type resolver', () => {
7 | it('Should resolve session', async () => {
8 | const session = { id: 'session-0' }
9 | SessionRepository.getSession = jest.fn().mockImplementationOnce(() => session)
10 |
11 | expect(Query.session({}, { id: 'session-0' }, { repo: {} })).toEqual(session)
12 | })
13 |
14 | it('Should resolve user when logged in', async () => {
15 | const user = { id: 'user-0' }
16 | UserRepository.getUser = jest.fn().mockImplementationOnce(() => user)
17 | Authentication.getEmail = jest.fn().mockImplementationOnce(() => 'user@google.com')
18 |
19 | expect(Query.user({}, {}, { repo: {} })).toEqual(user)
20 | })
21 |
22 | it('Should return no value when nobody logged in', async () => {
23 | expect(Query.user({}, {}, { repo: {} })).toEqual(undefined)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hackernews-react-apollo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "apollo-boost": "^0.4.8",
10 | "apollo-link-context": "^1.0.20",
11 | "graphql": "^15.0.0",
12 | "react": "^16.13.1",
13 | "react-apollo": "^3.1.5",
14 | "react-dom": "^16.13.1",
15 | "react-router": "^5.2.0",
16 | "react-router-dom": "^5.2.0",
17 | "react-scripts": "3.4.1"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/api/src/schema.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | info: String!
3 | session(id: ID!): Session
4 | user: User
5 | }
6 |
7 | type Mutation {
8 | createSession(title: String!, description: String!): Session!
9 | updateSession(id: ID!, title: String, description: String): Session
10 |
11 | createQuestion(text: String!, sessionId: ID!): Question!
12 |
13 | signup(name: String!, email: String!, password: String!): AuthPayload
14 | login(email: String!, password: String!): AuthPayload
15 |
16 | vote(questionId: ID!, sessionId: ID!): Vote
17 | }
18 |
19 | type Session {
20 | id: ID!
21 | createdAt: String!
22 | title: String!
23 | description: String!
24 | postedBy: User!
25 | questions: [Question!]!
26 | votesOfCurrentUser: [ID!]!
27 | totalQuestionCount: Int!
28 | totalVoteCount: Int!
29 | }
30 |
31 | type User {
32 | email: String!
33 | name: String!
34 | sessions: [Session!]!
35 | }
36 |
37 | type Question {
38 | id: ID!
39 | createdAt: String!
40 | text: String!
41 | voteCount: Int!
42 | }
43 |
44 | type Vote {
45 | id: ID!
46 | question: Question!
47 | user: User!
48 | }
49 |
50 | type AuthPayload {
51 | token: String
52 | user: User
53 | }
--------------------------------------------------------------------------------
/api/src/data/dynamodb/samples/CreateItem.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk');
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | });
7 |
8 | const docClient = new AWS.DynamoDB.DocumentClient();
9 |
10 | const params = {
11 | TableName: "Test",
12 | Item:{
13 | "year": 2018,
14 | "title": "The Big New Movie",
15 | "info":{
16 | "plot": "Nothing happens at all.",
17 | "rating": 0
18 | }
19 | }
20 | };
21 |
22 | console.log("Adding a new item...");
23 |
24 | async function persist (params) {
25 | try {
26 | const data = await docClient.put(params).promise()
27 | console.log("Added item:", JSON.stringify(data, null, 2));
28 | return data
29 | } catch (err) {
30 | console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
31 | }
32 | }
33 |
34 | persist(params);
35 |
36 | /*
37 | docClient.put(params, function(err, data) {
38 | if (err) {
39 | console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
40 | } else {
41 | console.log("Added item:", JSON.stringify(data, null, 2));
42 | }
43 | });
44 | */
--------------------------------------------------------------------------------
/api/src/repository/QuestionRepository.js:
--------------------------------------------------------------------------------
1 | const SessionRepository = require('./SessionRepository')
2 |
3 | async function createQuestion (repo, sessionId, postedBy, text) {
4 | if (!postedBy) {
5 | throw new Error('createQuestion - No user found')
6 | }
7 | const hasSession = await SessionRepository.getSession(repo, sessionId)
8 | if (!hasSession) {
9 | throw new Error('createQuestion - No session found')
10 | }
11 | // TODO string.notblank
12 |
13 | const question = {
14 | sessionId: sessionId,
15 | email: postedBy,
16 | createdAt: new Date().getTime(),
17 | text: text,
18 | voteCount: 0
19 | }
20 | const result = await repo.Question.persist(question)
21 |
22 | SessionRepository.incrementTotalQuestionCount(repo, sessionId)
23 |
24 | return result
25 | }
26 |
27 | function getQuestion (repo, id) {
28 | return repo.Question.get(id)
29 | }
30 |
31 | function getQuestionsOfSession (repo, sessionId) {
32 | return repo.Question.getQuestionsOfSession(sessionId)
33 | }
34 |
35 | function incrementVoteCount (repo, id) {
36 | return repo.Question.incrementVoteCount(id)
37 | }
38 |
39 | module.exports = {
40 | createQuestion,
41 | getQuestion,
42 | getQuestionsOfSession,
43 | incrementVoteCount
44 | }
--------------------------------------------------------------------------------
/app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { BrowserRouter } from 'react-router-dom'
4 | import './styles/index.css'
5 | import App from './components/App'
6 | import * as serviceWorker from './serviceWorker'
7 |
8 | import { ApolloProvider } from 'react-apollo'
9 | import { ApolloClient } from 'apollo-client'
10 | import { createHttpLink } from 'apollo-link-http'
11 | import { InMemoryCache } from 'apollo-cache-inmemory'
12 | import { setContext } from 'apollo-link-context'
13 |
14 | import { AUTH_TOKEN } from './constants'
15 |
16 | const httpLink = createHttpLink({
17 | uri: process.env.REACT_APP_API_URL
18 | })
19 |
20 | const authLink = setContext((_, { headers }) => {
21 | const token = localStorage.getItem(AUTH_TOKEN)
22 | return {
23 | headers: {
24 | ...headers,
25 | authorization: token ? `Bearer ${token}` : ''
26 | }
27 | }
28 | })
29 |
30 | export const client = new ApolloClient({
31 | link: authLink.concat(httpLink),
32 | cache: new InMemoryCache()
33 | })
34 |
35 | ReactDOM.render(
36 |
37 |
38 |
39 |
40 | ,
41 | document.getElementById('root')
42 | )
43 | serviceWorker.unregister()
44 |
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbQuestionRepository.js:
--------------------------------------------------------------------------------
1 | const Base = require('./DynamoDbBaseRepository')
2 | const { v4: uuidv4 } = require('uuid')
3 |
4 | function persist (question) {
5 | question.id = uuidv4()
6 |
7 | const params = {
8 | TableName: "Question",
9 | Item: question
10 | }
11 | return Base.persist(params)
12 | }
13 |
14 | function get (id) {
15 | const params = {
16 | TableName: "Question",
17 | Key: {
18 | id: id
19 | }
20 | }
21 | return Base.get(params)
22 | }
23 |
24 | function getQuestionsOfSession (sessionId) {
25 | const params = {
26 | TableName: "Question",
27 | IndexName: "SessionIdIndex",
28 | KeyConditionExpression: "sessionId = :s",
29 | ExpressionAttributeValues: {
30 | ":s": sessionId
31 | }
32 | }
33 | return Base.query(params)
34 | }
35 |
36 | function incrementVoteCount (id) {
37 | var params = {
38 | TableName: "Question",
39 | Key:{
40 | "id": id
41 | },
42 | UpdateExpression: "set voteCount = voteCount + :val",
43 | ExpressionAttributeValues:{
44 | ":val": 1
45 | },
46 | ReturnValues:"UPDATED_NEW"
47 | }
48 | return Base.update(params)
49 | }
50 |
51 | module.exports = {
52 | persist,
53 | get,
54 | getQuestionsOfSession,
55 | incrementVoteCount
56 | }
--------------------------------------------------------------------------------
/app/src/components/SessionList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import SessionItem from './SessionItem'
3 | import { Query } from 'react-apollo'
4 | import gql from 'graphql-tag'
5 |
6 | export const SESSION_LIST_QUERY = gql`
7 | {
8 | user {
9 | sessions {
10 | id
11 | title
12 | description
13 | createdAt
14 | totalQuestionCount
15 | totalVoteCount
16 | }
17 | }
18 | }
19 | `
20 |
21 | class SessionList extends Component {
22 | render () {
23 | return (
24 |
25 | {({ loading, error, data }) => {
26 | if (loading) return Fetching
27 | if (error) return Error
28 |
29 | const itemsToRender = data.user ? data.user.sessions : []
30 |
31 | return (
32 |
33 | {itemsToRender.map((item, index) => (
34 |
38 | ))}
39 | {itemsToRender.length === 0 && (
40 |
You don't have any session yet.
41 | )}
42 |
43 | )
44 | }}
45 |
46 | )
47 | }
48 | }
49 |
50 | export default SessionList
51 |
--------------------------------------------------------------------------------
/api/src/repository/SessionRepository.js:
--------------------------------------------------------------------------------
1 | function createSession (repo, postedBy, title, description) {
2 | if (!postedBy) {
3 | throw new Error('createSession - No user found')
4 | }
5 |
6 | const session = {
7 | email: postedBy,
8 | createdAt: new Date().getTime(),
9 | title: title,
10 | description: description,
11 | totalQuestionCount: 0,
12 | totalVoteCount: 0
13 | }
14 |
15 | return repo.Session.persist(session)
16 | }
17 |
18 | function getSession (repo, id) {
19 | return repo.Session.get(id)
20 | }
21 |
22 | async function updateSession (repo, id, updatedBy, title, description) {
23 | const session = await getSession(repo, id)
24 | if (!session) {
25 | throw new Error('updateSession - No session found')
26 | }
27 | if (session.email !== updatedBy) {
28 | throw new Error('updateSession - Not allowed')
29 | }
30 |
31 | session.title = title || session.title
32 | session.description = description || session.description
33 |
34 | return repo.Session.update(session)
35 | }
36 |
37 | function getSessionsOfUser (repo, email) {
38 | return repo.Session.getSessionsOfUser(email)
39 | }
40 |
41 | function incrementTotalQuestionCount (repo, id) {
42 | return repo.Session.incrementTotalQuestionCount(id)
43 | }
44 |
45 | function incrementTotalVoteCount (repo, id) {
46 | return repo.Session.incrementTotalVoteCount(id)
47 | }
48 |
49 | module.exports = {
50 | getSession,
51 | createSession,
52 | updateSession,
53 | getSessionsOfUser,
54 | incrementTotalQuestionCount,
55 | incrementTotalVoteCount
56 | }
--------------------------------------------------------------------------------
/api/src/repository/VoteRepository.js:
--------------------------------------------------------------------------------
1 | const QuestionRepository = require('./QuestionRepository')
2 | const SessionRepository = require('./SessionRepository')
3 |
4 | async function createVote (repo, questionId, sessionId, postedBy) {
5 | if (!postedBy) {
6 | throw new Error('createVote - No user found', postedBy)
7 | }
8 | const hasQuestion = await QuestionRepository.getQuestion(repo, questionId)
9 | if (!hasQuestion) {
10 | throw new Error('createVote - No question found', questionId)
11 | }
12 | const hasSession = await SessionRepository.getSession(repo, sessionId)
13 | if (!hasSession) {
14 | throw new Error('createVote - No session found', sessionId)
15 | }
16 |
17 | const hasVote = await getVote(repo, questionId, postedBy)
18 | if (hasVote) {
19 | throw new Error(`createVote - Already voted`, questionId, postedBy)
20 | }
21 |
22 | const vote = {
23 | questionId: questionId,
24 | sessionId: sessionId,
25 | email: postedBy,
26 | createdAt: new Date().getTime()
27 | }
28 | const result = await repo.Vote.persist(vote)
29 |
30 | QuestionRepository.incrementVoteCount(repo, questionId)
31 | SessionRepository.incrementTotalVoteCount(repo, sessionId)
32 |
33 | return result
34 | }
35 |
36 | function getVote (repo, questionId, email) {
37 | return repo.Vote.get(questionId, email)
38 | }
39 |
40 | function getSessionVotesOfCurrentUser (repo, sessionId, userId) {
41 | return repo.Vote.getSessionVotesOfCurrentUser(sessionId, userId).map(v => v.questionId)
42 | }
43 |
44 | module.exports = {
45 | createVote,
46 | getSessionVotesOfCurrentUser
47 | }
--------------------------------------------------------------------------------
/app/src/components/LinkList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Link from './Link'
3 | import { Query } from 'react-apollo'
4 | import gql from 'graphql-tag'
5 |
6 | export const LINKS_QUERY = gql`
7 | {
8 | links {
9 | id
10 | url
11 | description
12 | postedBy {
13 | id
14 | name
15 | }
16 | votes {
17 | id
18 | user {
19 | id
20 | }
21 | }
22 | }
23 | }
24 | `
25 |
26 | class LinkList extends Component {
27 | _updateCacheAfterVote = (store, createVote, linkId) => {
28 | const data = store.readQuery({ query: LINKS_QUERY })
29 |
30 | const votedLink = data.feed.links.find(link => link.id === linkId)
31 | votedLink.votes = createVote.link.votes
32 |
33 | store.writeQuery({ query: LINKS_QUERY, data })
34 | }
35 |
36 | render () {
37 | return (
38 |
39 | {({ loading, error, data }) => {
40 | if (loading) return Fetching
41 | if (error) return Error
42 |
43 | const linksToRender = data.links
44 |
45 | return (
46 |
47 | {linksToRender.map((link, index) => (
48 |
53 | ))}
54 |
55 | )
56 | }}
57 |
58 | )
59 | }
60 | }
61 |
62 | export default LinkList
63 |
--------------------------------------------------------------------------------
/api/src/__test__/resolver/Session.test.js:
--------------------------------------------------------------------------------
1 | const Session = require('../../resolver/Session')
2 | const UserRepository = require('../../repository/UserRepository')
3 | const QuestionRepository = require('../../repository/QuestionRepository')
4 | const VoteRepository = require('../../repository/VoteRepository')
5 |
6 | describe('Session type resolver', () => {
7 | it('Should resolve session fields correctly', async () => {
8 | const questions = [{ id: 'question-0' }]
9 | QuestionRepository.getQuestionsOfSession = jest.fn().mockImplementation(() => questions)
10 |
11 | const user = { email: 'user@google.com' }
12 | UserRepository.getUser = jest.fn().mockImplementation(() => user)
13 |
14 | const votes = [{ id: 'question-0-user@google.com' }]
15 | VoteRepository.getSessionVotesOfCurrentUser = jest.fn().mockImplementation(() => votes)
16 |
17 | const session = {
18 | id: 'session-0',
19 | createdAt: new Date().getTime(),
20 | title: 'title',
21 | description: 'desc',
22 | email: 'user@google.com'
23 | }
24 |
25 | expect(Session.id(session)).toBe(session.id)
26 | expect(Session.createdAt(session)).toBe(session.createdAt + "")
27 | expect(Session.title(session)).toBe(session.title)
28 | expect(Session.description(session)).toBe(session.description)
29 | expect(Session.postedBy(session, {}, {repo:{}})).toEqual(user)
30 | expect(Session.questions(session, {}, {repo:{}})).toEqual(questions)
31 | expect(Session.votesOfCurrentUser(session, {}, {repo:{}})).toEqual(votes)
32 | expect(Session.totalQuestionCount(session)).toEqual(session.totalQuestionCount)
33 | expect(Session.totalVoteCount(session)).toEqual(session.totalVoteCount)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/app/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { withRouter } from 'react-router'
4 | import { AUTH_TOKEN, EMAIL } from '../constants'
5 | import { client } from '../index'
6 |
7 | class Header extends Component {
8 | render () {
9 | const authToken = localStorage.getItem(AUTH_TOKEN)
10 | const email = localStorage.getItem(EMAIL)
11 | return (
12 |
13 |
14 |
KUESTION
15 |
16 | home
17 |
18 | {authToken && (
19 |
20 |
|
21 |
22 | create session
23 |
24 |
25 | )}
26 |
27 |
28 | {authToken ? (
29 |
30 |
{email}
31 |
|
32 |
{
35 | client.resetStore()
36 | localStorage.removeItem(AUTH_TOKEN)
37 | localStorage.removeItem(EMAIL)
38 | this.props.history.push(`/`)
39 | }}
40 | >
41 | logout
42 |
43 |
44 | ) : (
45 |
46 | login
47 |
48 | )}
49 |
50 |
51 | )
52 | }
53 | }
54 |
55 | export default withRouter(Header)
56 |
--------------------------------------------------------------------------------
/app/src/components/CreateLink.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Mutation } from 'react-apollo'
3 | import gql from 'graphql-tag'
4 | import { LINKS_QUERY } from './LinkList'
5 |
6 | const CREATE_LINK_MUTATION = gql`
7 | mutation CreateLinkMutation($description: String!, $url: String!) {
8 | createLink(description: $description, url: $url) {
9 | id
10 | url
11 | description
12 | }
13 | }
14 | `
15 |
16 | class CreateLink extends Component {
17 | state = {
18 | description: '',
19 | url: '',
20 | }
21 |
22 | render() {
23 | const { description, url } = this.state
24 | return (
25 |
58 | )
59 | }
60 | }
61 |
62 | export default CreateLink
--------------------------------------------------------------------------------
/app/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { AUTH_TOKEN } from '../constants'
3 | import { Mutation } from 'react-apollo'
4 | import gql from 'graphql-tag'
5 |
6 | const VOTE_MUTATION = gql`
7 | mutation VoteMutation($linkId: ID!) {
8 | vote(linkId: $linkId) {
9 | id
10 | link {
11 | id
12 | votes {
13 | id
14 | user {
15 | id
16 | }
17 | }
18 | }
19 | user {
20 | id
21 | }
22 | }
23 | }
24 | `
25 |
26 | class Link extends Component {
27 | render () {
28 | const authToken = localStorage.getItem(AUTH_TOKEN)
29 | return (
30 |
31 |
32 |
{this.props.index + 1}.
33 | {authToken && (
34 |
38 | this.props.updateStoreAfterVote(store, vote, this.props.link.id)
39 | }
40 | onError={() => {}}
41 | >
42 | {voteMutation => (
43 |
44 | ▲
45 |
46 | )}
47 |
48 | )}
49 |
50 |
51 |
52 | {this.props.link.description} ({this.props.link.url})
53 |
54 |
55 | {this.props.link.votes.length} votes | by{' '}
56 | {this.props.link.postedBy
57 | ? this.props.link.postedBy.name
58 | : 'Unknown'}{' '}
59 |
60 |
61 |
62 | )
63 | }
64 | }
65 |
66 | export default Link
67 |
--------------------------------------------------------------------------------
/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbSessionRepository.js:
--------------------------------------------------------------------------------
1 | const Base = require('./DynamoDbBaseRepository')
2 | const { v4: uuidv4 } = require('uuid')
3 |
4 | function persist (session) {
5 | session.id = uuidv4()
6 |
7 | const params = {
8 | TableName: "Session",
9 | Item: session
10 | }
11 | return Base.persist(params)
12 | }
13 |
14 | function get (id) {
15 | const params = {
16 | TableName: "Session",
17 | Key: {
18 | id: id
19 | }
20 | }
21 | return Base.get(params)
22 | }
23 |
24 | function getSessionsOfUser (email) {
25 | const params = {
26 | TableName: "Session",
27 | IndexName: "EmailIndex",
28 | KeyConditionExpression: "email = :e",
29 | ExpressionAttributeValues: {
30 | ":e": email
31 | }
32 | }
33 | return Base.query(params)
34 | }
35 |
36 | function update (session) {
37 | const params = {
38 | TableName: "Session",
39 | Key: {
40 | "id": session.id
41 | },
42 | UpdateExpression: "set title=:t, description=:d",
43 | ExpressionAttributeValues: {
44 | ":t": session.title,
45 | ":d": session.description
46 | },
47 | ReturnValues:"UPDATED_NEW"
48 | }
49 | return Base.update(params)
50 | }
51 |
52 | function incrementTotalQuestionCount (id) {
53 | var params = {
54 | TableName: "Session",
55 | Key:{
56 | "id": id
57 | },
58 | UpdateExpression: "set totalQuestionCount = totalQuestionCount + :val",
59 | ExpressionAttributeValues:{
60 | ":val": 1
61 | },
62 | ReturnValues:"UPDATED_NEW"
63 | }
64 | return Base.update(params)
65 | }
66 |
67 |
68 | function incrementTotalVoteCount (id) {
69 | var params = {
70 | TableName: "Session",
71 | Key:{
72 | "id": id
73 | },
74 | UpdateExpression: "set totalVoteCount = totalVoteCount + :val",
75 | ExpressionAttributeValues:{
76 | ":val": 1
77 | },
78 | ReturnValues:"UPDATED_NEW"
79 | }
80 | return Base.update(params)
81 | }
82 |
83 | module.exports = {
84 | persist,
85 | get,
86 | getSessionsOfUser,
87 | update,
88 | incrementTotalQuestionCount,
89 | incrementTotalVoteCount
90 | }
--------------------------------------------------------------------------------
/api/src/repository/dynamodb/DynamoDbBaseRepository.js:
--------------------------------------------------------------------------------
1 | const Client = require('./DynamoDbClient')
2 |
3 | async function get (params) {
4 | try {
5 | const data = await Client.docClient.get(params).promise()
6 | console.log("GetItem succeeded:", JSON.stringify(data, null, 2))
7 | return data.Item
8 | } catch (err) {
9 | console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2))
10 | throw new Error("Unable to read item")
11 | }
12 | }
13 |
14 | async function persist (params) {
15 | try {
16 | const data = await Client.docClient.put(params).promise()
17 | console.log("Added item:", JSON.stringify(data, null, 2))
18 | return params.Item
19 | } catch (err) {
20 | console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2))
21 | throw new Error("Unable to add item")
22 | }
23 | }
24 |
25 | async function update (params) {
26 | try {
27 | const data = await Client.docClient.update(params).promise()
28 | console.log("UpdateItem succeeded:", JSON.stringify(data, null, 2))
29 | return params.Item
30 | } catch (err) {
31 | console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2))
32 | throw new Error("Unable to update item")
33 | }
34 | }
35 |
36 | async function scan (params) {
37 | try {
38 | const data = await Client.docClient.scan(params).promise()
39 | console.log("Scan result:", JSON.stringify(data, null, 2))
40 | return data.Items
41 | } catch (err) {
42 | console.error("Unable to scan the table. Error JSON:", JSON.stringify(err, null, 2));
43 | throw new Error("Unable to scan the table")
44 | }
45 | }
46 |
47 | async function query (params) {
48 | console.log(params)
49 | try {
50 | const data = await Client.docClient.query(params).promise()
51 | console.log("Query result:", JSON.stringify(data, null, 2))
52 | return data.Items
53 | } catch (err) {
54 | console.error("Unable to query the table. Error JSON:", JSON.stringify(err, null, 2));
55 | throw new Error("Unable to query the table")
56 | }
57 | }
58 |
59 | module.exports = {
60 | get,
61 | persist,
62 | update,
63 | scan,
64 | query
65 | }
--------------------------------------------------------------------------------
/app/src/components/CreateSession.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Mutation } from 'react-apollo'
3 | import gql from 'graphql-tag'
4 | import { SESSION_LIST_QUERY } from './SessionList'
5 |
6 | const CREATE_SESSION_MUTATION = gql`
7 | mutation CreateSessionMutation($title: String!, $description: String!) {
8 | createSession(title: $title, description: $description) {
9 | id
10 | title
11 | description
12 | createdAt
13 | }
14 | }
15 | `
16 |
17 | class CreateSession extends Component {
18 | state = {
19 | title: '',
20 | description: ''
21 | }
22 |
23 | render() {
24 | const { title, description } = this.state
25 | return (
26 |
54 | )
55 | }
56 |
57 | _updateSessionListData (store, createSession) {
58 | const data = store.readQuery({ query: SESSION_LIST_QUERY })
59 | if (data.user) {
60 | data.user.sessions.push(createSession)
61 | store.writeQuery({
62 | query: SESSION_LIST_QUERY,
63 | data
64 | })
65 | }
66 | }
67 | }
68 |
69 | export default CreateSession
--------------------------------------------------------------------------------
/app/src/components/CreateQuestion.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Mutation } from 'react-apollo'
3 | import gql from 'graphql-tag'
4 | import { SESSION_QUERY } from './Session'
5 | import { SESSION_LIST_QUERY } from './SessionList'
6 |
7 | const CREATE_QUESTION_MUTATION = gql`
8 | mutation CreateQuestionMutation($text: String!, $sessionId: ID!) {
9 | createQuestion(text: $text, sessionId: $sessionId) {
10 | id
11 | createdAt
12 | text
13 | voteCount
14 | }
15 | }
16 | `
17 |
18 | class CreateQuestion extends Component {
19 | state = {
20 | text: ''
21 | }
22 |
23 | render() {
24 | const sessionId = this.props.sessionId
25 | const { text } = this.state
26 | return (
27 |
28 |
29 | this.setState({ text: e.target.value })}
33 | type="text"
34 | placeholder="Ask question"
35 | />
36 |
37 |
{
41 | this.setState({ text: '' })
42 | }}
43 | update={(store, { data: { createQuestion } }) => {
44 | this._updateSessionData(store, createQuestion, sessionId)
45 | this._updateSessionListData(store, sessionId)
46 | }}
47 | >
48 | {createQuestionMutation => }
49 |
50 |
51 | )
52 | }
53 |
54 | _updateSessionData (store, createQuestion, sessionId) {
55 | const queryData = { query: SESSION_QUERY, variables: { id: sessionId } }
56 | const data = store.readQuery(queryData)
57 | data.session.questions.push(createQuestion)
58 | store.writeQuery({
59 | query: SESSION_QUERY,
60 | variables: { id: sessionId },
61 | data
62 | })
63 |
64 | this.props.updateQuestions(data.session.questions)
65 | }
66 |
67 | _updateSessionListData (store, sessionId) {
68 | const queryData = { query: SESSION_LIST_QUERY }
69 | const data = store.readQuery(queryData)
70 | const session = data.user.sessions.find(s => s.id === sessionId)
71 | if (session) {
72 | session.totalQuestionCount += 1
73 |
74 | store.writeQuery({
75 | query: SESSION_LIST_QUERY,
76 | data
77 | })
78 | }
79 | }
80 | }
81 |
82 | export default CreateQuestion
--------------------------------------------------------------------------------
/app/src/components/Session.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import CreateQuestion from './CreateQuestion'
3 | import QuestionItem from './QuestionItem'
4 | import { Query } from 'react-apollo'
5 | import gql from 'graphql-tag'
6 |
7 | export const SESSION_QUERY = gql`
8 | query Session ($id: ID!) {
9 | session(id: $id) {
10 | id
11 | title
12 | description
13 | createdAt
14 | postedBy {
15 | name
16 | }
17 | questions {
18 | id
19 | createdAt
20 | text
21 | voteCount
22 | }
23 | }
24 | }
25 | `
26 |
27 | class Session extends Component {
28 | constructor(props) {
29 | super(props)
30 |
31 | this.updateQuestions = this.updateQuestions.bind(this)
32 | }
33 |
34 | updateQuestions(questions) {
35 | this.setState({
36 | questions: questions
37 | })
38 | }
39 |
40 | state = {
41 | sessionExist: false,
42 | questions: []
43 | }
44 |
45 | render () {
46 | const { id } = this.props.match.params
47 | const { sessionExist, questions } = this.state
48 |
49 | return (
50 |
51 |
this.setState({
55 | sessionExist: data.session !== null,
56 | questions: data.session ? data.session.questions : []
57 | })}
58 | >
59 | {({ loading, error, data }) => {
60 | if (loading) return Fetching
61 | if (error) return Error
62 | return (sessionExist &&
63 |
64 |
{data.session.title}
65 |
by {data.session.postedBy.name} | z time ago
66 |
{data.session.description}
67 |
68 | )
69 | }}
70 |
71 |
72 | {sessionExist ?
73 | (
74 |
75 | {questions.map((item, index) => (
76 |
82 | ))}
83 |
84 |
87 |
88 | ) : (
89 |
No such session
90 | )}
91 |
92 | )
93 | }
94 | }
95 |
96 | export default Session
--------------------------------------------------------------------------------
/app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/api/src/resolver/Mutation.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs')
2 | const jwt = require('jsonwebtoken')
3 | const Authentication = require('../authentication')
4 |
5 | const SessionRepository = require('../repository/SessionRepository')
6 | const UserRepository = require('../repository/UserRepository')
7 | const QuestionRepository = require('../repository/QuestionRepository')
8 | const VoteRepository = require('../repository/VoteRepository')
9 |
10 | function createSession (parent, { title, description }, context) {
11 | const postedBy = Authentication.getEmail(context)
12 | return SessionRepository.createSession(context.repo, postedBy, title, description)
13 | }
14 |
15 | function updateSession (parent, { id, title, description }, context) {
16 | const updatedBy = Authentication.getEmail(context)
17 | return SessionRepository.updateSession(context.repo, id, updatedBy, title, description)
18 | }
19 |
20 | function createQuestion (parent, { text, sessionId }, context) {
21 | const postedBy = Authentication.getEmail(context)
22 | return QuestionRepository.createQuestion(context.repo, sessionId, postedBy, text)
23 | }
24 |
25 | async function signup (parent, { name, email, password }, context) {
26 | const Authorization = context.request.get('Authorization')
27 | if (Authorization) {
28 | throw new Error('signup - Already logged in!')
29 | }
30 |
31 | const user = await UserRepository.createUser(context.repo, name, email, password)
32 | const token = jwt.sign({ email: user.email }, Authentication.APP_SECRET)
33 |
34 | return {
35 | token,
36 | user
37 | }
38 | }
39 |
40 | async function login (parent, { email, password }, context) {
41 | const Authorization = context.request.get('Authorization')
42 | if (Authorization) {
43 | throw new Error('login - Already logged in!')
44 | }
45 |
46 | const user = await UserRepository.getUser(context.repo, email)
47 | if (!user.email) {
48 | console.log(`No such user found for ${email}`)
49 | throw new Error('login - Invalid username or password')
50 | }
51 |
52 | const valid = await bcrypt.compare(password, user.password)
53 | if (!valid) {
54 | console.log(`Invalid password for ${email}`)
55 | throw new Error('login - Invalid username or password')
56 | }
57 |
58 | const token = jwt.sign({ email: user.email }, Authentication.APP_SECRET)
59 |
60 | return {
61 | token,
62 | user
63 | }
64 | }
65 |
66 | function vote (parent, { questionId, sessionId }, context) {
67 | const postedBy = Authentication.getEmail(context)
68 | return VoteRepository.createVote(context.repo, questionId, sessionId, postedBy)
69 | }
70 |
71 | module.exports = {
72 | createSession,
73 | updateSession,
74 |
75 | createQuestion,
76 |
77 | signup,
78 | login,
79 |
80 | vote
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/components/QuestionItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { AUTH_TOKEN } from '../constants'
3 | import { Mutation } from 'react-apollo'
4 | import gql from 'graphql-tag'
5 | import { SESSION_QUERY } from './Session'
6 | import { SESSION_LIST_QUERY } from './SessionList'
7 |
8 | const VOTE_MUTATION = gql`
9 | mutation VoteMutation($questionId: ID!, $sessionId: ID!) {
10 | vote(questionId: $questionId, sessionId: $sessionId) {
11 | id
12 | }
13 | }
14 | `
15 |
16 | class QuestionItem extends Component {
17 | render () {
18 | const authToken = localStorage.getItem(AUTH_TOKEN)
19 | const questionId = this.props.item.id
20 | const sessionId = this.props.sessionId
21 |
22 | return (
23 |
24 |
25 |
{this.props.index + 1}.
26 | {authToken && (
27 |
{
31 | this._updateQuestionData(store, sessionId, questionId)
32 | this._updateSessionListData(store, sessionId)
33 | }}
34 | onError={() => {}}
35 | >
36 | {voteMutation => (
37 |
38 | ▲
39 |
40 | )}
41 |
42 | )}
43 |
44 |
45 |
46 | {this.props.item.text}
47 |
48 |
49 | {this.props.item.voteCount} votes | by hede hüde | x time ago
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | _updateQuestionData (store, sessionId, questionId) {
57 | const queryData = { query: SESSION_QUERY, variables: { id: sessionId } }
58 | const data = store.readQuery(queryData)
59 | const question = data.session.questions.find(q => q.id === questionId)
60 | if (question) {
61 | question.voteCount += 1
62 |
63 | store.writeQuery({
64 | query: SESSION_QUERY,
65 | variables: { id: sessionId },
66 | data
67 | })
68 | }
69 |
70 | this.props.updateQuestions(data.session.questions)
71 | }
72 |
73 | _updateSessionListData (store, sessionId) {
74 | const queryData = { query: SESSION_LIST_QUERY }
75 | const data = store.readQuery(queryData)
76 | const session = data.user.sessions.find(s => s.id === sessionId)
77 | if (session) {
78 | session.totalVoteCount += 1
79 |
80 | store.writeQuery({
81 | query: SESSION_LIST_QUERY,
82 | data
83 | })
84 | }
85 | }
86 | }
87 |
88 | export default QuestionItem
89 |
--------------------------------------------------------------------------------
/api/src/__test__/resolver/Mutation.test.js:
--------------------------------------------------------------------------------
1 | const Mutation = require('../../resolver/Mutation')
2 | const SessionRepository = require('../../repository/SessionRepository')
3 | const QuestionRepository = require('../../repository/QuestionRepository')
4 | const VoteRepository = require('../../repository/VoteRepository')
5 | const Authentication = require('../../authentication')
6 |
7 | describe('Mutation type resolver', () => {
8 | it('Should create session when logged in', async () => {
9 | const session = {
10 | title: 'a',
11 | description: 'b'
12 | }
13 | SessionRepository.createSession = jest.fn().mockImplementationOnce(() => session)
14 | Authentication.getEmail = jest.fn().mockImplementationOnce(() => 'user@google.com')
15 |
16 | expect(Mutation.createSession({}, { title: 'a', describe: 'b' }, {})).toEqual(session)
17 | })
18 |
19 | it('Should fail to create session when not logged in', async () => {
20 | expect(Mutation.createSession({}, { title: 'a', describe: 'b' }, {})).toEqual(undefined)
21 | })
22 |
23 | it('Should update session when logged in', async () => {
24 | const session = {
25 | id: 'session-0',
26 | title: 'a',
27 | description: 'b'
28 | }
29 | SessionRepository.updateSession = jest.fn().mockImplementationOnce(() => session)
30 | Authentication.getUserId = jest.fn().mockImplementationOnce(() => 'user@google.com')
31 |
32 | expect(Mutation.updateSession({}, { id:'session-0', title: 'a', describe: 'b' }, {})).toEqual(session)
33 | })
34 |
35 | it('Should fail to update session when not logged in', async () => {
36 | expect(Mutation.createSession({}, { id:'session-0', title: 'a', describe: 'b' }, {})).toEqual(undefined)
37 | })
38 |
39 | it('Should create question when logged in', async () => {
40 | const question = {
41 | id: 'question-0',
42 | text: 'Where is my mind?'
43 | }
44 | QuestionRepository.createQuestion = jest.fn().mockImplementationOnce(() => question)
45 | Authentication.getUserId = jest.fn().mockImplementationOnce(() => 'user-0')
46 |
47 | expect(Mutation.createQuestion({}, { text: 'Where is my mind?' }, {})).toEqual(question)
48 | })
49 |
50 | it('Should fail to create question when not logged in', async () => {
51 | expect(Mutation.createQuestion({}, { text: 'Where is my mind?' }, {})).toEqual(undefined)
52 | })
53 |
54 | it('Should create vote when logged in', async () => {
55 | const vote = {
56 | userId: 'user-0',
57 | questionId: 'question-0',
58 | sessionId: 'session-0'
59 | }
60 | VoteRepository.createVote = jest.fn().mockImplementationOnce(() => vote)
61 | Authentication.getUserId = jest.fn().mockImplementationOnce(() => 'user-0')
62 |
63 | expect(Mutation.vote({}, { questionId: 'question-0', sessionId: 'session-0' }, {})).toEqual(vote)
64 | })
65 |
66 | it('Should fail to create vote when not logged in', async () => {
67 | expect(Mutation.vote({}, { questionId: 'question-0', sessionId: 'session-0' }, {})).toEqual(undefined)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/api/src/data/dynamodb/InitTables.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk')
2 |
3 | AWS.config.update({
4 | region: "local",
5 | endpoint: "http://localhost:8000"
6 | })
7 |
8 | const dynamodb = new AWS.DynamoDB()
9 |
10 | const tables = [{
11 | TableName : "User",
12 | KeySchema: [
13 | { AttributeName: "email", KeyType: "HASH" }
14 | ],
15 | AttributeDefinitions: [
16 | { AttributeName: "email", AttributeType: "S" }
17 | ],
18 | ProvisionedThroughput: {
19 | ReadCapacityUnits: 1,
20 | WriteCapacityUnits: 1
21 | }
22 | }, {
23 | TableName : "Session",
24 | KeySchema: [
25 | { AttributeName: "id", KeyType: "HASH" }
26 | ],
27 | AttributeDefinitions: [
28 | { AttributeName: "id", AttributeType: "S" },
29 | { AttributeName: "email", AttributeType: "S" }
30 | ],
31 | ProvisionedThroughput: {
32 | ReadCapacityUnits: 1,
33 | WriteCapacityUnits: 1
34 | },
35 | GlobalSecondaryIndexes: [{
36 | IndexName: "EmailIndex",
37 | KeySchema: [{
38 | AttributeName: "email",
39 | KeyType: "HASH"
40 | }],
41 | Projection: {
42 | ProjectionType: "ALL"
43 | },
44 | ProvisionedThroughput: {
45 | ReadCapacityUnits: 1,
46 | WriteCapacityUnits: 1
47 | }
48 | }]
49 | }, {
50 | TableName : "Question",
51 | KeySchema: [
52 | { AttributeName: "id", KeyType: "HASH" },
53 | ],
54 | AttributeDefinitions: [
55 | { AttributeName: "id", AttributeType: "S" },
56 | { AttributeName: "sessionId", AttributeType: "S" }
57 | ],
58 | ProvisionedThroughput: {
59 | ReadCapacityUnits: 1,
60 | WriteCapacityUnits: 1
61 | },
62 | GlobalSecondaryIndexes: [{
63 | IndexName: "SessionIdIndex",
64 | KeySchema: [{
65 | AttributeName: "sessionId",
66 | KeyType: "HASH"
67 | }],
68 | Projection: {
69 | ProjectionType: "ALL"
70 | },
71 | ProvisionedThroughput: {
72 | ReadCapacityUnits: 1,
73 | WriteCapacityUnits: 1
74 | }
75 | }]
76 | }, {
77 | TableName : "Vote",
78 | KeySchema: [
79 | { AttributeName: "questionIdEmail", KeyType: "HASH" }
80 | ],
81 | AttributeDefinitions: [
82 | { AttributeName: "questionIdEmail", AttributeType: "S" },
83 | { AttributeName: "sessionIdEmail", AttributeType: "S" }
84 | ],
85 | ProvisionedThroughput: {
86 | ReadCapacityUnits: 1,
87 | WriteCapacityUnits: 1
88 | },
89 | GlobalSecondaryIndexes: [{
90 | IndexName: "SessionIdEmailIndex",
91 | KeySchema: [{
92 | AttributeName: "sessionIdEmail",
93 | KeyType: "HASH"
94 | }],
95 | Projection: {
96 | ProjectionType: "ALL"
97 | },
98 | ProvisionedThroughput: {
99 | ReadCapacityUnits: 1,
100 | WriteCapacityUnits: 1
101 | }
102 | }]
103 | }]
104 |
105 | tables.forEach(table => {
106 | dynamodb.createTable(table, function(err, data) {
107 | if (err) {
108 | console.error("Unable to create table. Error JSON:", JSON.stringify(err, null, 2));
109 | } else {
110 | console.log("Created table. Table description JSON:", JSON.stringify(data, null, 2));
111 | }
112 | })
113 | })
114 |
--------------------------------------------------------------------------------
/app/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { AUTH_TOKEN, EMAIL } from '../constants'
3 | import { Mutation } from 'react-apollo'
4 | import gql from 'graphql-tag'
5 |
6 | const SIGNUP_MUTATION = gql`
7 | mutation SignupMutation($email: String!, $password: String!, $name: String!) {
8 | signup(email: $email, password: $password, name: $name) {
9 | token
10 | }
11 | }
12 | `
13 |
14 | const LOGIN_MUTATION = gql`
15 | mutation LoginMutation($email: String!, $password: String!) {
16 | login(email: $email, password: $password) {
17 | token
18 | }
19 | }
20 | `
21 |
22 | class Login extends Component {
23 | state = {
24 | login: true, // switch between Login and SignUp
25 | email: '',
26 | password: '',
27 | name: '',
28 | error: false
29 | }
30 |
31 | render() {
32 | const { login, email, password, name, error } = this.state
33 |
34 | function loginDisabledCondition () {
35 | return login ?
36 | (email === '' || password === '') :
37 | (email === '' || password === '' || name === '')
38 | }
39 |
40 | return (
41 |
42 |
{login ? 'Login' : 'Sign Up'}
43 |
69 |
70 |
this._confirm(data)}
74 | onError={() => this.setState({ error: true })}
75 | >
76 | {(mutation) => (
77 |
83 | )}
84 |
85 |
this.setState({ login: !login })}
88 | >
89 | {login
90 | ? 'need to create an account?'
91 | : 'already have an account?'}
92 |
93 |
94 |
95 | )
96 | }
97 |
98 | _confirm = async data => {
99 | const { token } = this.state.login ? data.login : data.signup
100 | this._saveUserData(token)
101 | this.props.history.push(`/`)
102 | }
103 |
104 | _saveUserData = token => {
105 | localStorage.setItem(AUTH_TOKEN, token)
106 | localStorage.setItem(EMAIL, this.state.email)
107 | }
108 | }
109 |
110 | export default Login
--------------------------------------------------------------------------------
/api/src/serverless.yml:
--------------------------------------------------------------------------------
1 | service: kuestion-graphql-api
2 |
3 | plugins:
4 | - serverless-offline
5 |
6 | provider:
7 | name: aws
8 | runtime: nodejs12.x
9 | region: eu-central-1
10 | memorySize: 128
11 | timeout: 30
12 |
13 | environment:
14 | REGION: ${self:custom.dynamodb.region}
15 | DYNAMODB_ENDPOINT: ${self:custom.dynamodb.url}
16 |
17 | iamRoleStatements:
18 | - Effect: Allow
19 | Action:
20 | - "dynamodb:DescribeTable"
21 | - "dynamodb:Query"
22 | - "dynamodb:Scan"
23 | - "dynamodb:GetItem"
24 | - "dynamodb:PutItem"
25 | - "dynamodb:UpdateItem"
26 | - "dynamodb:DeleteItem"
27 | Resource:
28 | - Fn::GetAtt: [ UserTable, Arn ]
29 | - Fn::GetAtt: [ SessionTable, Arn ]
30 | - Fn::GetAtt: [ QuestionTable, Arn ]
31 | - Fn::GetAtt: [ VoteTable, Arn ]
32 | - Effect: "Allow"
33 | Action:
34 | - "logs:*"
35 | - "cloudwatch:*"
36 | - "xray:*"
37 | Resource:
38 | - "*"
39 |
40 | package:
41 | exclude:
42 | - __test__/**
43 | - data/**
44 | - local.js
45 | - .serverless
46 |
47 | layers:
48 | commonLibs:
49 | path: ../layer
50 | compatibleRuntimes:
51 | - nodejs12.x
52 |
53 | functions:
54 | graphql:
55 | handler: handler.server
56 | layers:
57 | - {Ref: CommonLibsLambdaLayer}
58 | events:
59 | - http:
60 | path: /api
61 | method: post
62 | cors: true
63 |
64 | custom:
65 | dynamodb:
66 | url: 'http://localhost:8000'
67 | region: 'local'
68 | serverless-offline:
69 | httpPort: 4000
70 |
71 | resources:
72 | Resources:
73 | UserTable:
74 | Type: AWS::DynamoDB::Table
75 | Properties:
76 | TableName: User
77 | AttributeDefinitions:
78 | - AttributeName: email
79 | AttributeType: S
80 | KeySchema:
81 | - AttributeName: email
82 | KeyType: HASH
83 | ProvisionedThroughput:
84 | ReadCapacityUnits: 1
85 | WriteCapacityUnits: 1
86 | SessionTable:
87 | Type: AWS::DynamoDB::Table
88 | Properties:
89 | TableName: Session
90 | AttributeDefinitions:
91 | - AttributeName: id
92 | AttributeType: S
93 | - AttributeName: email
94 | AttributeType: S
95 | KeySchema:
96 | - AttributeName: id
97 | KeyType: HASH
98 | ProvisionedThroughput:
99 | ReadCapacityUnits: 1
100 | WriteCapacityUnits: 1
101 | GlobalSecondaryIndexes:
102 | - IndexName: emailIndex
103 | KeySchema:
104 | - AttributeName: email
105 | KeyType: HASH
106 | Projection:
107 | ProjectionType: ALL
108 | ProvisionedThroughput:
109 | ReadCapacityUnits: 1
110 | WriteCapacityUnits: 1
111 | QuestionTable:
112 | Type: AWS::DynamoDB::Table
113 | Properties:
114 | TableName: Question
115 | AttributeDefinitions:
116 | - AttributeName: id
117 | AttributeType: S
118 | - AttributeName: sessionId
119 | AttributeType: S
120 | KeySchema:
121 | - AttributeName: id
122 | KeyType: HASH
123 | ProvisionedThroughput:
124 | ReadCapacityUnits: 1
125 | WriteCapacityUnits: 1
126 | GlobalSecondaryIndexes:
127 | - IndexName: SessionIdIndex
128 | KeySchema:
129 | - AttributeName: sessionId
130 | KeyType: HASH
131 | Projection:
132 | ProjectionType: ALL
133 | ProvisionedThroughput:
134 | ReadCapacityUnits: 1
135 | WriteCapacityUnits: 1
136 | VoteTable:
137 | Type: AWS::DynamoDB::Table
138 | Properties:
139 | TableName: Vote
140 | AttributeDefinitions:
141 | - AttributeName: questionIdEmail
142 | AttributeType: S
143 | - AttributeName: sessionIdEmail
144 | AttributeType: S
145 | KeySchema:
146 | - AttributeName: questionIdEmail
147 | KeyType: HASH
148 | ProvisionedThroughput:
149 | ReadCapacityUnits: 1
150 | WriteCapacityUnits: 1
151 | GlobalSecondaryIndexes:
152 | - IndexName: SessionIdEmailIndex
153 | KeySchema:
154 | - AttributeName: sessionIdEmail
155 | KeyType: HASH
156 | Projection:
157 | ProjectionType: ALL
158 | ProvisionedThroughput:
159 | ReadCapacityUnits: 1
160 | WriteCapacityUnits: 1
--------------------------------------------------------------------------------
/app/src/serviceWorker.js:
--------------------------------------------------------------------------------
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 https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------