diff --git a/.gitignore b/.gitignore index 6704566..b50e8d7 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dist # TernJS port file .tern-port + +.vscode diff --git a/src/cli/init.db.js b/src/cli/init.db.js index 9bffbfd..4baa161 100644 --- a/src/cli/init.db.js +++ b/src/cli/init.db.js @@ -9,6 +9,7 @@ const container = configureDependencyInjection(app); * @type {import('sequelize').Sequelize} mainDb */ const mainDb = container.get('Sequelize'); + container.get('AreaModel'); container.get('ClubModel'); diff --git a/src/config/associations.js b/src/config/associations.js new file mode 100644 index 0000000..19c602e --- /dev/null +++ b/src/config/associations.js @@ -0,0 +1,6 @@ +/** + * @param {import('sequelize').Sequelize} sequelizeInstance + */ +module.exports = async function setupAssociations(ClubModel, AreaModel) { + ClubModel.belongsTo(AreaModel, { foreignKey: 'area_id' }); +}; diff --git a/src/config/di.js b/src/config/di.js index 70e88bd..49e115f 100644 --- a/src/config/di.js +++ b/src/config/di.js @@ -9,6 +9,8 @@ const SequelizeStore = require('connect-session-sequelize')(session.Store); const { ClubController, ClubService, ClubRepository, ClubModel } = require('../module/club/module'); const { AreaController, AreaService, AreaRepository, AreaModel } = require('../module/area/module'); +const setupSequelizeModelAssociations = require('./associations'); + function configureMainSequelizeDatabase() { const sequelize = new Sequelize({ dialect: 'sqlite', @@ -53,8 +55,6 @@ function configureSession(container) { saveUninitialized: false, cookie: { maxAge: ONE_WEEK_IN_SECONDS }, }; - - sequelize.sync(); return session(sessionOptions); } @@ -90,9 +90,13 @@ function addCommonDefinitions(container) { */ function addClubModuleDefinitions(container) { container.addDefinitions({ - ClubController: object(ClubController).construct(get('Multer'), get('ClubService')), + ClubController: object(ClubController).construct( + get('Multer'), + get('ClubService'), + get('AreaService') + ), ClubService: object(ClubService).construct(get('ClubRepository')), - ClubRepository: object(ClubRepository).construct(get('ClubModel')), + ClubRepository: object(ClubRepository).construct(get('ClubModel'), get('AreaModel')), ClubModel: factory(configureClubModel), }); } @@ -112,7 +116,8 @@ function addAreaModuleDefinitions(container) { module.exports = function configureDI() { const container = new DIContainer(); addCommonDefinitions(container); - addClubModuleDefinitions(container); addAreaModuleDefinitions(container); + addClubModuleDefinitions(container); + setupSequelizeModelAssociations(container.get('ClubModel'), container.get('AreaModel')); return container; }; diff --git a/src/module/club/controller/__tests__/club.test.js b/src/module/club/controller/__tests__/club.test.js index 08a17e0..ad4709d 100644 --- a/src/module/club/controller/__tests__/club.test.js +++ b/src/module/club/controller/__tests__/club.test.js @@ -1,5 +1,6 @@ const ClubController = require('../clubController'); const Club = require('../../entity/club'); +const Area = require('../../../area/entity/area'); const serviceMock = { save: jest.fn(), @@ -7,7 +8,7 @@ const serviceMock = { getById: jest.fn(() => Promise.resolve({})), getAll: jest.fn(() => Promise.resolve([])), }; -const controller = new ClubController({}, serviceMock); +const controller = new ClubController({}, serviceMock, serviceMock); test('Index renderea index.html', async () => { const renderMock = jest.fn(); @@ -22,19 +23,47 @@ test('Index renderea index.html', async () => { }); }); -test('Create renderea form.html', () => { - const renderMock = jest.fn(); +test('Create muestra un error si no hay áreas en el sistema', async () => { + const mockRes = { redirect: jest.fn() }; + const mockReq = { session: {} }; + await controller.create(mockReq, mockRes); + expect(mockRes.redirect).toHaveBeenCalledTimes(1); + expect(mockReq.session.errors).toEqual(['Para crear un club, primero debe crear un área']); +}); - controller.create({}, { render: renderMock }); +test('Create renderea form.html', async () => { + const renderMock = jest.fn(); + const mockAreasData = [new Area({ id: 1, name: 'Argentina' })]; + serviceMock.getAll.mockImplementationOnce(() => mockAreasData); + await controller.create({}, { render: renderMock }); expect(renderMock).toHaveBeenCalledTimes(1); - expect(renderMock).toHaveBeenCalledWith('club/view/form.html'); + expect(renderMock).toHaveBeenCalledWith('club/view/form.html', { + data: { areas: mockAreasData }, + }); }); test('Save llama al servicio con el body y redirecciona a /club', async () => { const redirectMock = jest.fn(); const FAKE_CREST_URL = 'ejemplo/escudo.png'; - const bodyMock = { id: 1, crestUrl: FAKE_CREST_URL }; + const bodyMock = new Club({ + Area: { + id: NaN, + name: undefined, + }, + address: undefined, + clubColors: undefined, + crestUrl: 'ejemplo/escudo.png', + email: undefined, + founded: undefined, + id: 1, + name: undefined, + phone: undefined, + shortName: undefined, + tla: undefined, + venue: undefined, + website: undefined, + }); await controller.save( { body: bodyMock, file: { path: FAKE_CREST_URL }, session: {} }, diff --git a/src/module/club/controller/clubController.js b/src/module/club/controller/clubController.js index b072cb8..ace016c 100644 --- a/src/module/club/controller/clubController.js +++ b/src/module/club/controller/clubController.js @@ -5,18 +5,21 @@ const AbstractController = require('../../abstractController'); module.exports = class ClubController extends AbstractController { /** * @param {import('../service/clubService')} clubService + * @param {import('../../area/service/areaService')} areaService */ - constructor(uploadMiddleware, clubService) { + constructor(uploadMiddleware, clubService, areaService) { super(); + this.ROUTE_BASE = '/club'; this.uploadMiddleware = uploadMiddleware; this.clubService = clubService; + this.areaService = areaService; } /** * @param {import('express').Application} app */ configureRoutes(app) { - const ROUTE = '/club'; + const ROUTE = this.ROUTE_BASE; // Nota: el `bind` es necesario porque estamos atando el callback a una función miembro de esta clase // y no a la clase en si. @@ -45,7 +48,14 @@ module.exports = class ClubController extends AbstractController { * @param {import('express').Response} res */ async create(req, res) { - res.render('club/view/form.html'); + const areas = await this.areaService.getAll(); + + if (areas.length > 0) { + res.render('club/view/form.html', { data: { areas } }); + } else { + req.session.errors = ['Para crear un club, primero debe crear un área']; + res.redirect(this.ROUTE_BASE); + } } /** @@ -60,7 +70,8 @@ module.exports = class ClubController extends AbstractController { try { const club = await this.clubService.getById(id); - res.render('club/view/form.html', { data: { club } }); + const areas = await this.areaService.getAll(); + res.render('club/view/form.html', { data: { club, areas } }); } catch (e) { req.session.errors = [e.message]; res.redirect('/club'); diff --git a/src/module/club/entity/club.js b/src/module/club/entity/club.js index 45b0b62..4049464 100644 --- a/src/module/club/entity/club.js +++ b/src/module/club/entity/club.js @@ -12,6 +12,7 @@ module.exports = class Club { founded, clubColors, venue, + Area, }) { this.id = id; this.name = name; @@ -25,5 +26,9 @@ module.exports = class Club { this.founded = founded; this.clubColors = clubColors; this.venue = venue; + /** + * @type {import('../../area/entity/area');} this.Area + */ + this.Area = Area; } }; diff --git a/src/module/club/mapper/clubMapper.js b/src/module/club/mapper/clubMapper.js index f9cff19..3a0d4f9 100644 --- a/src/module/club/mapper/clubMapper.js +++ b/src/module/club/mapper/clubMapper.js @@ -4,6 +4,7 @@ // map from Model to Entity const Club = require('../entity/club'); +const Area = require('../../area/entity/area'); /** * @@ -23,6 +24,8 @@ function fromDataToEntity({ founded, 'club-colors': clubColors, venue, + // eslint-disable-next-line camelcase + area_id, }) { return new Club({ id: Number(id), @@ -37,6 +40,7 @@ function fromDataToEntity({ founded, clubColors, venue, + Area: new Area({ id: Number(area_id) }), }); } diff --git a/src/module/club/repository/sqlite/__test__/clubRepository.test.js b/src/module/club/repository/sqlite/__test__/clubRepository.test.js index 430ace6..00865c7 100644 --- a/src/module/club/repository/sqlite/__test__/clubRepository.test.js +++ b/src/module/club/repository/sqlite/__test__/clubRepository.test.js @@ -1,6 +1,7 @@ const { Sequelize } = require('sequelize'); const ClubRepository = require('../clubRepository'); const ClubModel = require('../clubModel'); +const AreaModel = require('../../../../area/repository/sqlite/areaModel'); const ClubEntity = require('../../../entity/club'); const ClubNotFoundError = require('../../error/clubNotFoundError'); const ClubIdNotDefinedError = require('../../error/clubIdNotDefinedError'); @@ -25,10 +26,15 @@ const sampleClub = new ClubEntity({ clubColors: 'Red / White', venue: 'Emirates Stadium', lastUpdated: '2020-05-14T02:41:34Z', + Area: { id: 1 }, }); beforeAll(() => { - repository = new ClubRepository(ClubModel.setup(sequelizeInstance)); + const club = ClubModel.setup(sequelizeInstance); + const area = AreaModel.setup(sequelizeInstance); + club.belongsTo(area); + + repository = new ClubRepository(club, area); }); beforeEach(async (done) => { @@ -48,6 +54,7 @@ test('Actualiza un equipo cuando la entidad tiene un id', async () => { expect(newClub.id).toEqual(NEW_AUTOGENERATED_ID); newClub.name = 'Independiente'; + console.log(newClub); const modifiedClub = await repository.save(newClub); expect(modifiedClub.id).toEqual(NEW_AUTOGENERATED_ID); expect(modifiedClub.name).toEqual('Independiente'); diff --git a/src/module/club/repository/sqlite/clubRepository.js b/src/module/club/repository/sqlite/clubRepository.js index 83abe99..5f20da9 100644 --- a/src/module/club/repository/sqlite/clubRepository.js +++ b/src/module/club/repository/sqlite/clubRepository.js @@ -6,10 +6,12 @@ const ClubIdNotDefinedError = require('../error/clubIdNotDefinedError'); module.exports = class ClubRepository extends AbstractClubRepository { /** * @param {typeof import('./clubModel')} clubModel + * * @param {typeof import('../../../area/repository/sqlite/areaModel')} areaModel */ - constructor(clubModel) { + constructor(clubModel, areaModel) { super(); this.clubModel = clubModel; + this.areaModel = areaModel; } /** @@ -18,11 +20,12 @@ module.exports = class ClubRepository extends AbstractClubRepository { */ async save(club) { let clubModel; - if (!club.id) { - clubModel = await this.clubModel.create(club); - } else { - clubModel = await this.clubModel.build(club, { isNewRecord: false }).save(); - } + + const buildOptions = { isNewRecord: !club.id, include: this.areaModel }; + clubModel = this.clubModel.build(club, buildOptions); + clubModel.setDataValue('area_id', club.Area.id); + clubModel = await clubModel.save(); + return fromModelToEntity(clubModel); } @@ -43,7 +46,10 @@ module.exports = class ClubRepository extends AbstractClubRepository { * @returns {Promise} */ async getById(id) { - const clubModel = await this.clubModel.findOne({ where: { id } }); + const clubModel = await this.clubModel.findOne({ + where: { id }, + include: this.areaModel, + }); if (!clubModel) { throw new ClubNotFoundError(`No se encontró club con id ${id}`); diff --git a/src/module/club/view/form.html b/src/module/club/view/form.html index 4aadf4e..14c88da 100644 --- a/src/module/club/view/form.html +++ b/src/module/club/view/form.html @@ -2,6 +2,7 @@ {% block body %} {% set club = data.club %} +{% set areas = data.areas %}
@@ -62,6 +63,23 @@

+
+ +
+
+ +
+ + + +
+
+