Skip to content
This repository has been archived by the owner on Oct 21, 2020. It is now read-only.

Commit

Permalink
feat: add tiny GraphQL Lambda
Browse files Browse the repository at this point in the history
ojongerius committed Apr 17, 2018
1 parent 263b0a4 commit 5441b4c
Showing 11 changed files with 10,588 additions and 5,430 deletions.
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -58,4 +58,11 @@ typings/
.env

# next.js build output
.next
.next

# Webpack
.webpack

# Serverless
.serverless
.webpack
21 changes: 21 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const mongoose = require("mongoose");
const bluebird = require("bluebird");
mongoose.Promise = bluebird;
mongoose.Promise = global.Promise;

// Only reconnect if needed. State is saved and outlives a handler invocation
let isConnected;

const connectToDatabase = () => {
if (isConnected) {
console.log("Re-using existing database connection");
return Promise.resolve();
}

console.log("Creating new database connection");
return mongoose.connect(process.env.MONGODB_URL).then(db => {
isConnected = db.connections[0].readyState;
});
};

module.exports = connectToDatabase;
58 changes: 58 additions & 0 deletions handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { graphqlLambda, graphiqlLambda } from "apollo-server-lambda";
import lambdaPlayground from "graphql-playground-middleware-lambda";
import { makeExecutableSchema } from "graphql-tools";
import { mergeResolvers, mergeTypes } from "merge-graphql-schemas";
import { userType } from "./types/user";
import { userResolver } from "./resolvers/user";

const types = mergeTypes([userType]);
const solvers = mergeResolvers([userResolver]);
const graphqlSchema = makeExecutableSchema({
typeDefs: types,
resolvers: solvers,
logger: console
});

// Database connection logic lives outside of the handler for performance reasons
const connectToDatabase = require("./db");

const server = require("apollo-server-lambda");

exports.graphqlHandler = function graphqlHandler(event, context, callback) {
/* Cause Lambda to freeze the process and save state data after
the callback is called the effect is that new handler invocations
will be able to re-use the database connection.
See https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
and https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs */
context.callbackWaitsForEmptyEventLoop = false;

function callbackFilter(error, output) {
if (!output.headers) {
output.headers = {};
}
// eslint-disable-next-line no-param-reassign
output.headers["Access-Control-Allow-Origin"] = "*";
output.headers["Access-Control-Allow-Credentials"] = true;
output.headers["Content-Type"] = "application/json";

callback(error, output);
}

const handler = server.graphqlLambda({ schema: graphqlSchema });

connectToDatabase()
.then(() => {
return handler(event, context, callbackFilter);
})
.catch(err => {
console.log("MongoDB connection error: ", err);
// TODO: return 500?
process.exit();
});
};

exports.apiHandler = lambdaPlayground({
endpoint: process.env.GRAPHQL_ENDPOINT_URL
? process.env.GRAPHQL_ENDPOINT_URL
: "/production/graphql"
});
268 changes: 268 additions & 0 deletions model/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
const mongoose = require("mongoose");
const validator = require("validator");

const Schema = mongoose.Schema;
const SchemaTypes = Schema.Types;

