Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buy album #12

Merged
merged 11 commits into from
Jul 23, 2019
2 changes: 2 additions & 0 deletions app/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const DEFAULT_ERROR = 500,
DB_ERROR = 503,
VALIDATION_ERROR = 401,
TOKEN_ERROR = 500,
PERMISSION_ERROR = 401,
HASH_ERROR = 500;

exports.defaultError = message => createError(message, DEFAULT_ERROR);
Expand All @@ -16,4 +17,5 @@ exports.apiError = message => createError(message, API_ERROR);
exports.dbError = message => createError(message, DB_ERROR);
exports.validationError = message => createError(message, VALIDATION_ERROR);
exports.tokenError = message => createError(message, TOKEN_ERROR);
exports.permissionError = message => createError(message, PERMISSION_ERROR);
exports.hashError = message => createError(message, HASH_ERROR);
6 changes: 4 additions & 2 deletions app/graphql/albums/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const { queries, schema: queriesSchema } = require('./queries'),
{ typeResolvers } = require('./resolvers');
{ typeResolvers } = require('./resolvers'),
{ mutations, schema: mutationsSchema } = require('./mutations');

module.exports = {
queries,
mutations,
typeResolvers,
schemas: [queriesSchema]
schemas: [queriesSchema, mutationsSchema]
};
24 changes: 24 additions & 0 deletions app/graphql/albums/mutations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { gql } = require('apollo-server'),
{ createPurchase, album } = require('./resolvers'),
{ permissionError } = require('../../errors'),
{ getEmailFromToken } = require('../../helpers/token'),
{ user: User } = require('../../models');

module.exports = {
mutations: {
buyAlbum: (_, { albumToBuy }, context) => {
if (context.tokenValidated) {
return getEmailFromToken(context.token)
.then(email => User.getByEmail(email))
.then(user => createPurchase(albumToBuy.albumId, user.id).then(albumId => album(albumId)));
}
throw permissionError('User does not have permission');
}
},

schema: gql`
extend type Mutation {
buyAlbum(albumToBuy: AlbumInput!): Album
}
`
};
16 changes: 15 additions & 1 deletion app/graphql/albums/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { getAlbumById, requestAlbumPhotos, getAlbums, getAlbumsFiltered } = require('../../services/typicode'),
logger = require('../../logger');
logger = require('../../logger'),
{ purchase: Purchase } = require('../../models'),
{ validationError } = require('../../errors');

exports.album = albumId => {
logger.info(`Requesting album with id ${albumId}`);
Expand Down Expand Up @@ -28,6 +30,18 @@ exports.albums = (offset, limit, orderBy, filterBy) => {
});
};

exports.createPurchase = (albumId, userId) => {
logger.info(`Purchasing album with id ${albumId} for user with id ${userId}`);
return getAlbumById(albumId).then(() =>
Purchase.createPurchase(albumId, userId).then(([purchase, created]) => {
if (created) {
return purchase.albumId;
}
throw validationError('User already bought that album');
})
);
};

