Skip to content

Commit

Permalink
BCTW Data Migration: Device CRUD (#1394)
Browse files Browse the repository at this point in the history
- Telemetry Device CRUD operations
  • Loading branch information
MacQSL authored Oct 11, 2024
1 parent d2afdd5 commit 652798c
Show file tree
Hide file tree
Showing 11 changed files with 479 additions and 18 deletions.
10 changes: 10 additions & 0 deletions api/src/database-models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Database Models & Records Structure
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`

## Data Models
1 to 1 mapping of the database table.

## Data Records
1 to 1 mapping of the database table, ommitting the audit columns.
8 changes: 0 additions & 8 deletions api/src/database-models/critter_capture_attachment.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down
8 changes: 0 additions & 8 deletions api/src/database-models/critter_mortality_attachment.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down
37 changes: 37 additions & 0 deletions api/src/database-models/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from 'zod';
/**
* Device Model.
*
* @description Data model for `device`.
*/
export const DeviceModel = z.object({
device_id: z.number(),
survey_id: z.number(),
device_key: z.string(),
serial: z.string(),
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<typeof DeviceModel>;

/**
* 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<typeof DeviceRecord>;
Original file line number Diff line number Diff line change
@@ -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<DeviceRecord, 'survey_id' | 'serial' | 'device_make_id' | 'model' | 'comment'>;

/**
* Interface reflecting the telemetry device data required to update an existing device
*
*/
export type UpdateTelemetryDevice = Partial<Pick<DeviceRecord, 'serial' | 'device_make_id' | 'model' | 'comment'>>;
Original file line number Diff line number Diff line change
@@ -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');
}
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { z } from 'zod';
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 {surveyId} surveyId
* @param {number[]} deviceIds
* @returns {*} {Promise<DeviceRecord[]>}
*/
async getDevicesByIds(surveyId: number, deviceIds: number[]): Promise<DeviceRecord[]> {
const knex = getKnex();

const queryBuilder = knex
.select(['device_id', 'survey_id', 'device_key', 'serial', 'device_make_id', 'model', 'comment'])
.from('device')
.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<Array<{ device_id: string }>>}
*/
async deleteDevicesByIds(surveyId: number, deviceIds: number[]): Promise<Array<{ 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.
*
* @param {CreateTelemetryDevice} device
* @returns {*} {Promise<DeviceRecord>}
*/
async createDevice(device: CreateTelemetryDevice): Promise<DeviceRecord> {
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 {surveyId} surveyId
* @param {number} deviceId
* @param {UpdateTelemetryDevice} device
* @returns {*} {Promise<DeviceRecord>}
*/
async updateDevice(surveyId: number, deviceId: number, device: UpdateTelemetryDevice): Promise<DeviceRecord> {
const knex = getKnex();

const queryBuilder = knex
.update(device)
.from('device')
.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);

if (!response.rowCount) {
throw new ApiExecuteSQLError('Device was not updated', ['TelemetryDeviceRepository -> updateDevice']);
}

return response.rows[0];
}
}
1 change: 1 addition & 0 deletions api/src/services/bctw-service/bctw-device-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type BctwUpdateCollarRequest = {
frequency_unit?: number | null;
};

// BCTW-MIGRATION-TODO: DEPRECATED
export class BctwDeviceService extends BctwService {
/**
* Get a list of all supported collar vendors.
Expand Down
Loading

0 comments on commit 652798c

Please sign in to comment.