diff --git a/app/models/company-contact.model.js b/app/models/company-contact.model.js new file mode 100644 index 0000000000..6d46c450f0 --- /dev/null +++ b/app/models/company-contact.model.js @@ -0,0 +1,39 @@ +'use strict' + +/** + * Model for company contact (crm_v2.company_contacts) + * @module CompanyContactModel + */ + +const { Model } = require('objection') + +const BaseModel = require('./base.model.js') + +class CompanyContactModel extends BaseModel { + static get tableName () { + return 'company_contacts' + } + + static get relationMappings () { + return { + companies: { + relation: Model.HasManyRelation, + modelClass: 'company.model', + join: { + from: 'company_contacts.companyId', + to: 'companies.id' + } + }, + contacts: { + relation: Model.HasManyRelation, + modelClass: 'contact.model', + join: { + from: 'company_contacts.contactId', + to: 'contacts.id' + } + } + } + } +} + +module.exports = CompanyContactModel diff --git a/db/migrations/legacy/20240513145729_water-add-company-contacts.js b/db/migrations/legacy/20240513145729_water-add-company-contacts.js new file mode 100644 index 0000000000..16b499bf6f --- /dev/null +++ b/db/migrations/legacy/20240513145729_water-add-company-contacts.js @@ -0,0 +1,43 @@ +'use strict' + +const tableName = 'company_contacts' + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex + .schema + .withSchema('crm_v2') + .createTable(tableName, (table) => { + // Primary Key + table.uuid('company_contact_id').primary().defaultTo(knex.raw('gen_random_uuid()')) + + // Data + table.boolean('is_default') + table.boolean('is_test') + table.boolean('water_abstraction_alerts_enabled') + table.date('end_date') + table.date('start_date') + table.string('email_address') + table.uuid('company_id') + table.uuid('contact_id') + table.uuid('role_id') + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex + .schema + .withSchema('crm_v2') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/public/20240513134550_create-company-contacts-view.js b/db/migrations/public/20240513134550_create-company-contacts-view.js new file mode 100644 index 0000000000..bad1d2f0e4 --- /dev/null +++ b/db/migrations/public/20240513134550_create-company-contacts-view.js @@ -0,0 +1,38 @@ +'use strict' + +const viewName = 'company_contacts' +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex + .schema + .createView(viewName, (view) => { + // NOTE: We have commented out unused columns from the source table + view.as(knex('company_contacts').withSchema('crm_v2').select([ + 'company_contacts.company_contact_id AS id', + 'company_contacts.company_id', + 'company_contacts.contact_id', + 'company_contacts.role_id', + 'company_contacts.date_created AS created_at', + 'company_contacts.date_updated AS updated_at' + // company_contacts.is_default + // company_contacts.email_address + // company_contacts.start_date + // company_contacts.end_date + // company_contacts.is_test + // company_contacts.water_abstraction_alerts_enabled + ])) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex + .schema + .dropViewIfExists(viewName) +} diff --git a/test/models/company-contacts.model.test.js b/test/models/company-contacts.model.test.js new file mode 100644 index 0000000000..8315a1b874 --- /dev/null +++ b/test/models/company-contacts.model.test.js @@ -0,0 +1,103 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = exports.lab = Lab.script() +const { expect } = Code + +// Test helpers +const CompanyContactHelper = require('../support/helpers/company-contact.helper.js') +const CompanyHelper = require('../support/helpers/company.helper.js') +const CompanyModel = require('../../app/models/company.model.js') +const ContactsHelper = require('../support/helpers/contact.helper.js') +const ContactsModel = require('../../app/models/contact.model.js') +const DatabaseSupport = require('../support/database.js') + +// Thing under test +const CompanyContactModel = require('../../app/models/company-contact.model.js') + +describe('Company Contacts model', () => { + let testRecord + + beforeEach(async () => { + await DatabaseSupport.clean() + }) + + describe('Basic query', () => { + beforeEach(async () => { + testRecord = await CompanyContactHelper.add() + }) + + it('can successfully run a basic query', async () => { + const result = await CompanyContactModel.query().findById(testRecord.id) + + expect(result).to.be.an.instanceOf(CompanyContactModel) + expect(result.id).to.equal(testRecord.id) + }) + }) + + describe('Relationships', () => { + describe('when linking to companies', () => { + let testCompany + beforeEach(async () => { + testRecord = await CompanyContactHelper.add() + + testCompany = await CompanyHelper.add({ + id: testRecord.companyId + }) + }) + + it('can successfully run a related query', async () => { + const query = await CompanyContactModel.query() + .innerJoinRelated('companies') + + expect(query).to.exist() + }) + + it('can eager load the companies', async () => { + const result = await CompanyContactModel.query() + .findById(testRecord.id) + .withGraphFetched('companies') + + expect(result).to.be.instanceOf(CompanyContactModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.companies).to.be.an.array() + expect(result.companies[0]).to.be.an.instanceOf(CompanyModel) + expect(result.companies).to.include(testCompany) + }) + }) + describe('when linking to contacts', () => { + let testContact + beforeEach(async () => { + testRecord = await CompanyContactHelper.add() + + testContact = await ContactsHelper.add({ + id: testRecord.contactId + }) + }) + + it('can successfully run a related query', async () => { + const query = await CompanyContactModel.query() + .innerJoinRelated('contacts') + + expect(query).to.exist() + }) + + it('can eager load the company contacts', async () => { + const result = await CompanyContactModel.query() + .findById(testRecord.id) + .withGraphFetched('contacts') + + expect(result).to.be.instanceOf(CompanyContactModel) + expect(result.id).to.equal(testRecord.id) + + expect(result.contacts).to.be.an.array() + expect(result.contacts[0]).to.be.an.instanceOf(ContactsModel) + expect(result.contacts).to.include(testContact) + }) + }) + }) +}) diff --git a/test/support/helpers/company-contact.helper.js b/test/support/helpers/company-contact.helper.js new file mode 100644 index 0000000000..ccf8ab43f4 --- /dev/null +++ b/test/support/helpers/company-contact.helper.js @@ -0,0 +1,55 @@ +'use strict' + +/** + * @module CompanyContactHelper + */ + +const CompanyContactModel = require('../../../app/models/company-contact.model.js') +const { generateUUID } = require('../../../app/lib/general.lib.js') + +/** + * Add a new company contact + * + * If no `data` is provided, default values will be used. These are + * + * - `companyId` - [random UUID] + * - `contactId` - [random UUID] + * - `roleId` - [random UUID] + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + * + * @returns {Promise} The instance of the newly created record + */ +function add (data = {}) { + const insertData = defaults(data) + + return CompanyContactModel.query() + .insert({ ...insertData }) + .returning('*') +} + +/** + * Returns the defaults used + * + * It will override or append to them any data provided. Mainly used by the `add()` method, we make it available + * for use in tests to avoid having to duplicate values. + * + * @param {Object} [data] Any data you want to use instead of the defaults used here or in the database + */ +function defaults (data = {}) { + const defaults = { + companyId: generateUUID(), + contactId: generateUUID(), + roleId: generateUUID() + } + + return { + ...defaults, + ...data + } +} + +module.exports = { + add, + defaults +}