Skip to content

Instantly share code, notes, and snippets.

@matisiekpl
Created October 13, 2018 19:52
Show Gist options
  • Save matisiekpl/57e1934850ab2827a42e1250803a1529 to your computer and use it in GitHub Desktop.
Save matisiekpl/57e1934850ab2827a42e1250803a1529 to your computer and use it in GitHub Desktop.
// My schema for E-learning system for my school
directive @isAuthenticated on FIELD | FIELD_DEFINITION
directive @hasRole(role: String) on FIELD | FIELD_DEFINITION
directive @permit(code: String) on QUERY | FIELD | FIELD_DEFINITION
directive @private on FIELD | FIELD_DEFINITION
enum ContestType {
PRIVATE,
PUBLIC
}
enum AnswerType {
PROGRAM
}
enum AnswerStatus {
CREATED, PENDING, SUCCESS, FAILURE
}
enum Role {
MEMBER, ADMIN
}
type ProgramAnswerResult {
time: Int!
}
type ProgramTestCase {
maxTime: Int!
maxCpuUsage: Int!
input: String!
output: String!
}
type File {
url: String!
}
type Answer {
id: ID! @unique
sender: User!
message: String!
files: [File!]!
status: AnswerStatus!
programTestCase: [ProgramTestCase!]!
}
type Task {
id: ID! @unique
name: String!
description: String!
answerType: AnswerType!
end: Int!
created: Int!
updated: Int!
answers: [Answer!]!
}
type Contest {
id: ID! @unique
start: Int!
end: Int!
name: String!
description: String
type: ContestType!
tasks: [Task!]!
}
type Message {
id: ID! @unique
author: User!
content: String!
created: Int!
}
type Chat {
id: ID! @unique
name: String!
messages: [Message!]!
}
type Comment {
id: ID! @unique
content: String!
created: Int!
updated: Int!
author: User!
}
type Post {
id: ID! @unique
title: String!
content: String!
created: Int!
updated: Int!
author: User!
comments: [Comment!]!
}
type Blog {
id: ID! @unique
name: String!
description: String
posts: [Post!]!
}
type Class {
id: ID! @unique
name: String!
start: Int!
end: Int!
members: [User!]!
contests: [Contest!]!
blogs: [Blog!]!
chats: [Chat!]!
}
type User {
id: ID! @unique
email: String! @unique
password: String! @private
name: String!
roles: [Role!]!
}
type AuthPayload {
token: String
user: User
status: Boolean!
}
import { GraphQLServer, PubSub } from 'graphql-yoga'
import { importSchema } from 'graphql-import'
import { Prisma, Query, Mutation, Subscription } from './generated/prisma'
import { Context } from './utils'
import { readFileSync } from 'fs';
import { join } from 'path';
import { makeExecutableSchema } from 'graphql-tools';
import { mergeTypes } from 'merge-graphql-schemas';
import { signInResolver, signUpResolver } from './resolvers/auth';
import { runInNewContext } from 'vm';
import { extractFragmentReplacements } from 'prisma-binding';
import { parse } from 'graphql';
// Some code, that is required to make resolvers callable on fields
export function addFragmentToFieldResolvers(schemaAST, fragmentSelection) {
return schemaAST.definitions.reduce((result, schemaDefinition) => {
if (schemaDefinition.kind === 'ObjectTypeDefinition') {
return {
...result,
[schemaDefinition.name.value]: schemaDefinition.fields.reduce((result, fieldDefinition) => {
//TODO this includes check is naive and will break for some strings
if (fragmentSelection.includes(fieldDefinition.name.value)) {
return result;
}
return {
...result,
[fieldDefinition.name.value]: {
fragment: `fragment Fragment on ${schemaDefinition.name.value} ${fragmentSelection}`,
resolve: (parent, args, context, info) => {
return parent[fieldDefinition.name.value];
}
}
};
}, {})
};
}
else {
return result;
}
}, {});
}
// This function is copying resolvers from Prisma auto-generated SDK into our app
export function prepareTopLevelResolvers(resolverObject: Query | Mutation | Subscription, isSubscription: boolean = false) {
return Object.entries(resolverObject).reduce((result, entry) => {
const resolverName = entry[0];
const resolverFunction = entry[1];
if (isSubscription) {
return {
...result,
[resolverName]: {
subscribe: async (parent, args, context, info) => {
return await resolverFunction(args, info);
}
}
};
} else {
return {
...result,
[resolverName]: async (parent, args, context, info) => {
return await resolverFunction(args, info);
}
};
}
}, {});
}
const preparedFieldResolvers = addFragmentToFieldResolvers(parse(readFileSync('./database/datamodel.graphql').toString()), `{ id }`)
const generatedFragmentReplacements = extractFragmentReplacements(preparedFieldResolvers);
// Creating new instance of Prisma - here on test server
const db = new Prisma({
endpoint: 'https://eu1.prisma.sh/public-bevelreaper-37/funtest/dev', // the endpoint of the Prisma API
debug: false, // log all GraphQL queries & mutations sent to the Prisma API
fragmentReplacements: generatedFragmentReplacements
});
// Merging Prisma auto-generated resolvers with our custom resolvers
const resolvers = {
Query: {
...prepareTopLevelResolvers(db.query)
},
Mutation: {
...prepareTopLevelResolvers(db.mutation),
signIn: signInResolver,
signUp: signUpResolver
},
Subscription: {
...prepareTopLevelResolvers(db.subscription, true)
},
...preparedFieldResolvers
}
// Annotations in GraphQL are named as "Directives" - here you can observe the @private directive, that will throw exception, when you will go to get private field - for example - password field in User type
// Permit directive is a special directive, that requires "code" as a param and it executes code in nodejs vm (solution inspired from Firebase security rules)
const directiveResolvers = {
async hasRole(next, source, { role }, ctx) {
},
async permit(next, source, { code }, ctx) {
const sandbox = { ctx, resurce: source };
const result = runInNewContext('async () => ' + code, sandbox);
console.log(await result());
if (await result()) return await next();
throw new Error('Operation not permitted');
},
private(next, source, args, ctx) {
throw new Error('Given field is private');
}
}
// Merging fields
const ultimateSchemaString = mergeTypes([
readFileSync(join(__dirname, './generated/prisma.graphql')).toString(),
readFileSync(join(__dirname, './../database/datamodel.graphql')).toString(),
readFileSync(join(__dirname, './schema.graphql')).toString()
], {
all: true
});
// Constructing final schema
const ultimateSchema = makeExecutableSchema({
typeDefs: ultimateSchemaString,
resolvers,
directiveResolvers
});
// Running the server
const server = new GraphQLServer({
schema: ultimateSchema,
context: req => ({
...req,
db
}),
})
server.start(() => console.log('Server is running on http://localhost:4000'))
// Custom schema, that should not be migrated on Prisma server
type Mutation {
signUp(email: String!, password: String!, name: String!): AuthPayload!
signIn(email: String!, password: String!): AuthPayload!
createUser(data: UserCreateInput!): User! @isAuthenticated
}
type Query {
users: [User!]!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment