Skip to content

Commit e5461bd

Browse files
committed
Initial commit.
1 parent 4d627d1 commit e5461bd

29 files changed

+6688
-0
lines changed

.editorconfig

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# EditorConfig helps developers define and maintain consistent
2+
# coding styles between different editors and IDEs
3+
# editorconfig.org
4+
5+
root = true
6+
7+
[*]
8+
9+
# Change these settings to your own preference
10+
indent_style = space
11+
indent_size = 2
12+
13+
# We recommend you to keep these unchanged
14+
end_of_line = lf
15+
charset = utf-8
16+
trim_trailing_whitespace = true
17+
insert_final_newline = true
18+
19+
[*.md]
20+
trim_trailing_whitespace = false

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DB_HOST=127.0.0.1
2+
DB_USER=user
3+
DB_PASSWORD=secret
4+
DB_DATABASE=mydatabase

.eslintrc

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"extends": "airbnb-base",
3+
"globals": {
4+
"describe": false,
5+
"it": false,
6+
"beforeAll": false,
7+
"beforeEach": false,
8+
"expect": false
9+
},
10+
"env": {
11+
"node": true
12+
},
13+
"plugins": [
14+
"import"
15+
],
16+
"rules": {
17+
"no-param-reassign": [0],
18+
"radix": "off",
19+
"no-continue": "off",
20+
"no-use-before-define": "off",
21+
"no-underscore-dangle": "off",
22+
"class-methods-use-this": "off",
23+
"max-len": ["error", 150, 2, {
24+
"ignoreUrls": true,
25+
"ignoreComments": false,
26+
"ignoreRegExpLiterals": true,
27+
"ignoreStrings": true,
28+
"ignoreTemplateLiterals": true
29+
}]
30+
}
31+
}

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
node_modules
3+
yarn-error.log
4+
public.key
5+
private.key

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A GraphQL (Apollo) Express server with JWT authentication, and using Objection.js ORM that automagically generates GQL schema types & resolvers.

knexfile.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require('dotenv').config();
2+
3+
const {
4+
DB_HOST,
5+
DB_USER,
6+
DB_PASSWORD,
7+
DB_DATABASE,
8+
} = process.env;
9+
10+
module.exports = {
11+
12+
development: {
13+
client: 'mysql',
14+
debug: true,
15+
useNullAsDefault: true,
16+
connection: {
17+
host: DB_HOST,
18+
user: DB_USER,
19+
password: DB_PASSWORD,
20+
database: DB_DATABASE,
21+
},
22+
},
23+
24+
};

migrations/20180617203030_users.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
exports.up = async (knex) => {
3+
await knex.schema.createTable('User', (table) => {
4+
table.increments('id').primary();
5+
table.string('email');
6+
table.string('firstName');
7+
table.string('lastName');
8+
table.string('password');
9+
table.dateTime('createdAt').defaultTo(knex.fn.now(6));
10+
});
11+
await knex.schema.createTable('Project', (table) => {
12+
table.increments('id').primary();
13+
table.integer('ownerId');
14+
table.string('title');
15+
});
16+
};
17+
18+
exports.down = async (knex) => {
19+
await knex.schema.dropTable('User');
20+
await knex.schema.dropTable('Project');
21+
};

migrations/20181006213558_books.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
exports.up = async (knex) => {
3+
await knex.schema.createTable('Book', (table) => {
4+
table.increments('id').primary();
5+
table.string('title');
6+
table.string('author');
7+
});
8+
};
9+
10+
exports.down = async (knex) => {
11+
await knex.schema.dropTable('Book');
12+
};

nodemon.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"watch": [
3+
"src/",
4+
"knexfile.js",
5+
".env"
6+
],
7+
"env": {
8+
"NODE_ENV": "development"
9+
}
10+
}

package.json

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "apollo-graphql-express-objection-server",
3+
"version": "1.0.0",
4+
"description": "A GraphQL (Apollo) Express server with JWT authentication, and using Objection.js ORM that automagically generates GQL schema types & resolvers.",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "jest",
8+
"start": "./node_modules/.bin/nodemon src/server.js"
9+
},
10+
"author": "Jack Chapple",
11+
"license": "ISC",
12+
"dependencies": {
13+
"apollo-server-express": "^2.1.0",
14+
"bcryptjs": "^2.4.3",
15+
"body-parser": "^1.18.3",
16+
"cors": "^2.8.4",
17+
"dotenv": "^6.0.0",
18+
"express": "^4.16.3",
19+
"graphql": "^14.0.2",
20+
"jsonwebtoken": "^8.3.0",
21+
"knex": "^0.14.6",
22+
"lodash": "^4.17.11",
23+
"mysql": "^2.16.0",
24+
"objection": "^1.1.10",
25+
"objection-graphql": "^0.4.2"
26+
},
27+
"devDependencies": {
28+
"eslint": "^4.16.0",
29+
"eslint-config-airbnb-base": "^12.1.0",
30+
"eslint-plugin-import": "^2.8.0",
31+
"jest": "^23.1.0",
32+
"nodemon": "^1.18.4"
33+
}
34+
}

