From ad1c5fad00fb0c16e7393ec112aa1e58de8c0668 Mon Sep 17 00:00:00 2001 From: slevert Date: Thu, 10 Mar 2022 10:55:28 -0500 Subject: [PATCH 1/3] Adding sub total and sorting capabilities --- .../__tests__/device/DeviceDataSource.spec.ts | 5 +++++ .../__tests__/device/MockDeviceDataSource.ts | 2 +- .../device/__snapshots__/resolve.spec.ts.snap | 1 + .../device/dao/mongo/MongoDeviceDAO.spec.ts | 22 ++++++++++++++----- .../dao/mongo/MongoFilterBuilder.spec.ts | 14 ++++++++---- .../device/dao/static/StaticDeviceDAO.spec.ts | 21 ------------------ udmd/api/src/__tests__/device/resolve.spec.ts | 1 + udmd/api/src/device/DeviceDataSource.ts | 3 ++- udmd/api/src/device/dao/DeviceDAO.ts | 1 + .../dao/firestore/FirestoreDeviceDAO.ts | 13 ----------- .../src/device/dao/mongodb/MongoDeviceDAO.ts | 9 ++++++-- .../device/dao/mongodb/MongoFilterBuilder.ts | 7 +++++- .../src/device/dao/static/StaticDeviceDAO.ts | 14 +++++++----- udmd/api/src/device/model.ts | 1 + udmd/api/src/device/schema.graphql | 2 ++ 15 files changed, 62 insertions(+), 54 deletions(-) delete mode 100644 udmd/api/src/device/dao/firestore/FirestoreDeviceDAO.ts diff --git a/udmd/api/src/__tests__/device/DeviceDataSource.spec.ts b/udmd/api/src/__tests__/device/DeviceDataSource.spec.ts index 19fec8d43..124acd57e 100644 --- a/udmd/api/src/__tests__/device/DeviceDataSource.spec.ts +++ b/udmd/api/src/__tests__/device/DeviceDataSource.spec.ts @@ -19,4 +19,9 @@ describe('DeviceDataSource.getDevice()', () => { const result: DevicesResponse = await deviceDS.getDevices(searchOptions); await expect(result.totalCount).not.toBe(0); }); + + test('returns a total filtered count', async () => { + const result: DevicesResponse = await deviceDS.getDevices(searchOptions); + await expect(result.totalFilteredCount).not.toBe(0); + }); }); diff --git a/udmd/api/src/__tests__/device/MockDeviceDataSource.ts b/udmd/api/src/__tests__/device/MockDeviceDataSource.ts index 5abc1c510..105602871 100644 --- a/udmd/api/src/__tests__/device/MockDeviceDataSource.ts +++ b/udmd/api/src/__tests__/device/MockDeviceDataSource.ts @@ -13,6 +13,6 @@ export default class MockDeviceDataSource extends GraphQLDataSource { async getDevices(searchOptions: SearchOptions): Promise { const devices: Device[] = createDevices(30); - return { devices, totalCount: 30 }; + return { devices, totalCount: 30, totalFilteredCount: 10 }; } } diff --git a/udmd/api/src/__tests__/device/__snapshots__/resolve.spec.ts.snap b/udmd/api/src/__tests__/device/__snapshots__/resolve.spec.ts.snap index d5d7f72f7..b509d8597 100644 --- a/udmd/api/src/__tests__/device/__snapshots__/resolve.spec.ts.snap +++ b/udmd/api/src/__tests__/device/__snapshots__/resolve.spec.ts.snap @@ -337,6 +337,7 @@ Object { }, ], "totalCount": 30, + "totalFilteredCount": 10, }, }, "errors": undefined, diff --git a/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts b/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts index c17a6fbda..d18ed22b0 100644 --- a/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts +++ b/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts @@ -7,7 +7,9 @@ let connection: MongoClient; let db: Db; let deviceCollection: Collection; let mongoDeviceDAO: MongoDeviceDAO; -let mockDevices: Device[]; +const mockDevices: Device[] = createDevices(100); + +// mocks let findSpy; let sortSpy; let limitSpy; @@ -28,9 +30,8 @@ beforeEach(async () => { mongoDeviceDAO = new MongoDeviceDAO(db); // data prep - mockDevices = createDevices(100); deviceCollection = db.collection('device'); - deviceCollection.insertMany(mockDevices); + await deviceCollection.insertMany(mockDevices); // spies findSpy = jest.spyOn(Collection.prototype, 'find'); @@ -41,8 +42,9 @@ beforeEach(async () => { afterEach(async () => { await db.collection('device').deleteMany({}); - jest.resetAllMocks(); - jest.restoreAllMocks(); + + // jest.resetAllMocks(); + // jest.restoreAllMocks(); }); describe('MongoDeviceDAO.getDevices', () => { @@ -150,3 +152,13 @@ describe('MongoDeviceDAO.getDeviceCount', () => { expect(count).toBe(mockDevices.length); }); }); + +describe('MongoDeviceDAO.getFilteredDeviceCount', () => { + test('returns the filtered device count in the mongo db', async () => { + const searchOptions: SearchOptions = { batchSize: 10 }; + + const filterdDeviceCount: number = await mongoDeviceDAO.getFilteredDeviceCount(searchOptions); + + expect(filterdDeviceCount).toBe(mockDevices.length); + }); +}); diff --git a/udmd/api/src/__tests__/device/dao/mongo/MongoFilterBuilder.spec.ts b/udmd/api/src/__tests__/device/dao/mongo/MongoFilterBuilder.spec.ts index a5c5af911..d8fe12bec 100644 --- a/udmd/api/src/__tests__/device/dao/mongo/MongoFilterBuilder.spec.ts +++ b/udmd/api/src/__tests__/device/dao/mongo/MongoFilterBuilder.spec.ts @@ -3,16 +3,16 @@ import { getFilter } from '../../../../device/dao/mongodb/MongoFilterBuilder'; const emptyMongoFilter = {}; -const filters = [ +const containfilters = [ [getContainsFilter('make', 'value'), getExpectedRegex('make', 'value')], [getContainsFilter('model', 'value'), getExpectedRegex('model', 'value')], [getContainsFilter('operational', 'true'), emptyMongoFilter], - [getEqualsFilter('make', 'value'), emptyMongoFilter], + [getEqualsFilter('make', 'value'), getExpectedEqualsExpression('make', 'value')], ]; describe('MongoFilterBuilder.getFilter', () => { - test.each(filters)( - 'returns a regex filter object when a ~ is the operator', + test.each(containfilters)( + 'returns a regex filter object when ~ is the operator', async (filter: Filter, expectedResult) => { const jsonFilters: Filter[] = [filter]; expect(getFilter(jsonFilters)).toEqual(expectedResult); @@ -28,6 +28,12 @@ function getEqualsFilter(field: string, value: string): Filter { return { field, operator: '=', value }; } +function getExpectedEqualsExpression(field: string, value: string): any { + const equalsExpression = {}; + equalsExpression[field] = value; + return equalsExpression; +} + function getExpectedRegex(field: string, value: string): any { const regex = {}; regex[field] = { $regex: value, $options: 'i' }; diff --git a/udmd/api/src/__tests__/device/dao/static/StaticDeviceDAO.spec.ts b/udmd/api/src/__tests__/device/dao/static/StaticDeviceDAO.spec.ts index 4d1c9f1eb..09d7e7933 100644 --- a/udmd/api/src/__tests__/device/dao/static/StaticDeviceDAO.spec.ts +++ b/udmd/api/src/__tests__/device/dao/static/StaticDeviceDAO.spec.ts @@ -22,27 +22,6 @@ describe('StaticDeviceDAO.getDevices', () => { searchOptions = createSearchOptions(batchSize, offset); await expect(deviceDAO.getDevices(searchOptions)).resolves.toEqual(allDevices.slice(offset, offset + batchSize)); }); - - test('that providing an offset greater than the number of devices throws an error', async () => { - const batchSize = 20; - const offset = 200; - - const searchOptions: SearchOptions = createSearchOptions(batchSize, offset); - - await expect(deviceDAO.getDevices(searchOptions)).rejects.toThrowError( - 'An invalid offset that is greater than the total number of devices was provided.' - ); - }); - - test('that if a batch size of 0 is provided, an error is thrown', async () => { - const batchSize = 0; - - const searchOptions: SearchOptions = createSearchOptions(batchSize); - - await expect(deviceDAO.getDevices(searchOptions)).rejects.toThrowError( - 'A batch size greater than zero must be provided' - ); - }); }); describe('StaticDeviceDAO.getDeviceCount', () => { diff --git a/udmd/api/src/__tests__/device/resolve.spec.ts b/udmd/api/src/__tests__/device/resolve.spec.ts index 3d8351afa..6fea11b61 100644 --- a/udmd/api/src/__tests__/device/resolve.spec.ts +++ b/udmd/api/src/__tests__/device/resolve.spec.ts @@ -13,6 +13,7 @@ const QUERY_DEVICES = gql` query { devices(searchOptions: { batchSize: 10, offset: 10, sortOptions: { direction: DESC, field: "name" }, filter: "" }) { totalCount + totalFilteredCount devices { id name diff --git a/udmd/api/src/device/DeviceDataSource.ts b/udmd/api/src/device/DeviceDataSource.ts index ead8417b5..53803c4dc 100644 --- a/udmd/api/src/device/DeviceDataSource.ts +++ b/udmd/api/src/device/DeviceDataSource.ts @@ -14,6 +14,7 @@ export class DeviceDataSource extends GraphQLDataSource { async getDevices(searchOptions: SearchOptions): Promise { const totalCount = await this.deviceDAO.getDeviceCount(); const devices: Device[] = await this.deviceDAO.getDevices(searchOptions); - return { devices, totalCount }; + const totalFilteredCount: number = await this.deviceDAO.getFilteredDeviceCount(searchOptions); + return { devices, totalCount, totalFilteredCount }; } } diff --git a/udmd/api/src/device/dao/DeviceDAO.ts b/udmd/api/src/device/dao/DeviceDAO.ts index fe7507fd2..4a4603293 100644 --- a/udmd/api/src/device/dao/DeviceDAO.ts +++ b/udmd/api/src/device/dao/DeviceDAO.ts @@ -1,6 +1,7 @@ import { Device, SearchOptions } from '../model'; export interface DeviceDAO { + getFilteredDeviceCount(searchOptions: SearchOptions): Promise; getDevices(searchOptions: SearchOptions): Promise; getDeviceCount(): Promise; } diff --git a/udmd/api/src/device/dao/firestore/FirestoreDeviceDAO.ts b/udmd/api/src/device/dao/firestore/FirestoreDeviceDAO.ts deleted file mode 100644 index 45a4efb7c..000000000 --- a/udmd/api/src/device/dao/firestore/FirestoreDeviceDAO.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SearchOptions, Device } from '../../model'; -import { DeviceDAO } from '../DeviceDAO'; - -// this class allows interactions with a firestore db -export class FirestoreDeviceDAO implements DeviceDAO { - constructor() {} - getDevices(searchOptions: SearchOptions): Promise { - throw new Error('Method not implemented.'); - } - getDeviceCount(): Promise { - throw new Error('Method not implemented.'); - } -} diff --git a/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts b/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts index 433935ca5..34f6549d5 100644 --- a/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts +++ b/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts @@ -1,6 +1,6 @@ import { SearchOptions, Device } from '../../model'; import { DeviceDAO } from '../DeviceDAO'; -import { Db } from 'mongodb'; +import { Db, Filter, FindCursor } from 'mongodb'; import { fromString } from '../../../device/FilterParser'; import { getFilter } from './MongoFilterBuilder'; import { getSort } from './MongoSortBuilder'; @@ -20,11 +20,16 @@ export class MongoDeviceDAO implements DeviceDAO { .toArray(); } + async getFilteredDeviceCount(searchOptions: SearchOptions): Promise { + // console.log(await this.db.collection('device').countDocuments()); + return await this.db.collection('device').countDocuments(this.getFilter(searchOptions)); + } + async getDeviceCount(): Promise { return this.db.collection('device').countDocuments(); } - private getFilter(searchOptions: SearchOptions): any { + private getFilter(searchOptions: SearchOptions): Filter { return searchOptions.filter ? getFilter(fromString(searchOptions.filter)) : {}; } diff --git a/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts b/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts index cd75cbabc..2d0a367f3 100644 --- a/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts +++ b/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts @@ -5,11 +5,16 @@ export function getFilter(jsonFilters: Filter[]): any { const mongoFilters: any = {}; jsonFilters.forEach((filter) => { + if (filter.field === 'operational') { + } // '~' is our symbol for 'contains' // the operational field is a boolean so do not try a partial match on this field - if (filter.operator === '~' && filter.field !== 'operational') { + else if (filter.operator === '~') { // this means we need to do a case insensitive regex match for the value of the field mongoFilters[filter.field] = { $regex: filter.value, $options: 'i' }; + } else if (filter.operator === '=' && filter.field !== 'operational') { + // this means we need to do a case insensitive regex match for the value of the field + mongoFilters[filter.field] = filter.value; } }); diff --git a/udmd/api/src/device/dao/static/StaticDeviceDAO.ts b/udmd/api/src/device/dao/static/StaticDeviceDAO.ts index 4368e5adb..a1dc3a125 100644 --- a/udmd/api/src/device/dao/static/StaticDeviceDAO.ts +++ b/udmd/api/src/device/dao/static/StaticDeviceDAO.ts @@ -33,15 +33,17 @@ export class StaticDeviceDAO implements DeviceDAO { return this.devices.length; } - public async getDevices(searchOptions: SearchOptions): Promise { - if (searchOptions.batchSize == 0) { - throw new Error('A batch size greater than zero must be provided.'); + public async getFilteredDeviceCount(searchOptions: SearchOptions): Promise { + let filteredDevices = this.devices; + if (searchOptions.filter) { + const filters: Filter[] = fromString(searchOptions.filter); + filteredDevices = filterDevices(filters, filteredDevices); } - if (searchOptions.offset > this.devices.length) { - throw new Error('An invalid offset that is greater than the total number of devices was provided.'); - } + return filterDevices.length; + } + public async getDevices(searchOptions: SearchOptions): Promise { let filteredDevices = this.devices; if (searchOptions.filter) { const filters: Filter[] = fromString(searchOptions.filter); diff --git a/udmd/api/src/device/model.ts b/udmd/api/src/device/model.ts index 60ce17214..6dee113bd 100644 --- a/udmd/api/src/device/model.ts +++ b/udmd/api/src/device/model.ts @@ -1,6 +1,7 @@ export interface DevicesResponse { devices: Device[]; totalCount: number; + totalFilteredCount: number; } export interface SearchOptions { diff --git a/udmd/api/src/device/schema.graphql b/udmd/api/src/device/schema.graphql index 11d139cc3..950575801 100644 --- a/udmd/api/src/device/schema.graphql +++ b/udmd/api/src/device/schema.graphql @@ -28,6 +28,8 @@ type DevicesResponse { devices: [Device!]! # the total number of devices in the system totalCount: Int! + # the total number of devices after the filter in the search options has been applied + totalFilteredCount: Int! } type Device { From 5343a86601b351fc78dd739886eed3b73ce27ed6 Mon Sep 17 00:00:00 2001 From: slevert Date: Thu, 10 Mar 2022 11:01:54 -0500 Subject: [PATCH 2/3] fixing docs and removing commented out code --- udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts | 1 - udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts b/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts index 34f6549d5..04a96dff4 100644 --- a/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts +++ b/udmd/api/src/device/dao/mongodb/MongoDeviceDAO.ts @@ -21,7 +21,6 @@ export class MongoDeviceDAO implements DeviceDAO { } async getFilteredDeviceCount(searchOptions: SearchOptions): Promise { - // console.log(await this.db.collection('device').countDocuments()); return await this.db.collection('device').countDocuments(this.getFilter(searchOptions)); } diff --git a/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts b/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts index 2d0a367f3..80cce08e1 100644 --- a/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts +++ b/udmd/api/src/device/dao/mongodb/MongoFilterBuilder.ts @@ -6,14 +6,15 @@ export function getFilter(jsonFilters: Filter[]): any { jsonFilters.forEach((filter) => { if (filter.field === 'operational') { + // do nothing for filtering on the operational boolean yet + // TODO: resolve in a future PR } // '~' is our symbol for 'contains' - // the operational field is a boolean so do not try a partial match on this field else if (filter.operator === '~') { // this means we need to do a case insensitive regex match for the value of the field mongoFilters[filter.field] = { $regex: filter.value, $options: 'i' }; - } else if (filter.operator === '=' && filter.field !== 'operational') { - // this means we need to do a case insensitive regex match for the value of the field + } else if (filter.operator === '=') { + // this means we need to do an exact match for the value of the field mongoFilters[filter.field] = filter.value; } }); From 7559a8fae31e7c6c5215b875986eb6c2bac2a80d Mon Sep 17 00:00:00 2001 From: slevert Date: Thu, 10 Mar 2022 11:04:48 -0500 Subject: [PATCH 3/3] re-enabling restore functionality --- .../device/dao/mongo/MongoDeviceDAO.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts b/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts index d18ed22b0..f4c7ef59f 100644 --- a/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts +++ b/udmd/api/src/__tests__/device/dao/mongo/MongoDeviceDAO.spec.ts @@ -3,13 +3,17 @@ import { MongoDeviceDAO } from '../../../../device/dao/mongodb/MongoDeviceDAO'; import { Device, SearchOptions, SORT_DIRECTION } from '../../../../device/model'; import { createDevices } from '../../data'; +const mockDevices: Device[] = createDevices(100); + +// mongo objects let connection: MongoClient; let db: Db; let deviceCollection: Collection; + +// object under test let mongoDeviceDAO: MongoDeviceDAO; -const mockDevices: Device[] = createDevices(100); -// mocks +// spies let findSpy; let sortSpy; let limitSpy; @@ -26,14 +30,12 @@ afterAll(async () => { }); beforeEach(async () => { - // object under test mongoDeviceDAO = new MongoDeviceDAO(db); // data prep deviceCollection = db.collection('device'); await deviceCollection.insertMany(mockDevices); - // spies findSpy = jest.spyOn(Collection.prototype, 'find'); sortSpy = jest.spyOn(FindCursor.prototype, 'sort'); limitSpy = jest.spyOn(FindCursor.prototype, 'limit'); @@ -43,8 +45,8 @@ beforeEach(async () => { afterEach(async () => { await db.collection('device').deleteMany({}); - // jest.resetAllMocks(); - // jest.restoreAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); }); describe('MongoDeviceDAO.getDevices', () => {