diff --git a/.github/workflows/treetracker-api-build-deploy-dev.yml b/.github/workflows/treetracker-api-build-deploy-dev.yml index a8a46fd..88faa6d 100644 --- a/.github/workflows/treetracker-api-build-deploy-dev.yml +++ b/.github/workflows/treetracker-api-build-deploy-dev.yml @@ -13,7 +13,7 @@ jobs: name: Test runs-on: ubuntu-latest container: node:10.18-jessie - + # Service containers to run with `container-job` services: # Label used to access the service container @@ -29,7 +29,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - + if: | !contains(github.event.head_commit.message, 'skip-ci') steps: @@ -50,7 +50,6 @@ jobs: env: DATABASE_URL: postgresql://postgres:postgres@postgres/postgres - release: name: Release needs: test @@ -58,41 +57,41 @@ jobs: if: | !contains(github.event.head_commit.message, 'skip-ci') && github.event_name == 'push' && - github.repository == "Greenstand/${{ github.event.repository.name }}" + github.repository_owner == 'Greenstand' steps: - uses: actions/checkout@v2 - + - name: Use Node.js 16.x uses: actions/setup-node@v1 with: node-version: '16.x' - + - name: npm clean install run: npm ci working-directory: ${{ env.project-directory }} - + - run: npm i -g semantic-release @semantic-release/{git,exec,changelog} - + - run: semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: get-npm-version id: package-version uses: martinbeentjes/npm-get-version-action@master - + - name: Set up QEMU uses: docker/setup-qemu-action@v1 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - + - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Build snapshot and push on merge id: docker_build_release uses: docker/build-push-action@v2 @@ -101,12 +100,12 @@ jobs: file: ./Dockerfile push: true tags: greenstand/${{ github.event.repository.name }}:${{ steps.package-version.outputs.current-version }} - + - id: export_bumped_version run: | export BUMPED_VERSION="${{ steps.package-version.outputs.current-version }}" echo "::set-output name=bumped_version::${BUMPED_VERSION}" - + outputs: bumped_version: ${{ steps.export_bumped_version.outputs.bumped_version }} @@ -117,7 +116,7 @@ jobs: if: | !contains(github.event.head_commit.message, 'skip-ci') && github.event_name == 'push' && - github.repository == "Greenstand/${{ github.event.repository.name }}" + github.repository_owner == 'Greenstand' steps: - uses: actions/checkout@v2 - name: Install kustomize diff --git a/.github/workflows/treetracker-api-deploy-prod.yml b/.github/workflows/treetracker-api-deploy-prod.yml index 44c4619..37e4194 100644 --- a/.github/workflows/treetracker-api-deploy-prod.yml +++ b/.github/workflows/treetracker-api-deploy-prod.yml @@ -15,7 +15,7 @@ jobs: name: Deploy latest to prod environment, requires approval runs-on: ubuntu-latest if: | - github.repository == "Greenstand/${{ github.event.repository.name }}" + github.repository_owner == 'Greenstand' steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/treetracker-api-deploy-test.yml b/.github/workflows/treetracker-api-deploy-test.yml index 95f6aff..d9e75a8 100644 --- a/.github/workflows/treetracker-api-deploy-test.yml +++ b/.github/workflows/treetracker-api-deploy-test.yml @@ -15,7 +15,7 @@ jobs: name: Deploy latest to test environment, requires approval runs-on: ubuntu-latest if: | - github.repository == "Greenstand/${{ github.event.repository.name }}" + github.repository_owner == 'Greenstand' steps: - uses: actions/checkout@v2 with: diff --git a/api-tests/stakeholder-api.spec.js b/api-tests/stakeholder-api.spec.js index ce3f5b5..cbeef57 100644 --- a/api-tests/stakeholder-api.spec.js +++ b/api-tests/stakeholder-api.spec.js @@ -3,13 +3,17 @@ const request = require('supertest'); const { expect } = require('chai'); const server = require('../server/app'); const stakeholderSeed = require('../database/seeds/11_story_stakeholder'); -const knex = require('../database/connection'); +const knex = require('../server/database/knex'); describe('Stakeholder API tests.', () => { before(async () => { await stakeholderSeed.seed(knex); }); + after(async () => { + await knex('stakeholder').del(); + }); + describe('Stakeholder GET', () => { it(`Should raise validation error with error code 422 -- 'id' should be a uuid `, function (done) { request(server) @@ -34,7 +38,12 @@ describe('Stakeholder API tests.', () => { .expect(200) .end(function (err, res) { if (err) return done(err); - expect(res.body).to.have.keys(['stakeholders', 'totalCount']); + expect(res.body).to.have.keys([ + 'stakeholders', + 'totalCount', + 'links', + 'query', + ]); expect(res.body.totalCount).to.eq(1); expect(res.body.stakeholders[0]).to.eql({ ...stakeholderSeed.stakeholderOne, @@ -54,8 +63,13 @@ describe('Stakeholder API tests.', () => { .expect(200) .end(function (err, res) { if (err) return done(err); - expect(res.body).to.have.keys(['stakeholders', 'totalCount']); - expect(res.body.totalCount).to.be.greaterThanOrEqual(2); + expect(res.body).to.have.keys([ + 'stakeholders', + 'totalCount', + 'links', + 'query', + ]); + expect(res.body.totalCount).to.eql(2); expect(res.body.stakeholders).to.have.length(res.body.totalCount); return done(); }); diff --git a/database/seeds/11_story_stakeholder.js b/database/seeds/11_story_stakeholder.js index 45b6cd8..67402ba 100644 --- a/database/seeds/11_story_stakeholder.js +++ b/database/seeds/11_story_stakeholder.js @@ -40,7 +40,11 @@ const stakeholderThree = Object.freeze({ }); const seed = async function (knex) { - await knex('stakeholder').insert([stakeholderOne, stakeholderTwo]); + await knex('stakeholder').insert([ + stakeholderOne, + stakeholderTwo, + stakeholderThree, + ]); }; module.exports = { diff --git a/server/models/Session.js b/server/database/Session.js similarity index 96% rename from server/models/Session.js rename to server/database/Session.js index f17b528..3dfea06 100644 --- a/server/models/Session.js +++ b/server/database/Session.js @@ -6,7 +6,7 @@ * */ -const knex = require('../database/knex'); +const knex = require('./knex'); class Session { constructor() { diff --git a/server/handlers/stakeholderHandler.js b/server/handlers/stakeholderHandler.js index af46280..feb362e 100644 --- a/server/handlers/stakeholderHandler.js +++ b/server/handlers/stakeholderHandler.js @@ -1,20 +1,9 @@ const Joi = require('joi'); -const log = require('loglevel'); - +const StakeholderService = require('../services/StakeholderService'); const { - getAllStakeholdersById, - getAllStakeholders, - createStakeholder, - deleteStakeholder, - updateStakeholder, - // getRelations, - createRelation, - deleteRelation, -} = require('../models/Stakeholder'); - -const Session = require('../models/Session'); - -const StakeholderRepository = require('../repositories/StakeholderRepository'); + getFilterAndLimitOptions, + generatePrevAndNext, +} = require('../utils/helper'); const stakeholderGetQuerySchema = Joi.object({ id: Joi.string().uuid(), @@ -27,9 +16,7 @@ const stakeholderGetQuerySchema = Joi.object({ email: Joi.string(), phone: Joi.string(), website: Joi.string(), - children: Joi.array(), - parents: Joi.array(), - filter: Joi.object(), + search: Joi.string(), }).unknown(false); const stakeholderPostSchema = Joi.object({ @@ -43,41 +30,68 @@ const stakeholderDeleteSchema = Joi.object({ type: Joi.string(), }).unknown(); -const stakeholderGetAll = async (req, res, next) => { - const filter = req.query.filter ? JSON.parse(req.query.filter) : {}; - const query = { ...req.query, filter }; - await stakeholderGetQuerySchema.validateAsync(query, { +const updateStakeholderSchema = Joi.object({ + id: Joi.string().uuid().required(), + type: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), +}).unknown(true); + +const stakeholderGetAll = async (req, res) => { + await stakeholderGetQuerySchema.validateAsync(req.query, { abortEarly: false, }); - const session = new Session(); - const repo = new StakeholderRepository(session); - const executeGetAll = getAllStakeholders(repo); - try { - const result = await executeGetAll(query); - res.send(result); - res.end(); - } catch (e) { - next(e); - } + + const { filter, limitOptions } = getFilterAndLimitOptions(req.query); + const stakeholderService = new StakeholderService(); + + const { stakeholders, totalCount } = + await stakeholderService.getAllStakeholders(filter, limitOptions); + + const url = 'stakeholders'; + + const links = generatePrevAndNext({ + url, + count: totalCount, + limitOptions, + queryObject: { ...filter, ...limitOptions }, + }); + + res.send({ + stakeholders, + links, + totalCount, + query: { ...limitOptions, ...filter }, + }); }; -const stakeholderGetAllById = async function (req, res, next) { - const filter = req.query.filter ? JSON.parse(req.query.filter) : {}; - const query = { ...req.query, filter }; - await stakeholderGetQuerySchema.validateAsync(query, { +const stakeholderGetAllById = async (req, res) => { + await stakeholderGetQuerySchema.validateAsync(req.query, { abortEarly: false, }); const { id } = req.params; - const session = new Session(false); - const repo = new StakeholderRepository(session); - const executeGetById = getAllStakeholdersById(repo, id); - try { - const result = await executeGetById(query); - res.send(result); - res.end(); - } catch (e) { - next(e); - } + + const { filter, limitOptions } = getFilterAndLimitOptions(req.query); + const stakeholderService = new StakeholderService(); + + const { stakeholders, totalCount } = + await stakeholderService.getAllStakeholdersById(id, filter, limitOptions); + + const url = `stakeholders/${id}`; + + const links = generatePrevAndNext({ + url, + count: totalCount, + limitOptions, + queryObject: { ...filter, ...limitOptions }, + }); + + res.send({ + stakeholders, + links, + totalCount, + query: { ...limitOptions, ...filter }, + }); }; // const stakeholderGetRelations = async function (req, res, next) { @@ -149,96 +163,42 @@ const stakeholderGetAllById = async function (req, res, next) { // } // }; -const stakeholderCreate = async function (req, res, next) { +const stakeholderCreate = async function (req, res) { + const requestObject = await stakeholderPostSchema.validateAsync(req.body, { + abortEarly: false, + }); const { id } = req.params || req.body.relation_id; - const session = new Session(); - const repo = new StakeholderRepository(session); - const url = `${req.protocol}://${req.get('host')}/stakeholder`; - - const executeCreate = createStakeholder(repo, id); - const executeCreateRelation = createRelation(repo, id); - const executeGetAll = id - ? getAllStakeholdersById(repo, id) - : getAllStakeholders(repo); - - try { - const value = await stakeholderPostSchema.validateAsync(req.body, { - abortEarly: false, - }); - const data = await executeCreate(value); - await executeCreateRelation({ - type: value.relation, - data: { ...data, relation_id: value.relation_id }, - }); - const result = await executeGetAll({ filter: {} }, url); + const stakeholderService = new StakeholderService(); + const result = await stakeholderService.createStakeholder(id, requestObject); - res.status(201).json(result); - } catch (e) { - log.error(e); - next(e); - } + res.status(201).json(result); }; -const stakeholderDelete = async function (req, res, next) { +const stakeholderDelete = async function (req, res) { + const requestObject = await stakeholderDeleteSchema.validateAsync(req.body, { + abortEarly: false, + }); const { id } = req.params || req.body.relation_id; - const session = new Session(); - const repo = new StakeholderRepository(session); - const executeDelete = deleteStakeholder(repo, id); - const executeDeleteRelation = deleteRelation(repo, id); - const executeGetAll = id - ? getAllStakeholdersById(repo, id) - : getAllStakeholders(repo); + const stakeholderService = new StakeholderService(); + const result = await stakeholderService.deleteStakeholder(id, requestObject); - try { - const value = await stakeholderDeleteSchema.validateAsync(req.body, { + res.status(200).json(result); +}; + +const stakeholderUpdate = async function (req, res) { + const requestObject = await updateStakeholderSchema + .unknown(true) + .validateAsync(req.body, { abortEarly: false, }); - const url = `${req.protocol}://${req.get('host')}/stakeholder`; - delete value.data.parents; - delete value.data.children; - await executeDelete(value.data); - await executeDeleteRelation({ - type: value.type, - data: value.data, - }); - const result = await executeGetAll({ filter: {} }, url); - - res.status(201).json(result); - } catch (e) { - log.error(e); - next(e); - } -}; -const stakeholderUpdate = async function (req, res, next) { const { id } = req.params; - const session = new Session(); - const repo = new StakeholderRepository(session); - const executeUpdateStakeholder = updateStakeholder(repo, id); - - const updateStakeholderSchema = Joi.object({ - id: Joi.string().uuid().required(), - type: Joi.string().required(), - email: Joi.string().required(), - phone: Joi.string().required(), - }); - - try { - const value = await updateStakeholderSchema - .unknown(true) - .validateAsync(req.body, { - abortEarly: false, - }); - - const result = await executeUpdateStakeholder(value); + const stakeholderService = new StakeholderService(); + const result = stakeholderService.updateStakeholder(id, requestObject); - res.send(result); - res.end(); - } catch (e) { - next(e); - } + res.status(200).json(result); }; module.exports = { diff --git a/server/models/Stakeholder.js b/server/models/Stakeholder.js index 00b5d70..f161229 100644 --- a/server/models/Stakeholder.js +++ b/server/models/Stakeholder.js @@ -1,46 +1,11 @@ -/* eslint-disable no-param-reassign */ -/* eslint-disable prefer-destructuring */ - -const StakeholderPostObject = ({ - type, - org_name, - first_name, - last_name, - email, - phone, - website, - logo_url, - map, -}) => { - return Object.freeze({ - type, - org_name, - first_name, - last_name, - email, - phone, - website, - logo_url, - map, - }); -}; - -const StakeholderTree = ({ - id, - type, - org_name, - first_name, - last_name, - email, - phone, - website, - logo_url, - map, - children = [], - parents = [], -}) => { - return Object.freeze({ - id, +const StakeholderRepository = require('../repositories/StakeholderRepository'); + +class Stakeholder { + constructor(session) { + this._stakeholderRepository = new StakeholderRepository(session); + } + + static StakeholderPostObject({ type, org_name, first_name, @@ -50,295 +15,310 @@ const StakeholderTree = ({ website, logo_url, map, - children, - parents, - }); -}; - -const FilterCriteria = ({ - id = null, - type = null, - org_name = null, - first_name = null, - last_name = null, - image_url = null, - email = null, - phone = null, - website = null, - logo_url = null, - map = null, - search = null, -}) => { - return Object.entries({ + }) { + return Object.freeze({ + type, + org_name, + first_name, + last_name, + email, + phone, + website, + logo_url, + map, + }); + } + + static StakeholderTree({ id, type, org_name, first_name, last_name, - image_url, email, phone, website, logo_url, map, - search, - }) - .filter( - (entry) => entry[1] !== undefined && entry[1] !== null && entry[1] !== '', - ) - .reduce((result, item) => { - result[item[0]] = item[1]; - return result; - }, {}); -}; - -const createRelation = (repo, org_id) => async (stakeholder) => { - // eslint-disable-next-line no-use-before-define - const id = await getUUID(repo, org_id); - const { type, data } = stakeholder; - const insertObj = {}; - - if (type === 'parents' || type === 'children') { - // assign parent and child ids, but if the id is null, look for the relation_id on the data - insertObj.parent_id = type === 'parents' ? data.id : id || data.relation_id; - insertObj.child_id = type === 'children' ? data.id : id || data.relation_id; + children = [], + parents = [], + }) { + return Object.freeze({ + id, + type, + org_name, + first_name, + last_name, + email, + phone, + website, + logo_url, + map, + children, + parents, + }); } - const stakeholderRelation = await repo.createRelation(insertObj); - - return stakeholderRelation; -}; - -async function getUUIDbyId(repo, id, options = { limit: 100, offset: 0 }) { - // get organization from old entity table - const { stakeholders } = await repo.getStakeholderByOrganizationId( - id, - options, - ); - - const org_id = stakeholders[0].stakeholder_uuid; - const exists = await repo.getById(org_id); - - if (!exists) { - const updates = stakeholders.map(async (entity, i) => { - const foundStakeholder = await repo.getById(entity.stakeholder_uuid); - const executeCreateRelation = createRelation(repo, org_id); - - if (!foundStakeholder) { - // console.log('entity to create', entity); - // map from entity fields to stakeholder fields - const stakeholderObj = { - id: entity.stakeholder_uuid, - type: entity.type === 'O' ? 'Organization' : 'Person', - org_name: entity.name, - first_name: entity.first_name, - last_name: entity.last_name, - email: entity.email, - phone: entity.phone, - logo_url: entity.logo_url, - map: entity.map_name, - website: entity.website, - }; - - const stakeholder = await repo.createStakeholder(stakeholderObj); - - if (i > 0) { - // if there are relations, create links - await executeCreateRelation({ - type: 'children', - data: stakeholder, - }); - } - } - }); - await Promise.all(updates); + stakeholderTree(object) { + return this.constructor.StakeholderTree(object); } - return org_id; -} + async createRelation(org_id, stakeholder) { + const id = await this.getUUID(org_id); + const { type, data } = stakeholder; + const insertObj = {}; + + if (type === 'parents' || type === 'children') { + // assign parent and child ids, but if the id is null, look for the relation_id on the data + insertObj.parent_id = + type === 'parents' ? data.id : id || data.relation_id; + insertObj.child_id = + type === 'children' ? data.id : id || data.relation_id; + } -async function getUUID(repo, org_id) { - const orgId = Number(org_id); - // get organization from old entity table - return org_id === null || Number.isNaN(orgId) - ? org_id - : getUUIDbyId(repo, orgId); -} + const stakeholderRelation = + await this._stakeholderRepository.createRelation(insertObj); + + return stakeholderRelation; + } -const getRelationTrees = async (stakeholders, repo) => - Promise.all( - stakeholders.map(async (stakeholder) => { - const parents = await repo.getParents(stakeholder); - // don't want to keep getting all of the parents and children recursively, but do want to - // include the current stakeholder as parent/child - - stakeholder.parents = parents.map((parent) => { - parent.parents = []; - parent.children = [{ ...stakeholder }]; - return parent; + async getUUIDbyId(id) { + // get organization from old entity table + const { stakeholders } = + await this._stakeholderRepository.getStakeholderByOrganizationId(id); + + const org_id = stakeholders[0].stakeholder_uuid; + const exists = await this._stakeholderRepository.getById(org_id); + + if (!exists) { + const updates = stakeholders.map(async (entity, i) => { + const foundStakeholder = await this._stakeholderRepository.getById( + entity.stakeholder_uuid, + ); + + if (!foundStakeholder) { + // map from entity fields to stakeholder fields + const stakeholderObj = { + id: entity.stakeholder_uuid, + type: entity.type === 'O' ? 'Organization' : 'Person', + org_name: entity.name, + first_name: entity.first_name, + last_name: entity.last_name, + email: entity.email, + phone: entity.phone, + logo_url: entity.logo_url, + map: entity.map_name, + website: entity.website, + }; + + const stakeholder = + await this._stakeholderRepository.createStakeholder(stakeholderObj); + + if (i > 0) { + // if there are relations, create links + await this.createRelation(org_id, { + type: 'children', + data: stakeholder, + }); + } + } }); + await Promise.all(updates); + } - const children = await repo.getChildren(stakeholder); + return org_id; + } - stakeholder.children = children.map((child) => { - child.parents = [{ ...stakeholder }]; - child.children = []; - return child; - }); + async getUUID(org_id) { + const orgId = +org_id; + // get organization from old entity table + return orgId ? this.getUUIDbyId(orgId) : org_id; + } - return stakeholder; - }), - ); + async getRelationTrees(stakeholders) { + return Promise.all( + stakeholders.map(async (stakeholder) => { + const stakeholderCopy = { ...stakeholder }; + const parents = await this._stakeholderRepository.getParents( + stakeholderCopy, + ); + // don't want to keep getting all of the parents and children recursively, but do want to + // include the current stakeholder as parent/child + + stakeholderCopy.parents = parents.map((parent) => { + const parentCopy = { ...parent }; + parentCopy.parents = []; + parentCopy.children = [{ ...stakeholderCopy }]; + return parentCopy; + }); + + const children = await this._stakeholderRepository.getChildren( + stakeholderCopy, + ); + + stakeholderCopy.children = children.map((child) => { + const childCopy = { ...child }; + childCopy.parents = [{ ...stakeholderCopy }]; + childCopy.children = []; + return childCopy; + }); + + return stakeholderCopy; + }), + ); + } -const getAllStakeholders = - (repo) => - async ({ filter: { where }, ...idFilters } = undefined) => { - const filter = FilterCriteria({ ...idFilters, ...where }); + async getAllStakeholders(filter, limitOptions) { let dbStakeholders; let count; if (Object.keys(filter).length > 0) { - const { stakeholders, count: dbCount } = await repo.getFilter(filter); + const { stakeholders, count: dbCount } = + await this._stakeholderRepository.getFilter(filter, limitOptions); dbStakeholders = stakeholders; count = dbCount; } else { - const { stakeholders, count: dbCount } = await repo.getAllStakeholders(); + const { stakeholders, count: dbCount } = + await this._stakeholderRepository.getAllStakeholders(limitOptions); dbStakeholders = stakeholders; count = dbCount; } - const stakeholders = await getRelationTrees(dbStakeholders, repo); + const stakeholders = await this.getRelationTrees(dbStakeholders); return { stakeholders: stakeholders && stakeholders.map((row) => { - return StakeholderTree({ ...row }); + return this.stakeholderTree({ ...row }); }), totalCount: count, }; - }; + } -const getAllStakeholdersById = - (repo, org_id) => - async ({ filter: { where }, ...idFilters } = undefined) => { - const filter = FilterCriteria({ ...idFilters, ...where }); - const id = await getUUID(repo, org_id); + async getAllStakeholdersById(org_id, filter, limitOptions) { + const id = await this.getUUID(org_id); let dbStakeholders; let count = 0; if (Object.keys(filter).length > 0) { - const { stakeholders, count: dbCount } = await repo.getFilterById( - id, - filter, - ); + const { stakeholders, count: dbCount } = + await this._stakeholderRepository.getFilterById( + id, + filter, + limitOptions, + ); dbStakeholders = stakeholders; count = dbCount; } else { const { stakeholders, count: dbCount } = - await repo.getAllStakeholdersById(id); + await this._stakeholderRepository.getAllStakeholdersById( + id, + limitOptions, + ); dbStakeholders = stakeholders; count = dbCount; } - const stakeholders = await getRelationTrees(dbStakeholders, repo); + const stakeholders = await this.getRelationTrees(dbStakeholders); return { stakeholders: stakeholders && stakeholders.map((row) => { - return StakeholderTree({ ...row }); + return this.stakeholderTree({ ...row }); }), totalCount: count, }; - }; - -const getRelations = (repo, current_id) => async () => { - const { stakeholders, count } = await repo.getRelations(current_id); - - return { - stakeholders: - stakeholders && - stakeholders.map((row) => { - row.children = []; - row.parents = []; - return StakeholderTree({ ...row }); - }), - totalCount: count, - }; -}; - -// SAVE IN CASE WE NEED TO ADD AGAIN -// const getNonRelations = (repo, current_id) => async (org_id) => { -// const id = await getUUID(repo, org_id); -// const { stakeholders, count } = await repo.getNonRelations(current_id, id); - -// return { -// stakeholders: -// stakeholders && -// stakeholders.map((row) => { -// row.children = []; -// row.parents = []; -// return StakeholderTree({ ...row }); -// }), -// totalCount: count, -// }; -// }; - -const deleteRelation = (repo, current_id) => async (stakeholder) => { - const id = await getUUID(repo, current_id); - const { type, data } = stakeholder; - const removeObj = {}; - - if (type === 'parents' || type === 'children') { - removeObj.parent_id = type === 'parents' ? data.id : id; - removeObj.child_id = type === 'children' ? data.id : id; } - const stakeholderRelation = await repo.deleteRelation(removeObj); + async getRelations(current_id) { + const { stakeholders, count } = + await this._stakeholderRepository.getRelations(current_id); - return stakeholderRelation; -}; + return { + stakeholders: + stakeholders && + stakeholders.map((row) => { + const rowCopy = { ...row }; + rowCopy.children = []; + rowCopy.parents = []; + return this.stakeholderTree({ ...rowCopy }); + }), + totalCount: count, + }; + } + + async deleteRelation(current_id, stakeholder) { + const id = await this.getUUID(current_id); + const { type, data } = stakeholder; + const removeObj = {}; + + if (type === 'parents' || type === 'children') { + removeObj.parent_id = type === 'parents' ? data.id : id; + removeObj.child_id = type === 'children' ? data.id : id; + } + + const stakeholderRelation = + await this._stakeholderRepository.deleteRelation(removeObj); + + return stakeholderRelation; + } -const updateStakeholder = (repo) => async (data) => { - const editedStakeholder = StakeholderTree({ ...data }); + async updateStakeholder(data) { + const editedStakeholder = this.stakeholderTree({ ...data }); - // remove children and parents temporarily to update - const { children, parents, ...updateObj } = editedStakeholder; - const stakeholder = await repo.updateStakeholder(updateObj); + // remove children and parents temporarily to update + const { children, parents, ...updateObj } = editedStakeholder; + const stakeholder = await this._stakeholderRepository.updateStakeholder( + updateObj, + ); - return StakeholderTree({ ...stakeholder, children, parents }); -}; + return this.stakeholderTree({ ...stakeholder, children, parents }); + } -const createStakeholder = - (repo, org_id = null) => - async (newStakeholder) => { - const id = await getUUID(repo, org_id); - const stakeholderObj = StakeholderPostObject({ ...newStakeholder }); + async createStakeholder(org_id = null, newStakeholder) { + const id = await this.getUUID(org_id); + const stakeholderObj = this.constructor.StakeholderPostObject({ + ...newStakeholder, + }); - const stakeholder = await repo.createStakeholder(stakeholderObj, id); + // not sure what the id here is meant for but provision was not made for it in the repository + const stakeholder = await this._stakeholderRepository.createStakeholder( + stakeholderObj, + id, + ); - return StakeholderTree({ ...stakeholder }); - }; + return this.stakeholderTree({ ...stakeholder }); + } -const deleteStakeholder = (repo) => async (removeStakeholder) => { - const stakeholder = await repo.deleteStakeholder(removeStakeholder); + async deleteStakeholder(removeStakeholder) { + const stakeholder = await this._stakeholderRepository.deleteStakeholder( + removeStakeholder, + ); - return StakeholderTree({ ...stakeholder }); -}; + return this.stakeholderTree({ ...stakeholder }); + } + + // SAVE IN CASE WE NEED TO ADD AGAIN + // async getNonRelations(current_id, org_id) { + // const id = await this.getUUID(org_id); + // const { stakeholders, count } = + // await this._stakeholderRepository.getNonRelations(current_id, id); + + // return { + // stakeholders: + // stakeholders && + // stakeholders.map((row) => { + // row.children = []; + // row.parents = []; + // return this.stakeholderTree({ ...row }); + // }), + // totalCount: count, + // }; + // } +} -module.exports = { - getAllStakeholdersById, - getAllStakeholders, - createStakeholder, - deleteStakeholder, - updateStakeholder, - getRelations, - // getNonRelations, - createRelation, - deleteRelation, - StakeholderTree, - FilterCriteria, -}; +module.exports = Stakeholder; diff --git a/server/repositories/BaseRepository.spec.js b/server/repositories/BaseRepository.spec.js index 00c6506..8acbc22 100644 --- a/server/repositories/BaseRepository.spec.js +++ b/server/repositories/BaseRepository.spec.js @@ -4,7 +4,7 @@ const BaseRepository = require('./BaseRepository'); const knex = require('../database/knex'); const tracker = mockKnex.getTracker(); -const Session = require('../models/Session'); +const Session = require('../database/Session'); describe('BaseRepository', () => { let baseRepository; diff --git a/server/repositories/StakeholderRepository.js b/server/repositories/StakeholderRepository.js index e65772b..1de4790 100644 --- a/server/repositories/StakeholderRepository.js +++ b/server/repositories/StakeholderRepository.js @@ -9,7 +9,10 @@ class StakeholderRepository extends BaseRepository { } // RETURNS A FLAT LIST OF RELATED ORGS FROM OLD TABLE - async getStakeholderByOrganizationId(organization_id, options) { + async getStakeholderByOrganizationId( + organization_id, + options = { limit: 100, offset: 0 }, + ) { const result = await this._session .getDB() .raw( @@ -43,15 +46,24 @@ class StakeholderRepository extends BaseRepository { .first(); } - async getAllStakeholders() { + async getAllStakeholders(limitOptions) { // get only non-children to start building trees - const results = await this._session + let promise = this._session .getDB()('stakeholder as s') .select('s.*') .leftJoin('stakeholder_relation as sr', 's.id', 'sr.child_id') .whereNull('sr.child_id') .orderBy('s.org_name', 'asc'); + if (limitOptions?.limit) { + promise = promise.limit(limitOptions.limit); + } + if (limitOptions?.offset) { + promise = promise.limit(limitOptions.offset); + } + + const stakeholders = await promise; + // count all the stakeholders, regardless of nesting const count = await this._session.getDB()('stakeholder as s').count('*'); @@ -59,25 +71,34 @@ class StakeholderRepository extends BaseRepository { // .leftJoin('stakeholder_relation as sr', 's.id', 'sr.child_id') // .whereNull('sr.child_id'); - return { stakeholders: results, count: +count[0].count }; + return { stakeholders, count: +count[0].count }; } - async getAllStakeholdersById(id = null) { + async getAllStakeholdersById(id = null, limitOptions) { // get only non-children to start building trees - const results = await this._session + let promise = this._session .getDB()('stakeholder as s') .select('s.*') .leftJoin('stakeholder_relation as sr', 's.id', 'sr.child_id') .where('s.id', id) .orderBy('s.org_name', 'asc'); + if (limitOptions?.limit) { + promise = promise.limit(limitOptions.limit); + } + if (limitOptions?.offset) { + promise = promise.limit(limitOptions.offset); + } + + const stakeholders = await promise; + // count all the stakeholders, regardless of nesting const count = await this._session .getDB()(this._tableName) .count('*') .where('id', id); - return { stakeholders: results, count: +count[0].count }; + return { stakeholders, count: +count[0].count }; } // not currently being used but may be useful later @@ -150,17 +171,12 @@ class StakeholderRepository extends BaseRepository { return []; } - async getFilter(filter) { - const { search = '', org_name = '', ...rest } = filter; - - const stakeholders = await this._session - .getDB()(this._tableName) - .select('*') - .where(function () { - this.where({ ...rest }); - this.andWhere('org_name', 'ilike', `%${org_name}%`); - }) - .andWhere(function () { + async getFilter(filter, limitOptions) { + const whereBuilder = (object, builder) => { + const { search = '', org_name = '', ...rest } = object; + const result = builder; + result.where({ ...rest }).andWhere('org_name', 'ilike', `%${org_name}%`); + result.andWhere(function () { if (search) { this.where('org_name', 'ilike', `%${search}%`); this.orWhere('first_name', 'ilike', `%${search}%`); @@ -170,40 +186,56 @@ class StakeholderRepository extends BaseRepository { this.orWhere('website', 'ilike', `%${search}%`); this.orWhere('map', 'ilike', `%${search}%`); } - }) - .orderBy('org_name', 'asc'); + }); + + return result; + }; + + let promise = this._session + .getDB()(this._tableName) + .select('*') + .where((builder) => whereBuilder(filter, builder)) + .orderBy('org_name', 'asc') + .limit(limitOptions.limit) + .offset(limitOptions.offset); + + if (limitOptions?.limit) { + promise = promise.limit(limitOptions.limit); + } + if (limitOptions?.offset) { + promise = promise.limit(limitOptions.offset); + } + + const stakeholders = await promise; const count = await this._session .getDB()(this._tableName) .count('*') - .where(function () { - this.where({ ...rest }); - this.andWhere('org_name', 'ilike', `%${org_name}%`); - }) - .andWhere(function () { - if (search) { - this.where('org_name', 'ilike', `%${search}%`); - this.orWhere('first_name', 'ilike', `%${search}%`); - this.orWhere('last_name', 'ilike', `%${search}%`); - this.orWhere('email', 'ilike', `%${search}%`); - this.orWhere('phone', 'ilike', `%${search}%`); - this.orWhere('website', 'ilike', `%${search}%`); - this.orWhere('map', 'ilike', `%${search}%`); - } - }); + .where((builder) => whereBuilder(filter, builder)); return { stakeholders, count: +count[0].count }; } - async getFilterById(id, filter) { + async getFilterById(id, filter, limitOptions) { const relatedIds = await this.getRelatedIds(id); - const stakeholders = await this._session + let promise = this._session .getDB()(this._tableName) .select('*') .where((builder) => builder.whereIn('id', relatedIds)) .andWhere({ ...filter }) - .orderBy('org_name', 'asc'); + .orderBy('org_name', 'asc') + .limit(limitOptions.limit) + .offset(limitOptions.offset); + + if (limitOptions?.limit) { + promise = promise.limit(limitOptions.limit); + } + if (limitOptions?.offset) { + promise = promise.limit(limitOptions.offset); + } + + const stakeholders = promise; const count = await this._session .getDB()(this._tableName) diff --git a/server/repositories/StakeholderRepository.spec.js b/server/repositories/StakeholderRepository.spec.js index 4802ee4..432af48 100644 --- a/server/repositories/StakeholderRepository.spec.js +++ b/server/repositories/StakeholderRepository.spec.js @@ -4,7 +4,7 @@ const StakeholderRepository = require('./StakeholderRepository'); const knex = require('../database/knex'); const tracker = mockKnex.getTracker(); -const Session = require('../models/Session'); +const Session = require('../database/Session'); describe('StakeholderRepository', () => { let stakeholderRepository; diff --git a/server/services/.gitkeep b/server/services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/server/services/StakeholderService.js b/server/services/StakeholderService.js new file mode 100644 index 0000000..e78c734 --- /dev/null +++ b/server/services/StakeholderService.js @@ -0,0 +1,80 @@ +const Session = require('../database/Session'); +const Stakeholder = require('../models/Stakeholder'); + +class StakeholderService { + constructor() { + this._session = new Session(); + this._stakeholder = new Stakeholder(this._session); + } + + async getAllStakeholders(filter, limitOptions) { + return this._stakeholder.getAllStakeholders(filter, limitOptions); + } + + async getAllStakeholdersById(id, filter, limitOptions) { + return this._stakeholder.getAllStakeholdersById(id, filter, limitOptions); + } + + async createStakeholder(id, requestObject) { + try { + await this._session.beginTransaction(); + const createdStakeholder = await this._stakeholder.createStakeholder( + id, + requestObject, + ); + await this._stakeholder.createRelation(id, { + type: requestObject.relation, + data: { ...createdStakeholder, relation_id: requestObject.relation_id }, + }); + await this._session.commitTransaction(); + + return id ? this.getAllStakeholdersById(id) : this.getAllStakeholders(); + } catch (e) { + if (this._session.isTransactionInProgress()) { + await this._session.rollbackTransaction(); + } + throw e; + } + } + + async deleteStakeholder(id, requestObject) { + try { + await this._session.beginTransaction(); + // remove id?? id not used in the model + await this._stakeholder.deleteStakeholder(id, requestObject.data); + await this._stakeholder.deleteRelation(id, { + type: requestObject.type, + data: requestObject.data, + }); + await this._session.commitTransaction(); + + return id ? this.getAllStakeholdersById(id) : this.getAllStakeholders(); + } catch (e) { + if (this._session.isTransactionInProgress()) { + await this._session.rollbackTransaction(); + } + throw e; + } + } + + async updateStakeholder(id, requestObject) { + try { + await this._session.beginTransaction(); + // should it be {id, ...requestObject}??? + const result = await this._stakeholder.updateStakeholder( + id, + requestObject, + ); + await this._session.commitTransaction(); + + return result; + } catch (e) { + if (this._session.isTransactionInProgress()) { + await this._session.rollbackTransaction(); + } + throw e; + } + } +} + +module.exports = StakeholderService; diff --git a/server/utils/helper.js b/server/utils/helper.js new file mode 100644 index 0000000..9c042d4 --- /dev/null +++ b/server/utils/helper.js @@ -0,0 +1,49 @@ +const generatePrevAndNext = ({ + url, + count, + limitOptions: { limit, offset }, + queryObject, +}) => { + // offset starts from 0, hence the -1 + const noOfIterations = count / limit - 1; + const currentIteration = offset / limit; + + const queryObjectCopy = { ...queryObject }; + delete queryObjectCopy.offset; + + const query = Object.keys(queryObjectCopy) + .map((key) => `${key}=${encodeURIComponent(queryObjectCopy[key])}`) + .join('&'); + + const urlWithLimitAndOffset = `${url}?${query}&offset=`; + + const nextUrl = + currentIteration < noOfIterations + ? `${urlWithLimitAndOffset}${+offset + +limit}` + : null; + let prev = null; + if (offset - +limit >= 0) { + prev = `${urlWithLimitAndOffset}${+offset - +limit}`; + } + + return { next: nextUrl, prev }; +}; + +const getFilterAndLimitOptions = (query) => { + const filter = { ...query }; + const limitOptions = {}; + + const defaultRange = { limit: 100, offset: 0 }; + limitOptions.limit = +filter.limit || defaultRange.limit; + limitOptions.offset = +filter.offset || defaultRange.offset; + + delete filter.limit; + delete filter.offset; + + return { filter, limitOptions }; +}; + +module.exports = { + generatePrevAndNext, + getFilterAndLimitOptions, +}; diff --git a/server/utils/utils.js b/server/utils/utils.js index 6ff3331..8b44014 100644 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -28,7 +28,6 @@ exports.handlerWrapper = (fn) => const fnReturn = fn(...args); const next = args[args.length - 1]; return Promise.resolve(fnReturn).catch((e) => { - log.debug('handlerWrapper error:', e); next(e); }); };