seeds/books.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { Model } = require('objection');
2+
const { Book } = require('../src/models');
3+
4+
exports.seed = async (knex) => {
5+
Model.knex(knex);
6+
await Book.query().delete();
7+
await Book.query().insertGraph([
8+
{
9+
title: 'Harry Potter and the Chamber of Secrets',
10+
author: 'J.K. Rowling',
11+
},
12+
{
13+
title: 'Jurassic Park',
14+
author: 'Michael Crichton',
15+
},
16+
]);
17+
};

seeds/users.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { Model } = require('objection');
2+
const { User, Project } = require('../src/models');
3+
4+
exports.seed = async (knex) => {
5+
Model.knex(knex);
6+
await User.query().delete();
7+
await Project.query().delete();
8+
await User.query().insertGraph([
9+
{
10+
firstName: 'Alice',
11+
lastName: 'Smith',
12+
13+
password: 'alice',
14+
projects: [{ id: 1, title: 'Project by Alice' }],
15+
},
16+
{
17+
firstName: 'Bob',
18+
lastName: 'Smith',
19+
20+
password: 'bob',
21+
projects: [{ id: 2, title: 'Project by Bob' }],
22+
},
23+
{
24+
firstName: 'Eve',
25+
lastName: 'Smith',
26+
27+
password: 'eve',
28+
projects: [{ id: 3, title: 'Project by Eve' }],
29+
},
30+
]);
31+
};
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const removeXPoweredBy = () => (req, res, next) => {
2+
res.removeHeader('X-Powered-By');
3+
next();
4+
};
5+
6+
module.exports = removeXPoweredBy;

src/http-middleware/withAuth.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const { ForbiddenError } = require('apollo-server-express');
2+
const getUserFromRequest = require('../utils/getUserFromRequest');
3+
4+
const withAuth = server => (
5+
async (req, res, next) => {
6+
const user = await getUserFromRequest(req);
7+
8+
server.context = () => {
9+
if (!user) {
10+
throw new ForbiddenError('Invalid credentials');
11+
}
12+
return { user };
13+
};
14+
15+
res.locals.user = user;
16+
17+
server.requestOptions.rootValue = {
18+
async onQuery(qb) {
19+
await qb.mergeContext({
20+
isApiQuery: true,
21+
user,
22+
});
23+
},
24+
};
25+
26+
next();
27+
}
28+
);
29+
30+
module.exports = withAuth;

src/knex.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const Knex = require('knex');
2+
const knexConfigs = require('../knexfile');
3+
4+
const env = () => process.env.NODE_ENV || 'development';
5+
6+
const knexConfig = knexConfigs[env()];
7+
8+
module.exports = Knex(knexConfig);

src/models/BaseModel.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const { Model } = require('objection');
2+
3+
module.exports = class BaseModel extends Model {
4+
// Objection Model Configs
5+
static get modelPaths() {
6+
return [__dirname];
7+
}
8+
static get tableName() {
9+
return this.name;
10+
}
11+
12+
// eslint-disable-next-line
13+
static async modifyApiQuery(qb, context) {}
14+
15+
// eslint-disable-next-line no-unused-vars
16+
static async modifyApiResults(result, context, qb) {
17+
return result;
18+
}
19+
20+
static get QueryBuilder() {
21+
return class extends super.QueryBuilder {
22+
constructor(modelClass) {
23+
super(modelClass);
24+
this.runBefore(async (result, qb) => {
25+
const context = qb.context();
26+
if (!context.isApiQuery) return;
27+
await modelClass.modifyApiQuery(qb, context);
28+
});
29+
this.runAfter(async (result, qb) => {
30+
const context = qb.context();
31+
if (!context.isApiQuery) return result;
32+
return modelClass.modifyApiResults(result, context, qb);
33+
});
34+
}
35+
};
36+
}
37+
};

src/models/Book.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const BaseModel = require('./BaseModel');
2+
3+
module.exports = class Book extends BaseModel {
4+
static get jsonSchema() {
5+
return {
6+
type: 'object',
7+
8+
properties: {
9+
title: { type: 'string', minLength: 1, maxLength: 255 },
10+
author: { type: 'string', minLength: 1, maxLength: 255 },
11+
},
12+
};
13+
}
14+
};
15+

src/models/Project.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const BaseModel = require('./BaseModel');
2+
3+
module.exports = class Project extends BaseModel {
4+
static get jsonSchema() {
5+
return {
6+
type: 'object',
7+
8+
properties: {
9+
id: { type: 'integer' },
10+
title: { type: 'string', minLength: 1, maxLength: 255 },
11+
},
12+
};
13+
}
14+
15+
static async modifyApiQuery(qb, { user }) {
16+
if (user) {
17+
qb.where('ownerId', user.id);
18+
} else {
19+
qb.whereRaw('1=2');
20+
}
21+
}
22+
23+
static get relationMappings() {
24+
return {
25+
owner: {
26+
relation: this.BelongsToOneRelation,
27+
modelClass: 'User',
28+
join: {
29+
from: 'Project.ownerId',
30+
to: 'User.id',
31+
},
32+
},
33+
};
34+
}
35+
};
36+

0 commit comments

Comments
 (0)