From b3c243060dcba4f75eb8a167d74986bd3d3a5bf1 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 10 Oct 2024 13:58:12 -0700 Subject: [PATCH 1/5] feat: created new database models and service/repositories for device --- api/src/database-models/README.md | 11 +++ .../critter_capture_attachment.ts | 8 -- .../critter_mortality_attachment.ts | 8 -- api/src/database-models/device.ts | 36 ++++++++ .../telemetry-device-repository.interface.ts | 13 +++ .../telemetry-device-repository.ts | 83 +++++++++++++++++++ .../bctw-service/bctw-device-service.ts | 1 + .../telemetry-device-service.ts | 80 ++++++++++++++++++ 8 files changed, 224 insertions(+), 16 deletions(-) create mode 100644 api/src/database-models/README.md create mode 100644 api/src/database-models/device.ts create mode 100644 api/src/repositories/telemetry-repositories/telemetry-device-repository.interface.ts create mode 100644 api/src/repositories/telemetry-repositories/telemetry-device-repository.ts create mode 100644 api/src/services/telemetry-services/telemetry-device-service.ts diff --git a/api/src/database-models/README.md b/api/src/database-models/README.md new file mode 100644 index 0000000000..0255d41c95 --- /dev/null +++ b/api/src/database-models/README.md @@ -0,0 +1,11 @@ +# Database Models & Records Structure +The files in this directory should only contain the `Data Models` and `Data Records` +zod schemas and equivalent inferred types. + +Note: The file name should be a exact match to what is stored in the database ie: `survey.ts` + +## Data Models +1 to 1 mapping of the database table. + +## Data Records +1 to 1 mapping of the database table, ommitting the audit columns. diff --git a/api/src/database-models/critter_capture_attachment.ts b/api/src/database-models/critter_capture_attachment.ts index 9901c37da6..0e869ef52a 100644 --- a/api/src/database-models/critter_capture_attachment.ts +++ b/api/src/database-models/critter_capture_attachment.ts @@ -1,12 +1,4 @@ import { z } from 'zod'; -/** - * Note: These files should only contain the `Data Models` and `Data Records` with equivalent inferred types. - * - * Data Models contain a 1 to 1 mapping of the database table. - * - * Data Records contain a 1 to 1 mapping of the database table, minus the audit columns. - */ - /** * Critter Capture Attachment Model. * diff --git a/api/src/database-models/critter_mortality_attachment.ts b/api/src/database-models/critter_mortality_attachment.ts index f643cbf272..dac1d3f0f8 100644 --- a/api/src/database-models/critter_mortality_attachment.ts +++ b/api/src/database-models/critter_mortality_attachment.ts @@ -1,12 +1,4 @@ import { z } from 'zod'; -/** - * Note: These files should only contain the `Data Models` and `Data Records` with equivalent inferred types. - * - * Data Models contain a 1 to 1 mapping of the database table. - * - * Data Records contain a 1 to 1 mapping of the database table, minus the audit columns. - */ - /** * Critter Mortality Attachment Model. * diff --git a/api/src/database-models/device.ts b/api/src/database-models/device.ts new file mode 100644 index 0000000000..865d3804e8 --- /dev/null +++ b/api/src/database-models/device.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +/** + * Device Model. + * + * @description Data model for `device`. + */ +export const DeviceModel = z.object({ + device_id: z.number(), + survey_id: z.number(), + serial: z.number(), + device_make_id: z.number(), + model: z.string().nullable(), + comment: z.string().nullable(), + create_date: z.string(), + create_user: z.number(), + update_date: z.string().nullable(), + update_user: z.number().nullable(), + revision_count: z.number() +}); + +export type DeviceModel = z.infer; + +/** + * Device Record. + * + * @description Data record for `device`. + */ +export const DeviceRecord = DeviceModel.omit({ + create_date: true, + create_user: true, + update_date: true, + update_user: true, + revision_count: true +}); + +export type DeviceRecord = z.infer; diff --git a/api/src/repositories/telemetry-repositories/telemetry-device-repository.interface.ts b/api/src/repositories/telemetry-repositories/telemetry-device-repository.interface.ts new file mode 100644 index 0000000000..9a05d0441d --- /dev/null +++ b/api/src/repositories/telemetry-repositories/telemetry-device-repository.interface.ts @@ -0,0 +1,13 @@ +import { DeviceRecord } from '../../database-models/device'; + +/** + * Interface reflecting the telemetry device data required to create a new device + * + */ +export type CreateTelemetryDevice = Pick; + +/** + * Interface reflecting the telemetry device data required to update an existing device + * + */ +export type UpdateTelemetryDevice = Partial>; diff --git a/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts new file mode 100644 index 0000000000..6625f4829b --- /dev/null +++ b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts @@ -0,0 +1,83 @@ +import { DeviceRecord } from '../../database-models/device'; +import { getKnex } from '../../database/db'; +import { ApiExecuteSQLError } from '../../errors/api-error'; +import { BaseRepository } from '../base-repository'; +import { CreateTelemetryDevice, UpdateTelemetryDevice } from './telemetry-device-repository.interface'; + +/** + * A repository class for accessing telemetry device data. + * + * @export + * @class TelemetryDeviceRepository + * @extends {BaseRepository} + */ +export class TelemetryDeviceRepository extends BaseRepository { + /** + * Get a list of devices by their IDs. + * + * @param {number[]} deviceIds + * @returns {*} {Promise} + * + */ + async getDevicesByIds(deviceIds: number[]): Promise { + const knex = getKnex(); + + const queryBuilder = knex + .select() + .from('device') + .where({ device_id: deviceIds }) + .returning(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']); + + const response = await this.connection.knex(queryBuilder, DeviceRecord); + + return response.rows; + } + + /** + * Create a new device record. + * + * @param {CreateTelemetryDevice} device + * @returns {*} {Promise} + */ + async createDevice(device: CreateTelemetryDevice): Promise { + const knex = getKnex(); + + const queryBuilder = knex + .insert(device) + .into('device') + .returning(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']); + + const response = await this.connection.knex(queryBuilder, DeviceRecord); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Device was not created', ['TelemetryDeviceRepository -> createDevice']); + } + + return response.rows[0]; + } + + /** + * Update an existing device record. + * + * @param {number} deviceId + * @param {UpdateTelemetryDevice} device + * @returns {*} {Promise} + */ + async updateDevice(deviceId: number, device: UpdateTelemetryDevice): Promise { + const knex = getKnex(); + + const queryBuilder = knex + .update(device) + .from('device') + .where({ device_id: deviceId }) + .returning(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']); + + const response = await this.connection.knex(queryBuilder, DeviceRecord); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Device was not updated', ['TelemetryDeviceRepository -> updateDevice']); + } + + return response.rows[0]; + } +} diff --git a/api/src/services/bctw-service/bctw-device-service.ts b/api/src/services/bctw-service/bctw-device-service.ts index e53e38088a..7836bc780f 100644 --- a/api/src/services/bctw-service/bctw-device-service.ts +++ b/api/src/services/bctw-service/bctw-device-service.ts @@ -16,6 +16,7 @@ export type BctwUpdateCollarRequest = { frequency_unit?: number | null; }; +// TODO: Is this deprecated?? export class BctwDeviceService extends BctwService { /** * Get a list of all supported collar vendors. diff --git a/api/src/services/telemetry-services/telemetry-device-service.ts b/api/src/services/telemetry-services/telemetry-device-service.ts new file mode 100644 index 0000000000..d3676412a6 --- /dev/null +++ b/api/src/services/telemetry-services/telemetry-device-service.ts @@ -0,0 +1,80 @@ +import { DeviceRecord } from '../../database-models/device'; +import { IDBConnection } from '../../database/db'; +import { ApiGeneralError } from '../../errors/api-error'; +import { TelemetryDeviceRepository } from '../../repositories/telemetry-repositories/telemetry-device-repository'; +import { + CreateTelemetryDevice, + UpdateTelemetryDevice +} from '../../repositories/telemetry-repositories/telemetry-device-repository.interface'; +import { DBService } from '../db-service'; + +/** + * A service class for working with telemetry devices. + * + * Note: A telemetry `device` is different than a `deployment`. + * A device may have multiple deployments, but a deployment is associated with a single device. + * + * Device: The physical device. + * Deployment: The time period during which a device is attached to an animal. + * + * @export + * @class TelemetryDeviceService + * @extends {DBService} + */ +export class TelemetryDeviceService extends DBService { + telemetryDeviceRepository: TelemetryDeviceRepository; + + constructor(connection: IDBConnection) { + super(connection); + this.telemetryDeviceRepository = new TelemetryDeviceRepository(connection); + } + + /** + * Get a single device by its ID. + * + * @throws {ApiGeneralError} If the device is not found. + * @param {number} device_id + * @return {*} {Promise} + */ + async getDevice(device_id: number): Promise { + const devices = await this.telemetryDeviceRepository.getDevicesByIds([device_id]); + + if (devices.length !== 1) { + throw new ApiGeneralError('Device not found', ['TelemetryDeviceService -> getDevice']); + } + + return devices[0]; + } + + /** + * Get a list of devices by their IDs. + * + * @param {number[]} deviceIds + * @returns {*} {Promise} + * + */ + async getDevices(deviceIds: number[]): Promise { + return this.telemetryDeviceRepository.getDevicesByIds(deviceIds); + } + + /** + * Create a new device record. + * + * @param {CreateTelemetryDevice} device + * @returns {*} {Promise} + */ + async createDevice(device: CreateTelemetryDevice): Promise { + return this.telemetryDeviceRepository.createDevice(device); + } + + /** + * Update an existing device record. + * + * @param {number} deviceId + * @param {UpdateTelemetryDevice} device + * @returns {*} {Promise} + */ + async updateDevice(deviceId: number, device: UpdateTelemetryDevice): Promise { + return this.telemetryDeviceRepository.updateDevice(deviceId, device); + } +} From 96f20fc7f1e8488c9760c9bcfe4b5315b3bd876e Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 10 Oct 2024 15:33:21 -0700 Subject: [PATCH 2/5] chore: updated tests for service + modified serial db column type --- api/src/database-models/device.ts | 3 +- .../telemetry-device-repository.ts | 39 +++++- .../telemetry-device-service.test.ts | 126 ++++++++++++++++++ .../telemetry-device-service.ts | 33 +++-- .../20241008000000_bctw_migration.ts | 7 +- 5 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 api/src/services/telemetry-services/telemetry-device-service.test.ts diff --git a/api/src/database-models/device.ts b/api/src/database-models/device.ts index 865d3804e8..1c719ac03f 100644 --- a/api/src/database-models/device.ts +++ b/api/src/database-models/device.ts @@ -7,7 +7,8 @@ import { z } from 'zod'; export const DeviceModel = z.object({ device_id: z.number(), survey_id: z.number(), - serial: z.number(), + device_key: z.string(), + serial: z.string(), device_make_id: z.number(), model: z.string().nullable(), comment: z.string().nullable(), diff --git a/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts index 6625f4829b..64d6404026 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts @@ -1,3 +1,4 @@ +import { z } from 'zod'; import { DeviceRecord } from '../../database-models/device'; import { getKnex } from '../../database/db'; import { ApiExecuteSQLError } from '../../errors/api-error'; @@ -15,24 +16,47 @@ export class TelemetryDeviceRepository extends BaseRepository { /** * Get a list of devices by their IDs. * + * @param {surveyId} surveyId * @param {number[]} deviceIds * @returns {*} {Promise} * */ - async getDevicesByIds(deviceIds: number[]): Promise { + async getDevicesByIds(surveyId: number, deviceIds: number[]): Promise { const knex = getKnex(); const queryBuilder = knex - .select() + .select(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']) .from('device') - .where({ device_id: deviceIds }) - .returning(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']); + .whereIn('device_id', deviceIds) + .andWhere('survey_id', surveyId); const response = await this.connection.knex(queryBuilder, DeviceRecord); return response.rows; } + /** + * Delete a list of devices by their IDs. + * + * @param {surveyId} surveyId + * @param {number[]} deviceIds + * @returns {*} {Promise<{ device_id: string }[]>} + */ + async deleteDevicesByIds(surveyId: number, deviceIds: number[]): Promise<{ device_id: number }[]> { + const knex = getKnex(); + + const queryBuilder = knex + .delete() + .from('device') + .whereIn('device_id', deviceIds) + .andWhere({ survey_id: surveyId }) + .returning(['device_id']); + + const response = await this.connection.knex(queryBuilder, z.object({ device_id: z.number() })); + + return response.rows; + } + /** * Create a new device record. * @@ -59,17 +83,18 @@ export class TelemetryDeviceRepository extends BaseRepository { /** * Update an existing device record. * + * @param {surveyId} surveyId * @param {number} deviceId * @param {UpdateTelemetryDevice} device - * @returns {*} {Promise} + * @returns {*} {Promise} */ - async updateDevice(deviceId: number, device: UpdateTelemetryDevice): Promise { + async updateDevice(surveyId: number, deviceId: number, device: UpdateTelemetryDevice): Promise { const knex = getKnex(); const queryBuilder = knex .update(device) .from('device') - .where({ device_id: deviceId }) + .where({ device_id: deviceId, survey_id: surveyId }) .returning(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment']); const response = await this.connection.knex(queryBuilder, DeviceRecord); diff --git a/api/src/services/telemetry-services/telemetry-device-service.test.ts b/api/src/services/telemetry-services/telemetry-device-service.test.ts new file mode 100644 index 0000000000..61068c1e89 --- /dev/null +++ b/api/src/services/telemetry-services/telemetry-device-service.test.ts @@ -0,0 +1,126 @@ +import chai, { expect } from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { TelemetryDeviceRepository } from '../../repositories/telemetry-repositories/telemetry-device-repository'; +import { getMockDBConnection } from '../../__mocks__/db'; +import { TelemetryDeviceService } from './telemetry-device-service'; + +chai.use(sinonChai); + +describe.only('TelemetryDeviceService', () => { + beforeEach(() => { + sinon.restore(); + }); + + describe('getDevice', () => { + it('should return a device by its ID', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon.stub(TelemetryDeviceRepository.prototype, 'getDevicesByIds').resolves([true] as any); + + const device = await service.getDevice(1, 2); + + expect(repoStub).to.have.been.calledOnceWithExactly(1, [2]); + expect(device).to.be.true; + }); + + it('should throw an error if unable to get device', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon.stub(TelemetryDeviceRepository.prototype, 'getDevicesByIds').resolves([]); + + try { + await service.getDevice(1, 2); + expect.fail(); + } catch (err: any) { + expect(err.message).to.equal('Device not found'); + } + + expect(repoStub).to.have.been.calledOnceWithExactly(1, [2]); + }); + }); + + describe('deleteDevice', () => { + it('should delete a device by its ID', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon + .stub(TelemetryDeviceRepository.prototype, 'deleteDevicesByIds') + .resolves([{ device_id: 2 }] as any); + + const device = await service.deleteDevice(1, 2); + + expect(repoStub).to.have.been.calledOnceWithExactly(1, [2]); + expect(device).to.be.equal(2); + }); + + it('should throw an error if unable to delete device', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon.stub(TelemetryDeviceRepository.prototype, 'deleteDevicesByIds').resolves([]); + + try { + await service.deleteDevice(1, 2); + expect.fail(); + } catch (err: any) { + expect(err.message).to.equal('Unable to delete device'); + } + + expect(repoStub).to.have.been.calledOnceWithExactly(1, [2]); + }); + }); + + describe('createDevice', () => { + it('should delete a device by its ID', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon.stub(TelemetryDeviceRepository.prototype, 'createDevice').resolves(true as any); + + const device = await service.createDevice({ + device_make_id: 1, + model: null, + survey_id: 1, + serial: 'serial', + comment: 'comment' + }); + + expect(repoStub).to.have.been.calledOnceWithExactly({ + device_make_id: 1, + model: null, + survey_id: 1, + serial: 'serial', + comment: 'comment' + }); + + expect(device).to.be.equal(true); + }); + }); + + describe('updateDevice', () => { + it('should update a device by its ID', async () => { + const mockConnection = getMockDBConnection(); + const service = new TelemetryDeviceService(mockConnection); + + const repoStub = sinon.stub(TelemetryDeviceRepository.prototype, 'updateDevice').resolves(true as any); + + const device = await service.updateDevice(1, 2, { + device_make_id: 1, + serial: 'serial', + comment: 'comment' + }); + + expect(repoStub).to.have.been.calledOnceWithExactly(1, 2, { + device_make_id: 1, + serial: 'serial', + comment: 'comment' + }); + + expect(device).to.be.equal(true); + }); + }); +}); diff --git a/api/src/services/telemetry-services/telemetry-device-service.ts b/api/src/services/telemetry-services/telemetry-device-service.ts index d3676412a6..2727b573d5 100644 --- a/api/src/services/telemetry-services/telemetry-device-service.ts +++ b/api/src/services/telemetry-services/telemetry-device-service.ts @@ -33,11 +33,13 @@ export class TelemetryDeviceService extends DBService { * Get a single device by its ID. * * @throws {ApiGeneralError} If the device is not found. - * @param {number} device_id + * + * @param {number} surveyId + * @param {number} deviceId * @return {*} {Promise} */ - async getDevice(device_id: number): Promise { - const devices = await this.telemetryDeviceRepository.getDevicesByIds([device_id]); + async getDevice(surveyId: number, deviceId: number): Promise { + const devices = await this.telemetryDeviceRepository.getDevicesByIds(surveyId, [deviceId]); if (devices.length !== 1) { throw new ApiGeneralError('Device not found', ['TelemetryDeviceService -> getDevice']); @@ -47,14 +49,22 @@ export class TelemetryDeviceService extends DBService { } /** - * Get a list of devices by their IDs. + * Delete a single device by its ID. * - * @param {number[]} deviceIds - * @returns {*} {Promise} + * @throws {ApiGeneralError} If unable to delete the device. * + * @param {number} surveyId + * @param {number} deviceId + * @return {*} {Promise} The device ID that was deleted. */ - async getDevices(deviceIds: number[]): Promise { - return this.telemetryDeviceRepository.getDevicesByIds(deviceIds); + async deleteDevice(surveyId: number, deviceId: number): Promise { + const devices = await this.telemetryDeviceRepository.deleteDevicesByIds(surveyId, [deviceId]); + + if (devices.length !== 1 || devices[0].device_id !== deviceId) { + throw new ApiGeneralError('Unable to delete device', ['TelemetryDeviceService -> deleteDevice']); + } + + return devices[0].device_id; } /** @@ -70,11 +80,12 @@ export class TelemetryDeviceService extends DBService { /** * Update an existing device record. * + * @param {number} surveyId * @param {number} deviceId * @param {UpdateTelemetryDevice} device - * @returns {*} {Promise} + * @returns {*} {Promise} */ - async updateDevice(deviceId: number, device: UpdateTelemetryDevice): Promise { - return this.telemetryDeviceRepository.updateDevice(deviceId, device); + async updateDevice(surveyId: number, deviceId: number, device: UpdateTelemetryDevice): Promise { + return this.telemetryDeviceRepository.updateDevice(surveyId, deviceId, device); } } diff --git a/database/src/migrations/20241008000000_bctw_migration.ts b/database/src/migrations/20241008000000_bctw_migration.ts index b4fa8f6e9c..bff182b6a0 100644 --- a/database/src/migrations/20241008000000_bctw_migration.ts +++ b/database/src/migrations/20241008000000_bctw_migration.ts @@ -24,7 +24,7 @@ export async function up(knex: Knex): Promise { device_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), survey_id integer NOT NULL, device_key varchar NOT NULL, - serial integer NOT NULL, + serial varchar NOT NULL, device_make_id integer NOT NULL, model varchar(100), comment varchar(250), @@ -40,7 +40,7 @@ export async function up(knex: Knex): Promise { COMMENT ON COLUMN device.device_id IS '(Generated) Surrogate primary key identifier.'; COMMENT ON COLUMN device.survey_id IS 'Foreign key to the survey table.'; COMMENT ON COLUMN device.device_key IS '(Generated) The SIMS unique key for the device.'; - COMMENT ON COLUMN device.serial IS 'The serial number of the device.'; + COMMENT ON COLUMN device.serial IS 'The serial identifier of the device.'; COMMENT ON COLUMN device.device_make_id IS 'Foreign key to the device_make table.'; COMMENT ON COLUMN device.model IS 'The device model.'; COMMENT ON COLUMN device.comment IS 'A comment about the device.'; @@ -61,6 +61,9 @@ export async function up(knex: Knex): Promise { FOREIGN KEY (device_make_id) REFERENCES device_make(device_make_id); + -- Add unique constraints + ALTER TABLE device ADD CONSTRAINT device_uk1 UNIQUE (survey_id, serial, device_make_id); + -- Add indexes for foreign keys CREATE INDEX device_idx1 ON device(survey_id); From 606131d52b08e82abca46d9afe053e032697ce7e Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 10 Oct 2024 15:45:34 -0700 Subject: [PATCH 3/5] feat: added tests for repository --- .../telemetry-device-repository.test.ts | 89 +++++++++++++++++++ .../telemetry-device-service.test.ts | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 api/src/repositories/telemetry-repositories/telemetry-device-repository.test.ts diff --git a/api/src/repositories/telemetry-repositories/telemetry-device-repository.test.ts b/api/src/repositories/telemetry-repositories/telemetry-device-repository.test.ts new file mode 100644 index 0000000000..a4b14d6d87 --- /dev/null +++ b/api/src/repositories/telemetry-repositories/telemetry-device-repository.test.ts @@ -0,0 +1,89 @@ +import chai, { expect } from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMockDBConnection } from '../../__mocks__/db'; +import { TelemetryDeviceRepository } from './telemetry-device-repository'; + +chai.use(sinonChai); + +describe('TelemetryDeviceRepository', () => { + it('should construct', () => { + const mockDBConnection = getMockDBConnection(); + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + expect(telemetryDeviceRepository).to.be.instanceof(TelemetryDeviceRepository); + }); + + describe('getDevicesByIds', () => { + it('should get devices by IDs', async () => { + const mockRows = [{ device_id: 1 }]; + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: mockRows }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + const response = await telemetryDeviceRepository.getDevicesByIds(1, [1]); + expect(response).to.eql(mockRows); + }); + }); + + describe('deleteDevicesByIds', () => { + it('should delete devices by IDs', async () => { + const mockRows = [{ device_id: 1 }]; + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: mockRows }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + const response = await telemetryDeviceRepository.deleteDevicesByIds(1, [1]); + expect(response).to.eql(mockRows); + }); + }); + + describe('createDevice', () => { + it('should create a new device', async () => { + const mockRows = [{ device_id: 1 }]; + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: mockRows, rowCount: 1 }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + const response = await telemetryDeviceRepository.createDevice({ device_id: 1 } as any); + expect(response).to.eql({ device_id: 1 }); + }); + + it('should throw an error if unable to create a new device', async () => { + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: [], rowCount: 0 }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + try { + await telemetryDeviceRepository.createDevice({ device_id: 1 } as any); + expect.fail(); + } catch (err: any) { + expect(err.message).to.equal('Device was not created'); + } + }); + }); + + describe('updateDevice', () => { + it('should update an existing device', async () => { + const mockRows = [{ device_id: 1 }]; + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: mockRows, rowCount: 1 }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + const response = await telemetryDeviceRepository.updateDevice(1, 2, { comment: 1 } as any); + expect(response).to.eql({ device_id: 1 }); + }); + it('should throw an error if unable to update an existing device', async () => { + const mockDBConnection = getMockDBConnection({ knex: sinon.stub().resolves({ rows: [], rowCount: 0 }) }); + + const telemetryDeviceRepository = new TelemetryDeviceRepository(mockDBConnection); + + try { + await telemetryDeviceRepository.updateDevice(1, 2, { comment: 1 } as any); + expect.fail(); + } catch (err: any) { + expect(err.message).to.equal('Device was not updated'); + } + }); + }); +}); diff --git a/api/src/services/telemetry-services/telemetry-device-service.test.ts b/api/src/services/telemetry-services/telemetry-device-service.test.ts index 61068c1e89..a026228101 100644 --- a/api/src/services/telemetry-services/telemetry-device-service.test.ts +++ b/api/src/services/telemetry-services/telemetry-device-service.test.ts @@ -7,7 +7,7 @@ import { TelemetryDeviceService } from './telemetry-device-service'; chai.use(sinonChai); -describe.only('TelemetryDeviceService', () => { +describe('TelemetryDeviceService', () => { beforeEach(() => { sinon.restore(); }); From 5f946981311980cfef0ef8edd103416586b73216 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 10 Oct 2024 15:52:44 -0700 Subject: [PATCH 4/5] fix: updated database-model README --- api/src/database-models/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/database-models/README.md b/api/src/database-models/README.md index 0255d41c95..686ccd276d 100644 --- a/api/src/database-models/README.md +++ b/api/src/database-models/README.md @@ -1,6 +1,5 @@ # Database Models & Records Structure -The files in this directory should only contain the `Data Models` and `Data Records` -zod schemas and equivalent inferred types. +The files in this directory should only contain the `Data Models` and `Data Records` zod schemas and the equivalent types. Note: The file name should be a exact match to what is stored in the database ie: `survey.ts` From 17cd9eeb840c508a2f30df41ea633509ca8d3121 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 10 Oct 2024 15:54:22 -0700 Subject: [PATCH 5/5] doc: updated documentation --- .../telemetry-repositories/telemetry-device-repository.ts | 5 ++--- api/src/services/bctw-service/bctw-device-service.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts index 64d6404026..1c71996ffb 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-device-repository.ts @@ -19,7 +19,6 @@ export class TelemetryDeviceRepository extends BaseRepository { * @param {surveyId} surveyId * @param {number[]} deviceIds * @returns {*} {Promise} - * */ async getDevicesByIds(surveyId: number, deviceIds: number[]): Promise { const knex = getKnex(); @@ -40,9 +39,9 @@ export class TelemetryDeviceRepository extends BaseRepository { * * @param {surveyId} surveyId * @param {number[]} deviceIds - * @returns {*} {Promise<{ device_id: string }[]>} + * @returns {*} {Promise>} */ - async deleteDevicesByIds(surveyId: number, deviceIds: number[]): Promise<{ device_id: number }[]> { + async deleteDevicesByIds(surveyId: number, deviceIds: number[]): Promise> { const knex = getKnex(); const queryBuilder = knex diff --git a/api/src/services/bctw-service/bctw-device-service.ts b/api/src/services/bctw-service/bctw-device-service.ts index 7836bc780f..fe6f0114e5 100644 --- a/api/src/services/bctw-service/bctw-device-service.ts +++ b/api/src/services/bctw-service/bctw-device-service.ts @@ -16,7 +16,7 @@ export type BctwUpdateCollarRequest = { frequency_unit?: number | null; }; -// TODO: Is this deprecated?? +// BCTW-MIGRATION-TODO: DEPRECATED export class BctwDeviceService extends BctwService { /** * Get a list of all supported collar vendors.