Initial commit
This commit is contained in:
7
server/.gitignore
vendored
Normal file
7
server/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.env*
|
||||
dist
|
||||
package-lock.json
|
||||
node_modules
|
||||
.idea
|
||||
.vscode
|
||||
*.log
|
||||
3
server/README.md
Normal file
3
server/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# hackernews-graphql-js
|
||||
|
||||
This repository contains the final project for the [**GraphQL.js tutorial**](https://www.howtographql.com/graphql-js/0-introduction/) on [How to GraphQL](https://www.howtographql.com/). Note that it also serves as foundation for all frontend tutorials on the site.
|
||||
19
server/package.json
Normal file
19
server/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "hackernews-node",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^2.12.1",
|
||||
"apollo-server": "^2.19.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"jsonwebtoken": "8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/cli": "^2.12.1",
|
||||
"nodemon": "^2.0.6"
|
||||
}
|
||||
}
|
||||
BIN
server/prisma/dev.db
Normal file
BIN
server/prisma/dev.db
Normal file
Binary file not shown.
87
server/prisma/migrations/20201125185150-init/README.md
Normal file
87
server/prisma/migrations/20201125185150-init/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Migration `20201125185150-init`
|
||||
|
||||
This migration has been generated by Ryan Chenkie at 11/25/2020, 1:51:50 PM.
|
||||
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||
|
||||
## Database Steps
|
||||
|
||||
```sql
|
||||
CREATE TABLE "Link" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"description" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"postedById" INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)
|
||||
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL
|
||||
)
|
||||
|
||||
CREATE TABLE "Vote" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"linkId" INTEGER NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY ("linkId") REFERENCES "Link"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)
|
||||
|
||||
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
|
||||
|
||||
CREATE UNIQUE INDEX "Vote.linkId_userId_unique" ON "Vote"("linkId", "userId")
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
```diff
|
||||
diff --git schema.prisma schema.prisma
|
||||
migration ..20201125185150-init
|
||||
--- datamodel.dml
|
||||
+++ datamodel.dml
|
||||
@@ -1,0 +1,37 @@
|
||||
+datasource db {
|
||||
+ provider = "sqlite"
|
||||
+ url = "***"
|
||||
+}
|
||||
+
|
||||
+generator client {
|
||||
+ provider = "prisma-client-js"
|
||||
+}
|
||||
+
|
||||
+model Link {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ createdAt DateTime @default(now())
|
||||
+ description String
|
||||
+ url String
|
||||
+ postedBy User @relation(fields: [postedById], references: [id])
|
||||
+ postedById Int
|
||||
+ votes Vote[]
|
||||
+}
|
||||
+
|
||||
+model User {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ name String
|
||||
+ email String @unique
|
||||
+ password String
|
||||
+ links Link[]
|
||||
+ votes Vote[]
|
||||
+}
|
||||
+
|
||||
+model Vote {
|
||||
+ id Int @id @default(autoincrement())
|
||||
+ link Link @relation(fields: [linkId], references: [id])
|
||||
+ linkId Int
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
+ userId Int
|
||||
+
|
||||
+ @@unique([linkId, userId])
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
37
server/prisma/migrations/20201125185150-init/schema.prisma
Normal file
37
server/prisma/migrations/20201125185150-init/schema.prisma
Normal file
@@ -0,0 +1,37 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "***"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Link {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
description String
|
||||
url String
|
||||
postedBy User @relation(fields: [postedById], references: [id])
|
||||
postedById Int
|
||||
votes Vote[]
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String @unique
|
||||
password String
|
||||
links Link[]
|
||||
votes Vote[]
|
||||
}
|
||||
|
||||
model Vote {
|
||||
id Int @id @default(autoincrement())
|
||||
link Link @relation(fields: [linkId], references: [id])
|
||||
linkId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
@@unique([linkId, userId])
|
||||
}
|
||||
442
server/prisma/migrations/20201125185150-init/steps.json
Normal file
442
server/prisma/migrations/20201125185150-init/steps.json
Normal file
@@ -0,0 +1,442 @@
|
||||
{
|
||||
"version": "0.3.14-fixed",
|
||||
"steps": [
|
||||
{
|
||||
"tag": "CreateSource",
|
||||
"source": "db"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "db"
|
||||
},
|
||||
"argument": "provider",
|
||||
"value": "\"sqlite\""
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Source",
|
||||
"source": "db"
|
||||
},
|
||||
"argument": "url",
|
||||
"value": "\"***\""
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Link"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "createdAt",
|
||||
"type": "DateTime",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "createdAt"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "now()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "description",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "url",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "postedBy",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "postedBy"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "postedBy"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[postedById]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Link",
|
||||
"field": "postedBy"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "postedById",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Link",
|
||||
"field": "votes",
|
||||
"type": "Vote",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "User"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "name",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "email",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "User",
|
||||
"field": "email"
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "password",
|
||||
"type": "String",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "links",
|
||||
"type": "Link",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "User",
|
||||
"field": "votes",
|
||||
"type": "Vote",
|
||||
"arity": "List"
|
||||
},
|
||||
{
|
||||
"tag": "CreateModel",
|
||||
"model": "Vote"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Vote",
|
||||
"field": "id",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "id"
|
||||
},
|
||||
"directive": "default"
|
||||
},
|
||||
"argument": "",
|
||||
"value": "autoincrement()"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Vote",
|
||||
"field": "link",
|
||||
"type": "Link",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "link"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "link"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[linkId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "link"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Vote",
|
||||
"field": "linkId",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Vote",
|
||||
"field": "user",
|
||||
"type": "User",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "fields",
|
||||
"value": "[userId]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateArgument",
|
||||
"location": {
|
||||
"tag": "Directive",
|
||||
"path": {
|
||||
"tag": "Field",
|
||||
"model": "Vote",
|
||||
"field": "user"
|
||||
},
|
||||
"directive": "relation"
|
||||
},
|
||||
"argument": "references",
|
||||
"value": "[id]"
|
||||
},
|
||||
{
|
||||
"tag": "CreateField",
|
||||
"model": "Vote",
|
||||
"field": "userId",
|
||||
"type": "Int",
|
||||
"arity": "Required"
|
||||
},
|
||||
{
|
||||
"tag": "CreateDirective",
|
||||
"location": {
|
||||
"path": {
|
||||
"tag": "Model",
|
||||
"model": "Vote",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "",
|
||||
"value": "[linkId, userId]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"directive": "unique"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
server/prisma/migrations/migrate.lock
Normal file
3
server/prisma/migrations/migrate.lock
Normal file
@@ -0,0 +1,3 @@
|
||||
# Prisma Migrate lockfile v1
|
||||
|
||||
20201125185150-init
|
||||
37
server/prisma/schema.prisma
Normal file
37
server/prisma/schema.prisma
Normal file
@@ -0,0 +1,37 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Link {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
description String
|
||||
url String
|
||||
postedBy User @relation(fields: [postedById], references: [id])
|
||||
postedById Int
|
||||
votes Vote[]
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String @unique
|
||||
password String
|
||||
links Link[]
|
||||
votes Vote[]
|
||||
}
|
||||
|
||||
model Vote {
|
||||
id Int @id @default(autoincrement())
|
||||
link Link @relation(fields: [linkId], references: [id])
|
||||
linkId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
@@unique([linkId, userId])
|
||||
}
|
||||
68
server/src/index.js
Normal file
68
server/src/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { ApolloServer, PubSub } = require('apollo-server');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const Query = require('./resolvers/Query');
|
||||
const Mutation = require('./resolvers/Mutation');
|
||||
const Subscription = require('./resolvers/Subscription');
|
||||
const User = require('./resolvers/User');
|
||||
const Link = require('./resolvers/Link');
|
||||
const Vote = require('./resolvers/Vote');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getUserId } = require('./utils');
|
||||
|
||||
const pubsub = new PubSub();
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
errorFormat: 'minimal'
|
||||
});
|
||||
|
||||
const resolvers = {
|
||||
Query,
|
||||
Mutation,
|
||||
Subscription,
|
||||
User,
|
||||
Link,
|
||||
Vote
|
||||
};
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs: fs.readFileSync(
|
||||
path.join(__dirname, 'schema.graphql'),
|
||||
'utf8'
|
||||
),
|
||||
resolvers,
|
||||
context: ({ req }) => {
|
||||
return {
|
||||
...req,
|
||||
prisma,
|
||||
pubsub,
|
||||
userId:
|
||||
req && req.headers.authorization
|
||||
? getUserId(req)
|
||||
: null
|
||||
};
|
||||
},
|
||||
subscriptions: {
|
||||
onConnect: (connectionParams) => {
|
||||
if (connectionParams.authToken) {
|
||||
return {
|
||||
prisma,
|
||||
userId: getUserId(
|
||||
null,
|
||||
connectionParams.authToken
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
prisma
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server
|
||||
.listen()
|
||||
.then(({ url }) =>
|
||||
console.log(`Server is running on ${url}`)
|
||||
);
|
||||
16
server/src/resolvers/Link.js
Normal file
16
server/src/resolvers/Link.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function postedBy(parent, args, context) {
|
||||
return context.prisma.link
|
||||
.findUnique({ where: { id: parent.id } })
|
||||
.postedBy();
|
||||
}
|
||||
|
||||
function votes(parent, args, context) {
|
||||
return context.prisma.link
|
||||
.findUnique({ where: { id: parent.id } })
|
||||
.votes();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postedBy,
|
||||
votes
|
||||
};
|
||||
91
server/src/resolvers/Mutation.js
Normal file
91
server/src/resolvers/Mutation.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { APP_SECRET } = require('../utils');
|
||||
|
||||
async function post(parent, args, context, info) {
|
||||
const { userId } = context;
|
||||
|
||||
const newLink = await context.prisma.link.create({
|
||||
data: {
|
||||
url: args.url,
|
||||
description: args.description,
|
||||
postedBy: { connect: { id: userId } }
|
||||
}
|
||||
});
|
||||
context.pubsub.publish('NEW_LINK', newLink);
|
||||
|
||||
return newLink;
|
||||
}
|
||||
|
||||
async function signup(parent, args, context, info) {
|
||||
const password = await bcrypt.hash(args.password, 10);
|
||||
const user = await context.prisma.user.create({
|
||||
data: { ...args, password }
|
||||
});
|
||||
|
||||
const token = jwt.sign({ userId: user.id }, APP_SECRET);
|
||||
|
||||
return {
|
||||
token,
|
||||
user
|
||||
};
|
||||
}
|
||||
|
||||
async function login(parent, args, context, info) {
|
||||
const user = await context.prisma.user.findUnique({
|
||||
where: { email: args.email }
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('No such user found');
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(
|
||||
args.password,
|
||||
user.password
|
||||
);
|
||||
if (!valid) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: user.id }, APP_SECRET);
|
||||
|
||||
return {
|
||||
token,
|
||||
user
|
||||
};
|
||||
}
|
||||
|
||||
async function vote(parent, args, context, info) {
|
||||
const { userId } = context;
|
||||
const vote = await context.prisma.vote.findUnique({
|
||||
where: {
|
||||
linkId_userId: {
|
||||
linkId: Number(args.linkId),
|
||||
userId: userId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Boolean(vote)) {
|
||||
throw new Error(
|
||||
`Already voted for link: ${args.linkId}`
|
||||
);
|
||||
}
|
||||
|
||||
const newVote = context.prisma.vote.create({
|
||||
data: {
|
||||
user: { connect: { id: userId } },
|
||||
link: { connect: { id: Number(args.linkId) } }
|
||||
}
|
||||
});
|
||||
context.pubsub.publish('NEW_VOTE', newVote);
|
||||
|
||||
return newVote;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
post,
|
||||
signup,
|
||||
login,
|
||||
vote
|
||||
};
|
||||
29
server/src/resolvers/Query.js
Normal file
29
server/src/resolvers/Query.js
Normal file
@@ -0,0 +1,29 @@
|
||||
async function feed(parent, args, context, info) {
|
||||
const where = args.filter
|
||||
? {
|
||||
OR: [
|
||||
{ description: { contains: args.filter } },
|
||||
{ url: { contains: args.filter } }
|
||||
]
|
||||
}
|
||||
: {};
|
||||
|
||||
const links = await context.prisma.link.findMany({
|
||||
where,
|
||||
skip: args.skip,
|
||||
take: args.take,
|
||||
orderBy: args.orderBy
|
||||
});
|
||||
|
||||
const count = await context.prisma.link.count({ where });
|
||||
|
||||
return {
|
||||
id: 'main-feed',
|
||||
links,
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
feed
|
||||
};
|
||||
26
server/src/resolvers/Subscription.js
Normal file
26
server/src/resolvers/Subscription.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function newLinkSubscribe(parent, args, context, info) {
|
||||
return context.pubsub.asyncIterator("NEW_LINK")
|
||||
}
|
||||
|
||||
const newLink = {
|
||||
subscribe: newLinkSubscribe,
|
||||
resolve: payload => {
|
||||
return payload
|
||||
},
|
||||
}
|
||||
|
||||
function newVoteSubscribe(parent, args, context, info) {
|
||||
return context.pubsub.asyncIterator("NEW_VOTE")
|
||||
}
|
||||
|
||||
const newVote = {
|
||||
subscribe: newVoteSubscribe,
|
||||
resolve: payload => {
|
||||
return payload
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
newLink,
|
||||
newVote
|
||||
}
|
||||
9
server/src/resolvers/User.js
Normal file
9
server/src/resolvers/User.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function links(parent, args, context) {
|
||||
return context.prisma.user
|
||||
.findUnique({ where: { id: parent.id } })
|
||||
.links();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
links
|
||||
};
|
||||
16
server/src/resolvers/Vote.js
Normal file
16
server/src/resolvers/Vote.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function link(parent, args, context) {
|
||||
return context.prisma.vote
|
||||
.findUnique({ where: { id: parent.id } })
|
||||
.link();
|
||||
}
|
||||
|
||||
function user(parent, args, context) {
|
||||
return context.prisma.vote
|
||||
.findUnique({ where: { id: parent.id } })
|
||||
.user();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
link,
|
||||
user
|
||||
};
|
||||
71
server/src/schema.graphql
Normal file
71
server/src/schema.graphql
Normal file
@@ -0,0 +1,71 @@
|
||||
type Query {
|
||||
info: String!
|
||||
feed(
|
||||
filter: String
|
||||
skip: Int
|
||||
take: Int
|
||||
orderBy: LinkOrderByInput
|
||||
): Feed!
|
||||
}
|
||||
|
||||
type Feed {
|
||||
id: ID!
|
||||
links: [Link!]!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
post(url: String!, description: String!): Link!
|
||||
signup(
|
||||
email: String!
|
||||
password: String!
|
||||
name: String!
|
||||
): AuthPayload
|
||||
login(email: String!, password: String!): AuthPayload
|
||||
vote(linkId: ID!): Vote
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
newLink: Link
|
||||
newVote: Vote
|
||||
}
|
||||
|
||||
type AuthPayload {
|
||||
token: String
|
||||
user: User
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
name: String!
|
||||
email: String!
|
||||
links: [Link!]!
|
||||
}
|
||||
|
||||
type Link {
|
||||
id: ID!
|
||||
description: String!
|
||||
url: String!
|
||||
postedBy: User
|
||||
votes: [Vote!]!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Vote {
|
||||
id: ID!
|
||||
link: Link!
|
||||
user: User!
|
||||
}
|
||||
|
||||
input LinkOrderByInput {
|
||||
description: Sort
|
||||
url: Sort
|
||||
createdAt: Sort
|
||||
}
|
||||
|
||||
enum Sort {
|
||||
asc
|
||||
desc
|
||||
}
|
||||
|
||||
scalar DateTime
|
||||
30
server/src/utils.js
Normal file
30
server/src/utils.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const APP_SECRET = 'GraphQL-is-aw3some';
|
||||
|
||||
function getTokenPayload(token) {
|
||||
return jwt.verify(token, APP_SECRET);
|
||||
}
|
||||
|
||||
function getUserId(req, authToken) {
|
||||
if (req) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader) {
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
throw new Error('No token found');
|
||||
}
|
||||
const { userId } = getTokenPayload(token);
|
||||
return userId;
|
||||
}
|
||||
} else if (authToken) {
|
||||
const { userId } = getTokenPayload(authToken);
|
||||
return userId;
|
||||
}
|
||||
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APP_SECRET,
|
||||
getUserId
|
||||
};
|
||||
2181
server/yarn.lock
Normal file
2181
server/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user