const userSchema = new Schema({
email: {
type: "string"
},
newEmail: {
type: "string"
},
emailVerifyTTL: {
type: "date"
},
emailVerified: {
type: "boolean",
default: false
},
emailAuthLinkTTL: {
type: "date"
},
password: {
type: "string"
},
progressTimestamps: {
type: "array",
default: []
},
isBanned: {
type: "boolean",
description: "User is banned from posting to camper news",
default: false
},
isCheater: {
type: "boolean",
description:
"Users who are confirmed to have broken academic honesty policy are marked as cheaters",
default: false
},
isGithubCool: {
type: "boolean",
default: false
},
githubId: {
type: "string"
},
githubURL: {
type: "string"
},
githubEmail: {
type: "string"
},
joinedGithubOn: {
type: "date"
},
website: {
type: "string"
},
githubProfile: {
type: "string"
},
_csrf: {
type: "string"
},
isMigrationGrandfathered: {
type: "boolean",
default: false
},
username: {
type: "string"
},
bio: {
type: "string",
default: ""
},
about: {
type: "string",
default: ""
},
name: {
type: "string",
default: ""
},
gender: {
type: "string",
default: ""
},
location: {
type: "string",
default: ""
},
picture: {
type: "string",
default: ""
},
linkedin: {
type: "string"
},
codepen: {
type: "string"
},
twitter: {
type: "string"
},
currentStreak: {
type: "number",
default: 0
},
longestStreak: {
type: "number",
default: 0
},
sendMonthlyEmail: {
type: "boolean",
default: true
},
sendNotificationEmail: {
type: "boolean",
default: true
},
sendQuincyEmail: {
type: "boolean",
default: true
},
isLocked: {
type: "boolean",
description:
"Campers profile does not show challenges/certificates to the public",
default: false
},
currentChallengeId: {
type: "string",
description: "The challenge last visited by the user",
default: ""
},
currentChallenge: {
type: {},
description: "deprecated"
},
isUniqMigrated: {
type: "boolean",
description: "Campers completedChallenges array is free of duplicates",
default: false
},
isHonest: {
type: "boolean",
description: "Camper has signed academic honesty policy",
default: false
},
isFrontEndCert: {
type: "boolean",
description: "Camper is front end certified",
default: false
},
isDataVisCert: {
type: "boolean",
description: "Camper is data visualization certified",
default: false
},
isBackEndCert: {
type: "boolean",
description: "Campers is back end certified",
default: false
},
isFullStackCert: {
type: "boolean",
description: "Campers is full stack certified",
default: false
},
isRespWebDesignCert: {
type: "boolean",
description: "Camper is responsive web design certified",
default: false
},
is2018DataVisCert: {
type: "boolean",
description: "Camper is data visualization certified (2018)",
default: false
},
isFrontEndLibsCert: {
type: "boolean",
description: "Camper is front end libraries certified",
default: false
},
isJsAlgoDataStructCert: {
type: "boolean",
description:
"Camper is javascript algorithms and data structures certified",
default: false
},
isApisMicroservicesCert: {
type: "boolean",
description: "Camper is apis and microservices certified",
default: false
},
isInfosecQaCert: {
type: "boolean",
description:
"Camper is information security and quality assurance certified",
default: false
},
isChallengeMapMigrated: {
type: "boolean",
description: "Migrate completedChallenges array to challenge map",
default: false
},
challengeMap: {
type: "object",
description: "A map by ID of all the user completed challenges",
default: {}
},
completedChallenges: {
type: [
{
completedDate: "number",
lastUpdated: "number",
numOfAttempts: "number",
id: "string",
name: "string",
completedWith: "string",
solution: "string",
githubLink: "string",
verified: "boolean",
challengeType: {
type: "number",
default: 0
}
}
],
default: []
},
portfolio: {
type: "array",
default: []
},
rand: {
type: "number",
index: true
},
tshirtVote: {
type: "number"
},
timezone: {
type: "string"
},
theme: {
type: "string",
default: "default"
},
languageTag: {
type: "string",
description: "An IETF language tag",
default: "en"
},
badges: {
type: {
coreTeam: {
type: "array",
default: []
}
},
default: {}
}
});

module.exports = mongoose.model("User", userSchema, "user");
42 changes: 42 additions & 0 deletions mongo/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const validator = require("validator");
const UserModel = require("../model/user.js");
const moment = require("moment");
const fs = require("fs");

const createErrorResponse = (statusCode, message) => ({
statusCode: statusCode || 501,
headers: { "Content-Type": "text-plain" },
body: message || "Incorrect id"
});

export function getUsers(args) {
return new Promise((resolve, reject) => {
UserModel.find(args)
.then(users => {
let userMap = [];
users.forEach((user, index) => {
userMap[index] = user;
});
let res = {};
res.users = userMap;
resolve(res.users);
})
.catch(err => {
reject(err);
});
});
}

