The electric FeathersJS and Apollo Server : The finish.
Thank for returning to the sequel of this NoSQL demonstration of Feathers and Apollo Server. SQL puns are freely available if you decided to build yours with any number of the SQL platform we mentioned in the previous article.
Because of this, I copied schema.js to a backup file and removed all the unfinished mutations. So at this point, we have:
Schema.js
const typeDefinitions = `enum LoadType {
FIXTURE
OUTLET
}type User {
_id: String! # Indicadive of MongoDB , use id if you'd like to use SQL
createdAt: String
firstName: String
lastName: String
username: String!
buildings: [Building] # User has many Buildings
}type Building {
_id: String!
createdAt: String
name: String!
address1: String
address2: String
city: String
state: String
panels: [Panel] # Building has many Panels, even though there's probably only one.
rooms: [Room] # Building has many Rooms
}type Panel {
_id: String!
createdAt: String
name: String!
rating: Int!
slots: Int!
image: Image # Panel has one Image
breakers: [Breaker] # Panel has many Breakers
}type Room {
_id: String!
createdAt: String
label: String!
loads: [Load]
}type Image {
_id: String!
createdAt: String
url: String!
}type Breaker {
_id: String!
createdAt: String
label: String!
description: String
rating: Int!
loads: [Load] # Breaker has many Loads , IE lights and outlets
}type Load {
_id: String!
createdAt: String
label: String!
type: String!
image: Image # Load has one Image
switches: [Toggle] # Load may have many Toggles , single or 2 way
}type Toggle { # Can't call this switch
_id: String!
createdAt: String
label: String!
image: Image # Toggle has one Image
}# types for mutations
type AuthPayload {
token: String # JSON Web Token
data: User
}# the schema allows the following queries:
type RootQuery {
viewer: User
buildings: [Building]
building(_id: String!): Building
rooms(buildingId: String!): [Room]
room(_id: String!): Room
panels(buildingId: String!): [Panel]
panel(_id: String!): Panel
breakers(panelId: String!): [Breaker]
roomLoads(roomId: String!): [Load]
breakerLoads(breakerId: String!): [Load]
toggle(_id: String!): Toggle
}# this schema allows the following mutations:
type RootMutation {
signUp (
username: String!
password: String!
firstName: String
lastName: String
): UserlogIn (
username: String!
password: String!
): AuthPayload}# we need to tell the server which types represent the root query
# and root mutation types. We call them RootQuery and RootMutation by convention.
schema {
query: RootQuery
mutation: RootMutation
}
`;export default [typeDefinitions]
Resolvers.js
// src/services/graphql/resolvers.js
import request from 'request-promise';export default function Resolvers() {let app = this;// Define services here
let Users = app.service('users');
let Buildings = app.service('buildings');
let Panels = app.service('panels');
let Rooms = app.service('rooms');
let Images = app.service('images');
let Breakers = app.service('breakers');
let Loads = app.service('loads');
let Toggles = app.service('toggles');
let Viewer = app.service('viewers');const localRequest = request.defaults({
baseUrl: `http://${app.get('host')}:${app.get('port')}`,
json: true
});return {// Models here
User: {},
Building: {},
Panel: {},
Room: {},
Image: {},
Breaker: {},
Load: {},
Toggle: {},
AuthPayload: {
data(auth, args, context) {
return auth.data;
}
},RootQuery: {
viewer(root, args, context) {
return Viewer.find(context);
}
},RootMutation: {
signUp(root, args, context) {
return Users.create(args)
},
logIn(root, {username, password}, context) {
return localRequest({
uri: '/auth/local',
method: 'POST',
body: { username, password }
});
}
}}
}
Now start the dev server:
$ npm run dev
Here’s the resolvers file with the model relationships defined, we still need to work on the RootQuery.
When designing a GraphQL query using Feathers, you’ll have this 3rd parameter named “context”. It is a critical value because without it passed into the queries, none of the hooks will work. You’ll also need to append a query object to the context object for hooks that apply queries to work. For example, feathers-authentication has a “queryWithCurrentUser” hook that adds userId = current_user to the query. If you don’t add this query object, the server will throw a can’t append to undefined error.
buildings(root, args, context) {
console.log(context);
context.query = {};
return Buildings.find(context);
},
Full Resolvers file
// src/services/graphql/resolvers.js
import request from 'request-promise';export default function Resolvers() {let app = this;// Define services here
let Users = app.service('users');
let Buildings = app.service('buildings');
let Panels = app.service('panels');
let Rooms = app.service('rooms');
let Images = app.service('images');
let Breakers = app.service('breakers');
let Loads = app.service('loads');
let Toggles = app.service('toggles');
let Viewer = app.service('viewer');const localRequest = request.defaults({
baseUrl: `http://${app.get('host')}:${app.get('port')}`,
json: true
});return {// Models here
User: {
buildings(user, args, context) {
return Buildings.find({
query: {
userId: user._id
}
});
}
},
Building: {
panels(building, args, context) {
return Panels.find({
query: {
buildingId: building._id
}
});
},
rooms(building, args, context) {
return Rooms.find({
query: {
buildingId: building._id
}
});
}
},
Panel: {
breakers(panel, args, context) {
return Breakers.find({
query: {
panelId: panel._id
}
});
},
image(panel, args, context) {
return Images.find({
query: {
panelId: panel._id
}
});
}
},
Room: {
loads(room, args, context) {
return Loads.find({
query: {
roomId: room._id
}
});
}
},
Image: {},
Breaker: {
loads(breaker, args, context) {
return Loads.find({
query: {
breakerId: breaker._id
}
});
}
},
Load: {
switches(load, args, context) {
return Toggles.find({
query: {
loadId: load._id
}
});
}
},
Toggle: {},
AuthPayload: {
data(auth, args, context) {
return auth.data;
}
},RootQuery: {
viewer(root, args, context) {
return Viewer.find(context);
},
buildings(root, args, context) {
return Buildings.find(context);
}
},RootMutation: {
signUp(root, args, context) {
return Users.create(args)
},
logIn(root, {username, password}, context) {
return localRequest({
uri: '/auth/local',
method: 'POST',
body: { username, password }
});
}
}}
}
At this point, you may remember that we set up the user model to have a “username” field, the default Feathers approach uses an “email” field. So you need to edit /config/default.json and set the usernameField.
"auth": {
"usernameField": "username",
"token": {
"secret": "AiGVgTqhIgh6kdssOYnq0eEB4wJcpk5+JbHxN12KKKOAvuxLrcQLMAijZ26RxhxIMmBCzQxXQ+0hG7DlAY/WyQ=="
},
"local": {}
}
It’s nice to have data returned when testing an API, to do so we’ll use feathers-seeder.
npm install --save feathers-seeder
The configure /src/index.js to run the seeder when the app starts.
'use strict';
require("babel-register");
const app = require('./app');app.seed().then(() => {
const port = app.get('port');
const server = app.listen(port);server.on('listening', () =>
console.log(`Feathers application started on ${app.get('host')}:${port}`)
);
}).catch(err => {
// ...
});
Once it’s installed create /src/seeder-config.js . Read through the seeder script, it starts with creating two users after each model is seeded it will fire a callback after each model’s method.
var mongoose = require('mongoose');var roomData = {};/**
* Start the First Seeder here.
*/
module.exports = {
services: [
{
path: 'users',
count: 2,
template: {
'username': '{{internet.email}}',
'firstName': '{{name.firstName}}',
'lastName': '{{name.lastName}}',
'password': 'password'
},
callback(user, seed) {
//console.info(`User created: ${user._id}!`);
return seedBuildings(user, seed);
}
}
]
};const seedBuildings = function(user, seed) {
return seed({
count: 2,
path: 'buildings',
template: {
'userId': () => user._id,
'name': '{{company.companyName}}',
'address1': '{{address.streetAddress}}',
'address2': '{{address.secondaryAddress}}',
'city': '{{address.city}}',
'state': '{{address.state}}'
},
callback(building, seed) {
//console.info(`Building created: ${building._id}!`);
return seedRoom(building, seed);
}
});
};const seedRoom = function(building, seed) {
return seed({
count: 1,
path: 'rooms',
template: {
'userId': () => building.userId,
'buildingId': () => building._id,
'label': 'Living Room'
},
callback(room, seed) {
//console.info(`Room created ${room._id}!`);
if (roomData[room.buildingId]) {
roomData[room.buildingId].push(room);
} else {
roomData[room.buildingId] = [room];
}
return seedPanel(room, seed);
}
});
};const seedPanel = function(room, seed) {
return seed({
count: 1,
path: 'panels',
template: {
'userId': () => room.userId,
'buildingId': () => room.buildingId,
'name': '{{lorem.word}}',
'rating': 200,
'slots': 20
},
callback(panel, seed, room) {
// console.info(`Panel created ${panel._id}!`);
return seedBreaker(panel, seed);
}
});
};const seedBreaker = function(panel, seed) {
return seed({
count: 20,
path: 'breakers',
template: {
'userId': () => panel.userId,
'panelId': () => panel._id,
'label': '{{lorem.word}}',
'description': '{{lorem.words}}',
'rating': 20,
},
callback(breaker, seed) {
//console.info(`Breaker created ${breaker._id}!`);
return seedLoads(breaker, seed, panel);
}
});
};const seedLoads = function(breaker, seed, panel) {
// console.log(roomData[panel.buildingId]);
var rooms = roomData[panel.buildingId];
var room = rooms[Math.floor(Math.random()*rooms.length)];return seed({
count: 2,
path: 'loads',
template: {
'userId': () => breaker.userId,
'breakerId': () => breaker._id,
'roomId': () => room._id,
'label': 'Outlet',
'type': 'Single'
},
callback(load, seed) {
//console.info(`Load created ${load._id}!`);
return seedToggles(load, seed);
}
});
};const seedToggles = function(load, seed) {
return seed({
count: 1,
path: 'toggles',
template: {
'userId': () => load.userId,
'loadId': () => load._id,
'text': 'Light switch'
}
});
};
Now to log in, start up the server, and open the Graphiql interface at:
http://localhost:3030/graphiql
Open up Robomongo and grab one of the email addresses from the Users collection.
mutation {
logIn(username:"Ruthie_Gutmann6@hotmail.com", password:"password") {
token
data {
_id
firstName
lastName
username
}
}
}
Calling this mutation will return authorization token.
{
"data": {
"logIn": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1OGM1OTJiNDJlZDFmODg4YzkyOGEwNzAiLCJpYXQiOjE0ODkzNDMxOTAsImV4cCI6MTQ4OTQyOTU5MCwiaXNzIjoiZmVhdGhlcnMifQ.n9K_0y0-wvti269Nj4qa1w2jHDmNqy6BLFG6LgT0olY",
"data": {
"_id": "58c592b42ed1f888c928a070",
"firstName": "Lorna",
"lastName": "Rolfson",
"username": "Ruthie_Gutmann6@hotmail.com"
}
}
}
}
Unfortunately, Graphiql doesn’t have a way to pass authentication headers, so we’ll rely on a Chrome extension called Modheader to pass the authentication token.
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1OGM1OTJiNDJlZDFmODg4YzkyOGEwNzAiLCJpYXQiOjE0ODkzNDMxOTAsImV4cCI6MTQ4OTQyOTU5MCwiaXNzIjoiZmVhdGhlcnMifQ.n9K_0y0-wvti269Nj4qa1w2jHDmNqy6BLFG6LgT0olY
Warning, if you’re using Modheader, disable it when you’re done. The above token applies to all websites and will lock you out of many other sites.
So being authenticated is pointless unless it can restrict access and help you populate data so let’s set up hooks for the “buildings” service.
'use strict';const globalHooks = require('../../../hooks');
const hooks = require('feathers-hooks');
const auth = require('feathers-authentication').hooks;exports.before = {
all: [
// This hook validates the auth token.
auth.verifyToken(),
// This hook loads the current user.
auth.populateUser(),
// With this hook, you shall not pass if not authenticated.
auth.restrictToAuthenticated()
],
find: [
// This hook appends userId: _id from the User object
//{ idField: '_id', as: 'userId' } are the defaults as well
auth.queryWithCurrentUser()
],
get: [
// This hook checks that the user ID matches the record userId
auth.restrictToOwner()
],
create: [
// This hook applies the current user to the create query
// so you don't need to set it in your mutation
auth.associateCurrentUser({as: 'userId'})
],
update: [
// This hook checks that the user ID matches the record userId
auth.restrictToOwner()
],
patch: [
// This hook checks that the user ID matches the record userId
auth.restrictToOwner()
],
remove: [
// This hook checks that the user ID matches the record userId
auth.restrictToOwner()
]
};exports.after = {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
};
We can apply this to most of the models except for the User.
'use strict';const globalHooks = require('../../../hooks');
const hooks = require('feathers-hooks');
const auth = require('feathers-authentication').hooks;exports.before = {
all: [],
find: [],
get: [],
create: [
auth.hashPassword()
],
update: [
auth.verifyToken(),
auth.populateUser(),
auth.restrictToAuthenticated(),
auth.restrictToOwner({ ownerField: '_id' })
],
patch: [
auth.verifyToken(),
auth.populateUser(),
auth.restrictToAuthenticated(),
auth.restrictToOwner({ ownerField: '_id' })
],
remove: [
auth.verifyToken(),
auth.populateUser(),
auth.restrictToAuthenticated(),
auth.restrictToOwner({ ownerField: '_id' })
]
};exports.after = {
all: [hooks.remove('password')],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
};
Now, let’s define the rest of the resolvers. Study the RootQuery and RootMutation sections in the file below.
// src/services/graphql/resolvers.js
import request from 'request-promise';
export default function Resolvers() {let app = this;// Define services here
let Users = app.service('users');
let Buildings = app.service('buildings');
let Panels = app.service('panels');
let Rooms = app.service('rooms');
let Images = app.service('images');
let Breakers = app.service('breakers');
let Loads = app.service('loads');
let Toggles = app.service('toggles');
let Viewer = app.service('viewer');const localRequest = request.defaults({
baseUrl: `http://${app.get('host')}:${app.get('port')}`,
json: true
});return {// Models here
User: {
buildings(user, args, context) {
return Buildings.find({
query: {
userId: user._id
}
});
}
},
Building: {
panels(building, args, context) {
return Panels.find({
query: {
buildingId: building._id
}
});
},
rooms(building, args, context) {
return Rooms.find({
query: {
buildingId: building._id
}
});
}
},
Panel: {
breakers(panel, args, context) {
return Breakers.find({
query: {
panelId: panel._id
}
});
},
image(panel, args, context) {
return Images.find({
query: {
panelId: panel._id
}
});
}
},
Room: {
loads(room, args, context) {
return Loads.find({
query: {
roomId: room._id
}
});
}
},
Image: {},
Breaker: {
loads(breaker, args, context) {
return Loads.find({
query: {
breakerId: breaker._id
}
});
}
},
Load: {
switches(load, args, context) {
return Toggles.find({
query: {
loadId: load._id
}
});
}
},
Toggle: {},
AuthPayload: {
data(auth, args, context) {
return auth.data;
}
},RootQuery: {
viewer(root, args, context) {
return Viewer.find(context);
},
buildings(root, args, context) {
context.query = {};
return Buildings.find(context);
},
building(root, { id }, context) {
// get refuses to apply restrictToOwner or any hooks. Feathers bug :( won't accept context either.
// This appears to be the only way atm to limit the access via hooks.
context.query = {
_id: id
};
return Buildings.find(context).then((d) => d[0]);
},rooms(root, { buildingId }, context) {
context.query = {
buildingId: buildingId
};
return Rooms.find(context);
},
room(root, { id }, context) {
context.query = {
_id: id
};
return Rooms.find(context).then((d) => d[0]);
},panels(root, { buildingId }, context) {
context.query = {
buildingId: buildingId
};
return Panels.find(context);
},
panel(root, { id }, context) {
context.query = {
_id: id
};
return Panels.find(context).then((d) => d[0]);
},breakers(root, { panelId }, context) {
context.query = {
panelId: panelId
};
return Breakers.find(context);
},
breaker(root, { id }, context) {
context.query = {
_id: id
};
return Breakers.find(context).then((d) => d[0]);
},roomLoads(root, { roomId }, context) {
context.query = {
roomId: roomId
};
return Loads.find(context);
},
breakerLoads(root, { breakerId }, context) {
context.query = {
breakerId: breakerId
};
return Loads.find(context);
},
toggles(root, { loadId }, context) {
context.query = {
loadId: loadId
};
return Toggles.find(context);
},
toggle(root, { id }, context) {
context.query = {
_id: id
};
return Toggles.find(context).then((d) => d[0]);
}},RootMutation: {
signUp(root, args, context) {
return Users.create(args, context);
},
logIn(root, {username, password}, context) {
return localRequest({
uri: '/auth/local',
method: 'POST',
body: { username, password }
});
},createBuilding(root, args, context) {
return Buildings.create(args, context);
},
updateBuilding(root, args, context) {
return Buildings.update(args, context);
},
deleteBuilding(root, { _id }, context) {
return Buildings.remove({_id}, context);
},createRoom(root, args, context) {
return Rooms.create(args, context);
},
updateRoom(root, args, context) {
return Rooms.update(args, context);
},
deleteRoom(root, { _id }, context) {
return Rooms.remove(_id, context);
},createPanel(root, args, context) {
return Panels.create(args, context);
},
updatePanel(root, args, context) {
return Panels.update(args, context);
},
deletePanel(root, { _id }, context) {
return Panels.remove(_id, context);
},createBreaker(root, args, context) {
return Breakers.create(args, context);
},
updateBreaker(root, args, context) {
return Breakers.update(args, context);
},
deleteBreaker(root, { _id }, context) {
return Breakers.remove(_id, context);
},createLoad(root, args, context) {
return Loads.create(args, context);
},
updateLoad(root, args, context) {
return Loads.update(args, context);
},
deleteLoad(root, { _id }, context) {
return Loads.remove(_id, context);
},createToggle(root, args, context) {
return Toggles.create(args, context);
},
updateToggle(root, args, context) {
return Toggles.update(args, context);
},
deleteToggle(root, { _id }, context) {
return Toggles.remove(_id, context);
}}}
}
One issue that I ran into was that the Feathers hooks for GET queries didn’t work. I’m not sure if that’s my issue or Feathers. For now, I worked around it by using FIND to retrieve a single record. After the query, return only the first record.
buildings(root, args, context) {
context.query = {};
return Buildings.find(context);
},
building(root, { id }, context) {
// get refuses to apply restrictToOwner or any hooks.
/// Feathers bug? :( won't accept context either.
// This appears to be the only way atm to limit the access via hooks.
context.query = {
_id: id
};
return Buildings.find(context).then((d) => d[0]);
},
Test out your GraphQL api again at http://localhost:3030/graphiql. Now you can do a query like this and retrieve your entire electrical system in one request.
query {
buildings {
_id
name
panels {
_id
name
breakers {
label
description
loads {
label
type
switches {
_id
label
}
}
}
}
}
}
I hope you’ve enjoyed this “how to” on Feathers and Apollo Server. You can download the source for this example here: https://github.com/techiemon/electrical-system
I hope you pick it apart and get a feeling for how easy these two packages work together.