const photos = album => {
logger.info(`Requesting photos of album with id ${album.id}`);
return requestAlbumPhotos(album.id);
Expand Down
3 changes: 2 additions & 1 deletion app/graphql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ module.exports = makeExecutableSchema({
...albums.queries
},
Mutation: {
...users.mutations
...users.mutations,
...albums.mutations
},
Subscription: {
...users.subscriptions
Expand Down
3 changes: 3 additions & 0 deletions app/graphql/inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ module.exports = gql`
email: String!
password: String!
}
input AlbumInput {
albumId: Int!
}
`;
34 changes: 33 additions & 1 deletion app/helpers/token.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const jsrasign = require('jsrsasign'),
config = require('../../config').common.token,
logger = require('../logger');
logger = require('../logger'),
{ tokenError } = require('../errors'),
{ user: User } = require('../models');

exports.createToken = sub => {
logger.info('Creating token');
Expand All @@ -12,3 +14,33 @@ exports.createToken = sub => {
payload.exp = payload.nbf + config.sessionTime;
return jsrasign.jws.JWS.sign(config.algorithm, header, payload, config.pass);
};

const getEmailFromToken = token =>
new Promise(resolve => {
resolve(jsrasign.b64toutf8(token.split('.')[1]));
})
.then(jsonString => new Promise(resolve => resolve(JSON.parse(jsonString).sub)))
.catch(error => tokenError(error));

const validateWithEmail = (token, email) =>
User.getByEmail(email).then(foundUser => {
if (foundUser) {
return jsrasign.jws.JWS.verifyJWT(token, config.pass, {
alg: [config.algorithm],
sub: [email]
});
}
return false;
});

const resolveValidation = validated => validated;

exports.validateToken = token => {
logger.info('Validating token');
return getEmailFromToken(token)
.then(email => validateWithEmail(token, email))
.then(validated => resolveValidation(validated))
.catch(() => false);
};

exports.getEmailFromToken = getEmailFromToken;
27 changes: 27 additions & 0 deletions app/models/purchase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { dbError } = require('../errors');

module.exports = (sequelize, DataTypes) => {
const Purchase = sequelize.define(
'purchase',
{
albumId: {
type: DataTypes.INTEGER,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false
}
},
{
paranoid: true,
underscored: true
}
);

Purchase.createPurchase = (albumId, userId) =>
Purchase.findOrCreate({ where: { albumId, userId }, default: {} }).catch(error => {
throw dbError(error.message);
});
return Purchase;
};
24 changes: 24 additions & 0 deletions migrations/migrations/20190719160403-create-purchases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) =>
queryInterface.createTable('purchases', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
albumId: {
type: Sequelize.INTEGER,
allowNull: false
},
userId: {
type: Sequelize.INTEGER,
allowNull: false
},
created_at: Sequelize.DATE,
updated_at: Sequelize.DATE,
deleted_at: Sequelize.DATE
}),
down: queryInterface => queryInterface.dropTable('purchases')
};
17 changes: 13 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const { ApolloServer } = require('apollo-server'),
config = require('./config'),
migrationsManager = require('./migrations'),
logger = require('./app/logger'),
schema = require('./app/graphql');
schema = require('./app/graphql'),
{ validateToken } = require('./app/helpers/token');

const port = config.common.api.port || 8080;

Expand All @@ -14,9 +15,17 @@ migrationsManager
enabled: !!config.common.rollbar.accessToken,
environment: config.common.rollbar.environment || config.environment
}); */
new ApolloServer({ schema }).listen(port).then(({ url, subscriptionsUrl }) => {
logger.info(`🚀 Server ready at ${url}`);
logger.info(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
new ApolloServer({
schema,
context: ({ req }) => {
const token = req.headers.token || '';
return validateToken(token).then(validated => ({ tokenValidated: validated, token }));
}
})
.listen(port)
.then(({ url, subscriptionsUrl }) => {
logger.info(`🚀 Server ready at ${url}`);
logger.info(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
})
)
.catch(logger.error);
19 changes: 18 additions & 1 deletion test/albums/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,21 @@ const albums = (offset, limit, orderBy = null, filterBy = null) => gql`
}
`;

module.exports = { album, albums };
const buyAlbum = (albumToBuy, token) => ({
mutation: gql`
mutation buyAlbum($albumToBuy: AlbumInput!) {
buyAlbum(albumToBuy: $albumToBuy) {
id
title
}
}
`,
variables: { albumToBuy },
options: {
context: {
headers: { token }
}
}
});

module.exports = { album, albums, buyAlbum };
6 changes: 5 additions & 1 deletion test/server.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ const { createTestClient } = require('apollo-server-testing'),
{ ApolloServer } = require('apollo-server'),
schema = require('../app/graphql');

const { query: _query, mutate } = createTestClient(new ApolloServer({ schema }));
const { query: _query, mutate } = createTestClient(
new ApolloServer({
schema
})
);

const query = params => _query({ query: params });

Expand Down
2 changes: 1 addition & 1 deletion test/users/mutations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const { mutate } = require('../server.spec'),
correctEmail = 'useremail@wolox.com.ar',
correctFirstName = 'fn',
correctLastName = 'ln',
gmailEmail = 'email@gmail.com',
correctPassword = 'password',
gmailEmail = 'email@gmail.com',
shortPassword = 'pass',
passwordWithDots = 'p.a.s.s.w.o.r.d';

Expand Down