Skip to content

Commit

Permalink
Merge pull request #11 from wolox-training/buy-album
Browse files Browse the repository at this point in the history
Buy album
  • Loading branch information
dbenavidesv committed Jul 23, 2019
2 parents 5ec44ae + 487c0e2 commit f5d0812
Show file tree
Hide file tree
Showing 18 changed files with 213 additions and 12 deletions.
6 changes: 5 additions & 1 deletion app/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const errorCodes = {
DATABASE_ERROR: 503,
UNIQUE_EMAIL_ERROR: 409,
INVALID_INPUT_ERROR: 422,
USER_NOT_FOUND_ERROR: 404
USER_NOT_FOUND_ERROR: 404,
ITEM_NOT_FOUND_ERROR: 404
};

exports.defaultError = message => createError(message, errorCodes.DEFAULT_ERROR);
Expand All @@ -19,4 +20,7 @@ exports.databaseError = message => createError(message, errorCodes.DATABASE_ERRO
exports.uniqueEmailError = message => createError(message, errorCodes.UNIQUE_EMAIL_ERROR);
exports.invalidInputError = (message, invalidFields) => new UserInputError(message, { invalidFields });
exports.userNotFoundError = message => createError(message, errorCodes.USER_NOT_FOUND_ERROR);
exports.itemNotFoundError = message => createError(message, errorCodes.USER_NOT_FOUND_ERROR);
exports.badLogInError = message => new AuthenticationError(message);
exports.sessionError = message => new AuthenticationError(message);
exports.albumServiceError = (message, statusCode) => createError(message, statusCode);
6 changes: 5 additions & 1 deletion app/graphql/albums/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const { queries, schema: queriesSchema } = require('./queries');
const { mutations, schema: mutationSchema } = require('./mutations');
const { typeResolvers } = require('./resolvers');
const middlewares = require('./middlewares');

module.exports = {
queries,
mutations,
middlewares,
typeResolvers,
schemas: [queriesSchema]
schemas: [queriesSchema, mutationSchema]
};
4 changes: 4 additions & 0 deletions app/graphql/albums/middlewares.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const usersValidators = require('../../validators/users');

exports.buyAlbum = (resolve, root, args, context) =>
usersValidators.validateAuthetication(root, args, context).then(() => resolve(root, args, context));
14 changes: 14 additions & 0 deletions app/graphql/albums/mutations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { gql } = require('apollo-server');

const resolvers = require('./resolvers');

module.exports = {
mutations: {
buyAlbum: resolvers.buyAlbum
},
schema: gql`
extend type Mutation {
buyAlbum(albumId: ID!): Album!
}
`
};
13 changes: 13 additions & 0 deletions app/graphql/albums/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const errors = require('../../errors');
const logger = require('../../logger');
const albumsHelpers = require('../../helpers/albums');
const albumsService = require('../../services/albums');
Expand All @@ -20,6 +21,18 @@ exports.getPhotos = (root, args) => {
return albumsService.getPhotos({ albumId: id });
};

exports.buyAlbum = (root, { albumId, user }) => {
logger.info(`Buying album with id: ${albumId} for user: ${user.email}`);
return albumsService
.getAlbum(albumId)
.then(album => albumsService.addAlbum({ ...albumsHelpers.albumMapper(album), userId: user.id }))
.catch(error => {
const errorMessage = `Failed to buy album. Error: ${error.message}`;
logger.error(errorMessage);
throw errors.albumServiceError(errorMessage, error.extensions.code);
});
};

exports.typeResolvers = {
photos: exports.getPhotos
};
15 changes: 10 additions & 5 deletions app/graphql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const schema = makeExecutableSchema({
...albums.queries
},
Mutation: {
...users.mutations
...users.mutations,
...albums.mutations
},
Subscription: {
...users.subscriptions
Expand All @@ -34,10 +35,14 @@ const schema = makeExecutableSchema({
}
});

const middlewares = {
const schemaWithMiddlewares = applyMiddleware(schema, {
Mutation: {
...users.middlewares
...users.middlewares,
...albums.middlewares
}
};
});

module.exports = applyMiddleware(schema, middlewares);
module.exports = {
schema: schemaWithMiddlewares,
context: ({ req }) => ({ authorization: req.headers.authorization })
};
2 changes: 1 addition & 1 deletion app/graphql/users/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ exports.logIn = (root, { user }) => {
.validateCredentials(user)
.then(storedUser =>
storedUser
? userHelpers.generateToken(user)
? userHelpers.generateToken(storedUser)
: Promise.reject(errors.badLogInError('The email or password provided is incorrect'))
)
.then(userHelpers.mapToken)
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ exports.generateToken = (user, secret = sessionConfig.secret, expiresIn = sessio
return jwt.signAsync(payload, secret, { expiresIn });
};

exports.validateToken = (token, secret = sessionConfig.secret) => jwt.verifyAsync(token, secret);

exports.decodeToken = token => jwt.decode(token);

exports.validateCredentials = user => {
Expand Down
22 changes: 22 additions & 0 deletions app/models/album.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const Album = sequelize.define(
'Album',
{
id: { type: DataTypes.INTEGER, primaryKey: true, allowNull: false },
title: { type: DataTypes.STRING, allowNull: false },
artist: { type: DataTypes.INTEGER, allowNull: false },
userId: { type: DataTypes.INTEGER, field: 'user_id', primaryKey: true, allowNull: false }
},
{
tableName: 'albums',
underscored: true
}
);

Album.associate = models => {
Album.belongsTo(models.User, { foreignKey: 'userId' });
};

return Album;
};
4 changes: 4 additions & 0 deletions app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ module.exports = (sequelize, DataTypes) => {
}
);

User.associate = models => {
User.hasMany(models.Album);
};

User.createModel = user => User.create(user);

User.getOne = user => User.findOne({ where: user });
Expand Down
12 changes: 11 additions & 1 deletion app/services/albums.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const util = require('util');

const errors = require('../errors');
const logger = require('../logger');
const { Album } = require('../models');
const { albumsApi } = require('../../config').common;

exports.executeRequest = options => {
Expand All @@ -11,7 +12,9 @@ exports.executeRequest = options => {
logger.info(`Headers: [${Object.keys(options.headers).join(', ')}]`);
}
return request(options).catch(error => {
throw errors.albumApiError(error.message);
throw error.statusCode === 404
? errors.itemNotFoundError('Item not found in external api')
: errors.albumApiError(error.message);
});
};

Expand Down Expand Up @@ -42,3 +45,10 @@ exports.getPhotos = qs => {
};
return exports.executeRequest(options);
};

exports.addAlbum = album =>
Album.create(album).catch(error => {
throw error.name === 'SequelizeUniqueConstraintError'
? errors.uniqueEmailError(`The user has already bought album with id ${album.id}`)
: errors.databaseError(`${error.name}: ${error.message}`);
});
17 changes: 17 additions & 0 deletions app/validators/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const errors = require('../errors');
const usersHelpers = require('../helpers/users');

exports.validateAuthetication = (root, args, context) => {
const token = context.authorization;
if (token) {
return usersHelpers
.validateToken(token)
.then(decodedToken => {
args.user = decodedToken;
})
.catch(error => {
throw errors.sessionError(`Session error: ${error.message}`);
});
}
throw errors.sessionError('Session error: no token provided');
};
37 changes: 37 additions & 0 deletions migrations/migrations/20190722191147-create-albums.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) =>
queryInterface.createTable('albums', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
allowNull: false
},
title: {
type: Sequelize.STRING,
allowNull: false
},
artist: {
type: Sequelize.INTEGER,
allowNull: false
},
user_id: {
type: Sequelize.INTEGER,
primaryKey: true,
references: {
model: 'users',
key: 'id'
},
allowNull: false
},
created_at: {
type: Sequelize.DATE,
allowNull: false
},
updated_at: {
type: Sequelize.DATE,
allowNull: false
}
}),
down: queryInterface => queryInterface.dropTable('albums')
};
4 changes: 2 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { ApolloServer } = require('apollo-server'),
config = require('./config'),
migrationsManager = require('./migrations'),
logger = require('./app/logger'),
schema = require('./app/graphql');
{ schema, context } = require('./app/graphql');

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