export function createUser(args) {
return new Promise((resolve, reject) => {
// Needs rewriting / updating
const userModel = new UserModel(args); // Would need validation
const newUser = userModel.save();

if (!newUser) {
reject("Failed to add user");
}

resolve(newUser);
});
}
15,478 changes: 10,052 additions & 5,426 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,10 @@
"precommit": "lint-staged",
"commit": "git-cz",
"commitmsg": "commitlint -e",
"deploy-dev": "serverless --stage=dev deploy",
"deploy-prod": "serverless --stage=prod deploy",
"prepare-production": "snyk protect",
"start": "serverless offline start --skipCacheInvalidation",
"test": "snyk test"
},
"config": {
@@ -28,18 +31,37 @@
"config": "commitizen.config.js"
}
},
"dependencies": {},
"dependencies": {
"ajv": "^6.4.0",
"apollo-server-lambda": "^1.3.4",
"aws-lambda": "^0.1.2",
"bluebird": "^3.5.1",
"graphql": "0.13.2",
"graphql-playground-middleware-lambda": "^1.5.1",
"graphql-tools": "2.24.0",
"merge-graphql-schemas": "1.5.1",
"moment": "^2.20.1",
"mongoose": "^5.0.12",
"serverless-offline-scheduler": "^0.3.3",
"validator": "^9.4.1",
"webpack-node-externals": "^1.7.2"
},
"devDependencies": {
"@commitlint/cli": "^6.1.3",
"@commitlint/config-conventional": "^6.1.3",
"@commitlint/travis-cli": "^6.1.3",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"commitizen": "^2.9.6",
"cz-customizable": "^5.2.0",
"husky": "^0.14.3",
"lint-staged": "^7.0.3",
"lint-staged": "^7.0.4",
"prettier": "1.11.1",
"prettier-package-json": "^1.5.1",
"snyk": "^1.70.3"
"serverless-offline": "^3.20.0",
"serverless-webpack": "^5.1.1",
"snyk": "^1.71.0",
"webpack": "^4.5.0"
},
"keywords": ["open-api"],
"lint-staged": {
10 changes: 10 additions & 0 deletions resolvers/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as dbUsers from "../mongo/user";

export const userResolver = {
Query: {
users: (_, args) => dbUsers.getUsers(args)
},
Mutation: {
createUser: (_, args) => dbUsers.createUser(args)
}
};
46 changes: 46 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
service: serverless-graphql-apollo

frameworkVersion: ">=1.21.0 <2.0.0"

provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
environment:
MONGODB_URL: ${env:MONGODB_URL}
GRAPHQL_ENDPOINT_URL: ${env:GRAPHQL_ENDPOINT_URL}
iamRoleStatements:
- Effect: Allow
Action:
- s3:*
Resource: "*"

plugins:
- serverless-webpack
- serverless-offline-scheduler
- serverless-offline

custom:
serverless-offline:
port: 4000
webpack:
webpackConfig: ./webpack.config.js
includeModules: true

functions:
graphql:
handler: handler.graphqlHandler
events:
- http:
path: graphql
method: post
cors: true

api:
handler: handler.apiHandler
events:
- http:
path: api
method: get
cors: true
26 changes: 26 additions & 0 deletions types/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const userType = `
type Query {
users(
_id: ID
name: String
email: String
): [User]
}
type Mutation {
createUser(
_id: ID
name: String
email: String
): User
}
type User {
_id: ID!
email: String
name: String
}
`;

export { userType };
32 changes: 32 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");

module.exports = {
entry: slsw.lib.entries,
target: "node",
// Generate sourcemaps for proper error messages
devtool: "source-map",
// Since 'aws-sdk' is not compatible with webpack,
// we exclude all node dependencies
externals: [nodeExternals()],
mode: slsw.lib.webpack.isLocal ? "development" : "production",
optimization: {
// We no not want to minimize our code.
minimize: false
},
performance: {
// Turn off size warnings for entry points
hints: false
},
// Run babel on all .js files and skip those in node_modules
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: __dirname,
exclude: /node_modules/
}
]
}
};

0 comments on commit 5441b4c

Please sign in to comment.