Expand All @@ -14,7 +14,7 @@ migrationsManager
enabled: !!config.common.rollbar.accessToken,
environment: config.common.rollbar.environment || config.environment
}); */
new ApolloServer({ schema }).listen(port).then(({ url, subscriptionsUrl }) => {
new ApolloServer({ schema, context }).listen(port).then(({ url, subscriptionsUrl }) => {
logger.info(`🚀 Server ready at ${url}`);
logger.info(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
})
Expand Down
47 changes: 47 additions & 0 deletions test/albums/resolvers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { createUser } = require('../utils/common');
const albumMocks = require('../utils/mocks/albums');
const albumsFactory = require('../utils/factories/albums');
const { mutations } = require('../../app/graphql/albums/mutations');

describe('albums', () => {
describe('resolvers', () => {
describe('buyAlbum', () => {
it('should successfully buy album for user', () => {
const albumId = 1;
albumMocks.mockGetAlbumOK(albumId, albumsFactory.responseAlbumOK);
return createUser().then(({ data }) =>
mutations.buyAlbum(undefined, { albumId, user: data.createUser }).then(response => {
expect(response.id).toEqual(albumsFactory.responseAlbumOK.id);
expect(response.artist).toEqual(albumsFactory.responseAlbumOK.userId);
expect(response.userId).toEqual(parseInt(data.createUser.id));
})
);
});

it('should fail to add album due to user already bought it', () => {
const albumId = 1;
albumMocks.mockGetAlbumOK(albumId, albumsFactory.responseAlbumOK);
albumMocks.mockGetAlbumOK(albumId, albumsFactory.responseAlbumOK);
return createUser().then(({ data }) =>
mutations.buyAlbum(undefined, { albumId, user: data.createUser }).then(() =>
mutations.buyAlbum(undefined, { albumId, user: data.createUser }).catch(error => {
expect(error.message).toEqual(
`Failed to buy album. Error: The user has already bought album with id ${albumId}`
);
})
)
);
});

it('should fail to add album due album not found', () => {
const albumId = 'abc';
albumMocks.mockGetAlbumNotFound(albumId);
return createUser().then(({ data }) =>
mutations.buyAlbum(undefined, { albumId, user: data.createUser }).catch(error => {
expect(error.message).toEqual('Failed to buy album. Error: Item not found in external api');
})
);
});
});
});
});
2 changes: 1 addition & 1 deletion test/server.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { createTestClient } = require('apollo-server-testing'),
{ ApolloServer } = require('apollo-server'),
schema = require('../app/graphql');
{ schema } = require('../app/graphql');

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

Expand Down
12 changes: 12 additions & 0 deletions test/utils/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const usersFactory = require('./factories/user');
const { mutate } = require('../server.spec');
const { createUser, logIn } = require('../users/graphql');

exports.createUserAndLogIn = () =>
usersFactory
.attributes()
.then(user =>
mutate(createUser(user)).then(() => mutate(logIn({ email: user.email, password: user.password })))
);

exports.createUser = () => usersFactory.attributes().then(user => mutate(createUser(user)));
6 changes: 6 additions & 0 deletions test/utils/mocks/albums.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ exports.mockGetAlbumsOK = (responseAlbums = albumsFactory.albumsArray) => {
.reply(200, responseAlbums);
};

exports.mockGetAlbumNotFound = albumId => {
nock(configAlbumsApi.endpoint)
.get(`${configAlbumsApi.routes.albums}/${albumId}`)
.reply(404, '404 - {}');
};

exports.mockGetPhotosOK = (albumId, responsePhotos = albumsFactory.photosArray) => {
nock(configAlbumsApi.endpoint)
.get(`${configAlbumsApi.routes.photos}`)
Expand Down

0 comments on commit f5d0812

Please sign in to comment.